Extending the Win Forms Binding Source Component

By David Catherman

Introduction

The binding source component in Visual Studio 2005/2008 Windows Forms provides a valuable service for data driven applications by providing automatic data binding between controls and the data source. This article shows how the functionality can be extended to add a few features to make it more useful. The most important is marking records as dirty when edited and prompting the user to save when changing records.

In a previous article, I covered Extending the Binding Navigator which included a discussion of the binding source component. This article will demonstrate the need for binding source component to be extended as a custom component as well.

Overview of the Binding Source Component

In .NET 1.1, the Binding Context and the Currency Manager components were used to bind controls to a data source and were fairly complex to use. In .NET 2.0 the Windows Forms projects have a Binding Source component that encapsulates the Currency Manager to make the binding process more straight forward and less complex.

The Binding Source provides a level of indirection between the data source and the controls bound to it. If the data source changes, just change the Data Source property and all the bound controls will be bound to the same properties and fields in the new data source. When data changes in a control, the binding source communicates that change back to the data source.

The first step in using a Binding Source is to identify the data source. Any object that implements IList will work and even complex data sources can be used by setting the Data Member property as well (table in a dataset or child relation of another binding source). The List property can be used to access all of the data or the Current property will access one row in the list. Navigation methods allow changing the position pointer, which changes the row which is current. The Sort and Filter properties apply a DataView to the list to manipulate the data.

Controls can be bound to the Binding Source by setting a property on the control. Internally, the Currency Manager tracks which controls are bound. When current property changes, each control in the set performs a read against the data row and the values are displayed in the control. When the value of a bound control is edited, the change is communicated back to the binding source. When the EndEdit method is called, the current row with its changed values is written back to the data source.

Improvements Needed

One of the problems with the Binding Source is that there is no way to tell if the data has been edited. It is quite common for the user to make edits to the controls and move the binding source to another position without knowing whether or not the edits were saved (usually not). It would be nice if there were an IsCurrentDirty property to flag when the current record has been edited.

The second improvement needed is easy access to a list of the controls that are bound and which property of the control is bound to which field. This list could be accessed through the Currency Manager but an easier way might be needed.

Third, the component needs to have a reference to the form it is on. This is not as straight forward as with controls and turned out to be a quite a trick to accomplish.

Custom Component

To make the changes to the component, create a new custom component in a Windows Forms project. Right click the project, Add, New Item, Component Class and name it exBindingSource. Visual Studio will open a design canvas with a link to open the code page. On the code page, make the class inherit from the BindingSource component.

Imports System.ComponentModel.Design

Public Class exBindingSourceComponent
    Inherits System.Windows.Forms.BindingSource

Is Current Dirty Flag

Add a property to the class for the dirty flag. (The Snippets has an entry that will quickly build the structure.)

    Private _isCurrentDirtyFlag As Boolean = False

    Public Property IsCurrentDirty() As Boolean
        Get
            Return _isCurrentDirtyFlag
        End Get
        Set(ByVal value As Boolean)
            If _isCurrentDirtyFlag <> value Then
                _isCurrentDirtyFlag = value
                If value = True Then 'call the event when flag is set
                    OnSetCurrentDirty(New EventArgs)
                End If
            End If
        End Set
    End Property

There are times when the developer needs to know when the dirty flag is set, so the component implements an event that can be subscribed to. This would allow the form to enable the save button when the dirty flag is set and disable it after the record is saved.

    Public Event SetCurrentDirty As SetCurrentDirtyEventHandler

    ' Delegate declaration.
    Public Delegate Sub SetCurrentDirtyEventHandler(ByVal sender As Object, ByVal e As EventArgs)

    Protected Overridable Sub OnSetCurrentDirty(ByVal e As EventArgs)
        RaiseEvent SetCurrentDirty(Me, e)
    End Sub

Setting the Dirty Flag

