Skip navigation

Monthly Archives: July 2011

In my last article, I talked a little bit about taking time to program for yourself. The example used was a drop-down list control that automatically filled itself from an enumeration. Today, we will be looking at the process of filling a form from, and copying data back to, a class. A typical situation for software that deals with user input. Whether its data collection, sales order entry, or whatever, the standard CRUD (create,read,update,delete) theme is repeated over and over again. With anything that is repeated over and over, it can, and probably should be automated. The following is a simplified example of how to replace the process of manually ‘gluing’ your forms controls to the underlying class object.

I’ve explored the process of doing this before in other projects. The original scheme was to create a base class that all other controls/forms would inherit from that contained all the generic code for filling the controls and updating the class. Although this worked quite well, you can always learn something from exploring a new approach. So, instead of using a universal base control/form, we will instead be using a Custom Provider Control. To explain, this is similar to how the ToolTip component works. It provides a service to the form/control its placed on, and adds a property to each control in the property designer. So our control will provide the service of filling the form with class data, and updating the class data when the user changes what’s on the form.

First things first, we need to create or provider control. If you read the linked article above, you see that we just have to create a new class that inherits from System.ComponentModel.Component, and implements the System.ComponentModel.IExtenderProvider interface. It also needs to have a System.ComponentModel.ProvideProperty attribute, which give the property to be added to controls in the property wizard and also give a control type filter.

<System.ComponentModel.ProvideProperty("PropertyName", GetType(Control))>
Public Class ControlMapper
    Inherits System.ComponentModel.Component
    Implements System.ComponentModel.IExtenderProvider

The property name we give to the ProvideProperty attribute is “PropertyName”, since what we are mapping our controls to are public properties of a class. The type filter we give it is just Control, since we could bind a property to almost any type of control we can code for. That leads us to the implementation of the IExtenderProvider interface, which contains only one method, CanExtend. CanExtend is called for any control in the form and we return True if our class will extend it, or False if it will not.

    Public Function CanExtend(ByVal extendee As Object) As Boolean Implements System.ComponentModel.IExtenderProvider.CanExtend
        If extendee.GetType Is GetType(TextBox) Then
            Return True
        Else
            Return False
        End If
    End Function

For simplicity of the example, we will only deal with text boxes. I’ll leave it as an exercise for you to extend the class to handle other controls like combo boxes, picture boxes, calendars, etc. Now, the next thing that needs to be created is a Get and Set function for the property name we gave to the ProvideProperty attribute, in the format of GetPropertyName and SetPropertyName. These functions will use a private member Mapped which is an instance of Dictionary(of Control, String). This local will track the controls that are being associated to the provider and the property name that was set in the designer. We also set up and event handler when the property is set, so that we can update the forms class when the text is changed by the user.


    Public Function GetPropertyName(ByVal myControl As Control) As String
        'Get property name based on control, return empty string if not found
        If Mapped.ContainsKey(myControl) Then
            Return Mapped.Item(myControl)
        Else
            Return ""
        End If
    End Function

    Public Sub SetPropertyName(ByVal myControl As Control, ByVal value As String)
        'Add property/control pair to dictionary
        If Mapped.ContainsKey(myControl) Then
            Mapped.Item(myControl) = value
        Else
            Mapped.Add(myControl, value)
        End If
        'Add event handler for control type
        If myControl.GetType() Is GetType(TextBox) Then
            Dim tb As TextBox = myControl
            AddHandler tb.TextChanged, AddressOf Me.TextChanged
        End If
    End Sub

Now, we need to add a public property to the provider itself, so the form using it can give it a pointer to the class object that the form is editing.

Public Property FormObject As Object

And we will add a private field, to mark when we are filling the form. This will server to let us ignore events while we populate controls with class object data.

Dim Filling As Boolean = False

Now, the first thing we have to do before we edit data, is that we must load it to be edited. So a method is needed to fill all the associated controls with the value of the property they were mapped to. We first loop through the KeyValuePair(of Control, String) that are stored in our Mapped dictionary, this gives us the control object and the name of the property it is bound to.

Dim Cntl As Control = item.Key
Dim PropertyName As String = item.Value

Then, using reflection, we can retrieve the PropertyInfo for the property name and use it to get the value from the class object.

Dim Prop = FormObject.GetType().GetProperty(PropertyName)
Value = Prop.GetValue(FormObject, Nothing)

