User Controls and Other "Useful" Things
By Peter van Ooijen
Published: 3/8/2004
Reader Level: Intermediate
Rated: 3.00 by 7 member(s).
Tell a Friend
Rate this Article
Printable Version
Discuss in the Forums

In some of my previous DotNetJunkies.com articles, I showed you a way to do database editing on Web pages using templated controls like the DataGrid and DataList. I have discussed the lifecycle of a Web page and the events to hook into to read and write data. The principles are demonstrated in my article on the DataGrid, and in a sequel I demonstrated how to incorporate state management into a custom DataList control. In this article, I will cover a couple of subjects useful when building real-world applications; parts of it originated in my blog, and it is based on the custom DataList discussed in my last article.

Using Web.config for Application Settings

Your application's configuration has to be flexible. For example, take something like the connection to a database, most likely this will differ between your production and development machine. An easy way to store configuration settings is in the project's Web.config file. There is a place where you can store all your settings. Let's copy the connection string from the connection component into Web.config:

<configuration>

    <appSettings>
        <add key="dbConnection" value ="data source=BROCHIS;initial catalog=DNJ;user id=sa;Pwd=;"></add>
    </appSettings>

<system.web>

....

The first thing that should jump out is that the username/password combination (and not just some user...) is written out here in plain text. Internet Information Server (IIS) does offer some protection to Web.config files; any request for a .config file is rejected by IIS, as shown in FIGURE 1.

FIGURE 1: Rejected by IIS

The username/password combination was there already in the component properties, but anybody locally browsing the files on your Web server is now handed the combination on a silver platter. Let's take some time to fine-tune the connection string:

<configuration>

    <appSettings>
        <add key="dbConnection" value ="data source=BROCHIS;initial catalog=DNJ;integrated security=SSPI;
    </appSettings>

<system.web>

....

The copied content is a stripped version of what you get after dropping something from Visual Studio .NET's (VS .NET) server explorer. The good thing is that you don't need to add any SQL user information at all; just set the authentication to integrated and make sure this authentication mode is enabled in your SQL Server. Your app will authenticate using the user credentials of the ASP.NET account. A far better approach than the (potentially) devastating powers of the system administrator account.

ASP.NET combines the contents of all Web.config files found in the path to your application. This way you can store settings shared by several applications in a config file higher up the tree. It is easy to read the contents from code:

sqlConnection1.ConnectionString = System.Configuration.ConfigurationSettings.AppSettings["dbConnection"];

Creating a Component to Interact with the Database

When creating a demo app it is quite tempting to drop all data components straight on the Web form, but it is a lot clearer to organize those in another component. This way you separate the database access from your user interface, and it makes it easy to reuse the code on another page or even another app. The .NET Framework has the component class to organize non-visual controls. Let's completely split the data layer from the user interface layer of a Web application. First, add a new class library to your project, as shown in FIGURE 2.

FIGURE 2: Adding a new library

Next, add a component to this library, as shown in FIGURE 3.

FIGURE 3: Adding a component

The code accessing the data is now in another assembly (DLL) as the code responsible for the user interface. The component designer works just like the form designer; it does not accept any visual controls. Let's drop some database tables on it, just like we did on the form. Again, the query builder is the tool to fine-tune your SQL. Also, here VS .NET will automatically add the connection component when you drop a table.

When the component is created, it should open the connection to the database. In the constructor, the connection string is read from Web.config and the connection is opened. Now all data adapters can read and write to the database:

public DataComponent()
{
   InitializeComponent();
   sqlConnection1.ConnectionString = System.Configuration.ConfigurationSettings.AppSettings["dbConnection"];
   sqlConnection1.Open();
}

A database connection is an unmanaged resource, which means that you have to do some housekeeping when you no longer need the component. All cleanup of unmanaged stuff should be done in the Dispose() method. VS .NET has generated a skeleton of this method, and you can close the connection in there:

protected override void Dispose( bool disposing )
{
   if( disposing )
      {
         sqlConnection1.Close();
         if(components != null)
         {
            components.Dispose();
         }
   }
   base.Dispose( disposing );
}

The generated DataSets will also be part of the class library. The data component is going to expose its functionality in three public methods that use these DataSets for their typing:

public DataSetItemList MyList
{
   get
      {
         DataSetItemList ds = new DataSetItemList();
         sqlDataAdapterItemList.Fill(ds);
         return ds;
      }
}

public DataSetItem MyItem(int id)
{
   DataSetItem ds = new DataSetItem();
   sqlDataAdapterItem.SelectCommand.Parameters[0].Value = id;
   sqlDataAdapterItem.Fill(ds);
   return ds;
}

public void UpdateMyItem(DataSetItem ds)
{
   sqlDataAdapterItem.Update(ds);
}

