Cutting Edge The ASP.NET View State

类别:VC语言 点击:0 评论:0 推荐:
  The ASP.NET View State  
Dino Esposito

Download the code for this article: CuttingEdge0302.exe (39KB)

I n ASP.NET pages, the view state represents the state of the page when it was last processed on the server. It's used to build a call context and retain values across two successive requests for the same page. By default, the state is persisted on the client using a hidden field added to the page and is restored on the server before the page request is processed. The view state travels back and forth with the page itself, but does not represent or contain any information that's relevant to client-side page display. In this column, I'll take a tour of the view state implementation in ASP.NET and show how to keep your Web pages fit and trim and a bit more secure.

View State Pros and Cons
  View state has pros and cons that you might want to weigh carefully before making a decision to use it. First, view state does not require any server state resources and is simple to implement. View state, though, does require computational effort on the server and the cost of sending the extra data over the wire. Since it's a physical part of the page, it's fast to retrieve and use; for this same reason, though, it can become a weakness that can be exploited.
  Because the view state is packed with the page, it inevitably adds a few extra kilobytes of data to the payload. A complex, real-world page—especially one that does not optimize and restrict the use of the view state—can easily find 10KB of extra stuff packed in the HTML code sent to the browser. Because it's composed of plain text, the view state could be tampered with. Although programmers are not supposed to store sensitive data in the view state (credit card numbers, passwords, or connection strings, for example), it goes without saying that the view state can be used to carry out attacks on the server. View state is not a security hole per se, but just like query strings and other hidden fields you may have used in the past, it's a potential vehicle for malicious code. Since the view state is encoded, protected, and validated, it does provide more security features than other hidden fields that you may use for your own programming purposes.
  View state is one of the most important features of ASP.NET, not so much because of its technical relevance, but more because it makes the magic of the Web Forms model possible. However, if used carelessly, view state can easily become a burden.

The StateBag Class
  StateBag implements the view state and manages the information that ASP.NET pages and embedded controls persist across successive posts of the same page instance. The class works like a dictionary object and implements the IStateManager interface. The Page and the Control base classes expose the view state through the ViewState property. So you can add or remove items from StateBag as you would with any dictionary object:

ViewState["FontSize"] = value;
   You should start writing to the view state only after the Init event is fired for a page request. You can read from the view state during any stage of the page lifecycle, but not after the page enters rendering mode—that is, after the PreRender event is fired. Figure 1 lists the properties and the methods defined for StateBag.
  Each item in the StateBag class is represented by a StateItem object. An instance of the object is implicitly created when you set the Item indexer property with a value or when you call the Add method. Items added to the StateBag object are tracked until the view state is saved to the storage medium using the page's SaveViewState method. If a state item hasn't been saved, IsDirty property will be True. The IsItemDirty method represents an indirect way to call into the IsDirty method of a StateItem object.
  The contents of the StateBag collection are first serialized to a string, then Base64 encoded, and finally assigned to a hidden field in the page that is served to the client. The view state for the page is a cumulative property that results from the contents of the ViewState property of the page plus the view state of all the controls hosted in the page.

Security Issues
  Architecturally speaking, the importance of the view state cannot be denied since it is key to setting up the automatic state management feature of ASP.NET. A couple of hot issues surround its use, however. Most of the frequently asked questions about it are related to security and performance. Let's tackle security first.
  Many developers are hesitant to use view state because it's stored in a hidden field and left on the client where it has the potential for compromise. As Figure 2 shows, the information can be read by viewing the source of the page. While this action alone is not a threat to the application, having the view state on the client is a potential security issue because it is information that all ASP.NET pages contain and transmit to the Web server for processing.

