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 Controls have the property ReadOnly, set it to True. Otherwise if the Control and its child Controls have the property Enabled, set it to False. Otherwise skip it without throwing an error. If the Control or any of its child Controls has a type of ContextMenu, Form, GroupBox, MenuStrip, Panel, SplitsContainer, TabControl or ToolStripMenuItem, it will not be locked but any child Controls will be. Overloads allow for including or excluding Controls 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 Controls' 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 Controls will be. Overloads allow for including or excluding Controls 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 Strings.
<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 Controls 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
Gregory Gadow lives in Seattle, Washington and has been writing code for almost 25 years in more than a dozen programming languages. He works for a mid-size brokerage firm and holds the Series 7 and Series 66 brokerage licenses, but much prefers working as the company's programming department doing VB6, VB.Net, ASP, HTML, XML and SQL.