ONDotNet.com    
 Published on ONDotNet.com (http://www.ondotnet.com/)
 See this if you're having trouble printing code examples


O'Reilly Book Excerpts: ASP.NET Cookbook

Cooking with ASP.NET

Editor's note: Michael Kittel and Geoffrey LeBlond have selected a few of their favorite recipes from O'Reilly's recently released ASP.NET Cookbook. This week, learn how to add a Totals row to a DataGrid, how to communicate between user controls, and how to display user-friendly error messages. Next week, the authors offer two more recipes--for creating a reusable image handler and saving and reusing HTML output.

Recipe 1.19: Adding a Totals Row to a DataGrid

Problem

Related Reading

ASP.NET Cookbook
The Ultimate ASP.NET Code Sourcebook
By Michael A. Kittel, Geoffrey T. LeBlond

You have a DataGrid containing numeric information and you need to display a total of the data in the last row of the grid.

Solution

Enable the output of the footer in the DataGrid, accumulate the total for the data in the ItemDataBound event handler, and then output the total in the DataGrid footer.

In the .aspx file, set the ShowFooter attribute of the asp:DataGrid element to True.

In the code-behind class for the page, use the .NET language of your choice to:

  1. Initialize the totals to 0, and then bind the data to the DataGrid in the normal fashion.

  2. In the ItemDataBound event handler, add the values for each data row to the accumulated totals.

  3. In the ItemDataBound event handler, set the total values in the footer when the footer is data bound.

Figure 1-24 shows some typical output. Examples Example 1-57 through Example 1-59 show the .aspx file and code-behind files for an application that produces this output.


Figure 1-24. DataGrid with totals row output

Discussion

The best way to describe the addition of a totals row to a DataGrid is by example. In this recipe, you'll want to create the DataGrid a little differently than normal. In the asp:DataGrid element, set the ShowFooter attribute to True to cause a footer to be output when the control is rendered. You then place the totals data in the footer.

<asp:DataGrid 
  id="dgBooks" 
  runat="server" 
  BorderColor="000080" 
  BorderWidth="2px"
  AutoGenerateColumns="False"
  width="100%"
  
ShowFooter="True">

Next, add a FooterStyle element to format all of the columns in the footer with a stylesheet class, background color, and horizontal alignment:

<FooterStyle cssClass="TableCellNormal" HorizontalAlign="Right" 
             BackColor="#C0C0C0" />

All columns are defined in the Columns element as asp:TemplateColumn columns. This provides a lot of flexibility in the display of the columns. The first column contains only an ItemTemplate that is bound to the Title field in the DataSet. The FooterText property of this column is set to "Total:" to simply display the label for the other values in the footer.

<asp:TemplateColumn HeaderText="Title" 
FooterText="Total:"> 
  <ItemTemplate>
    <%# DataBinder.Eval(Container.DataItem, _
                        "Title") %>
  </ItemTemplate>
</asp:TemplateColumn>

The second and third columns contain an ItemTemplate element to define the format of the data placed in the rows of the grid and a FooterTemplate element to define the format of the data placed in the footer of the respective columns:

<asp:TemplateColumn HeaderText="List Price" 
                    ItemStyle-HorizontalAlign="Right">
  <ItemTemplate>
    <asp:Literal id="lblListPrice" runat="server" 
                 text='<%# DataBinder.Eval(Container.DataItem, _
                                           "ListPrice") %>' />
  </ItemTemplate>
  <FooterTemplate>
    <asp:Literal id="lblListPriceTotal" runat="server" />
  </FooterTemplate>
</asp:TemplateColumn>

In the code-behind, two private variables (mListPriceTotal and mDiscountedPriceTotal) are declared at the class level to store the accumulated sum for each of the price columns. The bindData method is identical to previous recipes, except for the addition of the code to set mListPriceTotal and mDiscountedPriceTotal to zero before the data binding is performed.