Figure 2 ViewState is Visible in Source

  Although freely accessible in a hidden field called __VIEWSTATE, the view state information is not clear text. By default, a machine-specific authentication code is calculated on the data and appended to the view state string. The resulting text is then Base64 encoded only, but not encrypted. If data confidentiality is desired, however, then SSL is the only solution since it protects not only view state, but all the data that travels to and from the page. Decoding view state is still possible, but a number of steps must be accomplished; not only must several undocumented and internal structures be disassembled, but a number of circumstances must also occur. In addition, consider that a tampered view state is normally detected on the server and a security exception is thrown. Finally, and most important of all, the view state contains data, not code. Unless you explicitly lessen the default security settings for the page, there's not much a hacker can do to modify the view state. If you change the default security settings, though, you should be careful about the view state. A hacker could modify the data that represents the state of the page. This is not a bug per se and opens holes for attacks only if the basic rules of data validation and data checking are not enforced. But this, you understand, is a more general problem when you're trying to write secure code.
  The view state internal implementation is quite complex and layered enough to discourage attacks. Encryption is the most important element in protecting view state information.
  In order to make the view state more secure, the ASP.NET @Page directive supports an attribute called EnableViewStateMac whose only purpose is detecting any possible attempt at corrupting original data. (The "Mac" in EnableViewStateMac stands for machine authentication check and, despite what some documentation claims, it is enabled by default.) When serialized, and if EnableViewStateMac is set to True, the view state is appended with a validator hash string based on the algorithm and the key defined in the <machineKey> section of the machine.config file. By default, the encryption algorithm is SHA1 and the encryption and decryption keys are auto-generated and stored in the Web server machine's Local Security Authority (LSA) subsystem. The LSA is a protected component of Windows NT®, Windows® 2000, and Windows XP. It provides security services and maintains information about all aspects of local security on a system.
  If EnableViewStateMac is True, then when the page posts back the encrypted view state is algorithmically checked to verify that it has not been tampered with on the client. The net effect is that you might be able to read the contents of the view state, but to replace it you need the encryption key, which is in the Web server's LSA.
  The EnableViewStateMac directive attribute corresponds to a protected page member with the same name. The EnableViewStateMac property defaults to True. The following excerpt comes from the source code that the ASP.NET runtime generates for a running .aspx page. (You can obtain it by adding the debug=true attribute to the @Page directive and searching for the source code in the temporary system folder of your ASP.NET application.)

protected override void FrameworkInitialize() { this.__BuildControlTree(this); this.FileDependencies = __fileDependencies; this.EnableViewStateMac = true; } If you're particularly concerned about attacks through the view state, simply make sure that EnableViewStateMac is on.
  You should look carefully at your own view state. If you maintain the default settings you should be OK. Later, after discussing performance issues, I'll return to security. At that time I'll discuss a possible alternative implementation of the view state that addresses both security concerns and bandwidth overhead.

Performance Issues
  Keeping the default view state settings is fine from a security perspective, but not in terms of performance. The view state can reach a considerable size, especially for feature-rich pages that make use of plenty of controls. This extra burden can cause serious overhead for the application as a whole.
  By default, the view state is enabled for all server controls, but this doesn't mean that you need it all the time. View state saves you a lot of coding and, more importantly, makes your job simpler. But if you find you're paying too much for this feature, drop view state altogether and reinitialize the state of the server controls at every postback. In this case, disabling view state saves processing time and speeds up the download process.
  You can disable the view state for an entire page by setting the EnableViewState attribute of the @Page directive to false:

<% @Page EnableViewState="false" %> While this is not usually recommended, you should definitely consider it for read-only pages that either don't post back or don't need state to be maintained.
  A better approach entails disabling the view state only for some of the server controls hosted in the page. To disable view state on a per-control basis, set the EnableViewState property of the control to false, as shown here:

<asp:datagrid runat="server" EnableViewState="false"> ••• </asp:datagrid>
   While developing the page, you can keep the size of the view state under control by enabling tracing on the page. Once you have set the @Page's trace attribute to true, look under the ViewState column of the control tree, as shown in Figure 3.
  The tracer doesn't show the total size of the view state for the page, but it gives you a precise idea of what each control does. In Figure 3, the page is made of a relatively simple DataGrid control. As you can see, the cells of the grid take up a large part of the view state. The TableCell control, in particular, saves complete user interface information in the view state, including text, column and row span, and style attributes.
  Contrary to what many developers think, the DataGrid server control doesn't store its data source in the view state. It caches almost all of its public properties, but the amount of space required is in no way related to the data source. However, the DataGrid uses child controls, like the TableCell, which store their text and style in view state, too. The text displayed in the cells is a subset of the DataGrid's data source.

Figure 4 Retrieve ViewState String

  In Figure 4, I've reworked the page in Figure 3 to add a client-side button that retrieves the view state string and calculates its length. The JavaScript code for this is pretty simple:

<script language="javascript"> function ShowViewStateSize() { var buf = document.forms[0]["__VIEWSTATE"].value; alert("View state is " + buf.length + " bytes"); } </script>

