More Controls

This chapter is excerpted from Programming ASP.NET 3.5, Fourth Edition by Jesse Liberty, Dan Maharry, Dan Hurwitz, published by O'Reilly Media

Programming ASP.NET 3.5, Fourth Edition

Logo

Buy Now

In the preceding chapter, we covered many of the standard ASP.NET controls and the AJAX extenders that you can use to enhance their functionality beyond the remit of the traditional HTML controls to which they correspond. In this chapter, we'll look at two more groups of controls:

  • Panel controls
    Those ASP.NET server controls and AJAX controls which can enclose a number of other controls and allow you to control the presentation and contents of the child controls in one

  • More standard controls from the toolbox
    Those ASP.NET server controls that offer a richer level of functionality than those you saw in Chapter 4, Basic Controls, such as the MultiView, Wizard, and Calendar controls

What this chapter does not offer is coverage of all the other controls in the Visual Studio toolbox, of which there are several dozen. Many which have a specific functionality, such as validation, data access, personalization, or navigation, are covered in individual chapters later in the book.

Panel Controls

The Panel control is used as a container for other controls much as a <div> (or a <span> to a lesser extent) acts in plain HTML. It serves several functions:

  • To control the visibility of the controls it contains

  • To control the appearance of the controls it contains

  • To make it easier to generate controls programmatically

The Panel control is derived from WebControl and has the read/write properties shown in Table 5.1, "Properties of the Panel control not inherited from Control or WebControl". The Panel control has no methods or events not inherited from the Control or WebControl class. Specifically, there are no events raised by user interaction.

Table 5.1. Properties of the Panel control not inherited from Control or WebControl

Name

Type

Values

Description

BackImageUrl

String

The URL of an image to display as the background of the panel. If the image is smaller than the panel, it will be tiled.

Direction

ContentDirection

LeftToRight, RightToLeft, NotSet

The direction to display text in a container control. Default is NotSet.

GroupingText

String

Causes the Panel to render to the browser as a <fieldset> element rather than a <div> element. The value of this property is used for the <legend> element.

HorizontalAlign

HorizontalAlign

Center, Justify, Left, NotSet, Right

Specifies the horizontal alignment of the contents, overriding any CSS styles that are already set on it. Default is NotSet. Note there is no VerticalAlign property.

ScrollBars

ScrollBars

Auto, Both, Horizontal, None, Vertical

Specifies the visibility and location of scroll bars. Default value is None.

Wrap

Boolean

true, false

If true (the default), the contents of the panel will wrap. If false, the contents will not wrap.

Tip

The Panel may seem like a humble control, but it is the basis for many of the excellent AJAX controls and extenders, as you'll see later in this section. In particular, the UpdatePanel is an ASP.NET AJAX control that works in the same way as a regular Panel control, but allows for the asynchronous updating (i.e., without posting back the whole page) of the controls it contains. It's probably the most important AJAX control after the ScriptManager.

To demonstrate this control, create in Visual Studio 2008 (VS2008) a new website solution called C5_MoreControls, and add a new web form called PanelDemo.aspx. On this page, you'll work with two Panel controls, as shown in Figure 5.1, "PanelDemo.aspx in action". The first panel demonstrates how to control the appearance and visibility of child controls and how to add controls programmatically. The second panel demonstrates the use of the GroupingText, ScrollBars, and Wrap properties to control the appearance of the control.

The first of the Panel controls in this example, with an ID of pnlDynamic, is highlighted in Example 5.1, "The first half of PanelDemo.aspx". It has some static content between the opening and closing Panel tags.

Figure 5.1. PanelDemo.aspx in action

PanelDemo.aspx in action

Example 5.1. The first half of PanelDemo.aspx

<%@ Page Language="C#" AutoEventWireup="true"
   CodeFile="PanelDemo.aspx.cs" Inherits="PanelDemo" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
   <title>Panel Demo</title>
</head>
<body>
   <form id="form1" runat="server">
   <div>
      <h1>Dynamic Contents</h1>
      <asp:Panel ID="pnlDynamic" runat="server" BackColor="Fuchsia"
         Height="150px" Width="80%" HorizontalAlign="Center"
         Font-Names="Courier" ScrollBars="Auto">
         This is static content in the panel.
         <br />
         This sentence is here to see the effect of changing the
         padding values. Padding values can be specified in terms
         of pixels (px), centimeters (cm), or percentage of the
         panel's width (%).
         <p />
      </asp:Panel>

      <table>
         <tr>
            <td>
               Number of labels:</td>
            <td>
               <asp:DropDownList ID="ddlLabels" runat="server">
                  <asp:ListItem Text="0" Value="0" />
                  <asp:ListItem Text="1" Value="1" />
                  <asp:ListItem Text="2" Value="2" />
                  <asp:ListItem Text="3" Value="3" />
                  <asp:ListItem Text="4" Value="4" />
               </asp:DropDownList>
            </td>
         </tr>
         <tr>
            <td>
               Number of textboxes:</td>
            <td>
               <asp:DropDownList ID="ddlBoxes" runat="server">
                  <asp:ListItem Text="0" Value="0" />
                  <asp:ListItem Text="1" Value="1" />
                  <asp:ListItem Text="2" Value="2" />
                  <asp:ListItem Text="3" Value="3" />
                  <asp:ListItem Text="4" Value="4" />
               </asp:DropDownList>
            </td>
         </tr>
         <tr>
            <td colspan="2">&nbsp;</td>
         </tr>
         <tr>
            <td>
               <asp:CheckBox ID="chkVisible"
                  Text="Make Panel Visible" runat="server" />
            </td>
            <td>
               <asp:Button ID="btnRefresh"
                  Text="Refresh Panel" runat="server" />
            </td>
         </tr>
      </table>

pnlDynamic has several properties defined, including BackColor, Height (in pixels), Width (in percentage of the browser window), Font-Names, and HorizontalAlign. Note that this control does not have a property for vertical alignment.

The Scrollbars attribute is set to Auto, which causes a horizontal or vertical scroll bar, or both, to be present only if necessary. Because the Wrap property is true by default, the static text will wrap within the space available; hence this first Panel control will never require a horizontal scroll bar. However, as you add enough labels and/or text boxes to the panel, a vertical scroll bar will appear as necessary.

The value for the Height attribute is an integer representing the number of pixels. The px as part of the value is optional, but it does serve to self-document. For example, the following two lines are equivalent:

Height="250px"
Height="250"

Alternatively, you can express the Height as a percentage, if it is contained within a fixed-size container, such as another Panel control. If it is not within a fixed-size container and it has no content, the panel will be only a single line high, no matter what percentage value is used. If the Height attribute is missing, the Panel control automatically sizes itself vertically to contain all of its children controls.

The Width attribute can be either an integer number of pixels or a percentage of the browser window. This example shows the latter. If the Width attribute is missing, the Panel control will default to a width of 100%.

Beneath pnlDynamic, an HTML table contains two DropDownList controls, a CheckBox control, and a Button control. Further content is added dynamically to the panel, depending on the values selected in the two drop downs: ddlLabels and ddlBoxes. There's also a CheckBox to make the panel visible or invisible. None of these three controls has its AutoPostBack property set, so to see any of the changes take effect, you need to click the button, which posts the form. This triggers the Page_Load event handler for this page-shown in Example 5.2, "Page_Load for pnlDynamic"-which then makes the necessary adjustments to the panel.

Example 5.2. Page_Load for pnlDynamic

using System;
using System.Web.UI;
using System.Web.UI.WebControls;

public partial class PanelDemo : Page
{
   protected void Page_Load(object sender, EventArgs e)
   {
      // First take care of the panel w/ the dynamically generated controls
      // Show or hide Panel contents.
      pnlDynamic.Visible = chkVisible.Checked;

      // Generate Label controls.
      int numlabels = Int32.Parse(ddlLabels.SelectedItem.Value);
      for (int i = 1; i <= numlabels; i++)
      {
         Label lbl = new Label( );
         lbl.Text = "Label" + (i).ToString( );
         lbl.ID = "Label" + (i).ToString( );
         pnlDynamic.Controls.Add(lbl);
         pnlDynamic.Controls.Add(new LiteralControl("<br />"));
      }

      // Generate TextBox controls.
      int numBoxes = Int32.Parse(ddlBoxes.SelectedItem.Value);
      for (int i = 1; i <= numBoxes; i++)
      {
         TextBox txt = new TextBox( );
         txt.Text = "TextBox" + (i).ToString( );
         txt.ID = "TextBox" + (i).ToString( );
         pnlDynamic.Controls.Add(txt);
         pnlDynamic.Controls.Add(new LiteralControl("<br />"));
      }
   }
}

The handler starts by setting the panel's Visible property to reflect whether the checkbox is checked. When the panel is not visible, its contents are not visible either. Likewise, when the panel is visible, all of its contents are visible.

Then there are two for loops, one each for labels and text boxes, which generate the contained controls. After converting the entry in the appropriate DropDownList control to an integer, the for loop iterates through the procedure for the specified number of times.

The procedure is similar in each of the two cases. A new control is instantiated, and then the Text and ID properties are assigned. The control is added to the Controls collection of the panel, and finally a LiteralControl containing some HTML is added to the collection as well. Note that the LiteralControl class is not the same as the Literal control we saw in Chapter 4, Basic Controls, although both are used to render basic text to the browser. LiteralControl is in fact the base class that all ASP.NET server controls use to render their static text.

Note how the font name specified inside the Panel tags affected the static text and labels in the panel but not the contents of the text boxes. This is because it applies only to the literal contents of the <div> corresponding to the Panel control and not to the <input> elements corresponding to the TextBox controls.

The second Panel control in this example, with an ID of pnlScroll, is highlighted in Example 5.3, "The second half of PanelDemo.aspx", which shows the rest of the code in PanelDemo.aspx.

Example 5.3. The second half of PanelDemo.aspx

      <h1>Scrollbars and Wrapping</h1>
      <asp:Panel Height="200px" ID="pnlScroll" runat="server"
         Width="90%" GroupingText="ScrollBars and Wrap">
         <asp:Label ID="lblPanelContent" runat="server"></asp:Label>
      </asp:Panel>
      <table>
         <tr>
            <td>
               ScrollBars:</td>
            <td>
               <asp:DropDownList ID="ddlScrollBars"
                  AutoPostBack="true" runat="server"
                  OnSelectedIndexChanged="ddlScrollBars_SelectedIndexChanged">
                  <asp:ListItem Text="None" Selected="True" />
                  <asp:ListItem Text="Auto" />
                  <asp:ListItem Text="Both" />
                  <asp:ListItem Text="Horizontal" />
                  <asp:ListItem Text="Vertical" />
               </asp:DropDownList>
            </td>
         </tr>
         <tr>
            <td>
               Wrap:</td>
            <td>
               <asp:RadioButtonList ID="rblWrap" runat="server"
                  AutoPostBack="true" RepeatDirection="Horizontal"
                  OnSelectedIndexChanged="rblWrap_SelectedIndexChanged">
                  <asp:ListItem Text="True" Value="true" Selected="True" />
                  <asp:ListItem Text="False" Value="false" />
               </asp:RadioButtonList>
            </td>
         </tr>
      </table>

   </div>
   </form>
</body>
</html>

As you can see, pnlScroll has only one new property declared: GroupingText. This has the effect of putting a border around the panel with the string value of the GroupingText property as a caption within the border.

The only content within pnlScroll is a Label control, lblPanelContent. To demonstrate the Panel control's scroll bar feature, Page_Load is extended to set the Label's Text property to a rather lengthy text string. This text string contains some HTML paragraph elements to force line breaks. This addition to the code-behind page, PanelDemo.aspx.cs, is shown in Example 5.4, "Additions to PanelDemo.aspx.cs".

As with pnlDynamic, an HTML table is added under pnlScroll containing two controls associated with this panel. One is the DropDownList ddlScrollBars which sets the value of the panel's Scrollbars property, and the other is a RadioButtonList setting the Wrap property. AutoPostback is set to true for both of these, so no further user action is required to see them take effect. New code is required to handle their default SelectedIndexChanged events and to make changes to pnlScroll, as shown in Example 5.4, "Additions to PanelDemo.aspx.cs".

Example 5.4. Additions to PanelDemo.aspx.cs

using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Text;

