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
Email: David (dot) Catherman (at) Hotmail (dot) com
David Catherman has 20+ years designing and developing database applications with |
References:
- Windows Forms Data Binding Issue: Conclusion by Rockford Lhotka