New Posts New Posts RSS Feed: Tech Tip: Binding User-Defined Fields to Controls in Your User Interface
  FAQ FAQ  Forum Search   Calendar   Register Register  Login Login

Tech Tip: Binding User-Defined Fields to Controls in Your User Interface

 Post Reply Post Reply
Author
IdeaBlade View Drop Down
Moderator Group
Moderator Group
Avatar

Joined: 30-May-2007
Location: United States
Posts: 353
Post Options Post Options   Quote IdeaBlade Quote  Post ReplyReply Direct Link To This Post Topic: Tech Tip: Binding User-Defined Fields to Controls in Your User Interface
    Posted: 06-Jun-2007 at 3:01pm

Level 300
DevForce Express

Sep 13, 06

In the Tech Tip, “Tech Tip: Working with User-Defined Fields”, we explored the discovery of custom columns (added by customers, for example) in our database tables. We were able to isolate the custom columns by comparing a comprehensive list of table columns against a list of PropertyDescriptors that includes only the columns present when we last generated our business model.

In this week’s tip we’ll see how to create and bind to dynamic properties on our business objects to expose the custom data.

Last Week's Code

The heart of last week’s code was a loop that isolated custom columns and called a mysterious method named CreateAndConfigureOneUiControl(col) to bind them to UI controls. 

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
 

CreateAndConfigureOneUiControl() Method

We didn’t expose the detail for that method, but here it is now:

C#:

private void CreateAndConfigureOneUiControl(DataColumn pColumn) {
  CreateAndConfigureLabel(pColumn);
  if (pColumn.DataType == typeof(System.DateTime)) {
    CreateConfigureAndBindDatePicker(pColumn);
  } else if (pColumn.DataType == typeof(System.String)) {
    CreateConfigureAndBindTextBox(pColumn);
  } else if (pColumn.DataType == typeof(System.Byte[])) {
    CreateConfigureAndBindPictureBox(pColumn);
  } else if (pColumn.DataType == typeof(System.Int16)) {
    CreateConfigureAndBindNumericUpDown(pColumn, 30);
  } else if (pColumn.DataType == typeof(System.Int32)) {
    CreateConfigureAndBindNumericUpDown(pColumn, 60);
  } else if (pColumn.DataType == typeof(System.Int64)) {
    CreateConfigureAndBindNumericUpDown(pColumn, 90);
  } else {
    // Use TextBox for all other data types
    CreateConfigureAndBindTextBox(pColumn);
  }
}

private void CreateAndConfigureLabel(DataColumn pColumn) {
  Label udp1Label = new Label();
  udp1Label.Text = pColumn.ColumnName;
  udp1Label.Top = mPositionTop;
  udp1Label.Left = 20;
  udp1Label.Width = 80;
  this.mCustomPropertiesTabPage.Controls.Add(udp1Label);
}

VB.NET:

Private Sub CreateAndConfigureOneUiControl(ByVal pColumn As DataColumn)
  CreateAndConfigureLabel(pColumn)
  If pColumn.DataType Is GetType(System.DateTime) Then
    CreateConfigureAndBindDatePicker(pColumn)
  ElseIf pColumn.DataType Is GetType(System.String) Then
    CreateConfigureAndBindTextBox(pColumn)
  ElseIf pColumn.DataType Is GetType(System.Byte()) Then
    CreateConfigureAndBindPictureBox(pColumn)
  ElseIf pColumn.DataType Is GetType(System.Int16) Then
    CreateConfigureAndBindNumericUpDown(pColumn, 30)
  ElseIf pColumn.DataType Is GetType(System.Int32) Then
    CreateConfigureAndBindNumericUpDown(pColumn, 60)
  ElseIf pColumn.DataType Is GetType(System.Int64) Then
    CreateConfigureAndBindNumericUpDown(pColumn, 90)
  Else
    ' Use TextBox for all other data types
    CreateConfigureAndBindTextBox(pColumn)
  End If
End Sub

