ASP.NET Scrollable Table Server Control
By Jon Wojtowicz

  Download Source Code
After working on several consulting assignments, I noticed I was using a lot of scrollable tables in HTML.  These were created using Repeater controls since most of the time I needed to have the column headers remain stationary.  Afer creating several variations of user controls, I decided to create a single server control to use across multiple projects.  This was my journey on my first server control.
Since I needed only single direction scrolling, I decided that it would be IE specific.  I also needed to support some type of data binding.   After a couple weeks of effort I had a scrollable table control that would work for the projects I had created.  Then the inevitable occurred, a requirements change.  On my next project, not only did the column headers need to remain stationary but the left columns needed to remain visible while the remainder of the table scrolled.   This is behavior similar to Excel with locked rows and columns.
I had worked on a similar page in classic ASP and knew the complexity of creating such a table.  Now I was working in .Net and had to figure how to dynamically create that structure within a control.  The other option would have been to manually generate all the HTML using standard markup.  I decided that creating a server control was the best approach since I needed the table on several pages.
The End Result
The control I created appears on a page as displayed in Figure 1.  An arbitrary number of left or right columns can be locked from scrolling horizontally.  The columns would still scroll vertically.  The remaining columns would scroll both horizontally and vertically.
Figure 1.
Scroll Table Image
Supported Column Types
In the design I needed to support different column structure within the table.  I created a base ScrollColumn class that had all the shared properties that I needed.  I derived three additional column type to support the various formatting, DataBoundColumn for simple data binding, SpannedScrollColumn to allow for the column header to span across multiple columns, and the RelationalScrollColumn to allow a single row to have repeating relational data.  The class diagram for the columns is shown in Figure 2.
Figure 2.
Column UML Diagram


Data Binding
I needed to support the standard data binding pattern for the output.  I followed Microsoft's recommended pattern for data bound controls.  The main method I needed was the DataBind. this would create the child control hierarchy.  The code for the DataBind was easy to implement and is as follows:
 
               Public Shadows Sub DataBind()

                MyBase.OnDataBinding(EventArgs.Empty)

                ' clear all previous controls
                Controls.Clear()

                ' create a new control heirarchy since the control does not use view state
                CreateControlHierarchy()

                ' set the flag that the children have been created
                ChildControlsCreated = True

            End Sub   
The main work was in the CreateControlHierarchy method.  This method is responsible for resolving the data source and creating the child controls needed.  The first step was to resolve the data source in a GetDataSource method. this was implemented as follows:
 
            Protected Overridable Function GetDataSource() As IEnumerable

                ' guard against nothing
                If IsNothing(m_dataSource) Then Return Nothing

                ' if this is an IEnumerable then we already have the data source
                If TypeOf (m_dataSource) Is IEnumerable Then Return m_dataSource

                ' search through the list source
                If TypeOf (m_dataSource) Is IListSource Then
                    Dim listSource As IListSource = CType(m_dataSource, IListSource)
                    Dim memberList As IList = listSource.GetList()

                    ' if the datasource has only one list, use the list
                    If Not listSource.ContainsListCollection Then
                        Return CType(memberList, IEnumerable)
                    End If

                    ' if we have a typed list as our collection, move throught to find the data memeber
                    If TypeOf (memberList) Is ITypedList Then
                        Dim typedMemberList As ITypedList = CType(memberList, ITypedList)
                        Dim propDesc As PropertyDescriptorCollection = typedMemberList.GetItemProperties(Nothing)
                        Dim memberProperty As PropertyDescriptor = Nothing

                        Dim exceptionString As String

                        If Not IsNothing(propDesc) Then
                            If propDesc.Count <> 0 Then

                                ' if the datamember is an empty string, use the first list
                                ' otherwise try to find  the data member
                                Dim dataMember As String = m_dataMember
                                If dataMember.Length = 0 Then
                                    memberProperty = propDesc(0)
                                Else
                                    memberProperty = propDesc.Find(dataMember, True)
                                End If

                                ' if we have found the datamemeber, process it
                                If Not IsNothing(memberProperty) Then
                                    Dim listRow As Object = memberList(0)
                                    Dim list As Object = memberProperty.GetValue(listRow)

                                    If TypeOf (list) Is IEnumerable Then
                                        Return CType(list, IEnumerable)
                                    End If
                                End If
                            Else
                                Throw New Exception("A list corresponding to the selected DataMember was not found")
                            End If
                        Else
                            Throw New Exception("A list corresponding to the selected DataMember was not found")
                        End If
                    Else
                        Throw New Exception("The selected data source did not contain any data members to bind to.")
                    End If
                End If

                Return Nothing

            End Function   
To get the client side behaviour I also needed to emit the client script and create my own unique ids for the various control elements.  I found it is easier to write the static script, test and debug before trying to create it dynamically.  Once the script has been tested, you can then add the format insertion points to relate back to the control ids.
Now that I had the basics of the control created I wrote the helper methods to render the appropriate control tree.  The various parts of the control and columns are listed in Figure 3.  I had to add what I essentially refer to as "fudge factors" obtaining the column alignment.  You can still see some misalignment in the column lines but this was not an issue as long as the scrolling functionality was provided.
Figure 3.
Scroll Table Details
Originally the control was designed to be read only.  Any edit would be accomplished through a separate page.  The end users insisted on being able to edit the data in the grid.  This posed a special challenge since an individual row is actually two distinct rows in two separate tables.  Obviously I had to implement the IPostBackDataHandler to get the data back when the page was post back. another issue I had to overcome was how could I determine which control to use for the cells.  There was also an issue of only some of the columns could be edited.  The approach I chose was to allow each row to have a control bound to it by the calling page.  I would then bind the data from the postback to the appropriate control so the page could have the data in a seamless manner.  I also implemented an auto postback if the user changed the data and navigated off the row.
The way I implemented the edit controls was to create an invisible div where I would actually emit the controls.  In each cell that had an associated edit control, I added an edit attribute.  When the user would select a row, I would walk the cells in the row and insert the control into the appropriate cells.  If the data was unchanged when the user navigated off the row, no postback would occur.  This provided the user with the experience they wanted.  A selected row for editing can be seen in Figure 4.
Figure 4.
Scroll Table Edit
Limitations
While the code was designed to handle some fairly complex data display it is not foolproof. Certain combinations of columns and edit controls may not work as expected.  The control is fairly flexible and will allow you to do a lot of combinations.  The result may not be what you expect and it will only works with Internet Explorer.
The sample project including with the code should provide you with information on how to use the control.  The control does not have a designer built with it.  Unfortunately when creating the control there was not enough time to create one.  Also the code was written several years ago and could probably use some revisions to make it cleaner.  I am not supporting the code so it is presented "as is".