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

视觉上可继承的非绑定数据窗体

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (14投票s)

2004年3月19日

CPOL

31分钟阅读

viewsIcon

69694

downloadIcon

306

一个使用数据读取器并且可以视觉上继承的基类数据窗体。

引言

在 Visual Basic .NET 中,您是否曾想过显示一个简单的数据表单,允许用户添加/更新/删除某些信息,但发现数据表单向导生成的数据表单有所欠缺?

您是否希望您的数据表单是标准的、外观精美的,并且包含一个简单的列表框,从中可以选择当前记录?当然,这对于拥有大量记录的表来说可能不是最佳解决方案,但对于您需要提供更改方式的简单表来说,它仍然有很多用途,而且您真的不想花费太多时间。一个很好的例子就是简单的查找表。在本篇文章中,简单的查找表是指包含另一个表中某个字段可能包含的所有可能值的表。但是,此表单也适用于允许用户管理具有少量记录(例如少于 1000 条)的简单表的数据。您还可以选择采用此想法并使其适用于大量记录。

假设您有一个跟踪执法机构失窃财产的表,并且该表中的一个字段名为“类别”。您创建一个名为 `tblCategory` 的表来存储所有有效的财产类别。当用户输入失窃财产时,“类别”组合框 (`cboPropCategory`) 将填充 `tblCategory`(查找表)中的所有值。虽然您可能需要花费一些时间来制作输入失窃财产的数据输入表单,但您可能不想花费太多时间制作一个简单的表单来管理可用类别,但您的用户希望能够随时添加或甚至更改它们。

在本教程中,我们将探讨如何制作一个无绑定的基数据表单,它使用 DataReader 而不是 DataSet。虽然 IDE 提供了许多使用 DataSet 的工具,但对 DataReader 的支持却很少。然而,在某些情况下,例如本例,您可能希望更改程序其余部分使用的一些值,并使数据库更改立即生效,因此 DataReader 和一些命令对象是最佳选择。我们将从讨论 Visual Basic .NET 中的表单继承开始。

背景

我编写此代码是为了解决我遇到的一个实际问题,并且目前正在一些应用程序中使用它。我对于仅仅为了获得一个漂亮、易于使用、外观良好且使用 DataReader 而不是 DataSet 的数据输入表单所需的工作量感到非常沮丧。自从我编写了这个基类表单以来,我反复使用它,并且对它的灵活性和易用性感到非常满意。我希望您会喜欢它,并希望它能在至少一些方面满足您的需求。

即使它可能不是您想要使用的东西,我希望您能从中获得一些收获,例如学习如何使用 Visual Inheritance,或如何使用 VB.NET 提供的一些 OO 功能,例如可以和必须重写的 (overridden) 方法、构造函数等。您还将发现,在源代码中包含了一个名为 `clsParamBuilder` 的漂亮类,它使得构建命令对象和添加参数所需的工作量大约是通常的三分之一。所以希望本文对每个人都有所启发。

请随时发布任何评论、错误报告或改进建议。

创建基类表单

基类表单实际上就像任何其他表单一样,但它是另一个表单的外观和代码将继承的来源。因此,在 IDE 中,创建一个 VB.NET 项目,然后在解决方案资源管理器中,右键单击项目,选择“添加”,然后选择“添加 Windows 窗体”。在出现的对话框中,为您的基类表单命名。我选择通过命名我的基类表单为 `BASEfrmUnboundDataForm` 来区分它与其他表单。“frm”前缀通常用于 Visual Basic 的表单名称中。我选择在前面加上“BASE”一词,以帮助我记住这是一个要继承的基类表单,而不是直接使用的。

我们的想法是创建这个基类表单,在其上布局所有数据表单应有的控件,并包含执行大多数操作所需的基本代码。我们应该允许许多方法被重写,如果继承表单的程序员希望偏离基本操作。请记住,其他表单将继承此表单,是的,仍然需要一些编程和添加控件,但我们将大大减少创建工作表单或多个工作表单所需的代码和思考量。因此,我们应该保持基类表单的布局简单和通用。

添加到基类表单的控件

  • 添加按钮 (`btnAdd`)
  • 更新按钮 (`btnUpdate`)
  • 删除按钮 (`btnDelete`)
  • 刷新按钮 (`btnRefresh`)
  • 查找列表框 (`lstLookup`)
  • 在查找列表框上方用于标题的标签 (`lblLookupList`)
  • 一些面板控件用于处理大小调整和间距问题 (`pnlTop`、`pnlRight` 和 `pnlFields`)
  • 一个 GroupBox 控件,用于创建简单的矩形外观 (`gbHorvRule`)
  • 一个 ErrorProvider,用于允许数据输入错误通知 (`errProvider`)

完成后,您的表单应如下面的图 1 所示。


图 1

查看上面的表单(图 1),请注意有 3 个面板控件,并且我所有的控件都放在这些面板中。`pnlTop` 是左上角的面板,包含查找列表框 `lstLookup`。它的 `dock` 属性设置为“Top”。

右侧的面板包含 **添加** (`btnAdd`)、**更新** (`btnUpdate`)、**删除** (`btnDelete`) 和 **刷新** (`btnRefresh`) 按钮,名为 `pnlRight`,其 `dock` 属性设置为“Right”。第三个面板控件名为 `pnlFields`,位于左下方。该面板的 `dock` 属性设置为“Fill”。这是继承此表单的表单将放置其数据输入控件(即文本框、复选框等)的位置。为了占据其他两个面板控件留下的所有空间,它的 `dock` 属性设置为“Fill”。