When a value is edited, the dirty flag needs to be set. The binding source has an event called OnBindingComplete that is raised when data is written to or back from a control to the data source. This is a very expensive event to handle because it fires quite often--once for each control every time the form is painted or when the current position changes. When handling the event, the event arguments parameter has some context settings that help distinguish which events are important to this process--specifically, the direction of the binding (updated back to the data source) and that it was successful.

    Private Sub _BindingComplete(ByVal sender As System.Object, _
            ByVal e As System.Windows.Forms.BindingCompleteEventArgs) _
            Handles Me.BindingComplete

        If e.BindingCompleteContext = BindingCompleteContext.DataSourceUpdate Then
            If e.BindingCompleteState = BindingCompleteState.Success And _
                    Not e.Binding.Control.BindingContext.IsReadOnly Then

                'Make sure the data source value is refreshed (fixes problem mousing off control)
                e.Binding.ReadValue()
                'if not focused then not a user edit.
                If Not e.Binding.Control.Focused Then Exit Sub

                'check for the lookup type of combobox that changes position instead of value
                If TryCast(e.Binding.Control, ComboBox) IsNot Nothing Then
                    'if the combo box has the same data member table as the binding source, ignore it
                    If CType(e.Binding.Control, ComboBox).DataSource IsNot Nothing Then
                        If CType(CType(e.Binding.Control, ComboBox).DataSource, _
                                BindingSource).DataMember = (Me.DataMember) Then Exit Sub
                    End If
                End If
                IsCurrentDirty = True 'set the dirty flag because data was changed
            End If
        End If
    End Sub

The ReadValue() method overcomes problem with earlier versions of the binding source that did not update the value in a control if the user tabbed out of the control (see the references at the end of the article). Checking for Focused() makes sure that only changes by the user are flagged--changes made by code should handle their own update concerns. There are also a couple other situations where false positives need to be captured, specifically combo boxes that are used for navigating to a new record rather than updating a value. Once the binding event meets all of these conditions, the dirty flag will be set.

Saving Changed Records

Knowing that the record was edited leads to the next topic of making sure the edits are updated to the persisted data store. Most of the time, edits should be persisted before the user moves to a different record and building that into the binding source component helps maintain the integrity of the data.

There are two options here: reminding the user that the data needs to be saved, or auto saving in the background. Then decision is handled through another property called AutoSave.

    Private _autoSaveFlag As Boolean

    Public Property AutoSave() As Boolean
        Get
            Return _autoSaveFlag
        End Get
        Set(ByVal value As Boolean)
            _autoSaveFlag = value
        End Set
    End Property

When the binding source component is added to the form, the developer can set the AutoSave property to true or the property can be tied to a checkbox or configuration parameter to allow the user to choose.

The PositionChanged() event of the binding source is handled to check for changed data.

    Private Sub _PositionChanged(ByVal sender As Object, ByVal e As EventArgs) _
            Handles Me.PositionChanged
        If IsCurrentDirty Then
            If AutoSave Or MessageBox.Show(_msg, "Confirm Save", _
                    MessageBoxButtons.YesNo) = DialogResult.Yes Then
                Try
                    'cast table as ITableUpdate to get the Update method
                    CType(_dataTable, Biz._Interface.ITableUpdate).Update()
                Catch ex As Exception
                    Win.Logger.LogError(_form, ex, _dataTable.TableName & " Update")
                End Try
            Else
                Me.CancelEdit()
                _dataTable.RejectChanges()
            End If
            IsCurrentDirty = False
        End If
    End Sub

When the position changes, if the dirty flag has been set, the data needs to be saved. If the AutoSave property is not set, the user will be prompted to save or reject the changes.

To understand how the data is updated, you will need to have read my other articles about building a data access layer (see references below). This architecture uses datasets as the data source and custom code is added to make sure each data table implements an interface with an Update method on it. Casting the data table as the interface will allow access to the Update method on the table generically.

Referencing the Data Table

