Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Form Region Extender Provider

0.00/5 (No votes)
4 Nov 2004 2  
Defining a Forms Region using an Extender Provider.

Sample Image

Sample Image

Introduction

If you are anything like me, you are always on the look out for new samples and articles that might help shed a little light on new or better design, development/coding, and testing techniques. Like a lot of other people, I found the Using the Region Master Controls article written by Mike Harsh of Microsoft to be an interesting example of controls designed to help with development of non-linear (or should I say non-rectangular?) user interfaces.

Even though The Region Master Controls library is only sample code, the overall quality and power is reasonably high, especially the design-time support. When I first reviewed the samples, I didn't have any direct use for the controls or techniques presented, but recently, I decided to revisit the concept of defining the Region of a Form, and reviewing the Region Master Controls was at the top of my list.

Anyway, to make a long story short, I began playing around with the Region Master Controls sample application and almost immediately felt that the technique used was clumsy and limiting. Don't get me wrong, the approach was reasonably close to my ideal, but I couldn't stop wondering why the whole RegionBuilder class had not been built as an Extender Provider, and why the defined region was limited to only those controls that implemented a special interface (IRegionControl). Isn't it true that every Control defines a Region (even if it is the boring old rectangle defining the bounds of the Control surface)? Given all these factors as well as a few other implementation 'faults' that I wanted to correct, it wasn't long before I started writing what I think is a simple, high-quality implementation that also 'feels right' from the designer point-of-view, is well-documented, and ready to use in your application.

Background

This article focuses on two major topics, 'defining a Form Region', and IExtenderProvider, but does not attempt to explain either to the depth you might desire. Fortunately for all of us, Code Project contains a wealth of information on these and lots of other topics. Although not required for understanding the contents of this article, I have included the following links as you should find them to be valuable additions to your personal knowledge base:

Using the code

In the provided FormRegion library, FormRegionExtenderProvider is a Component that can be dropped on a Form and will appear in the Component Tray. It defines two properties:

  • Form - The Form whose region is to be extended.
  • ShowFormAsRegion - true to show the Form as a composite region of its child controls, false to show the entire Form.

and via IExtenderProvider provides the 'extender property', AddToFormRegion, to each child control on the Form.

The Form property is provided so that the FormRegionExtenderProvider can be associated with a specific Form whose Region will be set to the composite region of the top-level child controls whose AddToFormRegion property is true, when (and only when) the ShowFormAsRegion property is true.