(注意:我的基类数据表单最初的高度和宽度更大,但我将其缩小以减小此网站的图像尺寸。将您的表单初始高度和宽度设置为您希望继承表单拥有的范围。)

需要记住的一点是,除非您适当地更改每个控件的 `modifiers` 属性,否则这些控件将无法更改。例如,将 `btnAdd` 的 `modifiers` 属性设置为“private”意味着继承的表单根本无法更改其大小、文本或任何其他方式。出于这个原因,我将所有控件的 `modifiers` 属性设置为“protected friend”,以便任何继承的表单都可以根据需要更改它们。这提供了最大的灵活性。

我添加了一个名为 `gbHorvRule` 的 GroupBox 控件,只是为了在视觉上将控件按钮与表单的左侧分开。

添加到基类表单的代码

为了提供所需的默认功能,但允许继承表单偏离它,需要在这一部分进行大量思考。

首先,我们需要一个枚举来跟踪表单处于添加模式还是更新模式。这是必需的,因为用户可以单击 `btnAdd` 添加新记录,从而将表单重置为接受新数据,并在输入数据后,用户必须单击 `btnUpdate` 来接受更改。但是,用户可能一直在编辑现有记录,刚刚进行了更改,然后单击 `btnUpdate` 来接受它们。`btnUpdate` 将调用一个方法,该方法将根据当前操作决定调用 `InsertRecord` 还是 `UpdateRecord`。

   Protected Enum FormDataMode
      Add
      Update
   End Enum

我们还需要在基类表单中添加一些成员变量。

   'internal to base form cannot be affected by outside means
   Private _DataMode As FormDataMode = FormDataMode.Update

以下是必须设置的成员变量,否则将无法正常工作。

   'must be set
   Private _ConnectionString As String = String.Empty
   Private _TableName As String
   Private _PrimaryKeyControl As Control                  
    'the form control that has the primary key--can be 
    'hidden but must exist
   Private _PrimaryKeyField As String

我选择将成员变量命名为以“_”开头。`_ConnectionString` 显然将包含连接到数据库所需的字符串。`_TableName` 是表单将代表的表的名称。`_PrimaryKeyControl` 实际上是对表单中包含主键的控件的引用。`_PrimarykeyField` 是此表中主键字段的名称。拥有所有这些信息后,基类表单无需或只需少量额外编码即可正确执行某些操作。当然,所有成员变量都使用属性或通过构造函数设置,而不是直接设置。

以下是可以设置但有默认值的变量

   'can be set
   Private _PanelsUseFormsBGColor As Boolean = True      
        'if true then panel is the same as the form's color
   Private _PanelsUseFormsBGImage As Boolean = True      
         'if true then panel uses same background image as the form
   Private _ConfirmBeforeDeleting As Boolean = True      
           'if true, then the default deletion mechanisms 
           'prompt with a messagebox before deleting.
   Private _PrimaryKeyValueNeedsQuotes As Boolean = False 
      'set true if pk is a varchar or something other than number
   Private _TypeOfDatabase As ifrmDataProviderType = _
          ifrmDataProviderType.SQLServer 

`ifrmDataProviderType.SQLServer` 是我在一个名为 BASEFORMEnums.vb 的文件中提供的枚举。它在一个单独的文件中,以便我可以在各种地方使用它。其内容如下:

   Public Enum ifrmDataProviderType
     SQLServer = 0
     OleDB
     Oracle 'not yet implemented
   End Enum

最后一个成员变量(如下)是一个字段,它将有一个只读属性,可以用来查看数据是否已更改。

   'readonly externally
   Private _DataChanged As Boolean = False

属性

为每个成员变量添加了基本属性,遵循明显的命名约定,除了返回该值或设置该值外,不做任何其他事情。例如,请参阅 `ConnectionString` 属性(如下)。

   Property ConnectionString() As String
      Get
         Return _ConnectionString
      End Get
      Set(ByVal Value As String)
         _ConnectionString = Value
      End Set
   End Property

例外的是 `DataMode` 属性(如下),它不仅设置 `_DataMode` 的值,而且还调用一个空的、可重写的函数来提供类似事件的功能。重写表单可以仅重写此函数并提供在数据模式更改时发生的任何所需功能。

   Private Property DataMode() As FormDataMode
     Get
        Return _DataMode
     End Get
     Set(ByVal Value As FormDataMode)
        _DataMode = Value
        DataModeChanged(Value)
             'call an empty overridable function
     End Set
   End Property
`DataModeChanged` 方法(如下)
   Protected Overridable Sub DataModeChanged(ByVal _
            CurDatamode As FormDataMode)
      'override this to make changes when form datamode changes
   End Sub
(所有其他属性的代码都包含在下载中。)

构造函数代码

对于构造函数代码,我选择为基类表单提供三个构造函数。**第一个“New()”只是为了让表单设计器允许我的表单加载。它不应用于其他目的。** 另外两个构造函数中的一个应始终使用。

另外两个构造函数是相同的,只是其中一个有一个额外的参数来指定数据库类型。如果未指定,则假定为 SQL Server。您可以根据需要添加其他数据库支持。