These three public members are the only thing the component exposes. They are typed as XML DataSets. The type of database from which the DataSet contents comes remains hidden to the consumer of the component. Perhaps there was even no database!

Using the Data Component

Add a reference to use this data component in the Web forms project, as shown in FIGURE 4.

FIGURE 4: Adding a reference

After doing this, you can drop typed DataSets on your Web forms. Drag a DataSet from the toolbox, for the type of DataSet you chose from the list of referenced DataSets. The DataSet on the form can be used by all controls. The controls don't care who will fill the DataSet, a SqlAdapter on the form, like in the demos, or another DLL (perhaps run on another machine) as in this project. The DataSet is filled from XML, which is easily transported, also over the Internet.

To fill the DataSets, the Web form creates an instance of the DataComponent we just built. The Page_Load event is a good place to do this:

private DataComponent dc;

private void Page_Load(object sender, System.EventArgs e)
{
   dc = new DataComponent();
}

Again, the DataList is filled in the PreRender event; this time the data is read from the component:

private void _default_PreRender(object sender, System.EventArgs e)
{
   dataSetItemList1.Merge(dc.MyList);
   GekkoDNJdataList1.DataBind();
}

As the DataComponent manages unmanaged (database) resources, it should be properly disposed. This is done in the Page_Unload event:

private void _default_Unload(object sender, System.EventArgs e)
{
   if (dc !=null)
      dc.Dispose();
}

Do check if dc was properly initialized. The Unload event also fires when the page hits an exception. If the exception occurred before the DataComponent was properly initialized, your app will hit another exception.

A data component is constructed every time a response page is built by the Web server. This data component is served by another assembly; this frees the path to have another machine serving the component. There are many ways to communicate with that other machine. All database data is passed as XML DataSets, these can even be transported over the Internet-standard HTTP protocol, using a classic ASMX Web Service. In this article, you'll find how to build a Web Service servicing XML DataSets; this article is on the pitfalls.

The data component opens and closes the connection to the database. The component is short-lived, from the Page_Load to the Page_Unload, so the connection to the database will be open for a short timespan. Behind the scenes, ADO.NET's connection pooling mechanism makes this fast and scalable. As IIS serves only one request at a time, only one connection will be used. To scale up, you can configure your Web server as a Web garden (multiple application instances on one machine) or Web forest (multiple Web server machines) to serve multiple requests simultaneously. The default connection pool size of ADO.NET is 100 connections, which leaves quite a lot of room to scale up.

Watching the ViewState

On every roundtrip, the page is completely reconstructed, and all data is reread from the database. An ASP.NET Web form can maintain (parts of) its state over roundtrips in the ViewState. This ViewState is stored in a hidden field of the returned response so it travels from server to client and back on every roundtrip. The size of the ViewState can really affect the performance of a Web application. The ViewState stores differences between property values of a control as they were when the control was created and as they are now. By default, the ViewState is switched on for every control, but as the page is almost completely rebuilt from scratch on every roundtrip, it does not make much sense to store everything in the ViewState.

The good thing about the ViewState is that you can disable it on a control basis. In the property window, the EnableViewState property is available for every control, including the page itself. There is one catch though; if you disable a control's ViewState, you also disable the ViewState of all child controls (except for user controls). If you disable the ViewState of the page, the ViewState of all controls on the page is disabled. Another catch is that the ViewState can be completely disabled in the configuration files. So, if you want to do nice things with the ViewState, make sure your system administrator doesn't mess that up for you.

When using a DataList, there are just some controls for which the ViewState is needed. These are text boxes and the like where users edit data and the ItemIndices of the list. This means that EnableViewState can be set to False for all controls except the list and the page itself. The ViewState of all those label and linkbutton controls can be disabled. In a DataList, you have more flexibility as you can disable the ViewState of all individual controls in the templates. Don't disable the ViewState of the list itself as this will ruin the workings of those essential SelectedItemIndex and EditItemIndex properties.

In Whidbey, the next version of ASP.NET, the essential properties are stored in ControlState, which cannot be disabled, but the main part is still under your ViewState control. Speaking of Whidbey, the DataView control will make you want to forget the DataGrid and DataList, but for the time being we still have to work with them. Anyway, disabling the ViewState wherever possible can reduce the amount of data traveling between client to Web server up to 90%. It really does pay off to pay some attention.

Templates vs. User Controls

The DataGrid and DataList work with templates. Templates are handy when it comes to loading a piece of a page on demand, like selecting or editing a row, but some templates turn out to be not that handy when it comes to coding. None of the controls on the template are declared in the code of the page. You have to write some muddy code to access anything at all. For example, take this snippet from the DataGrid story:

(e.Item.Cells[0].Controls[0] as> TextBox).Text;

