- Developing Components
- Developing Controls
- Resources and Internationalization
- Summary
8.2 Developing Controls
I find the process of developing a control rather odd. The whole point about a control is that it has a visual element, but the Windows Forms designer does not show the visual representation of a class derived from Control. Instead it gives a schematic showing the components that the control uses that you've dragged from the Server Explorer or Toolbox window. This makes developing the visual aspect of a control a bit of a nuisance: In effect you have to add a forms project to your control solution with a form that contains the control so that as you develop the control, you can see the effects as you make them. You do not have this problem when developing a UserControl class because the designer shows the control as a captionless, borderless form onto which you can drag and drop controls from the Toolbox window.
In this section I will describe the process of developing a simple control and point out some of the issues you will face. UserControl is composed of other controls, so developing a UserControl object is similar to developing a Control object, except that you have the additional steps of adding controls from the Toolbox window and adding event handlers generated by these constituent controls.
8.2.1 Developing a Sample Control
The control that I'll develop, called DiskSpace, is shown in Figure 8.1. This control has a property called Disk that holds the logical name of a disk on your machine. The control will display in its area the name of the disk and either the size
Figure 8.1 The DiskSpace control
of the disk or the amount of free space available to the current user on the disk. Which of these sizes will be displayed is determined by another property, called Display. These properties can be changed at design time in the Properties window.
The first step is to create a control library. I will use C# as the development language so that I can use the designer tool for some of the work. The project type to use is the Windows Control Library project, and I will call my project DiskData. Once you have created the project, the first thing you'll notice is that the Designer window will start up showing a gray, borderless box. Don't be fooled; you get this because the project wizard has generated control code derived from UserControl. In this example the control should derive from Control, and you can make this change in a moment.
While the control is visible in the Designer window, you should select its properties (through the context menu) and change its name (in the Name field) to DiskSpace. Now switch to code view by selecting View Code from the control's context menu, and edit the code so that the DiskSpace class derives from Control. Because the designer is not much use for this code, and because the control will not use other components, it is safe to remove the code that the wizard added for the designer. Finally, by selecting Rename in the context menu of the Solution Explorer window, change the name of the code file from UserControl1.cs to DiskSpace.cs.
After all these changes, the class should look like this:
// C# namespace DiskData { public class DiskSpace : Control { public DiskSpace() { } } }
If you now switch back to the designer, you'll see that the gray surface of the control has been removed, and there will be a message telling you to add components from the Server Explorer or the Toolbox window. The control has a UI and hence needs to draw itself. To do this it has to implement the OnPaint() method:
// C# protected override void OnPaint(PaintEventArgs pe) { Graphics g = pe.Graphics; g.FillRectangle(new SolidBrush(BackColor), 0, 0, Size.Width, Size.Height); g.DrawRectangle(new Pen(ForeColor), 0, 0, Size.Width - 1, Size.Height - 1); }
This code accesses the inherited properties BackColor and ForeColor; it fills the area of the control with the color specified by BackColor and draws a rectangle within the inside edge with the color specified by ForeColor. The area of the control is accessed through the Size property. Even though this control clearly has a user interface, the Designer window still does not give a visual representation, so the only way you can see what you are creating is by adding the control to a form.
Before doing this, you should close the Designer window, compile the project and then use Solution Explorer to add a new Windows Application project to the solution (I call my project CtrlTest). When this project is created, you'll see an empty form, and if you open the Toolbox window, you'll see Components and Windows Forms tabs. Select one of these tabs (or even create your own tab), and while the Toolbox window is open, switch to Windows Explorer, drag the DiskData.dll file from the bin\Debug folder of the project folder, and drop it on the Toolbox window. You'll see that the control will be added to the Toolbox window as shown in Figure 8.2. Notice that the image next to the control name is a cog. This is a standard image used when components don't specify a particular image; I'll explain how to change this later.
Figure 8.2 A control added to the Toolbox window
Now you can drag the DiskSpace control from the Toolbox window and drop it on the form, and you should see a gray square bordered by a black edge. As a quick test, grab one edge of the control and reduce the width or height; you'll see that the control does not repaint the edge that you have moved. Furthermore, if you increase the control's size, you'll see that the edge is moved but the interior is not repainted. The result is that lines appear on the control surface as the control is resized, as Figure 8.3 shows.
Figure 8.3 Resizing the control
The solution to this problem is to ensure that when the control is resized, it is redrawn. To do this you need to add the following method:
// C# protected override void OnSizeChanged(EventArgs e) { Invalidate(); base.OnSizeChanged(e); }
When the control is resized, this method will be called. I handle the resize event by indicating that the entire control should be redrawn. I could be more sophisticated and track the size of the control and then invalidate only the area that has changed, but for this example my code is sufficient.
The control has two properties, so the following code needs to be added to the class:
// C# public enum DisplayOptions {TotalSize, FreeSpace}; private string disk = "C:\\"; private DisplayOptions display = DisplayOptions.TotalSize; public string Disk { get { return disk; } set { disk = value; Invalidate(); } } public DisplayOptions Display { get { return display; } set { display = value; Invalidate(); } }
The Display property indicates whether the size of the disk or its free space is shown, as identified by the enum. The Disk property indicates the drive to be displayed. If you compile the project at this point and then select the control on the test form, you'll see that the Properties window has been updated with the new properties (Figure 8.4). Furthermore, the Properties window reads the metadata of the control to see that the Display property can have one of two named values, and it will insert these values in a drop-down list box.
By default the Properties window allows you to either type in a value (as in the case with Disk) or select a value from a list, and the Properties window will do the appropriate coercion from the value you input to the type needed by the property. You can change this behavior by applying the [TypeConverter] attribute to the property and pass the type (or name) of a class derived from TypeConverter as the attribute parameter. In addition, you can provide values for a drop-down list box, or even provide a dialog to edit the property, as I'll show later.
Figure 8.4 shows the properties in alphabetical order. The properties can also be listed by category; to do this you need to use an attribute on the propertyfor example:
// C# [Category("DiskSpace")] public string Disk{/* code */} [Category("DiskSpace")] public DisplayOptions Display{/* code */}
Figure 8.4 Properties window showing the new properties
The category can be one of the predefined categories documented in the MSDN entry for System.ComponentModel.CategoryAttribute, or you can create your own category, as I have done here. When you compile the assembly and look at the control's categorized properties, you'll see a new category called DiskSpace. Under this category are the two properties (see Figure 8.5).
Figure 8.5 Categorized properties
The properties are shown in the Properties window because by default, all properties are browsable. If you want to indicate that the property should not be shown in the Properties window, you can use the [Browsable(false)] attribute. In a similar way, if you write code that uses an instance of the DiskSpace control, IntelliSense will show the property names in a list box when you type a period after the name of a variable of DiskSpace (for C#). You can use the [EditorBrowsable] attribute to alter this behavior: The parameter is EditorBrowsableState, and if you use the value Never, the property will not be shown in the IntelliSense list box; the default is Always. At the bottom of the Properties window is a space for a description of the property, and because these properties do not have descriptions, just the property name is given. To add a description to a property, you should use the [Description] attribute. Here are the changes:
// C# initialized to default value private string disk = "C:\\"; [ Category("DiskSpace"), Browsable(true), EditorBrowsable, Description("The name of the disk") ] public string Disk { /* code */ } [Category("DiskSpace"), Browsable(true), EditorBrowsable, Description("Whether the total size or free " + "space on the disk is shown") ] public DisplayOptions Display { /* code */ }
The name of the property given in the Properties window will be the name of the property in the class. You can use the [ParenthesizePropertyName] attribute to indicate that the name should be shown in parentheses, which means that the property will appear near the top of the Properties window when properties are shown in alphabetical view, or near the top of the category when they are shown in categorized view. You will notice that all of the screen shots of the Properties window that you have seen here show the values of the Disk and Display properties in bold. The Properties window uses the convention of showing in bold any properties that have been changed from their default values. This poses the question, How do you specify a default value?
There are two ways to do this. The first is to use the [DefaultValue] attribute on the property, passing the value as the constructor parameter. This option is fine for primitive types (the attribute constructor is overloaded for all of the base types). If the type is more complex, you can provide a string version of the default value, as well as the type to which the value should be converted, and the system will attempt to find a TypeConverter class to do the conversion. If there is no type converter, you can use the second way to specify a default value: adding two methods to the class with the names Reset<property>() and ShouldSerialize<property>(), where <property> is the property name. Reset<property>() should change the property to its default value, and ShouldSerialize<property>() should return a bool value indicating whether the property has a value other than its default. This last method gets its name from the fact that if the property does not have its default value, the value should be stored so that it can be used at runtime (for a form generated by the C# or VB.NET designer, this means initializing the control's property with the value).
If the property has a default value, the value does not need to be serialized because when the control is created, the property will have the default value. Your implementation of the property must be initialized to the default value. Examples of the Reset<property>() and ShouldSerialize<property>() methods are shown in the following code:
// C# // initialized to default value private string disk = "C:\\"; [Category("DiskSpace"), Browsable(true), EditorBrowsable, Description("The name of the disk"), DefaultValue("C:\\") ] public string Disk { /* code */ } // default value private DisplayOptions display = DisplayOptions.TotalSize; [ Category("DiskSpace"), Browsable(true), EditorBrowsable, Description("Whether the total size or free " + "space on the disk is shown") ] public DisplayOptions Display { /* code */ } public void ResetDisplay() { display = DisplayOptions.TotalSize; } public bool ShouldSerializeDisplay() { return display != DisplayOptions.TotalSize; }
In both cases you'll find that the property value will be shown in normal text if it is the default value.
Properties can be changed at runtime, and the change in a property value can have effects on other code. A good example is the Size property of a control: If the size changes, in most cases the control will need to be redrawn; thus you need to catch the event of the property changing. This is what I showed earlier with the code that overrides the OnSizeChanged() method. You should also add events that are generated when your properties change, by adding an event and an event generation method, as illustrated here:
// C# public event EventHandler DiskChanged; public event EventHandler DisplayChanged; protected virtual void OnDiskChanged(EventArgs e) { if (DiskChanged != null) DiskChanged(this, e); } protected virtual void OnDisplayChanged(EventArgs e) { if (DisplayChanged != null) DisplayChanged(this, e); }
The event generation method should be named On<property>Changed() and should generate the event. The set methods for the properties should call this method:
// C# public string Disk { get { return disk; } set { disk = value; OnDiskChanged(null); Invalidate(); } } public DisplayOptions Display { get { return display; } set { display = value; OnDisplayChanged(null); Invalidate(); } }
Sometimes several properties may depend on one property. If that is the case, when that property changes the dependent properties will change too. In this case the Properties window should refresh all the values. To indicate this requirement, such a property should be marked with the [RefreshProperties] attribute.
The next task that needs to be carried out for this control is to make it actually do something! The first thing is to implement the Disk property so that it checks that the value passed to the property is valid:
// C# public string Disk { get { return disk; } set { string str; str = Char.ToUpper(value[0]) + ":\\"; string[] disks = Environment.GetLogicalDrives(); if (Array.BinarySearch(disks, str) < 0) throw new IOException(value + " is not a valid drive"); disk = str; OnDiskChanged(null); Invalidate(); } }
For this code to compile, you will need to add a using statement for the System.IO namespace at the top of the file. First I construct the disk name; then I obtain the list of logical drives on the current machine and perform a binary search to see if the requested disk is within the array of logical drive names. Now that I have a valid drive name, I need to obtain the size of the disk. I do this through interop to call the Win32 GetDiskFreeSpace() method:
// C# [ DllImport("kernel32", CharSet=CharSet.Auto, SetLastError = true) ] static extern bool GetDiskFreeSpace( string strRoot, out uint sectersPerCluster, out uint bytesPerSector, out uint numFreeClusters, out uint totalClusters); protected override void OnPaint(PaintEventArgs pe) { Graphics g = pe.Graphics; g.FillRectangle(new SolidBrush(BackColor), 0, 0, Size.Width, Size.Height); g.DrawRectangle(new Pen(ForeColor), 0, 0, Size.Width-1, Size.Height-1); uint spc, bps, fc, tc; GetDiskFreeSpace(disk, out spc, out bps, out fc, out tc); long free, total; long bPerCluster = (spc*bps); free = bPerCluster*fc/(1024*1024); total = bPerCluster*tc/(1024*1024); StringFormat sf = new StringFormat(); sf.Alignment = StringAlignment.Center; sf.LineAlignment = StringAlignment.Center; string str; if (display == DisplayOptions.FreeSpace) str = disk + " " + free + "Mb"; else str = disk + " " + total + "Mb"; g.DrawString(str, this.Font, new SolidBrush(ForeColor), new RectangleF(0, 0, Size.Width, Size.Height), sf); }
For this code to compile, you should add a using statement for the Sys-tem.Runtime.InteropServices namespace to the top of the file. The On-Paint() method calls the imported GetDiskFreeSpace() function and passes the Disk property. Depending on the value of Display, the string printed on the control is formatted as showing the total space on the disk or just the free space. Notice again how the control's properties are used. In the Draw-String() method at the end of OnPaint(), I draw the string in the color specified by ForeColor, using the default font for the control.
Once you have rebuilt the control, you should be able to view it on the test form, and you should be able to change the Disk and Display properties and see the control on the form change its view at design time. Before I leave this section, I ought to explain one property that you'll see in the Properties window: the parenthesized DynamicProperties complex property, which will have a sub-property with the parenthesized name Advanced. If you select this property, you get a list of most of the properties that the control supports and a check box next to each. If you check a property in this list, the designer will add a section for the property in the application's .config file (an XML file that is installed in the same folder as the application), and at runtime when the control is loaded, its values will be set according to the values in this .config file. For example, if I use Dynam-icProperties to select the Disk property, the .config file will look like this:1
<configuration> <appSettings> <add key="diskSpace1.Disk" value="D:\" /> </appSettings> </configuration>
Here I have specified that the Disk property of the control diskSpace1 should have the value D:\ when the control is loaded. The code on the form can still change this property; however, this is a useful facility because it allows you to give your users some control over how the controls on your forms are initialized.
8.2.2 Property Editor
When you type a value into the Properties window, what you are actually typing is a text value. Some typesfor example, Pointare complex and are made up of subtypes. The Properties window reads the type of the property, recognizes that the property has subtypes, and displays these subtypes in the grid as nodes in a tree view. The grid allows you to edit each subobject individually or, through an editor class, the entire property as one.
When the values of the property have been edited, the values are converted to the appropriate types through a converter class. The framework type converter classes are shown in Table 8.1. If your type is not covered by one of these converters, you can create your own converter by deriving from Type-Converter and then pass the type of this class to the constructor of the [TypeConverter] attribute, which you should apply to the definition of the type that is converted.
Table 8.1 Type converter classes
ArrayConverter |
DecimalConverter |
SByteConverter |
BaseNumberConverter |
DoubleConverter |
SingleConverter |
BooleanConverter |
EnumConverter |
StringConverter |
ByteConverter |
ExpandableObjectConverter |
TimeSpanConverter |
CharConverter |
GuidConverter |
TypeConverter |
CollectionConverter |
Int16Converter |
TypeListConverter |
ComponentConverter |
Int32Converter |
UInt16Converter |
CultureInfoConverter |
Int64Converter |
UInt32Converter |
DateTimeConverter |
ReferenceConverter |
UInt64Converter |
Imagine that you have developed a control class that has an array property:
// C# int[] b = new int[4]; public byte[] Data { get { return b; } set { b = value; } }
When you view the property in the Properties window, you'll see it shown as its constituent parts, and you can edit each item. If you select the property itself, an ellipsis button will appear (see Figure 8.6); and when you click this button, an appropriate UI editor will be shown. In the case of an array of Int32 members, the Int32 Collection Editor will be shown (Figure 8.7). This editor allows you to edit the values in the array, and to add and remove items in the array.
Figure 8.6 Array property in the Properties window
Figure 8.7 The collection editor for an array of Int32 members
You can also write your own editor. For example, imagine that you want to create an editor for the Disk property so that it gives you only the option of the disks that are available on the current machine. The first action is to design an appropriate editor dialog, by adding a form to the project called DiskEdi-tor.cs through the Solution Explorer window. Next you edit the class to look like this:
// C# public class DiskEditor : Form { private ComboBox cbDisks; private Button btnOK; private Container components = null; private string str; public string Value { get { return str; } } public DiskEditor(string currentVal) { str = currentVal; ClientSize = new Size(120, 70); components = new Container(); cbDisks = new ComboBox(); components.Add(cbDisks); cbDisks.DropDownStyle = ComboBoxStyle.DropDownList; cbDisks.Location = new Point(10, 10); cbDisks.Size = new Size(100, 20); string[] disks = Environment.GetLogicalDrives(); cbDisks.Items.AddRange(disks); cbDisks.Text = (string)cbDisks . Items[cbDisks.FindString(str)]; btnOK = new Button(); components.Add(btnOK); btnOK.Location = new Point(30, 40); btnOK.Size = new Size(60, 20); btnOK.Text = "OK"; btnOK.Click += new EventHandler(OnOK); Controls.AddRange( new Control[] {btnOK, cbDisks}); FormBorderStyle = FormBorderStyle.FixedDialog; Text = "Disks"; } protected override void Dispose( bool disposing ) { if (disposing) if (components != null) components.Dispose(); base.Dispose( disposing ); } private void OnOK(object sender, EventArgs e) { Close(); } protected override void OnClosing(CancelEventArgs e) { str = (string)cbDisks.SelectedItem ; } }
The DiskEditor constructor takes the current value of the property. The dialog has two controls: a drop-down list box that is initialized to the logical disk drives on the machine, and an OK button that, when clicked, will close the dialog. The form has a property called Value that is initialized to the disk that you selected, and this property is updated when the dialog closes.
Next you need a class derived from UITypeEditor that will be called to determine how the type should be edited. The GetEditStyle() method is called by the Properties window to determine how the value should be edited. UITypeEditorEditStyle has three values: None, which means that no UI element will be used to edit the value; DropDown, which means that a drop-down list will be shown; and Modal, which means that a modal dialog will be shown. I will first show an example of using a modal dialog. In this case the type editor class in DiskEditor.cs should be edited to look like this:
// C# public class DiskTypeEditor : UITypeEditor { public override object EditValue( ITypeDescriptorContext context, IServiceProvider provider, object value) { IWindowsFormsEditorService edSvc; edSvc = (IWindowsFormsEditorService) provider.GetService( typeof(IWindowsFormsEditorService)); DiskEditor editorForm; editorForm = new DiskEditor((string)value); edSvc.ShowDialog(editorForm); return editorForm.Value; } public override UITypeEditorEditStyle GetEditStyle( ITypeDescriptorContext context) { return UITypeEditorEditStyle.Modal; } }
For this code to compile you need to add a using statement for both the Sys-tem.Drawing.Design and System.Windows.Forms.Design namespaces to the top of the file. After the GetEditStyle() method is called, the Properties window will show either an ellipsis button (for the modal dialog) or a down-arrow button (for a drop-down list). When this UI button is clicked, the EditValue() method will be called to create the dialog to fill the list. The code here shows how to create the form. The first parameter of EditValue() provides information about the container, the Properties window. The second parameter gives access to the services that the Properties window provides, and in this case I request IWindowsFormsEdi-torService, which I use to call ShowDialog() to show the modal form. The final parameter of the method is the actual property that is being edited, so this parameter is used to initialize the form. When the modal form is closed, ShowDialog() will return; I access the value that the user selected through the DiskEditor.Value property. The final step is to indicate that a property will be edited with this particular editor; for this purpose the [Editor] attribute is used as follows:
// C# [ Editor(typeof(DiskTypeEditor), typeof(UITypeEditor)) ] public string Disk { /* code */ }
You will need to add a using statement for the System.Drawing.Design namespace to the top of the DiskSpace.cs file. Now when the ellipsis box of
the Disk property is clicked, the dialog will be shown (Figure 8.8). When the dialog is dismissed, the selected value will be written to the property.
Figure 8.8 The disk editor dialog
It may seem a little over the top to have a whole dialog to present this data; the alternative is to use a drop-down list box, which the following class does, and you should add to the DiskEditor.cs file:
// C# public class DiskTypeEditor2 : UITypeEditor { private IWindowsFormsEditorService edSvc; public override object EditValue( ITypeDescriptorContext context, IServiceProvider provider, object value) { edSvc = (IWindowsFormsEditorService) provider.GetService( typeof(IWindowsFormsEditorService)); ListBox cbDisks; cbDisks = new ListBox(); string[] disks = Environment.GetLogicalDrives(); cbDisks.Items.AddRange(disks); cbDisks.Text = (string)cbDisks .Items[cbDisks.FindString((string)value)]; cbDisks.SelectedValueChanged += new EventHandler(TextChanged); edSvc.DropDownControl(cbDisks); return cbDisks.Text; } public override UITypeEditorEditStyle GetEditStyle( ITypeDescriptorContext context) { return UITypeEditorEditStyle.DropDown; } private void TextChanged( object sender, EventArgs e) { if (edSvc != null) edSvc.CloseDropDown(); } }
The GetEditStyle() method of the DiskTypeEditor2 class returns UITypeEditorEditStyle.DropDown to indicate that the Properties window should show the down arrow button. The EditValue() method creates a list box and initializes it with the names of the logical disks. This list box is shown by a call to the blocking method IWindowsFormsEditorService.Drop-DownControl(), and it is removed by a call to CloseDropDown(). The user expects to have drop-down list box behavior; that is, when an item is selected, the drop-down box should be removed. To get this behavior I add a handler to the list box that calls CloseDropDown(), which makes the blocked Drop-DownControl() method return. At this point I can access from the list box control the item that was selected and return it from EditValue().
8.2.3 Licensing
Controls can be licensed; therefore you can add code to check whether the control is being used in a context where it is permitted. The licensing model recognizes two contexts: design time and runtime. Design time is the time when the control is being used in a designer (such as the Windows Forms designer) and as part of other code, such as a form. A developer must have a design-time license to be able to integrate your control into his application. Once the application has been compiled, it will be distributed to users and run, creating a new situation: The licensed control will perform a check for a runtime license when the application is run; if the runtime license is valid, the control can be created.
Having two licenses like this means that you can have a licensing scheme that is more secure for the design time than for the runtime. The licensing is based on a class called a license provider that is called to generate a license when an attempt is made to instantiate the object. Here is a license provider class:
// C# public class LicProvider : LicenseProvider { public override License GetLicense( LicenseContext context, Type type, object instance, bool allowExceptions) { if (context.UsageMode == LicenseUsageMode.Designtime) { if (!CheckForLicense()) { if (!allowExceptions) return null; throw new LicenseException(GetType()); } return new MyLic(type.Name + " design time"); } else return new MyLic(type.Name + " runtime time"); } }
This provider is passed a LicenseContext object that indicates the context in which the license is being requestedeither LicenseUsage-Mode.Designtime or Runtime. Your license provider can then check whether the license is available (as my method CheckForLicense() does)for example, by looking for the location of a valid license file or a registry value. If the check succeeds, a new license can be created. If the license check fails, the license provider should throw a LicenseException exception if allowEx-ceptions is true or just return null if it is false. In this example I have decided that the control should be freely available at runtime, so I don't perform any runtime checks; I merely return the license.
The license object should derive from License and provide implementations of the LicenseKey property and the Dispose() method. In my implementation I simply store a string:
// C# public class MyLic : License { string str; public MyLic(string t){ str = t; } public override string LicenseKey { get { return str; } } public override void Dispose(){} }
The LicenseKey property is not intended to be a secure key. Instead it should be treated as an opaque cookiean encoded string perhapsthat gives access to other data. This string could be stored as a resource in an assembly.
The license provider is associated with the control through the [Li-censeProvider] attribute, and the control should call the LicenseManager object to check that the license is valid:
// C# [ LicenseProvider(typeof(LicProvider))] public class DiskSpace : Control { public DiskSpace() { LicenseManager.Validate( typeof(DiskSpace), this); } // code }
If the control is not licensed, the call will throw an exception. If this happens in the Windows Forms designer, you'll get a message like the one shown in Figure 8.9. If the call to Validate()fails at runtime (a runtime license was not available), a LicenseException exception will be thrown. In the code I show here I do not catch this exception because I want to make sure that if Validate() fails, the control will not load.
Figure 8.9 Error message received if a form opened in the Windows Forms designer has a control that is not licensed
The Framework Class Library comes with one implementation of License called LicFileLicenseProvider. LicFileLicenseProvider will check for the existence of a license file, in much the same way as many ActiveX controls are licensed today.
8.2.4 Toolbox Items
The Toolbox can take any item derived from IComponent. When you add a control to the Toolbox window, the control will be shown with the standard control bitmap image, a cog. To change this image you have to apply the [Tool-boxBitmap] attribute to the control class. The image should be a 16_16 bitmap embedded as part of your assembly, and it should have the same name as your class; for example, if your class is called DiskSpace, the bitmap should be called DiskSpace.bmp. To add the bitmap you use the C# Solution Explorer window's Add New Item on the Add context menu. The bitmap should be an embedded resource (which I'll explain later), so through the bitmap's Properties window you should change its Build Action property to Embedded Resource. Finally, the constructor parameter of the [Toolbox-Bitmap] attribute should take the type of the class to which it is applied:
// C# [ ToolboxBitmap(typeof(DiskSpace))] public class DiskSpace : Control { // code }
For this new bitmap to be shown in the Toolbox window, you will need to remove the old control (from the context menu, select Delete) and then add it again by dragging and dropping it from Windows Explorer to a tab in the Toolbox window.