Article Overview
This article extends System.Windows.Forms.Control
to implement a consistent way of allowing and disallowing users to edit the contents of the control. Concepts include:
- Extending a base class to extend all derived classes
- Using extensions to implement standardized behavior
- Using extensions to simplify reflection
- An example of when not to use extensions
Note for C# Programmers
When I write articles, I try to provide both VB and C# code. I have not done this here because many of these methods require modifying the calling object, and C# does not currently support reference extensions. It is possible that latter versions of C# will, and you can always convert these extensions into toolbox methods, so there might still be something useful for you here.
Introduction
This is a follow-up to my earlier article, Extend the .NET Library with Extension Methods, which provides code in both VB and C#. If you are unfamiliar with how to write and implement extension methods, you might want to read that article first. Also, note that extensions require either Visual Studio 2008 or latter, or an earlier version of Visual Studio with the .NET 3.0 Framework.
In that article, several people left comments how extensions were "syntactic sugar" and how subclassing is a much better option. Generally speaking, that is true, but not all objects can be subclassed and sometimes, subclassing is not feasible. I saw those comments as a challenge to build a library of useful extensions.
That library currently has 54 methods implementing 36 extensions. That was too many to adequately cover in a single article, so I have broken out an interesting subset that extends the System.Windows.Forms.Control
object. These methods will be included in the larger library when I get it finished and published.
Included in this Article
The provided source code offers these extension methods:
- GetPropertyValue - If the
Control
has a property of the given name, return its value as the requested type. If the property name is not found, throw ArgumentException
. If the property's value cannot be converted to the requested type, throw InvalidCastException
. - HasProperty - Return
True
if the Control
's type has a property of the given name; otherwise, return False
. - IsLocked - If the
Control
has the property ReadOnly
, return True
if that property is True
. Otherwise if the Control
has the property Enabled
, return True
if that property is False
. Otherwise return False
. - Lock - If the
Control
and its child Control
s have the property ReadOnly
, set it to True
. Otherwise if the Control
and its child Control
s have the property Enabled
, set it to False
. Otherwise skip it without throwing an error. If the Control
or any of its child Control
s has a type of ContextMenu
, Form
, GroupBox
, MenuStrip
, Panel
, SplitsContainer
, TabControl
or ToolStripMenuItem
, it will not be locked but any child Control
s will be. Overloads allow for including or excluding Control
s based on their name or type. - SetPropertyValue - If the
Control
has a property of the given name, set it to the provided value. If the property name is not found, throw ArgumentException
. Any other error throws TargetException
, with the specific error returned through the exception's InnerException
property. - Unlock - Set the
Control
's and all of its child Control
s' ReadOnly
properties to False
and their Enabled
properties to True
, if these properties are implemented. If any Control
has a type of ContextMenu
, Form
, GroupBox
, MenuStrip
, Panel
, SplitsContainer
, TabControl
or ToolStripMenuItem
, it will not be unlocked but its child Control
s will be. Overloads allow for including or excluding Control
s based on their name or type.
Why Extensions
I am currently working to convert a complicated database front-end from VB6 to VB.NET. Several of the forms require that some or all of the form's controls be locked down to prevent user input, with some fields unlocked based on other user input. In the VB6 code, this was done by manually setting either the Locked
(if it had one) or Enabled
property to an appropriate value. Several of the forms had upwards of 200 controls, so toggling them individually was tedious and maintenance was... well, not fun. I started looking for an easier way to do this.
I thought, "It would be really nice if forms had some kind of 'disable user input on all the form's controls' method." But subclassing forms can be awkward, and anyway, several of the forms in question had already been laid out; my past efforts to subclass forms at that point have not been pretty. Writing extensions to lock and unlock all of the controls on the form allowed me to add that functionality without disturbing any of my previous work.
One of the big advantages with extensions is that I can extend the functionality of objects which inherit from a base class by writing an extension to the base class itself. Consider: Extensions effectively add new methods to a class. The rules of inheritance say that methods on a class propagate to classes that inherit from it. Form
, TextBox
, Button
and the other controls used on forms eventually inherit from Control
. So by extending Control
, I am also extending Form
, TextBox
, Button
and the rest, all without having the massive headache of trying to subclass everything separately.
Reflection
The first thing I needed to work out was a definition of a locked control. A few controls have a ReadOnly
property; I decided that this would be my first choice. All controls based on System.Windows.Forms.Control
inherit the Enabled
property, making it a safe backup. The question then became when to use which property. I could have created a list of controls that implement ReadOnly
, but that could easily go out-of-date.
This sort of thing is exactly why .NET allows for reflection. The code to get and set properties by reflection is pretty simple and can be made as generic as I need. Since I was writing extensions anyway, it seemed good to write these tools as extensions as well.
<Extension()> _
Private Function HasProperty(ByVal Ctrl As Control, ByVal PropertyName As String) _
As Boolean
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
I implemented these methods as Private
because there is no good reason to expose them to the coders, and because they do not check to see whether or not the requested property is Public
and actually available. Using these methods would look like this:
If TextBox1.HasProperty("ReadOnly") Then
...
Dim IsChecked As Boolean = CheckBox4.GetPropertyValue(Of Boolean)("Checked")
...
ComboBox2.SetPropertyValue("Text", "Some text")
The syntax for GetPropertyValue
requires a bit of explanation. I wanted the method to return a strongly typed result rather than a generic Object
. This meant using the Of T
syntax, which allows me to have T
as the return type; this, in turn, means declaring the type when the method is called.
Once the method has the property value, it attempts to cast the value into the requested type. Note that the cast is not dependent on the type of the property, but on its value. Observe:
Dim Value As Integer = TextBox1.GetPropertyValue(Of Integer)("Text")
This is functionally identical to:
Dim Value As Integer = Convert.ToInt32(TextBox1.Text)
As long as TextBox1.Text
contains a value that can be cast as an integer -- say, "1234" -- then the method will return the value of Text
converted into an integer. If the property holds a non-numeric value, however, the method will throw InvalidCastException
.
How to Lock and Unlock Everything
With the ability to read and set properties on a generic Control
, I can write the extensions to do the locking and unlocking. After some poking around, I realized that if a container control is locked, all of its child controls are treated as also being locked, even when they are not. So I decided that controls with the type Form
, GroupBox
, Panel
, SplitsContainer
or TabControl
would not themselves be set using these methods. I also added MenuStrip
, ContextMenuStrip
and ToolStripMenuItem
to the list, as sub-menus are treated differently than child controls, and I am writing a separate set of extensions for menu management anyway. The call to IsValidType
is explained further down.
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)
For Each C As Control In Ctrl.Controls
C.Lock()
Next
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)
For Each C As Control In Ctrl.Controls
C.Unlock()
Next
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
Both methods first call themselves on any of the control's child controls. Then, if the calling Control
's type is not in NeverChangeLocking
, set the relevant properties on it. Now, I can do stuff like this:
Me.Lock()
TextBox1.Unlock()
CheckBox2.Unlock()
When called directly on a container such as a Form
or Panel
, Lock
and Unlock
will affect all of the child controls but not the calling control itself. When called on other controls, the control is locked. Yes, I could set the ReadOnly
and Enabled
properties directly, like this:
TextBox1.ReadOnly = False
CheckBox2.Enabled = True
But this can be confusing, in that two different properties are involved, with the control allowing user updates if one is False
and the other is True
. This serves as an example of how extensions can create cleaner, clearer code.
The method IsValidType
shows when an extension method is NOT appropriate.
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
I could have written this as an extension on Type
and it would have worked just fine. However, the definition of "valid type" is specific to this one specific context. Compare this with the IsDerivedFrom
extension:
<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
This method is applicable to any Type
, in many different contexts, which makes it a good candidate for an extension. Because IsValidType
is so specialized, it is not a good candidate for an extension.
Everything, That is, Except...
Lock
and Unlock
work fine, if you want to set a single control and all of its child controls. I found that this is typically not what I wanted, though. Because extensions can be overloaded like any other method, I wrote variations that allow the coder to pass in an enumeration of types and an operation flag. Depending on the flag, the extension will either set only the specified types, or will set everything except the specified types.
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)
For Each C As Control In Ctrl.Controls
C.Lock(Types, Operation)
Next
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
Similar changes are made to create an overload for Unlock
. Now we can, say, lock everything on a form except for buttons:
Dim ExcludeTypes As Type() = {GetType(Button)}
Me.Lock(ExcludeTypes, LockOperation.Exclude)
You will note the use of Contains
. This is an extension method provided by Microsoft in the System.Linq
namespace, which returns True
if the parameter is found in the enumeration and False
otherwise. I have included my own version in the source. If you reference System.Linq
as well as TBS.ExtendingControls.Extensions
, both extensions will be available; my version has the parameter Check
while Microsoft's uses value
. As long as the extensions are in different namespaces, there will be no conflict. Generally, though, it is bad form to create duplicate extensions, as this can lead to confusion.
One other refinement I want to add was the ability to include or exclude controls by name. This is done by writing another overload for Lock
and Unlock
that takes an enumeration of String
s.
<Extension()> _
Public Sub Unlock(ByRef Ctrl As Control, ByVal Names As IEnumerable(Of String), _
ByVal Operation As LockOperation)
For Each C As Control In Ctrl.Controls
C.Unlock(Names, Operation)
Next
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
Now, I can lock everything on the form except the Control
s named "ExitButton
" and "HelpButton
", like so:
Me.Lock(New String() {"ExitButton", "HelpButton"}, LockOperation.Exclude)
Now What?
These methods are useful as they are, but there is room for improvement. If you find any interesting ways to modify this code, I hope you will share them, either in the Comments section below or as your own article (just give me credit, please). And as always, if there are any bugs in the article or accompanying code, let me know and I will get them corrected.
History
- 30th June, 2009: Clarified title to indicate
Form.Control
, minor fixes to the article text and code - 23rd June, 2009: Initial post