The ItemDataBound event is used to accumulate the sum of the prices as the rows in the DataGrid are bound. You can do this because the data binding always starts at the top of the grid and ends at the bottom. Because the ItemDataBound event method is called for every row in the grid, you must first determine what row this event applies to by checking the ItemType of the passed event arguments. Several groups of item types are needed here, so a Select Case statement (switch in C#) is used.

When the item type is a data row, you need to get the values in the list price and discounted price columns, and then add them to the appropriate total variables. Getting the price values requires getting the price values from the data passed to the method (e.Item.ItemData), adding the price data to the totals, getting a reference to the controls used to display the data, and then setting the price value in the controls for the row. Getting a reference to the control is the trickiest part. The easiest and most flexible approach is to use Literal controls in the ItemTemplates of the DataGrid defined in the .aspx file. By setting the IDs of the literal controls, the FindControl method of the row being data bound can be used to get a reference to the desired control.

TIP: If the IDs of the controls in the ItemTemplates are not defined, the only way to get a reference to a control is to index into the cells and controls collections of the row. In this example, the list price control is in the second column of the grid. Cells in a DataGrid are created with a literal control before and after the controls you define in a column; therefore, the list price control is the second control in the controls collection of the cell. Getting a reference to the list price control using this method would be done with listPriceControl = e.Item.Cells(1).controls(1). This approach is very dependent on column layout—rearranging columns would break code that uses this approach. The FindControl method is much easier to maintain and less likely to be broken by changing the user interface.

TIP: Literal controls are used in this example because they are rendered without the addition of other controls and because accessing the price value is as simple as getting the value of the text property of the control. An asp:Label control would seem like a good option here; however, it is created as three literal controls in the DataGrid, making it necessary to index into the controls collection of the control returned by the FindControl method to get the needed price value.

When the item is the footer, all data rows have been processed and you have the totals for the price columns in the mListPriceTotal and mDiscountedPriceTotal variables. Now you need to output these totals in the controls placed in the footer. This is done by again using the FindControl method of the passed item to get a reference to the controls in the footer. After a reference to the control is obtained, the text property is set to the total for the column. In our example, the totals are also being formatted to be displayed in currency format with two decimal places.

Example 1-57. DataGrid with totals row (.aspx)

<%@ Page Language="vb" AutoEventWireup="false" 
         Codebehind="CH01DataGridWithTotalsRowVB.aspx.vb" 
         Inherits="ASPNetCookbook.VBExamples.CH01DataGridWithTotalsRowVB" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
  <head>
    <title>DataGrid With Totals Row</title>
    <link rel="stylesheet" href="css/ASPNetCookbook.css">
  </head>
  <body leftmargin="0" marginheight="0" marginwidth="0" topmargin="0">
    <form id="frmDatagrid" method="post" runat="server">
      <table width="100%" cellpadding="0" cellspacing="0" border="0">
        <tr>
          <td align="center">
            <img src="images/ASPNETCookbookHeading_blue.gif">
          </td>
        </tr>
        <tr>
          <td class="dividerLine">
            <img src="images/spacer.gif" height="6" border="0"></td>
        </tr>
      </table>
      <table width="90%" align="center" border="0">
        <tr>
          <td><img src="images/spacer.gif" height="10" border="0"></td>
        </tr>
        <tr>
          <td align="center" class="PageHeading">
            DataGrid With Totals Row (VB)
          </td>
        </tr>
        <tr>
          <td><img src="images/spacer.gif" height="10" border="0"></td>
        </tr>
        <tr>
          <td align="center">
            <asp:DataGrid 
              id="dgBooks" 
              runat="server" 
              BorderColor="000080" 
              BorderWidth="2px"
              AutoGenerateColumns="False"
              width="100%"
              ShowFooter="True">
              <HeaderStyle 
                HorizontalAlign="Center" 
                ForeColor="#FFFFFF" 
                BackColor="#000080" 
                Font-Bold=true
                CssClass="TableHeader" /> 
              <ItemStyle
                BackColor="#FFFFE0" 
                cssClass="TableCellNormal" />
              <AlternatingItemStyle 
                BackColor="#FFFFFF" 
                cssClass="TableCellAlternating" />
              <FooterStyle cssClass="TableCellNormal" HorizontalAlign="Right" 
                           BackColor="#C0C0C0" />
              <Columns>
                <asp:TemplateColumn HeaderText="Title" FooterText="Total:"> 
                  <ItemTemplate>
                    <%# DataBinder.Eval(Container.DataItem, "Title") %>
                  </ItemTemplate>
                </asp:TemplateColumn>
                <asp:TemplateColumn HeaderText="List Price" 
                                    ItemStyle-HorizontalAlign="Right">
                  <ItemTemplate>
                    <asp:Literal id="lblListPrice" runat="server" 
                          text='<%# DataBinder.Eval(Container.DataItem, _
                                                    "ListPrice") %>' />
                  </ItemTemplate>
                  <FooterTemplate>
                    <asp:Literal id="lblListPriceTotal" runat="server" />
                  </FooterTemplate>
                </asp:TemplateColumn>
                <asp:TemplateColumn HeaderText="Discounted Price" 
                                    ItemStyle-HorizontalAlign="Right">
                  <ItemTemplate>
                    <asp:Literal id="lblDiscountedPrice" runat="server" 
                          text='<%# DataBinder.Eval(Container.DataItem, _
                                                    "DiscountedPrice") %>' />
                    </asp:Label>
                  </ItemTemplate>
                  <FooterTemplate>
                    <asp:Literal id="lblTotalDiscountedPrice" 
                                 runat="server" />
                  </FooterTemplate>
                </asp:TemplateColumn>
              </Columns>
            </asp:DataGrid>
          </td>
        </tr>
      </table>
    </form>
  </body>
</html>

Example 1-58. DataGrid with totals row code-behind (.vb)

Option Explicit On 
Option Strict On
'-----------------------------------------------------------------------------
'
'   Module Name: CH01DataGridWithTotalsRowVB.aspx.vb
'
'   Description: This class provides the code behind for
'                CH01DataGridWithTotalsRowVB
'
'*****************************************************************************
Imports Microsoft.VisualBasic
Imports System.Configuration
Imports System.Data
Imports System.Data.OleDb
Imports System.Web.UI.WebControls