请注意,通过构造函数,表单可以获取连接字符串、要处理的表名以及该表的主键名。

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

      'this called only here to allow it to load in designer; 
         'should never be used
      Debug.WriteLine("Do not use the new() constructor but" & _
             " only those with arguments")
   End Sub

   Public Sub New(ByVal strConnectionString As String, _
      ByVal strTableName As String, 
           ByVal strPrimaryKeyField As String)
      MyBase.New()
      'This call is required by the Windows Form Designer.
      InitializeComponent()

      _ConnectionString = strConnectionString
      _TableName = strTableName
      _PrimaryKeyField = strPrimaryKeyField
   End Sub

   Public Sub New(ByVal strConnectionString As String, _
          ByVal strTableName _
         As String, ByVal strPrimaryKeyField As String,_
          ByVal TypeOfDatabase _
         As ifrmDataProviderType)
      MyBase.New()

      'This call is required by the Windows Form Designer.
      InitializeComponent()
      _ConnectionString = strConnectionString
      _PrimaryKeyField = strPrimaryKeyField
      _TableName = strTableName
      _TypeOfDatabase = TypeOfDatabase
   End Sub

方法

现在让我们看看我们必须在基类表单中实现的方法。

我们的 `Form_Load` 事件需要通过调用 `FillLookup` 来填充我们的查找表。但在设计模式下不能填充它。

   Private Sub BASEfrmUnboundDataForm_Load(ByVal sender As Object, _
        ByVal e As System.EventArgs) Handles MyBase.Load
      'be sure to fill the lookup on load
      'don't fill in designer
      If Me.DesignMode = False Then FillLookup()
   End Sub

`FillLookup` 相关的方法将演示在基类表单中提供基本功能的通用方法,在各个步骤调用方法——其中一些方法默认执行操作,并且可以被重写以进行更改,而另一些方法是空的、可重写的,并且除非被重写,否则不执行任何操作。

`FillLookup` 的代码如下所示:

   Protected Overridable Sub FillLookup()
      'avoid filling lookup in designer
      If Me.DesignMode = True Then Exit Sub

      'This procedure populates the list box 
      'on the form with a list of 
      ' records; should select from this list.
      Dim conn As System.Data.IDbConnection
      Dim cmdGetRecords As System.Data.IDbCommand

      Dim dr As System.Data.IDataReader
      Dim objListItem As clsListItem
      Dim strID As String

      'open the connection
      conn = OpenConnection()            
          'call OpenConnection which returns nothing on failure
      If conn Is Nothing Then Exit Sub        
          'exit if  couldn'tt

      Try
请注意,在上面的代码段中,`conn` 和 `cmdGetRecords` 变量是如何定义的。我没有将它们定义为 `SQLConnection` 和 `SQLCommand` 对象,而是选择使用 `SQLConnection` 和 `OLEDBConnection` 实现的接口 (`IDBConnection`) 来定义它们,并对命令和 `DataReader` 对象 (`IDbCommand`、`IDataReader`) 做了同样的处理。这样做的好处是使用的连接和命令对象不会只绑定到一个数据库,而是可以接受实现这些接口的任何数据库。例如,我使用了 `IDbConnection`、`IDbCommand` 和 `IDataReader`,而不是 `SQLConnection`、`SQLCommand` 和 SQL `DataReader`。

另外请注意,在设置命令对象 `cmdGetRecords`(它实际上将获取用于填充查找列表框的记录)时,我调用了 `_SelectCommandToFillLookup` 方法。我选择在所有必须重写的方法前面加上“_”,以便它们在 IDE 代码窗口的“方法名”下拉列表中显示在顶部。**继承的表单必须重写 6 个这样的方法。** 因此,基类表单只是调用此方法,而继承的表单将适当的 SQL 字符串放入命令对象并返回它。然后执行此命令对象,并将结果传递给 DataReader。

    cmdGetRecords = _SelectCommandToFillLookup()
    'set connection object for this command
    cmdGetRecords.Connection = conn

    dr = cmdGetRecords.ExecuteReader()
然后,查找列表框 `lstLookup` 被清空,并使用 DataReader,将每条记录添加到列表框中。

正如您在 Visual Basic .NET 中所知,列表框可以包含对象而不是仅仅字符串。因此,您可以创建一个包含任何内容的类,并重写 `ToString` 函数以返回将在列表框或其他任何地方显示的值。

此方法中的代码调用 `_CreateNewLookupListItem`,该方法必须被重写。程序员可以根据需要重写,但必须返回一个 `clsListItem`,该对象包含要显示的内容和主键。`_CreateNewLookupItem` 接收 DataReader,其中包含当前行 **(不应在 DataReader 上执行读取操作)**。也许如果其他人要使用您的基类表单,您只会想给他们传递 DataReader 的克隆副本或类似的东西。您可以决定是否应修改此行为。

       'clear lookup items
       lstLookup.Items.Clear()

       ' Loop through the result set using the datareader class. 
       ' The datareader is used here because all that is needed; 
       'is a forward only cursor which is more efficient. 
       Do While dr.Read()
          objListItem = _CreateNewLookupListItem(dr)
          lstLookup.Items.Add(objListItem)
       Loop
下面显示了一个继承表单重写 `_CreateNewLookupListItem` 的示例。
   Protected Overrides Function _CreateNewLookupListItem(ByVal _
        CurDataReader As System.Data.IDataReader) As clsListItem
      Dim li As New clsListItem(CurDataReader.GetValue(0), _
           CurDataReader.GetValue(0))
      Return li
   End Function 

继续 `FillLookup` 方法(如下),我们看到如果列表中有任何内容,则选择第一项。还有一些清理工作。请注意,如果发生任何错误(例如,发生异常),则会调用 `FillLookupError` 并将异常传递给它。`FillLookupError` 仅使用 msgbox 显示异常的 `ex.message` 属性。如果程序员选择以略有不同的方式处理错误,则可以重写此方法。

    'if there is at least one item, then set lookup to first one
    If lstLookup.Items.Count > 0 Then lstLookup.SetSelected(0, True)

    ' Close and Clean up objects
    dr.Close()
    conn.Close()
    cmdGetRecords.Dispose()
    conn.Dispose()
      Catch ex As System.Exception
    FillLookupError(ex)
      End Try
