in

ASP.NET Weblogs

guyS's WebLog

IShare, My DotNet Fingerprint

How to build a Re-usable, Generic Lookup Grid Picker

Version #1 – extended DataGrid web control, VS Designer does not supported

Guy Sofer
Applies to:

·                     C#

·                     Visual Studio .NET 2003 (1.1)

 

**source code + demo available

or ** download Version 1.0.1 (update on 15/5/2004), v1.0.1 changes list

 

Summary: The article’s purpose is to show how simple it is to take existing ASP.NET web control (DataGrid in this sample) and extend its functionality using inheritance in order to get a new web control with new specialties (Lookup Grid with a generic & rich functionality). It will cover topics like web control events & methods, JS functionality for client interactions and more info its worth knowing when building your own extended web controls.

In my previous article I explain how to implement a List2List composite web control. This article is my second web control I implemented – this means that the difficulty level is higher, because now I have more experienceJ.

I found that the Lookup Grid is essential in almost every rich web applications. That is why I decided to wrap the functionality of it as a web control that can be re-use over and over in different applications

We can use the Lookup Grid in entry forms when the end user should pick one or more values from an existing list of available items – Employee List, Location list, Customer list and the options are endless

 Lookup Grid web control feature list:

1.       Picker control that enable the user to display list of values in a tabular display

2.       Display different repository table’s data defined in a configuration Xml file

3.       Enable the user to pick from it single row value (radio button for each row) or multiple values (checkbox for each row)  – according the lookup configurations

4.       Enable the programmer to decide if he wants to display a pre-selected items when the lookup grid being called

5.       Return value/s as an Xml string which gives the developer more flexibility & power when parsing the Xml.

6.       Enable sorting & paging

Future wish List

1.       Data table from repository will be done by an object instance that will support a pre-define Lookup grid interface

2.       Data will retrieved by an object instance from different data sources provider

3.       Designer support – is a must

4.       Lookup grid configuration should enable to configure a Search Entry Form which the user will use to commit searches on the grid

5.       Enhance JS Functionality to process the return Xml value

6.       Cache the Lookup grid configuration Xml

7.       Rich Layouts support

Our current lookup grid version looks as follow:

 What this article will cover

·                     How to sample for building extended web control

·                     Web Controls events

·                     Adding JavaScript capabilities to our web controls

 

Solution

One of the requirements is to enable the Lookup grid to be generic by display different data from different DB tables. I saw an article few months ago that keeps the data retrieval definitions for a grid in Xml file. I adopted this great idea and I added an Xml configuration file that holds the data retrieval configurations. There I specified – how to connect to the DB (connection string), the fields we would like to display in the grid, the filters we would like to enable to end user (for future use) and the identity key for each Lookup grid row.

One of the Lookup Grid public properties is the XmlPath that holds these configurations

The Xml definitions for Employees Table from Northwind Database will look like this:

<lookupforms>

<emplookupform>

      <dbconnection encrypted="false"></dbconnection>

    <sql>select firstname,lastname,city, employeeid from employees</sql>

    <filters>

            <filter active="true">

            <datafield>firstname</datafield>

            <heading>First Name</heading>

            <type>string</type>

            <value>'a%'</value>

            </filter>

            <filter active="false">

            <datafield>lastname</datafield>

            <heading>Last Name</heading>

            <type>string</type>

            <value></value>

            </filter>

            <filter active="false">

            <datafield>employeeid</datafield>

            <heading>Employee ID</heading>

            <type>int</type>

            <value></value>

            </filter>

    </filters>

    <columns>

            <column>

                  <datafield>employeeid</datafield>

                  <heading>Employee ID</heading>

                  <dataformatstring>None</dataformatstring>

                  <sorting>false</sorting>

                  <sortexpression></sortexpression>

                  <visible>false</visible>

                  <returncolumn>true</returncolumn>

                  <type>int</type>

            </column>

            <column>

                  <datafield>lastname</datafield>

                  <heading>Last Name</heading>

                  <dataformatstring>None</dataformatstring>

                  <sorting>true</sorting>

                  <sortexpression>Asc</sortexpression>

                  <visible>true</visible>

                  <returncolumn>false</returncolumn>

                  <type>string</type>

            </column>

            <column>

                  <datafield>firstname</datafield>

                  <heading>First Name</heading>

                  <dataformatstring>None</dataformatstring>

                  <sorting>true</sorting>

                  <sortexpression>Asc</sortexpression>

                  <visible>true</visible>

                  <returncolumn>false</returncolumn>

                  <type>string</type>

            </column>

            <column>

                  <datafield>city</datafield>

                  <heading>City</heading>

                  <dataformatstring>None</dataformatstring>

                  <sorting>false</sorting>

                  <sortexpression></sortexpression>

                  <visible>true</visible>

                  <returncolumn>false</returncolumn>

                  <type>string</type>

            </column>

      </columns>

