Working with the Page
Overview
Whatever you do will be insignificant, but it is very important that you do it.
-Mahatma Gandhi
Although formless pages are still accepted and correctly handled, the typical ASP.NET page is a single form page. The HtmlForm class defines the behavior of the server-side form when the runat attribute is set to server. The HtmlForm class acts as a container of server controls and wraps them in an HTML
element when the page is rendered. The obtained HTML form always posts to the same page and for this reason is said to be reentrant. The default method used to submit form data is POST but GET can be used as well.
In most cases, the server form is the outermost tag and is contained directly in
. In general, though, the server tag can be the child of any other server container control such as ,
,
, and any other HTML generic control. (We covered HTML controls and Web controls in Chapter 3.) If any noncontainer controls (for example, a TextBox) are placed outside the form tag, an exception is thrown. Note, though, that no check is made at compile time. The exception is raised by the control itself when the host page asks to render. Noncontainer Web controls, in fact, check whether they are being rendered within the boundaries of a server form and throw an HttpException if they are not. A call to the Page's VerifyRenderingInServerForm method does the job.
In this chapter, we'll examine some aspects of form-based programming in ASP.NET, including how to use multiple forms in the same page. We consider the use of pop-up windows and show how to inject script code in the body of the client page. We'll also consider the styling of the page and how a made-to-measure API, such as the ASP.NET globalization and localization classes, can in some situations help to easily and effectively change the look and feel of the page. When designing a Web site or any application to be consumed over the Web, you can't realistically pretend to keep it error-free! For this reason, debugging, tracing, and effective and user-friendly error handling are key aspects for top-notch ASP.NET developers.
Programming with Forms
One of the most common snags ASP developers face when they first approach the ASP.NET lifestyle is the fact that managed Web applications support the single-form interface (SFI) model.
Note |
If you've never heard anyone use the SFI acronym, there's no reason for you to panic. It's an acronym I've created to mimic other more popular acronyms that, although used in different contexts, describe similar programming models—the single-document interface (SDI) and its opposite, the multiple-document interface (MDI). |
In the SFI model, each page always posts to itself and does so through the POST or GET HTTP method. The HTTP method and the target frame of the post can be programmatically adjusted using ad hoc HtmlForm properties—Method and Target. The final destination of the postback can't be set in any way. What in HTML and ASP programming was the Action property of the form is simply not defined on the ASP.NET HtmlForm class. As a result, the SFI model is a built-in feature so much integrated with the ASP.NET platform that you can only take it or code the old ASP way without server forms.
Technically speaking, a kind of multiform interface (MFI) is still possible. Hold on, though. By MFI here, I simply mean the possibility of having multiple forms on the final HTML page. In no way does this mean you can have multiple server-side forms in an ASP.NET page. An ASP.NET page can host exactly one server form and as many HTML forms as needed. If you place two or more server forms in a page, you won't get any error at compile time. When processing the request, the Page-derived object examines markup blocks one at a time. The first block is processed and rendered. After that, the page object sets an internal flag to remember that one form object has been processed for the page. Guess what happens next? When the second block is found, the page verifies that no other similar block has been processed earlier. If the flag is set, an HttpException is thrown. Later in this chapter, we'll discuss an example in which multiple forms—one server and one client—work together.
Multiple forms, though, are sometimes needed—although probably not frequently. You would need them, for example, for functionality hosted by the application pages, such as search or login capabilities.
The HtmlForm Class
The HtmlForm class inherits from HtmlContainerControl and implements the IAttributeAccessor interface. The base class provides HtmlForm with the capability of containing child controls. This capability is shared with other HTML control classes, such as HtmlTable, characterized by child elements and a closing tag. The IAttributeAccessor interface defines two methods—GetAttribute and SetAttribute—used to read attribute values out of the opening tag.
Properties of the HtmlForm Class
The HtmlForm class provides programmatic access to the HTML element on the server through the set of properties shown in Table 4-1.
As you can see, the second and third
tags are not marked runat, meaning that ASP.NET will treat them as plain text to output verbatim. When you click on the submit1 button, the browser navigates to another.aspx—a different ASP.NET page that knows nothing about the main page's controls. You can still retrieve values from the referrer page by using the old ASP programming style.
<% @Page Language="C#" %>
Note also that when you use the ASP.NET form to post back, the content of the other text boxes is cleared. The same thing happens when you submit form data using the third button—the submit button of an HTML form that posts to the same page. Why doesn't ASP.NET work as usual? It's simple: there's no view-state information for the controls in the third form. That's the importance of working the ASP.NET way.
Important |
You can have multiple server-side forms in your ASP.NET page as long as only one at a time is visible. For example, a page with, say, three tags marked to run at the server is allowed, but only one of them can be actually rendered. Given the dynamics of page rendering, an exception is thrown if more than one HtmlForm control attempts to render. By playing with the Visible property of the form controls, you can change the active server form during the page lifetime. This approach mimics that of ASP.NET mobile controls. (See Chapter 11.) This trick doesn't really solve the problem of having multiple active forms, but it can be helpful sometimes. |
Passing Variables Across Pages
All in all, the HTML form mechanism is a way to invoke an external page to accomplish some specific tasks. When the form is posted, the browser packs the contents of the form fields in the body of the request. On the server, the target page retrieves those values and processes the request. In other words, you use the HTML element when you need to execute some code on the server passing parameters.
Accomplishing this task with ASP.NET is straightforward if you use postback events and the SFI programming model. In this case, you implicitly assume that all the server-side code resides within the same reentrant page. Let's consider, though, the more general case in which you just want to spawn any external page and make sure it can get some parameters from the caller. Figure 4-3 explains a possible ASP.NET architecture to transfer the control from the caller page to external pages passing some context information. This multiple-form model is implemented within the default SFI model.
Figure 4-3: Passing values between Web Forms pages.
The base page contains multiple logical forms—that is, not real tags but groups of input fields with each one having a submit button. Each group posts back to the same page, but then in the server-side event handler the page transfers the execution to another page—the actual action page. Should the action page access the context of the referrer by using the Session or Application objects?
Referencing Referrer Pages
In this model, the action page is activated through the Server.Transfer method. As will be discussed in Chapter 13, a page spawned through Transfer is processed in the same AppDomain and by the same HttpApplication object that was processing the referrer. The spawned page also inherits the original context of the request—the HttpContext object created upon startup.
In Chapter 13, we will learn that the HttpContext object has a property named Handler that returns the IHttpHandler object that is managing the request. When Transfer executes, the context of the original request is preserved and made available to the newly running page. Within the context, the Handler property in particular is not replaced and continues pointing to the handler that first received the HTTP request from the client. The spawned page can access the page object representing its referrer by using the Handler property as follows:
Page referrerPage = (Page) Context.Handler;
Because Handler returns a valid instance of the page object, the spawned page can access all its properties and methods. It cannot directly access the controls because of the protection level. In fact, if you look back at the source code for compiled pages in Chapter 2, you'll see that all page controls are mapped as protected members on the ASP.Xxx_aspx class. However, you could define ad hoc public properties on the caller page and expose to spawned pages all the data you want. For example, the following code exposes the content of the RegUserName text box through the RegistrationUserName public property:
<% @Page ClassName="MultiFormPage" %>
Notice the ClassName attribute in the @Page directive. It lets you assign a user-defined alias to the page class. In practice, any spawned pages can access the referrer page as an instance of the class MultiFormPage rather than as an instance of the ASP.Xxx_aspx class.
Retrieving Values
As mentioned, the spawned page can access the instance of the parent page using the Handler property. However, this fact alone is not sufficient to enable access to additional properties. The value returned by Handler must be cast to the actual page type—ASP.Xxx_aspx or whatever the ClassName attribute specifies. But how can the page add a reference to the type? That's just what the @Reference directive is all about. Figure 4-4 shows a page with a couple of logical forms—each of which transfers execution to a different page.
Figure 4-4: An ASP.NET page that contains multiple groups of logically related controls acting as independent forms.
When the user clicks the Register button, the page posts back and transfers to register.aspx. This page, in turn, retrieves the handler of the original request and casts it to an instance of the MultiFormPage class. After that, the register.aspx page can use any of the ad hoc properties defined by the caller page to expose its public data.
<% @Page ClassName="MultiFormPage" %>
Register
Login
The structure of register.aspx and login.aspx is nearly identical, although the two pages will do quite different things. Both pages access input data from the referrer using the @Reference directive and the Context.Handler property.
<% @Page Language="C#" %> <% @Reference Page="MultiForms.aspx" %>
User <%= refPage.RegistrationUserName %> has been registered. Password is <%= refPage.RegistrationPassword%>.
The model we discussed here has a couple of drawbacks that should be mentioned. First, it makes use of Server.Transfer, which is a server-to-server mechanism to transfer control. Although highly efficient in terms of performance, it does not notify the browser of the URL change. Second, the Server.Transfer method does not work across domains. In other words, you can't post to a page resident on another server. An alternative is using Response.Redirect, but in this case you must take care yourself of posting any relevant data to the target page. For more information and further references, see the "Resources" section at the end of the chapter.
Note |
From this example, it should be clear that having multiple server forms is not really impossible. The tag will remain unique, but one could create custom, form-like container controls and use them within the single server form. The new control will have an Action property and manage to handle the submit postback event on any of its buttons. The onclick handler would simply transfer the control to the page specified in Action. To establish uniformity in the way in which spawned pages retrieve input parameters, you could force the host page to implement a particular interface or take greater advantage of hidden fields. We'll return to this in Chapter 18 when discussing custom Web controls. |
Caution |
Note that the approach previously described to pass values to other pages requires the callee to know about the type and the programming interface of the caller. When assuming this is not safe, you might want to consider an alternative scheme for passing values. You can copy relevant information to the Context.Items collection and use Server.Transfer to pass the control. Because the context for the request is the same, caller and callee share the same HTTP context. In this way, the callee reads input data from a common cargo collection rather than referencing a particular page class. In terms of programming, using the Context.Items collection is nearly identical to using Request.Form or Request.QueryString. |
Pop Up Forms
The HTML 4.0 object model allows you to use the window.showModalDialog method to show a modal window with a Web page inside. In some situations, this is a very viable approach for retrieving information from the user. ASP.NET pages can be used with pop-up windows. If the page has read-only contents, no particular care should be taken—just open the window with some client-side script code and let it go. On the other hand, if the page shown through a modal window is interactive and posts back, some measures should be taken to ensure that all the output goes through the modal dialog until the user closes it.
To show logically modal forms without using client-side modal dialogs, you could resort to the trick we discussed earlier and build multiple forms with 100 percent ASP.NET code. (See Figure 4-2.) Let's drill down a bit into creating modal windows.
Client Modal Dialogs
To perform a modal operation—that is, a blocking operation that prevents interaction with the main page until terminated—a Web page can resort to the following JavaScript code:
Pro ASP.NET (Ch04)
The showModalDialog method on the window object opens the specified page in a Windows dialog box with the given title and styles. (For more information about the styling options of the dialog, see the MSDN documentation.) The preceding code pops up a 200-by-300-pixel modal window filled with the HTML content generated by dialog.aspx. So far so good.
Suppose now that the dialog.aspx page contains some links and buttons you can click. Again, think of a modal page you want to pop up for users' registration and logging. The user enters some data and then clicks; the page posts back, but the new output is displayed in a new browser window. Any value you can assign to the form's target value seems to be ineffective. The behavior is by design, but is there a workaround? Yes, for the browsers that support it, you can use a wrapper inline frame. (The tag is part of the HTML 4.0 specification.)
Instead of containing the code to display the window contents, dialog.aspx contains a properly sized tag.
At this point, the target attribute set on the multiforms.aspx page is detected and properly handled, as shown in Figure 4-5.
Figure 4-5: Navigable modal dialogs based on ASP.NET pages.
Using Embedded Forms
As mentioned earlier, by toggling on and off the visibility of blocks of related controls you can implement sort of pop-up forms as well. In the page, you define a logical form—that is, a block of controls acting as a real form—and hide and show it as needed. Basically, such a logical form would work with the rest of the page in a mutually exclusive way. The following code snippet shows a form with two logical subforms: one with a button and one with the typical login fields. Both are wrapped by a server-side
panel.
Login
When the button is clicked, the RunDlg block is hidden and Login is displayed. Note that when the Visible attribute is set to false, no code will be generated for any control.
Using this approach, the form expected to work as a modal dialog is the only piece of the page's UI being displayed at a time. Note that for a better implementation of the solution, you can take advantage of user controls to wrap the contents of a server-side
block. We'll cover user controls in Chapter 10.
Working with Script Code
When you have input forms that need to be displayed only in certain situations (for example, when the user is not registered), a commonly used approach entails that you keep hidden the optional form and show the user a link to click. (See Figure 4-6.)
Figure 4-6: The optional dialog shows up only if the user explicitly requests it.
The overall user interface of the page is cleaner, and only information that's strictly needed is displayed. The downside of this approach is that you need an extra roundtrip to display the optional input form. In some cases, typically when an up-level browser is used, resorting to some client-side script code can be as much as twice effective, giving you a clean user interface and avoiding the extra roundtrip.
Supporting Multiple Browsers
Browser information is packed in the HttpBrowserCapabilities object returned by the Request.Browser property. Table 4-3 lists all the available properties.
Property |
Description |
---|---|
ActiveXControls |
Gets whether the browser supports ActiveX controls. |
AOL |
Gets whether the client is an America Online browser. |
BackgroundSounds |
Gets whether the browser supports background sounds. |
Beta |
Gets whether the browser is a beta release. |
Browser |
Gets the user agent string transmitted in the HTTP User-Agent header. |
CDF |
Gets whether the browser supports Channel Definition Format (CDF) for webcasting. |
ClrVersion |
Gets the Version object containing version information for the .NET common language runtime installed on the client. |
Cookies |
Gets whether the browser supports cookies. |
Crawler |
Gets whether the browser is a Web crawler search engine. |
EcmaScriptVersion |
Gets the version number of ECMA script supported by the browser. |
Frames |
Gets whether the browser supports HTML frames. |
Item |
Indexer property, gets the value of the specified browser capability. |
JavaApplets |
Gets whether the client browser supports Java applets. |
JavaScript |
Gets whether the browser supports JavaScript. |
MajorVersion |
Gets the integer number denoting the major version number of the browser. |
MinorVersion |
Gets the decimal number denoting the minor version number of the browser. |
MSDomVersion |
Gets the version of the Microsoft HTML (MSHTML) document object model that the browser supports. |
Platform |
Gets the name of the platform that the client uses. |
Tables |
Gets whether the client browser supports HTML tables. |
Type |
Gets a string made of the name and the major version number of the browser (for example, Internet Explorer 6 or Netscape 7). |
VBScript |
Gets whether the browser supports VBScript. |
Version |
Gets a string representing the full (integer and decimal) version number of the browser. |
W3CDomVersion |
Gets the version of the W3C XML Document Object Model (DOM) supported by the browser. |
Win16 |
Gets whether the client is a Win16-based computer. |
Win32 |
Gets whether the client is a Win32-based computer. |
As a margin note, consider that if you're writing a mobile application, you must cast the Request.Browser return value to the MobileCapabilities object and work with that. The mobile capabilities object is supported only by version 1.1 of the .NET Framework. However, a separate download is available to enable mobile controls support for version 1.0 of the .NET Framework.
The Browser property returns the full string contained in the User-Agent header, and using it is probably the most flexible way to make sure the browser has just the characteristics you need to check. However, Type is another useful property for verifying the identity of the client browser. The following code shows how to make sure that the browser is Internet Explorer version 4.0 or newer:
bool upLevelBrowser = false; HttpBrowserCapabilities caps = Request.Browser; if (caps.Browser.ToUpper().IndexOf("IE") > -1) { // This is IE. Is version >3? upLevelBrowser = (caps.MajorVersion >3); }
By putting this code in the Page_Load event, you enable yourself to modify the structure of the page according to the capabilities of the browser.
Note |
The ClrVersion property does not include build information, which means you can't track which service pack is installed on the client, if any. For the local machine, you can get full version information about the installed CLR using the following code: Version v = Environment.Version; Console.WriteLine("{0},{1},{2},{3}", v.Major, v.Minor, v.Build, v.Revision); If the Revision property is 0, no service pack is installed. If the revision number is 209, service pack 1 is installed. The revision number for service pack 2 is 288. |
Now that we know a way to distinguish between browsers, let's use this technique to make up-level browsers show optional input forms via script. In the context of this example, an up-level browser is Internet Explorer 4.0 or a newer version.
Adding Client Side Scripts
The idea is to obtain the effect shown in Figure 4-6 using client-side DHTML script code on up-level browsers and the classic postback event elsewhere. When the user clicks the hyperlink, what happens depends on the type of the browser. Although clear in the overall design, the solution is trickier to implement than one might think at first. The hidden difficulty revolves around the type of object the user really clicks on. Whatever the browser, the user always clicks a hyperlink with some client script code attached. Because we're talking about ASP.NET pages, any script code is associated with the hyperlink on the server. And this is exactly the issue.
If the browser is up-level, you need to render the clickable hyperlink by using a client-side HTML element—that is, an element without the runat="server" attribute.
Click <a href="javascript:YourFunc()">here</a>
If the browser is down-level, you need to use the ASP.NET LinkButton control as shown in the following code:
Click
How should you design the page layout to meet this requirement? Let's review a few possibilities, starting with ASP-style code blocks:
If not registered, click <a href="javascript:ShowRegisterDlg">here</a>. .
Although effective, the solution shown generates messy and rather unreadable code when used on a large scale. It also requires you to declare the upLevelBrowser variable as global. Finally, ASP-style code-blocks are deprecated, although ASP.NET fully supports them.
A second possibility entails the use of ASP.NET custom controls smart enough to detect the browser and produce appropriate HTML output. The key advantage of this approach is that you use a single tag and bury all necessary logic in the folds of its implementation. However, because we'll not cover custom controls until Chapter 18, we'll go for a third alternative that falls somewhere in the middle of the previous two.
You use a placeholder control to mark the place in which the hyperlink should appear in one form or the other. Next, in the Page_Load event handler, you populate the Controls collection of the PlaceHolder control with either plain text or a dynamically created instance of the LinkButton control. You use plain text—more exactly, a LiteralControl instance—if the client is up-level and client script code can be used. You use the LinkButton otherwise. The following code snippet shows the page layout:
Login
If not registered, click .
In the Page_Load event handler, you first learn about the browser's capabilities and then configure the placeholder to host an HTML hyperlink or a server-side link button.
void Page_Load() { // Check browser capabilities bool upLevelBrowser = false; HttpBrowserCapabilities caps = Request.Browser; if (caps.Browser.ToUpper().IndexOf("IE") > -1) { // This is IE. Is version >3? upLevelBrowser = (caps.MajorVersion >3); } // if downlevel (considering only IE4+ uplevel) if (upLevelBrowser) { AddDhtmlScriptCode(theLink); RegPanel.Visible = true; RegPanel.Style["display"] = "none"; } else AddPostBackCode(theLink); }
If the browser is up-level, the registration panel should be included as HTML but not displayed to the user. For this reason, you must set the Visible property to true—ensuring that the HTML code for the panel will be generated—and, at the same time, you need to hide the controls from view by resorting to CSS-style properties.
If the browser proves to be down-level—whatever this means to your application—you simply create and configure the link button control to receive the user's clicking and post back.
void AddDhtmlScriptCode(PlaceHolder ctl) { // Name of the Javascript function string scriptFuncName = "ShowRegisterDlg"; // Token used to register the Javascript procedure // with the instance of the Page object string scriptName = "__ShowRegisterDlg"; // Create the hyperlink HTML code and add it to the placeholder string html = "<a href="javascript:{0}()">{1}</a>"; html = String.Format(html, scriptFuncName, "here"); LiteralControl lit = new LiteralControl(html); ctl.Controls.Add(lit); // Create the Javascript function (must include