在 Access 窗体中管理业务逻辑





5.00/5 (4投票s)
当业务逻辑变得复杂时,避免事件代码混乱。
引言: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` 函数结合应用,可以显著减少您为事件处理编写的代码量。
演示项目
我现在建议您查看演示项目,以更好地了解它是如何在实际中工作的。
演示项目展示了所有三个函数的操作,其中函数本身位于唯一的一个模块中。这与您已经获得的信息相结合,将帮助您非常快速地入门。如果您有其他控件属性的值经常更改,还可以进一步扩展函数集合。
当然,您永远无法完全摆脱事件处理代码中的“If”和其他流程控制结构,但应用这些技术将有助于您显著减少它们,并帮助您的代码随着时间的推移更易于维护。
更新
当在模态窗体(用户打开时不允许切换到另一个窗体的窗体)中使用 `SetControl` 函数时,焦点控件仍然停留在打开模态窗体的窗体上。在这种情况下,在 `SetControl` 函数中使用 `Screen.ActiveControl` 将无法按上述方式工作。我通过在每个函数中添加一个检查来解决此问题,该检查会查看 `Screen.ActiveControl` 是否与 `focusedControl` 位于同一窗体上。在模态窗体中,情况并非如此,并且作为参数定义的 `focusedControl` 将被忽略。