Follow this procedure to use the FormRegion library, and specifically the FormRegionExtenderProvider.

  1. Add a tab named 'FormRegion' to the VS 2003 Toolbox (right-click in the toolbox, select 'Add Tab', and type in the name).
  2. With the 'FormRegion' tab selected, add the FormRegion library components by selecting 'Add/Remove Items...' and browsing to the location of the debug version of the assembly.
  3. Add a Form to your project.
  4. Select FormRegionExtenderProvider from the Toolbox by dragging and dropping it onto the Form created in step 3.
  5. View the properties of the FormRegionExtenderProvider component, and set the Form property to the Form created in step 3.
  6. Set the ShowFormAsRegion property to true.
  7. Add controls to the Form, including a button or other logic that will allow you to close the form (there won't be a caption, remember?).
  8. Build and Run.

You should see the body of the form appear as the composite of the top-level child controls. This is because you set the ShowFormAsRegion property to true in step 6, and by default, the AddToFormRegion extender property of each Control on the Form is true. That is, by default, all controls are included in the composite region of the Form. To exclude a top-level control from the region, set its AddToFormRegion 'extender property' to false.

Coding the Extender Provider

FormRegionExtenderProvider extends System.ComponentModel.Component so that it can appear in the VS 2003 Toolbox as well as the Component tray. In addition, to be an Extender Provider, we need to implement the IExtenderProvider interface.

Implementing IExtenderProvider is really straightforward as it only has a single method:

bool CanExtend(object extendee) ;

This method is passed objects via the designer to allow the Extender Provider to determine if it wants to extend the properties of the specified object. For the FormRegionProviderExtender, we only care that typeof(extendee) is Control and that it is not a Form. We exclude Form types because the AddToFormRegion doesn't make sense for a Form, especially the one we are extending. Here is the code:

public bool CanExtend(object extendee) {
    return ((extendee != null) && (extendee is Control) && !(extendee is Form)) ;
}

Although this code is sufficient, what we would really like is to do something like the following, but for a variety of reasons we can't.

public bool CanExtend(object extendee) {
    if ((Form==null) || (extendee == null) || !(extendee is Control)) return false ;
    return ((Control)extendee).Parent == Form ;
}

What this would do would be to restrict extending only top-level child controls (Parent == Form) of the Form associated with the FormRegionProviderExtender. Unfortunately, the value of Parent is not defined when the designer invokes CanExtend. Another issue is that a control's Parent value can change and what was once a non top-level control can become a top-level control. Although this case is extremely esoteric, it doesn't hurt to be able to assign a non-default value to all Form controls in the designer. If we didn't allow this, it would have to be handled in code and would be one more thing the developer had to worry about.

The second step for being an Extender Provider is to define the 'extender property' with Getter and Setter methods. Unfortunately (IMO), the technique to do this is a bit obscure. I am sure it was the best possible choice, but it is non-orthogonal to most of the patterns we typically use for developing custom controls and Windows Forms applications.

First, we need to annotate our FormRegionExtenderProvider class with a ProviderProperty attribute for each 'extender property' we support. When specifying the attribute, we provide the name of the 'extender property' as well as the base type to which 'extender property' applies. Here is the code:

[ProvideProperty("AddToFormRegion",typeof(Control))]
public class FormRegionExtenderProvider : 
     System.ComponentModel.Component, IExtenderProvider { ... }

As you can see, the 'extender property' name is AddToFormRegion, and it is applied to (generally speaking) type Control or types derived from Control.

Now, we define the Getter/Setter methods for each property. This is done by defining methods with the following patterns:

<property type> Get<property name>(object extendee) {
    return <property value> of extendee ;
}

void Set<property name>(object extendee,<property type> value) {
    <property value> of extendee = value ;
}

At this point, my implementation of an Extender Provider is going to diverge from the normal. The primary reason for this is because the AddToFormRegion extender property is of type bool and as such has only two possible values, one of which is the default (true). The implication of this is that, by default, every Control participates in defining the Region of its parent Form, thus the only thing we have to remember is those controls that are not participating (i.e. are excluded).

So instead of tracking each extendee and its associated property value, I simplify it a bit (which reduces both implementation and initialization code) by maintaining a HashTable, but only remembering those extendees that are not participating (i.e. excluded).

Here is the implementation:

[Category("Misc"), 
  Description("true if the control should be added to the forms region")] 
[DefaultValue(true)] 
public bool GetAddToFormRegion(object extendee) { 
    return !properties.Contains(extendee) ; 
}

public void SetAddToFormRegion(object extendee,bool isAddedToFormRegion) { 
    if (extendee == null) return ; 

    if (!isAddedToFormRegion) { 
        properties[extendee] = false ; 
    } else { 
        properties.Remove(extendee) ; 
    } 
}

The implementation is simple. We define and initialize a HashTable member, properties, in the constructor. When the SetAddToFormRegion method is called by the designer, we either add the extendee to the properties collection (isAddedToFormRegion is false), or remove it from the properties collection (isAddedToFormRegion is true.) In the case of GetAddToFormRegion, we simply determine if the extendee has an entry in properties (i.e., is excluded) and invert the result of the lookup.

Coding the Form Region

Having a Form take on a non-standard shape is as simple as creating a Region and setting the Form.Region property to its value. If we want the Form to appear normally, then we can set Form.Region = null. In our case, creating the Form Region is a matter of taking the union of the regions of all the forms' top-level controls whose AddToFormRegion 'extender property' is true. Although in reality the basic algorithm looks something like this, the actual implementation is not quite so simple.

foreach(Control c in Form.Controls) { 
    if (GetAddToFormRegion(c)) 
        region.Union(c.Region) ; 
}

Form.Region = region ;

First, although a Control instance has a Region property which returns the Region for the Control, it is typically null. This is primarily because most controls are rectangular, and it is less expensive (resource wise) to not create an actual Region for this simple case. In the case where the Control has a non-null Region, we use it, but otherwise, we create a Region representing the bounding rectangle of the Control. I am sure you are aware that ClientRectangle does not represent the bounds of the Control, but only the client area. To get the true bounds of the Control, we need to use the Bounds property. A key difference between the ClientRectangle and Bounds property is that the origin of ClientRectangle is always (0,0), while the origin of the Bounds rectangle is the X,Y location relative to the origin of the parent. This is actually a good thing as we are going to need to adjust the location of the region relative to both the Form and the screen to get it properly offset for drawing.

Here is the full code with comments:

protected Region GetChildRegion(Control c) { 
    Region region = null ; 

    if (c is IControlRegionProvider) { 
        // ----------------------------------------------------------- 

        // If your control implements IControlRegionProvider then you 

        // need to handle offseting the controls X,Y relative to its 

        // parent form (unless you are using the equivalent of 

        // Control.Bounds to calculate the region.) 

        // ----------------------------------------------------------- 

        region = ((IControlRegionProvider) c).FormRegion ; 
    } 

    if (region == null) { 
        // ----------------------------------------------------------- 

        // If a control directly specifies a region, use it... 

        // ----------------------------------------------------------- 

        region = c.Region ; 
    } 

    // region not specified, assume the bounds 

    if (region == null) { 
        region = new Region(c.Bounds) ; 
    }

    // ------------------------------------------------------------------------- 

    // This tranlsates the overall region by offseting the position of the form 

    // on the screen relative to the "DesktopLocation". Thus the controls region 

    // is adjusted to its absolute position on the form. 

    // 

    // Note, that unlike the 'RegionMasterControls' implementation we do not 

    // offset the client's X,Y on the form as the 'Bounds' method already 

    // handles that for us (i.e., it is relative to the Form.) 

    // ------------------------------------------------------------------------- 

    Point formClientScreenLocation = Form.PointToScreen(new 
          Point(Form.ClientRectangle.Left, Form.ClientRectangle.Top)); 
    int x = formClientScreenLocation.X - Form.DesktopLocation.X ; 
    int y = formClientScreenLocation.Y - Form.DesktopLocation.Y ; 
    region.Translate(x, y) ; 

    return region ; 
}

protected virtual Region MakeFormRegion() {
    Region region = null ;

    // No form, no region

    if (Form == null)
        return null ;

    foreach(Control c in Form.Controls) {
        // If the control is not excluded from the region,

        // add it to the region

        if (!properties.Contains(c)) {
            // create region on demand

            if (region == null) {
                // Initialize to empty

                region = new Region(new Rectangle(0,0,0,0)) ;
            }

            // add child region to form region and dispose of child region

            using (Region cRegion = GetChildRegion(c)) {
                region.Union(cRegion);
            }
        } 
    }

    return region ;
}

The code contains one key aspect of the FormRegion library that I haven't discussed, and that is the IControlRegionProvider interface. This interface allows a Control to provide a custom Region (rather than setting the Control.Region property which is typically not done). This is similar to the IRegionControl interface defined in the Region Master Controls.

The IRegionControlProvider is a simple interface and is defined below:

/// <summary>

/// Optional interface implemented to provide more powerful support when using 

/// the FormRegionExtenderProvider class 

/// </summary>

public interface IControlRegionProvider { 
    /// <summary>

    /// Return the region representing the control when composited with other 

    /// controls to create the non-rectangular region for the parent Form 

    /// </summary>

    Region FormRegion { get ; } 
}

Essentially, a custom control that implements IControlRegionProvider implements the single method, FormRegion, to return the Region defining the control's surface. In doing this, the control has two basic responsibilities:

  • Offset the Region for the control surface so that it is relative to the origin of the Form.
  • If the custom control wants to cache the Region, it should return (Region) cachedRegion.Clone();.

The FormRegion library contains two custom controls, GradientPanel and FormCaption, that illustrates implementing IControlRegionProvider.

Points of Interest

Although I like my FormRegionExtenerProvider a lot better, the Region Master Controls are a lot more than just the RegionBuilder concept and includes a nice set of custom controls with great designer support. I definitely recommend that you check them out for yourself.

History

Version 1.0.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here