public partial class PanelDemo : Page
{
   protected void Page_Load(object sender, EventArgs e)
   {
      ... code as in Example 5.2, "Page_Load for pnlDynamic" ...

      // Next take care of the Scrollbar panel.
      StringBuilder strText = new StringBuilder("<p>Four score and seven years ago
our fathers brought forth, upon this continent, a new nation, conceived in liberty,
and dedicated to the proposition that \"all men are created equal.\"</p>");
      strText.Append("<p>Now we are engaged in a great civil war, testing whether
that nation, or any nation so conceived, and so dedicated, can long endure. We are
met on a great battle field of that war. We have come to dedicate a portion of it,
as a final resting place for those who died here, that the nation might live. This
we may, in all propriety do. But, in a larger sense, we can not dedicate -- we can
not consecrate -- we can not hallow, this ground -- The brave men, living and dead,
who struggled here, have hallowed it, far above our poor power to add or detract.
The world will little note, nor long remember what we say here; while it can never
forget what they did here.</p>");
      strText.Append("<p>It is rather for us, the living, we here be dedicated to
the great task remaining before us -- that, from these honored dead we take
increased devotion to that cause for which they here, gave the last full measure of
devotion -- that we here highly resolve these dead shall not have died in vain;
that the nation, shall have a new birth of freedom, and that government of the
people by the people for the people, shall not perish from the earth.</p>");

      lblPanelContent.Text = strText.ToString( );
   }
   protected void ddlScrollBars_SelectedIndexChanged(object sender, EventArgs e)
   {
      DropDownList ddl = (DropDownList)sender;
      string strValue = ddl.SelectedItem.ToString( );
      ScrollBars bars = (ScrollBars)Enum.Parse(typeof(ScrollBars), strValue);
      pnlScroll.ScrollBars = bars;
   }


   protected void rbl Wrap_SelectedIndexChanged(object sender, EventArgs e)
   {
      RadioButtonList rbl = (RadioButtonList)sender;
      pnlScroll.Wrap = Convert.ToBoolean(rbl.SelectedValue);
   }
}

In the event handler for the SelectedIndexChanged event of the drop-down list, ddlScrollBars_SelectedIndexChanged, the Scrollbars property of the panel is set. The technique of setting the value from the ScrollBars enumeration is exactly as described in Example 4.41, "BulletedListDemo.aspx.cs" in Chapter 4, Basic Controls for the BulletedListDemo example.

Of course, it's not the only way to do it. The following code does the same job by pulling all the possible values for a Panel control's Scrollbars property into an array and then checks the selected value against that:

ScrollBars theEnum = new ScrollBars( );
ScrollBars[] theScrollBars =
   (ScrollBars[])Enum.GetValues(theEnum.GetType( ));

foreach (ScrollBars scrollBar in theScrollBars)
{
   if (scrollBar.ToString( ) == strValue)
   {
      pnlScroll.ScrollBars = scrollBar;
   }
}

In the event handler method for the SelectedIndexChanged event of the radio button list, a reference to the radio button list is obtained by casting sender to a variable of type RadioButtonList. Then the Wrap property of pnlScroll is set appropriately by converting the SelectedValue of the control to a Boolean.

If the text string in strText did not have any HTML tags in it, it would display a single, very long line if Wrap were set to false. As it is, with each "line" enclosed in the paragraph tags, when Wrap is set to false it displays as three separate paragraphs.

The Panel Control and AJAX

The Panel control is the AJAX user's best friend, offering as it does a blank canvas on which to add some subtle and some not so subtle effects. In particular, the UpdatePanel is one of the core AJAX controls now available by default in .NET 3.5 along with the ScriptManager. It enables (asynchronous) partial-page rendering, or rather, it enables the controls it contains to post back and update from the web server asynchronously, without forcing the entire page to post back as well. You'll see more on this in the next section of the chapter.

Before then, you'll look at the following AJAX Control Toolkit controls which either extend the basic Panel or offer its functionality but in a slightly enhanced way:

  • DropShadowExtender
    Emphasizes the existence of the panel on the web page by adding a drop shadow behind it which you can customize as you see fit.

  • DragPanelExtender
    Allows the user to drag and drop the panel and its contents anywhere on the page.

  • CollapsiblePanelExtender
    Enables the panel to be "collapsed" (made invisible) and "expanded" based on whether a nominated control has been clicked or the user's mouse has passed over the panel.

  • Accordion
    Builds on the idea of the CollapsiblePanelExtender to offer a number of collapsible panes stuck together where only one can be expanded at any time.

  • Tabs
    Similar to the Accordion, the Tabs control offers the idea that a number of Panels are stacked on top of each other with only the topmost control being visible. Each panel has a header which is visible, and clicking it moves its corresponding contents to the top of the stack and makes them visible.

  • PopUpControlExtender
    Attaches an UpdatePanel to a nominated control and makes it invisible until such time as the nominated control comes into focus (is selected), when the UpdatePanel and its contents become visible ("pop up") and you can choose the value for the control from the contents of the Panel control.

Notice that all of these controls work on the idea of making a form or some static content available on a page in the most user-friendly way possible; an extension of the original intent of the Panel control.

Panel Extenders

The AJAX Control Toolkit contains two extender controls that specifically target the Panel control: the DropShadowExtender and the DragPanelExtender. To demonstrate, the next example extends PanelDemo.aspx, giving the top panel a drop shadow and allowing it to be dragged anywhere over the page. You can see the result in Figure 5.2, "PanelDemoWithAJAX.aspx in action".

Figure 5.2. PanelDemoWithAJAX.aspx in action

PanelDemoWithAJAX.aspx in action

Add a new AJAX web form to the C5_MoreControls website, call it PanelDemoWithAJAX.aspx, and copy the code for the pnlDynamic panel, its contents, and the associated HTML table, as given in Example 5.1, "The first half of PanelDemo.aspx", under the ScriptManager control. Copy also the code for only the Page_Load event handler in Example 5.2, "Page_Load for pnlDynamic" to PanelDemoWithAJAX.aspx.cs.

Now drag a DropShadowExtender from the VS2008 Toolbox so that it is under pnlDynamic but above the HTML table. Set its targetControlId property to pnlDynamic and run the page. Once you make the panel visible, you'll see the drop shadow based around the bottom right of the panel, as in Figure 5.2, "PanelDemoWithAJAX.aspx in action". You can customize the shadow's presentation and behavior a little using the five properties shown in Table 5.2, "DropShadowExtender properties".

Table 5.2. DropShadowExtender properties

Name

Type

Default

Description

Opacity

Float

0.5

Sets the transparency level of the shadow from 0.0 (totally transparent) to 1.0 (fully opaque)

Radius

Integer

5

Sets the radius in pixels of the shadow's rounded corners if Rounded is set to true

Rounded

Boolean

False

Sets whether the shadow has rounded corners

TrackPosition

Boolean

False

Sets whether the drop shadow should follow the targeted Panel if it moves on the page

Width

Integer

5

Sets the width of the shadow

To demonstrate, change the control's Opacity to 0.2, Rounded to true, and Radius to 10 and run the page again.

Now drag a DropPanelExtender from the Toolbox so that it is under the DropShadowExtender. Again, set its targetControlId property to pnlDynamic and run the page. You should notice a few issues:

  • You can drag the panel up the page but apparently not down the page. The issue here is that you can't drag the panel below the end of the content on the page. To give yourself a bit more space to experiment with the DragPanelExtender, set the style property for the main div on the page to height:800px;.

  • The drop shadow stays where it was originally created even when you drag and drop the panel elsewhere. Set the DropShadowExtender's TrackPosition property to true to fix this.

Notice how, even though the Panel or its shadow may sometimes obscure the button, the two DropDownLists do still affect the Panel in the same way as before. It's simply that the Panel has acquired new functionality. Example 5.5, "PanelDemoWithAJAX.aspx" highlights where a change has been made in PanelDemoWithAJAX.aspx over the original in Example 5.1, "The first half of PanelDemo.aspx".

Example 5.5. PanelDemoWithAJAX.aspx

<%@ Page Language="C#" AutoEventWireup="true"
   CodeFile="PanelDemoWithAJAX.aspx.cs"
   Inherits="PanelDemoWithAJAX" %>

<%@ Register Assembly="AjaxControlToolkit" Namespace="AjaxControlToolkit"
TagPrefix="cc1" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
   <title>Panel Extender Demo - DropShadows and DragPanels</title>

   <script type="text/javascript">

      function pageLoad( ) {
      }

   </script>

</head>

<body>
   <form id="form1" runat="server">
   <div style="height:800px;">
      <asp:ScriptManager ID="ScriptManager1" runat="server" />
      <h1>Dynamic Contents</h1>
      <asp:Panel ID="pnlDynamic" runat="server" BackColor="Fuchsia"
         Height="150px" Width="80%" Font-Names="Courier"
         HorizontalAlign="Center" ScrollBars="Auto">
         This is static content in the panel.
         <br />
         This sentence is here to see the effect of changing the padding values.
         Padding values can be specified in terms of pixels (px),
         centimeters (cm), or percentage of the panel's width (%).
      </asp:Panel>
      <cc1:DropShadowExtender ID="DropShadowExtender1" runat="server"
         TargetControlID="pnlDynamic" TrackPosition="true">
      </cc1:DropShadowExtender>
      <cc1:DragPanelExtender ID="DragPanelExtender1" runat="server"
         TargetControlID="pnlDynamic">
      </cc1:DragPanelExtender>

      <table>
        ... contents of table as in Example 5.1, "The first half of PanelDemo.aspx" ...
      </table>

   </div>
   </form>
</body>
</html>

In this example, you drag and drop the Panel by selecting the Panel itself. It's also possible to associate another control on the page with the panel you want to move, by setting the DragPanelExtender's DragHandleID property to the other control's ID. Having done that, the only way to move the Panel, then, will be to drag and drop the "handle" control.

Collapsible Panels and Accordions

You can get a lot of mileage out of toggling a Panel's Visible property to keep a page more user-friendly, hiding controls or forms from view until required. Indeed, PanelDemo.aspx demonstrated this earlier in the chapter, as well as the fact that the page must be posted back to the server before the change in its visibility occurs.

Naturally, AJAX presents several options to hide or make visible a panel and its contents without posting the page back to the server. In this section, you'll look at the CollapsiblePanelExtender, which provides the most direct mapping to the "toggle panel visibility by clicking a button" action in an AJAX world, and the Accordion, which stacks several collapsible panels on top of each other and substitutes the toggle button to click with a visible part of the panel itself.

In the next example, CollapsiblePanelDemo.aspx, you'll use and experiment with a CollapsiblePanelExtender (CPE) control, to hide and make visible the contents of a Panel (a RadioButtonList) in several ways. Figure 5.3, "CollapsiblePanelDemo.aspx in action" shows the final page.

Figure 5.3. CollapsiblePanelDemo.aspx in action

CollapsiblePanelDemo.aspx in action

Tip

To follow this example, you'll need ToggleButton_Checked.gif and ToggleButton_Unchecked.gif, which you can find either in the code download for this book or in the ToggleButton demo in the AJAX Control Toolkit sample website.

Add a new AJAX web form to the C5_MoreControls website and call it CollapsiblePanelDemo.aspx. Add two Label controls to the page under the ScriptManager control and give them the IDs of lblCollapse and lblExpand. Give them some text as shown in Figure 5.3, "CollapsiblePanelDemo.aspx in action". Now copy the code for the pnlDynamic panel and its contents, as given in Example 5.1, "The first half of PanelDemo.aspx", under the two Labels.

Now add a CPE control to the page under the panel. You want pnlDynamic to collapse, so set the extender's TargetControlID property to pnlDynamic. Running the page doesn't do much at the moment as the collapse and expand actions aren't hooked up to anything, so that's the next task. Set CollapseControlID to lblCollapse and ExpandControlID to lblExpand to wire up the labels. Also set Collapsed to true so that the panel is initially hidden when the page loads.

Run the page, and even if it doesn't look like you can, click the text to expand or collapse the panel. Label controls are deliberately in this example to emphasize that no postbacks are being used to collapse or expand the panel; they do not auto-postback a page when you click them.

Tip

A CollapsedPanelExtender will retain its state if a page is posted back to the server, so the panel it contains and the subordinate controls it targets will also retain the state they had before the postback.

So far, so not-too-thrilling, so let's look at some of the CPE's properties in Table 5.3, "CollapsiblePanelExtender properties" and put them to use.

Table 5.3. CollapsiblePanelExtender properties

Name

Type

Default

Description

AutoCollapse

Boolean

false

Sets whether the panel will collapse if the cursor moves off it.

AutoExpand

Boolean

false

Sets whether the panel will expand if the cursor moves onto it.

CollapseControlID

String

The ID of the control to click to toggle the panel collapsing.

Collapsed

Boolean

false

Sets whether the panel is collapsed when the page is initially loaded.

CollapsedImage

String

The URL of the image shown by the Image control named in ImageControlID when the panel is collapsed. Has no effect if ImageControlID is not set.

CollapsedSize

Integer

0

The width of the panel in pixels when collapsed.

CollapsedText

String

The value for the Text property of the Label control named in TextLabelID when the panel is collapsed. Has no effect if TextLabelID is not set.

ExpandControlID

String

The ID of the control to click to toggle the panel expanding.

ExpandDirection

Horizontal, Vertical

Vertical

The direction in which a panel is expanded/collapsed.

ExpandedImage

String

The URL of the image shown by the Image control named in ImageControlID when the panel is expanded. Has no effect if ImageControlID is not set.

ExpandedSize

Integer

The width of the panel in pixels when expanded. Overrides the target Panel control's Width property.

ExpandedText

String

The value for the Text property of the control specified by TextLabelID when the panel is expanded. Has no effect if TextLabelID is not set.

ImageControlID

String

The ID of the Image server control whose URL property will change when the panel is resized.

ScrollContents

Boolean

false

Sets whether scroll bars will be added around the panel if its contents are too large to be shown all at once when expanded. Note that these scroll bars are in addition to any that may be made visible through the Panel control's own Scrollbars property.

TextLabelID

String

The ID of the Label control whose Text property will change when the panel is resized.

Now you'll add a bit more interaction on the page besides the actual hiding and expanding of the panel. The CPE allows you to specify other controls whose values change depending on whether the panel is collapsed or visible. First consider a text control such as a Label. Add a Label control with an ID of lblState to the page and clear its Text property. Now set the CPE's textLabelID to lblState, and then use its ExpandedText and CollapsedText properties to set the Label's text when the panel has been expanded and collapsed, respectively.

You can also use the CPE to toggle between images to represent the expanded or collapsed state of the panel. Add an Image control with an ID of imgToggle to the page. Set the CPE's ImageControlID property to imgToggle and then set its ExpandedImage and CollapsedImage properties accordingly.

Finally, you'll make a few UI tweaks. The CPE's ExpandDirection property allows you to specify whether the resizing of the panel should be animated in a horizontal or vertical direction, and its CollapsedSize property lets you specify the width in pixels of the panel when collapsed, if you'd prefer it to be something other than zero. The final page is listed in Example 5.6, "CollapsiblePanel.aspx in full". You can also set AutoExpand or AutoCollapse to true if you want to toggle the state of the panel by mousing over it.

Example 5.6. CollapsiblePanel.aspx in full

<%@ Page Language="C#" AutoEventWireup="true"
    CodeFile="CollapsiblePanel.aspx.cs"
    Inherits="CollapsiblePanel" %>
<%@ Register Assembly="AjaxControlToolkit"
    Namespace="AjaxControlToolkit" TagPrefix="cc1" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
   <title>Collapsible Panel Demo</title>

   <script type="text/javascript">

      function pageLoad( ) {
      }

   </script>

</head>

<body>
   <form id="form1" runat="server">
   <div>
      <asp:ScriptManager ID="ScriptManager1" runat="server" />
      <p>
         <asp:Label ID="lblCollapse" runat="server" Text="Click to Collapse" />
         ||
         <asp:Label ID="lblExpand" runat="server" Text="Click to Expand" />
      </p>
      <p>
         <asp:Image ID="imgToggle" runat="server" /> :
         <asp:Label ID="lblState" runat="server" />
      </p>
      <p>

         <asp:Panel runat="server" ID="pnlDynamic"
            BorderWidth="2px" BorderColor="red">
         This is static content in the panel.
         <br />
         This sentence is here to see the effect of changing the padding values.
         Padding values can be specified in terms of pixels (px), centimeters (cm),
         or percentage of the panel's width (%).
         <p />
         </asp:Panel>
         <cc1:CollapsiblePanelExtender ID="CollapsiblePanelExtender1"
            runat="server" TargetControlID="pnlDynamic"
            CollapseControlID="lblCollapse" ExpandControlID="lblExpand"
            Collapsed="true" ExpandedText="Currently Expanded"
            CollapsedText="Currently Collapsed" TextLabelID="lblState"
            ExpandedImage="ToggleButton_Checked.gif"
            CollapsedImage="ToggleButton_Unchecked.gif"
            ImageControlID="imgToggle"
            ExpandDirection="Horizontal" CollapsedSize="100" />
      </p>
   </div>
   </form>
</body>
</html>

Moving now to look at the Accordion control, you'd be forgiven for looking at the next example in Figure 5.4, "AccordionDemo.aspx in action" and thinking that it is just a collection of CollapsiblePanelExtender controls wired up in a specific way. The Accordion control is in fact its own standalone control deriving from System.Web.UI.WebControls.WebControl rather than System.Web.UI.ExtenderControl.

Every Accordion control contains one or more AccordionPanes, which contain a Header area and a Content area. The user will click the Header area to reveal the contents of the AccordionPane to which it belongs. Only one pane can be shown at a time.

Tip

One of the best features of the Accordion control is that you can bind data from databases or another data source to it rather than having to write it all out longhand. We cover binding data to controls in Chapters Chapter 7, Data Source Controls and Connections through Chapter 10, Presenting LINQ.

Add a new AJAX web form to the C5_MoreControls website and call it AccordionDemo.aspx. The Accordion doesn't rely on any other controls, so just drag an Accordion control onto the page under the ScriptManager and drag two AccordionPanes into the Accordion. Give both AccordionPanes a header and a content area as highlighted in Example 5.7, "AccordionDemo.aspx".

Figure 5.4. AccordionDemo.aspx in action

AccordionDemo.aspx in action

Example 5.7. AccordionDemo.aspx

<%@ Page Language="C#" AutoEventWireup="true"
   CodeFile="AccordionDemo.aspx.cs" Inherits="AccordionDemo" %>

<%@ Register Assembly="Ajax ControlToolkit"
   Namespace="AjaxControlToolkit" TagPrefix="cc1" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
   <title>Accordion Demo</title>

   <script type="text/javascript">

      function pageLoad( ) {
      }

   </script>

   <style type="text/css">
      .header
      {
         background-color: Aqua;
         padding : 5px;
         border : solid 1px black;
         color : Black;
      }
      .selectedheader
      {
         background-color: Green;
         padding : 5px;
         border : solid 1px black;
         color : Yellow;
      }
      .content
      {
         padding: 5px;
         font-style: italic;
         font-family: Arial;
      }
   </style>
</head>
<body>
   <form id="form1" runat="server">
   <div>
      <asp:ScriptManager ID="ScriptManager1" runat="server" />
      <cc1:Accordion ID="Accordion1" runat="server"
         HeaderCssClass="header" ContentCssClass="content"
         HeaderSelectedCssClass="selectedheader"
         FadeTransitions="false" TransitionDuration="2000"
         FramesPerSecond="5" AutoSize="Limit">
         <Panes>
            <cc1:AccordionPane runat="server" ID="pane1">
               <Header>Question</Header>
               <Content>Why did the developer cross the road?</Content>
            </cc1:AccordionPane>
            <cc1:AccordionPane runat="server" ID="pane2">
               <Header>Unfunny Answer</Header>
               <Content>The chicken had a gun</Content>
            </cc1:AccordionPane>
         </Panes>
      </cc1:Accordion>
   </div>
   </form>
</body>
</html>

Save and run the page. You'll see that the Accordion expands the first pane and collapses the second by default. Clicking the header for the second pane will expand its contents and collapse those of the first. You can add as many AccordionPanes into the Panes collection as you have data, although it may be worth noting that too many will clutter the screen and will not help users find the information or the bit of the form they require.

Table 5.4, "Accordion control properties" presents a list of properties controlling the display of the Accordion on the page you'll use to tweak the control in AccordionDemo.aspx.

Table 5.4. Accordion control properties

Name

Type

Default

Description

AutoSize

Fill, Limit, None

None

Determines how much space the Accordion will take up on the page.

ContentCssClass

String

Sets the default CSS for the content area of all its child AccordionPanes.

FadeTransitions

Boolean

false

Sets whether the contents of a newly expanded AccordionPane fade into view.

FramesPerSecond

Integer

15

Sets the speed of the animation between AccordionPane objects.

HeaderCssClass

String

Sets the default CSS class for the header areas of all child AccordionPanes whose content areas are currently hidden.

HeaderSelectedCssClass

String

The default CSS class for the header area of the currently selected AccordionPane.

RequireOpenedPane

Boolean

true

Sets whether the Accordion must always have the contents of one pane visible to the user.

SelectedIndex

Integer

0

The index of the AccordionPane in the Accordion that should be open when the page is loaded initially and the index of the pane currently selected after that.

SuppressHeaderPostbacks

Boolean

false

Sets whether postback events which would have been raised by controls in an AccordionPane header area should be suppressed. So, if you have a hyperlink in the header, the browser navigates to that URL when clicked rather than resizing the Accordion.

TransitionDuration

Integer

500

Length in milliseconds of the animation between AccordionPane objects.

Now to demonstrate these properties: first, set RequireOpenedPane to false and run the page again. Note the top pane is still open by default on initial load, but you can now collapse it without having any other pane expanded. It's still true that only one AccordionPane can be expanded at any one time; you can't change that. If you want to change the pane that opens initially on-screen, set the Accordion's SelectedIndex property to a value other than 0 (zero). This value represents the (zero-based) index of the pane you want to be opened in the list of panes in your markup. Therefore if you want the fourth pane in your Accordion to be open initially, set SelectedIndex to 3.

The Accordion displays both the header and contents of its panes as plain text by default. Example 5.7, "AccordionDemo.aspx" suggests some basic CSS styles that you can use to differentiate between the header of a currently expanded (selected) pane and the headers of panes that are not currently selected. Set the Accordion's HeaderSelectedCssClass and HeaderCssClass to these styles, respectively, to link them. You can also style the contents of AccordionPanes using the Accordion control's ContentCssClass property.

You can also tweak the animation that occurs when a user switches between panes. Like a CollapsiblePanelExtender, when a pane is deselected its content area collapses into its header, and when it is selected its content area extends from the header into an area big enough to show the pane's contents, or to the size specified by the Accordion control's Height property.

You can control how quickly that collapse or expansion of the content area occurs using the TransitionDuration and FramesPerSecond properties. The former defines how long (in milliseconds) the complete collapse/expansion animation will take, and the latter determines how many steps will occur in the animation per second, effectively determining how smooth the animation will appear.

You can also control how the contents of the pane appear in a new expanded area on-screen using the FadeTransitions property. If you set this property to true, you will see the contents fade into view once the pane has expanded fully. Setting it to false will have the contents simply appear.

One final property to consider for the Accordion-AutoSize-controls how much space the Accordion takes up on the page. It takes one of three values:

  • None
    The Accordion will take up exactly enough space to show the currently expanded pane and all the other headers. When a new pane is selected, the other contents of the page may be moved up or down depending on the new size of the Accordion.

  • Limit
    The Accordion will take up exactly the space on the page given by its Height CSS property. If content in the selected pane doesn't fill the space, it will add whitespace so that it does. If content overfills the selected pane, scroll bars will be added to the pane accordingly.

  • Fill
    The Accordion will take up exactly the space on the page as determined by its Height CSS property. If content in the selected pane doesn't fill or overfills the space, it will be shrunk or expanded accordingly.

It's also worth noting that each AccordionPane also has a HeaderCssClass and a ContentCssClass property with which you can override the choice of style made in the Accordion's properties of the same name.

From Columns to Stacks

From working on the y-axis to working on the z-axis, the next AJAX Control Toolkit server control stacks Panel controls on top of each other, much as Windows Property pages do (see Figure 5.5, "Windows Property pages, which also use a tabbed interface"), rather than below each other as Accordions do. This tabbed interface is often the easiest for users to understand and the TabContainer control gives you the chance to use it on a web page.

Figure 5.5. Windows Property pages, which also use a tabbed interface

Windows Property pages, which also use a tabbed interface

As with the Accordion, there are two pieces to the puzzle here. The Accordion has its AccordionPanes and the TabContainer has its TabPanels, which also provide the idea of header text and contents, albeit in a different way:

<cc1:TabContainer>
   <cc1:TabPanel HeaderText=" header1 ">
      <ContentTemplate>
         content for tabpanel 1
      </ContentTemplate>
   </cc1:TabPanel>

   ...

   <cc1:TabPanel HeaderText=" header n ">
      <ContentTemplate>
         content for tabpanel n
      </ContentTemplate>
   </cc1:TabPanel>
</cc1:TabContainer>

As you can see, in contrast to AccordionPane, TabPanels use their HeaderText property for their tab header contents and a ContentTemplate to contain everything to be displayed in that panel.

Let's work through an example, in which you'll adapt the original PanelDemo.aspx page into a tabbed-style interface. Add a new AJAX web form to the C5_MoreControls website, and call it TabDemo.aspx. Copy the top panel, pnlDynamic, along with its contents from PanelDemo.aspx onto the new page and place it under the ScriptManager control. The TabContainer doesn't rely on any other controls (again like the Accordion control), so just drag one onto the page under the ScriptManager and then use its Smart Tag menu to add TabPanel controls to it.

For the first TabPanel, set its HeaderText property to "Number of Labels" and copy the DropDownList control, ddlLabels, from PanelDemo.aspx into its ContentTemplate. Copy the ddlBoxes DropDownList control into the ContentTemplate for the second TabPanel and give it the HeaderText "Number of TextBoxes".

Finally, copy btnRefresh onto the page and under the TabContainer. Example 5.8, "TabDemo.aspx" shows the markup code with the new TabContainer and contents highlighted.

Example 5.8. TabDemo.aspx

<%@ Page Language="C#" AutoEventWireup="true"
   CodeFile="TabDemo.aspx.cs" Inherits="TabDemo" %>

<%@ Register Assembly="AjaxControlToolkit"
   Namespace="AjaxControlToolkit" TagPrefix="cc1" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
   <title>Tab Demo</title>

   <script type="text/javascript">

      function pageLoad( ) {
      }

   </script>
</head>

<body>
   <form id="form1" runat="server">
   <div>
      <asp:ScriptManager ID="ScriptManager1" runat="server" />
      <asp:Panel ID="pnlDynamic" runat="server" BackColor="Fuchsia"
         Height="150px" HorizontalAlign="Center" ScrollBars="Auto">
         This is static content in the panel.
         <br />
         This sentence is here to see the effect of changing
         the padding values. Padding values can be specified
         in terms of pixels (px), centimeters (cm), or percentage
         of the panel's width (%).
         <p />
      </asp:Panel>
      <br />
      <hr />
      <br />
      <cc1:TabContainer ID="TabContainer1" runat="server"
         Height="200" ActiveTabIndex="1">
         <cc1:Tab Panel ID="TabPanel1" runat="server"
            HeaderText="Number of labels">
            <ContentTemplate>
               <p>How many labels should be added to the panel?</p>
               <asp:DropDownList ID="ddlLabels" runat="server">
                  <asp:ListItem Text="0" Value="0" />
                  <asp:ListItem Text="1" Value="1" />
                  <asp:ListItem Text="2" Value="2" />
                  <asp:ListItem Text="3" Value="3" />
                  <asp:ListItem Text="4" Value="4" />
               </asp:DropDownList>
            </ContentTemplate>
         </cc1:TabPanel>
         <cc1:TabPanel ID="TabPanel2" runat="server"
            HeaderText="Number of textboxes">
            <ContentTemplate>
            <p>How many textboxes should be added to the panel?</p>
               <asp:DropDownList ID="ddlBoxes" runat="server">
                  <asp:ListItem Text="0" Value="0" />
                  <asp:ListItem Text="1" Value="1" />
                  <asp:ListItem Text="2" Value="2" />
                  <asp:ListItem Text="3" Value="3" />
                  <asp:ListItem Text="4" Value="4" />
               </asp:DropDownList>
            </ContentTemplate>
         </cc1:TabPanel>
      </cc1:TabContainer>
      <br />
      <br />
      <asp:Button ID="btnRefresh"
         Text="Refresh Panel" runat="server" />
   </div>
   </form>
</body>
</html>

You'll also need to copy the code pertaining to the two DropDownList controls from the Page_Load handler in PanelDemo.aspx.cs into TabDemo.aspx.cs, as shown in Example 5.9, "TabDemo.aspx.cs".

Example 5.9. TabDemo.aspx.cs

using System;
using System.Web.UI;
using System.Web.UI.WebControls;

public partial class TabDemo : Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
       // Generate label controls
       int numlabels = Int32.Parse(ddlLabels.SelectedItem.Value);
       for (int i = 1; i <= numlabels; i++)
       {
          Label lbl = new Label( );
          lbl.Text = "Label" + (i).ToString( );
          lbl.ID = "Label" + (i).ToString( );
          pnlDynamic.Controls.Add(lbl);
          pnlDynamic.Controls.Add(new LiteralControl("<br />"));
       }

       // Generate textbox controls
       int numBoxes = Int32.Parse(ddlBoxes.SelectedItem.Value);
       for (int i = 1; i <= numBoxes; i++)
       {
          TextBox txt = new TextBox( );
          txt.Text = "TextBox" + (i).ToString( );
          txt.ID = "TextBox" + (i).ToString( );
          pnlDynamic.Controls.Add(txt);
          pnlDynamic.Controls.Add(new LiteralControl("<br />"));
       }
    }
}

