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

在 Access 窗体中管理业务逻辑

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2012年2月6日

CPOL

7分钟阅读

viewsIcon

32575

downloadIcon

564

当业务逻辑变得复杂时,避免事件代码混乱。

引言:Access 中糟糕的数据绑定

我猜所有基本输入控件最重要的三个属性是值属性、启用属性和可见属性。这些属性通常响应它们所代表的业务逻辑。例如,在添加了购买商品后,文本框将更新其值属性以显示订单的重新计算后的总金额;当用户单击单选按钮选择银行转账支付时,信用卡号文本框将被禁用或隐藏。

幸运的是,您所使用的技术允许您将控件的属性值直接链接到业务逻辑,这在 WPF 中是可能的。可悲的是,如果您仍然使用需要您在代码隐藏中手动完成所有事情的技术,就像在 Microsoft Access 中一样。

我既幸运又可悲,因为我“享受”这两者。我从 Access-VBA 起步,转向 WinForms,后来转向 WPF,几周前,当我荣幸地参与一个包含几十个基本数据录入控件的窗体项目时,我回忆起往事:其中大部分是复选框、文本框和组合框。背后的业务逻辑要求几乎所有的用户操作都会影响其他控件。一个被设置为 false,另一个被启用,再下一个变为不可见。考虑到所涉及的控件数量庞大,以及领域专家本身仍处于实验阶段的事实,我预见到一个维护的噩梦,表现形式如下:

Private Sub Check0_Click()
If Me.Check0.Value = True Then
    If Not Me.Check2 Is Null Then
        Me.Check2.Value = False
    End If
    If Not Me.Check4 Is Null Then
        Me.Check4.Value = False
    End If
    If Not Me.Check6 Is Null Then
        Me.Check6.Value = False
    End If
    ' To be continued...
End If 
End Sub

SetControl 函数

由于无法在 Access 中添加高级数据绑定,我尝试了次优选择:创建一组可重用的函数,尽可能灵活地设置控件的属性值。通过这样做,上面的代码会变成这样:

SetControlsValue Me.Check0, False, True, False, Me.Check2, Me.Check4, Me.Check6

总共有三个函数

  • SetControlsValue
  • 用于设置任意数量控件的值属性

  • SetControlsEnabled
  • 用于更改任意数量控件的启用状态

  • SetControlsVisible
  • 用于更改任意数量控件的可见状态

SetControlsValue 函数详解

通过修改上述函数调用中的一些参数,让我们来看一下 `SetControlsValue` 函数的一些更多变体。

SetControlsValue Nothing, False, True, False, Me.Check2, Me.Check4, Me.Check6

第一个参数代表必须是函数执行的活动控件。如果您希望函数在被调用时始终执行,请将第一个参数设置为 `Nothing`。

SetControlsValue Me.Check0, Nz(Me.Check0,False), True, False, Me.Check2, Me.Check4, Me.Check6

第二个参数代表要设置的控件的属性值。您可以引用另一个控件的值,或者使用像 `Iif`、`Nz` 等内联表达式而不是固定值。

SetControlsValue Me.Check0, False, Nothing, False, Me.Check2, Me.Check4, Me.Check6

第三个参数充当过滤器。在上面的第一个示例(1)中,函数仅在“`Me.Check0.Value = True`”为真时执行。如果您希望函数始终执行,请将第三个参数设置为 `Nothing`。请注意,此过滤器仅在函数调用绑定到第一个参数定义的特定控件时才有效。这意味着在函数调用(2)中,条件过滤器被忽略了。

SetControlsValue Me.Check0, False, True, Nothing, Me.Check2, Me.Check4, Me.Check6

第四个参数是另一个过滤器。这次它适用于值属性可能会被更改的控件。如果设置为 `Nothing`,控件的属性值将始终更改。

所有其他参数代表可能被更改值属性的控件。函数调用至少需要一个控件,并接受任意数量的控件。

下面您可以看到 `SetControlsValue` 函数的详细代码。我添加了一些代码注释以帮助您更好地理解正在发生的事情。

'Sets value property of multiple controls
'focusedControl     : If not nothing, this control must be the active control
'                     for the function to execute. In modal forms the function is always executed.
'newValue           : New value of the property
'focusedControlCriteriaValue  : If not nothing, only if the focused control's value
'                     corresponds to this value the function will execute
'controlToHandleCriteriaValue : If not nothing, only those controls whose value
'               correspond to the criteria value will get the new value
'controlsToHandle   : Controls whose value property may be changed.
Public Sub SetControlsValue(focusedControl As Control, newValue As Variant, _
       focusedControlCriteriaValue As Variant, controlToHandleCriteriaValue As Variant, _
       ParamArray controlsToHandle() As Variant)
    
    Dim i As Integer
    
    If Not focusedControl Is Nothing Then
        ' Workaround: in modal forms the Screen.ActiveControl remains the control on the calling form
        If focusedControl.Parent Is Screen.ActiveControl.Parent Then
            If Not Screen.ActiveControl Is focusedControl Then
                Exit Sub
            End If
        End If
        ' Apply focusedControl filter
        If Not Nz(focusedControl.Value, "Null") = _
               Nz(Nn(focusedControlCriteriaValue, focusedControl.Value), "Null") Then
            Exit Sub
        End If
    End If
           
    On Error Resume Next
        For i = 0 To UBound(controlsToHandle)
            ' Do not apply new value to the control which executed the function
            If Not controlsToHandle(i) Is focusedControl Then
                If Nz(controlsToHandle(i).Value, True) = _
                        Nz(Nn(controlToHandleCriteriaValue, controlsToHandle(i).Value), True) Then
                    controlsToHandle(i).Value = newValue
                    If Err.Number <> 0 Then
                        MsgBox ("Error when trying to set the value ' " & newValue & "' for control '" & _
                                controlsToHandle(i).Name & "'." & _
                                vbNewLine & vbNewLine & "Original error message:" & _
                                vbNewLine & Err.Description)
                    End If
                End If
            End If
        Next
    On Error GoTo 0
   
