Professional VB 2005 with .NET 3.0 (Programmer to Programmer)

With this background on the options for creating custom controls, the next step is to look in depth at the procedures used for their development. First up is creating a custom control by inheriting from an existing control and extending it with new functionality. This is the simplest method for the creation of new controls, and the best way to introduce generic techniques that apply to all new controls.

After looking at the general steps needed to create a custom control via inheritance, an example illustrates the details. It is important to understand that many of the techniques described for working with a control created through inheritance also apply to the other ways that a control can be created. Whether inheriting from the Control class, the UserControl class, or from an existing control, a control is a .NET class. Creating properties, methods, and events, and coordinating these members with the Visual Studio designers, is done in a similar fashion, regardless of the starting point.

Overview of the Process

Here are the general stages involved in creating a custom control via inheritance from an existing control. This is not a step-by-step recipe, just an overview. An example follows that goes into more detail on specific steps, but those steps follow these following stages:

  1. Create or open a Windows Control Library project and add a new Custom Control to the project. The class that is created will inherit from the System.Windows.Forms.Control base class. The line that specifies the inherited class must be changed to inherit from the control that is being used as the starting point.

  2. The class file then gets new logic added as necessary to add new functionality. Then the project is compiled with a Build operation in order to create a DLL containing the new control’s code.

  3. The control is now ready to be used. It can be placed in the Windows Forms Toolbox with the Choose Items option in Visual Studio 2005. From that point forward, it can be dragged onto forms like any other control.

Stage 2, of course, is where the effort lies. New logic for the custom control may include new properties, methods, and events. It may also include intercepting events for the base control and taking special actions as necessary. These tasks are done with standard .NET coding techniques.

Several coding techniques are specific to developing Windows Forms controls, such as the use of particular .NET attributes. While our example includes adding routine properties and events, we will focus on these special techniques for programming controls.

Writing Code for an Inherited Control

This section discusses how to place new logic in an inherited control, with special emphasis on techniques that go beyond basic object orientation. A detailed example using the techniques follows this section.

Creating a Property for a Custom Control

Creating a property for a custom control is just like creating a property for any other class. It is necessary to write a property procedure, and to store the value for the property somewhere, most often in a module-level variable, which is often called a backing field.

Properties typically need a default value - that is, a value the property takes on automatically when a control is instantiated. Typically, this means setting the backing field that holds the property value to some initial value. That can be done when the backing field is declared, or it can be done in the constructor for the control.

Here’s the code for a typical simple property for a custom control:

Dim _nMaxItemsSelected As Integer = 10 Public Property MaxItemsSelected() As Integer Get Return _nMaxItemsSelected End Get Set(ByVal Value As Integer) If Value < 0 Then Throw New ArgumentException("Property value cannot be negative") Else _nMaxItemsSelected = Value End If End Set End Property

Once a property is created for a control, it automatically shows up in the Properties window for the control. However, you can use some additional capabilities to make the property work better with the designers and the Properties window in Visual Studio.

Coordinating with the Visual Studio IDE

Controls are normally dragged onto a visual design surface, which is managed by the Visual Studio IDE. For your control to work effectively with the IDE, it must be able to tell the IDE the default value of its properties. The IDE needs the default value of a property for two important capabilities:

There are two ways for your control to work with the IDE to accomplish these tasks. For properties that take simple values, such as integers, Booleans, floating-point numbers, or strings, .NET provides an attribute. For properties that take complex types, such as structures, enumerated types, or object references, two methods need to be implemented.

Attributes

You can learn more about attributes in Chapter 5, but a short summary of important points is included here. Attributes reside in namespaces, just as components do. The attributes used in this chapter are in the System.ComponentModel namespace. To use attributes, the project must have a reference to the assembly containing the namespace for the attributes. For System.ComponentModel, that’s no problem - the project automatically has the reference.

However, the project will not automatically have an Imports for that namespace. Attributes could be referred to with a full type name, but that’s a bit clumsy. To make it easy to refer to the attributes in code, put the following line at the beginning of all modules that will need to use the attributes discussed in this chapter:

Imports System.ComponentModel

That way, an attribute can be referred to with just its name. For example, the DefaultValue attribute, discussed in detail below, can be declared like this:

< DefaultValue(4)> Public Property MyProperty() As Integer

Tip 

