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.