Namespace ASPNetCookbook.VBExamples
  Public Class CH01DataGridWithTotalsRowVB
    Inherits System.Web.UI.Page

    'controls on form
    Protected WithEvents dgBooks As System.Web.UI.WebControls.DataGrid

    'variables used to accumulate the sum of the prices
    Private mListPriceTotal As Decimal
    Private mDiscountedPriceTotal As Decimal

    '*************************************************************************
    '
    '   ROUTINE: Page_Load
    '
    '   DESCRIPTION: This routine provides the event handler for the page load
    '                event.  It is responsible for initializing the controls 
    '                on the page.
    '-------------------------------------------------------------------------
    Private Sub Page_Load(ByVal sender As System.Object, _
                          ByVal e As System.EventArgs) _
            Handles MyBase.Load
      Dim dbConn As OleDbConnection
      Dim da As OleDbDataAdapter
      Dim ds As DataSet
      Dim strConnection As String
      Dim strSQL As String

      If (Not Page.IsPostBack) Then
        Try
          'get the connection string from web.config and open a connection 
          'to the database
          strConnection = _
              ConfigurationSettings.AppSettings("dbConnectionString")
          dbConn = New OleDb.OleDbConnection(strConnection)
          dbConn.Open( )

          'build the query string and get the data from the database
          strSQL = "SELECT Title, ListPrice, DiscountedPrice " & _
                   "FROM Book " & _
                   "ORDER BY Title"
          da = New OleDbDataAdapter(strSQL, dbConn)
          ds = New DataSet
          da.Fill(ds)

          'set total values to 0 before data binding
          mListPriceTotal = 0
          mDiscountedPriceTotal = 0

          'set the source of the data for the datagrid control and bind it
          dgBooks.DataSource = ds
          dgBooks.DataBind( )

        Finally
          'cleanup
          If (Not IsNothing(dbConn)) Then
            dbConn.Close( )
          End If
        End Try
      End If
    End Sub  'Page_Load

    '*************************************************************************
    '
    '   ROUTINE: dgBooks_ItemDataBound
    '
    '   DESCRIPTION: This routine is the event handler that is called for each 
    '                item in the datagrid after a data bind occurs.  It is 
    '                responsible for accumlating the total prices and setting
    '                the values in the footer when all rows have been data 
    '                bound.
    '-------------------------------------------------------------------------
    Private Sub dgBooks_ItemDataBound(ByVal sender As Object, _
                ByVal e As System.Web.UI.WebControls.DataGridItemEventArgs) _
            Handles dgBooks.ItemDataBound
      Dim rowData As DataRowView
      Dim price As Decimal
      Dim listPriceLabel As System.Web.UI.WebControls.Literal
      Dim discountedPriceLabel As System.Web.UI.WebControls.Literal
      Dim totalLabel As System.Web.UI.WebControls.Literal
      'check the type of item that was databound and only take action if it 
      'was a row in the datagrid
      Select Case (e.Item.ItemType)
        Case ListItemType.AlternatingItem, ListItemType.EditItem, _
             ListItemType.Item, ListItemType.SelectedItem
          'get the data for the item being bound
          rowData = CType(e.Item.DataItem, _
                          DataRowView)
          'get the value for the list price and add it to the sum
          price = CDec(rowData.Item("ListPrice"))
          mListPriceTotal += price
          'get the control used to display the list price
          'NOTE: This can be done by using the FindControl method of the 
          '      passed item because ItemTemplates were used and the anchor
          '      controls in the templates where given IDs.  If a standard
          '      BoundColumn was used, the data would have to be accessed
          '      using the cellscollection (e.g. e.Item.Cells(1).controls(1)
          '      would access the label control in this example.
          listPriceLabel = CType(e.Item.FindControl("lblListPrice"), _
                                 System.Web.UI.WebControls.Literal)
          'now format the list price in currency format
          listPriceLabel.Text = price.ToString("C2")
          'get the value for the discounted price and add it to the sum
          price = CDec(rowData.Item("DiscountedPrice"))
          mDiscountedPriceTotal += price
          'get the control used to display the discounted price
       discountedPriceLabel = CType(e.Item.FindControl("lblDiscountedPrice"), _
                                    System.Web.UI.WebControls.Literal)
          'now format the discounted price in currency format
          discountedPriceLabel.Text = price.ToString("C2")
        Case ListItemType.Footer
          'get the control used to display the total of the list prices
          'and set its value to the total of the list prices
          totalLabel = CType(e.Item.FindControl("lblListPriceTotal"), _
                             System.Web.UI.WebControls.Literal)
          totalLabel.Text = mListPriceTotal.ToString("C2")
          'get the control used to display the total of the discounted prices
          'and set its value to the total of the discounted prices
          totalLabel = CType(e.Item.FindControl("lblTotalDiscountedPrice"), _
                             System.Web.UI.WebControls.Literal)
          totalLabel.Text = mDiscountedPriceTotal.ToString("C2")
        Case Else
          'ListItemType.Header, ListItemType.Pager, or ListItemType.Separator
          'no action required
      End Select
    End Sub  'dgBooks_ItemDataBound
  End Class  'CH01DataGridWithTotalsRowVB
End Namespace

Example 1-59. DataGrid with totals row code-behind (.cs)

//----------------------------------------------------------------------------
//
//   Module Name: CH01DataGridWithTotalsRowCS.aspx.cs
//
//   Description: This class provides the code behind for
//                CH01DataGridWithTotalsRowCS.aspx
//
//****************************************************************************
using System;
using System.Configuration;
using System.Data;
using System.Data.OleDb;
using System.Web.UI.WebControls;

namespace ASPNetCookbook.CSExamples
{
  public class CH01DataGridWithTotalsRowCS : System.Web.UI.Page
  {
    // control on form
    protected System.Web.UI.WebControls.DataGrid dgBooks;

    // variables used to accumulate the sum of the prices
    private Decimal mListPriceTotal;
    private Decimal mDiscountedPriceTotal;

    //************************************************************************
    //
    //   ROUTINE: Page_Load
    //
    //   DESCRIPTION: This routine provides the event handler for the page
    //                load event.  It is responsible for initializing the
    //                controls on the page.
    //------------------------------------------------------------------------
    private void Page_Load(object sender, System.EventArgs e)
    {
      OleDbConnection dbConn = null;
      OleDbDataAdapter da = null;
      DataSet ds = null;
      String strConnection = null;
      String strSQL =null;

      // wire the item data bound event
      this.dgBooks.ItemDataBound +=
        new DataGridItemEventHandler(this.dgBooks_ItemDataBound);

      if (!Page.IsPostBack)
      {
        try
        {
          // get the connection string from web.config and open a connection 
          // to the database
          strConnection = 
            ConfigurationSettings.AppSettings["dbConnectionString"];
          dbConn = new OleDbConnection(strConnection);
          dbConn.Open( );

          // build the query string and get the data from the database
          strSQL = "SELECT Title, ListPrice, DiscountedPrice " +
                   "FROM Book " +
                   "ORDER BY Title";
          da = new OleDbDataAdapter(strSQL, dbConn);
          ds = new DataSet( );
          da.Fill(ds);

          // set total values to 0 before data binding
          mListPriceTotal = 0;
          mDiscountedPriceTotal = 0;

          // set the source of the data for the datagrid control and bind it
          dgBooks.DataSource = ds;
          dgBooks.DataBind( );
        }  // try

        finally
        {
          //clean up
          if (dbConn != null)
          {
            dbConn.Close( );
          }
        }  // finally
      }
    }  // Page_Load

