DevForce® Classic Tech Tips
Working with User-Defined Fields 



Working with User-Defined Fields Level 300
DevForce Express
Sep 6, 06
Suppose you ship your software application to multiple customers who want to add their own custom columns to the database tables you provide. Naturally, such columns will be of little use if the data they contain can’t be exposed and maintained through the application’s user interface. But how to do that without creating and maintaining multiple versions of your app – something your organization can ill afford?

You might consider adding a set amount of generic columns to each of the tables (not too many; not too few: good luck!) and writing code to allow your customers to assign particular meanings (and labels) to them. But that’s a big chunk of complex functionality to code and maintain as well. Besides, the end result isn’t quite what your customers really want as they’re locked into the data types you provide, or limited to the particular number of custom columns you provide, or encumbered everywhere they turn (e.g., when writing reports) by columns with abstract meanings, some of which they didn’t need or want to begin with. It’s still a better solution than maintaining 10 (or 100, or 1000) custom variations of your app, but it’s messy, maintenance-intensive, and full of painful compromises.

What if, on the other hand, you could write a single-version application that could discover customer-added columns on its tables at runtime, and dynamically create bound controls for them? What if the app were also smart enough to permit your customers to add as many such columns as they wish, with their choice of names and data types? How much easier would that make your life? How much happier would that make your customers?

DevForce provides you with just such a capability. This week’s Tech Tip will describe the first half of the solution – that of discovering the custom columns at runtime. Next week’s tip will complete the picture by showing you how to create properties and bind user interface controls to them.

Custom Columns in the Object Mapper

As you may know, when the DevForce Object Mapper generates classes for your business objects, it creates both a developer-level class (e.g., “Employee”) and a “DataRow” class (“EmployeeDataRow”) which the former inherits. Columns in the base table map to simple properties whose definitions are written to the “DataRow” class.

In the Simple Properties grid of the Object Mapper, if you uncheck the "Include" checkbox for some columns of a table, no property definitions get generated into the “DataRow” class for those columns. From the viewpoint of the generated class – and objects instantiated from it – those properties do not exist.

This is exactly the same situation that would occur if you generated your business classes against a particular database table, and then someone – a customer, let’s say – subsequently added new columns to that table. The generated business class simply would know nothing about the new columns.

How DevForce Retrieves Data for Your Business Objects

On the “Class” sub-tab of the “Class Detail” tab in the Object Mapper, you will find a dropdown list labeled “Column Selection Options”. The default setting for this property is “SelectAllColumns”; an alternate setting is “AllowColumnSelection”.


Under the default setting, when data is retrieved for one of your business objects, what is retrieved is the entire data row behind that object – including columns for which no properties are defined in the business class. In SQL terms, the retrieval is implemented as a “SELECT *” rather than a “SELECT ColumnName1, ColumnName2, ColumnName3, etc.”. That means, for our customer-added columns scenario, that you get the data for the custom-added columns, even if your business class doesn’t know what to do with it.

To make use of the additional data, you must somehow discover which columns have been retrieved for which no property is defined in the business class. If you can do that, you have the first half of your custom columns problem solved.

Getting a List of Properties Defined in the Business Class

To isolate the custom columns, you need two things:

1. a list of the properties currently defined for the business object; and
2. a comprehensive list of the columns retrieved.

The custom columns can be isolated by identifying items in the second list that aren’t in the first.

You can get the list of currently defined properties by calling the Get() method of the IdeaBlade.Util.PropertyDescriptorsList class, letting it know the type about which you want information.

 

C#:

PropertyDescriptorList pdlist = PropertyDescriptorList.Get(typeof(Employee));


VB.NET:

Dim pdlist As PropertyDescriptorList = PropertyDescriptorList.Get(GetType(Employee))

Getting a List of Columns in the Base Table

To get a comprehensive list of the columns defined in the datasource, do the following:

 

C#:

PersistenceManager.DefaultManager.InitializeSchema(typeof(Employee));
EntityTable employeeTable =
  PersistenceManager.DefaultManager.GetTable(typeof(Employee));
foreach(DataColumn col in employeeTable.Columns) {...}


VB.NET:

PersistenceManager.DefaultManager.InitializeSchema(GetType(Employee))
Dim employeeTable As EntityTable =
  PersistenceManager.DefaultManager.GetTable(GetType(Employee))
For Each col As DataColumn In employeeTable.Columns

The call to InitializeSchema() tells the PersistenceManager to retrieve the schema for the table on which the business object is based from the datasource. In the normal operation of an app, this schema initialization occurs as a byproduct of other operations, such as a GetEntities() call, or creation of an object of the specified type. In most cases the end result is always the same. However, there is at least one scenario in which the schema created in the PersistenceManager’s cache does not reflect the custom columns. If Create() is called on the business class while the client app is disconnected from the datasource, and before a connected data retrieval has taken place, and before the cache has been restored from a locally stored entity set, the Create() method is forced – having no where else to turn – to look to the business class for its schema information. In doing so, it of course discovers only those properties that were defined in code for the class. Furthermore, the schema, once initialized, remains unchanged until its PersistenceManager is either destroyed or reset with a Clear() call. It never learns about the custom columns.

You can see the rest of the steps required to discover the full column set from the code sample shown above: you call GetTable() on the PersistenceManager to get an EntityTable and then simply iterate through its Columns collection to discover what’s there.

Isolating the Custom Columns

Once you have your two lists, isolating the custom columns is easy: just check, for each column discovered in the schema, for a PropertyDescriptor with a PropertyPath the same as the column name. If it isn’t found, you’ve got a custom column.

 

C#:

foreach(DataColumn col in employeeTable.Columns) {
  PropertyDescriptor pd = pdlist.Find(col.ColumnName);
  if (pd == null) {
    // Customer-added column found!
    CreateAndConfigureOneUiControl(col);
    }
}

VB.NET:

For Each col As DataColumn In employeeTable.Columns
  Dim pd As PropertyDescriptor = pdlist.Find(col.ColumnName)
  If pd Is Nothing Then
    ' Customer-added column found!
    CreateAndConfigureOneUiControl(col)
  End If
Next col

Note that the string returned by EntityTable.Columns(n).ColumnName will reflect any renaming done by you in the ObjectMapper. For example, if you rename the property generated from a BirthDate column to “DateOfBirth”, EntityTable.Columns(n).ColumnName will return “DateOfBirth” rather than “BirthDate”. So you needn’t worry that your own renamed properties will be mistaken for custom columns!

That completes your work of discovery to find the columns your customer added to the table after you shipped him your code. In next week’s Tech Tip, you’ll see how to write code that will, at runtime, create temporary properties on your business object for the data from those custom columns. Then, dynamically, you’ll toss some appropriate controls on a form for them and set up data bindings so their values can be displayed and maintained by your customer’s end users. You set all of this up without knowing the first detail about your customer’s post-delivery additions to your tables!