The above code leads to a discussion on how to get a reference to the data table the binding source is bound to as a data source. (Note: If you are using collections or other list data for your data source, this code will need to be customized.) Datasets are complex and the data source property may refer to an actual data table (or data view) in the dataset, but it may also be set to a child relationship of another binding source object, which is a relation between two tables in the database, in which case, finding the name of the data table requires a bit of looking up in the dataset.

First, a couple private fields are added to contain the information.

    Private _displayMember As String
    Private _dataTable As DataTable
    Private _dataSet As DataSet
    Private _parentBindingSource As BindingSource

The binding source exposes events that are raised when the DataSource and DataMember properties are changed. When the form loads and the properties are set in the designer code, these events will be raised and the following code executed.

    Private Sub _DataSourceChanged(ByVal sender As System.Object, _
            ByVal e As System.EventArgs) Handles Me.DataSourceChanged
        _parentBindingSource = Nothing
        If Me.DataSource Is Nothing Then
            _dataSet = Nothing
        Else
            'get a reference to the dataset
            Dim bsTest As BindingSource = Me
            'try to cast the data source as a binding source
            Do While Not TryCast(bsTest.DataSource, BindingSource) Is Nothing
                'set the parent binding source reference
                If _parentBindingSource Is Nothing Then _parentBindingSource = bsTest
                'if cast was successful, walk up the chain until dataset is reached
                bsTest = CType(bsTest.DataSource, BindingSource)
            Loop
            'since it is no longer a binding source, it must be a dataset
            If TryCast(bsTest.DataSource, DataSet) Is Nothing Then
                'Cast as dataset did not work
                Throw New ApplicationException("Invalid Binding Source ")
            End If
            _dataSet = CType(bsTest.DataSource, DataSet)
            'is there a data member - find the datatable
            If Me.DataMember <> "" Then
                _DataMemberChanged(sender, e)
            End If 'CType(value.GetService(GetType(IDesignerHost)), IDesignerHost)
            If _form Is Nothing Then GetFormInstance()
        End If
    End Sub

The data source property can either be set to a dataset or another binding source, in which case, examine its data source. There could be multiple parent-child relationships in the data source, so use a loop to walk up the chain until the top parent is reached. Then the top parent data source will be a reference to a dataset.

When the data member is changed then check to see if the name is in the list of the tables in the dataset. If it is not, then assume it is a relation and look in the set of relations in the dataset for the relation name. For a relation, the tablename is the child table in the relation.

    Private Sub _DataMemberChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) _
            Handles Me.DataMemberChanged
        If Me.DataMember = "" Or _dataSet Is Nothing Then
            _dataTable = Nothing
        Else
            'check to see if the Data Member is the name of a table in the dataset
            If _dataSet.Tables(Me.DataMember) Is Nothing Then
                'it must be a relationship instead of a table
                Dim rel As System.Data.DataRelation = _dataSet.Relations(Me.DataMember)
                If Not rel Is Nothing Then
                    _dataTable = rel.ChildTable
                Else
                    Throw New ApplicationException("Invalid Data Member")
                End If
            Else
                _dataTable = _dataSet.Tables(Me.DataMember)
            End If
        End If
    End Sub

Now the data table field should be populated by the time the PositionChanged event fires.

List of Bound Controls

Another nice-to-have feature of the extended binding source is a list of all the controls that are bound to the binding source. After adding a section added the name of the control to a collection from the BindingComplete event, I found that the base CurrencyManager object has a Bindings list of the controls.

Reference to the Host Form for a Component

At one point while building this component, I needed to get a reference to the hosting form. Looking back at it two months later, I cannot remember why I needed the reference, but I spent a lot of time trying to figure it out and the solution is interesting, so I will include it in the article. This may be optional for the binding source, but if you do much work with components you will need this someday.

Components don't have a .Parent property like controls do. They do have a container property, but it does not have a parent property either. Sometimes casting the container as a ContainerControl works (it does have a parent property), but in this case it won't cast correctly.

All Windows Designer forms add a container called "components" and all components are added to this container and show at the bottom of the screen in design mode. From inside the component, it is easy to get a reference to the container, but I could not get a reference to the parent form of the container?