End Sub

这是执行每个操作的通用方案。插入、更新和删除都使用相同的基本概念。例如,当要更新记录时,将调用 `UpdateRecord`,它从 `_UpdateCommand`(已由继承表单重写)获取用于执行更新的命令,执行它,然后在发生错误时调用 `UpdateError`,如果一切顺利则调用 `UpdateSucceeded`。`UpdateSucceeded` 默认什么也不做,但可以被继承表单重写以在更新成功时执行某些操作。插入和删除使用类似的方法——并采用相同的命名约定。

而不是显示插入、更新和删除方法的全部代码,我将只重点介绍 `UpdateRecord` 方法的有趣部分,这是更新记录的主要方法。`DeleteRecord` 和 `InsertRecord` 分别是删除和插入记录的主要方法。这些方法中的每一个都可以重写,但我建议不要重写它们,而是重写它们在正常操作过程中调用的“辅助”方法。这与我们上面检查过的 `FillLookup` 方法的工作方式类似。

所有源代码都可以下载,但让我们看看这三个方法中的一个——`UpdateRecord` 方法。

   Protected Overridable Sub UpdateRecord()
      ' This sub is used to update an existing 
      'record with values from the form.
      Dim conn As System.Data.IDbConnection
      Dim cmdUpdate As System.Data.IDbCommand
      Dim intRowsAffected As Integer

      ' Validate form values; only does something 
      'if IsValidForm is overridden
      If Not IsValidForm() Then Exit Sub

请注意 `IsValidForm` 方法。这是一个返回布尔值(true/false)的函数。默认情况下,它返回 true。继承表单可以通过重写它来验证数据,然后再进行更新。如果返回 false,则调用 Exit Sub,更新永远不会发生。

      'open and get the connection
      conn = OpenConnection()
      If conn Is Nothing Then Exit Sub

      Try
         'get update command object
         cmdUpdate = _UpdateCommand()
         'set command connection
         cmdUpdate.Connection = conn
         'execute command
         intRowsAffected = cmdUpdate.ExecuteNonQuery()

         If intRowsAffected <> 1 Then
            UpdateError()
         Else
            'so anyone can see if any data was changed
            _DataChanged = True
在上面的代码段中,请注意,如果发生任何错误,将调用 `UpdateError`(一个可重写的方法)。此外,如果一切顺利,则将 `_DataChanged` 设置为 true,执行其他一些操作,然后调用 `UpdateSucceeded`。
            Dim pkValue As String = PrimaryKeyControl.Text
            'refill the lookup
            FillLookup()
            'reposition it by finding item via primary key
            FindLookupItemByPK(pkValue)
            UpdateSucceeded()
         End If
 
在上面的代码中,请注意,主键控件的值被读取并存储,然后填充查找表,以便新值出现,然后调用 `FindLookupItemByPK`(一个私有方法)并传入存储的值,以便更新的项成为当前行。最后,由于更新成功,调用 `UpdateSucceeded`(另一个可重写的方法)。
     Catch ex As System.Exception
         UpdateError(ex)
     Finally
         ' Close and Clean up objects
         conn.Close()
         cmdUpdate.Dispose()
         conn.Dispose()
      End Try
End Sub
`UpdateError` 在发生任何异常时被调用。与 `FillLookup` 方法的错误方法一样,这个方法可以被重写,但如果没有重写,它只是在 `MsgBox` 中显示 `ex.message` 的值。在子程序退出之前,连接被关闭,并且命令和连接对象被释放。

`InsertRecord` 和 `DeleteRecord` 方法遵循类似的模式。但是,`DeleteRecord` 方法使用了一些新方法,主要是 `GetLookupItemIndexByPK` 和 `AllowRecordDeletion`。

  'call the overridable function that checks to see 
   'if deletion is allowed for this item
  If Not AllowRecordDeletion(PrimaryKeyControl.Text) Then Exit Sub

`AllowRecordDeletion`(上面调用)是一个函数,它接收主键并返回 true 或 false——true 表示“可以删除”。此函数可以重写,但默认情况下,它会检查 `ConfirmBeforeDeleting` 属性,如果设置为 true,则会提示用户使用 `MsgBox` 询问他们是否确定要删除。默认代码根据他们的答案返回 true 或 false,或者如果 `ConfirmBeforeDeleting` 设置为 false,则始终返回 true。此函数可以被重写以执行一些检查,例如,“此记录是否有需要删除的子记录等?”并且如果为 true,则可能拒绝删除。这对于我们的“查找表”来说是一个好例子,其中我们有一个可能的字段值列表,我们希望用户能够添加、更新和删除这些值,但如果它们正在使用中(即至少有一个记录是使用某个值创建的),那么就不应该允许删除它们。如果该值从未被使用过,那么允许删除将是可取的。

  'get list index of item being deleted
  iItemIndex = GetLookupItemIndexByPK(PrimaryKeyControl.Text)

另一个不同的方法是 `GetLookupItemIndexByPK` 方法(上面调用),顾名思义,它接收一个主键值并返回该项在列表框 (`lstLookup`) 中的索引。这是必需的,以便在记录被删除后,列表框可以选择之前的记录(如果可用)或在用户删除最后一条记录时选择无。`DeleteRecord` 默认处理所有这些事务,但正如您所见,可以根据需要逐个表单地重写以提供更多或不同的功能。

