Creating a User Control

For some designs, deriving from an existing control is insufficient. One common alternative is to create a new control by combining two or more existing controls. This is such a common idiom that Visual Studio .NET provides extensive support for it.

In the next exercise, you'll create a Teeter Totter, as shown in Figure 17-2.

Figure 17-2. Teeter Totter control

In a Teeter Totter, you have a list on the left side (source), and you selectively add from that list to the list on the right side. When you press OK, the list on the right is processed.

This example will not implement all the possible functionality of an industrial-strength Teeter Totter, but it focuses on the key issues in creating user controls: adding the constituent controls, handling internal events, publishing events, and publishing properties.

To get started, create a new Windows Control Library, shown in Figure 17-3, using your preference of VB.NET or C#.

Figure 17-3. Creating the Windows control library

Windows will create a UserControl and provide a UserControl to add the constituent controls. Click on the form, and change the name of the control from UserControl1 to TeeterTotter. Click on the source code file in the Solution Explorer, and change the name of the file from UserControl1.vb (or UserControl1.cs) to TeeterTotter.vb/cs.

Make the form larger, and add the controls as shown in Table 17-1.

Table 17-1. Controls for Teeter Totter user control

Control name

Type

Control text

lbSource

ListBox

 

lbDestination

ListBox

 

btnAdd

Button

Add >

btnRemove

Button

< Remove

btnAddAll

Button

Add All >>

btnRemoveAll

Button

<< Remove All

btnOK

Button

OK

Resize the form to fit all the controls, as shown in Figure 17-4.

Figure 17-4. Designing the Teeter Totter

