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






4.56/5 (3投票s)
本文介绍了一个根据最佳实践构建软件组件的示例,该组件可通过代码生成技术进行自定义。
引言
本文旨在介绍一个根据最佳实践构建软件组件的示例,该组件可通过代码生成技术进行自定义。
本文中的示例展示了一个与 DataTable
绑定的 Windows 窗体,其中包含一些额外的通用功能,如记录导航控件、访问控制管理和筛选功能。
目的是将所有必要功能集成到一个经过预先测试的控件中,并制定一套简单的自定义规则,这些规则可以通过自动过程来实现。
第一步:需求
我的窗口需要哪些功能?
- 仅查看和编辑模式管理。
- 在编辑模式下,用户可以添加、修改、删除记录,并具有更改确认能力。
- 用户必须能够撤销所做的更改。
- 从数据库检索并存储到数据库。
- 记录导航的控制。
- 查看、编辑和应用检索数据的筛选规则。
- 支持以编程方式更改数据源。
第二步:原型设计
逻辑全部封装在基类中。自定义是通过重写某些钩子方法实现的。基类窗体通过引发事件来通知继承者。有事件通知导航更改、视图模式更改、数据更改和筛选结果。
窗体有两种显示模式:“仅查看模式”,在此模式下不允许编辑操作,但用户可以跨记录导航以及编辑和应用筛选;以及“编辑模式”,在此模式下用户可以通过添加、删除和修改行来更改记录。
用户可以像 Microsoft Access 窗体一样输入筛选条件。与数据源绑定的控件变成文本框,用户可以在其中为与之关联的数据字段输入筛选规则。因此,窗口有一个 ViewMode
属性,可以设置为以下值之一:ViewMode.Data
、ViewMode.Filter
或 ViewMode.Constraint
。在 ViewMode.Data
中,控件绑定到数据源;在 ViewModeFilter
和 ViewModeConstraint
中,控件绑定到一个临时表,该表用于接受字符串值,这些字符串值是要应用的筛选或约束规则。约束模式尚未实现。目的是让用户输入类似筛选约束的约束,但这些约束会作为“where”约束传递给业务。
然后,架构将是
根据 MVC 模式,我创建了包含三个 DataTable
的 DataSet
(MainDataSet
)作为模型。
DataSource
:包含从数据库检索的数据,这些数据是绑定的目标;FilterSource
:一个包含筛选规则的临时表;ConstraintSource
:一个包含传递给 where 子句的规则的临时表。
窗体可以通过 DisplaySource
对象访问每个表的数据。DisplaySource
只是一个指向三个源之一的链接。DataSource
通过 DataView
访问,以使用筛选和访问策略功能。SetViewMode()
方法控制此行为。
基类需要了解哪些自定义信息?
基类窗体必须了解数据结构以及哪个控件已绑定。继承者可以通过重写一些方法(如 BindToData()
)来实现此目的。当设置 ViewMode.Data
时,窗体基类会调用此方法。当数据在控件中显示时,如果 WinMode
是 ViewOnly
,则这些控件必须是只读的;如果 WinMode
是 Edit
,则必须是可编辑的。为了管理此行为,基类调用方法 protected overridable SetReadonly(boolean)
,继承者可以控制该方法。
所有绑定都由 .NET 框架的原生绑定功能在内部管理。
Rowstate
和 Hasversion
属性用于跟踪记录更改。
基类窗体还在显示记录更改时引发 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。我希望读者能有所贡献……