All the examples in this chapter assume that the Imports statement has been placed at the top of the class, so all attributes are referenced by their short name. If you get a compile error on an attribute, it’s likely that you’ve omitted that line.

An attribute for a property must be on the same line of code as the property declaration. Of course, line continuation characters can be used so that an attribute is on a separate physical line but still on the same logical line in the program. For example, the last example could also be written as

< DefaultValue(4)> _ Public Property MyProperty() As Integer

Setting a Default Value with an Attribute

The .NET Framework contains many attributes. Most are used to tag classes, properties, and methods with metadata - that is, information that some other entity, such as a compiler or the Visual Studio IDE, might need to know.

For example, the DefaultValue attribute tells the Visual Studio IDE the default value of a property. Let’s change the preceding code for a simple property to include a DefaultValue attribute. Here are the first few lines, showing the change to the property declaration that applies the attribute:

Dim mnMaxItemsSelected As Integer = 10 <DefaultValue(10)> Public Property MaxItemsSelected() As Integer Get Return mnMaxItemsSelected ...

Including the DefaultValue attribute allows the Properties window to reset the value of the property back to the default value. That is, if you right-click the property in the Properties window and select Reset from the pop-up context menu, the value of the property returns to 10 from any other value to which it happens to be set.

Another effect of the attribute can be seen in the code generated by the visual designer. If the preceding property is set to any value that is not the default, a line of code appears in the designer-generated code to set the property value. This is called serializing the property.

That is, if the value of MaxItemsSelected is set to 5, then a line of code something like this appears in the designer-generated code:

MyControl.MaxItemsSelected = 5

If the property has the default value of 10 (because it was never changed, or because it was reset to 10), then the line to set the property value is not present in the designer-generated code. That is, the property does not need to be serialized in code if the value is at the default.

Tip 

To see serialized code, you need to look in the partial class that holds the Windows Forms designer-generated code. This partial class is not visible in the Solution Explorer by default. To see it, you need to press the Show All Files button in the Solution Explorer.

Alternate Techniques for Working with the IDE

The last sample property returned an Integer. Some custom properties return more complex types, such as structures, enumerated types, or object references. These properties cannot use a simple DefaultValue attribute to take care of resetting and serializing the property. An alternate technique is needed.

For complex types, designers check to see whether a property needs to be serialized by using a method on the control containing the property. The method returns a Boolean value that indicates whether a property needs to be serialized (True if it does, False if it does not).

For the following examples, suppose a control has a property named MyColor, which is of type Color. The Color type is a structure in Windows Forms, so the normal DefaultValue attribute can’t be used with it. Further suppose the backing variable for the property is named _MyColor.

In this case, the method to check serialization would be called ShouldSerializeMyColor. It would typically look something like the following code:

Public Function ShouldSerializeMyColor() As Color If Color.Equals(_MyColor, Color.Red) Then Return False Else Return True End If End Function

This is a good example of why a DefaultValue attribute can’t work for all types. There is no equality operator for the Color type, so you have to write appropriate code to perform the check to see whether the current value of the MyColor property is the default. In this case, that’s done with the Equals method of the Color type.

If a property in a custom control does not have a related ShouldSerializeXXX method or a DefaultValue attribute, then the property is always serialized. Code for setting the property’s value will always be included by the designer in the generated code for a form, so it’s a good idea to always include either a ShouldSerializeXXX method or a DefaultValue attribute for every new property created for a control.

Providing a Reset Method for a Control Property

The ShouldSerialize method only takes care of telling the IDE whether to serialize the property value. Properties that require a ShouldSerialize method also need a way to reset a property’s value to the default. This is done by providing a special reset method. In the case of the MyColor property, the reset method is named ResetMyColor. It would look something like the following:

Public Sub ResetMyColor() _MyColor = Color.Red End Sub

Other Useful Attributes

DefaultValue is not the only attribute that is useful for properties. The Description attribute is also one that should be used consistently. It contains a text description of the property, and that description shows up at the bottom of the Properties windows when a property is selected. To include a Description attribute, the declaration of the preceding property would appear as follows:

<DefaultValue(100), _ Description("This is a pithy description of my property")> _ Public Property MyProperty() As Integer

Such a property will look like Figure 16-1 when highlighted in the Properties window.

Figure 16-1

