Extending the Win Forms Binding Source Component

8
Extending the Win Forms Binding Source Component Posted by Quin Street on January 10th, 2008 By David Catherman Introduction Android on X86 - Go Native. Go Fast. Go with Intel for a Superior User Experience! Download Now 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.

Transcript of Extending the Win Forms Binding Source Component

Page 1: Extending the Win Forms Binding Source Component

Extending the Win Forms Binding Source Component Posted by Quin Street on January 10th, 2008

By David Catherman

Introduction

Android on X86 - Go Native. Go Fast. Go with Intel for a Superior User Experience! Download Now

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.

Page 2: Extending the Win Forms Binding Source Component

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

Page 3: Extending the Win Forms Binding Source Component

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

Page 4: Extending the Win Forms Binding Source Component

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.

Page 5: Extending the Win Forms Binding Source Component

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

Page 6: Extending the Win Forms Binding Source Component

_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

Page 7: Extending the Win Forms Binding Source Component

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.

Page 8: Extending the Win Forms Binding Source Component

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: • Windows Forms Data Binding Issue: Conclusion by Rockford Lhotka