Private Sub CreateAndConfigureLabel(ByVal pColumn As DataColumn)
  Dim udp1Label As Label = New Label()
  udp1Label.Text = pColumn.ColumnName
  udp1Label.Top = mPositionTop
  udp1Label.Left = 20
  udp1Label.Width = 80
  Me.mCustomPropertiesTabPage.Controls.Add(udp1Label)
End Sub
 

CreateAndConfigureOneUiControl() actually creates and configures a pair of UI controls – a label and another editable control whose type depends upon the data type of the data to be bound – but for the purpose of naming the method we’ve treated them like a single unit. In the code above we’re adding our controls to a TabPage, but you can of course add them to any suitable container.

Once the label is created, configured, and added to the desired container, another method is called to set up the editable control. We’re discriminating six different specific data types above and mapping those to a specific type of control that we’ve chosen. Any other data type we discover gets mapped to a TextBox control. Clearly you can augment this code to discriminate many more data types; or you can take it in the opposite direction and toss everything into a TextBox. Your choice!

Configuring the Editable Control

Here’s the method that configures a TextBox: 

C#:

private void CreateConfigureAndBindTextBox(DataColumn pColumn) {
  TextBox udp1TextBox = new TextBox();
  udp1TextBox.Width = 200;
  ConfigureAndBindControl(pColumn, udp1TextBox);
}
private void ConfigureAndBindControl(
  DataColumn pColumn, Control pControl) {
  pControl.Top = mPositionTop;
  pControl.Left = 110;
  this.mCustomPropertiesTabPage.Controls.Add(pControl);
  mPositionTop += pControl.Height + 5;
  DelegateHolder holder = new DelegateHolder(pColumn.ColumnName);
  PropertyDescriptor aDescriptor =
  new AdaptedPropertyDescriptor(
    pColumn.ColumnName, typeof(Employee),
    pColumn.DataType, holder.Getter, holder.Setter);
  mEmployees.AddPropertyDescriptor(aDescriptor);
  this.mEmployeeCBM.Descriptors.Add(pControl, pColumn.ColumnName);
}

VB.NET:

Private Sub CreateConfigureAndBindTextBox(ByVal pColumn As DataColumn)
  Dim udp1TextBox As TextBox = New TextBox()
  udp1TextBox.Width = 200
  ConfigureAndBindControl(pColumn, udp1TextBox)
End Sub

Private Sub ConfigureAndBindControl( _
  ByVal pColumn As DataColumn, ByVal pControl As Control)
  pControl.Top = mPositionTop
  pControl.Left = 110
  Me.mCustomPropertiesTabPage.Controls.Add(pControl)
  mPositionTop += pControl.Height + 5
  Dim holder As DelegateHolder = _
    New DelegateHolder(pColumn.ColumnName)
  Dim aDescriptor As PropertyDescriptor = _
    New AdaptedPropertyDescriptor(pColumn.ColumnName, _
    GetType(Employee), _
    pColumn.DataType, _
    AddressOf holder.Getter, _
    AddressOf holder.Setter)
  mEmployees.AddPropertyDescriptor(aDescriptor)
  Me.mEmployeeCBM.Descriptors.Add(pControl, pColumn.ColumnName)
End Sub

Most of the steps in configuring the control are the same for every control type, so those have been factored into the reusable ConfigureAndBindControl() method. For the TextBox, the only idiosyncratic step is that of setting the width. We can definitely get more sophisticated about that than we’ve done here and some control types will require a bit more setup, but you get the idea.

Binding the Dynamic Properties

All of the code above, beginning with the declaration of the DelegateHolder, relates to creating the databinding for the dynamic property. To accomplish that binding, we must first create a System.ComponentModel.PropertyDescriptor for the new property, and then add that to the set of descriptors for the type on which we want the dynamic property maintained (in this case, Employee).