当然,还有其他一些方法需要考虑。例如,当您单击“添加”时,基本上需要将表单清除为其默认的“新记录”值。这通过调用 `ClearForm` 方法来实现。

`ClearForm` 的默认实现使用一些非常巧妙的智能来清除字段面板 (`pnlFields`) 中的每个控件,该面板包含继承的表单将提供的​​数据输入控件。此实现检查每个控件以查看其控件类型,然后为其分配最逻辑和最预期的默认值。例如,列表框选择 -1,表示“无选择”,文本框清除文本。组合框尝试选择列表中的第一项,但失败后则不选择任何项,复选框则清除复选标记。此方法的代码如下所示。

   'ClearForm is called after new is clicked or upon 
    'startup if there are no records;
   Protected Overridable Sub ClearForm()
      'clears or sets all to defaults
      'assume basic default functionality by going through
     ' all controls in 
      'pnlfields and setting text controls to clear string; 
     'clearing checkboxes; 
      'setting combos to first element---should override to
      ' provide different functionality if needed
      Dim ctl As Control
      For Each ctl In pnlFields.Controls
         If ctl.GetType.Equals(GetType(TextBox)) Then
            CType(ctl, TextBox).Text = String.Empty
         ElseIf ctl.GetType.Equals(GetType(CheckBox)) Then
            CType(ctl, CheckBox).Checked = False
         ElseIf ctl.GetType.Equals(GetType(ComboBox)) Then
            Try
               'if possible set to first element
               CType(ctl, ComboBox).SelectedIndex = 0
            Catch
               CType(ctl, ComboBox).SelectedIndex = -1
            End Try
         ElseIf ctl.GetType.Equals(GetType(ListBox)) Then
            CType(ctl, ListBox).SelectedIndex = -1
         End If
      Next
   End Sub

将这些方法连接到我们的控件并处理一些 UI 事件

虽然我们不会讨论所有这些,但我们会看几个重要的。随意浏览下载的源代码将揭示一些其他方法,其目的应该是显而易见的。

`btnAdd_Click()`

添加按钮的点击方法只需要调用 `ClearForm`,将 `DataMode` 属性设置为 `FormDataMode.Add`,并禁用“添加”和“删除”按钮。当用户想要添加新记录时,会单击“添加”。要求他们单击“更新”按钮来提交此记录。

`btnUpdate_Click()`

更新按钮的点击方法仅在 `DataMode` 属性设置为 `FormDataMode.Add` 时调用 `InsertRecord`,在设置为 `FormDataMode.Update` 时调用 `UpdateRecord`。

`btnDelete_Click()`

删除按钮的点击方法很简单。它只调用 `DeleteRecord`。通过使用主键执行删除,此方法应默认提供所需的功能,并且可能永远不需要更改。

`btnRefresh_Click()`

刷新按钮的点击方法调用 `RefreshClicked` 方法,该方法默认调用 `FillLookup`,但也可以被重写。如果您有 `lstLookup` 列表框以外的其他列表框等需要刷新的,则应重写 `RefreshClicked`。

`lstLookup_SelectedIndexChanged`

当查找列表框 `lstLookup` 中选择的项发生变化时,我们需要用与所选项关联的记录值填充 `pnlFields` 中的控件。因此,此事件首先调用 `PopulateForm`,然后启用“添加”和“删除”按钮。它还将 `DataMode` 设置为 `FormDataMode.Update`,而不是 `FormDataMode.Add`。这样,如果用户更改了某些内容并单击“更新”按钮,将调用 `UpdateRecord` 方法来保存更改,而不是调用 `InsertRecord` 方法,后者将基于当前字段值添加新记录。

现在我们必须提供一些方法来使用 ErrorProvider 控件进行验证

我们应该提供一些方法来影响 `pnlFields` 中的控件,这些方法基于一些验证代码,这些代码可能放在 `IsValidForm` 方法中,该方法使用 ErrorProvider 控件在具有错误数据的控件旁边显示错误。

我创建了五个这样的公共方法来促进这一点。首先让我们看看它们,稍后我们将在“编写继承表单中的代码”部分看到它们如何使用。前两个方法 `ClearErr` 和 `SetErr` 可以用于任何地方,但主要用作其余三个更易于使用的方法的辅助方法。

ClearErr 它接收适当的控件,并通过调用 `errProvider.SetError` 并将控件的名称和空字符串传递给它来清除该控件的所有错误。
SetErr 它接收适当的控件,并调用 `errProvider.SetError` 将该控件的错误字符串设置为传递给 `SetErr` 的字符串。
ErrRequired-ValueSupplied 它接收适当的控件和一个错误消息。其功能类似于 ASP.NET 中的 `RequiredFieldVaildator`。要求用户输入一些内容。如果用户未输入,则返回 false 并调用 `SetErr`。
ErrRegExpMatched 它接收适当的控件、一个正则表达式和一个错误消息。它测试该控件是否符合给定的正则表达式,如果不符合,则返回 false 并使用错误消息调用 `SetErr`。
ErrText-LengthInRange 它接收适当的控件、文本的最小和最大长度以及一个错误消息。此方法仅用于文本相关控件,例如文本框或组合框(其中包含一个文本框)。如果无效,则返回 false 并使用错误消息调用 `SetErr`。

请随时根据需要添加自己的验证方法。也许您想添加一个限制数字数据最小和最大值的方法,例如“必须在 1 到 99 之间”。当调用 `IsValidForm` 时,如果您重写了它,您可以调用这些方法来决定表单是否有效,如果无效则返回 false。