If you run the page now, you'll see something similar to Figure 5.6, "TabDemo.aspx in action".

The page works the same way as PanelDemo.aspx. Just choose values in the two DropDownLists and click the button. The TabContainer affects nothing in their operation or the way they can be referenced from the code-behind file-just the way in which they are displayed on-screen.

You can tweak the TabContainer presentation using the properties shown in Table 5.5, "TabContainer's UI-related properties".

Table 5.5. TabContainer's UI-related properties

Name

Type

Default

Description

ActiveTabIndex

Integer

-1

The (zero-based) index of the TabPanel currently visible on the page.

CssClass

String

ajax_ _tab_xp

The parent CSS class with which you want to style the TabContainer and its panels (more on this shortly).

Height

Integer

The height in pixels of the TabContainer.

Scrollbars

Auto, None, Both, Horizontal, or Vertical

None

Sets whether a TabPanel should contain scroll bars if its content overfills the space available.

Width

Integer

The width of the TabContainer as a percentage of the width of the page.

Figure 5.6. TabDemo.aspx in action

TabDemo.aspx in action

You can format all the tab headers and their contents using two CSS styles:

  • ajax_ _tab_body formats the text contents of the tab.

  • ajax_ _tab_header formats the area containing all the headers of the tabs.

Three additional styles allow you to change the appearance of an individual tab header:

  • ajax_ _tab_tab formats the text in the tab header.

  • ajax_ _tab_inner surrounds ajax_ _tab_tab and is often used to set the righthand background image of the tab.

  • ajax_ _tab_outer surrounds ajax_tab_inner and is often used to set the lefthand background image of the tab.

Figure 5.7, "The TabPanel and its CSS styles" illustrates how these styles fit together in practice. You can also see this working in TabPanelStylesDemo.aspx, which is available in the download for this chapter.

Figure 5.7. The TabPanel and its CSS styles

The TabPanel and its CSS styles

To apply your own definitions to these five styles, you must first specify a "parent class" in the TabContainer's CssClass property. In Figure 5.7, "The TabPanel and its CSS styles", all five styles have a 2px border and their padding attributes are set to 10px. The TabContainer's CssClass property is set to DemoStyles, so the five styles are defined as follows:

<style type="text/css">
   .DemoStyles .ajax_ _tab_header {
      font-family:Courier New; padding: 10px; border:dotted 2px red;}

   .DemoStyles .ajax_ _tab_outer {
      padding:10px;border:solid 2px green;}

   .DemoStyles .ajax_ _tab_inner {
      padding:10px;border:dotted 2px blue;}

   .DemoStyles .ajax_ _tab_tab {
      padding:10px;border:solid 2px black;background-color:yellow;}

   .DemoStyles .ajax_ _tab_body {
      font-family:verdana;border:1px solid black;padding:10px}
</style>

You can also go further and define CSS styles for a tab header when the cursor is hovering over it by using the .ajax_ _tab_hover class. For instance:

.DemoStyles .ajax_ _tab_hover .ajax_ _tab_outer {}
.DemoStyles .ajax_ _tab_hover .ajax_ _tab_inner {}
.DemoStyles .ajax_ _tab_hover .ajax_ _tab_tab {}

Similarly, you can also define specific CSS styles for the currently selected tab header by using the .ajax_ _tab_active class:

.DemoStyles .ajax_ _tab_active .ajax_ _tab_outer {}
.DemoStyles .ajax_ _tab_active .ajax_ _tab_inner {}
.DemoStyles .ajax_ _tab_active .ajax_ _tab_tab {}

Both of these sets of styles will override the defaults for tab headers when applicable.

Tip

You can find the default styles which the AJAX Control Toolkit uses for the TabPanel in <toolkitInstallDirectory>\AjaxControlToolkit\Tabs\Tabs.css.

Finally, you can also set a TabPanel's Enabled property to true or false to make it visible or not visible in the container area.

The UpdatePanel Control

The majority of the controls you've seen thus far in this chapter and in Chapter 4, Basic Controls have had some sort of graphical component. The Panel control does not, nor does the UpdatePanel, which you'll look at next. Available initially in Microsoft's ASP.NET AJAX Extensions download and now included directly in .NET 3.5, the UpdatePanel has much the same purpose as the Panel control-to identify and isolate a set of controls on a page-but with a different intention once the controls have been added to the panel.

If you'll recall from Chapter 3, Controls: Fundamental Concepts, the UpdatePanel is a server-side control that enables the common AJAX pattern of partial-page updates. Or rather, as well as being able to make the controls it contains visible or invisible, the UpdatePanel lets you isolate content you want to update independently from the rest of the content on the page.

One of the best features of the UpdatePanel is exactly how easy it is to enable this partial-page postback. You just drag one or more UpdatePanel controls onto your page and then drag controls you want updated into the UpdatePanel. Each UpdatePanel is updated individually and asynchronously, without affecting one another or anything else on the page.

Tip

You can disable all UpdatePanels on a page by setting the ScriptManager's EnablePartialRendering property to false on that page.

It's time to demonstrate. Add a new AJAX web form to the C5_MoreControls website and call it UpdatePanelDemo.aspx. First, you need to establish what the page does when it posts back normally. Add two labels, lblTime and lblCounter, and a button, btnStdPostBack, to the page below the ScriptManager control. Delete lblTime.Text and set lblCounter.Text to 0. Set btnStdPostback.Text to "Standard Postback".

<p>
   Time:
   <asp:Label ID="lblTime" runat="server" /><br />
   Number of page_loads:
   <asp:Label ID="lblCounter" runat="server" Text="0" />
</p>
<p>
   <asp:Button ID="btnStdPostback" runat="server"
   Text="Standard Postback" />
</p>

Switch to the code-behind page and add the following code to the Page_Load handler:

protected void Page_Load(object sender, EventArgs e)
{
   // Get counter1 and increment it
   int counter = Int32.Parse(lblCounter.Text);
   lblCounter.Text = (++counter).ToString( );

   // Set current date and time
   lblTime.Text = DateTime.Now.ToString( );
}

Now run the page. lblTime displays the time the page was last loaded, and lblCounter displays the number of times it has been reloaded as a result of postbacks. So far, so boring; every time you click the button, the whole page is posted back and all the values are updated on the page accompanied by a flicker of the page in the browser as it is reloaded.

Next, copy the two labels, paste them below the button, and call them lblTime2 and lblCounter2. Add a new button under them, called btnAsyncPostback, and adjust the Page_Load routine to update the new labels as well, as shown in Example 5.10, "UpdatePanelDemo.aspx.cs". Note that all the code is in the one function. Run the page and verify that all the labels update with the same values regardless of which button is pressed.

Example 5.10. UpdatePanelDemo.aspx.cs

using System;
using System.Web.UI;

public partial class UpdatePanelDemo : Page
{

   protected void Page_Load(object sender, EventArgs e)
   {
      // Get counter1 and increment it
      int counter = Int32.Parse(lblCounter.Text);
      lblCounter.Text = (++counter).ToString( );

      // Get counter2 and increment it
      counter = Int32.Parse(lblCounter2.Text);
      lblCounter2.Text = (++counter).ToString( );

      // Set current date and time
      lblTime.Text = DateTime.Now.ToString( );
      lblTime2.Text = DateTime.Now.ToString( );
   }
}

Now drag an UpdatePanel control onto the page from the AJAX Extensions part of the Toolbox in VS2008. Cut and paste lblTime2, lblCounter2, and btnAsyncPostback into the UpdatePanel. If you do this in Source view, you'll need to add a <ContentTemplate> child element to the UpdatePanel and cut and paste the controls into it. In Design view, the content template is created automatically. Either way, the code for the page thus far should resemble that in Example 5.11, "UpdatePanelDemo.aspx with one UpdatePanel". (You'll also find it in the download for the chapter, saved as UpdatePanelDemoPart1.aspx.)

Example 5.11. UpdatePanelDemo.aspx with one UpdatePanel

<%@ Page Language="C#" AutoEventWireup="true"
   CodeFile="UpdatePanelDemo.aspx.cs"
   Inherits="UpdatePanelDemo" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
   <title>UpdatePanel Demo</title>
   <script type="text/javascript">

      function pageLoad( ) {
      }

   </script>

</head>
<body>
   <form id="form1" runat="server">
   <div>
      <asp:ScriptManager ID="ScriptManager1" runat="server" />
      <h1>
         Update Panel Demo Part 1</h1>
      <p>
         Time:
         <asp:Label ID="lblTime" runat="server" /><br />
         Number of page_loads:
         <asp:Label ID="lblCounter" runat="server" Text="0" />
      </p>
      <p>
         <asp:Button ID="btnStdPostback"
            runat="server" Text="Standard Postback" />
      </p>
      <br />
      <hr />
      <br />
      <asp:UpdatePanel ID="UpdatePanel1" runat="server">
         <ContentTemplate>
            <p>
               Time:
               <asp:Label ID="lblTime2" runat="server" /><br />
               Number of page_loads:
               <asp:Label ID="lblCounter2" runat="server" Text="0" />
            </p>
            <p>
               <asp:Button ID="btnAsyncPostback"
                  runat="server" Text="Async Postback" />
            </p>
         </ContentTemplate>
      </asp:UpdatePanel>
   </div>
   </form>
</body>
</html>

Run the page again. Note that clicking btnStdPostBack updates both sets of labels as before, but clicking btnAsyncPostback updates only the values for lblTime2 and lblCounter2, as shown in Figure 5.8, "A partial-page postback". Because btnAsyncPostback is in the UpdatePanel, ASP.NET AJAX understands that when it is clicked, only the controls inside the UpdatePanel should be updated. Hence, an asynchronous or "partial-page" update occurs and there is no flicker in the browser as the whole page reloads.

Thus, one of the most useful aspects of AJAX is demonstrated. It is not without its caveats, however.

Figure 5.8. A partial-page postback

A partial-page postback

Click btnAsyncPostback a few times so that the partial-page update occurs several times and the counters are out of sync. Now click btnStdPostback again so the whole page updates. Note how the counters are back in sync again. One has been added to the number shown by lblCounter2 for the latest Page_Load, but lblCounter is now equal to lblCounter2 rather than just having one added to it.

This demonstrates a key point. When a partial-page update occurs through an UpdatePanel, the whole page, its view state, and the rest is actually posted back to the server, and the values for the new version of the page are calculated but only the values for the controls in the UpdatePanel are sent back.

Thus, each time a partial-page update occurred, the value in lblCounter did actually increase, but it wasn't updated on-screen. When the full-page postback happened, lblCounter was incremented and updated at last. It always matched lblCounter2, but it wasn't updated.

Also worth noting at this time is the button-or more generally, the control-that kicks off the partial-page postback in the UpdatePanel doesn't actually have to be contained within it. We will show this in a later part of this demonstration.

Multiple Update Panels on One Page

At this point you could well conclude that the easiest way to eliminate flicker and improve the user experience of your web pages is to put all the contents of your page into one UpdatePanel and leave it at that. However, this is not that great an idea: putting only what actually will need to be updated in an UpdatePanel will increase the speed of the page even further. All you need to do is add other UpdatePanel to the page as appropriate and set the UpdatePanel's UpdateMode property to either Conditional or Always. The next part of this demo will demonstrate what each value does.

Copy UpdatePanel1 and all its contents, and paste them onto the bottom of UpdatePanelDemo.aspx. Rename the controls inside the new UpdatePanel to lblTime3, lblCounter3, and btnAsyncPostback2, and copy the code in Page_Load to update lblTime3 and lblCounter3 on postback to the server in the same way as lblTime2 and lblCounter2 are updated.

Run the page and note that clicking the buttons in either UpdatePanel causes the labels in both UpdatePanels to be updated. This is the default (Musketeer) setting for UpdatePanel: all for one and one for all. One partial postback in any UpdatePanel causes the contents of all UpdatePanel controls to be updated.

To override this setting, you need to set the UpdateMode property to Conditional for any UpdatePanel you want to act independently of the others on the page, rather than Always, which is the default. To demonstrate, set UpdateMode to Conditional for UpdatePanel2 and run the page again.

You'll see now that clicking btnAsyncPostback (which is not inside UpdatePanel2) does not cause the labels in UpdatePanel2 to update. However, clicking btnAsyncPostback2 still causes the labels in both UpdatePanels to update, because UpdatePanel1 still has its UpdateMode set to Always. Set UpdateMode to Conditional for UpdatePanel1 for both UpdatePanels to post back to the server by themselves.

While we're covering "modes," note that UpdatePanel has a second property called RenderMode, which determines whether the server renders it as a span or a div. Run the page again and see that both UpdatePanels are rendered as <div> elements-for example, <div id="UpdatePanel2"> … </div>.

Now set the RenderMode property for UpdatePanel2 to inline (rather than the default of block) and run the page again. View the source and you'll see that UpdatePanel2 is now rendered as <span id="UpdatePanel2"> … </span>.

Tip

You'll find the code for this part of the demo in the download as UpdatePanelDemo2.aspx.

Table 5.6, "UpdatePanel properties" lists a full set of properties for the UpdatePanel.

Table 5.6. UpdatePanel properties

Name

Values

Default

Description

ChildrenAsTriggers

Boolean

true

If one UpdatePanel is nested in another, set the parent UpdatePanel ChildrenAsTriggers property to false if its contents should not update when a control inside its nested UpdatePanel starts a postback.

ContentTemplate

Reference to the content template for the UpdatePanel.

Controls

Read-only. Gets the collection of all the UpdatePanel child controls.

IsInPartialRendering

Boolean

false

Read-only. Returns true if the UpdatePanel is currently being updated by a partial-page postback. Returns false otherwise.

RenderMode

Block, Inline

block

Specifies whether the UpdatePanel should be treated as a block-level element on the page (like a <div>) or inline inside another block element (like a <span>).

RequiresUpdate

Boolean

true

Read-only. Returns true if the control's Update method has been called or its UpdateMode is set to Always.

Triggers

Read-only. Returns the collection of the triggers that will cause the UpdatePanel to partially post back. See the next section for more information.

UpdateMode

Always, Conditional

Always

Sets whether the contents of the UpdatePanel will be updated on all asynchronous postbacks or only when it is explicitly set for postback by a trigger.

Now you can isolate UpdatePanel controls from each other. The next common issue with UpdatePanel controls is that the control (a Button, a DropDownList, etc.) causing the updates in the UpdatePanel is trapped inside the panel as well. From a UI standpoint, this may not be a good thing. Fortunately, you can nominate a control outside an UpdatePanel to asynchronously update its contents by adding a new AsyncPostBackTrigger object to the UpdatePanel triggers collection, as we'll demonstrate next.

In UpdatePanelDemo.aspx, cut and paste all the buttons from the three areas to the bottom of the page. You can run the page at this point to confirm that all three buttons now initiate only full-page postbacks. The trick now is to add a <triggers> collection to both UpdatePanel controls with each containing an AsyncPostBackTrigger object, where the ControlID is the associated button and the EventID is Click, as highlighted in Example 5.12, "Using UpdatePanels with AsyncPostBackTriggers".

Example 5.12. Using UpdatePanels with AsyncPostBackTriggers

<%@ Page Language="C#" AutoEventWireup="true"
   CodeFile="UpdatePanelDemo.aspx.cs"
   Inherits="UpdatePanelDemo" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
   <title>UpdatePanel Demo (Part 3)</title>

   <script type="text/javascript">

      function pageLoad( ) {
      }

   </script>
</head>

<body>
   <form id="form1" runat="server">
   <div>
      <asp:ScriptManager ID="ScriptManager1" runat="server" />
      <h1>
         Update Panel Demo Part 3</h1>
      <p>
         Time:
         <asp:Label ID="lblTime" runat="server" /><br />
         Number of page_loads:
         <asp:Label ID="lblCounter" runat="server" Text="0" />
      </p>
      <br />
      <hr />
      <br />
      <asp:UpdatePanel ID="UpdatePanel1"
         runat="server" UpdateMode="Conditional">
         <ContentTemplate>
            <p>
               Time:
               <asp:Label ID="lblTime2" runat="server" /><br />
               Number of page_loads:
               <asp:Label ID="lblCounter2" runat="server" Text="0" />
            </p>
         </ContentTemplate>
         <Triggers>
            <asp:AsyncPostBackTrigger
               ControlID="btnAsyncPostback" EventName="Click" />
         </Triggers>
      </asp:UpdatePanel>
      <br />
      <hr />
      <br />
      <asp:UpdatePanel ID="UpdatePanel2"
         runat="server" UpdateMode="Conditional">
         <ContentTemplate>
            <p>
               Time:
               <asp:Label ID="lblTime3" runat="server" /><br />
               Number of page_loads:
               <asp:Label ID="lblCounter3" runat="server" Text="0" />
            </p>
         </ContentTemplate>
         <Triggers>
            <asp:AsyncPostBackTrigger
               ControlID="btnAsyncPostback2" EventName="Click" />
         </Triggers>
      </asp:UpdatePanel>
      <br />
      <hr />
      <br />
      <p>
         <asp:Button ID="btnStdPostback"
            runat="server" Text="Standard Postback" />
      </p>
      <p>
         <asp:Button ID="btnAsyncPostback"
            runat="server" Text="Async Postback" />
      </p>
      <p>
         <asp:Button ID="btnAsyncPostback2"
            runat="server" Text="Async Postback 2" />
      </p>
   </div>
   </form>
