Taking Full Control: Build Your Own Reusable Datalist with VS.NET
By Peter van Ooijen
Published: 11/12/2003
Reader Level: Intermediate
Rated: 5.00 by 1 member(s).
Tell a Friend
Rate this Article
Printable Version
Discuss in the Forums

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.


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