Programming Without View State
  As I've discussed here, the view state represents the state of the page and its controls just before the page is rendered in HTML. When the page posts back, the view state is recovered from the hidden field, deserialized, and used to initialize the server controls in the page and the page itself. However, this is only half the story.
  After loading the view state, the page reads client-side information through the Request object and uses those values to override most of the settings for the server controls. In general, the two operations are neatly separated and take place independently. In particular, though, the second operation—reading from Request.Form—in many situations ends up just overriding the settings read out of the view state. In this particular case the view state is only an extra burden.
  Let's examine a typical case. Suppose you have a page with a textbox server control. You expect that, when the page posts back, the textbox server control is automatically assigned the value set on the client. To meet this rather common requirement, you don't need view state. Let's consider the page in Figure 5. In this case, the behavior of the page is state-aware even if view state is disabled. The reason is that you are using two server controls—TextBox and CheckBox—whose key properties are updated according to the values set by the user. These values will override any setting that view state may have set. In short, as long as you're simply interested in persisting properties such as Text or Checked you don't have any need for view state.
  All control properties declared as attributes in <asp:xxx> tags are automatically restored to their default values during the initialization of the control. As long as these extra properties are not expected to change during the session, you don't need view state. So when is view state really necessary?
  Suppose that your textbox is only editable if the user has certain privileges. You could declare the textbox as read-only to turn the attribute off the first time the page is loaded and after checking the user's credentials. The code would look like Figure 6.
  This programming style is inherently state-aware and works as expected as long as you have view state enabled. Try setting the EnableViewState attribute to false and then load the page again. In the OnInit page event, the textbox is instantiated as declared in the page layout—that is, with a ReadOnly property of true. The first time the page is processed (in this case, IsPostBack returns false), the attribute is programmatically turned on according to the user's role. So far, so good.
  The code in Figure 6 is state-aware because it assumes that after the first access to the page the runtime can remember all settings. This is no longer the case if you turn view state off. In this case, since IsPostBack now returns true, the Page_Load event won't have a chance to modify the textbox's ReadOnly property. By choosing an alternative programming style you can get the same functionality without the view state, thus saving 40 bytes of HTML for each instance of a TextBox control. Obviously, this sort of savings can add up. The code in Figure 7 shows the few changes needed to make the page work the same, but without view state.
  In some cases, you can disable view state and have the page still run unchanged. In other cases, some minor changes must be made to the code to make sure that all properties of all controls are correctly initialized. In general, you can do without view state whenever the state can be deduced either from the client or from the runtime environment (if the user has certain privileges, for example). By contrast, doing without view state is difficult whenever state information can't be dynamically inferred. For example, if you want to track the sort order of a pageable DataGrid you can cache it only in the view state.
  In my experience, any combination of default ASP.NET controls can be adapted to work in pages in which the view state has been disabled. The more you use custom controls—both yours and those from third-parties—the more disabling view state can be a problem. Keep this in mind if you're going to write custom ASP.NET controls, and don't overload the view state bag.

Keeping View State on the Server
  As I said, the view state is stored on the client in a hidden field. However, this is simply the default storage medium. Let's see how to save the view state in a file on the Web server instead.
  To design an alternative storage scheme for view state, you need the string that ASP.NET stores in the hidden field. You can then save it in a server-side file or a database table and read the view state whenever that page is being processed. Be aware that there are benefits and drawbacks to this alternative.
  The bad news is that the Base64 view state string is not publicly exposed to the code running in the page. The good news is that the class that the Microsoft® .NET Framework actually uses to serialize and deserialize the view state is configured as a public type (as opposed to an internal type) and as such can be called from user applications. This class, LosFormatter, is not yet fully documented.
  The LosFormatter class has quite a simple programming interface made of only two publicly callable methods—Serialize and Deserialize. The Serialize method writes the final Base64 representation of the view state to a Stream or a TextWriter object:

public void Serialize(Stream stream, object viewState); public void Serialize(TextWriter output, object viewState); The Deserialize method builds a StateBag object out of a stream, a TextReader object, or a plain Base64 string, as shown here:

public object Deserialize(Stream stream); public object Deserialize(TextReader input); public object Deserialize(string input); The LosFormatter class is the entrypoint in the view state internal mechanism; by using it, everything should happen exactly as in the default case.
  The LosFormatter class also features a read/write property called EnableViewStateMac whose role is enabling encryption/decryption on the Base64 string. The variable cannot be accessed programmatically because it is marked as internal, meaning that only classes in the same assembly can work with it. However, since I'm simply plugging my code into the ASP.NET view state pipeline, I can control the machine authentication check using the high-level tools I discussed earlier.
  Fortunately, the Page class can support alternative storage schemes. The class contains a couple of protected virtual methods that the runtime uses to deserialize or serialize the view state. LoadPageStateFromPersistenceMedium is used to restore the view state at the beginning of the page lifecycle. By contrast, a method named SavePageStateToPersistenceMedium is used to persist the view state just before the page is rendered:

