Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / VB10

Extending Forms.Control: Lock and Unlock

4.68/5 (12 votes)
30 Jun 2009CPOL9 min read 73.6K   1.3K  
Learn how to extend the Forms.Control object to add locking and unlocking capability

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.

VB.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

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:

VB.NET
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:

VB.NET
Dim Value As Integer = TextBox1.GetPropertyValue(Of Integer)("Text")

This is functionally identical to:

VB.NET
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.

VB.NET
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

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:

VB.NET
Me.Lock() 'Lock all controls on a form
'But unlock these controls
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:

VB.NET
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.

VB.NET
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:

VB.NET
<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.

VB.NET
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

Similar changes are made to create an overload for Unlock. Now we can, say, lock everything on a form except for buttons:

VB.NET
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.

VB.NET
<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

Now, I can lock everything on the form except the Controls named "ExitButton" and "HelpButton", like so:

VB.NET
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)