Enter the user control. A user control is built just like a Web form. Select Add New Web User Control in the solution explorer (see FIGURE 5), just like creating a new Web page. The designer looks and feels the same as the Web form designer. You can drop controls, including non-visual controls like DataSets. The difference with a Web form is that an user control is always in flow layout mode.

FIGURE 5: Creating a user control

The user control has full code behind, and the nice thing is that you can fully access all of its controls from code. We'll dive into that a little later on.

There are several ways to use a user control. The most flexible way is to add one at run time:

Controls.Add(LoadControl("MyUserControl.ascx"));

The user control is a control like any other. In this snippet of code, the loaded control is added to the controls collection of the page. It will show up on the bottom of the page as it is added to the end of the collection. To steer the position of the user control, you can also use the AddAt method and add it to the controls collection of another control, like a panel or a template:

MyPanel.Controls.AddAt(1, LoadControl("MyUserControl.ascx"));

Now the control is inserted at position 1 at the top of the panel. The thing to watch when adding controls at run time is the moment in the page life cycle you do this. Adding a control can (but not always does) ruin the contents of the ViewState, so you should add the controls before the page starts "tracking" the ViewState. This is in the on_int event; the Page_Load is too late!

You can also directly use the user control in the page designer. Drag the control from the solution explorer, and drop it where you need it: on the form, on a panel, or on a template. I built a user control that handles the editing of a row in a DataList. The demo project uses the custom DataList discussed in my previous article. Instead of constructing the edit template, an instance of the user control is dragged from the solution explorer and dropped on the edit template.

Inside the User Control

The user control is going to do the edit. Drop a DataSet component on its surface, a couple of text boxes and the like to do the editing, and two link buttons: Cancel and Save, as shown in FIGURE 6.

FIGURE 6: The user control with additional attributes

The control needs a way to communicate with the list and page it is on. To do so, I gave the parent page two read-only public properties. One to expose the data component, and the other to expose the ID of the selected row. The Browsable attribute prevents these properties from showing up in the designer's property window:

[Browsable(false)]
public DataComponent Data
{
   get
   {
      return dc;
   }
}

[Browsable(false)]
public int ItemId
{
   get
   {
      return (int) GekkoDNJdataList1.DataKeys[GekkoDNJdataList1.EditItemIndex];
   }
}

A user control has a page property; its value is the page that hosts the user control. By typecasting this page to the type of the page hosting the control (default.aspx), all public members of the page can be accessed. The Page_Load is going to use the data component of its parent page to get to the data:

private void Page_Load(object sender, System.EventArgs e)
{
   onPage = Page as _default;
   if (onPage == null)
      throw new Exception("Only the page _default is supported");
   int id = onPage.ItemId;
   if (id == 0)
      dataSetItem1.SomeData.AddSomeDataRow("", "", "");
   else
   dataSetItem1.Merge(onPage.Data.MyItem(id));
}

This page property is always available as well as when you load your control from code using the LoadControl method. To check if the control is actually on the proper page, the code checks if the typecast succeeded. If not, an exception is thrown. Having found the page, the ID property is checked, which will provide a key to get the proper row from the database or a value of 0 indicating that this is a new row. The user control's DataSet is filled accordingly.

The command-name property of the Cancel linkbutton is set to Cancel. Clicking this button will bubble the Cancel command to the user control's parent. This is a DataList and it will know how to handle the command. No code is needed. The command name of the Save linkbutton is set to Update. Also, this command is bubbled up to the DataList, but the nice thing with controls on an user control is that you can add an event handler. You could not do that with controls in a template; you had to handle all update code in the UpdateEvent handler of the containing list. Here's how the update is handled inside the user control:

private void LinkButtonSave_Click(object sender, System.EventArgs e)
{
   dataSetItem1.SomeData[0].AnyText = TextBox1.Text;
   dataSetItem1.SomeData[0].ChosenText = TextBox2.Text;
   dataSetItem1.SomeData[0].HiddenText = TextBox3.Text;
   onPage.Data.UpdateMyItem(dataSetItem1);
}

This is without a doubt far cleaner code than the template scenario. After the control has updated the database, the event bubbles up to the containing list. The custom DataList uses the event to reset the EditItemIndex.

Conclusion

I have now built a page that has the same functionality as my previous project, but it uses less and simpler code that is also organized in neater way. My custom DataList provides a handling of list indices and state. The user control takes care of the details of the database update, is an improvement over templates, and really quite useful.



Marketplace
(Sponsored Links)
What are the green links?
   



 
Copyright © 2007 CMP Tech LLC |
Privacy Policy (4/10/06) | Your California Privacy Rights (4/10/06) | Terms of Service | Advertising Info | About Us | Help