Here’s the complete fill method, complete with null checks, etc.

    Public Sub FillFromClass()
        'Check for null
        If FormObject IsNot Nothing Then
            Try
                'Set filing flag, use try...finally structure to 
                'ensure the flag is always turned off
                Filling = True
                'Loop through all handled controls
                For Each item In Mapped
                    'Get control on property name from dictionary item
                    Dim Cntl As Control = item.Key
                    Dim PropertyName As String = item.Value
                    'Get value from class 
                    Dim Value As Object
                    'Get reflection property
                    Dim Prop = FormObject.GetType().GetProperty(PropertyName)
                    'Check null 
                    If Prop IsNot Nothing Then
                        'get property value
                        Value = Prop.GetValue(FormObject, Nothing)
                        'Set value according to control type 
                        If Cntl.GetType() Is GetType(TextBox) Then
                            'Set text property for text boxes
                            Dim TB As TextBox = Cntl
                            TB.Text = Value
                            'Done!
                        End If
                    End If
                Next
            Finally
                Filling = False
            End Try
        End If
    End Sub

As you may have seen in the SetPropertyName method, we are adding an event handler to the TextChanged event of the text box. This event will handle updating the FormObject with the value from the modified control. We can use the GetPropertyName function to get the name of the property the control is bound to so that we can update FormObject using reflection.

    Private Sub TextChanged(ByVal sender As Control, ByVal e As EventArgs)
        'Check for DesingMode
        If Not DesignMode Then
            'Check if the control is being filled
            If Not Filling Then
                'Check for null
                If FormObject IsNot Nothing Then
                    'Get the name of the property bound to control
                    Dim PropetyName = GetPropertyName(sender)
                    'Check if found
                    If PropetyName <> "" Then
                        'Get property using reflection
                        Dim Prop = FormObject.GetType().GetProperty(PropetyName)
                        'Check null
                        If Prop IsNot Nothing Then
                            'Get value based on control type 
                            Dim Value As Object
                            If sender.GetType() Is GetType(TextBox) Then
                                'Get text property
                                Value = CType(sender, TextBox).Text
                            Else
                                'Unsupported control type
                                Return
                            End If
                            'Set the value of the class to the new value
                            Prop.SetValue(FormObject, Value, Nothing)
                            'All done!
                        End If
                    End If
                End If
            End If
        End If
    End Sub

That completes our provider. Here’s the complete code.