Another attribute you will sometimes need is the Browsable attribute. As mentioned earlier, a new property appears in the Properties window automatically. In some cases, you may need to create a property for a control that you do not want to show up in the Properties window. In that case, you use a Browsable attribute set to False. Here is code similar to the last one, making a property nonbrowsable in the Properties window:

<Browsable(False)> _ Public Property MyProperty() As Integer

One additional attribute you may want to use regularly is the Category attribute. Properties can be grouped by category in the Properties window by pressing a button at the top of the window. Standard categories include Behavior, Appearance, etc. You can have your property appear in any of those categories, or you can make up a new category of your own. To assign a category to a property, use code like this:

<Category("Appearance")> _ Public Property MyProperty() As Integer

There are other attributes for control properties that are useful in specific circumstances. If you understand how the common ones discussed here are used, then you can investigate additional attributes for other purposes in the documentation.

Defining a Custom Event for the Inherited Control

Events in .NET are covered in Chapters 2 and 3. In summary, for controls, the process for creating and handling an event includes these steps:

As the preceding example shows, the standard convention in .NET is to use two arguments for an event - Sender, which is the object raising the event, and e, which is an object of type EventArgs or a type that inherits from EventArgs. This is not a requirement of the syntax (you can actually use any arguments you like when you declare your event), but it’s a consistent convention throughout the .NET Framework, so it is used in this chapter. It is suggested that you follow this convention as well, because it will make your controls consistent with the built-in controls in their operation.

The following example illustrates the concepts discussed. In this example, you create a new control that contains a custom property and a custom event. The property uses several of the attributes discussed.

A CheckedListBox Limiting Number of Selected Items

This example inherits the built-in CheckedListBox control and extends its functionality. If you are not familiar with this control, it works just like a normal ListBox control except that selected items are indicated with a check in a check box at the front of the item, rather than by highlighting the item.

To extend the functionality of this control, the example includes the creation of a property called MaxItemsToSelect. This property holds a maximum value for the number of items that a user can select. The event that fires when a user checks an item is then monitored to determine whether the maximum has already been reached.

If selection of another item would exceed the maximum number, then the selection is prevented, and an event is fired to let the consumer form know that the user has tried to exceed the maximum limit. The code that handles the event in the form can then do whatever is appropriate. In this case, a message box is used to tell the user that no more items can be selected.

The DefaultValue, Description, and Category attributes are placed on the MaxItemsToSelect property to coordinate with the IDE.

