In many situations it is acceptable for a page response to be a little stale if this brings significant performance advantages. Want an example? Think of an e-commerce application and its set of pages for the products catalog. These pages are relatively expensive to create because they could require one or more database calls and likely some form of data join. All things considered, a page like this could easily cost you a few million CPU cycles. Why should you regenerate this same page a hundred times per second? Product pages tend to remain the same for weeks and are rarely updated more than once per day. Whatever the refresh rate is for the pages, there's little value in regenerating them on a per-request basis.
A much better strategy is to create the page once, cache it somewhere, and give the page output a maximum duration. Once the cached page becomes stale, the first incoming request will be served in the standard way, running the page's code, and the new page output will be cached for another period until it also becomes stale.
ASP.NET page output caching is the feature that allows you to cache page responses so that following requests can be satisfied without executing the page but instead by simply returning the cached output. Output caching can take place at two levels: entire pages or portions of the page. Page caching is smart enough to let you save distinct output based on the requesting URL, query string or form parameters, and even custom strings.
Easy to set up and terrifically effective, output caching can either be configured declaratively through the @OutputCache directive or programmatically through an API built around the HttpCachePolicy class.
Important | Page output caching is simply a way to have the application serve more pages more quickly. It has nothing to do with sophisticated caching strategies or elegant code design. In other words, it will enable your application to serve pages faster, but it won't necessarily make the application more efficient and scalable. With page output caching, you can certainly reduce the workload on the server as pages are cached downstream. Finally, be aware that page output caching works only for anonymous content. Requests for cached pages are served by Internet Information Server (IIS) 6.0 directly or by the ASP.NET worker process under IIS 5.0. In any case, a page request never reaches stages in the ASP.NET pipeline where it can be authenticated, which is a strategy employed to prevent access to protected content. |
Caching the output of a page is as easy as defining an @OutputCache directive at the top of the page. The directive accepts a handful of attributes, a couple of which Duration and VaryByParam are mandatory. The Duration attribute indicates in seconds how long the system should cache the page output. The VaryByParam attribute allows you to vary the cached output depending on the GET query string or form POST parameters. The following declaration will cache the page for one minute regardless of any GET or POST parameters:
<%@ OutputCache Duration="60" VaryByParam="None" %>
For frequently requested pages and relatively static pages, the @OutputCache directive is a real performance booster. With a shorter duration, even limited to one or two seconds, it provides a way to speed up the entire application.
The @OutputCache directive consists of six attributes that indicate the location of the cache, its duration, and the arguments to use to vary page caching. The list of supported attributes is shown in Table 14-7. Note that the directive can be applied to both pages (.aspx) and user controls (.ascx). Some of the attributes are valid in one case but not the other.
Attribute | Applies to | Description |
---|---|---|
CacheProfile | Page | Associates a page with a group of output caching settings specified in the web.config file (more on this later). Not supported in ASP.NET 1.x. |
Duration | Page, User control | The time, in seconds, that the page or user control is cached. |
Location | Page | Specifies a valid location to store the output of a page. The attribute takes its value from the OutputCacheLocation enumeration. |
NoStore | Page | Indicates whether to send a Cache-Control:no-store header to prevent additional storage of the page output. Not supported in ASP.NET 1.x. |
Shared | User control | Indicates whether the user control output can be shared with multiple pages. False by default. |
SqlDependency | Page, User control | Indicates a dependency on the specified table on a given SQL Server database. Whenever the contents of the table changes, the page output is removed from the cache. Not supported in ASP.NET 1.x. |
VaryByControl | User control | A semicolon-separated list of strings that represent properties of the user control. Each distinct combination of values for the specified properties will originate a distinct copy of the page in the cache. |
VaryByCustom | Page, User control | A semicolon-separated list of strings that lets you maintain distinct cached copies of the page based on the browser type or user-defined strings. |
VaryByHeader | Page | A semicolon-separated list of HTTP headers. |
VaryByParam | Page, User control | A semicolon-separated list of strings representing query string values sent with GET method attributes or parameters sent using the POST method. |
Note that the VaryByParam attribute is mandatory. If you omit it, a run-time exception is always thrown. However, if you don't need to vary by parameters, set the attribute to None. The empty string is not an acceptable value for the VaryByParam attribute.
When the output caching service is active on a page, the Duration attribute indicates the number of seconds that the caching system will maintain an HTML-compiled version of the page. Next, requests for the same page, or for an existing parameterized version of the page, will be serviced while bypassing most of the ASP.NET pipeline. As mentioned, this process has two important repercussions no authentication is possible and no code is run, meaning that no page events are fired and handled and no state is updated. The implementation of output caching varies with the ASP.NET process model in use. (See Chapter 3 for details on the ASP.NET process models.)
With the IIS 5.0 process model, any request for an ASP.NET page is always handed over to the worker process, assigned to an HttpApplication object, and processed by the pipeline. The ASP.NET pipeline includes an HTTP module named OutputCacheModule that captures two application-level events related to output caching ResolveRequestCache and UpdateRequestCache. In particular, the module uses the ResolveRequestCache event handler to short-circuit the processing of requests for pages that have been cached. In the end, the request is hooked by the HTTP module and served by returning the copy of the page stored in the cache. When the page is being generated, OutputCacheModule grabs the output of the pages marked with the @OutputCache directive and stores it internally for further use. The output of the page is stored in a private slot of the ASP.NET Cache object. Setting the Duration attribute on a page sets an expiration policy for the HTTP response generated by the ASP.NET runtime. The output is cached by the module for exactly the specified number of seconds. In the meantime, all the incoming requests that hit one of the cached pages are serviced by the module rather than by the ASP.NET pipeline.
With the IIS 6.0 process model, the output caching mechanism is integrated in the Web server, resulting in much better performance and responsiveness, thanks to the IIS 6.0 kernel caching. When enabled, this feature makes it possible for IIS to intercept the output of a page generated by ASP.NET. A copy of the page output is then cached by the IIS kernel. Incoming requests for an ASP.NET page are filtered by a kernel-level driver (http.sys) and examined to see whether they match cached pages. If so, the output is served to callers directly from kernel-level code without even bothering the worker process and the ASP.NET pipeline. If you have any ASP.NET applications and are still considering an upgrade to IIS 6.0, this is a great reason to do it as soon as possible. Note that this facility in IIS 6.0 is used by ASP.NET since version 1.1 to host the output cache. So, when using the output cache directive in ASP.NET 1.1 and ASP.NET 2.0 applications, your responses are being served from the kernel cache. See the sidebar "Inside IIS 6.0 Kernel Caching" for more performance details.
A fair value for the Duration attribute depends on the application, but it normally doesn't exceed 60 seconds. This value usually works great, especially if the page doesn't need to be updated frequently. A short duration (say, 1 second) can be useful for applications that claim live data all the time.
IIS 6.0 employs an ad hoc component to cache the dynamically generated response to a request in the kernel. This feature has tremendous potential and can dramatically improve the performance of a Web server, as long as enough of the content being served is cacheable. What's the performance gain you can get?
According to the numbers provided with the IIS 6.0 technical documentation, an application using the kernel cache returns a throughput of more than 10 times the through-put you would get in the non-cached case. Additionally, the latency of responses is dramatically better. The following table compares caching in IIS 6.0 kernel mode and user-mode caching as implemented by the ASP.NET runtime in IIS 5.0.
User Mode | Kernel Mode | |
---|---|---|
Requests / Sec | 1,394 | 15,416 |
TTFB / TTLB (msec) | 70.82 / 70.97 | 3.39 / 4.02 |
User Mode CPU % | 76.57% | 0.78% |
Kernel Mode CPU % | 22.69% | 99.22% |
System Calls / Sec | 20,110 | 2,101 |
Network Util (KB / Sec) | 6,153 | 68,326 |
Context Switches / Sec | 2,621 | 6,261 |
Source: http://www.microsoft.com/technet/prodtechnol/windowsserver2003/technologies/webapp/iis/iis6perf.mspx |
The numbers in the preceding table provide you with an idea of the results, but the results will vary according to the amount of work and size of the response. The bottom line, though, is that leveraging the kernel cache can make a dramatic difference in the performance of an application. The great news for ASP.NET developers is that no code changes are required to benefit from kernel caching, except for the @OutputCache directive.
On a high-volume Web site, an output cache duration of only one second can make a significant difference for the overall throughput of a Web server. There's more to know about kernel caching, though. First and foremost, kernel caching is available only for pages requested through a GET verb. No kernel caching is possible on postbacks. Furthermore, pages with VaryByParam and VaryByHeader attributes set are also not stored in the kernel cache. Finally, note that ASP.NET Request/Cache performance counters will not be updated for pages served by the kernel cache.
The output cache can be located in various places, either on the client that originated the request or on the server. It can also be located on an intermediate proxy server. The various options are listed in Table 14-8. They come from the OutputCacheLocation enumerated type.
Location | Cache-Control | Description |
---|---|---|
Any | Public | The HTTP header Expires is set according to the duration set in the @OutputCache directive. A new item is placed in the ASP.NET Cache object representing the output of the page. |
Client | Private | The output cache is located on the browser where the request originated. The HTTP header Expires is set according to the duration set in the @OutputCache directive. No item is created in the ASP.NET Cache object. |
DownStream | Public | The output cache can be stored in any HTTP cache-capable devices other than the origin server. This includes proxy servers and the client that made the request. The HTTP header Expires is set according to the duration set in the @OutputCache directive. No item is created in the ASP.NET Cache object. |
None | No-Cache | The HTTP header Expires is not defined. The Pragma header is set to No-Cache. No item is created in the ASP.NET Cache object. |
Server | No-Cache | The HTTP header Expires is not defined. The Pragma header is set to No-Cache. A new item is placed in the ASP.NET Cache object to represent the output of the page. |
A page marked with the @OutputCache directive also generates a set of HTTP headers, such as Expires and Cache-Control. Downstream proxy servers such as Microsoft ISA Server understand these headers and cache the page along the way. In this way, for the duration of the output, requests for the page can be satisfied even without reaching the native Web server.
In particular, the Expires HTTP header is used to specify the time when a particular page on the server should be updated. Until that time, any new request the browser receives for the resource is served using the local, client-side cache and no server roundtrip is ever made. When specified and not set to No-Cache, the Cache-Control HTTP header typically takes values such as public or private. A value of public means that both the browser and the proxy servers can cache the page. A value of private prevents proxy servers from caching the page; only the browser will cache the page. The Cache-Control is part of the HTTP 1.1 specification and is supported only by Microsoft Internet Explorer 5.5 and higher.
If you look at the HTTP headers generated by ASP.NET when output caching is enabled, you'll notice that sometimes the Pragma header is used in particular, when the location is set to Server. In this case, the header is assigned a value of No-Cache, meaning that client-side caching is totally disabled both on the browser side and the proxy side. As a result, any access to the page is resolved through a connection.
Note | To be precise, the Pragma header set to No-Cache disables caching only over HTTPS channels. If used over nonsecure channels, the page is actually cached but marked as expired. |
Let's examine the client and Web server caching configuration when each of the feasible locations is used.
Any. This is the default option. This setting means that the page can be cached everywhere, including in the browser, the server, and any proxies along the way. The Expires header is set to the page's absolute expiration time as determined by the Duration attribute; the Cache-Control header is set to public, meaning that the proxies can cache if they want or need to. On the Web server, a new item is placed in the Cache object with the HTML output of the page. In summary, with this option the page output is cached everywhere. As a result, if the page is accessed through the browser before it expires, no roundtrip is ever made. If, in the same timeframe, the page is refreshed meaning that server-side access is made anyway the overhead is minimal, the request is short-circuited by the output cache module, and no full request processing takes place.
Client. The page is cached only by the browser because the Cache-Control header is set to private. Neither proxies nor ASP.NET stores a copy of it. The Expires header is set according to the value of the Duration attribute.
DownStream. The page can be cached both on the client and in the memory of any intermediate proxy. The Expires header is set according to the value of the Duration attribute, and no copy of the page output is maintained by ASP.NET.
None. Page output caching is disabled both on the server and on the client. No Expires HTTP header is generated, and both the Cache-Control and the Pragma headers are set to No-Cache.
Server. The page output is exclusively cached on the server, and its raw response is stored in the Cache object. The client-side caching is disabled. No Expires header is created, and both the Cache-Control and Pragma headers are set to No-Cache.
ServerAndClient. The output cache can be stored only at the origin server or at the requesting client. Proxy servers are not allowed to cache the response.
The SqlDependency attribute is the @OutputCache directive's interface to the SqlCacheDependency class that we discussed earlier. When the SqlDependency attribute is set to a Database:Table string, a SQL Server cache-dependency object is created. When the dependency is broken, the page output is invalidated and the next request will be served by pushing the request through the pipeline as usual. The output generated will be cached again:
<% @OutputCache Duration="15" VaryByParam="none" SqlDependency="Northwind:Employees" %>
A page that contains this code snippet has its output cached for 15 seconds or until a record changes in the Employees table in the Northwind database. Note that the Northwind string here is not the name of a database it's the name of an entry in the <databases> section of the configuration file. That entry contains detailed information about the connection string to use to reach the database. You can specify multiple dependencies by separating multiple Database:Table pairs with a semicolon in the value of the SqlDependency attribute.
Note | A user control is made cacheable in either of two ways: by using the @OutputCache directive or by defining the PartialCaching attribute on the user control's class declaration in the code-behind file, as follows: [PartialCaching(60)] public partial class CustomersGrid : UserControl { ... } The PartialCaching attribute allows you to specify the duration and values for the VaryByParam, VaryByControl, and VaryByCustom parameters. |
The HttpCachePolicy class is a programming interface alternative to using the @OutputCache directive. It provides direct methods to set cache-related HTTP headers, which you could also control to some extent by using the members of the HttpResponse object.
Table 14-9 shows the properties of the HttpCachePolicy class.
Property | Description |
---|---|
VaryByHeaders | Gets an object of type HttpCacheVaryByHeaders, representing the list of all HTTP headers that will be used to vary cache output |
VaryByParams | Gets an object of type HttpCacheVaryByParams, representing the list of parameters received by a GET or POST request that affect caching |
When a cached page has several vary-by headers or parameters, a separate version of the page is available for each HTTP header type or parameter name.
Table 14-10 shows the methods of the HttpCachePolicy class.
Method | Description |
---|---|
AddValidationCallback | Registers a callback function to be used to validate the page output in the server cache before returning it. |
AppendCacheExtension | Appends the specified text to the Cache-Control HTTP header. The existing text is not overwritten. |
SetAllowResponseInBrowserHistory | When true, the response is available in the browser's History cache, regardless of the HttpCacheability option set on the server. |
SetCacheability | Sets the Cache-Control HTTP header to any of the values taken from the HttpCacheability enumeration type. |
SetETag | Sets the ETag header to the specified string. The ETag header is a unique identifier for a specific version of a document. |
SetETagFromFileDependencies | Sets the ETag header to a string built by combining and then hashing the last modified date of all the files upon which the page is dependent. |
SetExpires | Sets the Expires header to an absolute date and time. |
SetLastModified | Sets the Last-Modified HTTP header to a particular date and time. |
SetLastModifiedFromFileDependencies | Sets the Last-Modified HTTP header to the most recent timestamps of the files upon which the page is dependent. |
SetMaxAge | Sets the max-age attribute on the Cache-Control header to the specified value. The sliding period cannot exceed one year. |
SetNoServerCaching | Disables server output caching for the current response. |
SetNoStore | Sets the Cache-Control: no-store directive. |
SetNoTransforms | Sets the Cache-Control: no-transforms directive. |
SetOmitVaryStar | If true, causes HttpCachePolicy to ignore the * value in VaryByHeaders. Not supported by ASP.NET 1.x. |
SetProxyMaxAge | Sets the Cache-Control: s-maxage header. |
SetRevalidation | Sets the Cache-Control header to either must-revalidate or proxy-revalidate. |
SetSlidingExpiration | Sets cache expiration to sliding. When cache expiration is set to sliding, the Cache-Control header is renewed at each response. |
SetValidUntilExpires | Specifies whether the ASP.NET cache should ignore HTTP Cache-Control headers sent by some browsers to evict a page from the cache. If set to true, the page stays in the cache until it expires. |
SetVaryByCustom | Sets the Vary HTTP header to the specified text string. |
Most methods of the HttpCachePolicy class let you control the values of some HTTP headers that relate to the browser cache. The AddValidationCallback method, on the other hand, provides a mechanism to programmatically check the validity of page output in the server cache before it is returned from the cache.
Before the response is served from the ASP.NET cache, all registered handlers are given a chance to verify the validity of the cached page. If at least one handler marks the cached page as invalid, the entry is removed from the cache and the request is served as if it were never cached. The signature of the callback function looks like this:
public delegate void HttpCacheValidateHandler( HttpContext context, object data, ref HttpValidationStatus validationStatus );
The first argument denotes the context of the current request, whereas the second argument is any user-defined data the application needs to pass to the handler. Finally, the third argument is a reference to a value from the HttpValidationStatus enumeration. The callback sets this value to indicate the result of the validation. Acceptable values are IgnoreThisRequest, Invalid, and Valid. In the case of IgnoreThisRequest, the cached resource is not invalidated but the request is served as if no response was ever cached. If the return value is Invalid, the cached page is not used and gets invalidated. Finally, if the return value is Valid, the cached response is used to serve the request.
Depending on the application context from which a certain page is invoked, the page might generate different results. The same page can be called to operate with different parameters, can be configured using different HTTP headers, can produce different output based on the requesting browser, and so forth.
ASP.NET allows you to cache multiple versions of a page response; you can distinguish versions by GET and POST parameters, HTTP headers, browser type, custom strings, and control properties.
To vary output caching by parameters, you can use either the VaryByParam attribute of the @OutputCache directive or the VaryByParams property on the HttpCachePolicy class. If you proceed declaratively, use the following syntax:
<% @OutputCache Duration="60" VaryByParam="employeeID" %>
Note that the VaryByParam attribute is mandatory; if you don't want to specify a parameter to vary cached content, set the value to None. If you want to vary the output by all parameters, set the attribute to *. When the VaryByParam attribute is set to multiple parameters, the output cache contains a different version of the requested document for each specified parameter. Multiple parameters are separated by a semicolon. Valid parameters to use are items specified on the GET query string or parameters set in the body of a POST command.
If you want to use the HttpCachePolicy class to define caching parameters, first set the expiration and the cacheability of the page using the SetExpires and SetCacheability methods. Next, set the VaryByParams property as shown here:
Response.Cache.SetExpires(DateTime.Now.AddSeconds(60)); Response.Cache.SetCacheability(HttpCacheability.Public); Response.Cache.VaryByParams["employeeid;lastname"] = true;
This code snippet shows how to vary page output based on the employee ID and the last name properties. Note that the Cache property on the HttpResponse class is just an instance of the HttpCachePolicy type.
Most ASP.NET pages do postbacks. The page in Figure 14-7 (sqldepoutputcache.aspx) is no exception. The page has two key features: it is dependent on changes to the Customers table in the Northwind database, and it has a cache duration of 30 seconds. Furthermore, the drop-down list (named Countries) has auto-postback functionality and places a POST request for the same page whenever you change the selection.
Figure 14-7: To properly cache pages that post back, you need to vary them by one or more parameters.
With VaryByParam set to None, you'll wait 30 seconds (or whatever the cache duration is) to have your country selection processed. It is a bit frustrating: no matter which selection you make, it is blissfully ignored and the same page is displayed. Worse yet, if you test the page under the Visual Studio .NET Web Development server, after a couple of attempts you get a "page not found" error. If you test the page under IIS, you are repeatedly served the same page response, regardless of the selection made.
Two points clearly emerge from this discussion. First, pages with static content are a better fit for caching than interactive pages. Second, the postback mechanism returns a bunch of form parameters. You need to vary the cached copies of the page by the most critical of them. The sample page in Figure 14-7 has a few hidden fields (try snooping in its HTML source), such as __VIEWSTATE and __LASTFOCUS, and the drop-down list. Varying by view state makes no sense at all, but varying by the selected countries is exactly what we need:
<%@ OutputCache VaryByParam="Countries" Duration="30" SqlDependency="Northwind:Customers" %>
The directive stores each country-specific page for 30 seconds unless a change occurs in the Customers database. In such a case, all the cached versions of the page will be invalidated.
The bottom line is that enabling page output caching might not be painless for interactive pages. It is free of pain and free of charge for relatively static pages like those describing a product, a customer, or some news.
Caution | A cached ASP.NET page is served more quickly than a processed page, but not as quickly as a static HTML page. However, the response time is nearly identical if the ASP.NET page is kernel-cached in IIS 6.0. Unfortunately, IIS 6.0 doesn't store in its kernel-level cache ASP.NET pages requested via a POST verb and, more importantly, pages with VaryByParam or VaryByHeader. In the end, postback pages have very few chances to be cached in the IIS kernel. They are cached in the ASP.NET Cache, in downstream caching servers, or both. |
The VaryByHeader attribute and the HttpCachePolicy's VaryByHeaders property allow you to cache multiple versions of a page, according to the value of one or more HTTP headers that you specify.
If you want to cache pages by multiple headers, include a semicolon-delimited list of header names. If you want to cache a different version of the page for each different header value, set the VaryByHeader attribute to an asterisk *. For example, the following declaration caches for one-minute pages based on the language accepted by the browser. Each language will have a different cached copy of the page output.
<%@ OutputCache Duration="60" VaryByParam="None" VaryByHeader="Accept-Language" %>
If you opt for a programmatic approach, here's the code to use that leverages the VaryByHeaders property of the HttpCachePolicy class:
Response.Cache.VaryByHeaders["Accept-Language"] = true;
If you want to programmatically vary the pages in the cache by all HTTP header names, use the VaryByUnspecifiedParameters method of the HttpCacheVaryByHeaders class:
HttpCacheVaryByHeaders.VaryByUnspecifiedParameters();
The preceding code is equivalent to using the asterisk with the VaryByHeader attribute.
The VaryByCustom attribute in the @OutputCache directive allows you to vary the versions of page output by the value of a custom string. The string you assign to the VaryByCustom attribute simply represents the description of the algorithm employed to vary page outputs. The string is then passed to the GetVaryByCustomString method, if any, in the global.asax file. The method takes the string and returns another string that is specific to the request. Let's examine a concrete example varying pages by the type of device that requests the page. You use, say, the string device with the VaryByCustom attribute:
<%@ OutputCache Duration="60" VaryByParam="None" VaryByCustom="device" %>
Next, you add your application-specific GetVaryByCustomString method in the global.asax file. Here's a possible implementation:
string GetVaryByCustomString(HttpContext context, string custom) { if (custom == "device") return context.Request.Browser.Type; return null; }
The output of the page is varied by user agent string. You can use any other custom information as long as the information is available through the HttpContext class. You can't use information that is known only when the page is loaded, such as the theme. Custom information gathered by a custom HTTP module might be used if the HTTP module parks the information in the Items collection of the HttpContext object, and as long as the HTTP module is triggered before the request to resolve the page output cache is made.
Nicely enough, the feature described above varying pages by user agent strings is natively available since ASP.NET 1.0. The only difference is that it uses the keyword browser instead of device. In other words, the following code is perfectly acceptable and leverages the implementation of GetVaryByCustomString on the base HttpApplication class:
<%@ OutputCache Duration="60" VaryByParam="None" VaryByCustom="browser" %>
You use the SetVaryByCustom method on the HttpCachePolicy class if you don't like the declarative approach:
Response.Cache.SetVaryByCustom("browser");
The capability of caching the output of Web pages adds a lot of power to your programming arsenal, but sometimes caching the entire content of a page is not possible or it's just impractical. Some pages, in fact, are made of pieces with radically different features as far as cacheability is concerned. In these situations, being able to cache portions of a page is an incredible added value.
If caching the entire page is impractical, you can always opt for partial caching. Partial caching leverages the concept of ASP.NET user controls that is, small, nested pages that inherit several features of the page. In particular, user controls can be cached individually based on the browser, GET and POST parameters, and the value of a particular set of properties.
I cover user controls in detail in my other recent book, Programming Microsoft ASP.NET 2.0 Applications: Advanced Topics (Microsoft Press, 2005), but a quick introduction is in order for the purpose of partial caching.
A user control is a Web form saved to a distinct file with an .ascx extension. The similarity between user controls and pages is not coincidental. You create a user control in much the same way you create a Web form, and a user control is made of any combination of server and client controls sewn together with server and client script code. Once created, the user control can be inserted in an ASP.NET page like any other server control. ASP.NET pages see the user control as an atomic, encapsulated component and work with it as with any other built-in Web control.
The internal content of the user control is hidden to the host page. However, the user control can define a public programming interface and filter access to its constituent controls via properties, methods, and events.
User controls and pages have so much in common that transforming a page, or a part of it, into a user control is no big deal. You copy the portion of the page of interest to a new .ascx file and make sure the user control does not contain any of the following tags: <html>, <body>, or <form>. You complete the work by associating a code-behind file (or a <script runat="server"> block) to code the expected behavior of the user control. Finally, you add an @Control directive in lieu of the @Page directive. Here's an example of a user control:
<%@ Control Language="C#" CodeFile="Message.ascx.cs" Inherits="Message" %> <span style="color:<%= ForeColor%>"><%= Text%></span>
Here's the related code-behind class:
public partial class Message : System.Web.UI.UserControl { public string ForeColor; public string Text; }
To insert a user control into a ASP.NET page, you drag it from the project onto the Web form when in design mode. Visual Studio .NET registers the user control with the page and prepares the environment for you to start adding code:
<%@ Page Language="C#" CodeFile="Test.aspx.cs" Inherits="TestUserCtl" %> <%@ Register src="/books/2/370/1/html/2/Message.ascx" TagName="Message" TagPrefix="uc1" %> <html><body> <form runat="server"> <uc1:Message runat="server" /> </form> </body></html>
In the page code-behind class, you work the Message1 variable as you would with any other server control:
protected void Page_Load(object sender, EventArgs e) { Message1.ForeColor = "blue"; Message1.Text = "Hello world"; }
User controls are not only good at modularizing your user interface, but they're also great at caching portions of Web pages. User controls, therefore, fully support the @OutputCache directive, although they do so with some differences with ASP.NET pages, as outlined in Table 14-7.
A page that contains some dynamic sections cannot be cached entirely. What if the page also contains sections that are both heavy to compute and seldom updated? In this case, you move static contents to one or more user controls and use the user control's @OutputCache directive to set up output caching.
To make a user control cacheable, you declare the @OutputCache attribute using almost the same set of attributes we discussed earlier for pages. For example, the following code snippet caches the output of the control that embeds it for one minute:
<% @OutputCache Duration="60" VaryByParam="None" %>
The Location attribute is not supported because all controls in the page share the same location. So if you need to specify the cache location, do that at the page level and it will work for all embedded controls. The same holds true for the VaryByHeader attribute.
The output of a user control can vary by custom strings and form parameters. More often, though, you'll want to vary the output of a control by property values. In this case, use the new VaryByControl attribute.
The VaryByControl attribute allows you to vary the cache for each specified control property. For user controls, the property is mandatory unless the VaryByParam attribute has been specified. You can indicate both VaryByParam and VaryByControl, but at least one of them is required.
The following user control displays a grid with all the customers in a given country. The country is specified by the user control's Country property.
<%@ Control Language="C#" CodeFile="CustomersGrid.ascx.cs" Inherits="CustomersGridByCtl" %> <%@ OutputCache Duration="30" VaryByControl="Country" %> <h3><%= DateTime.Now.ToString() %></h3> <asp:ObjectDataSource runat="server" TypeName="ProAspNet20.DAL.Customers" SelectMethod="LoadByCountry"> </asp:ObjectDataSource> <asp:GridView runat="server" AutoGenerateColumns="false"> <Columns> <asp:BoundField DataField="ID" HeaderText="ID" /> <asp:BoundField DataField="CompanyName" HeaderText="Company" /> <asp:BoundField DataField="ContactName" HeaderText="Contact" /> <asp:BoundField DataField="Country" HeaderText="Country" /> </Columns> </asp:GridView>
Here is the code file of the user control:
public partial class CustomersGridByCtl : System.Web.UI.UserControl { public string Country; protected void Page_Load(object sender, EventArgs e) { if (!String.IsNullOrEmpty(Country)) { ObjectDataSource1.SelectParameters.Add("country", Country); GridView1.DataSourceID = "ObjectDataSource1"; } } }
The @OutputCache directive caches a different copy of the user control output based on the different values of the Country property. Figure 14-8 shows it in action.
Figure 14-8: Two pages created in different moments use the same user control output, as you can see from the creation time of the grid.
The strings you assign to VaryByControl can be properties of the user controls as well as ID property values for contained controls. In this case, you'll get a distinct cached copy for each distinct combination of property values on the specified control.
In Figure 14-8, you see two instances of the same page sharing the cached output of a user control. Try the following simple experiment. Make a plain copy of the page (say, page1.aspx) and give it another name (say, page2.aspx). You should have two distinct pages that generate identical output. In particular, both pages contain an instance of the same cacheable user control. Let's say that the cache duration of the user control is 30 seconds.
As the next step of the experiment, you open both pages at different times while the cache is still valid. Let's say you open the second page ten seconds later than the first. Interestingly enough, the two pages are no longer sharing the same copy of user control output, as Figure 14-9 documents.
Figure 14-9: Distinct pages don't automatically share the output of the same user control.
By default, distinct pages don't share the output of the same cacheable user control. Each page will maintain its own copy of the user control response instead. Implemented to guarantee total separation and avoid any sort of conflicts, this feature is far more dangerous than one might think at first. It might lead to flooding the Web server memory with copies and copies of the user control responses one for each varying parameter or control property and one set for each page that uses the control.
To allow distinct pages to share the same output of a common user control, you need to set the Shared attribute to true in the user control's @OutputCache directive:
<%@ OutputCache Duration="30" VaryByParam="None" Shared="true" %>
Tip | To avoid memory problems, you should put a limit on the total amount of memory available to IIS. It is set to 60 percent of the physical memory, but you should keep it under 800 MB per Microsoft recommendations. Setting the IIS 6.0 Maximum Used Memory parameter is important especially if output cache is used aggressively. You'll set the parameter on a per-application-pool basis by selecting the IIS 6.0 application pool where your application is configured to run and opening its Properties dialog box. |
If you plan to cache user controls that is, you're trying for partial caching it's probably because you just don't want to, or cannot, cache the entire page. However, a good question to ask is: What happens if user controls are cached within a cacheable page?
Both the page and the controls are cached individually, meaning that both the page's raw response and the control's raw responses are cached. However, if the cache duration is different, the page duration wins and user controls are refreshed only when the page is refreshed.
A cacheable user control can be embedded both in a cacheable page and in a wrapper cacheable user control.
Warning | Cacheable user controls should be handled with extreme care in the page's code. Unlike regular controls, a user control marked with the @OutputCache directive is not guaranteed to be there when your code tries to access it. If the user control is retrieved from the cache, the property that references it in the code-behind page class is just null. if (CustomerGrid1 != null) CustomerGrid1.Country = "USA"; To avoid bad surprises, you should always check the control reference against the null value before executing any code. |
The output caching subsystem has a few new features to offer in ASP.NET 2.0. They are caching profiles and post-cache substitution. In brief, caching profiles let you save a block of output caching-related settings to the configuration file. Post-cache substitution completes the ASP.NET offering as far as output caching is concerned. In addition to saving the entire page or only fragments of the page, you can now also cache the entire page except for a few regions.
The @OutputCache directive for pages supports the CacheProfile string attribute, which references an entry under the <outputCacheProfiles> section in the web.config file:
<caching> <outputCacheSettings> <outputCacheProfiles> <add name="..." enabled="true|false" duration="..." location="..." sqlDependency="..." varyByCustom="..." varyByControl="..." varyByHeader="..." varyByParam="..." noStore=true|false" /> </outputCacheProfiles> </outputCacheSettings> </caching>
Basically, by defining a named entry in the <add> section you can store in the configuration file all the cache-related settings to be applied to the page. Instead of specifying the same set of parameters for each page over and over again, you can put them in the web.config file and reference them by name. In this way, you can also modify settings for a number of pages without touching the source files:
<%@ OutputCache CacheProfile="MySettings" %>
In the preceding code, the application has a MySettings entry in the <outputCacheProfiles> section and doesn't need any additional attribute in the @OutputCache directive. As you can see, the attributes of the <add> node match the attributes of the @OutputCache directive.
With user controls, you can cache only certain portions of ASP.NET pages. With post-cache substitution, you can cache the whole page except specific regions. For example, using this mechanism, an AdRotator control can serve a different advertisement on each request even if the host page is cached.
To use post-cache substitution, you place a new control the <asp:substitution> control at the page location where content should be substituted, and set the MethodName property of the control to a callback method. Here's a quick example:
<form runat="server"> <h3>The output you see has been generated at: <%=DateTime.Now.ToString() %> and is valid for 30 seconds</h3> <hr /> This content is updated regularly <h2><asp:Substitution runat="server" MethodName="WriteTimeStamp" /></h2> <hr /> This is more static and cached content <asp:Button runat="server" Text="Refresh" /> </form>
Figure 14-10 shows the page in action.
Figure 14-10: The fragment of the page between the two horizontal lines is updated regularly at each postback; the rest of the page is retrieved from the cache.
The MethodName property must be set to the name of a static method that can be encapsulated in an HttpResponseSubstitutionCallback delegate, as follows:
public static string WriteTimeStamp(HttpContext context) { return DateTime.Now.ToString(); }
Whatever string the method returns will be rendered out and becomes the output of the Substitution control. Note also that the callback method must be static and thread-safe. The HttpContext parameter to the method may be used to retrieve request parameters such as query string variables, authentication information, or personalization details.
You can also set up post-cache substitution programmatically through the WriteSubstitution method of the HttpResponse object:
Response.WriteSubstitution( new HttpResponseSubstitutionCallback(WriteTimeStamp));
The preceding call inserts a sort of placeholder in the response, which will be replaced with the output of the method. This trick allows the AdRotator control in ASP.NET 2.0 to automatically display a new banner even on cached pages.
The use of post-cache substitution automatically enables server caching for the page output. If the page is configured for client output caching, the setting is ignored. The reason for this change lies in the fact that markup editing is necessarily a server-side operation. In addition, a page that makes use of post-cache substitution can't rely on IIS 6.0 kernel mode caching because ASP.NET needs to do some work before the page can be served to the user. In light of this, the page can't just be served by IIS without first involving ASP.NET.
Note | The Substitution control can also be used in pages that don't use output caching. In this case, the substitution callback will be called at rendering time to contribute to the response. You can think of the Substitution control as a server-side control that has the capability of expanding a placeholder to some server-side processed results. |
For performance reasons, you should also avoid calling the Substitution control from within the callback. If you do so, the callback will maintain a reference to the control and the page containing the control. As a result, the page instance won't be garbage-collected until the cached contents expires.
Note | During the development cycle and until the Beta 2 release disk-based, ASP.NET 2.0 supported a feature known as disk-output caching. In brief, disk output caching was designed to let you store the response of pages on disk as opposed to keeping them in memory and to allow the output cache content to persist across application restarts that always clear the in-memory cache. In the final release of ASP.NET 2.0, though, this feature was removed. |