<System.ComponentModel.ProvideProperty("PropertyName", GetType(Control))>
Public Class ControlMapper
    Inherits System.ComponentModel.Component
    Implements System.ComponentModel.IExtenderProvider

    ''' <summary>
    ''' Dictionary to hold control/property mapping
    ''' </summary>
    ''' <remarks></remarks>
    Dim Mapped As Dictionary(Of Control, String)

    Public Sub New()
        'Instantiate the dictionary
        Mapped = New Dictionary(Of Control, String)
    End Sub

    ''' <summary>
    ''' Get function used by the ProvideProperty attribute
    ''' </summary>
    ''' <param name="myControl"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function GetPropertyName(ByVal myControl As Control) As String
        'Get property name based on control, return empty string if not found
        If Mapped.ContainsKey(myControl) Then
            Return Mapped.Item(myControl)
        Else
            Return ""
        End If
    End Function

    ''' <summary>
    ''' Set function used by the ProvideProperty attribute
    ''' </summary>
    ''' <param name="myControl"></param>
    ''' <param name="value"></param>
    ''' <remarks></remarks>
    Public Sub SetPropertyName(ByVal myControl As Control, ByVal value As String)
        'Add property/control pair to dictionary
        If Mapped.ContainsKey(myControl) Then
            Mapped.Item(myControl) = value
        Else
            Mapped.Add(myControl, value)
        End If
        'Add event handler for control type
        If myControl.GetType() Is GetType(TextBox) Then
            Dim tb As TextBox = myControl
            AddHandler tb.TextChanged, AddressOf Me.TextChanged
        End If
    End Sub

    ''' <summary>
    ''' The IExtenderProvider interface, returns true if the passed control type can be extended.
    ''' </summary>
    ''' <param name="extendee"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function CanExtend(ByVal extendee As Object) As Boolean Implements System.ComponentModel.IExtenderProvider.CanExtend
        If extendee.GetType Is GetType(TextBox) Then
            Return True
        Else
            Return False
        End If
    End Function

    ''' <summary>
    ''' Used to set the class the form will be updating
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Property FormObject As Object

    ''' <summary>
    ''' Internal flag the the form is being filled
    ''' value changes should be ignored when true
    ''' </summary>
    ''' <remarks></remarks>
    Dim Filling As Boolean = False

    ''' <summary>
    ''' Called to initiallize all controls from the values stored in the class
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub FillFromClass()
        'Check for null
        If FormObject IsNot Nothing Then
            Try
                'Set filing flag, use try...finally structure to 
                'ensure the flag is always turned off
                Filling = True
                'Loop through all handled controls
                For Each item In Mapped
                    'Get control on property name from dictionary item
                    Dim Cntl As Control = item.Key
                    Dim PropertyName As String = item.Value
                    'Get value from class 
                    Dim Value As Object
                    'Get reflection property
                    Dim Prop = FormObject.GetType().GetProperty(PropertyName)
                    'Check null 
                    If Prop IsNot Nothing Then
                        'get property value
                        Value = Prop.GetValue(FormObject, Nothing)
                        'Set value according to control type 
                        If Cntl.GetType() Is GetType(TextBox) Then
                            'Set text property for text boxes
                            Dim TB As TextBox = Cntl
                            TB.Text = Value
                            'Done!
                        End If
                    End If
                Next
            Finally
                Filling = False
            End Try
        End If
    End Sub

    Private Sub TextChanged(ByVal sender As Control, ByVal e As EventArgs)
        'Check for DesingMode
        If Not DesignMode Then
            'Check if the control is being filled
            If Not Filling Then
                'Check for null
                If FormObject IsNot Nothing Then
                    'Get the name of the property bound to control
                    Dim PropetyName = GetPropertyName(sender)
                    'Check if found
                    If PropetyName <> "" Then
                        'Get property using reflection
                        Dim Prop = FormObject.GetType().GetProperty(PropetyName)
                        'Check null
                        If Prop IsNot Nothing Then
                            'Get value based on control type 
                            Dim Value As Object
                            If sender.GetType() Is GetType(TextBox) Then
                                'Get text property
                                Value = CType(sender, TextBox).Text
                            Else
                                'Unsupported control type
                                Return
                            End If
                            'Set the value of the class to the new value
                            Prop.SetValue(FormObject, Value, Nothing)
                            'All done!
                        End If
                    End If
                End If
            End If
        End If
    End Sub

End Class

Now, if you build you project, you should be able to drag and drop the provider control right onto your form.

Let’s create a simple class to test our new control.

Public Class ExampleClass

    Public Property Name As String

    Public Property City As String

    Public Property State As String

    Public Sub New()
        Name = "Testing Testerton"
        City = "Anytown"
        State = "WI"
    End Sub

End Class

Simple enough. We also need a form with some text boxes, like this.

After you drag a ControlMapper onto your form, you will notice that it appear in the designer in the component tray. You will also see that each text box will gain a new property.

Now for each of the text boxes, you just have to fill in the new PropertyName property with the name of the property in ExampleClass you want it to be mapped to. In the example form, we will create a new instance of ExampleClass and assign the ControlMapper1.FormObject that instance.

        FormObject = New ExampleClass
        ControlMapper1.FormObject = FormObject

Then to demonstrate that the form filling works, we will call ControlMapper1.FillFromClass() in the Load event of the form.

    Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
        'Fill the form with the class values
        ControlMapper1.FillFromClass()
    End Sub

Lastly, we can demonstrate that the updating works by displaying a message box with the updated values when we click on the button.

    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
        Dim Msg As String = String.Format("Name: {0}, City: {1}, State: {2}", FormObject.Name, FormObject.City, FormObject.State)
        MsgBox(Msg)
    End Sub

Here’s the complete example form code.

Public Class Form1

    Dim FormObject As ExampleClass

    Public Sub New()
        ' This call is required by the designer.
        InitializeComponent()
        ' Add any initialization after the InitializeComponent() call.

        'Setup form object, and add to control mapper
        FormObject = New ExampleClass
        ControlMapper1.FormObject = FormObject
    End Sub

    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
        Dim Msg As String = String.Format("Name: {0}, City: {1}, State: {2}", FormObject.Name, FormObject.City, FormObject.State)
        MsgBox(Msg)
    End Sub

    Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
        'Fill the form with the class values
        ControlMapper1.FillFromClass()
    End Sub
End Class

Lets see this in action. First the form fill.

The let’s edit the values.

Let’s click the button to see if the class was updated.

It may seem a bit much for a filling a couple text boxes, but when multiple control types are supported, the forms get big, and there are lots of forms, such an automation of the fill-form/update-object cycle can be a big time saver when building your forms. It reduces coding needed to make the form and update the form, and by implementing a reusable component, reduces the risk of introducing errors when you update.

Download the Sample Project.

There are many things to consider when you write code. Often times you are focused on what you are trying to deliver to your customer. Lately, I’ve been trying to focus more on what will make my job as a programmer easier. Mostly that boils down to two things I like in my code, re-usability¬†and ease of updating. In the past I’ve found myself writing the same code over and over again, and when it came time for changes, updating the same code in a bunch of different places.

Consider this, you have a reasonably large number of forms/controls in your application and many of them contain a drop-down list of the same set of values. Usually, you might just edit each combo box and type in the values for each instance of the list on each form/control it appears on. However, if you need to update those values, you now have to do it in many places and you may forget one and thus introduce a bug into your program. One solution is to create a drop-down list that automatically loads its values from an enumeration. That way every instance of this list will always be the same and can be added by just dropping it onto the form/control without having to set its properties.

To do this, you need to first create a new class that inherits from ComboBox and accepts a type parameter T. This will be your generic base class that will do all the work.

Public Class EnumComboBox(Of T)
    Inherits ComboBox
End Class

Since you are inheriting from ComboBox, your new control has most of the functionality it needs already. Next, it’s just a matter of using reflection over the enumeration type passed in the type parameter T to fill in the DataSource property. You can do this in the constructor, and it looks like this:

    Public Sub New()
        MyBase.New()
        'Drop down list style, no text entry
        Me.DropDownStyle = ComboBoxStyle.DropDownList
        'Get type variable
        Dim EnumType As Type = GetType(T)
        'Check if it really is a enum
        If Not EnumType.IsEnum Then
            Throw New Exception(String.Format("Type {0} is not an enumeration.", EnumType.Name))
        End If
        'Get enum values
        Dim Values() As T = [Enum].GetValues(EnumType)
        'Setup datasource
        'Use linq query 
        Dim NewItems = From x In Values Select Key = [Enum].GetName(EnumType, x), Value = x
        'Set datasource to NewItems
        DataSource = NewItems.ToList
        'set display and value members
        DisplayMember = "Key"
        ValueMember = "Value"
    End Sub

First we are setting the DropDownStyle property to DropDownList, as this disables the free-entry text box portion of the ComboBox. The next statement, Dim EnumType As Type = GetType(T), retrieve a type variable that contains the reflection information we need. We also added a check to the IsEnum property just to make sure that the type parameter is actually and enumeration type. The line, Dim Values() As T = [Enum].GetValues(EnumType), is pretty simple and just retrieves an array of all the values in the enumeration. Next, we are using a LINQ statement to pair up each value with its name into an IEnumerable of a generic type. Note that we gave the properties in the generic type the specific names Key and Value. Then we set the Datasource property to be NewItems.ToList. Setting the Datasource directly to the LINQ query object doesn’t work, as the ComboBox class doesn’t seem to recognize query objects as a valid data source. ToList processes the query and converts it into a list object. Lastly, we just set the DisplayMember and ValueMember properties to “Key” and “Value”.

There are two other additions to this control. One is simply a property that exposes SelectedValue as type T rather that just plain object. This mostly just helps when coding against the control.

    Public ReadOnly Property EnumValue As T
        Get
            Return SelectedValue
        End Get
    End Property

The other addtion is just a hack to work around some problems with the forms designer. Basically what happens is the designer tries to serialize properties from the controls when you place them on a form/control. Well, if you try to serialize our datasource property, it will fail since it can’t serialize an anonymous type. That error will prevent you from adding your control to a form/control.

    <System.ComponentModel.DesignerSerializationVisibility(System.ComponentModel.DesignerSerializationVisibility.Hidden)>
    Shadows Property DataSource
        Get
            Return MyBase.DataSource
        End Get
        Set(ByVal value)
            MyBase.DataSource = value
        End Set
    End Property

That leaves us with the complete control code, shown here.

Public Class EnumComboBox(Of T)
    Inherits ComboBox

    Public Sub New()
        MyBase.New()
        'Drop down list style, no text entry
        Me.DropDownStyle = ComboBoxStyle.DropDownList
        'Get type variable
        Dim EnumType As Type = GetType(T)
        'Check if it really is a enum
        If Not EnumType.IsEnum Then
            Throw New Exception(String.Format("Type {0} is not an enumeration.", EnumType.Name))
        End If
        'Get enum values
        Dim Values() As T = [Enum].GetValues(EnumType)
        'Setup datasource
        'Use linq query 
        Dim NewItems = From x In Values Select Key = [Enum].GetName(EnumType, x), Value = x
        'Set datasource to NewItems
        DataSource = NewItems.ToList
        'set display and value members
        DisplayMember = "Key"
        ValueMember = "Value"
    End Sub

    <System.ComponentModel.DesignerSerializationVisibility(System.ComponentModel.DesignerSerializationVisibility.Hidden)>
    Shadows Property DataSource
        Get
            Return MyBase.DataSource
        End Get
        Set(ByVal value)
            MyBase.DataSource = value
        End Set
    End Property

    Public ReadOnly Property EnumValue As T
        Get
            Return SelectedValue
        End Get
    End Property
End Class

Now, another issue with the Visual Studio form designer, is that you cannot add a generically defined control through the control toolbox. To get it to work in the designer you just have to create a non-generic class, like this.

Public Class ComboBoxExample
    Inherits EnumComboBox(Of ExampleEnum)

End Class

At this point, if you build your project, ComboBoxExample should appear in your toolbox to be dropped onto your control.

That’s it. You now have a reusable drop down list, based on an enumeration that, no matter where it’s used will always reflect the correct values.