Extending the Forms Binding in Visual Studio

in #utopian-io6 years ago (edited)

What Will I Learn?

This tutorial demonstrates how to create a User Control to extend the functionality of the Binding Navigator component.

Requirements

  • Visual Studio Code
  • Northwind database

Difficulty

  • Intermediate

Tutorial Contents

  • Binding Navigator Component in Visual Studio
  • Implementing a Generic Table Update Interface
  • Extending the Binding Navigator Control
  • Prompting the User for Saves

Extending the Forms Binding in Visual Studio

Visual Studio offers fantastic improvements in rapidly constructing Windows and Smart Client applications. The Data Sources panel automates placing controls on a form and binding them through the Binding Source component. While the Binding Navigator component offers great potential, some simple extensions vastly improve its capability to completely automate navigation and CRUD functionality for data access. This tutorial demonstrates how to create a User Control to extend the functionality of the Binding Navigator component.

Much of the increased functionality is gained by extending the Binding Navigator component in a user control to automate the saving of edited data. In the process, the Binding Source control needs to be extended and an interface template needs to be added to the data access logic.

Binding Navigator Component in Visual Studio

Create a new Windows application and add a new Data Source (DataSet) based on the Customer and Order tables in the Northwind database. The Data Sources panel offers the convenience of dragging tables or fields from a Typed Dataset and dropping them on a form to create fully bound and ready to use data access logic. Use the new data source created above to drag the Customer table onto the form. The wizard then creates an instance of the dataset, a Binding Source component linked to the table in the dataset, and a Binding Navigator component linked to the binding source. If the dataset is local to the project, the wizard also instantiates a table adapter and adds a call to the Fill method. (But if you are using an Object Data Source, you have to add this code manually.)

The Binding Navigator places a tool strip docked to the top of the form that allows user navigation through the records. Buttons are provided to navigate to the first, previous, next and last records as well as any relative position by typing an index number.

Buttons are also added to add records, delete records and save edits. The save button (diskette icon) is added specifically by the Data Sources wizard and is not part of the standard navigation bar. It is this save button that has instigated this article.

When the wizard adds the save button to the navigation strip, the proper code will be generated if the dataset is local to the project (as apposed to an Object Data Source.) The following method is added for the Customer table:

Private Sub CustomersBindingNavigatorSaveItem_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles CustomersBindingNavigatorSaveItem.Click
    Me.Validate()
    Me.CustomersBindingSource.EndEdit()
    Me.CustomersTableAdapter.Update(Me.NorthwindDataSet.Customers)
End Sub

The Validate command closes out the editing of a control; the EndEdit method of the binding source writes any edited data in the controls back to the record in the dataset; and then the Update command of the table adapter sends updated records back to the database.

Implementing a Generic Table Update Interface

We need to create generic code to Fill and Update the data table. In part 3 of my previous article, I showed the code to wrap the data access logic in the business layer. Since then, I have revised the structure slightly to make use of the data table subclass to make the methods more consistent. To enforce this consistency, we need to create an interface that each table class should implement in the wrapper.

Step 1: In Solution Explorer, right click on the project and click Add, New Item.

Step 2: Choose a Class Item and name it _Interface (so it sorts to the top). This will be a utilities type of class where general methods are kept that can be accessed through out the program.

Step 3: Create an Interface called ITableUpdate which requires a Fill method and an Update method.

