guyS's WebLog

IShare, My DotNet Fingerprint

How to build a simple List to List Composite Web Control

By Guy S.
Applies to

 C#

Visual Studio .NET 2003 (1.1)

Summary: List2List custom composite web control enables a developer to bind two Tables in a DataSet into two list boxes and enables the end user to move items from the source list box to the destination list box (Add). The end user will also be able as you can see in the screen capture to remove items from the destination list box and to order the destination list box items using the arrows – move up, move down.

We also implement in the level of the control a ListChange event for the developer who uses the control. There, the developer can get all the latest changes in the destination list box items (Added, Deleted, Un changed).

Last but not least – the control should eliminate conflicts when using more then one instance in the same ASPX (conflict on JS level or other)

What this article will cover

·                     Steps for building custom web control

·                     Keeping state of the control properties we are using during postback/s

·                     Define an Event in our control and subscribe to it from the application

·                     Adding JavaScript capabilities to our web controls

Downloads

·                     Demo / Source Code 

Introduction

ASP.NET enables us to re-use its out of the box web controls. ASP.NET also gives us a very rich infrastructure to build our own custom controls. We can do it when we need a functionality which does not exist in the out of the box web controls pool.

The different ways we can implement our custom controls are -

·                     User Control (ascx file) – – A way that enable us to create a new control functionality using VS.NET designer by dragging one or more existing server control to the designer and attach to this control our custom functionality . Because the user control compound from HTML and controls that contained in VS.NET designer the control compiled as part of the project it’s located in. This option will not enable us to compile the result code as stand alone assembly and locate the compile dll file in the GAC for re-using in all the web applications which located on the server

·                     Composite custom web control – A way that enables us to create a new control from one or more existing server control but instead of using the VS.NET designer we add the composite controls in our code where we also add the code for the functionality and behavior. The new control is stand alone and can be compile and deploy in the GAC for re-using at the server level. This is exactly what we need.

·                     Rendered custom control Instead of using composition of existing web controls and HTML controls like the previous methods we should construct the control entirely from HTML elements, attributes etc step by step. This gives us the most flexibility to things we can do while constructing the control but this is also the most complex way to build your own controls

Solution

In order to build our own composite custom web control we need a class that inherits from WebControl Class. This will gives our custom control the methods and properties of a web control. Part of the functionality can be overridden and extend. We also need to declare that our custom control support INamingContainer interface in order to get the ASP.NET plumbing to composite control IDs unique names (TODO: sample for IDs names - list2listctrl0:ourName, ctrl1:).

public class List2List :

            System.Web.UI.WebControls.WebControl, INamingContainer

In order to create a project template in VS.NET 2003        for web control we need to open a new project from the template – Web Control Library.

The template created a basic control – which does not do much. I added to the project another new web control called List2List.

This is what we get from VS.NET:

/// <summary>

      /// Summary description for DefaultVSControl.

      /// </summary>

      [DefaultProperty("Text"),

            ToolboxData("<{0}:DefaultVSControl runat=server></{0}:DefaultVSControl>")]

      public class DefaultVSControl : 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);

            }

      }

We need to omit the method Render (don’t do it right a way – we would like to run it as it is) as its name applied, rendered HTML elements while stream it to the client as output. We will not use it directly.

Before you omit the Render method and the Text Property test the default web control by doing the following –

Add to the Web Control, Text property a constant return value – “Hello World”

Compile the control

Add another web application project to the solution

Add as Reference in the web application to the web control

Add the Web Control dll to the Control Tool Box menu (you can add to the Toolbox your own tab that will contain your controls). It will look like that –

Drag the control from the Toolbox and run the testing web application page

As expected – we will see the Hello World the control return from its Text property

Few words on ViewState – ViewState is a collection type instance that is available for us in each ASP.NET page. There we can hold key/value pairs. The ViewState mechanism is responsible to keep these values between round trips to the client and back to the server. The values are kept in a hidden input element called __ViewState when the page rendered on the server side and stream to the client. The data encrypted and restore when the client posts the page to the server.

ViewState is in a way similar to keep values in HTML form hidden elements. Hidden elements (not like the view state) available on the client directly using DOM (Html document Object Model)

There is no support to the VS.NET Designer but in order to get the minimum support we had to add at the control class level attribute declaration the following statement which state what is the class that handle the design

[DefaultEvent ( "ListChange" ),

      DefaultProperty ( "Text" ),

      Designer ( "CustomControls.Design.TemplatedListDesigner, CustomControls.Design", typeof ( IDesigner ) )

      ,

      ToolboxData("<{0}:List2List runat=server></{0}:List2List>")]

     

