Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Mobile

Windows Forms Bindings: A best practice oriented to code generation (Part I)

4.56/5 (3 votes)
15 May 2007CPOL5 min read 1   151  
This article shows an example of how to build a software component according to best practices that is customizable through code generation techniques.

Screenshot - Best.jpg

Introduction

The aim of this article is to show an example of how to build a software component according to best practices that is customizable through code generation techniques.

The example reported in this article shows a Windows Form bound to a DataTable with several additional common features like record navigation control, access control management, and filter capability.

The purpose is to include all the features needed in one pre-tested control and make a set of simple customization rules that can be implemented by an automatic process.

First Step: Requirements

What are the features I need for my window?

  • View only and Edit mode management.
  • In Edit mode, the users can add, modify, and delete records with a changes confirmation ability.
  • User must be able to undo the changes made.
  • Retrieve from and store to database.
  • The control of navigation across records.
  • View, edit, and apply filter rules on data retrieved.
  • Support for changing the data source programmatically.

Second Step: Prototyping

The logic is all encapsulated in the base class. Customization is made by overriding some hook methods. The base class form notifies inheritors by raising events. There are events to notify navigation changes, view mode changes, data changes, and the filtering result.

The form has two display modes: "View only mode" where edit operations are not permitted but the user can navigate across records and edit and apply filters, and "Edit mode" where the user can change the records by adding, deleting, and modifying rows.

The user can input filters like a Microsoft Access form. The controls bound to the data source become text boxes where the user can put filter rules for the data fields associated to them. For this reason, the window has a ViewMode property which can be set to one of these values: ViewMode.Data, ViewMode.Filter, or ViewMode.Constraint. In ViewMode.Data, the controls are bound to the data source, and in ViewModeFilter and ViewModeConstraint, the controls are bound to a temporary table made to accept string values which are the filter or constraint rules to apply. The constraint mode has not been implemented yet. The purpose is to let the user input constraints like the filter constraints but that are passed to the business like "where" constraints.

Then, the architecture will be:

Screenshot - Architecture.jpg

According to the MVC pattern, I've created the model with a DataSet (MainDataSet) which contains three DataTables:

  • DataSource: it contains the data retrieved from the database which are the target of the binding,
  • FilterSource: a temporary table which contains the filter rules,
  • ConstraintSource: a temporary table which contains the rules passed to the where clause.

The form can access data of each table through the DisplaySource object. The DisplaySource is only a link which points to one of the three sources. The DataSource is accessed through a DataView to use a filter and access policy features. The SetViewMode() method controls this behavior.

What does the base class need to know about customization?

The base form must know the data structure and which control is bound to. Inheritors can do this by overriding some methods as BindToData(). The form base calls this method when ViewMode.Data is set. When data is displayed in the controls, they have to be readonly if WinMode is ViewOnly, or have to be editable when WinMode is Edit. To manage this behavior, the base class calls the method protected overridable SetReadonly(boolean) that inheritors can control.

All bindings are internally managed by the native binding features of the .NET framework.

The properties Rowstate and Hasversion are used to track record changes.

The base form also raises the event OnCurrent when the displayed record changes, so inheritors can execute additional tasks like displaying additional information related to the current record.

Data is retrieved from and stored to a database by calling the RetriveData() and CommitData() base methods.

Third Step: Customization

The example project is shipped with a jet database that contains the "Project" table. This table is the data source of the customized form. It consists of five fields with some constraints to be near to real-world needs.

  • ProjID: Primary key, not nullable, autonumber
  • ProjName: not nullable, string
  • ProjDesc: nullable, string
  • StartDate: nullable, date
  • Progress: nullable, numeric

The methods that access the database and that describe the data structure are defined in the test for convenience. (It would be better if these methods were defined in a business class.) These business methods are defined in the "business functions" region.

Writing the customization: At first, we need to customize the method SetTableDefinition() which is called when the form is initializing. Then, we have to make the binding:

VB
Protected Overrides Sub SetTableDefinition()
    'Build the table definition

    Me.DataSource = GetTableSchema()
End Sub

'This can be a simple strongly typed DataTable.

Private Function GetTableSchema() As DataTable
    Dim t As New DataTable("Projects")
    Dim dc As DataColumn
    dc = New DataColumn("ProjID", GetType(Int32))
    dc.AutoIncrement = True
    dc.AllowDBNull = False
    Me.DataSource.Columns.Add(dc)
    dc = New DataColumn("ProjName", GetType(String))
    dc.AllowDBNull = False
    Me.DataSource.Columns.Add(dc)
    dc = New DataColumn("ProjDesc", GetType(String))
    Me.DataSource.Columns.Add(dc)
    dc = New DataColumn("StartDate", GetType(Date))
    Me.DataSource.Columns.Add(dc)
    dc = New DataColumn("Progress", GetType(Int32))
    Me.DataSource.Columns.Add(dc)
    Return t
End Function

Protected Overrides Sub BindTo_Data()
    'ProjID

    Me.BindControl(Me.txtProjID, "Text", Me.DisplaySource, "ProjID")
    'ProjName

    Me.BindControl(Me.txtProjName, "Text", Me.DisplaySource, "ProjName")
    'ProjDesc

    Me.BindControl(Me.txtProjDesc, "Text", Me.DisplaySource, "ProjDesc")
    'StartDate

    Me.BindControl(Me.dtpStartDate, "Value", Me.DisplaySource, "StartDate")
    'Progress

    Me.BindControl(Me.txtProgress, "Text", Me.DisplaySource, "Progress")