也许我没有提到其他一些方法,但您会在下载的源代码中找到它们。

现在,让我们看看如何使用这个家伙……

创建继承的表单

创建继承的表单很简单,只需在解决方案资源管理器中右键单击项目,选择“添加”,然后在“添加 Windows 窗体”而不是“添加 Windows 窗体”之前,选择“添加继承的窗体”。为您的继承表单命名,然后单击“确定”。之后,“继承选择器”对话框将显示。选择 `BASEfrmUnboundDataForm` 作为要继承的表单,然后单击“确定”。现在您应该在解决方案资源管理器窗口中看到您的表单。

请记住,有 6 个方法绝对必须重写,您的表单才能工作,还有一些其他细微之处也应该执行。

查看新表单的代码,您会注意到前两行代码类似如下:

   Public Class frmInheritingForm
      Inherits MYPROJECTNAME.BASEfrmUnboundDataForm

当然,您的表单名称将是类名,并且您的项目名称实际上会出现在这里,而不是“MYPROJECTNAME”。我们可以看到我们的表单继承自 `BASEfrmUnboundDataForm`。这意味着它拥有基类表单的所有方法和代码,并且可以访问继承的表单允许访问的项。由于表单的视觉方面实际上只是由表单设计器生成到 .vb 代码文件中的代码,因此我们的表单继承了基类表单的方法和外观。这真的很酷,可以为您节省成百上千小时的重复编码。

想象一下,我们正在为警察局编写一个应用程序。我们的所有用户都是警官。因此,我们的警官表也应该是我们的用户,即基于警官的警徽号码(实际上是警徽号码加上 SSN 的后四位数字——这样做是为了防止警官 55 离职,新警官接任并获得警徽 55。如果警官一是 559306,警官二是 552963,那么它们很容易区分,是不同的警官,并且不会丢失任何历史记录)。使用此示例,我们假设我们的警官表包含警徽、名字、中间名、姓氏、密码、用户类型和是否在职(仍然在我们这里或不在)。

考虑到这一点,我们将为每个字段添加文本框控件,除了最后两个字段,它们分别是列表框和复选框。我们将为它们命名为显而易见的名称,例如 `txtBadge`。

我的实际警官表单截图(如下)

当然,数据是假的,我添加了一些我不会在文章中讨论的内容,但这个表单的完整源代码包括在 `frmOfficer.vb` 和 `frmOfficer.resx` 中供您查看。我目前实际上正在执法应用程序中使用这个表单。因此,请更改您的表单的外观,如果您实际上创建了一个真实的警官表单作为要销售的程序的一部分。请注意,在我提供的版本中,我有一个用于更改警徽的特殊按钮。在添加新记录期间,我还将警徽字段设置为只读。此外,警官图标是我的原创作品和财产,不可用于商业用途。如果您为执法部门制作一个类似此表单的商业产品,请随时用您自己的图标替换它。

编写构造函数

我们要做的第一件事是为新表单创建一个构造函数。为了使我们的表单正常工作,我们必须有一个连接字符串。

   Public Sub New(ByVal strConnectionString As String)
      MyBase.New(strConnectionString, "tblOfficer", "Badge")
      InitializeComponent()

      'indicate primary key control
      PrimaryKeyControl = txtBadge
      PrimaryKeyValueNeedsQuotes = True
      PrimaryKeyField = "Badge"
   End Sub

请注意构造函数(上面),我们接受一个连接字符串,以便当某人实例化我们的表单时,他们会将连接字符串作为 `New` 方法(构造函数)的一部分传递。

我们仍然可以使用 `MyBase` 关键字来访问我们的基类表单。因此,我们的新构造函数首先调用基类表单的构造函数,通过调用 `MyBase.New` 将连接字符串、表名和主键名传递给它。*此行应始终添加到您表单的构造函数中。*

接下来,我们设置一些属性——*这些是必需的*。我们告诉基类表单将保存主键值的控件的名称是 `txtBadge`。然后我们设置 `PrimaryKeyValueNeedsQuotes`,因为主键是字符串而不是数字,尽管大多数都会存储在文本框中。最后,我们将 `PrimaryKeyField` 属性设置为 `Badge`。

现在,我们只需要重写必须重写的 6 个方法。请记住,这些方法以“_”字符开头。要查看它们,请在继承表单的代码窗口中,单击左上角的列表框中的“overrides”(重写),然后在右侧列表框中选择要重写的方法。其中一个方法是 `_CreateNewLookupListItem` 方法。通过选择其中一个方法,VB.NET 会将方法代码模板添加到代码窗口中,即函数或子程序及其参数以及“End Function”或“End Sub”。我们只需要填充空白。

`_CreateNewLookupListItem`

`_CreateNewLookupListItem` 方法在每次要将项添加到列表框时被调用。当前的 DataReader 被传递给此方法,因此通过使用 GetString 或 GetValue 方法并创建一个 `clsListItem`,我们可以告诉基类表单如何创建列表项。这使我们在要显示的内容和该项的键方面具有一定的灵活性。下面显示了一个示例实现。

      Dim li As New clsListItem(CurDataReader.GetString(0), _
         CurDataReader.GetValue(1))
      Return li

`_SelectCommandToFillLookup`