</body>
</html>

No alterations to the code-behind page are needed. Run the page again, and you'll see that the buttons now work with their associated UpdatePanel controls as before. The AsyncPostBackTrigger objects use their ControlID and EventID properties to identify a specific event occurring on a page to trigger a partial postback. The example identifies a Button's Click event for each UpdatePanel, but it could equally identify the SelectedIndexChanged event for a DropDownList or a ListBox.

Tip

You'll find the code for this part of the demo in the download as UpdatePanelDemo3.aspx.

Using the UpdateProgress Control

The demos in this chapter are necessarily short so we can cover all the relevant material on UpdatePanel without making the chapter any longer than it has to be. Thus, it may not have occurred to you to ask what happens if a partial-page update takes a long time to complete. Actually, nothing happens, which might leave your users wondering whether the page is broken or whether they've done something wrong. The best way to assure them that neither of these is the case is to add an UpdateProgress control to the page, the easiest analogy for which is the "Loading" screen you will have seen while many large Flash animations load in your browser. This demonstration isn't as fancy as one of those, but you'll get the idea.

Taking UpdatePanelDemo.aspx, move the buttons back to their UpdatePanel and delete the trigger collections. You know from earlier in this section that all the partial postbacks in this page occur almost instantaneously, so you'll need to create an artificial delay. In Design view, double-click btnAsyncPostback to generate an event handler for its Click handler. Now add the following line of code to "pause" the postback for five seconds:

protected void btnAsyncPostback_Click(object sender, EventArgs e)
{
   System.Threading.Thread.Sleep(5000);
}

Run the page and click btnAsyncPostback to see how long it feels. Back in VS2008, add an UpdateProgress control to the page-it doesn't need to be in or near any UpdatePanels-and set its AssociatedUpdatePanelID to UpdatePanel1.

Now add a message to the UpdatePanel. In Design view, VS2008 will automatically create the ProgressTemplate for the control which contains the message. In Source view, you'll need to add a <ProgressTemplate> child element to the UpdatePanel and cut and paste the controls into that:

<asp:UpdateProgress ID="UpdateProgress1" runat="server"
   AssociatedUpdatePanelID="UpdatePanel1">
   <ProgressTemplate>
      <p style="color:Red;">Please wait. This page is loading.</p>
   </ProgressTemplate>
</asp:UpdateProgress>

Run the page again and click btnAsyncPostback. After 0.5 seconds, the UpdateProgress control kicks in and the contents of the ProgressTemplate are added to the screen until the update is complete, as shown in Figure 5.9, "The UpdateProgress control in action".

You can tweak how it works using the DisplayAfter property to set the number of milliseconds the control will display after the asynchronous update was started. This can help prevent the UpdateProgress control from flashing if the asynchronous update happens very quickly. Use the DynamicUpdate property to set whether the page should include a blank area for the UpdateProgress control's contents, or whether it should just be added dynamically to the page (as is the case in this example). Table 5.7, "Properties for the UpdateProgress control" provides a full list of UpdateProgress properties.

Figure 5.9. The UpdateProgress control in action

The UpdateProgress control in action

Table 5.7. Properties for the UpdateProgress control

Name

Values

Default

Description

AssociatedUpdatePanelID

String

The ID of the UpdatePanel this control is associated with.

DisplayAfter

Integer

500

The number of milliseconds after an UpdatePanel starts posting back before the UpdateProgress control is shown on the page.

DynamicUpdate

Boolean

true

Sets whether any space is set aside on the page for the control even when it isn't visible.

ProgressTemplate

Gets and sets a reference to the template containing the contents of the UpdateProgress control when the control is visible.

UpdatePanel Controls: A Reminder

Before we leave the topic of UpdatePanel controls, it's worth reiterating two key points in their use:

  • When a partial-page update occurs, a full postback to the page does occur-including all view state, script, (hidden) form fields, and events (a complete page life cycle does occur, albeit under the covers)-but only the content necessary to update the content in the UpdatePanel is being updated. So, in the context of performance, it's worth remembering that the partial-page update is an iceberg. You only see about 10% of what's going on, but there is another 90% under the surface that will hammer your connections if you abuse UpdatePanel controls.

  • Don't add just a single UpdatePanel for all the controls on a page. It's wiser to use individual panels for each set of controls dealing with a certain set of information and an action upon it. That way, only that certain set of information is sent back to the browser, rather than the whole form, and you reduce the bandwidth used to update your page.

The trick, then, is to make sure each UpdatePanel does or does not act in sync with the other. For example, when you add a new customer to a database from a DetailsView control in one UpdatePanel, you will want the DataGrid showing customer data (in a different UpdatePanel) to update, but you won't want the Repeater showing today's special offers in a third UpdatePanel to be updated as well. Use the UpdateMode property for the UpdatePanel set to Conditional so that it will cause an asynchronous postback only when an appropriate event is raised from within the UpdatePanel or by a trigger control.

It's time to leave the world of panels for other controls whose purpose is not a million miles away from that of the Tab or Accordion control.

MultiView and View Controls

Sometimes you may want to break a web page into different pieces, displaying only a single piece at a time, with easy transitions from piece to piece. So far, this scenario isn't any different from an Accordion or TabContainer control. However, the MultiView, View, and Wizard controls add the notion of an order to visit their panels, whereas Tabs and Accordions do not.

The classic use of this addition technique is to walk a user through a number of steps within the context of a static page, such as the checkout procedure from an online store, or the procedure to transfer funds from one account to another. You can also use these controls to create wizard-like applications, although there is now a Wizard control, described shortly, for this exact purpose.

Tip