Here is the step-by-step construction of our example:

  1. Start a new Windows Control Library project in Visual Studio. Give it the name MyControls. In the Solution Explorer, select the UserControl1.vb file, right-click it, and delete it.

  2. Select Project Add New Item, and select the item template called Custom Control. Name the item LimitedCheckedListBox.

  3. Click the button in the Solution Explorer to show all files for the project. Bring up the file LimitedCheckedListBox.Designer.vb, which is found by clicking the plus sign next to LimitedCheckedListBox.vb.

  4. At the top of the LimitedCheckedListbox.Designer.vb code, look for the line that reads Inherits System.Windows.Forms.Control.

  5. Change that line to read Inherits System.Windows.Forms.CheckedListbox.

  6. Add the following declarations at the top of the code (before the line declaring the class):

    Imports System.ComponentModel

    This enables you to utilize the attributes required from the System.ComponentModel namespace.

  7. The code for LimitedCheckedListBox.vb will contain an event for painting the control. Since you are not doing a control that draws its own surface, delete that event. (It won’t hurt to leave it, but you don’t need it.)

  8. Now begin adding code specifically for this control. First, you need to implement the MaxItemsToSelect property. A module-level variable is needed to hold the property’s value, so insert this line just under the class declaration line:

    Private _nMaxItemsToSelect As Integer = 4

  9. Now create the code for the property itself. Insert the following code into the class just above the line that says End Class:

    <DefaultValue(4), Category("Behavior"), _ Description("The maximum number of items allowed to be checked")> _ Public Property MaxItemsToSelect() As Integer Get Return _nMaxItemsToSelect End Get Set(ByVal Value As Integer) If Value < 0 Then Throw New ArgumentException("Property value cannot be negative") Else _nMaxItemsToSelect = Value End If End Set End Property

    This code sets the default value of the MaxItemsToSelect property to 4, and sets a description for the property to be shown in the Properties window when the property is selected there. It also specifies that the property should appear in the Behavior category when properties in the Properties window are sorted by category.

  10. Declare the event that will be fired when a user selects too many items. The event will be named MaxItemsExceeded. Just under the code for Step 9, insert the following line:

    Public Event MaxItemsExceeded(Sender As Object, e As EventArgs)

  11. Insert code into the event routine that fires when the user clicks on an item. For the CheckedListBox base class, this is called the ItemCheck property. Open the left-hand drop-down box in the code window and select the option LimitedCheckedListBox Events. Then, select the ItemCheck event in the right-hand drop-down box of the code window. The following code will be inserted to handle the ItemCheck event:

    Private Sub LimitedCheckedListBox_ItemCheck(ByVal sender As Object, _ ByVal e As System.Windows.Forms.ItemCheckEventArgs) _ Handles MyBase.ItemCheck End Sub

  12. The following code should be added to the ItemCheck event to monitor it for too many items:

    Private Sub LimitedCheckedListBox_ItemCheck(ByVal sender As Object, _ ByVal e As System.Windows.Forms.ItemCheckEventArgs) _ Handles MyBase.ItemCheck If (Me.CheckedItems.Count >= _nMaxItemsToSelect) _ And (e.NewValue = CheckState.Checked) Then RaiseEvent MaxItemsExceeded(Me, New EventArgs) e.NewValue = CheckState.Unchecked End If End Sub

  13. Build the project to create a DLL containing the LimitedCheckedListBox control.

  14. Add a new Windows Application project to the solution (using the File ⇒ Add Project ⇒ New Project menu) to test the control. Name the new project anything you like. Right-click the project in the Solution Explorer, and select Set as Startup Project in the pop-up menu. This will cause your Windows application to run when you press F5 in Visual Studio.

  15. Scroll to the top of the controls in the Toolbox. The LimitedCheckedListBox control should be there.

  16. The Windows Application will have a Form1 that was created automatically. Drag a LimitedCheckedListBox control onto Form1, just as you would a normal list box. Change the CheckOnClick event for the LimitedCheckedListBox to True (to make testing easier). This property was inherited from the base CheckedListBox control.

  17. In the Items property of the LimitedCheckedListBox, click the button to add some items. Insert the following list of colors: Red, Yellow, Green, Brown, Blue, Pink, and Black. At this point, your Windows Application Project should have a Form1 that looks something like Figure 16-2.

    Figure 16-2

  18. Bring up the code window for Form1. In the left-hand drop-down box above the code window, select LimitedCheckedListBox1 to get to its events. Then, in the right-hand drop-down box, select the MaxItemsExceeded event. The empty event will look like the following code:

    Private Sub LimitedCheckedListBox1_MaxItemsExceeded( _ ByVal sender As System.Object, e As System.EventArgs) _ Handles LimitedCheckedListBox1.MaxItemsExceeded End Sub

  19. Insert the following code to handle the event:

    MsgBox("You are attempting to select more than " & _ LimitedCheckedListBox1.MaxItemsToSelect & _ " items. You must uncheck some other item " & _ " before checking this one.")

  20. Start the Windows Application project. Check and uncheck various items in the list box to verify that the control works as intended. You should get a message box whenever you attempt to check more than four items. (Four items is the default maximum, and it was not changed.) If you uncheck some items, then you can check items again until the maximum is once again exceeded. When finished, close the form to stop execution.

  21. If you want to check the serialization of the code, look at the designer-generated code in the partial class for Form1 (named LimitedCheckedListBox.Designer.vb), and examine the properties for LimitedCheckedListBox1. Note how there is no line of code that sets MaxSelectedItems. Remember that if you don’t see the partial class in the Solution Explorer, then you’ll need to press the Show All button at the top of the Solution Explorer.

  22. Go back to the Design mode for Form1 and select LimitedCheckedListBox1. In the Properties window, change the MaxSelectedItems property to 3.

  23. Return to the partial class and look again at the code that declares the properties for LimitedCheckedListBox1. Note that there is now a line of code that sets MaxSelectedItems to the value of 3.

  24. Go back to the Design mode for Form1 and select LimitedCheckedListBox1. In the Properties window, right-click the MaxSelectedItems property. In the pop-up menu, select Reset. The property will change back to a value of 4, and the line of code that sets the property that you looked at in the last step will be gone.

These last few steps showed that the DefaultValue attribute is working as it should.

Категории