| Printable Version
How to build a simple List to List Composite Web Control
By Guy Sofer
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 :-)
|