Then I found a clue. It turns out that the Error Provider component manages to get a reference to the parent form and exposes it in a property called "ContainerControl". The secret the Error Provider uses is to override the Site function of the component and capture the IDesignerHost type of service.

    Public Overrides Property Site() As System.ComponentModel.ISite
        Get
            Return MyBase.Site
        End Get
        Set(ByVal value As System.ComponentModel.ISite)
            'runs at design time to initiate ContainerControl
            MyBase.Site = value
            If value Is Nothing Then Return
            ' Requests an IDesignerHost service using Component.Site.GetService()
            Dim service As IDesignerHost = _
                    CType(value.GetService(GetType(IDesignerHost)), IDesignerHost)
            If service Is Nothing Then Return
            _form = CType(service.RootComponent, Form)
        End Set
    End Property

Each component has a Site property that contains information about the container and is added by the IContainer interface. Overriding the property allows some information to be collected when the site is set by the container.

One of the methods of the site object is GetService. The Site may be filled several times with different types of services, but we are only interested in the IDesignerHost type. If the cast of the service as an IDesignerHost works, then the reference to the form can be found in a property called RootComponent.

As cool as this looks, there is a solution that is way simpler. In the same way that a list of controls is available on the CurrencyManager, once you have a reference to a control, just get the form reference from the control.

        If _form Is Nothing And Me.CurrencyManager.Bindings.Count > 0 Then
            _form = Me.CurrencyManager.Bindings(0).Control.FindForm()
        End If

Some times it is so frustrating to spend days struggling to find a solution to a problem and then it turns out to be so easy. Oh well, that's life.

Applying the Extended Binding Source Component

After building the project, the custom control is listed in the Toolbox pane, usually near the top. Just drag and drop the component to a form and it will be added to the component container at the bottom of the screen. With the component selected, set the Data Source and Data Member properties in the properties pane. Then on each control, add a binding to the field in the binding source control. Clicking the down arrow next to the desired binding property (i.e. Text for a Textbox control), will display tree view list box. Expand the desired binding source and select the field.

When using Visual Studio, most binding source components are added using the Data Sources pane. When a data source object is dragged from the Data Source pane and dropped on a form, the wizard will automatically add a binding source component to bind the controls created to the data source represented on the Data Sources pane. By default, Visual Studio will use a normal binding source component. (If anyone knows how to change this, please let me know.) To convert it to the new extended binding source component, you will need to open the designer code and change the line of code where the component is declared (near the bottom) and where it instantiated (near the top). In both lines, change the System.Windows.Forms.BindingSource type to the custom binding source as "ProjectName.exBindingSource". This is a bit cumbersome, but does work.

Conclusion

Extending the binding source component mostly solves the problem of ensuring that data edits are properly saved. This solves the number one complaint of users of Window Forms or Smart Client applications. This is a reusable component that may be copied to and easily implemented in other applications.

About the Author

David Catherman

Email: David (dot) Catherman (at) Hotmail (dot) com

David Catherman has 20+ years designing and developing database applications with specific concentration for the last 5-6 years on Microsoft .NET and SQL Server. He is currently working as an Application Architect and Senior Developer using Visual Studio 2005 and SQL Server 2005. He has several MCPs in .NET and is pursuing MCSD.

References:



Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • The latest release of SugarCRM's flagship product gives users new tools to build extraordinary customer relationships. Read an in-depth analysis of SugarCRM's enhanced ability to help companies execute their customer-facing initiatives from Ovum, a leading technology research firm.

  • Live Event Date: September 10, 2014 @ 11:00 a.m. ET / 8:00 a.m. PT Modern mobile applications connect systems-of-engagement (mobile apps) with systems-of-record (traditional IT) to deliver new and innovative business value. But the lifecycle for development of mobile apps is also new and different. Emerging trends in mobile development call for faster delivery of incremental features, coupled with feedback from the users of the app "in the wild". This loop of continuous delivery and continuous feedback is …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds