65.9K
CodeProject 正在变化。 阅读更多。
Home

Windows 窗体绑定:一种面向代码生成的最佳实践(第一部分)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.56/5 (3投票s)

2007年5月15日

CPOL

5分钟阅读

viewsIcon

37784

downloadIcon

168

本文介绍了一个根据最佳实践构建软件组件的示例,该组件可通过代码生成技术进行自定义。

Screenshot - Best.jpg

引言

本文旨在介绍一个根据最佳实践构建软件组件的示例,该组件可通过代码生成技术进行自定义。

本文中的示例展示了一个与 DataTable 绑定的 Windows 窗体,其中包含一些额外的通用功能,如记录导航控件、访问控制管理和筛选功能。

目的是将所有必要功能集成到一个经过预先测试的控件中,并制定一套简单的自定义规则,这些规则可以通过自动过程来实现。

第一步:需求

我的窗口需要哪些功能?

  • 仅查看和编辑模式管理。
  • 在编辑模式下,用户可以添加、修改、删除记录,并具有更改确认能力。
  • 用户必须能够撤销所做的更改。
  • 从数据库检索并存储到数据库。
  • 记录导航的控制。
  • 查看、编辑和应用检索数据的筛选规则。
  • 支持以编程方式更改数据源。

第二步:原型设计

逻辑全部封装在基类中。自定义是通过重写某些钩子方法实现的。基类窗体通过引发事件来通知继承者。有事件通知导航更改、视图模式更改、数据更改和筛选结果。

窗体有两种显示模式:“仅查看模式”,在此模式下不允许编辑操作,但用户可以跨记录导航以及编辑和应用筛选;以及“编辑模式”,在此模式下用户可以通过添加、删除和修改行来更改记录。

用户可以像 Microsoft Access 窗体一样输入筛选条件。与数据源绑定的控件变成文本框,用户可以在其中为与之关联的数据字段输入筛选规则。因此,窗口有一个 ViewMode 属性,可以设置为以下值之一:ViewMode.DataViewMode.FilterViewMode.Constraint。在 ViewMode.Data 中,控件绑定到数据源;在 ViewModeFilterViewModeConstraint 中,控件绑定到一个临时表,该表用于接受字符串值,这些字符串值是要应用的筛选或约束规则。约束模式尚未实现。目的是让用户输入类似筛选约束的约束,但这些约束会作为“where”约束传递给业务。

然后,架构将是

Screenshot - Architecture.jpg

根据 MVC 模式,我创建了包含三个 DataTableDataSetMainDataSet)作为模型。

  • DataSource:包含从数据库检索的数据,这些数据是绑定的目标;
  • FilterSource:一个包含筛选规则的临时表;
  • ConstraintSource:一个包含传递给 where 子句的规则的临时表。

窗体可以通过 DisplaySource 对象访问每个表的数据。DisplaySource 只是一个指向三个源之一的链接。DataSource 通过 DataView 访问,以使用筛选和访问策略功能。SetViewMode() 方法控制此行为。

基类需要了解哪些自定义信息?

基类窗体必须了解数据结构以及哪个控件已绑定。继承者可以通过重写一些方法(如 BindToData())来实现此目的。当设置 ViewMode.Data 时,窗体基类会调用此方法。当数据在控件中显示时,如果 WinModeViewOnly,则这些控件必须是只读的;如果 WinModeEdit,则必须是可编辑的。为了管理此行为,基类调用方法 protected overridable SetReadonly(boolean),继承者可以控制该方法。

所有绑定都由 .NET 框架的原生绑定功能在内部管理。

RowstateHasversion 属性用于跟踪记录更改。

基类窗体还在显示记录更改时引发 OnCurrent 事件,以便继承者可以执行额外任务,例如显示与当前记录相关的其他信息。

通过调用 RetriveData()CommitData() 基类方法,从数据库检索数据并将其存储到数据库。

第三步:自定义

示例项目附带一个 jet 数据库,其中包含“Project”表。该表是自定义窗体的数据源。它由五个字段组成,并包含一些约束,以接近实际需求。

  • ProjID:主键,非空,自动编号
  • ProjName:非空,字符串
  • ProjDesc:可空,字符串
  • StartDate:可空,日期
  • Progress:可空,数字

用于访问数据库和描述数据结构的方法已在测试中定义,以便于使用。(如果这些方法定义在业务类中会更好。)这些业务方法定义在“业务函数”区域。

编写自定义:首先,我们需要自定义 SetTableDefinition() 方法,该方法在窗体初始化时调用。然后,我们需要进行绑定。

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

通过调用 SetReadOnly(boolean) 方法来控制用户输入。当 WindowMode 更改为 ViewOnlyMode 时,它会以 true 值调用;当更改为 EditMode 时,它会以 false 值调用。

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

在测试窗体初始化期间,会添加一个记录导航控件,打开数据库连接,并创建一个 DataAdapter 以用于检索和存储数据。

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

加载期间,会调用 RetriveDisplayData() 方法,并将数据源设置为检索到的表。

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

BindingBest_Current() 处理程序展示了如何使用当前事件来根据显示当前记录的值更改视图。cmdTest_Click() 处理程序展示了如何以编程方式更改当前记录中的值。

最后一步:创建代码生成器

此步骤将在本文的第二部分讨论。

其他值得关注的点

记录更改由 .NET 框架的原生绑定功能管理。根据 Microsoft 的最佳实践,所有更改都必须通过 CurrencyManager 进行。当用户修改记录时,关联的行会有一个“建议版本”,该版本将通过调用 currencymanager.EndCurrentEdit 来确认,或通过调用 CancelCurrentEdit 来回滚(请参阅 FriendSave()FriendRestore() 方法)。然后,“建议”版本成为“当前”版本,并且行状态已更改。然后,当记录被修改时,行状态会跟踪所做更改的历史记录。行可能处于已添加、已删除或已修改的状态,因此数据适配器知道何时发送 insert、update 或 delete 语句。完成工作后,行状态会重置。

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

增强功能

有几种方法可以使软件组件准备好由自动进程自定义。我认为最好的 .NET 方法是利用 Attributes。我希望读者能有所贡献……

© . All rights reserved.