Build a Self-Caching Database Driven
ASP.NET Listbox ServerControl
By Peter A. Bromberg, Ph.D.
Printer - Friendly Version

Peter Bromberg

One of the most common needs we have as ASP.NET developers is to put DropDown listboxes on forms. Listboxes of States, Countries, product types, you name it. Wouldn't it be nice if you could keep all the items in a single database table and re-use the same table for all the listboxes and dropdown lists in your application? How about if you could set the properties that determined the SQL statement that selected your items out of this table, just as you would any other property of the control? And while we're at it, how about if once we get the list, let's have the control go ahead and cache it since we really want to avoid requesting all the data from the database over and over again every time the control is rendered on a page. That's what my "DbListBox" DropDownList does. You can even set the "first field" with a message such as "Please Select" so that every real item in the listbox will fire the SelectedIndexChanged event. And best of all, it uses OleDb so you can use it with virtually any data source, including MSAccess.



This control's code is much like any other ServerControl; we start out by "not reinventing the wheel" - since it's basically a DropDownList, we have the class inherit from this as a base class:

Imports System
Imports System.Data
Imports System.Data.OleDb
Imports System.Web.UI
Imports System.Web.UI.WebControls
Imports System.Diagnostics
Imports System.ComponentModel
Namespace PAB.WebControls
    <DefaultProperty("TableName"), ToolboxData("<{0}:DbListBox runat=server></{0}:DbListBox>")> _
    Public Class DbListBox
        Inherits System.Web.UI.WebControls.DropDownList
        Private dt As New DataTable
        Private pTableName As String
        Private pDisplayColumn As String
        Private pValueColumn As String
        Private pFirstField As Boolean = False
        Private pFfirstFieldText As String = ""
        Private pConnectionString As String = ""
        Private pCacheId As String = ""
        Private pWhereClause As String = ""
        <Bindable(True), Category("Appearance"), DefaultValue(""), Browsable(True)> _
        Public Property TableName() As String
            Get
                Return pTableName
            End Get
            Set(ByVal Value As String)
                pTableName = Value
            End Set
        End Property
        Public Property ConnectionString() As String
            Get
                Return pConnectionString
            End Get
            Set(ByVal Value As String)
                pConnectionString = Value
            End Set
        End Property
        <Bindable(True), Category("Appearance"), DefaultValue(""), Browsable(True)> _
               Public Property DisplayColumn() As String
            Get
                Return pDisplayColumn
            End Get
            Set(ByVal Value As String)
                pDisplayColumn = Value
            End Set
        End Property
        <Bindable(True), Category("Appearance"), DefaultValue(""), Browsable(True)> _
         Public Property ValueColumn() As String
            Get
                Return pValueColumn
            End Get
            Set(ByVal Value As String)
                pValueColumn = Value
            End Set
        End Property
        <Bindable(True), Category("Appearance"), DefaultValue("dbListBox"), Browsable(True)> _
 Public Property CacheId() As String
            Get
                Return pCacheId
            End Get
            Set(ByVal Value As String)
                pCacheId = Value
            End Set
        End Property
        <Bindable(True), Category("Appearance"), DefaultValue(""), Browsable(True)> _
               Public Property FirstField() As Boolean
            Get
                Return pFirstField
            End Get
            Set(ByVal Value As Boolean)
                pFirstField = Value
            End Set
        End Property
        <Bindable(True), Category("Appearance"), DefaultValue(""), Browsable(True)> _
               Public Property FirstFieldText() As String
            Get
                If Nothing = Me.pFfirstFieldText Then
                    Me.pFfirstFieldText = ""
                End If
                Return pFfirstFieldText
            End Get
            Set(ByVal Value As String)
                pFfirstFieldText = Value
            End Set
        End Property
        <Bindable(True), Category("Appearance"), DefaultValue(""), Browsable(False)> _
         Public Overrides Property DataSource() As Object
            Get
                Return MyBase.DataSource
            End Get
            Set(ByVal Value As Object)
            End Set
        End Property
        <Bindable(True), Category("Appearance"), DefaultValue(""), Browsable(False)> _
        Public Overrides Property DataTextField() As String
            Get
                Return MyBase.DataTextField
            End Get
            Set(ByVal Value As String)
            End Set
        End Property
        <Bindable(True), Category("Appearance"), DefaultValue(""), Browsable(False)> _
        Public Overrides Property DataValueField() As String
            Get
                Return MyBase.DataValueField
            End Get
            Set(ByVal Value As String)
            End Set
        End Property
        <Bindable(True), Category("Appearance"), DefaultValue(""), Browsable(False)> _
        Public Overrides Property DataMember() As String
            Get
                Return MyBase.DataMember
            End Get
            Set(ByVal Value As String)
            End Set
        End Property
        <Bindable(True), Category("Appearance"), DefaultValue(""), Browsable(True)> _
        Public Property WhereClause() As String
            Get
                Return pWhereClause
            End Get
            Set(ByVal Value As String)
                pWhereClause = Value
            End Set
        End Property
        Public Sub New()
        End Sub
        Private Sub LoadListItems()
            Try
                Try
                    dt = DirectCast(System.Web.HttpContext.Current.Cache("listBox"), DataTable)
                Catch
                End Try
                If dt Is Nothing Then
                    Dim strQry As String = "SELECT " & Me.ValueColumn & " AS VALUE_FIELD, " & _