    //************************************************************************
    //
    //   ROUTINE: dgBooks_ItemDataBound
    //
    //   DESCRIPTION: This routine is the event handler that is called for each
    //                item in the datagrid after a data bind occurs.  It is
    //                responsible for accumlating the total prices and setting
    //                the values in the footer when all rows have been data
    //                bound.
    //
    //------------------------------------------------------------------------
    private void dgBooks_ItemDataBound(Object sender,
                        System.Web.UI.WebControls.DataGridItemEventArgs e)
    {
      DataRowView rowData;
      Decimal price;
      System.Web.UI.WebControls.Literal listPriceLabel = null;
      System.Web.UI.WebControls.Literal discountedPriceLabel = null;
      System.Web.UI.WebControls.Literal totalLabel = null;
      // check the type of item that was databound and only take action if it
      // was a row in the datagrid
      switch (e.Item.ItemType)
      {
        case ListItemType.AlternatingItem:
        case ListItemType.EditItem:
        case ListItemType.Item:
        case ListItemType.SelectedItem:
          // get the data for the item being bound
          rowData = (DataRowView)(e.Item.DataItem);
          // get the value for the list price and add it to the sum
          price = (Decimal)(rowData["ListPrice"]);
          mListPriceTotal += price;
          // get the control used to display the list price
          // NOTE: This can be done by using the FindControl method of the 
          //      passed item because ItemTemplates were used and the anchor
          //      controls in the templates where given IDs.  If a standard
          //      BoundColumn was used, the data would have to be accessed
          //      using the cellscollection (e.g. e.Item.Cells(1).controls(1)
          //      would access the label control in this example.
          listPriceLabel = (System.Web.UI.WebControls.Literal)
                           (e.Item.FindControl("lblListPrice"));
          // now format the list price in currency format
          listPriceLabel.Text = price.ToString("C2");
          // get the value for the discounted price and add it to the sum
          price = (Decimal)(rowData["DiscountedPrice"]);
          mDiscountedPriceTotal += price;
          // get the control used to display the discounted price
          discountedPriceLabel = (System.Web.UI.WebControls.Literal)
                                 (e.Item.FindControl("lblDiscountedPrice"));
          // now format the discounted price in currency format
          discountedPriceLabel.Text = price.ToString("C2");
          break;
        case ListItemType.Footer:
          // get the control used to display the total of the list prices
          // and set its value to the total of the list prices
          totalLabel = (System.Web.UI.WebControls.Literal)
                       (e.Item.FindControl("lblListPriceTotal"));
          totalLabel.Text = mListPriceTotal.ToString("C2");
          // get the control used to display the total of the discounted 
          // prices and set its value to the total of the discounted prices
          totalLabel = (System.Web.UI.WebControls.Literal)
                       (e.Item.FindControl("lblTotalDiscountedPrice"));
          totalLabel.Text = mDiscountedPriceTotal.ToString("C2");
          break;
        default:
          // ListItemType.Header, ListItemType.Pager, or ListItemType.Separator
          // no action required
          break;
      }
    }  // dgBooks_ItemDataBound
  }  // CH01DataGridWithTotalsRowCS
}

Recipe 4.4: Communicating Between User Controls

Problem

You have multiple user controls on a page, and one of the user controls needs to send data to another as, for example, when one control takes its form or content from the user's action on another.

Solution

Create a source user control, a destination user control, and a web form that contains both user controls. (See Recipe 4.1 and Recipe 4.2 for detailed steps.) In the source user control, create a custom event and raise the event when the required action is performed, such as when a user completes her entry for a form. In the destination user control, create an event handler to receive the event from the source user control. Finally, "wire" the event raised in the source user control to the event handler in the destination user control in the Page_Load event of the web form. The output of a test page showing one user control communicating with another appears in Figure 4-4. The code for our example application that implements the solution is shown in Examples Example 4-15 through Example 4-23. Example 4-15 shows the .ascx file for the source user control. Example 4-16 and Example 4-17 show the VB and C# code-behind for the source user control. Example 4-18 shows the .ascx file for the destination user control. Example 4-19 and Example 4-20 show the VB and C# code-behind for the destination user control. Example 4-21 shows the .aspx file for the web form used to demonstrate the user controls and their interconnection. Example 4-22 and Example 4-23 show the VB and C# code-behind for the demonstration web form.


Figure 4-4. Communicating between controls output

Discussion

Rather than focus on the basic content and creation of user controls, which is the focus of the previous recipes in the chapter, this recipe instead focuses on the interaction between user controls. The approach we advocate for handling this interaction involves creating a custom event for the source user control and raising the event when the communication is to be initiated, such as when the user clicks a button to complete his entry for the form. In order to receive the event from the source user control, the destination user control must have an event handler tailored for that purpose.

In our approach, creating the custom event for the source user control involves creating a custom event argument class, which provides the ability to add a message to the event arguments. It also involves using a delegate, which is a convenient way to pass to the destination user control a reference to an event handler for the OnSend event raised by the source user control.

We've created an application to illustrate our approach. Because of the unusually high number of interrelated files, this example may appear a bit overwhelming at first, but it is actually pretty straightforward. Keep in mind that there are three basic pieces:

The source user control contains only a button that is used to initiate sending a message.