End Sub

'Similar to Nz, except that a value of Nothing (instead of Null) will be replaced
Public Function Nn(Value As Variant, valueIfNothing As Variant) As Variant
    On Error GoTo ErrorHandler
        If Value Is Nothing Then
            Nn = valueIfNothing
        Else
            Nn = Value
        End If
    On Error GoTo 0
    
    Exit Function
    
ErrorHandler:
    Nn = Value
End Function

在实现这些函数时,让用户输入可选参数值进行过滤有些棘手,因为可选修饰符不能与 `paramArray`(用于定义无限数量的控件进行设置的签名)结合使用。受 .NET 的启发,我选择了“`Nothing`”来表示不应用任何过滤器。不幸的是,在 VBA 中测试作为变体传递的过滤器是否为“`Nothing`”并不容易,因为您只能测试对象是否为“`Is Nothing`”;尝试在变体封装整数等原始数据类型时进行测试会引发错误。我也找不到一种方法来检查变体是保存对象还是原始数据类型。这就是为什么我不得不在一个地方使用“`On Error`”。如果您知道更好的方法,请告诉我!

附加控件事件时避免程序过多

我知道使用属性窗口中的设计器通过双击来生成事件很方便,看起来像这样:

Private Sub txt0_Click()
End Sub

不幸的是,这样您的代码隐藏中会充斥大量过程,这会导致代码碎片化、可重复且难以维护。只要有可能,就尝试创建一个中央过程来捕获所有相关的事件。首先通过在代码隐藏中创建一个函数来实现这一点。然后返回到设计器,并将此函数附加到打算使用它的控件。您可以通过导航到控件的属性表中的事件来实现此目的。但不要双击并生成与该控件相关的事件,而是将函数名输入到所选事件的行中,如下所示:

=ApplyRules_SetValueDifferentControls()

`"ApplyRules_SetValueDifferentControls"` 是代码隐藏中一个函数的名称。如果事件包含参数,则函数需要在其签名中声明相同的参数,并且您还需要在属性表中的字符串中定义参数。VBA 稍后会进行正确的数据类型转换。当然,字符串意味着参数是静态的。

但是,如果您在代码隐藏中将函数附加到控件事件而不是使用设计器,就可以解决这个问题。这样您就可以动态构建字符串,并为每个控件分配一个单独的事件字符串。

如果您需要处理许多控件,则动态地在代码隐藏中将事件处理程序附加到控件是我首选的方法。为了识别要将事件附加到的控件,您可以使用任何控件都可用的 `Tag` 属性。有时您还想将同一个事件函数分配给不同事件,具体取决于控件的类型。您可以通过区分控件类型来实现这一点。

下面是从演示项目中提取的代码片段,您可以看到动态附加控件事件是什么样的:

For Each ctl In Me.Controls
    If ctl.Tag = "InputControl" Then
        If TypeOf ctl Is CheckBox Then
            ctl.OnClick = "=ApplyRules_SetCheckBoxValues()"
        ElseIf TypeOf ctl Is TextBox Then
            ctl.OnKeyUp = "=ApplyRules_CopyText("CStr(ctl.TabIndex) & ", -1)"
        End If
    End If
Next

这样,您就避免了每个控件事件一个过程定义,并促进了处理一个或多个控件的集中式过程的使用。这使得您的代码更紧凑,更易于维护。

另一个有助于您生成更通用事件处理的有用功能是:

Screen.ActiveControl

这使得函数调用对触发事件的控件敏感,如下所示(代码片段摘自演示项目):

SetControlsValue Screen.ActiveControl, Screen.ActiveControl.Text, Nothing, Nothing, Me.txt0, 
Me.txt1, Me.txt2, Me.txt3, Me.txt4

总而言之,将这些技术与 `SetControls` 函数结合应用,可以显著减少您为事件处理编写的代码量。

演示项目

我现在建议您查看演示项目,以更好地了解它是如何在实际中工作的。

325018/DemoProject.jpg

演示项目展示了所有三个函数的操作,其中函数本身位于唯一的一个模块中。这与您已经获得的信息相结合,将帮助您非常快速地入门。如果您有其他控件属性的值经常更改,还可以进一步扩展函数集合。

当然,您永远无法完全摆脱事件处理代码中的“If”和其他流程控制结构,但应用这些技术将有助于您显著减少它们,并帮助您的代码随着时间的推移更易于维护。

更新

当在模态窗体(用户打开时不允许切换到另一个窗体的窗体)中使用 `SetControl` 函数时,焦点控件仍然停留在打开模态窗体的窗体上。在这种情况下,在 `SetControl` 函数中使用 `Screen.ActiveControl` 将无法按上述方式工作。我通过在每个函数中添加一个检查来解决此问题,该检查会查看 `Screen.ActiveControl` 是否与 `focusedControl` 位于同一窗体上。在模态窗体中,情况并非如此,并且作为参数定义的 `focusedControl` 将被忽略。

© . All rights reserved.