Public Class _Interface
    ''' <summary>
    ''' Interface for Data table in a dataset to standardize fill and update methods
    ''' </summary>
    Public Interface ITableUpdate
        Sub Fill()
        Sub Update()
    End Interface
End Class

In the Partial Class behind the DataSet (right click on the dataset and click View Code), create the following template code for each updateable table in the DataSet. In this example, I am implementing the Customer class for the NorthwindDataSet.

Partial Class NorthwindDataSet
    Shared taCustomer As New NorthwindDataSetTableAdapters.CustomersTableAdapter

    Partial Class CustomersDataTable
        Implements _Interface.ITableUpdate

        Public Sub Fill() Implements _Interface.ITableUpdate.Fill
            taCustomer.Fill(Me)
        End Sub

        Public Sub Update() Implements _Interface.ITableUpdate.Update
            taCustomer.Update(Me)
        End Sub
    End Class
End Class

By implementing the interface, a generic UI call can work for any table. Now the Save method can call the update using generic code instead of calling an update method specific for the Customer table. Then the save routine can be written once and reused rather than written specifically for each table.

Extending the Binding Navigator Control

One way to fix the problems of the navigation toolbar and extend the functionality is to create a user control based on the Binding Navigator control and then add the properties and methods that are needed.

1- Right click on the Windows project and Add, New Item. From the list of items, select the User Control and give it a name of "exBindingNavigator.vb". (If you want to use this enhancement in other applications, the user controls and custom controls should be in a separate project that can be added to each solution. For this demonstration, leave it in the Windows project.)

2- VS will create a blank control with an empty container. Click and drag the bottom right corner until it is the size of the Binding Navigator tool bar.

3- Switch back to the Customer form (or any form where the Data Sources wizard has created the navigation control) and copy the navigation tool bar.

4- Switch back to the user control and paste the navigation bar into the container.

5- In the properties box, rename the Navigator from CustomerBindingNavigator to something more generic like GenericBindingNavigator.

6- Click on the save button (diskette icon) and in the properties panel, change the name to a more general name of BindingNavigatorSaveItem.

7- Right click on the form and select View Code to open the code page. Add a Sub New constructor. In the constructor, set the Dock property to the top of the form.

Public Class exBindingNavigator
    Public Sub New()
        ' This call is required by the Windows Form Designer.
        InitializeComponent()

        ' Add any initialization after the InitializeComponent() call.
        Me.Dock = DockStyle.Top
    End Sub
End Class

8- First, we need a property to track the reference to the binding source for the embeded navigator control. One of the tasks when the binding source is defined is to get a reference to the underlying table. We could include the code to get the reference to the table here, but sometimes, the data source has not been defined before the binding source is defined, or the data source may change. Therefore, an event handler is added to handle the Data Source Changed event and an method is called to set the table reference. To fill the table automatically, a reference to the containing form is derived and the Form Load event is subscribed to.

Private WithEvents _BindingSource As BindingSource
Public Property BindingSource() As BindingSource
    Get
        Return _BindingSource
    End Get
    Set(ByVal value As BindingSource)
        GenericBindingNavigator.BindingSource = value
        _BindingSource = value
        If Not _BindingSource Is Nothing Then
            'subscribe to the events in case not yet set
            AddHandler _BindingSource.DataSourceChanged, _
                AddressOf bs_DataSourceChanged
            'get a reference to the table now
            bs_DataSourceChanged(New Object, New EventArgs)
        End If
    End Set
End Property

9- Add the following method to handle the data source changed event above.

Private Sub bs_DataSourceChanged(ByVal sender As Object, ByVal e As EventArgs)
    If Not _BindingSource Is Nothing Then
        _DataTable = GetTableFromBindingSource(GenericBindingNavigator.BindingSource)
        If Not _DataTable Is Nothing Then
            'if child BS, get reference to parent BS
            Dim testBS As BindingSource = _
                TryCast(GenericBindingNavigator.BindingSource.DataSource, BindingSource)
            If Not testBS Is Nothing Then
                ParentBindingSource = testBS 'call the getter to capture event
            End If
        End If
    End If
End Sub

Another method is needed to get the table reference from a Binding Source as used above. The Binding Source has two properties that determine which table to bind to: the Data Source and the Data Member. Most of the time, The Data Source is set to an instance of a DataSet and the Data Member is the name of the table in the DataSet. But in situations with parent-child relationships, the Data Source can be another Binding Source and the Data Member is the name of the relationship. Therefore, we need a little calculation to deduce the table.

10- Add a method called GetTableFromBindingSource which passes a Binding Source as a parameter and returns a reference to a Data Table.

Public Function GetTableFromBindingSource(ByVal bs As BindingSource)
    'get a reference to the dataset
    Dim ds As DataSet, dt As DataTable
    'try to cast the data source as a binding source
    Dim bsTest As BindingSource = bs
    Do While Not TryCast(bsTest.DataSource, BindingSource) Is Nothing
        '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
    ds = CType(bsTest.DataSource, DataSet)
    'check to see if the Data Member is the name of a table in the dataset
    If ds.Tables(bs.DataMember) Is Nothing Then
        'it must be a relationship instead of a table
        Dim rel As System.Data.DataRelation = ds.Relations(bs.DataMember)
        If Not rel Is Nothing Then
            dt = rel.ChildTable
        Else
            Throw New ApplicationException("Invalid Data Member")
        End If
    Else
        dt = ds.Tables(bs.DataMember)
    End If
    If TryCast(dt, ITableUpdate) Is Nothing Then
        Throw New ApplicationException("Table " & dt.TableName & _
            " does not implement ITableUpdate interface")
    End If
    Return dt
End Function

To get the table, we first need a reference to the dataset. If the Data Source casts correctly as a Binding Source, then we must walk back up the chain until we get to the original dataset. Once we have the dataset, we can see if the Data Member is a table in the dataset. If it is not, then it must be a relation. Looking up the relation in the set of relations in the dataset, we can get the table reference from the Child table property of the relation.

Once we get a reference to the table, we can use polymorphism to cast it as the interface we created earlier. If it does not cast, it means that the data table did not implement the interface correctly and an exception should be thrown. If it does cast, then the generic update routine can be called to save the data.

11- Add the following method into the exBindingNavigator class to cover the click event of the save button. (You can double click on the save button to generate the code stub.)

Private Sub SaveItem_Click(ByVal sender As System.Object, ByVal e As System.EventArgs)
    Me.Validate()
    _BindingSource.EndEdit()
    'cast table as ITableUpdate to get the Update method
    CType(_DataTable, _Interface.ITableUpdate).Update()
    IsDataDirty = False
End Sub

12- By adding another method to handle the Form Load event, you could have the table automatically fill itself when the form opens. This is not always needed since the tables are many times filled as needed, using logic. Therefore, this function should be selectable by adding another property, allowing the developer to choose.

Private _AutoFillFlag As Boolean = True
Public Property AutoFillFlag() As Boolean
    Get
        Return _AutoFillFlag
    End Get
    Set(ByVal value As Boolean)
        _AutoFillFlag = value
    End Set
End Property

Private Sub Form_Load(ByVal sender As Object, ByVal e As EventArgs)
    If _AutoFillFlag Then
        'cast table as ITableUpdate to get the Fill method
        CType(_DataTable, _Interface.ITableUpdate).Fill()
    End If
End Sub

13- Lastly, by subscribing to the load event of the parent form, all this should happen. When the load event of the user control fires, we can get a reference to the parent form.

Private Sub exBindingNavigator_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
    'get the reference to the hosting form
    Dim frm As Object = CType(Me, ContainerControl).ParentForm
    While TryCast(frm, System.Windows.Forms.Form) Is Nothing
        'if not a form, walk up chain
        If Not TryCast(frm, System.ComponentModel.Container) Is Nothing Then
            frm = CType(frm, ContainerControl).Parent
        Else
            frm = CType(frm, Control).Parent
        End If
    End While
    _Form = CType(frm, System.Windows.Forms.Form)
    'add the handler for the Form Load to fill the table
    AddHandler _Form.Load, AddressOf Form_Load
End Sub

Now, by adding this control to your form and setting the Binding Source property, the save button will be functional with what ever dataset you are using and with the property set, the table will automatically fill (as long as the table implements the interface).

Prompting the User for Saves

The next problem is that the user may want to be warned and given the option of changing data or rolling back the edits. Of course this should be an option, so first we need another property called AutoSave that if false, prompts the user before making the save.

1- Add this code to the Property region of the code page.

Private _AutoSaveFlag As Boolean
Public Property AutoSaveFlag() As Boolean
    Get
        Return _AutoSaveFlag
    End Get
    Set(ByVal value As Boolean)
        _AutoSaveFlag = value
    End Set
End Property

2- The above handler for the Position Changed event can be modified to check the flag and prompt the user if needed.

Private Sub bs_PositionChanged(ByVal sender As Object, ByVal e As EventArgs) _
    Handles _BindingSource.PositionChanged

    If (_IsDataDirty And Not _DataTable Is Nothing) Then
        Dim msg As String = "Do you want to save edits to the previous record?"
        If _AutoSaveFlag Or MessageBox.Show(msg, "Confirm Save", _
            MessageBoxButtons.YesNo) = DialogResult.Yes Then

            SaveItem_Click(New Object(), New EventArgs())
        Else
            _DataTable.RejectChanges()
            MessageBox.Show("All unsaved edits have been rolled back.")
            _IsDataDirty=False
        End If
    End If
End Sub

3- While we are prompting the user, the Delete record routine needs a prompt to confirm deletes. First, we need to turn off the built-in method. In the designer view of the user control, select the Binding Navigator tool strip. In the properties panel, find the property for DeleteItem (in the Items section) , drop down the list and choose (none).

4- We need another method to delete the records only after prompting. Double click on the Delete icon (red X) in the toolbar to create a code stub and add the following:

Private Sub BindingNavigatorDeleteItem_Click(ByVal sender As System.Object, _
    ByVal e As System.EventArgs) Handles BindingNavigatorDeleteItem.Click

    Dim msg As String = "Are you sure you want to delete the current record? "
    If _AutoSaveFlag Or MessageBox.Show(msg, "Confirm Delete", _
        MessageBoxButtons.YesNo) = DialogResult.Yes Then

        'Delete the current record
        _BindingSource.RemoveCurrent()
        CType(_DataTable, Win._Interface.ITableUpdate).Update()
    End If
End Sub

The Binding Navigator component can be easily extended by making it into a user control and adding properties and methods. Adding this user control to a form will implement auto filling of a data table, implement the update functionality (with user prompting), and allow specific record lookup. By putting the user control into a control library project, it can be added to any solution and speed development of data access applications.

Implementing the Interface in the Data Access logic quickly creates stubs for the needed methods for each table in the dataset, but some code does need to be written. But, this is very structured code and can easily be generated using CodeDom or a 3rd party code generation system.



Posted on Utopian.io - Rewarding Open Source Contributors

Sort:  

Thank you for the contribution. It has been approved.

  • Why not add more sreenshot about your operation

You can contact us on Discord.
[utopian-moderator]

Hey @cha0s0000, I just gave you a tip for your hard work on moderation. Upvote this comment to support the utopian moderators and increase your future rewards!

Congratulation

Today one year ago you joined SteemIt
Thank you, for making SteemIt great and Steem on for more years to come!

(You are being celebrated here)

Hey @yissakhar I am @utopian-io. I have just upvoted you!

Achievements

  • WOW WOW WOW People loved what you did here. GREAT JOB!
  • You have less than 500 followers. Just gave you a gift to help you succeed!
  • Seems like you contribute quite often. AMAZING!

Community-Driven Witness!

I am the first and only Steem Community-Driven Witness. Participate on Discord. Lets GROW TOGETHER!

mooncryption-utopian-witness-gif

Up-vote this comment to grow my power and help Open Source contributions like this one. Want to chat? Join me on Discord https://discord.gg/Pc8HG9x

Awesome! Nice post yissakhar

Your article is well written, and I will continue to pay attention to you.
I want to ask you how to do this steemit?

What do you do to make your steemit get such a high income?
Sincerely ask for you, thank you!

If u want check my article about Price of 20 SBD/60USD: 7 World's Continents photo challenge WEEKLY WINNER announcement
You can find the article here Price of 20 SBD/60USD: 7 World's Continents photo challenge WEEKLY WINNER announcement

I will follow you yissakhar

Coin Marketplace

STEEM 0.33
TRX 0.14
JST 0.041
BTC 66682.58
ETH 3737.81
USDT 1.00
SBD 5.31