| |
ContentsIntroductionIn this tutorial we will walk through the process of creating a custom Windows Forms control from the start of the project all the way to inclusion in the Visual Studio ToolBox. The control will be a simple DividerPanel - an inherited Panel control that features selectable border appearance and border sides. After completing this tutorial, readers should have a basic foundation on class inheritance, creating custom properties, overriding base methods, using property comments, creating a ToolBox icon, creating a simple designer class and integrating a custom control into the Visual Studio ToolBox. Along the way we will discuss some best practices and detail some shortcuts in Visual Studio that help to simplify control development. Creating a New SolutionWhen creating a new project in Visual Studio for control development, it's usually a good idea to start with a new Blank Solution rather than jumping straight into a new Control Library project with the project wizard. In doing so, you can create multiple projects within the one solution - this allows your test application and control library to remain as separate projects, and also adds the ability to easily share linked classes and include global solution items. To create a new Blank Solution, select File > New > Blank Solution. In the New Solution dialog, enter the solution name as Windows Forms Divider Panel and click OK. Once your new solution has been created, right-click on the solution title and select Add > New Project. When the Add New Project dialog opens, select the Windows Control Library option, and enter DividerPanel as the project name. The wizard will create the control library with two files by default: UserControl1.cs and AssemblyInfo.cs. For this tutorial we will delete UserControl1.cs and create our control in a new, empty class. Highlight UserControl1.cs then right-click and select Delete to remove it from the project. Next, right-click on the DividerPanel project in Solution Explorer, then select Add > Add Class from the context menu. In the Add Class dialog, enter DividerPanel.cs as the class name and click OK.
Inheriting From Existing ControlsInheritance is one of the major factors that makes object oriented programming so powerful. When we inherit from an existing class we automatically pick up all of the base classes' functionality and gain the ability to extend upon it to create a more specialized class. All Windows Forms controls at some point must inherit from Fortunately inheriting from an existing control is a snap - once you have decided the control that has the base functionality you wish to extend upon, it takes just one addition to your class declaration line to make your class inherit from it: public class DividerPanel : System.Windows.Forms.Panel
{
} For our DividerPanel control, we have specified that we are inheriting our base functionality from the standard System.Windows.Forms.Panel control. In doing this, our new control now has all of the Properties and Methods of the Panel control - we can now add our own custom properties to it, and override some of the Panel controls methods in order to implement our customizations. Adding Properties and AccessorsIn order to implement our While there a few different ways we can expose these properties, there is only one good way - create a private variable for use by methods within our control class, and a complementary public accessor for exposing the property to other classes that will use the control, like so: // This system of private properties with public accessors is a
// best practice coding style.
// Note that our private properties are in camelCasing -
// the first letter is lower case, and additional word
// boundaries are capitalized.
private System.Windows.Forms.Border3DSide borderSide;
private System.Windows.Forms.Border3DStyle border3DStyle;
// Next we have our public accessors, Note that for public accessors
// we use PascalCasing - the first letter is capitalized and additional
// word boundaries are also capitalized.
public System.Windows.Forms.Border3DSide BorderSide
{
get { return this.borderSide; }
set
{
if( this.borderSide != value )
{
this.borderSide = value;
this.Invalidate();
}
}
}
public System.Windows.Forms.Border3DStyle Border3DStyle
{
get { return this.border3DStyle; }
set
{
if( this.border3DStyle != value )
{
this.border3DStyle = value;
this.Invalidate();
}
}
}
In the above code we first define two private properties: borderSide and border3DStyle - these are the variables we will use within our class. As these are both defined with the private attribute they cannot be accessed by any code outside of our control class. You will also note that I have specified the full path to the property objects - // good
private System.Windows.Forms.Border3DSide borderSide; As I have included a using System.Windows.Forms directive for my class I could have just declared the property as: // bad
private Border3DSide borderSide; But it is always good practice to include the full path to alleviate possible naming obscurities. Next we have two public properties, both using We could have simply created two public only properties as in the sample below, but then we could not do any processing such as the // This is bad coding, never expose public properties like this!
// Always use private variables with complementary public accessors
public System.Windows.Forms.Border3DSide BorderSide;
public System.Windows.Forms.Border3DStyle Border3DStyle;
Even if you don't intend on doing any processing in you public accessors, you should still always stick to good quality coding conventions such as this. The next point is my choice of naming conventions - many people still use naming conventions from other languages such as The third reason for not using prefixes such as Any variables you define should always be explicitly assigned a value before they are used. Always set initial values in the class's constructor, or in a method called fom your constructor. Variables without initial value assignments can sometimes create problems that can take hours to find at a later time. For our DividerPanel class, the Constructor looks like this: // This is the Constructor for our class. Any private variables
// we have defined should have their initial values set here. It
// is good practice to always initialize every variable to a specific
// value, rather then leaving it as an inferred value. For example,
// all bool's should be set to false rather than leaving them as an
// inferred false, and any objects referenced that are initially null
// should be explicitly set to null. (eg. myObject = null)
public DividerPanel()
{
// Set default values for our control's properties
this.borderSide = System.Windows.Forms.Border3DSide.All;
this.border3DStyle = System.Windows.Forms.Border3DStyle.Etched;
}
The constructor is the first method called in any class, and is called upon instantiation. You would normally carry out any initialization work necessary here, to ensure that everything is ready to go before any other methods can be called by code using your class. Constructors are always named the same as their parent class, and every class that is to be used as a control must have a parameter-less public constructor if it is to be used with the Visual Studio designer, or be visible to COM clients. Overriding Inherited MethodsWhen you override a method from a base class, the CLR will run your code instead of the code normally contained in the base class's corresponding method. This allows you to easily change the behaviour and extend upon the functionality of most of the base controls in the framework. In order to add a border to our DividerPanel, we are going to override the Naturally every method has it's own unique parameters and in order to perform an override we need to know exactly what those parameters are. Fortunately Visual Studio makes it a snap for us to add overrides when creating custom controls. On the right side of the Visual Studio window you have the Solution Explorer displayed by default, at the bottom of the Solution Explorer panel, click the tab labelled Class View . Next expand out the The Class View is also handy for understanding the path of inheritance your control uses. In our case we can see that our inheritance path is: For our Visual Studio will then insert an overridden version of the protected override void OnPaint(System.Windows.Forms.PaintEventArgs e)
{
// allow normal control painting to occur first
base.OnPaint(e);
// add our custom border
System.Windows.Forms.ControlPaint.DrawBorder3D (
e.Graphics,
this.ClientRectangle,
this.border3DStyle,
this.borderSide );
} The first line we'll add to the overriden method is After the base control has been painted (and only after) we use the At this stage we now have a fully functional DividerPanel control, that could be compiled and manually referenced in an application, but don't stop now because we are far from finished! Adding Property Descriptions & Documentation SupportIn order to make our control look and feel like one from the framework ToolBox, we need to add descriptions to our new public accessors, and create a new properties group. Without this, our new properties will be listed as Misc, and it will not be clear exactly what our new properties are intended to do to other developers. Once again, adding property descriptions is a snap, and while we're at it well include support for creating Xml documentation files which can later be created with utilities such as NDoc.
To achieve these, we'll modify our public accessors using the code sample below. The <summary> lines are used to generate Xml documentation files by Visual Studio - to add a summary to any property or class, simply position the caret to a line directly above your property/class and enter three forward slashes (///). Visual Studio will automatically build the required summary structure and all you need to do is fill in the blanks. The next addition we make here is the designer attributes line, starting with the Bindable attribute. We'll step through the attributes added here one by one: Bindable(true) - When set to true, any changes to this property will raise a property changed notification, which makes your designer class (discussed later) aware of changes made at design time. Category("Border Options") - The Category attribute specifies where this property should be grouped in the Properties panel of the forms designer. If this attribute is not set, your new property will be listed under "Misc". DefaultValue(System.Windows.Forms.Border3dSide.All) - this attribute tells the Visual Studio designer what the default value of the property is, and is used to highlight changes from the default value with bold text. In our screenshot you can see that the value "Etched" is not in bold because we are using the default value, but the value "Top" is in bold because we have changed it from the default value of "All". Note that this attribute has nothing to do with the default value assigned to the property - you must still set a default value for all properties in your controls' constructor. Description("Specifies the sides of the panel to apply a three-dimensional border to.") - the Description value is displayed at the bottom of the properties panel whenever a property is selected, and should clearly convey to developers what the property does. /// <summary>
/// Specifies the sides of the panel to apply a three-dimensional border to.
/// </summary>
[Bindable(true), Category("Border Options"),
DefaultValue(System.Windows.Forms.Border3DSide.All),
Description("Specifies the sides of the panel to apply a
three-dimensional border to.")]
public System.Windows.Forms.Border3DSide BorderSide
{
get { return this.borderSide; }
set
{
if( this.borderSide != value )
{
this.borderSide = value;
this.Invalidate();
}
}
}
/// <summary>
/// Specifies the style of the three-dimensional border.
/// </summary>
[Bindable(true), Category("Border Options"),
DefaultValue(System.Windows.Forms.Border3DStyle.Etched),
Description("Specifies the style of the three-dimensional border.")]
public System.Windows.Forms.Border3DStyle Border3DStyle
{
get { return this.border3DStyle; }
set
{
if( this.border3DStyle != value )
{
this.border3DStyle = value;
this.Invalidate();
}
}
} Adding Toolbox SupportAdding Toolbox support to our new control is just a simple, and involves creating a bitmap to be used as a Toolbox icon for our control, and setting a couple of attributes so that Visual Studio knows how to display our control in the Toolbox. To create an icon for our control, right-click on our DividerPanel project entry in Solution Explorer, then click Add > New Item. In the dialog that opens, select Bitmap File and set the name to DividerPanel.bmp. Now highlight DividerPanel.bmp in Solution Explorer and set the Build Action to Embedded Resource. Next Open the DividerPanel.bmp for editing, and set both the Height and Width to 16, and set the Colors property to 16. Now paint your icon bitmap and save it: The final step is to add two new attributes to our DividerPanel class, so that the compiler knows to associate the bitmap and to allow the control to be included in the Visual Studio Toolbox: [ToolboxItem(true)]
[ToolboxBitmap(typeof(DividerPanel))]
public class DividerPanel : System.Windows.Forms.Panel
{
} The first attribute we are adding allows our class to be used as a Toolbox item - if you omit this attribute Visual Studio will imply it is already set to true, but as always it is good coding practice to explicitly set this attribute so that your original intentions are always clear when revisting your code. The second line tells the compiler to associate the DividerPanel.bmp file with our control. The name specified in this attribute is simply the resource name of the bitmap, excluding the .bmp extension. As a final note, Toolbox icons must always be bitmap files, with a maximum color depth of 16 colors, and dimensions of 16x16. Adding a Simple Designer ClassWe could now compile our control, add it to the Toolbox and start dragging onto Forms at will, but there's one potential problem we need to address first: The Panel control we derived from has a There are a few ways to achieve the result we want, but the cleanest way is to create a simple designer class that filters the property list, removing To start our designer class, right-click on the DividerPanel project in Solution Explorer, then select Add > Add Class from the context menu. In the Add Class dialog, enter DividerPanelDesigner.cs as the class name and click OK.
To implement our designer class, our project needs to reference System.Design.dll from the framework. To add the reference, right-click on References in Solution Explorer, then click Add Reference. In the Add Reference dialog, select System.Design.dll as pictured above, and click OK. Open DividerPanelDesigner.cs, and change the Class line so that we are inheriting from the public class DividerPanelDesigner :
System.Windows.Forms.Design.ScrollableControlDesigner
{
} Now change to Class View and expand our As before, Visual Studio will automatically add the overridden method into our designer class code. All we have to do now is add the code to filter out our unwanted BorderStyle property: protected override void PreFilterProperties(
System.Collections.IDictionary properties)
{
properties.Remove("BorderStyle");
} And finally add a designer attribute to our [ToolboxItem(true)]
[ToolboxBitmap(typeof(DividerPanel))]
[DesignerAttribute(typeof(DividerPanelDesigner))]
public class DividerPanel : System.Windows.Forms.Panel
{
} For this tutorial that's all our designer class needs to do, so we'll leave it at that for now. Creating designer classes is a good subject for a book, not a simple tutorial such as this, but at least you now have an introduction on how to create and use them. Assembly Attributes & SigningThe final steps we should do before releasing our new control on the unsuspecting public are related to the compilation process. The additions discussed here are all non-essential but do represent best practice coding and a very small amount of work, so there's no reason to omit them. First we'll open up the AssemblyInfo.cs file and provide some values to the default assembly attributes as follows: [assembly: AssemblyTitle("Divider Panel")]
[assembly: AssemblyDescription(
"A Panel control with selectable border appearance")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("CodeShack")]
[assembly: AssemblyProduct("Divider Panel Tutorial")]
[assembly: AssemblyCopyright("Copyright (c) 2003-2004 CodeShack")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")] Next we'll add a couple more assembly attributes that are not included by default (I'll let my code comments speak for themselves): // Flagging your assembly with the CLS Compliant attribute
// lets the Framework know that it will work with all
// CLS Compliant languages, and theoretically with all
// future framework platforms (such as Mono on Linux).
// If possible you should avoid using any non-CLS compliant code
// in your controls such as unsigned integers (uint) and
// calls to non-framework assemblies.
[assembly: System.CLSCompliant(true)]
// The ComVisible attribute should always be explicitly set for controls.
// Note that in order for your control to be consumed by
// COM callers, you must use parameter-less constructors
// and you cannot use static methods.
[assembly: System.Runtime.InteropServices.ComVisible(true)] And now the final step before we can use our new control, signing your assembly. Signing your assembly is always good practice as it prevents modified or corrupted libraries from being loaded, protecting the application using it against things such as download errors or tampering. First we must use the Strong Name utility provided with Visual Studio to create a new public/private key pair for our assembly. To do this open up the Visual Studio Command Prompt by clicking Start > All Programs > Visual Studio.Net > Visual Studio .Net Tools > Visual Studio Command Prompt. At the command prompt enter: sn -k [outputfile].snk If you didn't specify the output file to be in your project directory, copy it there now and then modify the AssemblyInfo.cs file as follows, so that Visual Studio knows the relative path to your key file: [assembly: AssemblyKeyFile("..//..//..//DividerPanel.snk")] Using the ControlNow all that's left is to build our control, and add it to the Toolbox so we can use it in our applications. Once you are satisfied that your control is bug-free and feature complete, change the Build Configuration in Visual Studio to Release and hit F5 to to compile it. Next create a new Windows Application project and open a form in design view so that the Windows Forms Toolbox is visible. Right-click on the Toolbox and select Customize Toolbox, when the Customize Toolbox dialog opens, click the .Net Framework Components tab, click Browse, locate the DividerPanel.dll file in the bin/Release directory and click OK. You will now see the DividerPanel control in the Toolbox items list, and you are ready to start dragging it into your applications. SummaryIn this tutorial we have touched on the following topics:
|