Friday, September 15, 2006
« Utah .NET User Group - September 2006 Re... | Main | Date Formatting in the ASP.NET GridView ... »

There is a very cool feature built into .NET (has been since the beginning) that I find not too many people know about called an Extender.  Well, an Extender isn't so much of a feature as it is an implementation of an interface formally called IExtenderProvider, but for brevity, I'll simply call implementors of the IExtenderProvider "Extenders".

An Extender class is one that provides additional functionality to a control or controls by augmenting its property set and providing the corresponding behavior.  An example of an Extender with which many people are familiar is the ToolTip.  Have you ever noticed that controls don't have a ToolTip property?  This is easily remedied by dragging a ToolTip control onto your form.  The ToolTip takes residence in the tray at the bottom of the designer.  All of the sudden all of the controls on the form magically have a new 'ToolTip' property.  In fact, the property is displayed more like "ToolTip on ToolTip1" to indicate the name of the control providing the property to the control.  This is the Extender in action.  The ToolTip control essentially provides one or more properties to a given type or types of controls.

Creating Extenders is remarkably easy to do, but there are some nuances to it that I'd like to explore here for a bit.  Basically the steps are as follows:

  1. Simply create a class (either a UserControl or a Component depending on whether or not you want your Extender to have a visual presence on the form) that implements the IExtenderProvider interface.
  2. Decorate the class with the [ProvidePropertyAttribute] specifying the name of the property and the type|type name of the control|component to be extended.
  3. Implement the IExtenderProvider interface (which consists of a single method called CanExtend).
  4. Because our Extender provides the property to the control, the control itself has no notion (or even knowledge) of this property.  Therefore, the onus is on the Extender to be able to store the associated property values for each control extended on the form.  I like to represent this storage with a Hashtable keyed off of the control being extended.  Create the property Hashtable.
  5. This now, is the key to making it work:  you must create two public methods whose name matches the property name specified in the [ProvidePropertyAttribute] from step 2, prefixed with 'Get' and 'Set' respectively.  The designer looks for these methods and calls them to persist property settings.  While these are technically 'methods', they behave as a property would (I wish we could simply create a property definition, but alas).
  6. For additional design-time support and control, feel free to decorate the 'Get' method with attributes of your choice (such as [CategoryAttribute] or [DescriptionAttribute]).

A common demo use case for an Extender is to provide visual feedback to the user based on some event or occurrence in the form.  Examples might include an Extender that provides a "ContextDescription" property such that on mouse over on a control, the description is displayed elsewhere on the form (or on the Extender itself).  Perhaps you want to highlight the control over which the mouse is positioned to give nice visual feedback to the user, to enhance accessibility of your applications, or for training purposes.  The list goes on and on.

One very cool aspect of Extenders is that in many ways, the Extender provides functionality that would otherwise be difficult or tedious to obtain.  For instance, if you want to implement the "ContextDescription" example above, you could do one of the following:

  • Write all of the event code on the containing form and hardcode all of the descriptions.  This is 1) tedious and 2) prone to errors and changes.  What if you reparent your controls to a new/different container? does your event code need to change?  It might.  If you move them to another form you'll have to reimplement the functionality there.  It becomes a burden to maintain.
  • Subclass (inherit) the control to provide the desired functionality or to raise the appropriate events.  This is more complicated and not as universally applicable.  For instance, what if you wanted the functionality to work for both Labels and TextBoxes?  What if you wanted to add Buttons to the mix?  Subclassing each of these is a pain and not very extensible.

I've created a very simple yet fun Extender that provides feedback as the user moves the mouse over a DataGridView control, highlighting the cell that the user is over with a color of their choice.   This simple example illustrates the techniques mentioned above:

using System;
using System.Collections;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;

namespace DataGridViewExtenderExample.Components {

   [ProvideProperty("CellHighlightColor", typeof(DataGridView))]
   public partial class DataGridViewCellHighlighterExtender : Component, IExtenderProvider {
      public DataGridViewCellHighlighterExtender() {
         InitializeComponent();
      }

      public DataGridViewCellHighlighterExtender(IContainer container) {
         container.Add(this);
         InitializeComponent();
      }

      private const int InvalidCell = -1;

      private Hashtable        _properties = new Hashtable();
      private DataGridViewCell _lastCell;
      private int              _lastCol = InvalidCell,
                               _lastRow = InvalidCell;

      [Category("Appearance")]
      [Description("Sets or returns the color to highlight the cell as the mouse moves over the cell.")]
      public Color GetCellHighlightColor(Control control) {
         object color = _properties[control];
         return ( null == color ? SystemColors.Window : (Color)color );
      }

      public void SetCellHighlightColor(Control control, Color value) {
         DataGridView grid = control as DataGridView;
         MouseEventHandler ehMove = new MouseEventHandler(mouseMove);
         EventHandler ehLeave = new EventHandler(mouseLeave);

         if ( SystemColors.Window == value ) {
            _properties.Remove(grid);
            grid.MouseMove -= ehMove;
            grid.MouseLeave -= ehLeave;
         }
         else {
            _properties[grid] = value;
            grid.MouseMove += ehMove;
            grid.MouseLeave += ehLeave;
         }
      }

      bool IExtenderProvider.CanExtend(object extendee) {
         return ( extendee is DataGridView );
      }

      private void mouseMove(object sender, MouseEventArgs e) {
         DataGridView grid = sender as DataGridView;
         DataGridView.HitTestInfo hti = grid.HitTest(e.X, e.Y);
         int col = hti.ColumnIndex;
         int row = hti.RowIndex;

         if ( col != _lastCol || row != _lastRow )
            restoreCell();

         if ( col >= 0 && row >= 0 ) {
            DataGridViewCell cell = grid[col, row];
            cell.Style.BackColor = GetCellHighlightColor(grid);

            _lastCell = cell;
            _lastCol = col;
            _lastRow = row;
         }
      }

      private void mouseLeave(object sender, EventArgs e) {
         restoreCell();
      }

      private void restoreCell() {
         if ( null != _lastCell ) {
            _lastCell.Style.BackColor = SystemColors.Window;
            _lastCell = null;
         }
      }
   }

}

Enjoy!  As you can see, there's not much to it.  You can download an entire sample application that puts the Extender to work.  :)

Friday, September 15, 2006 3:12:00 AM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [2]  |  Trackback