扩展 Forms.Control:锁定和解锁






4.68/5 (11投票s)
了解如何扩展 Forms.Control 对象以添加锁定和解锁功能
文章概述
本文扩展了 System.Windows.Forms.Control
,以实现一种允许和禁止用户编辑控件内容的一致方法。概念包括:
- 扩展基类以扩展所有派生类
- 使用扩展来实现标准化行为
- 使用扩展来简化反射
- 不使用扩展的示例
致 C# 程序员的说明
当我撰写文章时,我尝试同时提供 VB 和 C# 代码。在这里我没有这样做,因为许多这些方法需要修改调用对象,而 C# 目前不支持引用扩展。未来的 C# 版本可能会支持,并且您始终可以将这些扩展转换为工具箱方法,因此这里可能仍然有一些对您有用的东西。
引言
这是我早期文章《使用扩展方法扩展 .NET 库》的后续,该文章提供了 VB 和 C# 两种代码。如果您不熟悉如何编写和实现扩展方法,您可能需要先阅读那篇文章。另外请注意,扩展需要 Visual Studio 2008 或更高版本,或者安装了 .NET 3.0 框架的早期 Visual Studio 版本。
在那篇文章中,有些人评论说扩展是“语法糖”,而子类化是更好的选择。一般来说,这是真的,但并非所有对象都可以子类化,有时,子类化是不可行的。我将这些评论视为构建有用扩展库的挑战。
该库目前有 54 个方法,实现了 36 个扩展。这对于一篇文章来说太多了,所以我从中提取了一个有趣的子集,它扩展了 System.Windows.Forms.Control
对象。这些方法将在我完成并发布更大的库时包含在内。
本文包含的内容
提供的源代码提供了以下扩展方法:
- GetPropertyValue - 如果
Control
具有给定名称的属性,则将其值作为请求的类型返回。如果未找到属性名称,则抛出ArgumentException
。如果属性值无法转换为请求的类型,则抛出InvalidCastException
。 - HasProperty - 如果
Control
的类型具有给定名称的属性,则返回True
;否则,返回False
。 - IsLocked - 如果
Control
具有属性ReadOnly
,则当该属性为True
时返回True
。否则,如果Control
具有属性Enabled
,则当该属性为False
时返回True
。否则返回False
。 - Lock - 如果
Control
及其子Control
具有属性ReadOnly
,则将其设置为True
。否则,如果Control
及其子Control
具有属性Enabled
,则将其设置为False
。否则跳过而不抛出错误。如果Control
或其任何子Control
的类型为ContextMenu
、Form
、GroupBox
、MenuStrip
、Panel
、SplitsContainer
、TabControl
或ToolStripMenuItem
,则它不会被锁定,但其任何子Control
将被锁定。重载允许根据名称或类型包含或排除Control
。 - SetPropertyValue - 如果
Control
具有给定名称的属性,则将其设置为提供的值。如果未找到属性名称,则抛出ArgumentException
。任何其他错误都会抛出TargetException
,并通过异常的InnerException
属性返回特定错误。 - Unlock - 如果这些属性已实现,则将
Control
及其所有子Control
的ReadOnly
属性设置为False
,并将其Enabled
属性设置为True
。如果任何Control
的类型为ContextMenu
、Form
、GroupBox
、MenuStrip
、Panel
、SplitsContainer
、TabControl
或ToolStripMenuItem
,则它不会被解锁,但其子Control
将被解锁。重载允许根据名称或类型包含或排除Control
。
为什么使用扩展
我目前正在将一个复杂的数据库前端从 VB6 转换为 VB.NET。其中一些表单要求锁定表单的部分或全部控件以防止用户输入,而某些字段则根据其他用户输入解锁。在 VB6 代码中,这是通过手动将 Locked
(如果有)或 Enabled
属性设置为适当的值来完成的。其中一些表单有 200 多个控件,因此单独切换它们既繁琐又难以维护……嗯,不好玩。我开始寻找一种更简单的方法来做到这一点。
我想,“如果表单有一些‘禁用表单所有控件用户输入’的方法,那该多好。”但是子类化表单可能很笨拙,而且,一些相关的表单已经布局好了;我过去在这方面子类化表单的尝试并不美观。编写扩展来锁定和解锁表单上的所有控件,让我可以在不干扰我之前任何工作的情况下添加该功能。
扩展的一大优势是,我可以通过为基类本身编写扩展来扩展继承自基类的对象的功能。请考虑:扩展有效地为类添加了新方法。继承规则规定,类上的方法会传播到继承自它的类。Form
、TextBox
、Button
和表单上使用的其他控件最终都继承自 Control
。因此,通过扩展 Control
,我也扩展了 Form
、TextBox
、Button
和其他所有控件,所有这些都无需单独子类化所有内容所带来的巨大麻烦。
Reflection(反射)
我首先需要弄清楚的是锁定控件的定义。一些控件具有 ReadOnly
属性;我决定这将是我的首选。所有基于 System.Windows.Forms.Control
的控件都继承了 Enabled
属性,使其成为一个安全的备用选项。问题是如何选择使用哪个属性。我可以创建一个实现 ReadOnly
的控件列表,但这很容易过时。
这种事情正是 .NET 允许反射的原因。通过反射获取和设置属性的代码非常简单,并且可以根据我的需要进行泛化。由于我无论如何都要编写扩展,因此将这些工具也编写为扩展似乎是个好主意。
<Extension()> _
Private Function HasProperty(ByVal Ctrl As Control, ByVal PropertyName As String) _
As Boolean
'If Nothing is returned, then the property is not implemented.
Return Not (Ctrl.GetType.GetProperty(PropertyName) Is Nothing)
End Function
<Extension()> _
Private Function GetPropertyValue(Of T)(ByVal Ctrl As Control, _
ByVal PropertyName As String) As T
If Ctrl.HasProperty(PropertyName) Then
Dim Obj As Object = _
Ctrl.GetType.GetProperty(PropertyName).GetValue(Ctrl, Nothing)
Try
Return CType(Obj, T)
Catch ex As Exception
Throw New InvalidCastException("Property " + PropertyName + _
" has type " + Obj.GetType.Name + ", which cannot be converted to " + _
GetType(T).Name + ".", ex)
End Try
Else
Throw New ArgumentException("Cannot find property " + PropertyName + ".")
End If
End Function
<Extension()> _
Private Sub SetPropertyValue(ByRef Ctrl As Control, ByVal PropertyName As String, _
ByVal value As Object)
If Ctrl.HasProperty(PropertyName) Then
Try
Ctrl.GetType.GetProperty(PropertyName).SetValue(Ctrl, value, Nothing)
Catch ex As Exception
Throw New TargetException("There was an error setting this property. " + _
"See InnerException for details.", ex)
End Try
Else
Throw New ArgumentException("Cannot find property " + PropertyName + ".")
End If
End Sub
我将这些方法实现为 Private
,因为没有充分的理由将它们暴露给编码人员,而且它们不检查请求的属性是否为 Public
并且实际可用。使用这些方法看起来像这样:
If TextBox1.HasProperty("ReadOnly") Then
...
Dim IsChecked As Boolean = CheckBox4.GetPropertyValue(Of Boolean)("Checked")
...
ComboBox2.SetPropertyValue("Text", "Some text")
GetPropertyValue
的语法需要一些解释。我希望该方法返回一个强类型结果,而不是一个泛型 Object
。这意味着使用 Of T
语法,它允许我将 T
作为返回类型;这反过来意味着在调用方法时声明类型。
一旦方法获得属性值,它会尝试将该值转换为请求的类型。请注意,转换不依赖于属性的*类型*,而是依赖于其*值*。观察:
Dim Value As Integer = TextBox1.GetPropertyValue(Of Integer)("Text")
这在功能上与以下内容相同:
Dim Value As Integer = Convert.ToInt32(TextBox1.Text)
只要 `TextBox1.Text` 包含可以转换为整数的值(例如,“1234”),那么该方法将返回转换为整数的 `Text` 值。但是,如果属性包含非数字值,则该方法将抛出 `InvalidCastException`。
如何锁定和解锁所有内容
有了在泛型 Control
上读取和设置属性的能力,我就可以编写用于锁定和解锁的扩展。经过一番摸索,我意识到如果一个容器控件被锁定,它的所有子控件也会被视为被锁定,即使它们没有被锁定。所以我决定类型为 Form
、GroupBox
、Panel
、SplitsContainer
或 TabControl
的控件本身不会使用这些方法进行设置。我还将 MenuStrip
、ContextMenuStrip
和 ToolStripMenuItem
添加到列表中,因为子菜单与子控件的处理方式不同,而且我正在为菜单管理编写一组单独的扩展。对 IsValidType
的调用在下面进一步解释。
Private NeverChangeLocking As Type() = {GetType(ContextMenu), GetType(Form), _
GetType(GroupBox), GetType(MenuStrip), GetType(Panel), GetType(SplitContainer), _
GetType(TabControl), GetType(ToolStripMenuItem)}
<Extension()> _
Public Sub Lock(ByRef Ctrl As Control)
'Lock any constituent controls recursively
For Each C As Control In Ctrl.Controls
C.Lock()
Next
'Now lock the referenced control
If IsValidType(Ctrl.GetType, NeverChangeLocking) Then
If Ctrl.HasProperty("ReadOnly") Then
Ctrl.SetPropertyValue("ReadOnly", True)
ElseIf Ctrl.HasProperty("Enabled") Then
Ctrl.SetPropertyValue("Enabled", False)
End If
End If
End Sub
<Extension()> _
Public Sub Unlock(ByRef Ctrl As Control)
'Unlock any constituent controls recursively
For Each C As Control In Ctrl.Controls
C.Unlock()
Next
'Now unlock the referenced control. Note that both
'ReadOnly and Enabled must be set to insure that the
'control really is unlocked.
If IsValidType(Ctrl.GetType, NeverChangeLocking) Then
If Ctrl.HasProperty("ReadOnly") Then Ctrl.SetPropertyValue("ReadOnly", False)
If Ctrl.HasProperty("Enabled") Then Ctrl.SetPropertyValue("Enabled", True)
End If
End Sub
这两个方法都首先在控件的任何子控件上调用自身。然后,如果调用 Control
的类型不在 NeverChangeLocking
中,则在其上设置相关属性。现在,我可以做这样的事情:
Me.Lock() 'Lock all controls on a form
'But unlock these controls
TextBox1.Unlock()
CheckBox2.Unlock()
当直接在像 Form
或 Panel
这样的容器上调用时,Lock
和 Unlock
会影响所有子控件,但不会影响调用控件本身。当在其他控件上调用时,控件会被锁定。是的,我可以像这样直接设置 ReadOnly
和 Enabled
属性:
TextBox1.ReadOnly = False
CheckBox2.Enabled = True
但这可能会让人困惑,因为涉及到两个不同的属性,如果其中一个为 False
而另一个为 True
,则控件允许用户更新。这说明了扩展如何创建更简洁、更清晰的代码。
方法 IsValidType
展示了何时不适合使用扩展方法。
Friend Function IsValidType(ByVal Test As Type, _
ByVal InvalidTypes As IEnumerable(Of Type)) As Boolean
Dim Result As Boolean = True
For Each T As Type In InvalidTypes
If Test.IsDerivedFrom(T) Then
Result = False
Exit For
End If
Next
Return Result
End Function
我本可以将其编写为 Type
上的扩展,并且它会工作得很好。然而,“有效类型”的定义仅限于这个特定的上下文。将其与 IsDerivedFrom
扩展进行比较:
<Extension()> _
Public Function IsDerivedFrom(ByVal T As Type, ByVal Test As Type) As Boolean
Dim Ty As Type = T
Dim Result As Boolean = False
Do While Ty IsNot GetType(Object)
If Ty Is Test Then
Result = True
Exit Do
End If
Ty = Ty.BaseType
Loop
Return Result
End Function
此方法适用于任何 Type
,在许多不同的上下文中,这使其成为扩展的好选择。因为 IsValidType
是如此专业化,所以它不是扩展的好选择。
所有东西,除了...
如果您想设置单个控件及其所有子控件,Lock
和 Unlock
都能正常工作。不过,我发现这通常*不是*我想要的。由于扩展可以像其他任何方法一样重载,我编写了变体,允许编码人员传入类型枚举和操作标志。根据标志,扩展将*仅*设置指定的类型,或者*除了*指定的类型之外,设置所有其他内容。
Public Enum LockOperation
Exclude
Include
End Enum
<Extension()> _
Public Sub Lock(ByRef Ctrl As Control, ByVal Types As IEnumerable(Of Type), _
ByVal Operation As LockOperation)
'Lock any constituent controls recursively
For Each C As Control In Ctrl.Controls
C.Lock(Types, Operation)
Next
'Now lock the referenced control
If IsValidType(Ctrl.GetType, NeverChangeLocking) Then
If (Operation = LockOperation.Exclude _
AndAlso Not Types.Contains(Ctrl.GetType)) _
OrElse (Operation = LockOperation.Include _
AndAlso Types.Contains(Ctrl.GetType)) Then
If Ctrl.HasProperty("ReadOnly") Then
Ctrl.SetPropertyValue("ReadOnly", True)
ElseIf Ctrl.HasProperty("Enabled") Then
Ctrl.SetPropertyValue("Enabled", False)
End If
End If
End If
End Sub
类似的变化也用于为 Unlock
创建重载。现在我们可以,比如说,锁定表单上的所有内容,除了按钮:
Dim ExcludeTypes As Type() = {GetType(Button)}
Me.Lock(ExcludeTypes, LockOperation.Exclude)
您会注意到 Contains
的使用。这是 Microsoft 在 System.Linq
命名空间中提供的扩展方法,如果参数在枚举中找到,则返回 True
,否则返回 False
。我在源代码中包含了我的版本。如果您同时引用 System.Linq
和 TBS.ExtendingControls.Extensions
,则两个扩展都将可用;我的版本具有参数 Check
,而 Microsoft 的版本使用 value
。只要扩展在不同的命名空间中,就不会发生冲突。然而,通常来说,创建重复的扩展是不好的做法,因为这可能导致混淆。
我想添加的另一个改进是根据名称包含或排除控件的能力。这是通过为 Lock
和 Unlock
编写另一个重载来实现的,该重载接受 String
的枚举。
<Extension()> _
Public Sub Unlock(ByRef Ctrl As Control, ByVal Names As IEnumerable(Of String), _
ByVal Operation As LockOperation)
'Unlock any constituent controls recursively
For Each C As Control In Ctrl.Controls
C.Unlock(Names, Operation)
Next
'Now unlock the referenced control. Note that both
'ReadOnly and Enabled must be set to insure that the
'control really is unlocked.
If IsValidType(Ctrl.GetType, NeverChangeLocking) Then
If (Operation = LockOperation.Exclude _
AndAlso Not Names.Contains(Ctrl.Name)) _
OrElse (Operation = LockOperation.Include _
AndAlso Names.Contains(Ctrl.Name)) Then
If Ctrl.HasProperty("ReadOnly") Then _
Ctrl.SetPropertyValue("ReadOnly", False)
End If
If Ctrl.HasProperty("Enabled") Then _
Ctrl.SetPropertyValue("Enabled", True)
End If
End If
End If
End Sub
现在,我可以锁定表单上的所有控件,除了名为“ExitButton
”和“HelpButton
”的控件,如下所示:
Me.Lock(New String() {"ExitButton", "HelpButton"}, LockOperation.Exclude)
接下来呢?
这些方法本身很有用,但仍有改进空间。如果您发现任何修改此代码的有趣方法,我希望您能分享它们,无论是在下面的评论区还是作为您自己的文章(请务必注明出处)。一如既往,如果文章或附带代码有任何错误,请告诉我,我会尽快纠正。
历史
- 2009年6月30日:澄清标题以指明
Form.Control
,对文章文本和代码进行了小幅修正 - 2009 年 6 月 23 日:首次发布