DekGenius.com
[ Team LiB ] Previous Section Next Section

6.3 Inheriting from Other Controls

Because inheritance is central to the way controls work in the Windows Forms framework, we are not limited to deriving from forms or composite controls. In principle, we can use any control as a base class (unless it has marked itself as sealed in C# or NonInheritable in VB, but very few controls do this). We must live without the convenience of visual editing when deriving from other control types, but of course that is also the case when we write custom controls.

The usual reason for inheriting from some non-composite control (e.g., one of the built-in controls) is to provide a version of that control with some useful extra feature. For example, we will see later how to add an autocompletion facility to the TextBox control. As always, it is crucially important only to use inheritance where it makes sense—developers must be able to use our derived control just as they would use the base control.

Visual Studio .NET does not provide a direct way of creating a new derived control with a non-composite base class. However, it is reasonably easy to get started: simply add a new Custom Control to the project, delete the OnPaint method it supplies, and change the base class from Control to whichever control class you want to derive from. Alternatively, just create a new normal class definition from scratch that inherits from the base class of your choice. (These two techniques are exactly equivalent. Although it is more straightforward just to create a normal class, using the Custom Control template with C# provides you with a useful set of using declarations at the top of the file.)

Thanks to the wonders of inheritance, your derived control will now be capable of doing everything that the base control can do. All that remains is to add whatever extra functionality you require. There are two ways in which controls are typically extended. The first involves modifying its programming interface by adding or overriding methods or properties. The second involves modifying the behavior of the control itself. A deriving control can use either or both of these modes of extension. We will now look at each in turn.

6.3.1 Extending the Programming Interface

Sometimes we will want to use a built-in control, but to make some change to its programming interface. We can do this without changing its visual behavior—as far as the user is concerned, the control will appear to be just another ListView or Label. We are modifying the control purely to make things more convenient for the developer. This might be as simple as adding some helper methods to populate a ListView with a collection of some application-specific data type. Such methods are straightforward: the code usually looks exactly the same as it would in a non-inheritance situation; it just happens to have been bolted onto an existing class using inheritance. The more challenging and interesting changes are those that extend the behavior of existing methods.

Modifying the behavior of existing methods can be particularly useful in a multithreaded program. Remember the golden rule for threads in Windows Forms introduced in Chapter 3: you should only use a control from the thread on which it was created; the only exception is you can use the Invoke, BeginInvoke, and EndInvoke methods, and the InvokeRequired property. While this is a simple rule, it is often tedious to comply with—marshaling all calls through Invoke or BeginInvoke adds unwanted complexity, and introduces scope for programming errors.

So if you have a worker thread that needs to update the user interface on a regular basis (e.g., updating a text field to keep the user informed of the thread's progress), it might be worth creating a control that has slightly more relaxed threading constraints. So we will now look at how to build a control derived from Label that allows its Text property to be accessed safely from any thread.

Remember that the correct way to use a control from a worker thread is to direct all calls through either Invoke or BeginInvoke. These will arrange for the call to occur on the correct thread. They both need to know which method you would like to invoke, so they take a delegate as a parameter. None of the delegates defined by the framework quite meet our needs, so our multithreaded[3] label class starts with a couple of private delegate type definitions. In C#, the code is as follows:

[3] Strictly speaking, it's not fully thread-safe—we are only enabling the Text property for multithreaded use to keep things simple.

public class LabelMT : System.Windows.Forms.Label
{
    private delegate string GetTextDelegate();
    private delegate void SetTextDelegate(string s);

The equivalent VB code is:

Public Class LabelMT
    Inherits System.Windows.Forms.Label

    Private Delegate Function GetTextDelegate() As String
    Private Delegate Sub SetTextDelegate(ByVal s As String)

The first delegate will be used when setting the text, and the second will be used when retrieving it. Next, we override the Text property itself to make it safe for use in a multithreaded environment. The C# code that does this is:

public override string Text
{
    get
    {
        if (DesignMode || !InvokeRequired)
        {
  return base.Text;
        }
        else
        {
  return (string) Invoke(new GetTextDelegate(DoGetText));
        }
    }
    set
    {
        if (DesignMode || !InvokeRequired)
        {
  base.Text = value;
        }
        else
        {
  object[] args = { value };
  BeginInvoke(new SetTextDelegate(DoSetText),
      args);
        }
    }
}

The VB code to override the Text property is:

Public Overrides Property Text() As String
   Get
      If DesignMode OrElse Not InvokeRequired Then
         Return MyBase.Text
      Else
         Return DirectCast(Invoke(New GetTextDelegate( _
                 AddressOf DoGetText)), String)
      End If
   End Get
   Set(ByVal Value As String)
      If DesignMode OrElse Not InvokeRequired Then
         MyBase.Text = Value
      Else
         Dim args() As Object = {Value}
         BeginInvoke(New SetTextDelegate(AddressOf DoSetText), _
           args)
      End If
   End Set
End Property

Note that both of these start by checking to see if they actually need to marshal the call to another thread. If the property is being used from the correct thread, we just defer directly to the base class's implementation, avoiding the overhead of a call through Invoke. (And in the case of the property set method, we use BeginInvoke—this doesn't wait for the UI thread to complete the call, and just returns immediately. If you don't need to wait for a return value, it is usually better to use BeginInvoke instead of Invoke—it returns more quickly, and it can reduce the chance of accidentally freezing your application by causing a deadlock.)

The InvokeRequired property tells us whether we are already on the UI thread. You may be wondering why this code also tests the DesignMode flag. The reason is that when our control is in design mode (i.e., it is being displayed in the Forms Editor in Visual Studio .NET), certain things don't work in quite the same way as they do at runtime. Controls are initialized differently in design mode, so some features are unavailable. One of these is the Invoke mechanism—any attempt to use it will cause an error. Unfortunately, InvokeRequired is always true in design mode, despite the fact that it is not actually possible to use Invoke. So if we are in design mode, we just ignore the InvokeRequired property and always call the base class's implementation. (It is safe to assume that the Forms Designer will never access our controls on the wrong thread, so it will always be safe to ignore InvokeRequired here.)

If we are not in design mode (i.e., the control is running normally, not inside the Designer) but InvokeRequired indicates that we are on the wrong thread, we use the Invoke method to marshal the call to the correct thread. We pass it a delegate wrapped around either the DoGetText or the DoSetText method, which will then be called by the framework on the UI thread. These methods simply call the base class implementation. Their C# code is:

private string DoGetText()
{
    Debug.Assert(!InvokeRequired);
    return base.Text;
}

private void DoSetText(string s)
{
    Debug.Assert(!InvokeRequired);
    base.Text = s;
}

Their equivalent VB code is:

Private Function DoGetText() As String
    Debug.Assert(Not InvokeRequired)
    Return MyBase.Text
End Function

Private Sub DoSetText(ByVal s As String)
    Debug.Assert(Not InvokeRequired)
    MyBase.Text = s
End Sub

These methods are only ever called via the Invoke or BeginInvoke method, which means that they will always run on the UI thread.[4] This means that they can simply access the property directly using the base class's implementation.

[4] In this example, this assumption has been encoded in a Debug.Assert call—these assertions will fail on debug builds if any developer later modifies this control and tries to call these methods on the wrong thread. As with most assertion mechanisms, these aren't compiled into release builds.

If you are used to writing multithreaded code, you might be surprised at the absence of locks or critical sections. Most thread-safe code protects itself by synchronizing access to shared data using locking primitives supplied by the system (such as critical sections in Win32). The CLR provides these kinds of facilities, and both VB and C# have intrinsic support for them with their lock and SyncLock keywords, but they turn out to be unnecessary here. Concurrency is never an issue because we make sure that all work is done on the UI thread, and a thread can only do one piece of work at a time.


Threading issues aside, this is an example of a common pattern: overriding a feature of the base class but calling back to the base class's original implementation to do the bulk of the work. The code in the derived class simply adds some value on top of the original code (correct operation in a multithreaded environment in this case). But we will now look at the other way of extending a control—modifying the behavior seen by the user.

6.3.2 Extending Behavior

The second way of extending a control is to modify the behavior that the end user sees. This typically means changing the way the control reacts to user input, or altering its appearance. It always involves overriding the internal event handling methods (i.e., the OnXxx methods, such as OnPaint or OnMouseDown) to change the way the control behaves.

Examples Example 6-7 and Example 6-8 show a class, AutoTextBox, that derives from the built-in TextBox control. It augments the basic TextBox by adding a simple autocomplete functionality. This is a common feature of many text fields in Windows applications—the control has some list of potential values for the field, and if the text that the user has typed in so far matches any of those values, it prompts the user with them. This is a very useful enhancement. It can save a lot of typing and is becoming increasingly widely adopted. In Windows XP, most text fields that accept filenames will autocomplete, using the filesystem as the source of potential values. Internet Explorer uses a list of recently visited pages when you type into the address bar. So it could be good to provide this functionality in our own applications.

The example shown here is pretty simple—it has a very low-tech API for setting the list of known strings,[5] and it will only provide one suggestion at a time, as shown in Figure 6-4. (Internet Explorer will show you all possible matches in a drop-down list.) It is left as an exercise to the reader to add advanced features such as supporting data binding for the known string list, and an Internet Explorer-style drop-down suggestions list. But even this minimal implementation is surprisingly useful.

[5] So this control actually illustrates both types of extension—it augments the original control's user interface as well as its API. The API extensions are trivial, though.

Figure 6-4. Automatic completion in action
figs/winf_0604.gif

To use this control, a developer would simply add it to a form as she would a normal TextBox. It will behave in exactly the same way thanks to inheritance. The only difference is that at some point during initialization (probably in the form's constructor) there would be a series of calls to the AutoTextBox object's AddAutoCompleteString method to provide the control with its suggestion list.

At runtime, the main autocompletion work is done in the overridden OnTextChanged method. This method will be called by the base TextBox class every time the control's text changes. (It corresponds to the TextChanged event.) We simply examine the text that has been typed in so far (by looking at the Text property) and see if it matches the start of any of our suggestion strings. If it does, we put the full suggested text into the control. We also select the part of the text that we added (i.e., everything after what the user had already typed). This means that if our suggestion is wrong and the user continues typing, the text we added will be wiped out—text boxes automatically delete the selection if you type over it. (This is the standard behavior for automatic text completion.)

Example 6-7. An autocompleting TextBox in C#
using System.Collections.Specialized;
using System.Windows.Forms;

public class AutoTextBox : System.Windows.Forms.TextBox
{
    private StringCollection suggestions = new StringCollection();
    public void AddAutoCompleteString(string s)
    {
        suggestions.Add(s);
    }

    private bool ignoreNextChange = false;
    protected override void OnTextChanged(EventArgs e)
    {
        if (!ignoreNextChange)
        {
  foreach (string str in suggestions)
  {
      if (str.StartsWith(Text))
      {
          if (str.Length == Text.Length)
              return;
          int origLength = Text.Length;
          Text = str;
          Select(origLength, str.Length - origLength);
      }
        }
        base.OnTextChanged(e);
    }

    protected override void OnKeyDown(KeyEventArgs e)
    {
        switch (e.KeyCode)
        {
  case Keys.Back:
  case Keys.Delete:
      ignoreNextChange = true;
      break;
  default:
      ignoreNextChange = false;
      break;
        }
        base.OnKeyDown(e);
    }
}
Example 6-8. An autocompleting TextBox in VB
Imports System.Collections.Specialized
Imports System.Windows.Forms

Public Class AutoTextBox
   Inherits System.Windows.Forms.TextBox

   Private ignoreNextChange As Boolean = False
   Private suggestions As New StringCollection()

   Public Sub AddAutoCompleteString(ByVal s As String)
      suggestions.Add(s)
   End Sub

   Protected Overrides Sub OnTextChanged(ByVal e As EventArgs)
      If Not ignoreNextChange Then
         Dim str As String
         Dim origLength As Integer
         For Each str In suggestions
  If str.StartsWith(Text) Then
     If str.Length = Text.Length Then Return
     origLength = Text.Length
     Text = str
     [Select](origLength, str.Length - origLength)
  End If
         Next
      End If
      MyBase.OnTextChanged(e)
   End Sub

   Protected Overrides Sub OnKeyDown(ByVal e As KeyEventArgs)
      Select Case e.KeyCode
         Case Keys.Back
  ignoreNextChange = True
         Case Keys.Delete
  ignoreNextChange = True
         Case Else
  ignoreNextChange = False
      End Select
      MyBase.OnKeyDown(e)
   End Sub

End Class

There is one minor complication that requires a little more code than just the text change handler. Notice that AutoTextBox also overrides the OnKeyDown method. This is because we need to handle deletion differently. With nothing more than an OnTextChanged method, when the user hits the Backspace key, the control will delete the selection if one is active. For this control, the selection will most likely be the tail end of the last suggestion. This deletion will cause the OnTextChanged method to be called, which will promptly put the same suggestion straight back again!

This will make it seem as though the Backspace key isn't working. So we need to detect when the user has just deleted something and not attempt to autocomplete. So we override the OnKeyDown method, and if either Backspace or Delete is pressed, we set the ignoreNextChange flag to indicate to the OnTextChanged method that it shouldn't try to suggest anything this time round. The default or else case is important—if the key is not performing a deletion, we do want to provide a suggestion if there is a matching one.

Although this code is fairly simple (it overrides just two methods, using only a handful of lines of code), it illustrates an important point: the derived class's implementation is dependent upon the base class's behavior. This is partly evident in the complication surrounding deletion—the derived class has to understand the input model of the base class and work around it. It's less obvious is whether the code in Examples Example 6-7 and Example 6-8 is necessarily the right way to achieve this—why present the suggestions in OnTextChanged and not OnKeyDown, OnKeyPress, ProcessKeyMessage, or any of the other methods that sound like they might be relevant? To find the answer involves examining the documentation for every likely looking method, and then using trial and error on the ones that look as though they might work.[6]

[6] To save you the effort in this particular case, ProcessKeyMessage is far too low-level, OnKeyDown and OnKeyPress look as though they should work but turn out not to due to a subtlety in the order in which events percolate through the system, so OnTextChanged wins the day.

Even if we identify the right method or methods to override, we can never be completely sure that we have anticipated all the issues, such as special handling for deletion. As it happens, we haven't in this case—the TextBox control has a context menu with a Delete entry. This doesn't work properly with the code as it stands (because of the same deletion issue that required OnKeyPress to be overridden), and as there is no direct way to detect that this Delete menu item was selected, this is not a simple issue to solve.[7]

[7] If you would like to fix this problem as an exercise, a better approach is to make OnTextChanged remember what text had been typed in (excluding any suggestion) last time round. If it sees the same text twice running, it should not attempt to supply a suggestion the second time. You will then need to deal with the fact that setting the Text property causes a second reentrant call to OnTextChanged. With this solution you will no longer need to override OnKeyDown.

Even more insidiously, we cannot be completely sure that this will continue to work in the future. The standard controls in Windows have evolved over the years, and will almost certainly continue to do so. We cannot reasonably expect to anticipate every new feature that might emerge for every control.

So even for a fairly undemanding extension to a simple control, there are numerous hazards to negotiate, many of which are not obvious. So we will now consider the general nature of the problems you will encounter when deriving from controls, so that you can have a fighting chance of avoiding problems.

    [ Team LiB ] Previous Section Next Section