We get the necessary PropertyDescriptor by constructing an IdeaBlade.Util.AdaptedPropertyDescriptor. Its constructor needs the following pieces of information:

  1. A name for the new property (supplied above as pColumn.ColumnName)
  2. The type on which the new property will be defined (Employee)
  3. The property’s data type (pColumn.DataType)
  4. A delegate instance that will return a value when the property’s value is requested
  5. A delegate instance that will set the property’s value.

We’ll discuss the two delegate instances in a moment, but for now let’s continue with the last two statements in ConfigureAndBindControl(). The statement

    mEmployees.AddPropertyDescriptor(aDescriptor)

adds the PropertyDescriptor just created to the list maintained by the mEmployees EntityList. That list might be entirely specific to the mEmployees EntityList; or it might be centrally maintained for use by all lists of Employees used by your application. In this case, take our word for it that we said nothing in the configuration of the mEmployees EntityList to tell it to use a private PropertyDescriptorList. So, by default, it uses the global list. As such, when we add the new PropertyDescriptor as we have done above, we are in fact making the new property available to all BindableLists and EntityLists of Employees in our app. Any BindableList or EntityList of Employees subsequently declared tells whomever asks that the set of properties maintained on Employee objects includes the dynamically defined one(s).

To divert into a discussion of private PropertyDescriptorLists would take this article in yet another direction, so we will defer that to another time and another place. We hope it will suffice for now to say that it is possible to define a dynamic property on a type only in connection with a single specific BindableList or EntityList rather than for all lists containing that type, that there are some specialized use cases for that, and that we don’t need to use this facility for our purposes here.

Adding the BindingDescriptor to the BindingManager’s Collection

The final statement in ConfigureAndBindControl() creates a BindingDescriptor for the dynamic property and adds it to the Descriptors collection of an IdeaBlade.UI.Winforms.ControlBindingManager named “mEmployeeCBM”:

    Me.mEmployeeCBM.Descriptors.Add(pControl, pColumn.ColumnName)

Note that mEmployeeCBM consults something to determine if the property named with the value returned by pColumn.ColumnName actually exists. If the mEmployeeCBM’s BindingSource has already been configured, it consults the list that supplies the BindingSource. If not, it consults the global PropertyDescriptorsList. If the list consulted says the property exists, the binding configures; otherwise, it fails.

Supplying the Delegate Instances for the AdaptedPropertyDescriptor Constructor

There’s just one last detail to clear up. If you recall, we instantiated a DelegateHolder object, referencing the Getter and Setter delegates needed by the constructor of AdaptedPropertyDescriptor() as members thereof. We did that in order to be able to get the name of our dynamic property into the delegates as a variable (since we can’t change their signatures!). The code is shown below.  

C#:

public class DelegateHolder {
  public DelegateHolder(String pColumnName) {
    mColumnName = pColumnName;
  }

 

  public object Getter(object pParent) {
    DataRow aRow = (DataRow)pParent;
    return aRow[mColumnName];
  }

 

  public void Setter(object pParent, object pValue) {
    DataRow aRow = (DataRow)pParent;
    aRow[mColumnName] = pValue;
  }

 

  private String mColumnName;
}

VB.NET:

Public Class DelegateHolder
  Public Sub New(ByVal pColumnName As String)
    mColumnName = pColumnName
  End Sub

 

  Public Function Getter(ByVal pParent As Object) As Object
    Dim aRow As DataRow = CType(pParent, DataRow)
    Return aRow(mColumnName)
  End Function

 

  Public Sub Setter(ByVal pParent As Object, _
      ByVal pValue As Object)
  Dim aRow As DataRow = CType(pParent, DataRow)
  aRow(mColumnName) = pValue
  End Sub

 

  Private mColumnName As String

End Class

That completes your work to bind the dynamic properties based on customer-added data columns to UI controls. There remain potentially a few challenges in getting the layout of the UI just right for all the potential customizations you might see. But there again, it may be fair to impose a few rules on your customers to keep things manageable. Otherwise, just roll up your sleeves and write some clever code that will handle anything thrown at it!

Back to Top
 Post Reply Post Reply

Forum Jump Forum Permissions View Drop Down