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.