</emplookupform>

</lookupforms>

**The Filters are for future use and aimed to enable the user to search in the grid data rows

During the Lookup grid loading each column definitions in the Xml will be copy to a pre-define struct that will be part of a Columns arrays. This will make it easier to process during the grid binding

public struct GridColumn

      {

            public string           Name;

            public ColumnType Type;

            public string           DisplayName;

            public string           FormattingExpression;

            public bool             Visible;

            public bool             Sortable;

            public string           SortExpression;

            public bool             ReturnColumn;

      }

Private member – Grid Columns Array

GridColumn[] _GridColumns               = null;

In the Lookup Constructor we subscribed to the following DataGrid build in events:

Load event that occurred after the Page container complete its load – here we do our initializations,

ItemDataBound – occurred when one of the composite DataGrid row controls fire an event. I eventually did not use it. Usually we will need it in order to handle grid row level events (like button press even)

SortCommand - occur when ever the user press on one of the Headers

ItemDataBound – occur for each row in the Grid during the binding process. In this event we add a LinkButton control for each row (for single value select lookup grid) or a CheckBox control (for multiple lookup grid behavior). We also check when we fetch the Footer row. In this case we check if we should add a Save button control (for multiple select grid in a modeling window)

PreRander – fired just before the web control HTML is streaming to the client browser. This is good place to attach our client JS scripting.

PageIndexChange – fired when ever the user click on one of the grid pages

The full list is

this.Load+= new EventHandler(LookupGrid_Load);

this.ItemCommand+= new DataGridCommandEventHandler(dgLookup_OnItemCommand);

this.SortCommand+= new DataGridSortCommandEventHandler(dgLookup_OnSort); 

this.ItemDataBound+= new DataGridItemEventHandler(dgLookup_OnItemBound);

this.PreRender+= new EventHandler(dgLookup_OnPreRander);

this.PageIndexChanged += new DataGridPageChangedEventHandler(LookupGrid_PageIndexChanged);

           

Within the Load event handler (LookupGrid_Load) you can actually get the idea of the main flow of the Lookup Grid flow

Let’s have a look at the main flow and then explain it:

if (this.ReturnMultipleValues && this.UsingModelingWindow)

      this.ShowFooter = true;

                 

if (Page.IsPostBack && this.ReturnMultipleValues)

{

     //Construct the new Selected Items Array

      ConstructSelectedItems();

}

LoadXmlConfig();

CollectGridColumnsInfo(); //collect grid columns

ConstructGridColumns(); //create the grid columnx

DisplayGrid();

First I check if we should display the Grid Footer. We should display it when the Lookup is located in a Modeling Window container and only if it configured to enable Multiple Select Items.

On the Footer we located a Save Button that responsible to trigger the On Selected Event Handler where we returning the selected Items as Xml to the parent window JS function

Next, we check if the Grid loaded as part of postback event. If it does & the grid supported multiple selections we have to keep the selected items from the Request Object (where the checkbox selected items are) to a private Dictionary that we use to hold all the selected items per grid page.

The Key in the Dictionary is the Page and the value is a delimited string that holds the selected Items IDs

Here is the Dictionary declaration:

private HybridDictionary PagesSelectedItems

