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
.
- Add a tab named 'FormRegion' to the VS 2003 Toolbox (right-click in the toolbox, select 'Add Tab', and type in the name).
- 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.
- Add a
Form
to your project.
- Select
FormRegionExtenderProvider
from the Toolbox by dragging and dropping it onto the Form
created in step 3.
- View the properties of the
FormRegionExtenderProvider
component, and set the Form
property to the Form
created in step 3.
- Set the
ShowFormAsRegion
property to true
.
- 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?).
- 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 extendee
s 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) {
region = ((IControlRegionProvider) c).FormRegion ;
}
if (region == null) {
region = c.Region ;
}
if (region == null) {
region = new Region(c.Bounds) ;
}
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 ;
if (Form == null)
return null ;
foreach(Control c in Form.Controls) {
if (!properties.Contains(c)) {
if (region == null) {
region = new Region(new Rectangle(0,0,0,0)) ;
}
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:
public interface IControlRegionProvider {
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.