End Sub

The user input is controlled by calling the SetReadOnly(boolean) method. It is called with a true value when WindowMode is changed to ViewOnlyMode and with a false value when it is changed to EditMode.

VB
Protected Overrides Sub SetReadonly(ByVal Value As Boolean)
    If Value Then
        'view only mode (user input on all widgets are disabled)

        Me.txtProjID.ReadOnly = True
        Me.txtProjName.ReadOnly = True
        Me.txtProjDesc.ReadOnly = True
        Me.dtpStartDate.Enabled = False
        Me.txtProgress.ReadOnly = True
    Else
        'edit mode 

        ' Although user input is allowed,
        ' the PrimaryKey of type autonumber cannot be editable.

        ' Otherwise when window displays the filters, user can type an expression.

        If Me.ViewMode = ViewModeEnum.ViewFilters Then
            Me.txtProjID.ReadOnly = False
        Else
            Me.txtProjID.ReadOnly = True 
        End If
        'the other controls are enabled

        Me.txtProjName.ReadOnly = False
        Me.txtProjDesc.ReadOnly = False
        Me.dtpStartDate.Enabled = True
        Me.txtProgress.ReadOnly = False

    End If
End Sub

During test form initialization, a record navigation control is added, the database connection is opened, and a DataAdapter is created to be used to retrieve and store data:

VB
Private Function GetAllProject() As DataTable
    Dim t As DataTable
    t = New DataTable("Projects")

  'use the adapter to fill the table

    Me.ad.Fill(t)
    Return t
End Function

Private Sub SaveProjects(ByVal table As System.Data.DataTable)
  'use the adapter to store the table changes

    ad.Update(table)
End Sub

During loading, the method RetriveDisplayData() is called and the data-source is set to the table retrieved.

VB
Private Sub BindingBest_Load(ByVal sender As Object, _
            ByVal e As System.EventArgs) Handles MyBase.Load
    'Custom: on load force a request to load data

    newDtTb = RetriveDisplayData("")
    Me.DataSource = newDtTb
End Sub

The handler BindingBest_Current() shows how we can use the current event to change the view according to the value of the current record displayed. The cmdTest_Click() handler shows how to programmatically change the values in the current record.

Last Step: Make a Code Generator

This step will be discussed in the second part of this article.

Other Points of Interest

Record changes are managed by the native binding features of the .NET framework. According to Microsoft best practices, all changes have to be made through the CurrencyManager. While the user is modifying a record, the associated row has a "proposed version" that will be confirmed by calling currencymanager.EndCurrentEdit, or rolled back by calling CancelCurrentEdit (see the FriendSave() and FriendRestore() methods). Then, the "proposed" version becomes the "current" and the row state has changed. Then, while the records are modified, the row state tracks the history of the changes made. The rows can be in the state added, deleted, or modified so the data adapter can know when to send an insert or an update or a delete statement. After committing the work, the row state is reset.

VB
Public Sub FriendSave(Optional ByVal DisplayWarning As Boolean = False)
    Dim cm As CurrencyManager = Me.BindingContext(Me.DisplaySource)
    Me.Cursor.Current = System.Windows.Forms.Cursors.WaitCursor

    'If there are records

    If cm.Count > 0 Then
        Dim r As DataRowView = cm.Current
        Dim mret As DialogResult
        If DisplayWarning Then
            mret = MessageBox.Show("Do you want to save changes ?", _
                        "Confirmation", MessageBoxButtons.YesNoCancel, _
            MessageBoxIcon.Question)
        End If

        If mret = DialogResult.Cancel Then
            'Custom exception to let inheritors trap the specific error

            Throw New cancelException("User cancel error.")
        End If

        If Not DisplayWarning Or mret = DialogResult.Yes Then
            Try
                'proposed version becomes current

                cm.EndCurrentEdit()

                'TODO: New transaction mode. For each record change commit the work.

                If Me.p_TransactionMode = TransactionModeEnum.Row Then
                    Me.CommitSave(Me.DisplaySource.Table)
                End If

            Catch ex As Exception
                'UserCancelError

                MessageBox.Show(ex.Message, "Error", _
                      MessageBoxButtons.OK, MessageBoxIcon.Error)
                'Throw ex

            End Try
        Else
            cm.CancelCurrentEdit()
        End If

        cm.Refresh()
    End If
    Me.Cursor.Current = System.Windows.Forms.Cursors.Default

    'Refresh the view

    Me.dbgp.Text = GetRowState()

End Sub
Public Sub FriendRestore(Optional ByVal DisplayWarning As Boolean = False)
    Dim cm As CurrencyManager = Me.BindingContext(Me.DisplaySource)
    If cm.Count > 0 Then
        Dim r As DataRowView = cm.Current
        Dim accept As Boolean
        accept = True
        If DisplayWarning Then
            If MessageBox.Show("Do you want to discard changes ?", _
                 "Confirmation", MessageBoxButtons.OKCancel) = DialogResult.Cancel Then
                accept = False
            End If
        End If

        If accept Then
            cm.CancelCurrentEdit()
        End If

    End If

    'Refresh the view

    Me.dbgp.Text = GetRowState()

End Sub

Enhancements

There are several ways to make a software component ready to be customized by automatic processes. I think that the better .NET way is to make use of Attributes. I hope readers will contribute...

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)