The complete code is shown in Example 17-3 (in C#) and in Example 17-4 (in VB.NET), and is analyzed in the following pages.

Example 17-3. Code for Teeter Totter control (C#)

using System; using System.Collections; using System.ComponentModel; using System.Drawing; using System.Data; using System.Windows.Forms; namespace TeeterTotter { // Specialized EventArgs class to provide // the items collection of the destination list box public class TeeterTotterEventArgs : EventArgs { private ListBox.ObjectCollection items; public TeeterTotterEventArgs (ListBox.ObjectCollection items) { this.items = items; } // cleaner than just making items public public ListBox.ObjectCollection Items { get { return items; } } } // the teeter totter user control public class TeeterTotter : System.Windows.Forms.UserControl { // the constituent controls private System.Windows.Forms.Button btnAdd; private System.Windows.Forms.Button btnRemove; private System.Windows.Forms.Button btnAddAll; private System.Windows.Forms.Button btnRemoveAll; private System.Windows.Forms.Button btnOK; private System.Windows.Forms.ListBox lbSource; private System.Windows.Forms.ListBox lbDestination; // private variables to hold the color of the // buttons and list boxes private Color btnColor = Color.Beige; private Color lbColor = Color.PapayaWhip; // declare a delegate and an event // fired when an item is added to lbDestination public delegate void OKButtonHandler( object sender, TeeterTotterEventArgs e); public event OKButtonHandler OKButtonEvent; private System.ComponentModel.Container components = null; // constructor sets the colors public TeeterTotter( ) { InitializeComponent( ); SetColors( ); } // helper method to factor out setting colors private void SetColors( ) { btnAdd.BackColor = btnColor; btnRemove.BackColor = btnColor; btnAddAll.BackColor = btnColor; btnRemoveAll.BackColor = btnColor; lbSource.BackColor = lbColor; lbDestination.BackColor = lbColor; } // raise the event protected virtual void OnOK(TeeterTotterEventArgs e) { if ( OKButtonEvent != null ) OKButtonEvent(this,e); } // add items from the client to the source list box public void AddSource(string s) { lbSource.Items.Add(s); } // properties for button and listbox background color public Color BtnBackColor { get { return btnColor; } set { btnColor = value; SetColors( ); } } public Color ListBoxBackColor { get { return lbColor; } set { lbColor = value; SetColors( ); } } ///

/// Clean up any resources being used. ///

protected override void Dispose( bool disposing ) { if( disposing ) { if( components != null ) components.Dispose( ); } base.Dispose( disposing ); } #region Component Designer generated code #endregion // internal event handlers private void btnRemove_Click(object sender, System.EventArgs e) { lbDestination.Items.Remove(lbDestination.SelectedItem); } private void btnAdd_Click(object sender, System.EventArgs e) { lbDestination.Items.Add(lbSource.SelectedItem); } private void btnAddAll_Click(object sender, System.EventArgs e) { lbDestination.Items.Clear( ); foreach (object o in lbSource.Items) { lbDestination.Items.Add(o); } } private void btnRemoveAll_Click( object sender, System.EventArgs e) { lbDestination.Items.Clear( ); } // they clicked ok, raise the event private void btnOK_Click(object sender, System.EventArgs e) { TeeterTotterEventArgs tte = new TeeterTotterEventArgs(lbDestination.Items); OnOK(tte); } } }

Example 17-4. Code for Teeter Totter control (VB.NET)

' Specialized EventArgs class to provide ' the items collection of the destination list box Public Class TeeterTotterEventArgs Inherits EventArgs Private _items As ListBox.ObjectCollection Public Sub New(ByVal items As ListBox.ObjectCollection) _items = items End Sub ' cleaner than making _items public ReadOnly Property Item( ) As ListBox.ObjectCollection Get Return _items End Get End Property End Class ' TeeterTotterEventArgs ' the teeter totter control Public Class TeeterTotter Inherits System.Windows.Forms.UserControl ' declare the delegate and event for handling the ' okay button event Public Delegate Sub OKButtonHandler( _ ByVal sender As Object, ByVal e As TeeterTotterEventArgs) Public Event OKButtonEvent As OKButtonHandler ' private variables to hold the color for the buttons and ' list boxes Private btnColor As Color = Color.Beige Private lbColor As Color = Color.PapayaWhip Public Sub New( ) MyBase.New( ) 'This call is required by the Windows Form Designer. InitializeComponent( ) SetColors( ) 'Add any initialization after the InitializeComponent( ) call End Sub #Region " Windows Form Designer generated code " #End Region ' internal event handlers Public Sub AddSource(ByVal s As String) lbSource.Items.Add(s) End Sub Private Sub btnAdd_Click( _ ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles btnAdd.Click lbDestination.Items.Add(lbSource.SelectedItem) End Sub Private Sub btnRemove_Click( _ ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles btnRemove.Click lbDestination.Items.Remove(lbDestination.SelectedItem) End Sub Private Sub btnAddAll_Click( _ ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles btnAddAll.Click lbDestination.Items.Clear( ) Dim o As Object For Each o In lbSource.Items lbDestination.Items.Add(o) Next End Sub Private Sub btnRemoveAll_Click( _ ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles btnRemoveAll.Click lbDestination.Items.Clear( ) End Sub ' helper method to factor out setting the colors Private Sub SetColors( ) btnAdd.BackColor = btnColor btnRemove.BackColor = btnColor btnAddAll.BackColor = btnColor btnRemoveAll.BackColor = btnColor lbSource.BackColor = lbColor lbDestination.BackColor = lbColor End Sub ' properties for setting the background colors ' for the buttons and list boxes Public Property BtnBackColor( ) As Color Get Return btnColor End Get Set(ByVal Value As Color) btnColor = Value SetColors( ) End Set End Property Public Property ListBoxBackColor( ) As Color Get Return lbColor End Get Set(ByVal Value As Color) lbColor = Value SetColors( ) End Set End Property ' handle the click of the ok button Private Sub btnOK_Click( _ ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles btnOK.Click Dim tte As New TeeterTotterEventArgs(lbDestination.Items) OnOK(tte) End Sub ' raise the event Public Overridable Sub OnOK(ByVal e As TeeterTotterEventArgs) RaiseEvent OKButtonEvent(Me, e) End Sub End Class

17.2.1 Teeter Totter Analysis

There is a great deal of code here, but none of it is terribly complex. The code's key aspects are as follows:

17.2.2 Properties

You'll provide your Teeter Totter with a couple of properties to show that the client code can set properties on the new user control. In this example, you'll provide a BtnBackColor property that will get or set the background color of the buttons, and a ListBoxBackColor property that will get or set the background color of the listboxes.

To facilitate this, first create private member variables to hold the respective colors:

private Color btnColor = Color.Beige; private Color lbColor = Color.PapayaWhip;

Private btnColor As Color = Color.Beige Private lbColor As Color = Color.PapayaWhip

Set the colors for the buttons and the listbox when you first start the application; again, each time a property is set. To manage this, factor out this code into a method:

private void SetColors( ) { btnAdd.BackColor = btnColor; btnRemove.BackColor = btnColor; btnAddAll.BackColor = btnColor; btnRemoveAll.BackColor = btnColor; lbSource.BackColor = lbColor; lbDestination.BackColor = lbColor; }

Private Sub SetColors( ) btnAdd.BackColor = btnColor btnRemove.BackColor = btnColor btnAddAll.BackColor = btnColor btnRemoveAll.BackColor = btnColor lbSource.BackColor = lbColor lbDestination.BackColor = lbColor End Sub

Modify the constructor to invoke the SetColors method when the control is initialized:

public TeeterTotter( ) { InitializeComponent( ); SetColors( ); }

Public Sub New( ) MyBase.New( ) InitializeComponent( ) SetColors( ) End Sub

You are now ready to implement the properties. The only tricky part is that the set accessor must not only set the color, but also invoke SetColors( ):

public Color BtnBackColor { get { return btnColor; } set { btnColor = value; SetColors( ); } } public Color ListBoxBackColor { get { return lbColor; } set { lbColor = value; SetColors( ); } }

Public Property BtnBackColor( ) As Color Get Return btnColor End Get Set(ByVal Value As Color) btnColor = Value SetColors( ) End Set End Property Public Property ListBoxBackColor( ) As Color Get Return lbColor End Get Set(ByVal Value As Color) lbColor = Value SetColors( ) End Set End Property

17.2.3 Handling Internal Events

Many events handled by the Teeter Totter will not be made available to the client; they will be handled internally by the Teeter Totter itself. For example, if the user clicks on Add, Remove, Add All, or Remove All, you will not raise an event to the client application; instead, the Teeter Totter will take the appropriate action.

In some Teeter Totters, when an item is added to the destination list, it is removed from the source list. You may want to make this behavior depend on a property of the Teeter Totter. This is, as they say, left as an exercise for the reader.

As a further enhancement, you may want to add the ability to doubleclick on an item in the source list to add that item or in the destination list to remove that item, or implement drag-and-drop.

Implement custom control event handlers just as you would any other event handler. Visual Studio .NET makes it simple; double-click on the buttons and Visual Studio .NET will create the event handlers; you only have to fill in the event-handling code.

The Add button simply adds the selected item from the source list to the destination list:

private void btnAdd_Click(object sender, System.EventArgs e) { lbDestination.Items.Add(lbSource.SelectedItem); }

Private Sub btnAdd_Click( _ ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles btnAdd.Click lbDestination.Items.Add(lbSource.SelectedItem) End Sub

The Remove button removes the currently selected item from the destination list.

private void btnRemove_Click(object sender, System.EventArgs e) { lbDestination.Items.Remove(lbDestination.SelectedItem); }

Private Sub btnRemove_Click( _ ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles btnRemove.Click lbDestination.Items.Remove(lbDestination.SelectedItem) End Sub

The Add All button's event handler iterates through the items list in the source and adds each one to the destination list. To avoid the problem of duplicating items that were already in the destination list, clear the destination list first:

private void btnAddAll_Click(object sender, System.EventArgs e) { lbDestination.Items.Clear( ); foreach (object o in lbSource.Items) { lbDestination.Items.Add(o); } }

Private Sub btnAddAll_Click( _ ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles btnAddAll.Click lbDestination.Items.Clear( ) Dim o As Object For Each o In lbSource.Items lbDestination.Items.Add(o) Next End Sub

Finally, the Remove All button clears the destination listbox:

private void btnRemoveAll_Click(object sender, System.EventArgs e) { lbDestination.Items.Clear( ); }

Private Sub btnRemoveAll_Click( _ ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles btnRemoveAll.Click lbDestination.Items.Clear( ) End Sub

17.2.4 Publishing Events

Some events, however, should be made available to the client application. In this example, you will add an OK button to the Teeter Totter. When the user clicks this button, the Teeter Totter will raise the OKButtonEvent event, giving the client application an opportunity to process the items in the destination list. To facilitate this process, you'll create a custom EventArgs type named TeeterTotterEventArgs, which will provide a collection of all the strings in the destination listbox.

public class TeeterTotterEventArgs : EventArgs { private ListBox.ObjectCollection items; public TeeterTotterEventArgs (ListBox.ObjectCollection items) { this.items = items; } public ListBox.ObjectCollection Items { get { return items; } } }

Public Class TeeterTotterEventArgs Inherits EventArgs Private _items As ListBox.ObjectCollection Public Sub New(ByVal items As ListBox.ObjectCollection) _items = items End Sub ReadOnly Property Item( ) As ListBox.ObjectCollection Get Return _items End Get End Property End Class

The TeeterTotterEventArgs class has only one member: an object of type ListBox.ObjectCollection; which is the type returned by the List Box's items property. Create a TeeterTotterEventArgs by passing in to the TeeterTotterEventArgs constructor an object of type ListBox.ObjectCollection, which is stashed in the private member variable. The TeeterTotterEventArgs class also provides a public read-only property to return this value on demand.

When the user clicks OK, you'll call OnOK, passing in a TeeterTotterEventArgs object that was initialized with the items collection from the destination listbox:

private void btnOK_Click(object sender, System.EventArgs e) { TeeterTotterEventArgs tte = new TeeterTotterEventArgs(lbDestination.Items); OnOK(tte); }

Private Sub btnOK_Click( _ ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles btnOK.Click Dim tte As New TeeterTotterEVentArgs(lbDestination.Items) OnOK(tte) End Sub

The OnOK method takes a TeeterTotterEventArgs object as a parameter, and then raises the OKButtonEvent, passing in a reference to the Teeter Totter itself, along with the TeeterTotterEventArgs object:

protected virtual void OnOK(TeeterTotterEventArgs e) { if ( OKButtonEvent != null ) OKButtonEvent(this,e); }

Public Overridable Sub OnOK(ByVal e As TeeterTotterEVentArgs) RaiseEvent OKButtonEvent(Me, e) End Sub

To make this work, first create the appropriate delegate for the Event Handler.

public delegate void OKButtonHandler( object sender, TeeterTotterEventArgs e); public event OKButtonHandler OKButtonEvent;

Public Delegate Sub OKButtonHandler( _ ByVal sender As Object, ByVal e As TeeterTotterEVentArgs)

Then create an instance of the delegate:

Public Event OKButtonEvent As OKButtonHandler

The net effect is that when the user clicks the OK button, the client receives an event, with a TeeterTotterEventArgs object that contains the items collection from the destination listbox.

To see how this works, you'll need to create the client.

17.2.5 Testing the User Control

Your test program will be very simple: just a Windows Form on which you will place a Teeter Totter and a single button to trigger code to set the properties, as shown in Figure 17-5.

Figure 17-5. Teeter Totter tester

When you click the Modify Colors button, the colors of the buttons and the listbox will change, as shown in Figure 17-6.

Figure 17-6. Changing the colors

When you click OK on the control, the test form will catch the event, uncork the TeeterTotterEventArgs object, and take out the list of strings, which it will then display in a MessageBox, as shown in Figure 17-7.

Figure 17-7. Clicking OK

17.2.5.1 Creating the Teeter Totter tester project

To implement the test program shown here, right-click on the solution in the Solution Explorer, and add a new project. Choose the appropriate language and name for your project.

To make this interesting, you might choose to implement your tester program in a different language from the language in which you created the control.

Make your new project the startup project by right-clicking on the project in the Solution Explorer and choosing Set as Startup Project, as shown in Figure 17-8. This makes the tester project the project that is started when you press F5 or Ctrl-F5.

Figure 17-8. Setting the startup project

You must now add a reference to the Teeter Totter control to your tester project by right-clicking on the References item for the Tester project in the Solutions Explorer and choosing Add Reference, as shown in Figure 17-9.

Figure 17-9. Adding a reference

Within the Add Reference dialog, click on Projects. You will see the project, which you can double-click to add it to the list of selected components, as shown in Figure 17-10.

Figure 17-10. Adding the project

This will also add the Teeter Totter to your toolbox. Drag a Teeter Totter from the toolbox onto the form, and resize the form to fit. Rename the Teeter Totter from TeeterTotter1 to teeter.

While you are at it, drag a button onto the form, name it btnColors, set its BackColor property to a pleasant shade of orange, and set its text to Modify Colors. Your form should now look like the form shown in Figure 17-11.

Figure 17-11. The test form

17.2.5.2 Interacting with properties

The Teeter Totter exposes the BtnBackColor and ListBoxBackColor properties. You'll test interacting with these properties from your test program by setting the properties in response to a click on the ModifyColors button.

To make this work, implement a click-event handler for the button. The easiest way to do so is by double-clicking on the button in the Visual Studio .NET designer window. Visual Studio .NET will create the event handler for you and place your cursor in the event handler. Fill in the handler as follows:

private void btnColors_Click(object sender, System.EventArgs e) { if ( teeter.BtnBackColor != Color.Red ) teeter.BtnBackColor = Color.Red; else teeter.BtnBackColor = Color.Beige; if ( teeter.ListBoxBackColor != Color.PapayaWhip ) teeter.ListBoxBackColor = Color.PapayaWhip; else teeter.ListBoxBackColor = Color.Aquamarine; }

Private Sub btnColors_Click( _ ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles btnColors.Click If Not teeter.BtnBackColor.Equals(Color.Red) Then teeter.BtnBackColor = Color.Red Else teeter.BtnBackColor = Color.Beige End If If Not teeter.ListBoxBackColor.Equals(Color.PapayaWhip) Then teeter.ListBoxBackColor = Color.PapayaWhip Else teeter.ListBoxBackColor = Color.Aquamarine End If End Sub

Begin by testing whether the current value for the BtnBackColor property is equal to Color.Red. This test calls the get-accessor for the property. If the color does not match, then set the color (calling the set-accessor). Repeat the process for the ListBoxBackColor.

In production code, you probably would not hardcode color values, but would invoke a color picker dialog. This code was simplified because you need only to test the property, not demonstrate picking colors.

 

17.2.5.3 Handling events

Most events raised by the buttons in the Teeter Totter are handled by the Teeter Totter itself. For example, if the user clicks on Add, the Teeter Totter's internal code copies the highlighted value from the source listbox to the destination listbox (as shown earlier in Section 17.2.3).

Remember, though, that the Teeter Totter does publish one event: OKButtonEvent. Your test program will want to respond to this event. This is accomplished differently in Visual Studio .NET for C# programs than for VB.NET programs. Let's discuss them one at a time.

17.2.5.4 Handling the event in C#

In your C# application, you click on the Teeter Totter within the form, and then click on the Events property button (the yellow lightening bolt in the Properties window). The Properties window shows all the possible events supported by the Teeter Totter control, including the OKButtonEvent, as shown circled in Figure 17-12.

Figure 17-12. Clicking on the events button

Double-click on that event, and Visual Studio .NET will set up an event handler for you. You can then fill in the following code:

private void teeter_OKButtonEvent(object sender, TeeterTotter.TeeterTotterEventArgs e) { StringBuilder sb = new StringBuilder( ); foreach (string s in e.Items) { sb.Append(s); sb.Append(" "); } MessageBox.Show(sb.ToString( )); }

For this test program, you'll assemble a string (using the StringBuilder class) based on the string items in the TeeterTotterEventArgs object passed in to the event handler. (Notice that Visual Studio .NET was able to set the method with the correct argument type defined in the control itself.) Then display that string in a message box to validate that you've responded to the event and extracted the relevant information from the TeeterTotterEventArgs object.

17.2.5.5 Handling the event In Visual Basic .NET

To create the event handler in VB.NET, go to the code-editing window and use the class-name drop-down menu to locate the Teeter Totter class. The Declarations drop-down menu will then contain the list of events for the Teeter Totter class, and you can choose OKButtonEvent, as shown in Figure 17-13.

Figure 17-13. Setting the event handler in VB.NET

Choosing this event will cause VB.NET to create the event handler for you, and you can fill in the code, as shown here:

Private Sub teeter_OKButtonEvent( _ ByVal sender As Object, _ ByVal e As TeeterTotterVB.TeeterTotterEventArgs) _ Handles teeter.OKButtonEvent Dim sb As New StringBuilder( ) Dim s As String For Each s In e.Item sb.Append(s) sb.Append(" ") Next MessageBox.Show(sb.ToString( )) End Sub

For this test program, as in the C# example, you'll assemble a string (using the StringBuilder class) based on the string items in the TeeterTotterEventArgs object passed in to the event handler. (Notice that Visual Studio .NET was able to set the method with the correct argument type defined in the control itself.) You then display that string in a message box to validate that you've responded to the event and extracted the relevant information from the TeeterTotterEventArgs object.

You'll need to add an imports (using) statement for System.Text to use StringBuilder.

 

17.2.5.6 Populating the listbox

Before you can test the program, you must populate the source listbox with test strings. You'll do so in the constructor for the TeeterTotterTester form, using the AddSource public method of the Teeter Totter:

public Form1( ) { InitializeComponent( ); teeter.AddSource("One"); teeter.AddSource("Two"); teeter.AddSource("Three"); teeter.AddSource("Four"); }

Public Sub New( ) MyBase.New( ) InitializeComponent( ) teeter.AddSource("One") teeter.AddSource("Two") teeter.AddSource("Three") teeter.AddSource("Four") End Sub

You may now start the test program by pressing F5. Highlight a string (e.g., "Two") in the source box and press Addit should be added to the destination. Add and remove a few strings, and click OKyou should see the strings in the destination box shown in the message box. Before you exit, be sure to click Modify Colors a few times to see the properties at work.

Категории