`_SelectCommandToFillLookup` 方法返回一个命令对象,其中包含获取我们表中所有记录所需的适当 SQL,显示两个字段,第一个是要在列表框中显示的内容——可以连接起来,如下面的代码所示——但应该只返回 2 个字段。第二个字段应该是表的主键字段。这由 `FillLookup` 使用。

     Dim cmd As New System.Data.SqlClient.SqlCommand(_
        "SELECT LastName + ', ' + _
         FirstName + ' ' + 
       MidName as Name,Badge FROM tblOfficer ORDER BY LastName")
     Return cmd

上面的代码只有 2 行代码,就可以完成我们在显示查找表中的行方面想要完成的所有工作,这些行将用于导航到要删除或修改的记录。第二行仅仅返回命令对象。所以实际上,如果您愿意,可以通过将“Dim cmd as”替换为“return”并删除“Return cmd”行来在一行中完成。

`_SetFields`

基类方法 `PopulateFields` 调用 `_SetFields` 以将数据从 DataReader 移动到相应的控件。由于基类表单不知道这些控件,因此它允许您通过传递 DataReader(设置为当前行)来告诉它如何执行此操作。您只需使用 `GetString` 或 `GetValue` 来读取适当的字段。下面的代码中有一个示例。

      With CurDataReader
         txtBadge.Text = .GetString(0)
         txtFirstName.Text = .GetValue(1)
         txtMidName.Text = .GetValue(2)
         txtLastName.Text = .GetValue(3)
         txtPassword.Text = .GetValue(4)
         Try
            cboUserType.SelectedIndex = _
             cboUserType.FindString(.GetValue(5))
         Catch
         End Try
         ckActive.Checked = .GetBoolean(6)
       End With

从上面的简单代码可以看出,它只是读取正确的字段并将其放入我们的控件中。这非常直接且易于操作。对于组合框,我们将使用 `FindString` 方法来查找正确的值并选择它。这是一个不允许列表之外的项的组合框。

`_SelectCommandToGetRecordByPK`

基类方法 `PopulateFields` 调用 `_SelectCommandToGetRecordByPK`,将 `lstLookup` 中选定的项的主键传递给它,并获取一个命令对象,该对象将用于获取该精确记录。然后,`PopulateFields` 调用 `_SetFields` 以将该记录数据传输到位于 `pnlFields` 中的相应控件。

`_InsertCommand`

如果单击了“添加”按钮,输入了数据,然后单击了“更新”按钮,则基类将调用 `InsertRecord`。`btnUpdate` 事件决定由于我们处于添加模式,它将调用 `InsertRecord`,后者使用 `_InsertCommand` 获取正确的命令对象来插入新记录。

基本上,您创建一个新的命令对象,它将从您的控件(`pnlFields` 中的那些)获取数据并将其移动到添加到命令对象的数据参数中,然后返回该命令对象。为了简化事情,由于我正在使用 MSDE(SQL Server 桌面版),我决定创建一个存储过程来处理我的插入。还有一个特殊的类可以使整个过程更简单。

      Dim pb As New clsParamBuilder(New SqlClient.SqlCommand(_
        "spAddNewOfficer"), CommandType.StoredProcedure)

      'add params
      pb.AddParam("@Badge", txtBadge.Text)
      pb.AddParam("@FirstName", txtFirstName.Text)
      pb.AddParam("@MidName", txtMidName.Text)
      pb.AddParam("@LastName", txtLastName.Text)
      pb.AddParam("@Password", txtPassword.Text)
      pb.AddParam("@UserType", cboUserType.Text)
      pb.AddParam("@Active", ckActive.Checked)

      'return the command object
      Return pb.CommandObj
您可以通过创建普通的命令对象来实现这一点,将 `spAddNewOfficer` 作为文本,将 commandtype 设置为“stored procedure”,声明一个 SQLParameter 变量,添加每个参数的信息,然后将每个参数添加到命令对象并返回它。但是,这会导致大量代码。因此,正如您在上面注意到的,我创建了一个名为 `clsParamBuilder` 的类来大大简化这一点。

该类(位于单独的代码文件中)的代码如下所示:

clsParamBuilder 的代码

Public Class clsParamBuilder
Private _cmd As IDbCommand

Public ReadOnly Property CommandObj() As IDbCommand
Get
Return _cmd
End Get
End Property

Sub New(ByVal cmd As IDbCommand)
_cmd = cmd
End Sub

Sub New(ByVal cmd As IDbCommand, ByVal CommandType As _
    System.Data.CommandType)
_cmd = cmd
_cmd.CommandType = CommandType
End Sub

Public Sub AddParam(ByVal ParamName As String, _
         ByVal ParamValue As Object, _
  Optional ByVal ParamDirection As System.data.ParameterDirection = _
  ParameterDirection.Input)
Dim p As System.Data.IDbDataParameter

'simplifies adding parameters to a command object
If TypeOf _cmd Is System.Data.SqlClient.SqlCommand Then
p = New System.Data.SqlClient.SqlParameter(ParamName, ParamValue)
ElseIf TypeOf _cmd Is System.Data.OleDb.OleDbCommand Then
p = New System.Data.oledb.OleDbParameter(ParamName, ParamValue)
End If

p.Direction = ParamDirection

_cmd.Parameters.Add(p)
End Sub
End Class
我不会花时间解释这个类,因为它的用法和功能应该根据代码和上面的示例就能明显看出来。我发现它节省了大量时间。它避免了在处理命令对象及其参数时重复冗余的代码。

`_UpdateCommand`

基类方法在未单击“添加”按钮、更改了数据然后单击“更新”按钮时调用 `UpdateRecord`。`btnUpdate` 事件决定由于我们处于更新模式,它将调用 `UpdateRecord`,后者使用 `_UpdateCommand` 获取正确的命令对象来更新当前记录。

`_UpdateCommand` 方法的工作方式与 `_InsertCommand` 相同,只是它有执行更新的代码。在我的情况下,它只是调用一个不同的存储过程,其余代码与 `_InsertCommand` 的代码相同。

现在,由于我们使用了组合框来显示用户类型,我们必须以某种方式填充它。假设这些是固定值,我们将仅仅重写 `Form_Load` 事件,并将代码放在那里来用这些固定值填充我们的组合框。**请确保在重载 Form_Load 时调用 `PopulateForm`。**

到目前为止,我们的表单应该可以运行了。正如您所见,通过继承这个大约有 835 行代码(不包括表单设计器生成和维护的代码)的基类表单,我们节省了大量的时间和编码工作。我最近一个功能齐全的继承表单只有不到 200 行代码——当然,这包括空格、注释、函数定义和 IDE 仅通过从方法下拉列表中选择方法而添加的存根。所以,是的,仍然需要一些编码,特别是如果您有组合框等控件需要限制为特定值,或者想编写验证代码,但它是相当简单的代码。实际上,我所做的是打开一个已有的继承表单,复制大部分代码并粘贴到我的新继承表单中。**请注意不要复制全部内容。当然,您不会想要复制相同的生成代码,也不会覆盖表单类名等。**

为我们的表单添加最后的专业润色

虽然我们的表单可以运行,但我们应该为它添加一些其他东西,例如验证,以使其更专业。

ClearForm

在我的表单中,我重写了 `ClearForm` 方法,它将清除所有文本框并清除我的复选框 (`ckActive`)。由于大多数人默认是激活的,我希望 `ClearForm` 执行其正常操作,所以我调用 `MyBase.ClearForm`,然后我想在默认情况下选中复选框,所以我会在调用 `MyBase.ClearForm` 之后添加一行 `ckActive.Checked=true`。就这么简单。

DataModeChanged

您可能希望重写 `DataModeChanged` 方法以:

  • 根据是添加新记录还是更新现有记录,使某些控件变为只读。
  • 根据模式更改执行一些额外的操作。

AllowRecordDeletion

在我的表单中,我还重写了 `AllowRecordDeletion` 方法,以便它接收传入的主键,检查警官的警徽(PK)是否已作为外键存在于另一个表中,如果是,则不允许删除该警官。您可能需要类似的检查才能允许删除记录。`AllowRecordDeletion` 就是执行此操作的地方。

IsValidForm

您很可能希望重写 `IsValidForm` 方法以提供表单的数据验证。让我们看看我在警官表单中是如何做的。

      Dim bValid As Boolean = True
      If Not ErrRegExpMatched(txtBadge, "[A-Z][A-Z]\d{6}", _
           "Badge format should be 2 Letters, 2-digit badge number, " & _ 
           "then last 4 SSNO of officer. example BD021234") _
             Then bValid = False
      If Not ErrRequiredValueSupplied(txtFirstName, _
             "First name is required")_
             Then bValid = False
      If Not ErrRequiredValueSupplied(txtLastName, "Last name is required")_
            Then bValid = False
      If Not ErrTextLengthInRange(txtPassword, 4, 15, _
          "Password must be between 4 and 15 characters long.") _
                Then bValid = False

      If Not bValid Then
         MessageBox.Show("There were invalid entries on this form." & _
         " It cannot be added/updated until you've fixed the problems." & _
         " There are exclamation points(!) beside each problem.", _
         "INCOMPLETE OR INVALID DATA", MessageBoxButtons.OK, _
           MessageBoxIcon.Exclamation, _
         MessageBoxDefaultButton.Button1, _
         MessageBoxOptions.DefaultDesktopOnly)
      End If
      Return bValid

从上面的代码列表(我的 `IsValidForm` 重写方法的代码),您可以看到如何使用我们添加到基类表单的验证方法。看看以这种方式进行表单验证有多简单。我们从将 `bValid` 设置为 true 开始,如果发生任何错误,我们就将其设置为 false。如果为 false,我们会告知用户某项无效,然后返回 `bValid`,以便基类知道采取何种操作。基本上,如果我们在添加或更新记录并且我们返回无效,基类将不会添加或更新记录。有问题的控件旁边将有一个小的红色感叹号图标,如果您将鼠标悬停在上面,会显示一个工具提示。

您可能非常希望在继承的表单中拥有更多功能,并且可以轻松添加它们。请记住,重写基类表单中的某些方法将需要您在重写方法中的某个时刻调用基类方法(使用 `MyBase` 关键字),以便在获得新行为的同时获得默认行为。

结论

好了,希望您喜欢这个教程,希望这个基类表单对您有用。即使您可能不使用这个基类表单,也希望您对使用 Visual Inheritance 以及其他一些概念有了一些了解。

请随意使用此代码并根据需要进行扩展。我要求您在 `BASEfrmUnboundDataForm` 和其他代码文件(*s)的顶部保留我的版权声明,并在其下方添加注释以注明您所做的任何更改。此外,此代码提供“按原样提供,不附带任何保证,对于其可能造成的任何意外损坏,我概不负责。”除这些要求外,您可以随意使用和修改此代码。它可以包含在要销售的产品中,但不得单独出售。换句话说,我提供此代码供所有人免费使用,没有人应该为此付费,但如果您制作了一个使用它的销售程序,那么只要我如上所述获得荣誉——原始设计的版权声明——这是完全可以接受的。

再次,任何评论、改进建议或错误都欢迎。

历史

  • 2004年3月 首次提交
© . All rights reserved.