In fact, the View and MultiView controls were originally designed for mobile devices, which explains their limited capabilities for style (which you'll see in a minute) and their high utility in pages optimized for low-res screens.

ASP.NET provides the View control to manage the chunks-that is, the content in a section of the page: one View control per chunk. All of the View objects are contained together within a MultiView object, which makes one View object, called the active view, visible at a time.

As shown in Figure 3.8, "Relationships of controls in the System.Web.UI.WebControls namespace" in Chapter 3, Controls: Fundamental Concepts, both the View and MultiView controls derive not from WebControl, but directly from System.Web.UI.Control.

The MultiView control has a read-only property called Views, of type ViewCollection, which is a collection of the View controls contained within the MultiView. As with all .NET collections, the elements in the collection are indexed. Hence, the MultiView control has a property called ActiveViewIndex, which gets or sets the zero-based index of the currently active view. If no view is active, ActiveViewIndex will be -1, which is the default value.

The MultiView control has four properties, listed in Table 5.8, "MultiView CommandNames", that correspond to the four CommandName attributes which you can assign to buttons for automated navigation of the views.

Table 5.8. MultiView CommandNames

Field

Default CommandName

Description

NextViewCommandName

NextView

Navigates to the next higher ActiveViewIndex. If currently at the last view, sets ActiveViewIndex to -1, and nothing is displayed.

PreviousViewCommandName

PrevView

Navigates to the next lower ActiveViewIndex. If currently at the first view, sets ActiveViewIndex to -1, and nothing is displayed.

SwitchViewByIDCommandName

SwitchViewByID

Navigates to the View with the ID specified in the CommandArgument property.

SwitchViewByIndexCommandName

SwitchViewByIndex

Navigates to the View with the index specified. Can use the CommandArgument attribute to specify the index.

For example, a Button, ImageButton, or LinkButton with the CommandName NextView will automatically navigate the MultiView control to the next View when that button is clicked, with no additional code required. The developer (that's you) does not have to write a Click event handler for the button.

You can also set or retrieve the active view by calling the SetActiveView or GetActiveView method of the MultiView control. SetActiveView takes a reference to a View object as an argument, and GetActiveView returns a reference to a View object.

Tip

An important point to remember is that all the controls on all the Views, even those Views not currently visible, are available to the app and server-side processing. Not only are they available to code, but also they participate in View state and are part of the Controls collection of the page.

Every time a view is changed, the page is posted back to the server and a number of events are raised by both the MultiView and View controls.

Whenever the active view changes, the ActiveViewChanged event is raised by the MultiView control. At the same time, the View control that is now active raises the Activate event, and the View control that is now inactive raises the Deactivate event.

All of these events have an event argument of type EventArgs, which you will remember is a placeholder and provides no additional information about the event. However, as with all event handlers, a reference to the sender is passed to the event handler, as we will demonstrate shortly.

The View control has a Visible property of type Boolean, which can be set to control the visibility of specific View objects or retrieved to determine programmatically which View is visible.

Neither the MultiView nor the View control has any style properties. That is not too surprising for the MultiView, because it is just a container for the Views. In the case of the View control, if you want to impose style properties, you must apply them to each control contained within the View. Another technique would be to embed a Panel control in the View and set the style properties of the Panel control.

The following example, MultiViewDemo.aspx, demonstrates many of the features of the MultiView and View controls. The web page, after some navigation has occurred, is shown in Figure 5.10, "MultiViewDemo.aspx in action".

MultiViewDemo, shown in Design view in Figure 5.11, "MultiViewDemo in Design view", consists of a single web page with a MultiView control, MultiView1. MultiView1 contains four View controls (vwFirst, vwSecond, vwThird, and vwLast), along with other controls for navigating the MultiView. The page also contains controls for displaying data from and about the MultiView and its contained Views.

Figure 5.10. MultiViewDemo.aspx in action

MultiViewDemo.aspx in action

Each of the four Views contains buttons for navigation. In addition, the first two Views contain a TextBox for demonstrating how controls on a View are accessible to the application even when that View is not visible.

To create this example, add to the chapter's website a new web form called MultiViewDemo.aspx. The complete content file for this example is listed in Example 5.13, "Default.aspx for MultiViewDemo", with the MultiView and View controls highlighted. We will look first at the MultiView and View controls in this source file and then at the other controls on the page.

Figure 5.11. MultiViewDemo in Design view

MultiViewDemo in Design view

Example 5.13. Default.aspx for MultiViewDemo

<%@ Page Language="C#" AutoEventWireup="true"
   CodeFile="MultiViewDemo.aspx.cs" Inherits="MultiViewDemo" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
   <title>MultiView & View Demo</title>
</head>

<body>
   <form id="form1" runat="server">
   <div>
      <h1>MultiView & View Controls</h1>
      <br />

      <asp:RadioButtonList AutoPostBack="True" ID="rblView"
         OnSelectedIndexChanged="rblView_SelectedIndexChanged"
         RepeatDirection="Horizontal" runat="server">
         <asp:ListItem Value="-1">Nothing</asp:ListItem>
         <asp:ListItem Value="0" Selected="True">First</asp:ListItem>
         <asp:ListItem Value="1">Second</asp:ListItem>
         <asp:ListItem Value="2">Third</asp:ListItem>
         <asp:ListItem Value="3">Last</asp:ListItem>
      </asp:RadioButtonList>
      <br />
      Current Index:
      <asp:Label ID="lblCurrentIndex" runat="server"></asp:Label>
      <br />

      <asp:MultiView ID="MultiView1" runat="server"
         ActiveViewIndex="0"
         OnActiveViewChanged="MultiView1_ActiveViewChanged">

         <asp:View ID="vwFirst" runat="server"
            OnActivate="ActivateView" OnDeactivate="DeactivateView">
            <h2>First View</h2>
            <asp:TextBox ID="txtFirstView" runat="server" />
            <asp:Button CommandName="NextView" ID="btnNext1"
               runat="server" Text="Go To Next" />
            <asp:Button CommandArgument="vwLast"
               CommandName="SwitchViewByID" ID="btnLast"
               runat="server" Text="Go to Last" />
         </asp:View>

         <asp:View ID="vwSecond" runat="server"
            OnActivate="ActivateView" OnDeactivate="DeactivateView">
            <h2>Second View</h2>
            <asp:TextBox ID="txtSecondView" runat="server" />
            <asp:Button CommandName="NextView" ID="btnNext2"
               runat="server" Text="Go To Next" />
            <asp:Button CommandName="PrevView" ID="btnPrevious2"
               runat="server" Text="Go to Previous" />
         </asp:View>

         <asp:View ID="vwThird" runat="server"
            OnActivate="ActivateView" OnDeactivate="DeactivateView">
            <h2>Third View</h2>
            <asp:Button CommandName="NextView" ID="btnNext3"
               runat="server" Text="Go To Next" />
            <asp:Button CommandName="PrevView" ID="btnPrevious3"
               runat="server" Text="Go to Previous" />
         </asp:View>

         <asp:View ID="vwLast" runat="server"
            OnActivate="ActivateView" OnDeactivate="DeactivateView">
            <h2>Last View</h2>
            <asp:Button CommandName="PrevView" ID="btnPrevious4"
               runat="server" Text="Go to Previous" />
            <asp:Button CommandArgument="0" ID="btnFirst"
               CommandName="SwitchViewByIndex" runat="server"
               Text="Go to First" />
         </asp:View>
      </asp:MultiView>
      <br />
      <br />
      First TextBox:
      <asp:Label ID="lblFirstTextBox" runat="server" />
      <br />
      Second TextBox:
      <asp:Label ID="lblSecondTextBox" runat="server" />
      <br />
      <br />
      <strong>
         <span style="text-decoration: underline">
            View Activation History:
         </span>
      </strong>
      <br />
      <asp:Label ID="lblViewActivation" runat="server" />
   </div>
   </form>
</body>
</html>

The MultiView control highlighted in Example 5.13, "Default.aspx for MultiViewDemo" is declared with an ID of MultiView1. The ActiveViewIndex attribute is set to 0, so the first view in the Views collection will display. If this property is not set, it will default to -1 and none of the View controls will display.

The OnActiveViewChanged attribute binds an event handler method in the code-behind file, MultiView1_ActiveViewChanged (listed in Example 5.14, "ActiveViewChanged event handler in MultiViewDemo.aspx.cs"), which will fire every time the active index changes.

Example 5.14. ActiveViewChanged event handler in MultiViewDemo.aspx.cs

protected void MultiView1_ActiveViewChanged(object sender, EventArgs e)
{
   lblFirstTextBox.Text = txtFirstView.Text;
   lblSecondTextBox.Text = txtSecondView.Text;
   rblView.SelectedIndex = MultiView1.ActiveViewIndex + 1;
}

This event handler does two things. First, it retrieves the values of the two text boxes on the first two Views and displays them on the page in the labels that were placed there for that purpose. Second, it sets the RadioButtonList at the top of the page, rblView, to the proper value. This code is relatively straightforward because the RadioButtonList.SelectedIndex and the MultiView.ActiveViewIndex properties are of type integer. However, you must compensate for the fact that the lowest value of the ActiveViewIndex property is -1 whereas the lowest index of the RadioButtonList is 0. That is, they are offset by one.

Four View instances are declared: vwFirst, vwSecond, vwThird, and vwLast. All of them declare event handlers for both OnActivate and OnDeactivate:

<asp:View ID="vwThird" runat="server"
   OnActivate="ActivateView" OnDeactivate="DeactivateView">

The Activate event is handled by a method called ActivateView and the Deactivate event by DeactivateView, as listed in Example 5.15, "Activate and Deactivate event handlers for MultiView.aspx.cs". They retrieve the contents of the Label control holding the activation history, append the ID and action of the current View, and assign the string back to the Label.

Example 5.15. Activate and Deactivate event handlers for MultiView.aspx.cs

protected void ActivateView(object sender, EventArgs e)
{
   string str = lblViewActivation.Text;
   View v = (View)sender;
   str += "View " + v.ID + " activated <br/>";
   lblViewActivation.Text = str;
}

protected void DeactivateView(object sender, EventArgs e)
{
   string str = lblViewActivation.Text;
   View v = (View)sender;
   str += "View " + v.ID + " deactivated <br/>";
   lblViewActivation.Text = str;
}

Because sender is of type object, it must first be cast to type View before assigning it to a reference to View and obtaining properties of the View object, such as the ID property.

Each View object contains navigation buttons. The .NET Framework makes it particularly easy to set them up. (It's harder to explain in words than to actually do it.) If a button has its CommandName attribute set to one of the default values (Sort, Select, Update, Insert, Delete), the corresponding action will automatically occur. So, the first View object, vwFirst, will have buttons labeled "Go to Next" and "Go to Last", with CommandNames of NextView and SwitchViewByID, respectively. The first is easy; it just navigates to the next View.

The SwitchViewByID action requires an ID to switch to, so the CommandArgument attribute is used to pass in that argument:

<asp:Button ID="btnLast" runat="server" Text="Go to Last"
   CommandArgument="vwLast" CommandName="SwitchViewByID" />

Each View control contains content in addition to navigation buttons. This content includes standard HTML elements and ASP.NET server controls.

Run the page now and you'll see that these server controls are accessible to the application even when their View is not displayed.

The next step is to update the RadioButtonList at the top of the page. As you have seen, the SelectedIndex property is set by the code in MultiView1_ActiveViewChanged every time the active view changes. When the SelectedIndex changes due to user action the SelectedIndexChanged event is fired, which calls rblView_SelectedIndexChanged:

<asp:RadioButtonList AutoPostBack="True" ID="rblView"
   OnSelectedIndexChanged="rblView_SelectedIndexChanged"
   RepeatDirection="Horizontal" runat="server">

This method, listed in Example 5.16, "SelectedIndexChanged event handler for MultiViewDemo.aspx.cs", consists of a single line that sets the ActiveViewIndex property of the MultiView to the SelectedValue of the RadioButtonList.

Example 5.16. SelectedIndexChanged event handler for MultiViewDemo.aspx.cs

protected void rblView_SelectedIndexChanged(object sender, EventArgs e)
{
   MultiView1.ActiveViewIndex =
      Convert.ToInt32(rblView.SelectedValue);
}

The final bit of code is contained in the event handler for the PreRender event of the Page (listed in Example 5.17, "Page_PreRender event handler for MultiViewDemo.aspx.cs"), where lblCurrentIndex is populated with the index of the currently active view. This code cannot be run in Page_Load because Page_Load is called before the label is updated.

Tip

See Chapter 6, Website Fundamentals for complete coverage of the Page class life cycle.

Example 5.17. Page_PreRender event handler for MultiViewDemo.aspx.cs

protected void Page_PreRender(object sender, EventArgs e)
{
   lblCurrentIndex.Text = MultiView1.ActiveViewIndex.ToString( );
}

Now that you've seen how to use the MultiView class to create a simple paged application, you can expand it to more complex uses. Sometimes, though, you want a traditional Windows-style wizard on your page. In that case, you can use the Wizard control, discussed next.

The Wizard Control

Users expect modern applications to provide wizards to walk them through multistep processes. These UIs are distinguished by the use of magic. Sorry, just kidding.

Wizard controls provide the infrastructure to present the user with successive steps in a process, providing access to all the data collected in all the steps, with easy forward and backward navigation.

Similar to the MultiView control, the Wizard control contains a collection of WizardStep objects. These WizardStep objects derive from the View class, as you can see in Figure 5.12, "View class hierarchy", and the relationship between WizardSteps and the Wizard control is analogous to the relationship between the View and MultiView.

Figure 5.12. View class hierarchy

View class hierarchy

As with the MultiView control, all of the controls on all of the WizardStep controls are part of the page's control hierarchy and are accessible via code at runtime regardless of which specific WizardStep is currently visible. Every time a user clicks on a navigation button or link, the page posts back to the server. It will not, however, post onward to another page, a process known as cross-page posting, which we'll look at more in Chapter 6, Website Fundamentals.

The Wizard control takes care of all the plumbing required to implement navigation, both linear (going from one step to the next or back) and nonlinear (going from any step to any other step). It automatically creates the appropriate buttons, such as Next, Previous, and Finish (on the very last step). The first step does not have a Previous button and the last step does not have a Next button. It also makes provisions for steps that can be navigated to only a single time. In addition, by default, the Wizard displays a toolbar with navigation links, enabling the user to go to any step from any other step.

You can customize almost every aspect of the look and feel of the Wizard with styles and templates. This includes all the various buttons and links, the header and footer, the sidebar, and the WizardStep controls.

The best way to explore the Wizard control is to look at an example. In this example, you will create a whimsical wizard to guide you through the steps you follow when waking up in the morning.

Create for the chapter's website a new web page called WizardDemo.aspx. Drag a Wizard control onto the page. This creates a default two-step wizard, which, though sparse, is fully functional. In Design view, you will see the two sidebar links, the first step, and the Next button, as shown in Figure 5.13, "A fresh Wizard control in Design view and its smart tag" with its smart tag.

Looking at the Source view of the content page, you will see the Wizard declaration looks like the following:

<asp:Wizard ID="Wizard1" runat="server">
   <WizardSteps>
      <asp:WizardStep runat="server" title="Step 1">
      </asp:WizardStep>
      <asp:WizardStep runat="server" title="Step 2">
      </asp:WizardStep>
   </WizardSteps>
</asp:Wizard>

Within the <asp:Wizard> tags are a pair of <WizardSteps> tags. The WizardStep controls are declared within those tags.

Run this page and you will see the web page shown in Figure 5.14, "A fresh Wizard control in action". Because this is the first step, only the Next button is displayed, but the sidebar shows links for both steps.

Figure 5.13. A fresh Wizard control in Design view and its smart tag

A fresh Wizard control in Design view and its smart tag

Let's put a bit more meat on this example's bones. Click the content area of the first WizardStep and type in some text-for example, an <h2> heading that says "Wake Up," as shown in Figure 5.15, "Adding content to a Wizard step".

Next, switch to Design view, open the smart tag of the Wizard control, as seen in Figure 5.13, "A fresh Wizard control in Design view and its smart tag", then select Add/Remove WizardSteps. This will bring up the WizardStep Collection Editor, as shown in Figure 5.16, "WizardStep Collection Editor". Add five more steps so there are a total of seven. For each (including the first two), enter a value for the Title and the ID, as listed in Table 5.9, "WizardSteps for WizardDemo".

Figure 5.14. A fresh Wizard control in action

A fresh Wizard control in action

Figure 5.15. Adding content to a Wizard step

Adding content to a Wizard step

Figure 5.16. WizardStep Collection Editor

WizardStep Collection Editor

Table 5.9. WizardSteps for WizardDemo

ID

Title

stpWakeUp

Step 1

stpShower

Step 2

stpTakeMeds

Step 3

stpBrushTeeth

Step 4

stpGetDressed

Step 5

stpEatBreakfast

Step 6

stpFinish

Step 7

Select each step in turn by clicking "Step 2", "Step 3", and so on, and add content to each step as you did with Step 1. You can also use the drop-down list in the smart tag to switch between steps, but it is easier to switch to Source view and edit the WizardStep declarations directly. When you are done, the Wizard control declaration should look something like that shown in Example 5.18, "Wizard declaration after adding steps".

Example 5.18. Wizard declaration after adding steps

<asp:Wizard ID="Wizard1" runat="server" ActiveStepIndex="1">
   <WizardSteps>
      <asp:WizardStep ID="stpWakeUp" runat="server" title="Step 1">
         <h2>Wake Up</h2>
      </asp:WizardStep>
      <asp:WizardStep ID="stpShower" runat="server" title="Step 2">
         <h2>Shower</h2>
      </asp:WizardStep>
      <asp:WizardStep ID="stpTakeMeds" runat="server" Title="Step 3">
         <h2>Take Medicine</h2>
      </asp:WizardStep>
      <asp:WizardStep ID="stpBrushTeeth" runat="server" Title="Step 4">
         <h2>Brush Teeth</h2>
      </asp:WizardStep>
      <asp:WizardStep ID="stpGetDressed" runat="server" Title="Step 5">
         <h2>Get Dressed</h2>
      </asp:WizardStep>
      <asp:WizardStep ID="stpEatBreakfast" runat="server" Title="Step 6">
         <h2>Eat Breakfast</h2>
      </asp:WizardStep>
      <asp:WizardStep ID="stpFinish" runat="server" Title="Step 7">
         <h2>Out The Door</h2>
      </asp:WizardStep>
   </WizardSteps>
</asp:Wizard>

The Wizard control has many properties for controlling appearance and behavior. Table 5.10, "Wizard properties not related to style or button display" lists some of the most important Wizard properties other than those relating to the appearance of the buttons. The button-related properties are listed in Table 5.11, "Wizard properties related to button displays". You'll see many of these properties as you continue with WizardDemo.aspx.

Table 5.10. Wizard properties not related to style or button display

Name

Type

Description

ActiveStep

WizardStepBase

Read-only. The currently displayed step in the WizardSteps collection.

ActiveStepIndex

Integer

The zero-based index of the currently displayed step in the WizardSteps collection.

CancelDestinationPageUrl

String

The URL the user navigates to when clicking the Cancel button.

CellPadding

Integer

The number of pixels between the cell's contents and border. Default is 0.

CellSpacing

Integer

The number of pixels betweens cells. Default is 0.

DisplayCancelButton

Boolean

If true, a Cancel button will be displayed. Default is false.

DisplaySideBar

Boolean

If true, the default, the sidebar area will be displayed.

FinishDestinationPageUrl

String

The URL the user navigates to when clicking the Finish button.

FinishNavigationTemplate

ITemplate

The template used to specify content and styles for the navigation area of the Finish step, either the last step or the step with StepType = Finish.

HeaderStyle

TableItemStyle

Read-only. Style properties for the header area.

HeaderTemplate

ITemplate

The template used to specify content and styles for the header area displayed at the top of every step.

HeaderText

String

Text displayed in the header area.

NavigationButtonStyle

Style

Read-only. The style properties that specify the appearance of the buttons in the navigation area.

NavigationStyle

TableItemStyle

Read-only. The style properties for the navigation area.

SideBarButtonStyle

Style

Read-only. The style properties that specify the appearance of the buttons in the sidebar area.

SideBarStyle

TableItemStyle

Read-only. The style properties for the sidebar area.

SideBarTemplate

ITemplate

The template used to specify content and styles for the sidebar area.

SkipLinkText

String

Rendered as alternative text with an invisible image to work with assistive technologies. Default is "Skip Navigation Links", localized for server locale.

StartNavigationTemplate

ITemplate

The template used to specify content and styles for the navigation area of the Start step, either the first step or the step with StepType = Start.

StepNavigationTemplate

ITemplate

The template used to specify content and styles for the navigation area of all the steps other than Start, Finish, and Complete.

StepStyle

TableItemStyle

Read-only. The style properties for the WizardStep objects.

WizardSteps

WizardStepCollection

Read-only. A collection of WizardStep objects.

Table 5.11. Wizard properties related to button displays

Property

Type

Description

CancelButtonImageUrl
FinishStepButtonImageUrl
FinishStepPreviousButton-ImageUrl
NextStepButtonImageUrl
PreviousStepButtonImageUrl
StartStepNextButtonImageUrl

String

The URL of the image displayed for the button.

CancelButtonStyle
FinishStepButtonStyle
FinishStepPreviousButton-Style
NextStepButtonStyle
PreviousStepButtonStyle
StartStepNextButtonStyle

Style

Read-only. The style properties that specify the appearance of the button.

CancelButtonText
FinishStepButtonText
FinishStepPreviousButton-Text
NextStepButtonText
PreviousStepButtonText
StartStepNextButtonText

String

The text displayed on the button.

CancelButtonType
FinishStepButtonType
FinishStepPreviousButton-Type
NextStepButtonType
PreviousStepButtonType
StartStepNextButtonType

ButtonType

The type of button rendered as the button. Possible values are Button, Image, and Link.

Many of the properties are of type TableItemStyle. This class, which derives from System.Web.UI.WebControls.Style, contains properties used to format the table rows and cells that make up the Wizard control. The TableItemStyle class has many properties, including BackColor, BorderColor, BorderStyle, BorderWidth, CssClass, Font, ForeColor, Height, HorizontalAlign, VerticalAlign, Width, and Wrap.

When you are setting the properties of a Wizard control in VS2008 in Design view, the properties that are of type TableItemStyle appear in the Properties window with a plus sign next to them. Clicking the plus sign expands the list to display the TableItemStyle properties as subproperties, as seen in Figure 5.17, "TableItemStyle type properties in Design view". Properties set in this manner will be contained in separate elements within the Wizard control declaration in the content file, as in the highlighted code in the following snippet:

<asp:Wizard ID="Wizard1" runat="server" ActiveStepIndex="1">
   <WizardSteps>
      ... all seven wizard steps ...
   </WizardSteps>

   <HeaderStyle BackColor="#666666" BorderColor="#E6E2D8"
      BorderStyle="Solid" BorderWidth="2px"
      Font-Size="0.9em" ForeColor="White"
      HorizontalAlign="Center" Font-Bold="True" />
/asp:Wizard>

When working in Source view, you can also add the TableItemStyle type properties directly to the Wizard control declaration. For example, to add those properties set in Figure 5.16, "WizardStep Collection Editor", you would add the following:

<asp:Wizard ID="Wizard1" runat="server" ActiveStepIndex="1"
   HeaderStyle-backcolor="#666666" HeaderStyle-bordercolor="#E6E2D8"
   HeaderStyle-borderstyle="Solid" HeaderStyle-borderwidth="2px"
   HeaderStyle-font-size="0.9em" HeaderStyle-forecolor="White"
   HeaderStyle-HorizontalAlign="Center"
   HeaderStyle-font-bold="True">
   <WizardSteps>
      ... all seven wizard steps ...
   </WizardSteps>
</asp:Wizard>

Figure 5.17. TableItemStyle type properties in Design view

TableItemStyle type properties in Design view

The WizardStep class has a StepType property, which has one of the values of the WizardStepType enumeration listed in Table 5.13, "Wizard control events". By default, the StepType is Auto, in which the navigation UI is determined by the order of the steps in the WizardSteps collection. The first step has only a Next button, the last step has only a Previous button, and all the other steps of StepType Auto have both Previous and Next buttons.

Alternatively, you can assign a different value to the StepType property to modify the default behavior, as described in Table 5.12, "WizardStepType enumeration members". For example, you can create a confirmation page, with no navigation buttons at all, by changing the StepType to Complete.

Table 5.12. WizardStepType enumeration members

Member

Description

Auto

Navigation UI determined automatically by the order in which the step is declared. The default value.

Complete

The last step to display. No navigation buttons are rendered; all sidebar links will be rendered as plain text.

Finish

The final data collection step. Renders only Finish and Previous buttons.

Start

The first step. Renders only a Next button.

Step

Any step other than Start, Finish, or Complete. Renders Previous and Next buttons.

The WizardStep class has one additional property of particular interest: AllowReturn. This property enforces linear navigation, meaning that all steps in the wizard must be visited in order, which is handy if all steps must be completed and information and options in subsequent steps depend on the choices made in previous steps.

It is impossible to navigate to a step more than once with AllowReturn set to false. If the DisplaySideBar property is true (the default) so that the sidebar is displayed, any step with AllowReturn set to false will still display in the navigation links, but clicking that link will have no effect.

Tip

The AllowReturn property only disallows user interaction; program code can force a return to a step even if the AllowReturn property is false.

The Wizard control has six events, listed in Table 5.13, "Wizard control events". One is the ActiveStepChanged event, which is raised when the current step has changed. The other five events are all raised in response to button clicks. As noted in Table 5.13, "Wizard control events", all the button click events other than CancelButtonClick have an event argument of type WizardNavigationEventArgs, which exposes three properties:

  • Cancel
    Set this Boolean value to true if the navigation to the next step should be canceled. The default is false.

  • CurrentStepIndex
    The zero-based integer index of the current step in the WizardSteps collection.

  • NextStepIndex
    The zero-based integer index of the step that will display next. If the Previous button has been clicked, for example, the value of NextStepIndex will be one less than the CurrentStepIndex.

Table 5.13. Wizard control events

Event

Event argument

Description

ActiveStepChanged

EventArgs

Raised when a new step is displayed

CancelButtonClick

EventArgs

Raised when the Cancel button is clicked

FinishButtonClick

WizardNavigationEventArgs

Raised when the Finish button is clicked

NextButtonClick

WizardNavigationEventArgs

Raised when the Next button is clicked

PreviousButtonClick

WizardNavigationEventArgs

Raised when the Previous button is clicked

SideBarButtonClick

WizardNavigationEventArgs

Raised when a sidebar button is clicked

The Wizard control has three methods of particular interest, listed in Table 5.14, "Wizard methods".

Table 5.14. Wizard methods

Method name

Return type

Description

GetHistory

ICollection

Returns a collection of WizardStepBase objects in the order they were accessed, where index 0 is the most recent step visited, index 1 is the step before that, and so on. (It is essentially like a browser history list, but for steps in a wizard.)

GetStepType

WizardStepType

The type of step, as listed in Table 5.12, "WizardStepType enumeration members".

MoveTo

Void

Moves to the WizardStep object passed in as a parameter.

Now, let's return to the WizardDemo.aspx example, where you will apply many of the properties, methods, and events we just listed and discussed.

First, add a bit of text to each WizardStep. The content of each step can include text and HTML, other ASP.NET server controls, and user controls (covered in Chapter 15, Custom and User Controls), enabling the easy reuse of UI and code.

Next, set the StepType of Step 1 to Start and of Step 7 to Finish. Set the AllowReturn property of Step 3 to False, so you can access that step only one time. Finally, add one additional WizardStep to the WizardSteps collection, with a StepType of Complete. The new <WizardSteps> section of the Wizard declaration will now look like Example 5.19, "A final set of WizardSteps for WizardDemo.aspx", with the modified code highlighted (except for the added text content).

Example 5.19. A final set of WizardSteps for WizardDemo.aspx

<WizardSteps>
   <asp:WizardStep ID="stpWakeUp" runat="server" title="Step 1"
      StepType="Start">
      <h2>Wake Up</h2>
      Rise and shine sleepy head.
   </asp:WizardStep>

   <asp:WizardStep ID="stpShower" runat="server" title="Step 2">
      <h2>Shower</h2>
      Make it cold!
   </asp:WizardStep>

   <asp:WizardStep ID="stpTakeMeds" runat="server" Title="Step 3"
      AllowReturn="false">
      <h2>Take Medicine</h2>
      Only do this once.
   </asp:WizardStep>

   <asp:WizardStep ID="stpBrushTeeth" runat="server" Title="Step 4">
      <h2>Brush Teeth</h2>
      Don't forget to floss.
   </asp:WizardStep>

   <asp:WizardStep ID="stpGetDressed" runat="server" Title="Step 5">
      <h2>Get Dressed</h2>
      Got to look good.
   </asp:WizardStep>

   <asp:WizardStep ID="stpEatBreakfast" runat="server" Title="Step 6">
      <h2>Eat Breakfast</h2>
      The most important meal of the day.
   </asp:WizardStep>

   <asp:WizardStep ID="stpFinish" runat="server" Title="Step 7"
      StepType="Finish">
      <h2>Out The Door</h2>
      Meet the world!
   </asp:WizardStep>

   <asp:WizardStep ID="stpComplete" runat="server"
      Title="Complete" StepType="Complete">
      <h2>Complete!</h2>
      Your morning routine is now complete.
   </asp:WizardStep>
</WizardSteps>

Next, add a drop down along with several labels to the page below the Wizard control. The labels will be used to display various types of information and the drop down will be used to demonstrate how step navigation can occur programmatically outside the Wizard control. The code snippet from the content file shown in Example 5.20, "Additional controls for WizardDemo.aspx" declares these additional controls.

Example 5.20. Additional controls for WizardDemo.aspx

<br />
Select a step:&nbsp;
<asp:DropDownList ID="DropDownList1" runat="server"
   AutoPostBack="True"
   OnSelectedIndexChanged="DropDownList1_SelectedIndexChanged">
   <asp:ListItem>1</asp:ListItem>
   <asp:ListItem>2</asp:ListItem>
   <asp:ListItem>3</asp:ListItem>
   <asp:ListItem>4</asp:ListItem>
   <asp:ListItem>5</asp:ListItem>
   <asp:ListItem>6</asp:ListItem>
   <asp:ListItem>7</asp:ListItem>
</asp:DropDownList>
<p>
   Active Step:&nbsp;
   <asp:Label ID="lblActiveStep" runat="server" />
</p>
<p>
   ActiveStepIndex:&nbsp;
   <asp:Label ID="lblActiveStepIndex" runat="server" />
</p>
<p>
   StepType:&nbsp;
   <asp:Label ID="lblStepType" runat="server" />
</p>
<p>
   Button Info:&nbsp;
   <asp:Label ID="lblButtonInfo" runat="server" />
</p>
<p>
   <u>History</u>
</p>
<p>
   <asp:Label ID="lblHistory" runat="server" />
</p>

Go back to Design view, click the smart tag of the Wizard control, and select Auto Format. Select one of the format schemes presented; in this example, we use Simple. This will automatically apply a number of formatting properties, as you will see momentarily.

In the Properties window for the Wizard control, set the DisplayCancelButton property to true.

While in Design view with the Wizard control selected, click the Events icon (the lightning bolt) in the Properties window. Double-click the cell next to ActiveStepChanged to insert an event handler with the default name (Wizard1_ActiveStepChanged) for that event. Do the same for the CancelButtonClick event. For the FinishButtonClick event, enter the name Button_Click, which will insert a skeleton event handler with that name in the code-behind file. You should also enter Button_Click for each of the NextButtonClick, PreviousButtonClick, and SideBarButtonClick events. Finally, double-click the DropDownList control under the Wizard control to generate an empty handler for its SelectedIndexChanged event.

Now add the highlighted code from Example 5.21, "WizardDemo.aspx.cs" to WizardDemo.aspx.cs.

Example 5.21. WizardDemo.aspx.cs

using System;
using System.Collections;
using System.Web.UI;
using System.Web.UI.WebControls;

public partial class WizardDemo : Page
{
   protected void Wizard1_ActiveStepChanged
      (object sender, EventArgs e)
   {
      lblActiveStep.Text = Wizard1.ActiveStep.Title;
      lblActiveStepIndex.Text = Wizard1.ActiveStepIndex.ToString( );
      lblStepType.Text = Wizard1.ActiveStep.StepType.ToString( );

      //  get the history
      ICollection steps = Wizard1.GetHistory( );
      string str = ";
      foreach (WizardStep step in steps)
      {
         str += step.Title + "<br/>";
      }
      lblHistory.Text = str;
   }

   protected void Wizard1_CancelButtonClick
      (object sender, EventArgs e)
   {
      lblActiveStep.Text = ";
      lblActiveStepIndex.Text = ";
      lblStepType.Text = ";
      lblButtonInfo.Text = "Canceled";
      Wizard1.Visible = false;
   }

   protected void Button_Click
      (object sender, WizardNavigationEventArgs e)
   {
      string str = "Current Index: " +
         e.CurrentStepIndex.ToString( ) +
         ".   Next Step: " + e.NextStepIndex.ToString( );
      lblButtonInfo.Text = str;
   }

   protected void DropDownList1_SelectedIndexChanged
      (object sender, EventArgs e)
   {
      int index = DropDownList1.SelectedIndex;
      WizardStepBase step = Wizard1.WizardSteps[index];
      Wizard1.MoveTo(step);
   }
}

The reference to the System.Collections namespace is needed so you can use the ICollection object returned by the GetHistory method of the Wizard control.

The ActiveStepChanged event handler, Wizard1_ActiveStepChanged, is fired every time the current step changes, whether through user interaction or programmatically. This method gathers three pieces of information, populates the labels, and displays a history of the steps accessed.

The first label displays the currently active step. The ActiveStep property of the Wizard control returns a WizardStep object. The Title property of that object gives you the information you want. The second label is filled with the ActiveStepIndex property value. Because it is of type Integer, it must be converted to a string. The third label displays the StepType property of the WizardStep class, which is of type WizardStepType and therefore must be converted to a string for assignment to the Text property of the TextBox.

Second, the Wizard1_ActiveStepChanged method calls the GetHistory method of the Wizard class, which returns a collection of WizardStep objects (strictly speaking, it returns a collection of WizardStepBase objects, from which WizardStep derives). The collection is iterated and the Title property of each step is appended to a text string, which is then assigned to the lblHistory label. The most recent step accessed has an index of 0; the previous step has an index of 1, and so on.

All the buttons and links, other than the Cancel button, use the same event handler method: Button_Click. This method fills lblButtonInfo with the current-step index and the next-step index, both of which are properties of the event argument.

The Cancel button's Click event handler, Wizard1_CancelButtonClick, clears all the labels and hides the Wizard control.

The DropDownList control on this page lets the user move to any of the WizardSteps. When one of its values is selected, the SelectedIndexChanged handler for the control gets the new value for its SelectedIndex property. This value is used as an index into the WizardSteps collection to get a reference to the desired WizardStep object (actually a WizardStepBase object, from which WizardStep is derived). Then the MoveTo method of the Wizard is called to move programmatically to that step. The really interesting thing here is that it is possible to move multiple times to a step, such as Step 3, even if that step's AllowReturn property is set to false.

As a result of setting the event handlers and the Auto Formatting, the Wizard declaration will now look something like that shown in Example 5.22, "Wizard declaration after adding event handlers and formatting".

Example 5.22. Wizard declaration after adding event handlers and formatting

<asp:Wizard ID="Wizard1" runat="server" ActiveStepIndex="1"
   BackColor="#E6E2D8" BorderColor="#999999" BorderStyle="Solid"
   BorderWidth="1px" DisplayCancelButton="True"
   Font-Names="Verdana" Font-Size="0.8em"
   onactivestepchanged="Wizard1_ActiveStepChanged"
   oncancelbuttonclick="Wizard1_CancelButtonClick"
   onfinishbuttonclick="Button_Click"
   onnextbuttonclick="Button_Click"
   onpreviousbuttonclick="Button_Click"
   onsidebarbuttonclick="Button_Click">

   <stepstyle backcolor="#F7F6F3" bordercolor="#E6E2D8"
      borderstyle="Solid" borderwidth="2px" />

   <WizardSteps>
      ... as given in Figure 5.18, "WizardDemo.aspx after some navigation" ...
   </WizardSteps>

   <sidebarbuttonstyle forecolor="White" />
   <navigationbuttonstyle backcolor="White" bordercolor="#C5BBAF"
      borderstyle="Solid" borderwidth="1px" font-names="Verdana"
      font-size="0.8em" forecolor="#1C5E55" />
   <sidebarstyle backcolor="#1C5E55" font-size="0.9em"
      verticalalign="Top" />
   <HeaderStyle BackColor="#666666" BorderColor="#E6E2D8"
      BorderStyle="Solid" BorderWidth="2px"
      Font-Size="0.9em" ForeColor="White"
      HorizontalAlign="Center" Font-Bold="True" />
</asp:Wizard>

Running the web page and navigating through several of the steps will yield something similar to Figure 5.18, "WizardDemo.aspx after some navigation".

The Wizard control is a fairly well-defined control, with obvious uses. You could walk your user through entering his preferences or setting up a stock sale. Anytime you want a clearly defined series of steps for the user to follow, the Wizard control is useful.

The FileUpload Control

Often an application needs to allow users to upload files to the web server. The FileUpload control makes it easy for the user to browse for and select the file to transfer, providing a Browse button and a text box for entering the filename. Once the user has entered a fully qualified filename in the text box, either by typing it directly or by using the Browse button, the SaveAs method of the FileUpload control can be called to save the file to disk.

In addition to the normal complement of members inherited from the WebControl class, the FileUpload control also exposes several read-only properties of particular interest, listed in Tables Table 5.15, "FileUpload properties (read-only)" and Table 5.16, "HttpPostedFile properties (read-only)".

Figure 5.18. WizardDemo.aspx after some navigation

WizardDemo.aspx after some navigation

Table 5.15. FileUpload properties (read-only)

Name

Type

Description

FileContent

Stream

Returns a Stream object that points to the file to upload.

FileName

String

Returns the name of the file to be uploaded, without any qualifying path information.

HasFile

Boolean

If true, indicates the control has a file to upload.

PostedFile

HttpPostedFile

Returns a reference to the file which has been uploaded. Exposes the read-only properties listed in Table 5.16, "HttpPostedFile properties (read-only)".

Table 5.16. HttpPostedFile properties (read-only)

Name

Type

Description

ContentLength

Integer

Returns the size of the file, in bytes, of an uploaded file.

ContentType

String

Returns the MIME content type of the uploaded file (e.g., text\html, text\css, or text\javascript).

FileName

String

Returns the fully qualified filename on the client computer.

InputStream

Stream

Returns a Stream object that points to the uploaded file.

We will demonstrate all of these properties in the following example.

To see a FileUpload control in action, add to the chapter's website a new web page called FileUploadDemo.aspx. Drag a FileUpload control onto the page. Add two ASP.NET Button controls, with Text properties set to "Save" and "Display", and ID properties set to btnSave and btnDisplay, respectively. Add two Label controls with IDs set to lblMessage and lblDisplay. Sprinkle a few <br/> HTML elements to space things out. Switch to Design view and double-click each button to create Click event handlers for each button in the code-behind file. When you are done, the content file should look something like Example 5.23, "FileUploadDemo.aspx".

Example 5.23. FileUploadDemo.aspx

<%@ Page Language="C#" AutoEventWireup="true"
   CodeFile="FileUploadDemo.aspx.cs" Inherits="FileUploadDemo" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
   <title>FileUpload Control Demo</title>
</head>

<body>
   <form id="form1" runat="server">
   <div>
      <h1>FileUpload Control</h1>
      <asp:FileUpload ID="FileUpload1" runat="server" />
      <br />
      <asp:Button ID="btnSave" runat="server"
         Text="Save" OnClick="btnSave_Click" />
      <asp:Button ID="btnDisplay" runat="server"
         Text="Display" OnClick="btnDisplay_Click" />
      <br />
      <br />
      <asp:Label ID="lblMessage" runat="server" />
      <asp:Label ID="lblDisplay" runat="server" />
   </div>
   </form>
</body>
</html>

In the code-behind file, add the highlighted code from Example 5.24, "FileUploadDemo.aspx.cs". Note that you'll need to change <c:\\SaveDirectory> to the directory to which you want your files saved.

Example 5.24. FileUploadDemo.aspx.cs

using System;
using System.IO;     //needed for StringBuilder class
using System.Text;   //needed for Stream class
using System.Web.UI;

public partial class FileUploadDemo : Page
{
   protected void btnSave_Click(object sender, EventArgs e)
   {
      StringBuilder str = new StringBuilder( );
      if (FileUpload1.HasFile)
      {
         try
         {
            str.AppendFormat("Uploading file: {0}", FileUpload1.FileName);

            //  Save the file
            FileUpload1.SaveAs("<c:\\SaveDirectory>"
               + FileUpload1.FileName);

            //  show info about the file
            str.AppendFormat("<br/>Saved As: {0}",
               FileUpload1.PostedFile.FileName);
            str.AppendFormat("<br/>File Type: {0}",
               FileUpload1.PostedFile.ContentType);
            str.AppendFormat("<br/>File Length (bytes): {0}",
               FileUpload1.PostedFile.ContentLength);
            str.AppendFormat("<br/>PostedFile File Name: {0}",
               FileUpload1.PostedFile.FileName);
         }
         catch (Exception ex)
         {
            str.Append("<br/><b>Error</b><br/>");
            str.AppendFormat(
               "Unable to save <c:\\SaveDirectory>{0}<br />{1}",
               FileUpload1.FileName, ex.Message);
            }
        }
        else
        {
            lblMessage.Text = "No file uploaded.";
        }
        lblMessage.Text = str.ToString( );
        lblDisplay.Text = ";
   }

   protected void btnDisplay_Click(object sender, EventArgs e)
   {
      StringBuilder str = new StringBuilder( );
      str.AppendFormat("<u>File:  {0}</u><br/>", FileUpload1.FileName);
      if (FileUpload1.HasFile)
      {
         try
         {
            Stream stream = FileUpload1.FileContent;
            StreamReader reader = new StreamReader(stream);
            string strLine = ";
            do
            {
               strLine = reader.ReadLine( );
               str.Append(strLine);
            } while (strLine != null);
         }
         catch (Exception ex)
         {
            str.Append ("<br/><b>Error</b><br/>");
            str.AppendFormat("Unable to display {0}<br/>{1}",
                 FileUpload1.FileName, ex.Message);
         }
      }
      else
      {
         lblDisplay.Text = "No file uploaded.";
      }
      lblDisplay.Text = str.ToString( );
      lblMessage.Text = ";
   }
}

In btnSave_Click, the event handler for the Save button, the HasFile property of the FileUpload control is used to test whether a valid, fully qualified filename is entered in the control text box. If the text box is blank or the filename entered is not a valid file, this test will fail, and lblMessage will display "No file uploaded.".

Assuming there is a valid file to upload, the code in the try block is executed. The key statement here calls the SaveAs method of the FileUpload control, using a hardcoded path along with the FileName property to pass in a fully qualified filename. This statement may fail for any number of reasons, including insufficient disk space, an invalid path, or security issues (more on that in a moment).

If the SaveAs fails, the catch block will come into play, displaying an error message in lblMessage, including ex.Message, the Exception Message property.

Tip

It's not a security best practice to echo exception messages to the user, because they can reveal sensitive information, but it suffices for this demo. Usually you would log this exception in the event log, in a database table, or in an email to the web administrator for further review.

If the SaveAs is successful, a number of pieces of information about the uploaded file are displayed in lblMessage, retrieved from properties of the FileUpload.PostedFile property (which is of type HttpPostedFile).

After saving a file, the page will look something like that shown in Figure 5.19, "FileUploadDemo after saving a file".

Figure 5.19. FileUploadDemo after saving a file

FileUploadDemo after saving a file

The event handler for the Display button's Click event is similar, except that instead of displaying information about the file, it displays the contents of the file itself. It does this by using the FileContent property to retrieve the contents of the uploaded file as a Stream object, which is then used to instantiate a StreamReader object. The ReadLine method of the StreamReader class is then used to step through the file, line by line, concatenating the lines to display in lblDisplay.

After displaying a file, in this case a text file containing a blog post, the page will look something like that shown in Figure 5.20, "FileUploadDemo displaying a file".

Whenever you talk about uploading a file to a web server from clients, security is a big concern. There are two considerations. First, opening your web server in this way can present a huge security hole, and you should do it only with care and careful consideration. Not only can uploaded files contain viruses, Trojan horses, and other malicious software, but it would be dangerous to allow the client to browse the directory structure of the web server itself. For that reason, you will almost always want to either hardcode the target directory or at least severely circumscribe where the uploaded files can be saved.

Figure 5.20. FileUploadDemo displaying a file

FileUploadDemo displaying a file

The other consideration is the permissions necessary to allow a process to write a file to disk. When developing a web application, the development computer typically runs its own web server, especially when using the default mode of VS2008, in which the ASP.NET Development Server is used and the access to the website is via the filesystem and in the security of the user running VS2008. In this situation, you will probably never run into any file permission problems.

However, when the website is deployed to a production web server and the website is accessed via Internet Information Services (IIS) and a virtual directory, problems will arise. This is because the account under which ASP.NET runs must have write permission for the directory in which the uploaded files are to be saved. In Windows 2000/XP, this account is named ASPNET. In Windows Server 2003, it's called NETWORK SERVICE.

With a FileUpload control and good security precautions in place (e.g., checking the file's MIME types and file extensions), your users will be able to send their own files to your site, increasing your site's versatility.

Tip

ASP.NET sets a default maximum size (4 MB) for files that the FileUpload control can upload. You can set this up to a maximum of 2 GB by setting the maxRequestLength attribute for the <system.web>/<httpRuntime> in your web.config file. See Chapter 18, Application Logic and Configuration for more on configuring your website.

The AdRotator Control

The AdRotator control gets its name because it is most often used to display advertisements on web pages. It displays an image randomly selected from a list stored in either a separate XML file or a data-bound data source. In either case, the list contains image attributes, including the path to the image and a URL to link to when the image is clicked. The image changes every time the page is loaded.

In addition to the properties inherited from WebControl, the AdRotator control has the properties listed in Table 5.17, "Properties of the AdRotator control", and an event you can handle called AdCreated. This occurs once per round trip to the server after creation of the control, but before the page is rendered.

Table 5.17. Properties of the AdRotator control

Name

Type

Description

AdvertisementFile

String

The path to an XML file that contains the list of advertisements and their attributes. We describe this file in detail shortly.

AlternateTextField

String

The element name from the AdvertisementFile or data field from which the alternative text is stored. Default is AlternateText.

DataMember

String

The name of the specific list of data the control will bind to if the control is not using an XML file for ads.

DataSource

Object

An object from which the control can retrieve data.

DataSourceID

String

The ID of the control from which the AdRotator can retrieve data.

ImageUrlField

String

The element name from the advertisement file or data field from which the URL for the image is stored. Default is ImageUrl.

KeywordFilter

String

Filters ads displayed to include only those with the specified keyword in the AdvertisementFile.

NavigateUrlField

String

The element name from the advertisement file or data field in which the URL to navigate to is stored. Default is NavigateUrl.

Target

String

The browser window or frame that displays the contents of the page linked to when the AdRotator is clicked.

The Target property is used to specify which browser window or frame is used to display the results of clicking the AdRotator control. It dictates whether the resultant page displaces the contents in the current browser window or frame, opens a new browser window, or does something else. The values of the Target property must begin with any letter in the range of a to z, case-insensitive, except for the special values shown in Table 4.6, "Special values of the Target attribute" in Chapter 4, Basic Controls, which begin with an underscore. These are the same values recognized by the Target property of the HTML <a> element.

The Advertisement File

The advertisement file is an XML file that contains information about the advertisements to be displayed by the AdRotator control. Its location and filename are specified by the AdvertisementFile property of the control.

Tip

You can save the advertisement file anywhere in your website, but the recommended location is the special App_Data folder, because ASP.NET will never send files from that folder to the browser. VS2008 creates this folder by default for new websites.

The location of the advertisement file can be relative to the website root directory or can be absolute. If its location is something other than the web root, you will need to ensure the application has sufficient rights to access the file, especially after deployment.

The AdvertisementFile property cannot be set simultaneously with the DataSource, DataMember, or DataSourceID property. In other words, if the data is coming from an advertisement file, it cannot simultaneously come from a data source, and vice versa, or else an error occurs.

The advertisement file and the AdvertisementFile property are optional. If you want to create an advertisement programmatically, without the use of an advertisement file, place the code to display the desired elements in the AdCreated event.

As an XML file, the advertisement file is a structured text file with well-defined tags delineating the data.

Tip

Those who understand schemas can find the schema for the advertisement file in %Program Files%\Microsoft Visual Studio 9.0\Xml\Schemas\adrotator.xsd.

Table 5.18, "XML elements used in the advertisement file" lists the standard elements, which are enclosed in angle brackets (< >) and require matching closing tags.

Table 5.18. XML elements used in the advertisement file

Tag

Description

Advertisements

Encloses the entire advertisement file.

Ad

Delineates each separate ad.

ImageUrl

The URL of the image to display. Required.

NavigateUrl

The URL of the page to navigate to when the control is clicked.

AlternateText

The text displayed in the control if the image is unavailable. In browsers that support the ToolTips feature this text is also displayed as a tool tip.

Keyword

The advertisement category. The keyword can be used to filter the advertisements displayed by the control by setting the AdRotator KeywordFilter property.

Impressions

A value indicating how often the ad should be displayed relative to the other ads in the file.

Height

The height of the image being displayed.

Width

The width of the image being displayed.

Warning

Because this is XML and not HTML, it is much less forgiving of files that are not well formed. These tags are case-sensitive: ImageUrl will work; ImageURL will not. For a complete description of well-formed XML, see the "Well-Formed XHTML" sidebar in Chapter 3, Controls: Fundamental Concepts.

In addition to the tags listed in Table 5.18, "XML elements used in the advertisement file", you can include your own custom tags to have custom attributes. The sample advertisement file in Example 5.25, "ads.xml, sample advertisement file" contains a custom attribute called Animal, which will hold the animal pictured on the cover of each book. (No, O'Reilly authors have no say in selecting the animal that goes on their books.)

Example 5.25. ads.xml, sample advertisement file

<?xml version="1.0" encoding="utf-8" ?>
<Advertisements xmlns="
   https://schemas.microsoft.com/AspNet/AdRotator-Schedule-File-1.2">
   <Ad>
      <ImageUrl>ProgAspNet.gif</ImageUrl>
      <NavigateUrl>
         http://www.oreilly.com/catalog/progaspdotnet3/index.html
      </NavigateUrl>
      <AlternateText>Programming ASP.NET</AlternateText>
      <Keyword>Web</Keyword>
      <Impressions>50</Impressions>
      <Animal>stingray</Animal>
   </Ad>
   <Ad>
      <ImageUrl>LearningASP.gif</ImageUrl>
      <NavigateUrl>
         http://www.oreillynet.com/catalog/9780596513979/
      </NavigateUrl>
      <AlternateText>Learning ASP.NET 2.0 With AJAX</AlternateText>
      <Keyword>Windows</Keyword>
      <Impressions>40</Impressions>
      <Animal>stingray</Animal>
   </Ad>
   <Ad>
      <ImageUrl>ProgCSharp.gif</ImageUrl>
      <NavigateUrl>
         http://www.oreilly.com/catalog/9780596527433/
      </NavigateUrl>
      <AlternateText>Programming C# 3.0</AlternateText>
      <Keyword>Language</Keyword>
      <Impressions>40</Impressions>
      <Animal>African Crowned Crane</Animal>
   </Ad>
   <Ad>
      <ImageUrl>ProgVB.gif</ImageUrl>
      <NavigateUrl>
         http://www.oreilly.com/catalog/progvb2005/index.html
      </NavigateUrl>
      <AlternateText>Programming VB .NET</AlternateText>
      <Keyword>Language</Keyword>
      <Impressions>30</Impressions>
      <Animal>grebe</Animal>
   </Ad>
</Advertisements>

All the elements in the advertisement file are parsed and placed in the adProperties dictionary. This dictionary can be used programmatically to access attributes, either standard or custom, by placing code in the AdCreated event handler.

Example 5.25, "ads.xml, sample advertisement file" shows a sample advertisement file that contains references to books and websites for several excellent programming books. :)

Warning

The NavigateUrl element of an Ad should be on one line, or else the AdRotator will try to interpret the whitespace as part of the URL to link to. It's split across three lines in Example 5.25, "ads.xml, sample advertisement file" purely for clarity.

Using AdRotator

Now all you need is a web page with an AdRotator control and some graphics to use this advertisement file, as shown in the next example, AdRotatorDemo.aspx. After creating a new web form for the chapter website and calling it AdRotatorDemo.aspx, drag an AdRotator control onto the page, along with a Label control to display the animal. The content file should look something like Example 5.26, "AdRotatorDemo.aspx".

Example 5.26. AdRotatorDemo.aspx

<%@ Page Language="C#" AutoEventWireup="true"
   CodeFile="AdRotatorDemo.aspx.cs" Inherits="AdRotatorDemo" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
   <title>AdRotator</title>
</head>

<body>
   <form id="form1" runat="server">
   <div>
      <h1>
         AdRotator Control</h1>
      <asp:AdRotator ID="ad" runat="server" Target="_blank"
         AdvertisementFile="ads.xml" OnAdCreated="ad_AdCreated" />
      <br />
      Animal:
      <asp:Label ID="lblAnimal" runat="server" />
   </div>
   </form>
</body>
</html>

The event handler for the AdRotator control is contained in the code-behind file, as highlighted in Example 5.27, "AdRotatorDemo.aspx.cs".

Example 5.27. AdRotatorDemo.aspx.cs

using System;
using System.Web.UI;
using System.Web.UI.WebControls;

public partial class AdRotatorDemo : Page
{
   protected void Page_Load(object sender, EventArgs e)
   {

   }
   protected void ad_AdCreated(object sender, AdCreatedEventArgs e)
   {
      if ((string)e.AdProperties["Animal"] != ")
         lblAnimal.Text = (string)e.AdProperties["Animal"];
      else
         lblAnimal.Text = "Not Applicable";
   }
}

Make certain that the advertisement file called ads.xml, listed in Example 5.25, "ads.xml, sample advertisement file", is located in the website root directory, along with the image files specified within that file: ProgAspNet.gif, ProgCSharp.gif, ProgVB.gif, and LearningASP.gif. You'll find them in the download for this book.

The results of running AdRotatorDemo are shown in Figure 5.21, "AdRotatorDemo.aspx in action". To see the images cycle through, refresh the view on your browser.

This control raises an AdCreated event, which occurs on every round trip to the server after the control is created but before the page is rendered. An attribute in the control declaration, called OnAdCreated, specifies the event handler to execute whenever the event fires. The event handler is passed an argument of type AdCreatedEventArgs, which has the properties listed in Table 5.19, "Properties of the AdCreatedEventArgs class".

Figure 5.21. AdRotatorDemo.aspx in action

AdRotatorDemo.aspx in action

Table 5.19. Properties of the AdCreatedEventArgs class

Property

Description

AdProperties

Gets a dictionary object that contains all the advertisement properties contained in the advertisement file.

AlternateText

The alternative text displayed by the browser when the advertisement image is unavailable. If the browser supports tool tips, this text will be displayed as a tool tip.

ImageUrl

The URL of an image to display.

NavigateUrl

The URL of the web page to display when the control is clicked.

Every time the ad is changed (i.e., every time the page is reloaded), the event handler, ad_AdCreated, fires and updates lblAnimal contained on the page. ad_AdCreated first tests to be certain a value is in the Animal attribute. If not, "Not available." is displayed.

AdProperties returns a Dictionary object. When the AdProperties property is invoked, it implicitly calls the Item method of the Dictionary object, which returns the value corresponding to the dictionary entry whose key is Animal. This value is then cast, or converted, to a string. In C#, this is done with the following syntax:

(string)e.AdProperties["Animal"]

The Calendar Control

The ASP.NET Calendar control is a rich web control that provides several capabilities:

  • Displays a calendar showing a single month

  • Allows the user to select a day, week, or month

  • Allows the user to select a range of days

  • Allows the user to move to the next or previous month

  • Programmatically controls the display of specific days

The Calendar control is customizable, with various properties and events. Before digging into all the detail, have a look at a bare-bones .aspx file showing a simple Calendar control. Add to the chapter website a new web form called Calendar-Simple.aspx, and drag a Calendar control onto the page.

Example 5.28, "Calendar-Simple.aspx" contains the code with the Calendar declaration highlighted, and Figure 5.22, "A bare-bones Calendar control in Calendar-Simple.aspx" shows the results. There is no code-behind file with this example other than the default boilerplate created by VS2008.

Example 5.28. Calendar-Simple.aspx

<%@ Page Language="C#" AutoEventWireup="true"
   CodeFile="Calendar-Simple.aspx.cs" Inherits="Calendar_Simple" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
   <title>Simple Calendar Demo</title>
</head>

<body>
   <form id="form1" runat="server">
   <div>
      <asp:Calendar ID="Calendar1" runat="server"></asp:Calendar>
   </div>
   </form>
</body>
</html>

Pretty spiffy; zero manual coding yields a web page with a working calendar that displays the current month. The user can select a single day (though at this point nothing happens when a day is selected, other than it being highlighted) and move through the months by clicking the navigation symbols (< and >) on either side of the month name.

In addition to the properties inherited by all the ASP.NET server controls that derive from WebControl, the Calendar control has many properties of its own. The most important ones are listed in Table 5.20, "Read/write properties of the Calendar control".

Figure 5.22. A bare-bones Calendar control in Calendar-Simple.aspx

A bare-bones Calendar control in Calendar-Simple.aspx

As you can see in Table 5.20, "Read/write properties of the Calendar control", the aforementioned navigation symbols are specified by the NextMonthText and PrevMonthText properties as > and <, respectively. These HTML character entities normally display as the greater than (>) and less than (<) symbols. However, in the Calendar control, these symbols are displayed as underlined. This is because all the selectable elements in the Calendar control are rendered to the browser like LinkButton controls, hence the underlines.

Table 5.20. Read/write properties of the Calendar control

Name

Type

Values

Description

Caption

String

Text to display on the page above the calendar.

CaptionAlign

TableCaption-Align

Bottom, Left, NotSet, Right, Top

Specifies horizontal and vertical alignment of the Caption property value.

CellPadding

Integer

0, 1, 2, etc.

Distance in pixels between the border and contents of a cell. Applies to all the cells in the calendar and to all four sides of each cell. Default is 2.

CellSpacing

Integer

0, 1, 2, etc.

Distance in pixels between cells. Applies to all the cells in the calendar. Default is 0.

DayNameFormat

DayName-Format

Full, Short, FirstLetter, FirstTwoLetters

Format of days of the week. Values are self-explanatory, except Short, which is the first three letters. Default is Short.

FirstDayOfWeek

FirstDayOfWeek

Default, Sunday, Monday, … Saturday

Day of week to display in the first column. Default (the default) specifies system setting.

NextMonthText

String

Text for next month navigation control. The default is &gt;, which renders as the greater than sign (>). Applies only if the ShowNextPrevMonth property is true.

NextPrevFormat

NextPrevFormat

CustomText, FullMonth, ShortMonth

To use CustomText, set this property and specify the actual text to use in NextMonthText and PrevMonthText.

PrevMonthText

String

Text for previous month navigation control. Default is &lt;, which renders as the less than sign (<). Applies only if the ShowNextPrevMonth property is true.

SelectedDate

DateTime

A single selected date. Only the date is stored; the time is set to 00:00:00 (12:00 A.M.).

SelectedDates

DateTime

Collection of DateTime objects when multiple dates are selected.

SelectionMode

Calendar-SelectionMode

Described later in this section.

SelectMonthText

String

Text for month selection element in the selector column. Default is &gt;&gt;, which renders as two greater than signs (>). Applies only if the SelectionMode property is set to DayWeekMonth.

ShowDayHeader

Boolean

true, false

If true, the default, the days-of-week headings are shown.

ShowGridLines

Boolean

true, false

If true, grid lines between cells are displayed. Default is false.

ShowNextPrev-Month

Boolean

true, false

Indicates whether the next and previous month navigation elements are shown. Default is true.

ShowTitle

Boolean

true, false

Indicates whether the title is shown. If false, the next and previous month navigation elements will be hidden. Default is true.

TitleFormat

TitleFormat

Month, MonthYear

Indicates whether the title is month only or month and year. Default is MonthYear.

TodaysDate

DateTime

The date in the calendar that is formatted as the current date.

UseAccessible-Header

Boolean

true, false

Specifies whether a header accessible to assistive technologies is to be used.

VisibleDate

DateTime

Any date in the month that you want to display. Initially not set.

Selecting Dates in the Calendar

If you want to give the user the ability to select a single day, an entire week, or an entire month, you must set the SelectionMode property. Table 5.21, "Members of the CalendarSelectionMode enumeration" lists the legal values for the SelectionMode property.

Table 5.21. Members of the CalendarSelectionMode enumeration

Member

Description

Day

Allows the user to select a single day. This is the default value.

DayWeek

Allows the user to select a single day or an entire week.

DayWeekMonth

Allows the user to select a single day, an entire week, or an entire month.

None

Nothing on the Calendar can be selected.

To see the effects of setting the SelectionMode property, add another new web form to the chapter website and call it Calendar-SelectionMode.aspx. Once again, add a Calendar control to the empty page, but add the following highlighted attribute to the Calendar:

<asp:Calendar ID="Calendar1" runat="server"
   SelectionMode="DayWeekMonth">
</asp:Calendar>

The resultant calendar, with the entire month selected, looks like Figure 5.23, "A Calendar control with a month selected". (It's saved as Calendar-SelectionMode.aspx in the download for the chapter.)

Figure 5.23. A Calendar control with a month selected

A Calendar control with a month selected

When the SelectionMode property is set to DayWeek, an extra column containing the ≥ symbol is added to the left side of the calendar. Clicking one of those symbols selects that entire week.

Similarly, when the SelectionMode property is set to DayWeekMonth, in addition to the week selection column, a ≥ symbol (two "greater than or equal to" symbols) is added to the left of the Day Names row. Clicking that symbol selects the entire month, as shown in Figure 5.23, "A Calendar control with a month selected".

Controlling the Calendar's Appearance

A number of read/write properties, all of type TableItemStyle, control the style for each part of the calendar. These TableItemStyle type properties are listed in Table 5.22, "Calendar control properties of type TableItemStyle" and are demonstrated in the next example, Calendar-Styles.aspx, shown in finished form in Figure 5.24, "Calendar-Styles.aspx in action".

Table 5.22. Calendar control properties of type TableItemStyle

Name

Sets style for …

DayHeaderStyle

Days of the week

DayStyle

Dates

NextPrevStyle

Month navigation controls

OtherMonthDayStyle

Dates that are visible but are not in the current month

SelectedDayStyle

Selected dates

SelectorStyle

Week and month selection column

TitleStyle

Title section

TodayDayStyle

Today's date (or the date represented by the value of the TodaysDate property)

WeekendDayStyle

Weekend dates

These TableItemStyle type properties work the same way in VS2008 for the Calendar control as we described previously for the Wizard control. When you work in Design view, the properties appear as in Figure 5.17, "TableItemStyle type properties in Design view", and when you work in Source view, the properties appear in IntelliSense in their expanded form. The format of the declaration also follows the same pattern described for the Wizard control.

In addition to the TableItemStyle type properties there are four read/write Boolean properties that control various aspects of the calendar, shown in Table 5.23, "Boolean properties controlling various aspects of the Calendar control's appearance".

Table 5.23. Boolean properties controlling various aspects of the Calendar control's appearance

Property

Default

Controls visibility of …

ShowDayHeader

true

Names of the days of the week

ShowGridLines

false

Grid lines between the days of the month

ShowNextPrevMonth

true

Month navigation controls

ShowTitle

true

Title section

Figure 5.24. Calendar-Styles.aspx in action

Calendar-Styles.aspx in action

You could click the Calendar control smart tag in Design view and select one of the Auto Format formats, but in this example you will choose your own styles. You can do this in the Properties window after selecting the Calendar control in either Design or Source view.

To see how these style properties are used, copy the Calendar control from Calendar-SelectionMode.aspx to a new web form, Calendar-Styles.aspx. The complete content file for this latest example is listed in Example 5.29, "Calendar-Styles.aspx in full", with the Calendar declaration highlighted, and the finished page is shown in Figure 5.24, "Calendar-Styles.aspx in action" (or at least as best as it can be in grayscale rather than color). You can see what styles to set for this example by looking at the declarations in Example 5.29, "Calendar-Styles.aspx in full".

Example 5.29. Calendar-Styles.aspx in full

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Calendar-Styles.aspx.cs"
   Inherits="Calendar_Styles" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
   <title>Calendar Control Styles</title>
</head>
<body>
   <form id="form1" runat="server">
   <div>
      <asp:Calendar ID="Calendar1"
         runat="server" SelectionMode="DayWeekMonth"
         CellPadding="7" CellSpacing="5"
         DayNameFormat="FirstTwoLetters" FirstDayOfWeek="Monday"
         NextMonthText="Next >" PrevMonthText="< Prev"
         ShowGridLines="True" DayStyle-BackColor="White"
         DayStyle-ForeColor="Black" DayStyle-Font-Names="Arial">

         <DayHeaderStyle BackColor="Black"
            Font-Names="Arial Black" ForeColor="White" />
         <SelectedDayStyle BackColor="Cornsilk" Font-Bold="True"
            Font-Italic="True" Font-Names="Arial" ForeColor="Blue" />
         <SelectorStyle BackColor="Cornsilk"
            Font-Names="Arial" ForeColor="Red" />
         <WeekendDayStyle BackColor="LavenderBlush"
            Font-Names="Arial" ForeColor="Purple" />
         <OtherMonthDayStyle BackColor="LightGray"
            Font-Names="Arial" ForeColor="White" />
         <TodayDayStyle BackColor="Cornsilk" Font-Bold="True"
            Font-Names="Arial" ForeColor="Green" />
         <NextPrevStyle BackColor="DarkGray"
            Font-Names="Arial" ForeColor="Yellow" />
         <TitleStyle BackColor="Gray" Font-Names="Arial Black"
            ForeColor="White" HorizontalAlign="Left" />
      </asp:Calendar>
   </div>
   </form>
</body>
</html>

Programming the Calendar Control

The Calendar control provides three events that are not inherited from other control classes and are of particular interest. By providing event handlers for the events, you can exercise considerable control over how the calendar behaves. These events are:

  • SelectionChanged

  • DayRender

  • VisibleMonthChanged

The following sections describe each of these in detail.

SelectionChanged event

The SelectionChanged event fires when the user makes a selection-either a day, a week, or an entire month-in the Calendar control. The event is not fired if the selection is changed programmatically or if you reselect the currently selected day. The event handler is passed an argument of type EventArgs.

The next example demonstrates handling the SelectionChanged event. Whenever you select a new date, it displays text strings with today's date, the selected date, and the number of days selected.

Copy the calendar control from Calendar-SelectionMode.aspx (the one without all the styles) to a new web form called Calendar-SelectionChanged.aspx. Add the default named event handler for the SelectionChanged event by double-clicking the Calendar control in Design view. This will add the OnSelectionChanged attribute to the Calendar declaration in the content file and will open the code-behind file with the event handler skeleton in place. In the Calendar1_SelectionChanged method, type in the highlighted code from Example 5.30, "Calendar-SelectionChanged.aspx.cs", as well as the highlighted helper method, lblCountUpdate.

Example 5.30. Calendar-SelectionChanged.aspx.cs

using System;
using System.Web.UI;

public partial class Calendar_SelectionChanged : Page
{
   protected void Calendar1_SelectionChanged(object sender, EventArgs e)
   {
      lblTodaysDate.Text = "Today's date is "
         + Calendar1.TodaysDate.ToShortDateString( );
      if (Calendar1.SelectedDate != DateTime.MinValue)
      {
         lblSelected.Text = "The date selected is "
            + Calendar1.SelectedDate.ToShortDateString( );
      }
      lblCountUpdate( );
   }

   private void lblCountUpdate( )
   {
      lblCount.Text = "Count of days selected:  "
         + Calendar1.SelectedDates.Count.ToString( );
   }
}

You must also add three Label controls to the bottom of the page to display the information from the calendar. Calendar-SelectionChanged.aspx is listed in full in Example 5.31, "Calendar-SelectionChanged.aspx in full", with the style attributes of the Calendar control omitted because they are the same as the previous example. Additions for this example are highlighted.

Example 5.31. Calendar-SelectionChanged.aspx in full

<%@ Page Language="C#" AutoEventWireup="true"
   CodeFile="Calendar-SelectionChanged.aspx.cs"
   Inherits="Calendar_SelectionChanged" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
   <title>Calendar SelectionChanged Demo</title>
</head>

<body>
   <form id="form1" runat="server">
   <div>
      <asp:Calendar ID="Calendar1"
         runat="server" SelectionMode="DayWeekMonth"
         OnSelectionChanged="Calendar1_SelectionChanged">
      </asp:Calendar>
      <br />
      <asp:Label ID="lblCount" runat="server" />
      <br />
      <asp:Label ID="lblTodaysDate" runat="server" />
      <br />
      <asp:Label ID="lblSelected" runat="server" />
   </div>
   </form>
</body>
</html>

Running the page and selecting a date or dates will result in a screen similar to the one shown in Figure 5.25, "Calendar-SelectionChanged.aspx in action".

Looking at Example 5.31, "Calendar-SelectionChanged.aspx in full", you can see this example adds the OnSelectionChanged property to the Calendar control. This property binds the SelectionChanged event to the Calendar1_SelectionChanged method in the code-behind file, shown in Example 5.30, "Calendar-SelectionChanged.aspx.cs". Three Label controls have been added after the Calendar control. The first of these, lblCount, is used to display the number of days selected. The other two labels, named lblTodaysDate and lblSelected, are used to display today's date and the currently selected date, respectively.

All three of these labels have their Text property set in the SelectionChanged event handler method. Looking at that method in Example 5.30, "Calendar-SelectionChanged.aspx.cs", you can see that lblTodaysDate is filled by getting the Calendar control's TodaysDate property, with the following line of code:

lblTodaysDate.Text = "Today's date is "
   + Calendar1.TodaysDate.ToShortDateString( );

The ID of the Calendar control is Calendar1. TodaysDate is a property of the Calendar control that returns an object of type System.DateTime. To assign this to a Text property (which is an object of type String), you must convert the DateTime to a String.

Figure 5.25. Calendar-SelectionChanged.aspx in action

Calendar-SelectionChanged.aspx in action

You do this with the ToShortDateString method, which returns a shorter (mm/dd/yy) version of the date.

Tip

The DateTime structure has many methods for converting a DateTime object to other formats based on the current culture for the website. For the lowdown on DateTime format strings, go to https://blogs.msdn.com/kathykam/archive/2006/09/29/.NET-Format-String-102_3A00_-DateTime-Format-String.aspx.

lblSelected is filled by the following line of code:

if (Calendar1.SelectedDate != DateTime.MinValue)
{
   lblSelected.Text = "The date selected is "
      + Calendar1.SelectedDate.ToShortDateString( );
}

To detect whether any date has been selected, you test to see whether the currently selected date, Calendar1.SelectedDate, is equal to DateTime.MinValue. DateTime.MinValue is a constant representing the smallest possible value of DateTime, and is the default value for the SelectedDate property if nothing has been selected yet. MinValue has the literal value of 12:00:00 A.M., 1/1/0001 CE. There is also a MaxValue that has the literal value of 11:59:59 P.M., 12/31/9999 CE.

Tip

CE (Common Era) is the scientific notation for the span of years referred to as AD (Anno Domini) on the Gregorian calendar. BCE (Before Common Era) is the scientific equivalent of BC (Before Christ).

If the user has selected a date, the Text property of lblSelected will be set to the string value of the SelectedDate property.

The Label control, lblCount, displays the number of days selected. The SelectionChanged event handler calls the lblCountUpdate method, which sets the Text property of lblCount. To set that control, you must determine how many dates were selected. The Calendar control has a SelectedDates property that returns a SelectedDates collection. SelectedDates is a collection of DateTime objects representing all the dates selected in the Calendar control. Count is a property of the SelectedDatesCollection object that returns an integer containing the number of dates in the collection. Because the Count property is an integer, you must use the ToString method to convert it to a string so it can be assigned to the Text property:

Calendar1.SelectedDates.Count.ToString( );

Though SelectedDates (the collection of selected dates) and SelectedDate (the single selected date) both contain DateTime objects, only the date value is stored. The time value for these objects is set to 0 (equivalent to 12:00 A.M.) in C#.

The range of dates in the SelectedDates collection is sorted in ascending order by date. When the SelectedDates collection is updated, the SelectedDate property is automatically updated to contain the first object in the SelectedDates collection.

The user can navigate from month to month by clicking the month navigation controls located on either side of the month title. The user can also select a single day by clicking that day, an entire week by clicking the week selector control, or the entire month by clicking the month selector control.

However, you can give the user much more flexibility than this. To demonstrate, you must add several controls and methods. Create in the chapter's website a new web form called Calendar-MoreSelections.aspx and copy across the Calendar, Labels, and event handler code from Calendar-SelectionChanged.aspx.

To enable the user to navigate directly to any month in the current year, add a DropDownList containing all the months of the year and a button, labeled TGIF, which selects all the Fridays in the currently viewed month.

The Calendar control also allows the user to select a range of dates. You might expect to be able to use the standard Windows techniques of holding down the Ctrl or Shift key while clicking on dates, but this does not work. However, you can put controls on the page to select a starting day and ending day. In Calendar-MoreSelections, you will add a pair of TextBox controls to accept a starting day and an ending day for a range of dates. A Button control can force the selection of the range of dates.

The markup for this is shown in Example 5.32, "Calendar-MoreSelections.aspx in full", with the new additions highlighted.

Example 5.32. Calendar-MoreSelections.aspx in full

<%@ Page Language="C#" AutoEventWireup="true"
   CodeFile="Calendar-MoreSelections.aspx.cs"
   Inherits="Calendar_MoreSelections" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
   <title>Calendar SelectionChanged Demo Part 2</title>
</head>

<body>
   <form id="form1" runat="server">
   <div>
      <asp:Calendar ID="Calendar1" runat="server"
         SelectionMode="DayWeekMonth"
         OnSelectionChanged="Calendar1_SelectionChanged">
      </asp:Calendar>
      <br />
      <asp:Label ID="lblCount" runat="server" />
      <br />
      <asp:Label ID="lblTodaysDate" runat="server" />
      <br />
      <asp:Label ID="lblSelected" runat="server" />
      <br />

      <table>
         <tr>
            <td>
               Select a month:
            </td>
            <td>
               <asp:DropDownList ID="ddl" runat="server"
                  AutoPostBack="true"
                  OnSelectedIndexChanged="ddl_SelectedIndexChanged">
                  <asp:ListItem Text="January" Value="1" />
                  <asp:ListItem Text="February" Value="2" />
                  <asp:ListItem Text="March" Value="3" />
                  <asp:ListItem Text="April" Value="4" />
                  <asp:ListItem Text="May" Value="5" />
                  <asp:ListItem Text="June" Value="6" />
                  <asp:ListItem Text="July" Value="7" />
                  <asp:ListItem Text="August" Value="8" />
                  <asp:ListItem Text="September" Value="9" />
                  <asp:ListItem Text="October" Value="10" />
                  <asp:ListItem Text="November" Value="11" />
                  <asp:ListItem Text="December" Value="12" />
               </asp:DropDownList>
            </td>
            <td>
               <asp:Button ID="btnTgif" runat="server"
                  Text="TGIF" onclick="btnTgif_Click" />
            </td>
         </tr>
         <tr>
            <td colspan="2">
               &nbsp;</td>
         </tr>
         <tr>
            <td colspan="2">
               <b>Day Range</b></td>
         </tr>
         <tr>
            <td>
               Starting Day</td>
            <td>
               Ending Day</td>
         </tr>
         <tr>
            <td>
               <asp:TextBox ID="txtStart" runat="server"
                  Width="25" MaxLength="2" />
            </td>
            <td>
               <asp:TextBox ID="txtEnd" runat="server"
                  Width="25" MaxLength="2" />
            </td>
            <td>
               <asp:Button ID="btnRange" runat="server"
                  Text="Apply" onclick="btnRange_Click" />
            </td>
         </tr>
      </table>

   </div>
   </form>
</body>
</html>

Example 5.33, "Calendar-MoreSelections.aspx.cs" shows the code-behind file, Calendar-MoreSelections.aspx.cs, in full.

Example 5.33. Calendar-MoreSelections.aspx.cs

using System;
using System.Web.UI;

public partial class Calendar_MoreSelections : Page
{
   protected void Page_Load(object sender, EventArgs e)
   {
      if (!IsPostBack)
      {
         Calendar1.VisibleDate = Calendar1.TodaysDate;
         ddl.SelectedIndex = Calendar1.VisibleDate.Month - 1;
      }
      lblTodaysDate.Text = "Today's Date is "
         + Calendar1.TodaysDate.ToShortDateString( );
   }
   protected void Calendar1_SelectionChanged(object sender, EventArgs e)
    {
      lblSelectedUpdate( );
      lblCountUpdate( );
      txtClear( );
    }

   private void lblSelectedUpdate( )
   {
      if (Calendar1.SelectedDate != DateTime.MinValue)
      {
         lblSelected.Text = "The date selected is "
            + Calendar1.SelectedDate.ToShortDateString( );
      }
   }

   private void lblCountUpdate( )
   {
      lblCount.Text = "Count of Days Selected:  "
         + Calendar1.SelectedDates.Count.ToString( );
   }

   protected void ddl_SelectedIndexChanged(object sender, EventArgs e)
   {
      Calendar1.SelectedDates.Clear( );
      lblSelectedUpdate( );
      lblCountUpdate( );
      Calendar1.VisibleDate = new DateTime(Calendar1.VisibleDate.Year,
         Int32.Parse(ddl.SelectedItem.Value), 1);
      txtClear( );
   }

   protected void btnTgif_Click(object sender, EventArgs e)
   {
      int currentMonth = Calendar1.VisibleDate.Month;
      int currentYear = Calendar1.VisibleDate.Year;
      Calendar1.SelectedDates.Clear( );
      for (int i = 1;
         i <= System.DateTime.DaysInMonth(currentYear, currentMonth);
         i++)
         {
         DateTime date = new DateTime(currentYear, currentMonth, i);
         if (date.DayOfWeek == DayOfWeek.Friday)
         {
            Calendar1.SelectedDates.Add(date);
         }
      }
      lblSelectedUpdate( );
      lblCountUpdate( );
      txtClear( );
   }

   protected void btnRange_Click(object sender, EventArgs e)
   {
      int currentMonth = Calendar1.VisibleDate.Month;
      int currentYear = Calendar1.VisibleDate.Year;
      DateTime StartDate = new DateTime(currentYear, currentMonth,
         Int32.Parse(txtStart.Text));
      DateTime EndDate = new DateTime(currentYear, currentMonth,
         Int32.Parse(txtEnd.Text));
      Calendar1.SelectedDates.Clear( );
      Calendar1.SelectedDates.SelectRange(StartDate, EndDate);
      lblSelectedUpdate( );
      lblCountUpdate( );
   }

   private void txtClear( )
   {
      txtStart.Text = ";
      txtEnd.Text = ";
   }
}

The result of running Calendar-MoreSelections is shown in Figure 5.26, "Calendar-MoreSelections.aspx in action" after selecting a range of days.

All of the selection controls are in a static HTML table so you can control the layout of the page.

The ListItem objects in the DropDownList contain the names of the months for the Text properties and the number of the months for the Value properties.

The Calendar1_SelectionChanged method has been modified by having the bulk of its code refactored into a separate method named lblSelectedUpdate, which updates the Text property of the lblSelected label. This method is then called from Calendar1_SelectionChanged, as well as several other places throughout the code. In addition, another helper method, txtClear, is called to clear the Starting Day and Ending Day text boxes.

The ddl_SelectedIndexChanged event handler method begins by clearing the SelectedDates collection:

Calendar1.SelectedDates.Clear( );

A call is made to the lblSelectedUpdate method to clear the Label control containing the first selected date and to the lblCountUpdate method to clear the Label control containing the count of selected dates. Then the VisibleDate property of the Calendar control is set to the first day of the newly selected month:

Calendar1.VisibleDate = new DateTime(Calendar1.VisibleDate.Year,
   Int32.Parse(ddl.SelectedItem.Value), 1);

Figure 5.26. Calendar-MoreSelections.aspx in action

Calendar-MoreSelections.aspx in action

The VisibleDate property is of type DateTime; a new DateTime object is instantiated. The DateTime structure, like many objects in the .NET Framework, uses an overloaded constructor. An object may have more than one constructor; each must be differentiated by having different types of arguments or a different number of arguments.

In this case, you want to instantiate a DateTime object that contains only the date or the date with the time set to zero. To do so requires three integer parameters: year, month, and day. The first parameter, Calendar1.VisibleDate.Year, and the last parameter, 1, are integers. However, the month parameter comes from the Value property of the selected item in the DropDownList control. The Value property is a string, not an integer, though the characters it contains look like an integer. Therefore, it must be converted to an integer using the statement:

Int32.Parse(ddl.SelectedItem.Value)

The TGIF button is named btnTgif and has an event handler for the Click event, btnTgif_Click. This method iterates over all the days of the currently visible month and tests to see whether it is Friday. If so, it will add that date to the collection of SelectedDates.

First, the btnTgif_Click method gets the month and year of the currently visible month, using the VisibleDate property of the Calendar control, which is a DateTime object, and gets the Month and Year properties of the DateTime object:

int currentMonth = Calendar1.VisibleDate.Month;
int currentYear = Calendar1.VisibleDate.Year;

Then, it clears all the currently selected dates:

Calendar1.SelectedDates.Clear( );

Now, it does the iteration. The limit part of the for loop is the number of days in the month as determined by the DaysInMonth property of the DateTime object. The month in question is specified by the currentYear and currentMonth arguments:

System.DateTime.DaysInMonth(currentYear, currentMonth)

Once inside the for loop, a DateTime variable called date is assigned to each day. Again, the DateTime object is instantiated with parameters for year, month, and day. The crucial question becomes, "Is the day of the week for this day a Friday?" If so, TGIF and add it to the collection of SelectedDates:

DateTime date = new DateTime(currentYear, currentMonth, i);
if (date.DayOfWeek == DayOfWeek.Friday)
{
   Calendar1.SelectedDates.Add(date);
}

Finally, after iterating over all the days of the month, call the lblSelectedUpdate method to update the label showing the first selected date, call the lblCountUpdate method to update the label showing the number of days selected, and call txtClear to clear the Start and End text boxes.

You will notice a Page_Load method in the code-behind file. This makes the page behave correctly the first time the TGIF button is clicked, even before the month is changed. Without this Page_Load event procedure, the page behaves correctly for the TGIF button only after the month has been changed at least once. The btnTgif_Click method uses the VisibleDate property to set the current month and year variables. If that property is not initialized during the initial page load, the values assigned to those variables will not correspond to the visible month.

In addition, the code to update the label displaying today's data, lblTodaysDate, has been moved from the Calendar1_SelectionChanged method to the Page_Load method because it makes more sense to have it there.

The controls for selecting the range are in the same static HTML table as the controls for selecting the month and all the Fridays. There are two text boxes, one named txtStart for the start day and one named txtEnd for the end day. In this example, the TextBox controls' Width and MaxLength attributes provide limited control over the user input.

The UI provided in Calendar-MoreSelections.aspx for selecting a range of dates is admittedly limiting because you cannot span multiple months. You could almost as easily provide three independent Calendar controls: one for the start date, one for the end date, and one for the range. Also, the day range does not apply after the month changes without reapplying the selection because the VisibleMonthChanged event is not trapped. (See "VisibleMonthChanged event," later in this chapter.)

A helper method, txtClear, is provided to clear the day range selection boxes. This method is called at appropriate points in the other methods.

The Apply button is named btnRange, with the Click event handled by the method btnRange_Click. In btnRange_Click, you set integer variables to hold the current month and year:

int currentMonth = Calendar1.VisibleDate.Month;
int currentYear = Calendar1.VisibleDate.Year;
DateTime StartDate = new DateTime(currentYear, currentMonth,
   Int32.Parse(txtStart.Text));
DateTime EndDate = new DateTime(currentYear, currentMonth,
   Int32.Parse(txtEnd.Text));

Similar to the month DropDownList described previously, the DateTime object requires the year, month, and day. You have the year and month as integers; all you need is the day. You get the day by converting the text entered in the appropriate text box to an integer.

Warning

This is not very robust code. If the user enters non-numeric data in one of the text boxes, or a value greater than the number of days in the month, an ugly error will result. If the start date is later than the end date, no error message will result, but neither will anything be selected. In a real application, you will want to use validation controls, as described in Chapter 11, Validation.

Once the method has the start and end dates as DateTime objects, it clears any currently selected dates and uses the SelectRange method of the SelectedDatesCollection class to add the range of dates to the SelectedDates collection:

Calendar1.SelectedDates.Clear( );
Calendar1.SelectedDates.SelectRange(StartDate, EndDate);

The SelectRange method requires two parameters: the start date and the end date.

DayRender event

Data binding is not supported directly for the Calendar control. However, you can modify the content and formatting of individual date cells. This allows you to retrieve values from a database, process those values in some manner, and place them in specific cells.

Before the Calendar control is rendered to the browser, all of the components that comprise the control are created. As each date cell is created, it raises the DayRender event.

The DayRender event handler receives an argument of type DayRenderEventArgs. This object has two properties that can be read programmatically:

  • Cell
    TableCell object that represents the cell being rendered

  • Day
    CalendarDay object that represents the day being rendered in that cell

This next example, Calendar-Events.aspx, will demonstrate the DayRender event. All the weekend days will have their background color changed, and a New Year's greeting will be displayed for January 1.

Copy the previous example, Calendar-MoreSelections.aspx, to a new web form called Calendar-Events.aspx; then, change the class name in both the markup and the code-behind file to Calendar_Events. In this section, you will make the changes to handle the DayRender event. There are only three changes.

First, go into Design view, select Calendar1, click the Events icon in the Properties window (the lightning bolt), and double-click the text box next to DayRender. This will add the following attribute to the Calendar1 declaration in the content file:

OnDayRender="Calendar1_DayRender"

This will also create an empty event handler in the code-behind file. Add to this handler the code highlighted in Example 5.34, "DayRender event handler in Calendar-Events.aspx.cs".

Example 5.34. DayRender event handler in Calendar-Events.aspx.cs

protected void Calendar1_DayRender(object sender, DayRenderEventArgs e)
{
   //  Notice that this overrides the WeekendDayStyle.
   if (!e.Day.IsOtherMonth & e.Day.IsWeekend)
   {
      e.Cell.BackColor = System.Drawing.Color.LightGreen;
   }
   if (e.Day.Date.Month == 1 & e.Day.Date.Day == 1)
   {
      e.Cell.Controls.Add(
         new LiteralControl("<br/>Happy New Year!"));
   }
}

You'll also need to import the System.Web.UI.WebControls namespace to use the DayRenderEventArgs class in the DayRender event handler:

using System.Web.UI.WebControls;

The first thing the Calendar1_DayRender method does is color the weekends LightGreen. Recall that a WeekendDayStyle property is set for this control which colors the weekends LavenderBlush. The DayRender method overrides the WeekendDayStyle. (The distinction may not be apparent in the printed book, but you will see the colors when you run the web page in a browser.)

The event handler method is passed two parameters:

protected void Calendar1_DayRender(object sender, DayRenderEventArgs e)

DayRenderEventArgs contains properties for the Day and the Cell. The Day is tested to see whether it is both the current month and a weekend day:

(!e.Day.IsOtherMonth & e.Day.IsWeekend)

The Day property is a member of the CalendarDay class, which has the properties shown in Table 5.24, "Properties of the CalendarDay class" (all of which are read-only except for IsSelectable).

Table 5.24. Properties of the CalendarDay class

Property

Type

Description

Date

DateTime

Date represented by this Day. Read-only.

DayNumberText

String

String representation of the day number of this CalendarDay object. Read-only.

IsOtherMonth

Boolean

Indicates that this CalendarDay object is in a different month than the month currently displayed by the Calendar control. Read-only.

IsSelectable

Boolean

Indicates whether the CalendarDay object can be selected. Not read-only.

IsSelected

Boolean

Indicates whether the CalendarDay object is selected.

IsToday

Boolean

Indicates whether the CalendarDay object is today's date.

IsWeekend

Boolean

Indicates whether the CalendarDay object is a weekend date.

If the date is both in the current month and a weekend day, the Cell.BackColor property is assigned a color:

e.Cell.BackColor=System.Drawing.Color.LightGreen;

Calendar1_DayRender then tests to see whether the selected date is New Year's Day. Again, the Day property of the DayRenderEventArgs object is tested; this time to see whether the Month of the Date is 1 and the Day of the Date is 1:

if (e.Day.Date.Month == 1 & e.Day.Date.Day == 1)

If so, a LiteralControl is added to the cell that adds an HTML break tag and a greeting:

e.Cell.Controls.Add(new LiteralControl("<br/>Happy New Year!"));

The thing to remember here is, like all ASP.NET server controls, what is actually sent to the browser is HTML. Thus, a calendar is rendered on the browser as an HTML table. Each selectable component of the calendar has an anchor element associated with it, along with some JavaScript that accomplishes the postback. (This is evident when you hold the cursor over any clickable element of the calendar: the status line of the browser will display the name of the JavaScript function that will be executed if the link is clicked.) Using a LiteralControl inserts the text in its argument as a control into the HTML cell as is. A look at a snippet from the source code visible on the browser confirms this:

<td align="center" style="width:12%;">
   <a href="javascript:_ _doPostBack('Calendar1','2922')"
      style="color:Black" title="01 January">1</a>
   <br/>Happy New Year!
</td>

When the Calendar-Events example is run and the month navigated to is January, you will see something like Figure 5.27, "Calendar-Events.aspx adding a Literal to New Year's Day".

Figure 5.27. Calendar-Events.aspx adding a Literal to New Year's Day

Calendar-Events.aspx adding a Literal to New Year's Day

VisibleMonthChanged event

The Calendar control also provides an event to indicate that the user has changed months. You will extend the current example, Calendar-Events.aspx, to handle this event.

In the same manner as you added an event handler to Calendar1 for the DayRender event, add a handler for the VisibleMonthChanged event. This will add the following attribute to the Calendar1 declaration in the content file:

OnVisibleMonthChanged="Calendar1_VisibleMonthChanged"

It will also create a default event handler code skeleton in the code-behind file, with the cursor placed ready to type. Enter into this code skeleton the code highlighted in Example 5.35, "VisibleMonthChanged event handler in Calendar-Events.aspx".

Example 5.35. VisibleMonthChanged event handler in Calendar-Events.aspx

protected void Calendar1_VisibleMonthChanged
   (object sender, MonthChangedEventArgs e)
{
   if (e.NewDate.CompareTo(e.PreviousDate) == 1)
   {
      lblMonthChanged.Text = "My future's so bright...";
   }
   else
   {
      lblMonthChanged.Text = "Looking into the past";
   }
   Calendar1.SelectedDates.Clear( );
   lblSelectedUpdate( );
   lblCountUpdate( );
   txtClear( );
}

You will also need to add a Label control, named lblMonthChanged, to the content file just before the Calendar control:

<asp:Label id="lblMonthChanged" runat="server" />

The Calendar1_VisibleMonthChanged event handler method receives an argument of type MonthChangedEventArgs. This argument contains two properties that may be read programmatically:

  • NewDate
    Represents the month currently displayed by the Calendar

  • PreviousDate
    Represents the month previously displayed by the Calendar

These values are tested in the Calendar1_VisibleMonthChanged method to see which came first. Depending on the results, one of two text strings is assigned to the Text property of lblMonthChanged.

Finally, the selected dates are cleared from the calendar, the text strings below the calendar are updated, and the day range edit boxes are cleared with the following lines of code:

Calendar1.SelectedDates.Clear( );
lblSelectedUpdate( );
lblCountUpdate( );
txtClear( );

The results of running Calendar-Events.aspx and navigating a month are shown in Figure 5.28, "Calendar-Events.aspx with effects of VisibleMonthChanged event".

Figure 5.28. Calendar-Events.aspx with effects of VisibleMonthChanged event

Calendar-Events.aspx with effects of VisibleMonthChanged event