The Properties List2List control supports are

Property

Description

Override

Things to remember

String HeaderText

Display the Header above the composite ListBoxes

No

We use ViewState to track the property value between round trips

TableSource

A pre define class (TableDetails type) to keep each ListBox definitions

Each list box class definitions compound from the following public properties–

string TableName

int TableIndex

string DataTextField

string DataValueField

string Header

           

No

When defining a class as public property in a web control we can set its properties using the designer as inner element of the web control element

For example -

<cc1:list2list id=”myList2list”>

<TableSource attribute1=”” attribute2=””></ TableSource> </cc1:list2list>

TableDestination

Same as above

No

“” – “”

ListSource

The ListBox where we display the source items

No

We gave the control client ID a suffix (l1 = list #1) in order we could easily do manipulation on the client side as follow –

Prefix will be given as parameter to the client function. So in order get a reference to the control we would use this JS syntax

var src = document.getElementById(rootName+'_l1');

var dest = document.getElementById(rootName+'_l2');

                       

ListDestination

The ListBox where we maintain the list of items that were selected from the source ListBox

No

“” – “”

DataSource

The property where we keep the DataSet object. The given DataSet should hold by definition two tables – SourceTable and Destination Table

*In this walkthrough I did not get into validation issues. If the DataSet will not be the excepted DataSet than an error will occurred

No

We also define here a primary key to each table. The primary key in each table is the ListBox given Value Field. This will gain us a small improvement in performance

List of Methods/Events we are using -

Method/Event

Description

Override

Things to remember

Void BuildControls

Method for create the composites controls and add them to the one by one to the Control collection (this.Controls.Add)

No

Here we also attached the relevant JS function calls. We send as parameter to the JS function the unique name of the web control by using this.ClientID

The unique name for our control maintain by the ASP.NET INamingContainer INTERFACE

You can see that we add this declaration at the web control class definition. Its enable the ASP.NET to keep our control name unique in the page level. No matter how many List2List controls we will use in the page – each one of them will have a unique name

Void CreateChildControls

In this build in method that we override we obtain the constructing of our composite inner controls

Yes

This is a build in method. ASP.NET uses it to ensure the composite elements available. We should do so too. Instead of constructing the controls here we did it in the previous BuildControls – so we should call it from here

Void LoadViewState

Restore any state of our data and controls. We use it to update our DataSet->Destination Table Rows with the latest update from the Destination ListBox. Actually because we kept all the latest updates that occurred in the destination ListBox in an hidden field we use it in LoadViewState method in the restore process

Yes

We move over an hidden field and over the destination DataSet Table and synchronize the DataSet Table according

New selected IDs – been added

Removed Items – should be removed from the DataSet

Un Changed items should not be touch

If a change in the destination DataSet Table occurred we raise OnChange Event – a developer who subscribe to this event can get the Destination Table and retrieve all the latest changes – we will see it in the testing web page

Void DataBind

Bind the given DataSet input to the internal ListBoxes

Yes

void OnPreRender

Event that occur a moment before the HTML get to the client. This is the place where we usually rendered the JS client functionality

Yes

You can see we use in the client side the Add / Move items to and from accordingly the destination ListBox. You can see also that we are using a hidden input box to keep truck of all the items in the destination list box. That way it will be easier for us to retrieve it on the server and update our internal DataSet

Public ListChange Event

An external developer can subscribe to this event. It will fire the moment a post back will fire by the end user and changes occurred in the destination ListBox

No

Controls Naming Conventions

OK – controls unique names handled by the ASP.NET framework the moment we declare that our custom web control support the INamingContainer

When a developer use two list to list custom control on the same page the controls name will be in this structure – namePrefix:nameSuffix

namePrefix – the name the developer gave to our control

nameSuffix – the name we gave to one of the composite controls

In grids control the principle is very similar but the namePrefix will contain a unique addition (ctl1, ctrl2 etc). I will get into it in a future article on Grids 

Example of grid naming prefix/suffix

Using ViewState

As specified in the previous section - ViewState enable us to keep truck on properties values during round trips from the server to the client and vice versa.

In the ViewState we keep the Control properties initial values. We will need the DataSet, where the source Table data located and the destination Table Data. In the Destination table we will maintain the Destination ListBox list of items.

In order to maintain the DataSet we need to override the build in base web control event – LoadViewState

Before we start any manipulations on the DataSource Property we have to call the base.LoadViewState with the event given argument. Afterward we can be sure the ViewState restored and ready for use

 View State Load Event Header

protected override void LoadViewState(object savedState)

            {

                  base.LoadViewState (savedState);

                  DataSet ds = (DataSet)this.DataSource;

                 

In the event as already mentioned we want to re-construct the Destination Items within the DataSource Destination Table

In this custom control all changes to the Destination ListBox been done on the client before a post occurred on the form. These are the available operations -

Add Item to Destination ListBox

Remove Item from Destination ListBox

On the client side we maintain a hidden input box that holds all the IDs of the Destination ListBox Items. We could not use the Destination ListBox for that – because in order to gets its items values on the server within the Request.Params collection we would need to select them before the Post occurred. This is how ListBox is working. 

List2List ListBoxes accessible internally using properties

The benefits to do so are the benefits of properties. In the property block (get/set) we centralize the code we need before accessing the property contained object and control the access to the data/object itself.

In our custom control within the ListBoxes get properties we check the existing of the ListBox, we create it if it’s the first access to it and we set its ID

Our DataSource Property

Here we create & define our DataSource using the given DataSet.

We define for each Table in the DataSet the relevant primary column to gain performance when accessing its rows

public object DataSource

{

      get{return ViewState["DataSource"];}

      set

      {

            try

            {

            if (value is DataSet)

            {

            if (this.TableSource.TableName !=string.Empty && this.TableSource.TableName !=string.Empty)

            {

                  DataSet ds = (DataSet)value;

                  System.Data.DataColumn[] pk = {ds.Tables[this.TableSource.TableName].Columns[this.TableSource.DataValueField] };

                                          ds.Tables[this.TableSource.TableName].PrimaryKey = pk;

                                         

                                          System.Data.DataColumn[] pk1 = {ds.Tables[this.TableDestination.TableName].Columns[this.TableDestination.DataValueField] };

                                          ds.Tables[this.TableDestination.TableName].PrimaryKey = pk1;

                                         

                                          ViewState["DataSource"] = ds;

                                    }

                              }

                        }

                        catch (System.ArgumentException ex)

                        {

                              throw(ex);

                        }

                        finally

                        {

                        }

                  }

            }

Building our Controls Display

Our Custom List2List control display created in this method. Here we construct the HTML table with all the other HTML elements in it that will transfer to our client. The using of a table enables us a better control of the HTML final result.

The controls we are using in order to accomplish all the functionality of the List2List custom control we are –

Two List boxes – Source List Box, Destination List Box

Hidden Input Box – to keep all the items IDs we have in our Destination ListBox.  On round trip to the server we will use its values (between each value we use comma delimiter) to update the internal DataSource destination Table

Add Button – enable the user to add the selected item from the source list box to the destination list box

Remove Button – to remove the selected item from the Destination List Box

Move Up Link Button – enable to move one step up the selected item in the destination ListBox

Move Down Link Button - enable to move one step up the selected item in the destination ListBox

On this stage we also add the JavaScript functions calls. We attach it to the different buttons.

Overriding LoadViewState Method

In this build in event we override from the base control we update the DataSource destination Table.

Before accessing to the Destination table we ensure that ViewState mechanism did his work and restore the ViewState properties by calling the base.LoadViewState

After we have the destination table we remove from it all the items that don’t exist in the Hidden field where we have the user selected items IDs. Afterward – we add all the items that the user selected and exists in the Hidden field values but still does not exists in the Destination Table.

If we discover a change during the process mention above we raise the OnChange flag

According the OnChange flag we know if we should raise the ListChange Event

Declare the OnChange Event

We first define the delegate type method signature that we will use to define our OnChange event. A developer should subscribe to our ListChange Event by pointing his delegate implementation to our event

This been done as follow -

public delegate void ListChangeEventHandler(object sender, List2ListChangeEventArgs args);

            [

            Category ( "Behavior" ),

            Description ( "Raised when an item is created and is ready for customization." )

            ]

            public event ListChangeEventHandler ListChange;

                       

Then we need to define our internal ListChange Handler. The principle is the same with why we are using properties – we don’t raise the external events directly – we do it from our internal event handler. This centralizes our code, enable us more control on what will be done before and after the external event will be raised

protected virtual void OnListChange(List2ListChangeEventArgs args)

            {

                  if (ListChange != null)

                  {

                        ListChange(this,args);

                  }

            }

The last thing to do is to raise the above internal event the moment a change been done in the Destination Items.

This will be done in the LoadViewState event that was describe in the previous section

//CHANGE EVENT OCCUR

                  if (eventChange)

                  {

                        List2ListChangeEventArgs args = new List2ListChangeEventArgs();

                        OnListChange(args);

                        ds.AcceptChanges();

                  }

 

Binding our ListBoxes – DataBind Method

There is nothing much to explain here. The ListBoxes been bounded to their equivalents Tables

Overriding the PreRander Event

Here we usually add the JS client functionality

We concatenate a string that holds the JS function and we attach it to our page using the following code –

if (!Page.IsClientScriptBlockRegistered("controlJSScript") )

                              Page.RegisterClientScriptBlock("controlJSScript",controlJSScript);

           

Client JavaScript functionality

The functions we use are –

function list2list_Add(rootName)

                 

function list2list_Remove(rootName)

                 

function list2list_SetNewValues(rootName)

                 

function list2list_MoveUp(rootName)

function list2list_MoveDn(rootName)

           

We always send the rootName which is our custom control Client unique ID which will be use as prefix for any element we used in our composite control. The moment we have it we can access to our Lists object by using the following syntax and base on the prefix we have

var src = document.getElementById(rootName+'_l1');

                        var dest = document.getElementById(rootName+'_l2');

                        var newValuesHidden = document.getElementById(rootName+'_h');

                       

How to use the Control

In order to use the control we need to create a DataSet with two tables. The first Table should contain the Source ListBox items and the other will be use for the Destination ListBox items.

One limitation when we supply the input DataSource DataSet is that we have to supply the Destination Table & its schema definition – even if it’s empty. You cannot supply only a Source Items

This is something that should be improved in the future.

DataSet ds = null;

                  string sQuery = "Select CategoryID,CategoryName From Categories Order By CategoryName";

                 

                  SqlConnection oConn = new SqlConnection(CON_STR);

                  //Open it

                  oConn.Open();

                  //Create a command object

                  SqlCommand oCmd = new SqlCommand();

                  oCmd.CommandType = CommandType.Text;

                  oCmd.CommandText = sQuery ;

                  oCmd.Connection = oConn;

                       

                  //Set the DataAdapter with the Command Instance Request

                  SqlDataAdapter da = new SqlDataAdapter();

                  da.SelectCommand = oCmd;

                                               

                  //Create the DataSet Instance and fill it with the Data from DB                       

                  ds = new DataSet();

                  da.Fill(ds,"Categories");

     

                  //Set another query – also when we only need an empty table

                  sQuery = "SELECT     ProductCategory.CategoryID, Categories.CategoryName "+

                        "FROM         ProductCategory INNER JOIN"+

                        " Categories ON ProductCategory.CategoryID = Categories.CategoryID"+

                        " WHERE ProductCategory.CategoryID=-1";

                  oCmd.CommandText = sQuery;

                 

                  da.Fill(ds,"ProductsCategory");

                  list2List.DataSource = ds;

                  list2List.DataBind();

We can drag the Control from the ToolBox

Then add to it the declarations in the HTML layout:

<cc1:list2List id, runat, HeaderText,

child element for the

<TableSource with the attributes – TableName, DataTextField, DataValueField,  

<TableDestination with the attributes – TableName, DataTextField, DataValueField, 

We should subscribe to the List2List event to get the changed items in the Destination List Box

this.list2List.ListChange += new IShareWebControls.List2List.ListChangeEventHandler(this.List2List_ListChange);

                 

And define the event handler

//display all the changes in the destination table (table in location #1)

            private void List2List_ListChange(object sender, IShareWebControls.List2ListChangeEventArgs args)

            {

                  kmPackageControls.Misc.ListToListSelection oList = (kmPackageControls.Misc.ListToListSelection)sender;

                  DataSet ds = (DataSet)oList.DataSource;

                  for (int i=0;i<ds.Tables[1].Rows.Count ; i++)

                  {

                        switch (ds.Tables[1].Rows[i].RowState )

                        {

                              case DataRowState.Added:

                                    Response.Write("added" + Convert.ToString(ds.Tables[1].Rows[i][1]));

                                    break;

                              case DataRowState.Unchanged:

                                    Response.Write("un change" + Convert.ToString(ds.Tables[1].Rows[i][1]));

                                    break;

                             

                              case DataRowState.Deleted:

                                    Response.Write("deleted" + Convert.ToString(ds.Tables[1].Rows[i][1,DataRowVersion.Original]));

                                    break;

                       

                        }

                  }

           

            }

That’s all fox :-)

Future enhancements

DataSource – The property should be override the existing DataSource of any Web Control

Different Type of DataSource – I used DataSet with two Tables for the ListBoxes otherwise it won’t work – very restricted pre-define rules. It will be nice if a user will be able to bind it to Arrays, DataReader and so on

DataBind – The same as the DataSource property apply here. It should be override and not re-declared as I did

Designer Support – It will be very nice to have a support for the VS.NET Designer.

Comments

TrackBack said:

# March 26, 2004 4:40 PM

TrackBack said:

# March 26, 2004 4:43 PM

Will G said:

Guy,

I am just starting out with asp. Your example code has given me a great starting point - Thank you.

Kind Regards
Will
# April 23, 2004 9:56 AM

ASP.NET GOD said:

Dude, plz do some testing before puting it up on the Internet!

Select 1 item from the left box and add it to the right box. Then without selecting any item, click the up and down arrow. A Javascript error will occur.
# April 23, 2004 8:44 PM

Guy Sofer said:

This control can have added value for anyone who try to get into custom/composite web controls even though its possible that there is bugs in it and I'm sorry for that.

My major purpose was to raise issues I encountered when I wrote it - like HTML entry form elements IDs conflicts when you drop two controls in the same form and how to avoid it and other.

I hope my next samples will be clean from bugs.
# April 24, 2004 8:15 AM

Jay. said:

Guy ,
Iam thrilled to see the List2List custom composite web control demo.

After Downloading the source,
I found the Following files in IShareWebControls folders.

AssemblyInfo.cs
DefaultVSControl.cs
List2List.cs

I was able to create IShareWebControls ,web control library with AssemblyInfo.cs
DefaultVSControl.cs
List2List.cs
files.

Issues
1.When i drag this control from the tool box to another WebForm i get the following Error Message "Error Creating Control".

2.Where should i copy the List2List_ListChange() method .

3.kmPackageControls package is not available in the download.


4.Please eloborate on the Steps to be done,
before Dragging the ListToList web control
to a web form.

Thanks
Jay.
# April 24, 2004 11:47 AM

Guy Sofer said:

Thanks Jay for your feedback.

I've just uploaded the project code again - control + web application sample which I hope will clarify by example the use of this control and make it easier to compile and use.
I suggest to download it again.

Because there is no HTML VS editor support after u drag the control to a form you should manually add the control element attributes as follow -

<P><cc1:list2list id="list2List" runat="server" Text="guy" HeaderText="Article Category (list2list #1)"><TABLESOURCE TableName="Categories" DataTextField="CategoryName" DataValueField="CategoryID"></TABLESOURCE>
<TABLEDESTINATION TableName="ProductsCategory" DataTextField="CategoryName" DataValueField="CategoryID"></TABLEDESTINATION>
</cc1:list2list></P>

The HTML editor should also include a Register header - <%@ Register TagPrefix="cc1" Namespace="IShareWebControls" Assembly="IShareWebControls" %>

And - don't forget the ConnStr definition in the web.config file

List2List change event enable you to catch the postback event when fired from the Save button and get the destination ListBox latest changes. In order to use the event you need to declare your event handler and attach it to the control event property as follow-

this.list2List.ListChange += new IShareWebControls.List2List.ListChangeEventHandler(this.List2List_ListChange);

You can see the exact List2List_ListChange usage in the zipped file

I hope now it will be easier
# April 24, 2004 1:12 PM

Jay said:

Guy thanks for your quick response..and i tested your list2list web control with the IsareWeb Application..its gr8 ..
Jay.
# April 26, 2004 1:06 AM

Ali said:

Guy,

I was looking for a component like this. Its great but it has a bug; one that someone mentioned earlier which is; when U add an item and without selecting it if you press down, it gives a javascript error. and Another feature which would be great is that multiple selection can be made when you are adding or removing. Right now I see that you can only add or remove one by one. Other then that its great tutorial.

Ali
# June 3, 2004 1:58 PM

Ali said:

me again. trying to use the component on a webform but it doesn't show the visual at design time. Shows the grey box like a user control and says error. I'm not sure what is wrong. hope to hear from you soon
Ali
# June 3, 2004 3:13 PM

Guy S. said:

Thanks Ali for your feedback

Regards the Down error bug when user don't selected an item -

u should add to the list2list control js another check to list2list_MoveDn function as follow -

if (dest.selectedIndex<0)
return false; //user does not select an item. Do nothing

//Other code here

Regard the other bug - on VS.Designer its not rendering as valid HTML - I hope I will find the time soon to fix it


Anyway - I was happy to hear that its give u a sort of added value

Guy
# June 4, 2004 1:29 AM

Rob Blij said:

I am having problems maintaining viewstate when an event occurs. Am I missing something?
# June 8, 2004 5:39 AM

Rob Blij said:

If anyone is having problems with the view state on the destination listbox when data is added to it when the control is created and cannot get those initial values into viewstate I have the solution

email: rob@kineticmds.com

Rob
# June 14, 2004 4:55 AM

Guy S. said:

Hi Rob,

I just download the fix for default items in destination list box
# August 1, 2004 12:27 AM