{

      get

      {

      if (ViewState["PagesSelectedItems"]==null)

            ViewState["PagesSelectedItems"] = new HybridDictionary();

 

      return (HybridDictionary)ViewState["PagesSelectedItems"];

}

      set

      {

      ViewState["PagesSelectedItems"] = value;

      }

}

                 

                 

The other method calls in the Load Handler deal with the loading of the Lookup configuration Xml and afterward displaying the Grid accordingly.

I find that it easier to use a local grid columns array instead of working directly with the Xml so I first, as you can see copy the Xml definitions into the local columns array.

ConstructGridColumns Method

In this method we construct the Grid Column as it was defined in the Xml

The column with the select row option I added during the ItemDataBound event handler as I specified.

DisplayGrid Method – does not do much. It’s responsible to get the Data for the display from the GetGridData Method (which return a DataSet) and activate the grid binding process.

 

The Lookup supported public properties are:

 

Property

Description

XmlLookupConfigPath

The Path of the Lookup/s Xml configuration file

LookupName

The Lookup Xml Section element where beneath it we can find all the lookup definitions

ReturnMultipleValues

Should the Lookup enable the user to select & return Multiple values or only a single value

UsingModelingWindow

Does the Lookup should return the selected values from a Modeling Window or by rasiing OnPostBack event with the return values when a Modeling window does not in use

ButtonSaveCSS

 

SortExpression

The current sort expression (support ascending/descending keyword)

UsingUpDownSortingImages

Indicate if we should use Images for Asc/Desc sorting options as part of the headers

SortAscImgPath

Ascending image path when enabled

SortDescImgPath

Descending image path when enabled

SQLQuery

The query we should use in order to get the data from the DB table. We initialize it using the Xml file sql element

OriginalSelectedValues

A list of input selected items values – that the user already selected – when given they will be display as selected in the grid 

ShowDefaultSelected

Show or Hide the default given selected items (OriginalSelectedValues)

GridCurrentPage

Current display grid page

 

 

Handling the Multiple Select checkboxes

During the ItemDataBound event handler we add in each row a regular HTML input element from type – checkbox. The checkbox NAME is the same for all the rows in the current grid page. Checkboxes input elements that using the same name in the HTML entry form will send to the server when a PostBack fired the list of selected items as string delimiter (comma delimiter).

This enables us to parse the selected items and process it when ever a postback occurred.  We do it in the ConstructSelectedItems method.

I also attach each onclick event at the checkbox element to a JS function handler (for the example purpose). The JS function called addRemoveItem and only show the selected checkbox in a JS alert window.

if (this.ReturnMultipleValues){

                             

oTdCheck.Text = "<input name='"+ this.ClientID +"_chkItem" + "'type='checkbox' onclick='addRemoveItem(this)'" +

" value='" + currentItemValue + "' " +

(this.IsItemSelected(currentItemValue)?"checked":"")+ "></input>";

 

e.Item.Cells.AddAt(0,oTdCheck);

}

 

Returning selected Item/s to the client JS

This is done within the btnSaveSingle_Command event handler (for single value select) and btnSaveMultiple_Command event handler for multiple select. The selected item/s been retrive using GetReturnValuesXml method. The retrieve method calls to GetGridData with input parameters which specified we interest only with the selected Items.

When working with HTML Modeling windows we can return values to the parent window using the following syntax –

top.returnValue = “a return value”;

From server side code we would stream the script to the client using Response.Write method

Page.Response.Write("<script language='javascript'>top.returnValue=unescape('" + strResults + "');window.close();</script>");

The parent window JS function (caller) looks as follow –

function ShowDialog(lookupform,ctrlid,allowMultiple){

var retval="";

var xmlResults = null;

                                                                               

retval=window.showModalDialog("DialogWinHelper.aspx?lookupform=" + lookupform + "&AllowMultiple="+allowMultiple,null);

 

                if(retval!="" && retval!=null)                {

                xmlResults = new ActiveXObject("Microsoft.XMLDOM");

                xmlResults.async = false;

                xmlResults.loadXML(retval);                            &nb