Creating a Dynamic Data-Driven User Interface (Part 3)By Scott Mitchell
|Creating a Dynamic, Data-Driven User Interface|
This article is one in a series of articles on building a web application that supports dynamic, data-driven user interfaces.
This article is the third installment of a four-part series that examines how to build a data-driven web applications that offers dynamic user interfaces. Over the past two articles we created a sample web application that allows for numerous law firms to log in to the site and manage their clientele. The application's data model contains a
Clientstable that defines the fixed attributes for a client -
LastName, and so on. All law firms have these fixed attributes available to them. Each law firm can also define dynamic attributes. For example, a law firm that specializes in personal injury might need to capture client information like type of injury, whether the injury occurred on a job site, and so forth. The custom client attributes for each law firm are stored in a database table named
Part 1 examined the scope of the project and created the data model, while Part 2 showed how to allow customers (law firms) to define their custom client attributes. In this installment we create the web pages for managing clients. This includes two pages: one page to create new clients and manage their fixed attributes, and a second page to manage their custom attributes. Read on to learn more!
Our web application models client information using three database tables:
Clients- captures fixed client attributes, which are attributes common to all clients across all law firms. This includes attributes like
DynamicAttributesForClients- captures the set of custom client attributes for each law firm. Part 2 looked at how to create a web interface for creating and managing these custom attributes.
DynamicValuesForClients- this table captures the values of the custom client attributes for a particular client. For instance, if a law firm has three custom client attributes - Date of Injury, Injured On Job Site, and Cannot Work Because of Injury - a particular client might have three records in
DynamicValuesForClients, storing the values 4/1/2008, True, and False for the three custom client attributes.
FirstName, etc.) and the custom ones (Date of Injury, Injured On Job Site, etc.). However, this adds more complexity. For the time being I'm going to make a very simple user interface that separates the fixed and custom attributes into two separate pages. On one page users can create new clients by specifying the values for their fixed attributes. Moreover, they can edit these fixed attributes and delete clients. To manage a client's custom attributes the user will need to visit a second page, which enumerates the custom attributes, loads the current client's information, and allows this data to be edited. (With a little work you could meld the fixed and custom attributes into a single, unified page.)
Creating a New Client
The demo application (available for download at the end of this article) has been updated to include a new page named
~/Customers/Default.aspx. This page uses a DetailsView and SqlDataSource control to allow visitors to add a new client to the system. As aforementioned, when adding a new client to the system the user is prompted for just the fixed data attributes, after which a new record is added to the fixed data model table,
I started building this page by adding a SqlDataSource control with the following
Note that the user will be prompted to enter values for the
CustomerId value is based on what customer (law firm) the currently logged on user is associated with. In Part 2 we built a
Helpers class with a method named
GetCustomerIdForLoggedOnUser that returns the
CustomerId value for the
currently logged on user. The value for the
CustomerId parameter is set in the DetailsView control's
After creating and configuring the SqlDataSource, I bound it to the DetailsView and configured the DetailsView to support inserting. This DetailsView is used
solely for adding new clients; in the next section we'll add a GridView to the page to display the clientele. To have the DetailsView permanently
render its inserting interface I set its
DefaultMode property to
Insert. Finally, I customized the DetailsView control's
inserting interface to include validation controls for the
The following screen shot shows the DetailsView control when viewed through a browser.
Editing and Deleting Clients
Along with the DetailsView control the
~/Customers/Default.aspxpage also includes a GridView that lists the customer's clientele. The GridView lists only the fixed attribute values, and these values are editable from the GridView interface. When a client is edited from the GridView the SqlDataSource control's
UpdateCommandis sent to the database. The
UPDATEstatement used here simply updates the corresponding record in the
Clientstable; it does not modify any of the custom attribute values for the client.
The GridView also includes the ability to delete clients. The SqlDataSource's
DELETE statement deletes the corresponding record
Clients table. If there are any custom attribute values for the client then there will be records in the
DynamicValuesForClients table. When defining the data model in Part 1 we created a foreign key constraint between the
DynamicValuesForClients.ClientId columns, and instructed the relationship to cascade deletes.
If you are following along by creating your own web application and did not configure the constraint to cascade deletes you will get an exception
if you attempt to delete a client that has custom attribute values. To remedy this you can either configure the constraint to cascade deletes or
proactively delete the corresponding custom attribute values before deleting the client. This can be done through a stored procedure.
The GridView allows the visitor to only edit the fixed client attributes. To manage the custom attributes the visitor must go to another page.
To help the user reach this page I added a HyperLinkField to the GridView as its far right column. This renders a hyperlink that points to
~/Customers/ClientCustomAttributes.aspx?ID=ClientID, where ClientID is the value of the
of the client whose custom attributes are to be viewed or edited.
The following screen shot shows the GridView in action. Note that the Edit and Delete buttons enable the visitor to edit a client's fixed attribute values and delete a client. The View/Edit Custom Attributes link, when clicked, whisks the user to a second page where they can view and edit the selected client's custom attributes.
The Challenges Behind Building the Custom Client Attributes User Interface
Because each customer can define their own custom client attributes, the
~/Customers/ClientCustomAttributes.aspxpage, which displays a user interface for viewing and editing these attributes, needs to be able to dynamically generate the user interface for the attributes defined for the customer to which the currently logged on user is associated with. The good news is that ASP.NET enables page developers to programmatically add Web controls to the page's control hierarchy. In other words, we can write code to programmatically construct TextBoxes, CheckBoxes, and other controls so as to dynamically generate the appropriate user interface for the customer's own unique client attributes. In this article we will skim over the code for dynamically adding controls to a page; for a more in-depth look see Dynamic Controls in ASP.NET and Creating Dynamic Data Entry User Interfaces.
Generating the appropriate user interface is one challenge. Another is displaying the custom attribute values for the client being edited. Yet another challenge is saving these custom attribute values back to the database. These challenges are explored over the next two sections.
Creating the Custom Client Attribute UI and Loading the Client's Current Values from the Database
When programmatically adding Web controls to an ASP.NET page it is essential that the controls are added to the page on each and every page visit. This includes the first page visit and all subsequent postbacks. For the demo application this user interface is created during the Init event. In the Page_Init event handler I get all custom client attributes from the
DynamicAttributesForClientstable for the currently logged on custom and
LEFT JOINthat with the
DynamicValuesForClientstable, which returns the values (if any) for the custom client attributes for the client whose
ClientIdvalue was passed through the querystring.
After the custom attributes (and the client's corresponding values) the results are enumerated and the appropriate set of controls are created.
~/Customers/ClientCustomAttributes.aspx page includes a Table Web control named
CustomUITable. This Table has
a row programmatically added for each custom client attribute. Each row includes two columns that contain:
AttributeNamevalue (i.e., the text, "Date of Injury").
- The user interface associated with the attribute's data type.
AddCustomAttributeadds a two-column row to the
CustomUITabletable for each custom client attribute.
CreateCustomAttributeUI method generates the Web controls for the user interface for the current custom client attribute.
This method returns a
Control instances (each Web control in ASP.NET derives from the
the appropriate user interface for each custom client attribute.
Recall that each attribute is assigned a data type, which dictates the type of information expected from the user. We defined four data types in Part 1. The following list enumerates these data types and specifies the user interface the demo application uses for each:
- String - a multi-line TextBox Web control.
- Boolean - a CheckBox Web control.
- Numeric - a TextBox Web control and a CompareValidator that ensures that the user's input is a valid numeric value.
- Date - a TextBox Web control and a CompareValidator that ensures that the user's input is a valid date value.
CreateCustomAttributeUImethod. An alternative approach would be to model the data types as classes in the web application that were responsible for their own UI rendering. This is the ideal approach, but it is easier to see and understand what's happening with the UI creation hard-coded in the code-behind, which is why I've gone with that approach here. In addition to creating the UI, the
CreateCustomAttributeUImethod also assigns the current value in the database to the Web control. For example, if there's a custom client attribute named "Date of Injury" and the edited client has a value of 4/1/2008 for this attribute, the
CreateCustomAttributeUImethod both creates the TextBox and CompareValidator controls associated with this UI and assigns the
Textproperty of the TextBox to "4/1/2008".
CreateCustomAttributeUI method and helper methods called from within follow.
A couple of comments before we move on. First, note the each Web control added to the page has its
ID property set. This is done because
later, when we update the database with the user's input, we need to be able to programmatically access the controls. Therefore, the
ID is set
to the primary key of the
DynamicAttributeId. This column is a
which is equivalent to a
Guid type in the .NET Framework. The
Guid class has a
ToString() method that turns the
value into a string representation like 37146444-9d4c-4306-bc4a-fdab87911015. However, the hyphens cause problems with the client-side script emitted
by the CompareValidator controls. Therefore, I created and use the
GetID method when assigning a control's
ID value. The
GetID method converts the
Guid to a string and then replaces all hyphens with underscores.
Another thing to take note of is that when working with the
AttributeValue we need to first see if it is equivalent to a database
value before assigning the value to the control. This is because there may not be a corresponding record in
DynamicValuesForClients for all
of the custom client attributes. For example, if a client has just been created but has not yet had any custom attributes defined then the
query used to return the custom client attributes will have a
NULL for the custom attribute values.
The following screenshot shows the
~/Customers/ClientCustomAttributes.aspx page when visited for a client that does not yet have any
custom attributes values specified. This customer - Hutz and Hutz - has five custom client attributes defined:
- Birthdate (Date)
- Employed (Boolean)
- Married (Boolean)
- Number of Years at Current Job (Numeric)
- Reason for Law Suit (String)
Saving the Client's Custom Attribute Values
At this point the
~/Customers/ClientCustomAttributes.aspxpage displays the user interfaces for the custom client attributes and includes the currently edited client's current values. The last piece of the puzzle is to update changes to the client's custom attribute values. The
~/Customers/ClientCustomAttributes.aspxpage includes an Update button that, when clicked, updates the client's custom attribute values and then redirects the user back to the Manage Clients page (i.e.,
Our first order of business is to get the values the user entered into the various custom attribute user interfaces. This is accomplished by
querying the database to retrieve the list of custom client attributes for the client and then enumerating the attributes and using the
FindControl(controlId) method to programmatically access the control and get its value.
In the following code snippet (which is found in the Update Button's
Click event handler) gets the custom client attributes from the
database, loops through them and for each attribute it calls the
GetValueForCustomAttribute method to retrieve the user's input into the user interface.
We'll examine this method momentarily.
The values returned from the
GetValueForCustomAttribute method are stored in a
Dictionary object named
Dictionary object is useful for storing a collection of items that are indexed by some value other
than an ordinal number. In this case I want to have a collection of attributes, accessible by
Guid value), whose values are
SqlParameter objects with the appropriate value entered by the user. Therefore, I created a
Dictionary object with
keys of type
Guid and values of type
Dim AttributeValues As New Dictionary(Of Guid, SqlParameter).
(If this doesn't make too much sense it should become clearer once we look at the code later on in the
Click event handler that actually updates the database.)
GetValueForCustomAttribute method returns a
SqlParameter object with a
ParameterName value of
@DynamicValue and appropriately set
DbType properties. If the user does not enter any value, the
Value is set a database NULL value (
DBNull.Value). Moreover, the
is set according to the data type of the custom client attribute. The user interface used for a particular custom client attribute is retrieved via
FindControl(controlId) method (namely,
CustomUITable.FindControl(controlId)). Recall that when creating
the user interface controls for the custom client attributes we set the
ID of the control to the value of the
DynamicAttributeId column, formatted using the
GetID method. This same logic is used to programmatically find the control so
that the value entered by the user can be retrieved.
Like with the code used to create the user interface, logic used in the
GetValueForCustomAttribute method to retrieve the user's
value is hard-coded in the ASP.NET code-behind class.
Once we have all of the custom client attribute values loaded into the
Dictionary object we are ready
to update the database. Each custom client attribute value is stored as a record in the
DynamicValuesForClients table. If there are
five custom attributes each client whose custom attribute values have been saved will have five corresponding records in
DynamicValuesForClients. When saving a client's custom attribute values we may need to:
- Insert records into
DynamicValuesForClients- if the client has not yet had any custom attribute values specified, or if a new custom client attribute has been added to the system, then there is no corresponding record in
DynamicValuesForClientsfor a particular attribute's value. Therefore we'll need to add a new record to
- Update records in
DynamicValuesForClients- if a client already has a record in
DynamicValuesForClientsfor a particular custom attribute, updating the attribute value requires updating the corresponding database record.
DynamicValuesForClientstable for the
AttributeIdpair. I created a stored procedure named
lawfirm_AddOrUpdateDynamicValueForClientto handle this decision. This makes the ASP.NET code very simple: just loop through the
Dictionaryobject and for each item call the
lawfirm_AddOrUpdateDynamicValueForClientstored procedure, passing in the
AttributeId, and the value for that attribute. (Because the items in the
SqlParameterobjects we can add them to the
lawfirm_AddOrUpdateDynamicValueForClient stored procedure follows. It is pretty simple - it checks to see if there is already a record
AttributeId pair passed in. If so, then the record already exists in the table so an
statement is used; if not, the record needs to be added, so an
INSERT statement is used instead.
That's it! I encourage you to download the code demo at the end of this article and closely examine the code in the page. One thing you'll notice
is that the entire set of SQL statements in the Update Button's
Click event handler are wrapped up in a transaction. This technique
ensures that the set of
UPDATE statements that occur as each custom client attribute value is updated
are treated as an atomic operation. For more on implementing transactional support in your .NET code, see
Maintaining Database Consistency with Transactions.
We now have a fully functional dynamic, data-drive web application! Customers can log on to the site and define custom client attributes and manage values for those custom attributes. Of course, there is much room for improvement and some lingering questions, like what happens if a customer redefines an existing custom attribute, and how can we extend the system to include more data types, and how do we add extra attributes to these data types, such as indicating that a particular string field is required or that a date value must be between some range of legal values? Part 4 discusses questions like these.
Until then... Happy Programming!