A tale about a victory involving ASP.NET and encapsulation
There are challenges in building cool, dynamic Web UI's
A short while back I took on the task of migrating a VB6 windows client application into an ASP.NET app. It's quite an exciting task because of the fact that Windows is such a rich UI environment whereas the web has always been seen quite limited in comparison.
Throughout the course of the conversion I had to solve many challenging problems such as: UI state management, User preference state management, emulating some of the complex UI Widgets which had been used in the Windows client and messaging event notifications between each component both on the server and in the client. To do this I borrowed heavily from Metabuilders and developed some of my own. I've since made the source code to some of these public:
There are also unique and interesting problems to solve
One of the most challenging problems that I had to solve was that, the app. which I was converting consisted of thousands of input forms. Each form was dynamically generated and had both it's schema and data stored in the database. To render a form I had to: A) get the UI schema information from one set of tables; B) dynamically construct UI elements and add them to the WebForm; C) fetch any previously input data and read it into the controls. There were also instances where more than one result could be returned so I had to build a custom pager to "page" through the data. The information about each UI element included:
LabelInformation: lblWidth, lblHeight, lblCaption, lblLeft, lblTop, LabelOnly ControlInformation: ctlType, ctlDataField, ctlWidth, ctlHeight, ctlLeft, ctlTop MiscData: StatusBarText, DataFormat
In the Windows application, rendering controls was very easy because they could create a container panel and offset the position of the UI elements based on 0,0 representing the top, left corner of the panel. In a web application things weren't quite that simple because the UI is much more fluid. To get around this I decided to render each dynamic form in an embedded IFRAME so that I could always count on 0,0 representing a reliable document position rather than having to calculate varying page offsets based on the structure of the page which contained the form. I also decided to use Dennis Bauer's DynamicControlsPlaceholder:
http://www.denisbauer.com/ASPNETControls.aspx
This control helps to "cushion" you from some of the nasties associated with adding controls to pages at run-time.
The IFRAME approach was neat because it enabled me to easily pass arguments in from the containing document via the querystring and have a "Sub-Form" which hosted the DynamicControlsPlaceholder be responsible for doing the lookup in the database for the form information and determining how to layout the controls based on whether I was editing or viewing and also to handle any paging which needed to be done.
In many instances the actual data records being displayed in the dynamic forms was query'able from an interface which was hosted in the container document so, for that reason I decided to de-couple the form-schema and form result-data via the Session object into 2 separate state objects: Session("DynamicFormSchema") and Session("DynamicResultData"). This way, my dynamic form needed only to check for the existance of them to kick off the rendering process and this also ensured that the Dynamic Form was only responsible to rendering/layout logic. The logic therefore became:
' PSUEDO-CODE FOR PAGE_LOAD If Not IsPostBack Then ' interrogate the querystring for out "mode" SetMode( Request.QueryString("Mode") ) RenderForm( Session("DynamicFormSchema") ) DisplayFormData( Session("DynamicResultData") ) End If ' PSUEDO-CODE FOR RenderForm For Each dr As DataRow In Session("DynamicFormSchema") Dim ctl As WebControl = DynamicControlFactory.Make( dr ) ctl.ID = dr.Item("DataField").ToString ctl.Attributes.Add("ColumnIndex", dr.Item("ColumnIndex").ToString) DynamicCanvass.Controls.Add(ctl) Next ' PSUEDO-CODE FOR DisplayFormData Dim data As DataRow = dtDynamicResultData.Rows(RecordPager.SelectedIndex - 1) For Each ctl As Control In DynamicCanvass.Controls Select Case True Case TypeOf ctl Is TextBox: Dim colIdx As Integer = CInt(CType(ctl, TextBox).Attributes("ColumnIndex")) If Not row.Item(colIdx) Is Nothing Then CType(ctl, TextBox).Text = data.Item(colIdx).ToString End If '.... Case TypeOf ctl Is CheckBox: '.... End Select CType(ctl, WebControl).Enabled = Me.IsEditMode Next
Handling paging events was simple too...
Private Sub RecordPager_SelectedIndexChanged() Handles RecordPager.SelectedIndexChanged RenderForm( Session("DynamicResultData") ) End Sub
I thought that I'd blog this so that anybody who had an interest in building this type of dynamic UI's in WebForms could get a "from 40,000ft" overview of the approach which I took. All-in-all it was an incredibly painless experience. I think that the key to something such as this is encapsulation, something that ASP.NET has provided us with the ability to do quite easily!. As an example of this, it was very reassuring to be building atop such a useful control as the DynamicControlsPlaceholder and I'm sure that this took away many potential sources of angst.