The source user control code-behind contains the bulk of the code. First, we create a custom event argument class to provide the ability to add the message to the event arguments. This class inherits from System.EventArgs and adds a message property, as shown in Example 4-16 (VB) and Example 4-17 (C#).

Next, we define a new delegate signature, customMessageHandler, to allow the MessageEventArgs object to be passed as the event arguments. Without this delegate, you would have to use the EventArgs object, which does not provide the ability to pass custom information. An event is then defined with this type of delegate.

TIP: A delegate is a class that can hold a reference to a method. A delegate class has a signature, and it can only hold references to methods that match its signature. The delegate object is passed to code that calls the referenced method, without having to know at compile time which method will actually be invoked. The most common example is building a generic sort routine, one that allows you to sort any type of data, where you pass to it not only the data to be sorted but also a reference to the comparison routine needed to compare the particular data. The situation here is somewhat similar. In this case, we are passing a message to the destination user control (contained within an instance of MessageEventArgs) as well as a reference to an event handler for the OnSend event raised by the source user control. A delegate provides the best, most convenient way to accomplish this.

Our remaining task in the source user control code-behind is to provide a standard event handler for the send message button click event. In this handler, an instance of MessageEventArgs is created and populated with the message being sent. The OnSend event is then raised, passing a reference to the source user control as the event source and a reference to the messageArgs object containing the message being sent. In our example, this is a simple hardwired message, but it demonstrates the basic principal.

WARNING In C#, the OnSend event must be checked to make sure it is not null before raising the event. Failure to do so will result in an exception being thrown if no handler is wired to the event. This is not required for VB.

Our example's destination user control, which is shown in Example 4-18, contains only a label used to display the message sent from the source user control.

The destination user control code-behind, which is shown in VB in Example 4-19 and in C# in Example 4-20, contains a single method to handle the event raised from the source user control. The signature of the method must match the customMessageHandler delegate defined in the source user control. The only operation performed is to update the label in the user control with the message passed in the event arguments.

In our example, the .aspx file for the web form used to demonstrate the user controls, which appears in Example 4-21, registers the two user controls and instantiates each of the controls.

The code-behind for the demonstration web form, which is shown in VB in Example 4-22 and in C# in Example 4-23, provides the glue for tying the event from the source user control to the destination user control. This is done by adding the updateLabel of the destination user control as an event handler for the OnSend event raised by the source user control. What we're actually doing here is adding a delegate to the source user control's OnSend event's event handler list; that list now consists of just one event handler, but can include more.

TIP: Event delegates in .NET are multicast, which allows them to hold references to more than one event handler. This provides the ability for one event to be processed by multiple event handlers. You can try it yourself by adding a label to the demonstration web form, adding a new event handler in the web form, and then adding your new event handler to the OnSend event's event handler list. This will cause the label on the destination user control and the web form to be updated with the message from the source user control. An example that does this with multiple user controls is shown in Recipe 4.5.

WARNING In VB, when using the event/delegate model, the keyword WithEvents is not used. (Recall that the WithEvents keyword indicates that a declared object variable refers to a class instance that can raise events.) WithEvents and the event/delegate model can be intermixed, but they should not be used for the same event.

See Also

Programming C# or Programming Visual Basic .NET, both by Jesse Liberty (O'Reilly), for more about delegates

Example 4-15. Communicating between controls—source user control (.ascx)

<%@ Control Language="vb" AutoEventWireup="false" 
      Codebehind="CH04UserControlCommSourceVB.ascx.vb" 
      Inherits="ASPNetCookbook.VBExamples.CH04UserControlCommSourceVB" %>
<asp:Button ID="btnSendMessage" runat="server" Text="Send Message" />

Example 4-16. Communicating between controls—source user control code-behind (.vb)

Option Explicit On 
Option Strict On
'-----------------------------------------------------------------------------
'
'   Module Name: CH04UserControlCommSourceVB.ascx.vb
'
'   Description: This module provides the code behind for
'                CH04UserControlCommSourceVB.ascx
'
'*****************************************************************************
Imports System

Namespace ASPNetCookbook.VBExamples
  Public MustInherit Class CH04UserControlCommSourceVB
    Inherits System.Web.UI.UserControl

    'controls on the user control
    Protected WithEvents btnSendMessage As System.Web.UI.WebControls.Button

    'define the delegate handler signature and the event that will be raised
    'to send the message
    Public Delegate Sub customMessageHandler(ByVal sender As System.Object, _
                                             ByVal e As MessageEventArgs)
    Public Event OnSend As customMessageHandler

    '*************************************************************************
    '
    '   ROUTINE: btnSendMessage_Click
    '
    '   DESCRIPTION: This routine provides the event handler for the send 
    '                message button click event.  It creates a new
    '                MessageEventArgs object then raises an OnSend event
    '-------------------------------------------------------------------------
    Private Sub btnSendMessage_Click(ByVal sender As Object, _
                                     ByVal e As System.EventArgs) _
                Handles btnSendMessage.Click
      Dim messageArgs As New MessageEventArgs
      messageArgs.message = "This message came from the source user control"
      RaiseEvent OnSend(Me, messageArgs)
    End Sub  'btnSendMessage_Click
  End Class  'CH04UserControlCommSourceVB

  'The following class provides the definition of the custom event arguments
  'used as the event arguments for the message sent from this control
  'This class simply inherits from System.EventArgs and adds a message property
  Public Class MessageEventArgs
    Inherits EventArgs
    Private mMessage As String
    Public Property message( ) As String
      Get
        Return (mMessage)
      End Get
      Set(ByVal Value As String)
        mMessage = Value
      End Set
    End Property
  End Class  'MessageEventArgs
End Namespace

Example 4-17. Communicating between controls—source user control code-behind (.cs)

//----------------------------------------------------------------------------
//
//   Module Name: CH04UserControlCommSourceCS.ascx.cs
//
//   Description: This module provides the code behind for
//                CH04UserControlCommSourceCS.ascx
//
//****************************************************************************
using System;

namespace ASPNetCookbook.CSExamples
{
  public abstract class CH04UserControlCommSourceCS : System.Web.UI.UserControl
  {
    // controls on the user control
    protected System.Web.UI.WebControls.Button btnSendMessage;

    // define the delegate handler signature and the event that will be raised
    // to send the message
    public delegate void customMessageHandler(System.Object sender,
                                              MessageEventArgs e);
    public event customMessageHandler OnSend;

    //************************************************************************
    //
    //   ROUTINE: Page_Load
    //
    //   DESCRIPTION: This routine provides the event handler for the page 
    //                load event.  It is responsible for initializing the 
    //                controls on the user control.
    //------------------------------------------------------------------------
    private void Page_Load(object sender, System.EventArgs e)
    {
      // wire the click event for the send button
      this.btnSendMessage.Click += 
        new System.EventHandler(this.btnSendMessage_Click);
    }  // Page_Load

    //************************************************************************
    //
    //   ROUTINE: btnSendMessage_Click
    //
    //   DESCRIPTION: This routine provides the event handler for the send 
    //                message button click event.  It creates a new
    //                MessageEventArgs object then raises an OnSend event
    //------------------------------------------------------------------------
    private void btnSendMessage_Click(Object sender,
                                      System.EventArgs e)
    {
      MessageEventArgs messageArgs = new MessageEventArgs( );
      messageArgs.message = "This message came from the source user control";
      if (OnSend != null)
      {
        OnSend(this, messageArgs);
      }
    }  // btnSendMessage_Click
  }  // CH04UserControlCommSourceCS

  // The following class provides the definition of the custom event 
  // arguments used as the event arguments for the message sent from this 
  // control.  This class simply inherits from System.EventArgs and adds 
  // a message property.
  public class MessageEventArgs : System.EventArgs
  {
    private String mMessage;
    public String message
    {
      get
      {
        return(mMessage);
      }
      set
      {
        mMessage = value;
      }
    }  // message
  }  // MessageEventArgs
}

Example 4-18. Communicating between controls—destination user control (.ascx)

<%@ Control Language="vb" AutoEventWireup="false" 
      Codebehind="CH04UserControlCommDestinationVB.ascx.vb" 
      Inherits="ASPNetCookbook.VBExamples.CH04UserControlCommDestinationVB" %>
<asp:Label ID="labMessage" Runat="server">No Message Yet</asp:Label>

Example 4-19. Destination user control code-behind (.vb)

Option Explicit On 
Option Strict On
'-----------------------------------------------------------------------------
'
'   Module Name: CH04UserControlCommDestinationVB.ascx.vb
'
'   Description: This module provides the code behind for
'                CH04UserControlCommDestinationVB.ascx
'
'*****************************************************************************
Imports System

Namespace ASPNetCookbook.VBExamples
  Public MustInherit Class CH04UserControlCommDestinationVB
    Inherits System.Web.UI.UserControl

    'controls on the user control
    Protected labMessage As System.Web.UI.WebControls.Label

    '*************************************************************************
    '
    '   ROUTINE: updateLabel
    '
    '   DESCRIPTION: This routine provides the event handler that is the 
    '                recipient of the event raised by the source user control.
    '-------------------------------------------------------------------------
    Public Sub updateLabel(ByVal sender As System.Object, _
                           ByVal e As MessageEventArgs)
      'update the label with the message in the event arguments
      labMessage.Text = e.message
    End Sub  'updateLabel
  End Class  'CH04UserControlCommDestinationVB
End Namespace

Example 4-20. Destination user control code-behind (.cs)

//----------------------------------------------------------------------------
//
//   Module Name: CH04UserControlCommDestinationCS.ascx.cs
//
//   Description: This module provides the code behind for
//                CH04UserControlCommDestinationCS.ascx
//
//****************************************************************************
using System;

namespace ASPNetCookbook.CSExamples
{
  public abstract class CH04UserControlCommDestinationCS : 
                        System.Web.UI.UserControl
  {
    // controls on the user control
    protected System.Web.UI.WebControls.Label labMessage;

    //************************************************************************
    //
    //   ROUTINE: updateLabel
    //
    //   DESCRIPTION: This routine provides the event handler that is the 
    //                recipient of the event raised by the source user 
    //                control.
    //------------------------------------------------------------------------
    public void updateLabel(System.Object sender,
                            MessageEventArgs e)
    {
      // update the label with the message in the event arguments
      labMessage.Text = e.message;
    }  // updateLabel
  }  // CH04UserControlCommDestinationCS
}

Example 4-21. Communicating between controls—main form (.aspx)

<%@ Page Language="vb" AutoEventWireup="false" 
         Codebehind="CH04UserControlCommTestVB.aspx.vb" 
         Inherits="ASPNetCookbook.VBExamples.CH04UserControlCommTestVB" %>
<%@ Register TagPrefix="ASPCookbook" TagName="SourceControl" 
             Src="CH04UserControlCommSourceVB.ascx" %>         
<%@ Register TagPrefix="ASPCookbook" TagName="DestinationControl" 
             Src="CH04UserControlCommDestinationVB.ascx" %>         
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
  <head>
    <title>User Control Communication Test</title>
    <link rel="stylesheet" href="css/ASPNetCookbook.css">
  </head>
  <body leftmargin="0" marginheight="0" marginwidth="0" topmargin="0">
    <form id="frmUCCommTest" method="post" runat="server">
      <table width="100%" cellpadding="0" cellspacing="0" border="0">
        <tr>
          <td align="center">
            <img src="images/ASPNETCookbookHeading_blue.gif">
          </td>
        </tr>
        <tr>
          <td class="dividerLine">
            <img src="images/spacer.gif" height="6" border="0"></td>
        </tr>
      </table>
      <table width="90%" align="center" border="0">
        <tr>
          <td><img src="images/spacer.gif" height="10" border="0"></td>
        </tr>
        <tr>
          <td align="center" class="PageHeading">
            User Control Communication Test (VB)
          </td>
        </tr>
        <tr>
          <td><img src="images/spacer.gif" height="10" border="0"></td>
        </tr>
        <tr>
          <td align="center">
            <table border="0">
              <tr>
                <td class="PageHeading">
                  Source User Control:
                </td>
              </tr>
              <tr>
                <td bgcolor="#ffffcc" align="center" height="75">
                  <ASPCookbook:SourceControl id="ucSource" runat="server" />
                </td>
              </tr>
              <tr>
                <td>&nbsp;</td>
              </tr>
              <tr>
                <td class="PageHeading">
                  Destination User Control:
                </td>
              </tr>
              <tr>
                <td bgcolor="#ffffcc" align="center" height="75">
                  <ASPCookbook:DestinationControl id="ucDestination" 
                                                  runat="server" />
                </td>
              </tr>
            </table>
          </td>
        </tr>
      </table>
    </form>
  </body>
</html>

Example 4-22. Communicating between controls—main form code-behind (.vb)

Option Explicit On 
Option Strict On
'-----------------------------------------------------------------------------
'
'   Module Name: CH04UserControlCommTestVB.aspx.vb
'
'   Description: This module provides the code behind for
'                CH04UserControlCommTestVB.aspx
'
'*****************************************************************************
Namespace ASPNetCookbook.VBExamples
  Public Class CH04UserControlCommTestVB
    Inherits System.Web.UI.Page

    'controls on the form
    Protected ucSource As CH04UserControlCommSourceVB
    Protected ucDestination As CH04UserControlCommDestinationVB

    '*************************************************************************
    '
    '   ROUTINE: Page_Load
    '
    '   DESCRIPTION: This routine provides the event handler for the page load
    '                event.  It is responsible for wiring the source user
    '                control to the destination user control.
    '-------------------------------------------------------------------------
    Private Sub Page_Load(ByVal sender As System.Object, _
                          ByVal e As System.EventArgs) _
            Handles MyBase.Load
      'wire the event to the destination user control handler
      AddHandler ucSource.OnSend, AddressOf ucDestination.updateLabel
    End Sub  'Page_Load
  End Class  'CH04UserControlCommTestVB
End Namespace

Example 4-23. Communicating between controls—main form code-behind (.cs)

//----------------------------------------------------------------------------
//
//   Module Name: CH04UserControlCommTestCS.aspx.cs
//
//   Description: This module provides the code behind for
//                CH04UserControlCommTestCS.aspx
//
//****************************************************************************
using ASPNetCookbook.CSExamples;
using System;

namespace ASPNetCookbook.CSExamples
{
  public class CH04UserControlCommTestCS : System.Web.UI.Page
  {
    // controls on the form
    protected CH04UserControlCommSourceCS ucSource;
    protected CH04UserControlCommDestinationCS ucDestination;
    
    //************************************************************************
    //
    //   ROUTINE: Page_Load
    //
    //   DESCRIPTION: This routine provides the event handler for the page 
    //                load event.  It is responsible for initializing the 
    //                controls on the page.
    //------------------------------------------------------------------------
    private void Page_Load(object sender, System.EventArgs e)
    {
      // wire the event to the destination user control handler
ucSource.OnSend += 
  new 
   CH04UserControlCommSourceCS.customMessageHandler(ucDestination.updateLabel);
    }  // Page_Load
  }  // CH04UserControlCommTestCS
}

Recipe 7.4: Displaying User-Friendly Error Messages

Problem

You want the event-handling methods described in this chapter to write detailed messages to an error log for use in debugging your application, but you want to display friendly, informative messages to the user.

Solution

Create a custom exception class that includes a property to hold the user-friendly message, and then, when an error occurs, instantiate a new exception object of the custom type in the Catch block of your error-handling code, set the property of the exception to the desired message, and then throw the new exception.

Use the .NET language of your choice to create the custom exception class by deriving from System.ApplicationException and adding a property to hold the user-friendly message, giving it a name like userFriendlyMessage.

In the code-behind for the ASP.NET pages of your application that need to perform error handling, use the .NET language of your choice to:

  1. In the Catch block of methods where informative messages are useful, instantiate a new exception object of your custom class type, set the userFriendlyMessage property to the desired message, and then throw the new exception.

  2. In the Application_Error event handler, write the detailed information provided by the exception object to the event log and then display the message contained in the userFriendlyMessage property of the exception on a common error message page.

The custom exception class we've created to demonstrate this solution is shown in Example 7-10 (VB) and Example 7-11 (C#). The code showing how to create the new exception is shown in Example 7-12 (VB) and Example 7-13 (C#). The code for the Application_Error event handler is shown in Example 7-14 (VB) and Example 7-15 (C#). (Because the .aspx file for this example contains nothing related to the error handling, it is not included in this recipe.)

Discussion

The first step to providing user-friendly messages with your exceptions is to create a new class that inherits from System.ApplicationException. (The System.ApplicationException class extends System.Exception but adds no new functionality. It is meant to be used to differentiate between exceptions defined by applications and those defined by the system.) You then need to add a property to the class to support the user-friendly message. The last step in creating the new exception class is to create a constructor that will create the base exception, by calling the base class constructor with the raw message and a reference to the inner exception, and then set the user-friendly message. Example 7-10 (VB) and Example 7-11 (C#) show how we have implemented these steps.

The new exception class is put to use in the Catch block of your code by creating an instance of the new exception class, passing it the original message, a reference to the exception, and the desired user-friendly message. The reference to the original exception is passed to preserve the linked list of exceptions. In this case, your new exception will point to the original exception by using the inner property of the new exception. After the new exception class is created, it is then thrown. Example 7-12 (VB) and Example 7-13 (C#) illustrate a sample Catch block.

As shown in Example 7-14 (VB) and Example 7-15 (C#), our sample Application_Error event handler writes detailed information to the event log and then displays the message contained in the userFriendlyMessage property of the exception. This example event code is actually a variation of the event code described in Recipe 7.3, modified to check whether the exception being processed has a user-friendly message to use instead of the raw exception message.

This recipe's approach can be extended many ways to suite your needs. For example, the custom exception class could contain a "nextPage" property that could be set to pass information on where the user should be taken after reviewing the error message.

See Also

Recipe 7.3

Example 7-10. Custom exception class with user-friendly message property (.vb)

Option Explicit On 
Option Strict On
'-----------------------------------------------------------------------------
'
'   Module Name: CH07FriendlyExceptionVB.vb
'
'   Description: This class provides an exception class with support for a
'                user friendly message
'
'*****************************************************************************
Imports System

Namespace ASPNetCookbook.VBFriendlyException
  Public Class CH07FriendlyExceptionVB
    Inherits System.ApplicationException

    'private copy of user friendly message
    Private mUserFriendlyMessage As String = ""

    '*************************************************************************
    '
    '   ROUTINE: Property userFriendlyMessage
    '
    '   DESCRIPTION: Provides access to the message to be displayed to the
    '                user friendly message.
    '-------------------------------------------------------------------------
    Public Property userFriendlyMessage( ) As String
      Get
        Return (mUserFriendlyMessage)
      End Get
      Set(ByVal Value As String)
        mUserFriendlyMessage = Value
      End Set
    End Property  'userFriendlyMessage

    '*************************************************************************
    '
    '   ROUTINE: New
    '
    '   DESCRIPTION: Provides a constructor supporting an error message, a 
    '                reference to the exception that threw this exeception, 
    '                and a user friendly message for the exception
    '-------------------------------------------------------------------------
    Public Sub New(ByVal message As String, _
                   ByVal inner As Exception, _
                   ByVal userFriendlyMessage As String)
      'call base class constructor.  NOTE: This must be the first line in
      'this constructor
      MyBase.New(message, inner)
      mUserFriendlyMessage = userFriendlyMessage
    End Sub  'New
  End Class  'CH07FriendlyExceptionVB
End Namespace

Example 7-11. Custom exception class with user-friendly message property (.cs)

//----------------------------------------------------------------------------
//
//   Module Name: CH07FriendlyExceptionCS.aspx.cs
//
//   Description: This class provides an exception class with support for a
//                user friendly message
//
//****************************************************************************
using System;

namespace ASPNetCookbook.CSFriendlyException
{
  public class CH07FriendlyExceptionCS : System.ApplicationException
  {
    // private copy of user friendly message
    private String mUserFriendlyMessage = "";

    //************************************************************************
    //
    //   ROUTINE: userFriendlyMessage
    //
    //   DESCRIPTION: Provides access to the message to be displayed to the 
    //                user friendly message.
    //------------------------------------------------------------------------
    public String userFriendlyMessage
    {
      get
      {
        return(mUserFriendlyMessage);
      }
      set
      {
        mUserFriendlyMessage = value;
      }
    }  // userFriendlyMessage

    //************************************************************************
    //
    //   ROUTINE: CH07FriendlyExceptionCS
    //
    //   DESCRIPTION: Provides a constructor supporting an error message, a 
    //                reference to the exception that threw this exeception, 
    //                and a user friendly message for the exception.
    //------------------------------------------------------------------------
    public CH07FriendlyExceptionCS(String message,
                                   Exception inner,
                                   String userFriendlyMessage) : 
           base(message, inner)
    {
      mUserFriendlyMessage = userFriendlyMessage;
    }
  }  // CH07FriendlyExceptionCS
}

Example 7-12. Creation of new custom exception (.vb)

    Private Sub Page_Load(ByVal sender As System.Object, _
                          ByVal e As System.EventArgs) Handles MyBase.Load
      Dim values As Hashtable

      Try
        'add a key/value pair to the hashtable without first creating
        'the hashtable which will cause a null exception error
        values.Add("Key", "Value")

      Catch exc As Exception
        Throw New CH07FriendlyExceptionVB(exc.Message, _
                                          exc, _
                                          "The application is currently " & _
                                          "experiencing technical " & _
                                          "difficulties ... " & _
                                          "Please try again later")
      End Try
    End Sub  'Page_Load

Example 7-13. Creation of new custom exception (.cs)

    private void Page_Load(object sender, 
                           System.EventArgs e)
    {
      Hashtable values = null;

      // wire the page error event
      this.Error += new System.EventHandler(this.Page_Error);

      try
      {
        // add a key/value pair to the hashtable without first creating
        // the hashtable which will cause a null exception error
        values.Add("Key", "Value");
      }

      catch (Exception exc)
      {
        throw new CH07FriendlyExceptionCS(exc.Message,
                                          exc,
                                          "The application is currently " +
                                          "experiencing technical " +
                                          "difficulties ... " +
                                          "Please try again later");
      }
    }  // Page_Load

Example 7-14. Application_Error code for displaying a user-friendly message (.vb)

    '*************************************************************************
    '
    '   ROUTINE: Application_Error
    '
    '   DESCRIPTION: This routine provides the event handler for the 
    '                application error event.  It is responsible for 
    '                processing errors at the application level.
    '-------------------------------------------------------------------------
    Sub Application_Error(ByVal sender As Object, ByVal e As EventArgs)
      Const EVENT_LOG_NAME As String = "Application"

      Dim lastException As Exception
      Dim userFriendlyException As CH07FriendlyExceptionVB
      Dim Log As EventLog
      Dim message As String

      'get the last error that occurred
      lastException = Server.GetLastError( )

      'create the error message from the message in the last exception along
      'with a complete dump of all of the inner exceptions (all exception
      'data in the linked list of exceptions)
      message = lastException.Message & _
                vbCrLf & vbCrLf & _
                lastException.ToString( )

      'Insert error information into the event log
      Log = New EventLog
      Log.Source = EVENT_LOG_NAME
      Log.WriteEntry(message, _
                     EventLogEntryType.Error)

      'perform other notifications, etc. here

      'check to if the exception has a user friendly message
      If (TypeOf (lastException) Is CH07FriendlyExceptionVB) Then
        'exception has a user friendly message 
        userFriendlyException = CType(lastException, _
                                      CH07FriendlyExceptionVB)
        message = userFriendlyException.userFriendlyMessage
      Else
        'exception does not have a user friendly message to just
        'output the raw message
        message = lastException.Message
      End If

      'clear the error and redirect to the page used to display the 
      'error information
      Server.ClearError( )
      Server.Transfer("CH07DisplayErrorVB.aspx" & _
                      "?PageHeader=Error Occurred" & _
                      "&Message1=" & message & _
                      "&Message2=" & _
                      "This exception used a user friendly mesage")
    End Sub  'Application_Error

Example 7-15. Application_Error code for displaying a user-friendly message (.cs)

    //************************************************************************
    //
    //   ROUTINE: Application_Error
    //
    //   DESCRIPTION: This routine provides the event handler for the 
    //                application error event.  It is responsible for 
    //                processing errors at the application level.
    //------------------------------------------------------------------------
    protected void Application_Error(Object sender, EventArgs e)
    {
      const String EVENT_LOG_NAME = "Application";

      Exception lastException = null;
      CH07FriendlyExceptionCS userFriendlyException = null;
      EventLog log = null;
      String message = null;

      // get the last error that occurred
      lastException = Server.GetLastError( );

      // create the error message from the message in the last exception along
      // with a complete dump of all of the inner exceptions (all exception
      // data in the linked list of exceptions)
      message = lastException.Message +
                "\r\r" +
                lastException.ToString( );

      // Insert error information into the event log
      log = new EventLog( );
      log.Source = EVENT_LOG_NAME;
      log.WriteEntry(message, 
                     EventLogEntryType.Error);

      // perform other notifications, etc. here


      // check to if the exception has a user friendly message
      if (lastException.GetType( ) == typeof(CH07FriendlyExceptionCS))
      {
        // exception has a user friendly message 
        userFriendlyException = (CH07FriendlyExceptionCS)(lastException);
        message = userFriendlyException.userFriendlyMessage;
      }
      else
      {
        // exception does not have a user friendly message to just
        // output the raw message
        message = lastException.Message;
      }

      // clear the error and redirect to the page used to display the 
      // error information
      Server.ClearError( );
      Server.Transfer("CH07DisplayErrorCS.aspx" +
                      "?PageHeader=Error Occurred" +
                      "&Message1=" + message +
                      "&Message2=" +
                      "This exception used a user friendly mesage");
    }  // Application_Error


View catalog information for ASP.net Cookbook

Return to ONDotnet.com.

Copyright © 2009 O'Reilly Media, Inc.