In my previous contribution The Datagrid Revisited: Editing a Live Database in Template Columns,
I spent some time working with the datagrid control to browse and edit database
data. In the article, I had used template columns to customize columns of the
grid. The datagrid's sibling in the framework is the datalist. At first instance
it looks like a datagrid with only one column, which is a template column. The
control has a large number of different items, from header to selected item.
As all items are templates, the output of a datalist is far more flexible than
a datagrid when it comes to customizing. In this article, I will show you how
to build a custom control based on the .NET datalist control. This custom control
will implement a lot of the functionality discussed in the datagrid story. As
an introduction to creating custom controls, I will start with creating a simple
one. It is a delete LinkButton, which implements the user confirmation I discussed
in the last article. A demo WebForm will use both custom controls to demonstrate
their usage.
Custom Controls and Libraries: The Basics
The Web controls that come with VS.NET are organized in libraries.
These libraries are assemblies (a .dll on disk) that house the code of the classes
that implement the controls. As .NET is fully object-oriented, you can build
on the existing controls and change or add functionality. To create a new library,
start a new project and choose Web Control Library as the project template.

VS.NET will create a project to build the assembly, which has one
unit WebCustomControl1, with some code for an example
custom control:
[DefaultProperty("Text"),
ToolboxData("<{0}:WebCustomControl1 runat=server></{0}:WebCustomControl1>")]
public
class WebCustomControl1
: System.Web.UI.WebControls.WebControl
{
private
string text;
[Bindable(true),
Category("Appearance"),
DefaultValue("")]
public
string Text
{
get
{
return text;
}
set
{
text =
value;
}
}
///
<summary>
///
Render this control to the output parameter specified.
///
</summary>
///
<param name="output">
The HTML writer to write out to </param>
protected
override
void
Render(HtmlTextWriter output)
{
output.Write(Text);
}
|
The first thing to do is to give the class a proper name: GekkoDNJdeleteLinkButton
is quite a mouthful but it does describe what it is. You make up your own name.
You also have to change this name in the ToolboxData
attribute. The string is the HTML that will be inserted in the WebForms on which
the control is used.
The class inherits from System.Web.UI.WebControls.WebControl,
making this a basic control. You want to add some functionality to the LinkButton.
To reuse all functionality in there we will inherit from the System.Web.UI.WebControls.LinkButton
class. To chose this base class, you can use code completion:

VS.NET had created a property text. There is no need for this property,
so its code is deleted. You do need the Render method,
in which the control generates the HTML that will be sent to the browser. You
need the things Render does in the base class so base.Render will be called. In a later stage, you can add your own functionality. The
basic code of the custom LinkButton control now looks
like this:
using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.ComponentModel;
namespace
GekkoDNJWebControlLibrary
{
///
<summary>
///
Summary description for GekkoDNJdeleteLinkButton.
///
</summary>
[ToolboxData("<{0}:GekkoDNJdeleteLinkButton runat=server></{0}:GekkoDNJdeleteLinkButton>")]
public class
GekkoDNJdeleteLinkButton : System.Web.UI.WebControls.LinkButton
{
protected override void
Render(HtmlTextWriter output)
{
base.Render(output);
}
}
}
|
This control does exactly the same things as the LinkButton—nothing
more, nothing less.
Adding Functionality to the Custom Control
The following will be added to the control:
• Set the command name to Delete to have it perform
the delete action in a template.
• Add a property to hold the message to the user.
• Set some client-side script that will pop up the confirmation.
The initialization is taken care of in the constructor:
|
private string defaultConfirmation;
public
GekkoDNJdeleteLinkButton() : base()
{
this.CommandName = "Delete";
defaultConfirmation = "Do you want to delete this ?";
}
|
The constructor makes sure that the constructor of the base class
is also called by basing it on : base().
When a LinkButton is clicked, it will fire an event to its parent
control (the template), which will again fire the event to its parent. The event
is said to bubble. A LinkButton passes the command
name in the event bubbled up. This way the container, like a datalist, of the
LinkButton can use an eventhandler to respond to button
clicks. We already met the Update, Edit and Cancel command and their associated
eventhandlers in the datagrid. The CommandName property
is a member of the LinkButton class. The CommandName
of a LinkButton is normally set in the Properties window
of the designer. Initializing it in the control's constructor relieves the developer
of having to fill in the default value. Note that this CommandName
is not fixed; you can invent your own commands. It's up to the container to
undertake action, depending on the command that came bubbling up.
A default for the user message is set in the constructor. By publishing
this message as a public property, the developer can change it in the designer
or even at runtime. To save the value over roundtrips, it has to be stored in
the ViewState.
| const string ctname = "GdnjConfirm";
[Bindable(true),
Category("Behavior")]
public string Confirmation
{
get
{
object o = this.ViewState[ctname];
return o == null ?
defaultConfirmation : (string) o;
}
set
{
this.ViewState[ctname]
= value;
}
}
|
The implementation is straightforward. If the string cannot be found
(yet) in the ViewState, possibly because that has been
disabled, the default value will be used. The attributes add some extra functionality,
setting Bindable to true enables
storing the string in an external source, such as a database or a configuration
file. The Category attribute groups the property control
in the Properties window of the designer.
All that remains is setting the client-side script. Before rendering
the control to the response, the OnClick attribute
of the control is set to a snippet of JavaScript, just as in the previous article.
protected override void Render(HtmlTextWriter output)
{
this.Attributes.Add("OnClick",
string.Format("return
confirm('{0}')",this.Confirmation));
base.Render(output);
}
|
The attribute is set in the overridden Render
method. base.Render will do the rendering itself.
Deploying the Control
Now your custom control is ready. After a successful build it can
be installed, tested, and used in VS.NET. To add your control to the toolbox,
right-click the toolbox and choose Add / Remove Items. A dialog pops
up. Choose browse to select the GekkoDNJWebControlLibrary.dll. VS will find
all controls in the library and add them to the toolbox.
The control appears in the toolbox with a default icon. You can
drop the control on a WebForm and set its properties in the Properties window.

The nice thing is that you can debug your control in a solution
that contains the control library and some Web pages on which the control is
used. All debugging options of VS.NET work for the control as well as for the
WebForm itself. When you create a deployment project in VS.NET, the wizard is
smart enough to realize that it has to include the custom library in the list
of files to deploy.
A Custom Datalist Control
Now that you have seen the basics of creating a custom control,
it is time to build the custom datalist. In the previous article on the datagrid,
you already met template columns. The .NET datalist consists entirely of templates.
The control has the following list:
• Header template
• Item template
• AlternatingItem template
• SelectedItem template
• EditItem template
• Separator template
• Footer template
The most striking difference with the datagrid is the SelectedItem
template. Combined with the SelectedIndex property
this gives you the possibility to highlight one selected row and show a detail
form containing more information than the normal item. It is a little more work
designing a datalist than a datagrid, but the result is far more flexible.
Based on the experience with the datagrid, among others, you need
to add the following functionality to the grid:
• Design to edit database data
• Maintain a state: browse, edit, or insert
• Maintain scroll position (New !)
To add the new custom control to the library select Add New Item
in the solution explorer. VS.NET will pop up a dialog where you can select Web
Custom Control.

This will result in the same example code you have seen before,
but this time I will strip it completely and inherit the control from the dataList
control.
|
namespace GekkoDNJWebControlLibrary
{
///
<summary>
///
Summary description for GekkoDNJdataList.
///
</summary>
[ToolboxData("<{0}:GekkoDNJdataList runat=server></{0}:GekkoDNJdataList>")]
public class
GekkoDNJdataList : System.Web.UI.WebControls.DataList
{
}
}
|
The state is again described in an enumeration, which is added to
this same source file:
public enum GekkoDNJDataListState
{
Browse,
Edit,
Insert
} |
Properties of the Control
Again, all values of the properties will be stored in the ViewState.
The state of the control is published and can be read and set by code. When
the state is set, the control will update its EditItemIndex
as well as its SelectedIndex when appropiate.
|
private const string
stateName = "GDLstate";
[Browsable(false)]
public GekkoDataListState State
{
get
{
object o = this.ViewState[stateName];
return o == null
? GekkoDNJDataListState.Browse : (GekkoDNJDataListState) o;
}
set
{
this.ViewState[stateName]
= value;
switch(value)
{
case
GekkoDNJDataListState.Browse :
this.EditItemIndex = -1;
break;
case
GekkoDNJDataListState.Edit :
this.SelectedIndex = -1;
break;
case GekkoDNJDataListState.Insert :
this.EditItemIndex = 0;
this.SelectedIndex = -1;
break;
}
}
} |
The EditItemIndex is reset when the list
goes to browse mode. In case of an insert, the edited row will be the first
row, so EditItemIndex is set to 0. A datalist can have
one row selected while editing another one—something that I don't consider
that clear. So I reset the SelectedIndex when entering
an edit.
VS.NET will show all public properties by default in the Properties
window, but for this state property this would not make much sense. Applying
the Browsable(false) attribute will hide the property
from the designer.
Two more properties are added:
• A read only Boolean Updated property, which
reflects any updates that have been written to the database. This property is
also hidden from the designer.
• A Boolean BookmarkPosition flag, which
indicates if the list should maintain focus on the selected item. This property
will show up in the designer under the Behaviour catergory.
The implementation of these properties is straightforward and does
not demonstrate anything new. You will find it in the sample code, but here
I will not dive any deeper into them.
Handling Ccommands
Just like the datagrid, the behaviour of the datalist is steered
by issuing named Action commands. These commands are
issued by LinkButtons on the templates, just like the
deleteLinkButton we just built. The list of commands
the datalist responds to is
• Select
• Edit
• Update
• Delete
• Cancel
Users of the control hook into these actions by setting eventhandlers.
This is usually done from the Properties window in the designer, but in .NET
more than one eventhandler can subscribe to an event (see my DNJ
story on event handlers). These eventhandlers are fired from protected methods
of the control base class. Overriding these methods is the place to hook in
your own code.
Let's start with the Edit command.
protected override void OnEditCommand(DataListCommandEventArgs e)
{
this.EditItemIndex = e.Item.ItemIndex;
State = GekkoDNJDataListState.Edit;
base.OnEditCommand(e);
} |
Just as in the datagrid, the item that fired the Edit
command is passed in the parameter so that the datalist can update its EditItemIndex. The state is set after all eventhandlers are called by the base class.
An alternative to overriding this method would be to add an eventhandler
to the ItemCommand event. But first of all, you do
not know in which order all eventhandlers will execute. Here I do my own thing
first, after which the eventhandlers can do theirs. A second reason for
doing it this way is performance because invoking an eventhandler does have
an overhead.
The Cancel command can be issued from various
states. When the list was in an edit state, I want to cancel the edit but keep
the item selected. When the list was in a browse state, I wanted to cancel the
selection.
protected override void
OnCancelCommand(DataListCommandEventArgs e)
{
base.OnCancelCommand(e);
if ((State ==
GekkoDataListState.Edit) && (this.EditItemIndex
>= 0))
this.SelectedIndex
= this.EditItemIndex;
else
this.SelectedIndex = -1;
State = GekkoDNJDataListState.Browse;
} |
This time all other eventhandlers are called first by the base class.
They might do something in which they could be interested in the current state
or index, or they might even cancel the cancel. When the eventhandlers are finished,
the control updates its state.
On the Update command, the updated property
has to be updated.
protected override void
OnUpdateCommand(DataListCommandEventArgs e)
{
base.OnUpdateCommand(e);
upDated = true;
} |
The private Update variable is published
in the read-only public UpDated property.
The datalist has no command associated with the addition of a new
row. It is no big deal to add a new command and give users the possibility to
subscribe to the command event. First you declare a new event of type DataListCommandEventHandler.
[Category("Action")]
public event
DataListCommandEventHandler NewCommand;
|
The event will show up in the Properties window of VS.NET, and everything
will work just like all other command events: "all by itself."

The editor handles generating the eventhandler and its subscription
to the event. The LinkButtons in the templates can
start bubbling the "New" event. All commands
bubbled in the datalist fire the ItemCommand event
first. In the OnItemCommand method you intercept the
New command, set the state of the control to Insert
and fire the NewCommand event.
protected override void
OnItemCommand(System.Web.UI.WebControls.DataListCommandEventArgs e)
{
if (e.CommandName == "New")
{
State = GekkoDNJDataListState.Insert;
NewCommand(this, e);
}
base.OnItemCommand(e);
}
|
The datalist takes care of some general bookkeeping of the Delete
command.
protected override void
OnDeleteCommand(DataListCommandEventArgs e)
{
base.OnDeleteCommand(e);
this.SelectedIndex = -1;
State = GekkoDNJDataListState.Browse;
}
|
In the base implementation, the eventhandlers will fire, which perform
the actual delete. After this (selected) item has been deleted, the SelectedItem
property has to be reset and the state is set to Browse.
Maintaining the Proper Index
This custom datalist tries to keep the row the user is working on
selected by setting the SelectedIndex property. As
the list works with the database data, finding out the proper index can be a
problem. Database data is (usually) sorted on some key. When the key's value
gets changed in the edit, the row will get a different index. New rows always
start, by design, as the first row; when they are written to the database they
can end up at any position. What the control needs is a method that will set
the selected index to a row, according to the value of the sort key passed to
the method.
I will do this in a two-step approach. The control will publish
a method, which receives the table and the key value in the parameters.
public bool SelectByKey(System.Data.DataTable
tb, int kv)
{
this.SelectedIndex
= IndexInTable(tb,this.DataKeyField , kv);
return this.SelectedIndex >= 0;
}
|
You might argue that the control already has a reference to the
table through its DataSource and DataMember
properties. But this datasource does not have to be a dataset. Getting to the
actual table is something I couldn't get done.
The real work is done in the private IndexInTable
method. This method gets passed the name and value of the key
field.
static public int IndexInTable(System.Data.DataTable
tb, string keyName, int
keyValue)
{
bool found = false;
int i;
for(i=0; i < tb.Rows.Count; ++i)
{
if
((int>) tb.Rows[i][keyName] == keyValue)
{
found = true;
break;
}
}
if (found)
return i;
else
return -1;
}
|
As this method does not use any instance members, it is declared
as a static method. As a datatable does not have any
find methods, which return the index of the row found,
I have to scan the rows one by one. This is quick and dirty code, which should
be easy to understand, but please feel free to improve.
Using the Custom Datalist
Now that the control is ready, it's time to try it. Add the datalist
to the toolbox in the same way you added the custom LinkButton. This time VS.NET
will see two custom controls in the .dll; by default it will add both. There
will be a second instance of the LinkButton. To .NET, every version of an assembly
has its own identity, so the first shot at the deleteLinkButton
is a different one as the one added now. If you want to read a little more about
libraries and versions, there is a small
article on that on my website. For now, you can take the controls as they
are.
The demo project uses exactly the same data as in the datagrid
sample. After adding the datacomponts to the Web page, again a dataSetGrid
and dataSetRow, you can
drop a GekkoDNJdataList control
on the form. It has inherited a lot from the DataList
class, including its designers. You can use these to set the dataset properties.

Again, the connection to the database is opened in Page_Load,
the datalist filled in the PreRender event, and in
the Unload event the database is closed again.
private void
Page_Load(object sender, System.EventArgs e)
{
sqlConnection1.Open();
}
private void
WebForm1_PreRender(object sender, System.EventArgs e)
{
sqlDataAdapterGrid.Fill(dataSetGrid1.SomeData);
GekkoDNJdataList1.DataBind();
}
private void
WebForm1_Unload(object sender, System.EventArgs e)
{
sqlConnection1.Close();
}
|
To edit the templates, right-click the control. First you'll design
the item templates. The item template is comparable to a normal row in a datagrid,
as it contains a summary of the data. All controls are bound to the data item
of the container. To mimic the datagrid, drop a LinkButton in the item, which
will fire the Select command. Clicking this button
will set the SelectedIndex property of the control.
No coding needed here.

The SelectedItem and the EditItem templates contain more info. Their
controls are bound to the dataSetRow. You have to fill
this dataSetRow when there is an item selected.
private void WebForm1_PreRender(object
sender, System.EventArgs e)
{
sqlDataAdapterGrid.Fill(dataSetGrid1.SomeData);
if (GekkoDNJdataList1.SelectedIndex >= 0)
{
sqlDataAdapterRow.SelectCommand.Parameters[0].Value = (int)
GekkoDNJdataList1.DataKeys[GekkoDNJdataList1.SelectedIndex];
sqlDataAdapterRow.Fill(dataSetRow1.SomeData);
}
GekkoDNJdataList1.DataBind();
}
|
The call to GekkoDNJdataList1.DataBind()will
bind the control, and all controls it contains, so the SelectedItem will show
all info from the details row. The SelectedItem contains three link buttons.
Their CommandName property determines what has to be
done.
• A Close button, CommandName is Cancel. The
control will clear its SelectedIndex.
• An Edit button, CommandName is Edit.
The control will edit the current row.
• A Delete button, CommandName is Delete. The
control will delete the current row.
In the EditCommand event, the dataSetRow
is read from the database.
private void
GekkoDNJdataList1_EditCommand(object
source, System.Web.UI.WebControls.DataListCommandEventArgs e)
{
sqlDataAdapterRow.SelectCommand.Parameters[0].Value = (int)
GekkoDNJdataList1.DataKeys[GekkoDNJdataList1.EditItemIndex];
sqlDataAdapterRow.Fill(dataSetRow1.SomeData);
}
|
Also in the datalist, the DataKeys collection
provides the key to the records. This time the EditItemIndex
is used to find the a parameter value. The EditItem template has twoLlinkButtons,
and again the CommandName determines what has to be
done.
• A Cancel button, CommandName is Cancel. The
control will cancel the edit.
• An Update button, CommandName
is Update. The control will write the updates to the database.
The code in the UpdateCommand eventhandler
is quite similar to the code you had to write for the datagrid template. The
controls containing the text are found again using the FindControl
method. Depending on the state, the details dataset is initiated from the database
or as an empty row, after which the row is updated with the edited values, and
the dataset is written back to the database.
private void GekkoDNJdataList1_UpdateCommand(object source, System.Web.UI.WebControls.DataListCommandEventArgs e)
{
string anyText = (e.Item.FindControl("textBox1")
as TextBox).Text;
string
chosenText = (e.Item.FindControl("textBox2")
as TextBox).Text;
string
hiddenText = (e.Item.FindControl("textBox3")
as TextBox).Text;
switch(GekkoDNJdataList1.State)
{
case
GekkoDNJWebControlLibrary.GekkoDNJDataListState.Edit :
sqlDataAdapterRow.SelectCommand.Parameters[0].Value = (int)
GekkoDNJdataList1.DataKeys[GekkoDNJdataList1.EditItemIndex];
sqlDataAdapterRow.Fill(dataSetRow1);
break;
case
GekkoDNJWebControlLibrary.GekkoDNJDataListState.Insert :
dataSetRow1.SomeData.AddSomeDataRow("", "", "");
break;
}
DataSetRow.SomeDataRow dr = dataSetRow1.SomeData[0];
dr.AnyText = anyText;
dr.ChosenText = chosenText;
dr.HiddenText = hiddenText;
sqlDataAdapterRow.Update(dataSetRow1);
GekkoDNJdataList1.State =
GekkoDNJWebControlLibrary.GekkoDNJDataListState.Browse;
}
|
Initiatiing a new row is far simpler than in the datagrid article.
Now you have an event that will be fired when the New
command is issued. In the eventhandler, the row dataset is filled and merged
with the grid dataset.
private void GekkoDNJdataList1_NewCommand(object source, DataListCommandEventArgs e)
{
dataSetRow1.SomeData.AddSomeDataRow("", "", "");
dataSetGrid1.Merge(dataSetRow1.SomeData);
}
|
To initiate a new row, all you have to do is issue the New
command. This is done with a LinkButton. This LinkButton needs no eventhandler,
so it can be placed in a template. (Last time you saw that, controls in templates
could not have eventhandlers). Go ahead and place the new LinkButton in the
footer template:

The last thing you need to do is write some code to delete a row.
This code will be in the DeleteCommand event handler,
which is the same as in the datagrid project.
private void GekkoDNJdataList1_DeleteCommand(object source, System.Web.UI.WebControls.DataListCommandEventArgs e)
{
sqlDataAdapterRow.SelectCommand.Parameters[0].Value = (int)
GekkoDNJdataList1.DataKeys[GekkoDNJdataList1.SelectedIndex];
sqlDataAdapterRow.Fill(dataSetRow1.SomeData);
dataSetRow1.SomeData[0].Delete();
sqlDataAdapterRow.Update(dataSetRow1.SomeData);
}
|
The selected item is read from the database, then deleted from the
dataset, after which the update is sent back to the database.
This is all the code needed to do full database edits. Take a look
at the control at work. In the first figure, an item is selected to show the
details

And now the item is in an edit:

Maintaining Scroll Position
There is one final piece of functionality to be added to the control.
When a list of items is longer than the window of the browser, it is very annoying
for the user to have to scroll up and down after every postback to find the
row he or she was working on. The newest version of IE supports smart browsing
to maintain scrolling position over postbacks, but that maintains the position
in pixels. When the user updates an item, it can move to quite a different position
in the list. Maintaining this position requires a more intelligent approach.
The solution presented here is a stripped version of an
article on DNJ by Donny Mack. Donny's idea was to mark the position
you want to go to with an HTML bookmark and fire a snippet of script to the
bookmark on the opening of the page to browse. Donny injected a whole array
of bookmarks, one for every row in the list. I will only inject one bookmark,
at the place I want to navigate to on this roundtrip.
This private method injects a bookmark
in a datalist item:
|
private const string bookMarkName = "GdnjDLpos";
private void bookMarkItem(DataListItem li)
{
LiteralControl anchor = new LiteralControl();
anchor.Text = "<a name=\"" + bookMarkName + "\">";
li.Controls.AddAt(0,anchor);
System.Text.StringBuilder jScript =
new System.Text.StringBuilder();
jScript.Append("<script language=\"JavaScript\">");
jScript.Append("location.href=\"#" + bookMarkName + "\";");
jScript.Append("</script>");
this.Page.RegisterClientScriptBlock("Bookmark",
jScript.ToString());
}
|
A LiteralControl does nothingbut write
its Text property to the response. You set the text
to some HTML, which represents a bookmark, and insert the control at the first
position in the item. The following script is constructed to navigate to the
bookmark.
<script language="JavaScript">
location.href="#GdnjDLpos";
</script>"
|
The registerClientScriptBlock method will
insert this script in the page so that it will be executed when the page opens
and direct the browser to the item intended. Take a look with the View Source
option of IE to see the script and bookmark in the response.
To enable this, you had introduced the BookmarkPosition
property. The functionality is added to the control. When an item of the list
is created, the OnItemCreated event fires. Again you
override the event. When the item is selected, the bookmark is inserted.
protected override void OnItemCreated(DataListItemEventArgs e)
{
if (this.BookmarkPosition)
{
if
(e.Item.ItemIndex >= 0 && (e.Item.ItemIndex ==
this.SelectedIndex ||
e.Item.ItemIndex == this.EditItemIndex))
bookMarkItem(e.Item);
}
base.OnItemCreated(e);
}
|
The code first checks if the property is set, after which SelectedIndex
and the EditItemIndex are compared to the index of
the item created. In both cases, the user will want to jump to the item. This
is all that is needed. When the property is set, the list will always present
the user with the active item.
To Conclude
VS.NET comes with a lot of wizards and tools to make the building
of custom controls an manageable job. The beauty of the C# language and the
components found in the .NET framework make adding functionality to a custom
control an almost easy task. But you have to know what to do. The documentation
that comes with VS.NET does not contain much information on the possibilities.
There is a wonderful book published by Microsoft Press called Developing
ASP.NET Server Controls and Components, but it is over 600 pages. And the
.NET team is nowhere near finished yet with the functionality of the controls
in the framework. In the next "Whidbey" version, there will be things
you can only dream of. For the moment, I hope I have given you some starting
points to build your own controls in the current VS.NET. It will enable you
to reuse your development investments in addition to making every programming
day easier. After all, the code needed to get the same things done using the
GekkoDNJdataList is smaller and easier than the code
you encountered last time using the datagrid. So, just say OOPs inside your
head.
The sample code is available here.