Me.DisplayColumn & " AS DISPLAY_FIELD FROM " & Me.TableName & " " If Me.WhereClause <> "" Then strQry += Me.WhereClause End If strQry += " ORDER BY " & Me.DisplayColumn Dim cn As New OleDbConnection(Me.ConnectionString) cn.Open() Dim cmd As New OleDbCommand(strQry, cn) Dim da As New OleDbDataAdapter(cmd) dt = New DataTable da.Fill(dt) cn.Close() cmd.Dispose() System.Web.HttpContext.Current.Cache(Me.CacheId) = dt End If Dim dt2 As DataTable = dt.Copy() dt2.TableName = Me.TableName Dim i As Integer For i = 0 To dt2.Rows.Count - 1 Me.Items.Add(New ListItem(dt.Rows(i)("DISPLAY_FIELD").ToString(), _
dt.Rows(i)("VALUE_FIELD").ToString())) Next i If Me.FirstField Then If Me.Items(0).Text <> "" Then Me.Items.Insert(0, New ListItem(Me.FirstFieldText, "")) End If End If Catch ex As Exception Throw End Try End Sub Protected Overrides Function SaveViewState() As Object Dim objItems(Me.Items.Count + 1) As Object Try If Not Page.IsPostBack Then Me.LoadListItems() End If Dim baseState As Object = MyBase.SaveViewState() objItems(0) = baseState Catch ex As Exception End Try Return objItems End Function Protected Overrides Sub LoadViewState(ByVal savedState As Object) Try If Not (savedState Is Nothing) Then Dim objState As Object() = CType(savedState, Object()) If Not (objState(0) Is Nothing) Then MyBase.LoadViewState(objState(0)) End If End If Catch ex As Exception End Try End Sub Protected Overrides Sub RenderContents(ByVal tw As HtmlTextWriter) Try Dim li As ListItem For Each li In Me.Items If li.Attributes.Count > 0 Then tw.WriteBeginTag("option") If li.Selected Then tw.WriteAttribute("selected", "selected", False) End If li.Attributes.Render(tw) tw.WriteAttribute("value", li.Value.ToString()) tw.Write(HtmlTextWriter.TagRightChar) tw.Write(li.Text) tw.WriteEndTag("option") tw.WriteLine() End If Next li Catch ex As Exception tw.WriteBeginTag("option") tw.WriteAttribute("value", "Error") tw.Write(HtmlTextWriter.TagRightChar) tw.Write(ex.Message) tw.WriteEndTag("option") tw.WriteLine() End Try Page.DataBind() MyBase.RenderContents(tw) End Sub End Class End Namespace

 

As can be seen above, the key trick is that as soon as the items are loaded, the datatable is cached using the Cache Id that you supply for the control in the Property Sheet. As well, we override the Load and Save ViewState methods to integrate the behavior with the base class. And the RenderContents method uses the standard ServerControl authoring method of having an HtmlTextWriter class instance take care of the actual rendering of the control out to the page.

To the left, you can see the custom properties we've created- the Connection String, Display Column, FirstFieldText, TableName, ValueColumn, and WhereClause all enable us to fine -tune the desired database action of the control, and the CacheId property, which should be set to a different name for controls that load different contents, enables us to provide the scalability we want.

The downloadable solution has a Web App test harness illustrating sample usage, and uses the "Where" clause property to populate two separate DbListBox controls with different lists of items, even though they are all coming from the same MSAccess sample database. Certainly you could enhance this control easily. Perhaps you may want to add a new property to include a more customized "Order By" clause in the generated SQL statement, one that might be based on a table column of your choice that is not used to populate the actual control. Or, you could enhance it to run a stored procedure as an alternative.

To build and run the included sample, make a new folder "dbListboxSoln" under your wwwroot. Unzip this into your new folder, and make the subfolder C:\Inetpub\wwwroot\DBListboxSoln\DbListBoxWeb into an IIS virtual directory and Application. Enjoy!

Download the code that accompanies this article

 


Peter Bromberg is a C# MVP, MCP, and .NET consultant who has worked in the banking and financial industry for 20 years. He has architected and developed web - based corporate distributed application solutions since 1995, and focuses exclusively on the .NET Platform.