protected virtual void SavePageStateToPersistenceMedium (object viewState); protected virtual object LoadPageStateFromPersistenceMedium(); Override both methods and you can load and save view state information to and from virtually any storage medium other than a hidden field. (Note that you cannot override only one method; you have to override both.) Since the methods are defined as protected members, the only way to redefine them is to create a new class that derives from Page. The following code gives you an idea of the default behavior of the loading method:

string m_viewState = Request.Form["__VIEWSTATE"]; LosFormatter m_formatter = new LosFormatter(); object viewStateBag = m_formatter.Deserialize(m_viewState);
   Notice that the object returned by the LosFormatter's Deserialize method is not the StateBag object that the programmers actually work with. What the Deserialize method returns is just one of the intermediate internal objects that I mentioned earlier. The structure of the page I'm going to create is shown here:

public class ServerViewStatePage : Page { protected override object LoadPageStateFromPersistenceMedium() { ... } protected override void SavePageStateToPersistenceMedium(object viewState) { ... } }
   The tasks accomplished by the SavePageStateToPersistenceMedium method are very easy to understand. The method takes the string as an argument, opens the output stream, and calls into the LosFormatter serializer:

protected override void SavePageStateToPersistenceMedium (object viewStateBag) { string file = GetFileName(); StreamWriter sw = new StreamWriter(file); LosFormatter m_formatter = new LosFormatter(); m_formatter.Serialize(sw, viewStateBag); sw.Close(); return; } In this code snippet the view state is saved in a server-side file. With minimal changes, you could save it to a database as well. How should you choose the name of the file to ensure that no conflicts arise? The view state is specific to a page request made within a particular session. So the session ID and the request URL are unique pieces of information that can be used to associate the request with the right file. Alternatively, you could give the view state file a randomly generated name and persist the file name in a custom hidden field within the page. Note that in this case you should create the hidden field manually and not rely on the __VIEWSTATE field because in overriding the methods you alter the internal procedure that would have created it.
  The GetFileName function in the code I just showed gives the file a name according to the following pattern:

SessionID_URL.viewstate Figure 8 shows some of these files created in a temporary directory. Note that for an ASP.NET application to create a local file you must give the ASP.NET account special permissions on a file or a folder. I suggest you create a new subfolder to contain all the view state files. Deleting files for expired sessions can be a bit tricky; you could write a Windows NT service that would periodically scavenge the temp directory to delete useless files. This would be better than just deleting files from within the Session_OnEnd event.
  The LoadPageStateFromPersistenceMedium method determines the name of the file to read from, extracts the Base64 string, and calls LosFormatter to deserialize (see Figure 9).
  To be able to create the view state on the server, a page only needs to inherit from the ServerViewStatePage class that I described a few code snippets earlier. Here's how it's done:

<% @Page Language="C#" Inherits="MsdnMag.ServerViewStatePage" %>
   Note, though, that session IDs (or any state mechanism) is also subject to sniffing and replay. So some of the same caveats discussed earlier apply here as well. Again, SSL is the best tool for ensuring the integrity of data on the wire. In addition, consider creating files outside the Web application space and give them random names. The actual file name can then be encoded and stored in a hidden field.

  The view state is a key element of an ASP.NET page because it is the primary means to persist the state of the Web server controls. Whenever the page posts back, the state is restored, updated using the current form parameters, then used to run the postback event handler. Normally, the view state is a hashed string encoded as Base64 and stored in a hidden field called __VIEWSTATE. In this way, the view state is not cached on the client, but simply transported back and forth with potential issues both for security and performance. As I said, you could keep the view state on the server, which requires only minimal coding. You will pay extra overhead because you're accessing a file off the disk or a database, but you'll be returning much slimmer pages while taking less of a security risk. Although in this column I considered persistent storage on the server (file or database), nothing would really prevent you from using the Cache object, thus combining server-side storage and speed of execution.

Send your questions and comments for Dino to [email protected].
Dino Esposito is an instructor and consultant based in Rome, Italy. Author of Building Web Solutions with ASP.NET and ADO.NET and Applied XML Programming for .NET, both from Microsoft Press, he spends his time teaching ASP.NET. Get in touch with Dino at [email protected].
From the February 2003 issue of MSDN Magazine.
Get it at your local newsstand, or better yet, subscribe.