Forms


Earlier in this chapter, you learned how to create a simple Windows application. The example contained one class derived from the System.Windows.Forms.Form class. According to the .NET Framework documentation, "a Form is a representation of any window in your application." If you come from a Visual Basic background, the term form will seem familiar. If your background is C++ using MFC, you're probably used to calling a form a window, dialog box, or maybe a frame. Regardless, the form is the basic means of interacting with the user. Earlier, the chapter covered some of the more common and useful properties, methods, and events of the Control class, and because the Form class is a descendant of the Control class, all of the same properties, methods, and events exist in the Form class. The Form class adds considerable functionality to what the Control class provides, and that's what this section discusses.

Form Class

A Windows client application can contain one form or hundreds of forms. They can be an SDI-based (Single Document Interface) or MDI-based (Multiple Document Interface) application. Regardless, the System.Windows.Forms.Form class is the heart of the Windows client. The Form class is derived from ContainerControl, which is derived from ScrollableControl, which is derived from Control. Because of this you can assume that a form is capable of being a container for other controls, capable of scrolling when the contained controls do not fit the client area, and has many of the same properties, methods, and events that other controls have. Because of this, it also makes the Form class rather complex. This section looks at much of that functionality.

Form instantiation and destruction

The process of form creation is important to understand. What you want to do depends on where you write the initialization code. For instantiation, the events occur in the following order:

  • Constructor

  • Load

  • Activated

  • Closing

  • Closed

  • Deactivate

The first three events are of concern during initialization. Depending on what type of initialization you want to do could determine which event you would hook into. The constructor of a class occurs during the object instantiation. The Load event occurs after object instantiation, but just before the form becomes visible. The difference between this and the constructor is the viability of the form. When the Load event is raised, the form exists but isn't visible. During constructor execution, the form is in the process of existing. The Activated event occurs when the form becomes visible and current.

There is a situation where this order can be altered slightly. If during the constructor execution of the form the Visible property is set to true or the Show method is called (which sets the Visible property to true), the Load event fires immediately. Because this also makes the form visible and current, the Activate event is also raised. If there is code after the Visible property has been set, it will execute.

So the startup event might look something like this:

  • Constructor, up to Visible = true

  • Load

  • Activate

  • Constructor, after Visible = true

This could potentially lead to some unexpected results. From a best practices standpoint, it would seem that doing as much initialization as possible in the constructor might be a good idea.

Now what happens when the form is closed? The Closing event gives you the opportunity to cancel the process. The Closing event receives the CancelEventArgs as a parameter. This has a Cancel property that if set to true cancels the event and the form remains open. The Closing event happens as the form is being closed, whereas the Closed event happens after the form has been closed. Both allow you to do any cleanup that might have to be done. Notice that the Deactivate event occurs after the form has been closed. This is another potential source of difficult-to-find bugs. Be sure that you don't have anything in Deactivate that could keep the form from being properly garbage-collected. For example, setting a reference to another object would cause the form to remain alive.

If you call the Application.Exit() method and you have one or more forms currently open, the Closing and Closed events will not be raised. This is an important consideration if you have open files or database connections that you were going to clean up. The Dispose method is called, so perhaps another best practice would be to put most of your cleanup code in the Dispose method.

Some properties that relate to the startup of a form are StartPosition, ShowInTaskbar, and TopMost. StartPosition can be any of the FormStartPosition enumeration values. They are:

  • CenterParent — The form is centered in the client area of the parent form.

  • CenterScreen — The form is centered in the current display.

  • Manual — The form's location is based on the values in the Location property.

  • WindowsDefaultBounds — The form is located at the default Windows position and uses the default size.

  • WindowsDefaultLocation — The Windows default location is used, but the size is based on the Size property.

The ShowInTaskbar property determines if the form should be available in the taskbar. This is only relevant if the form is a child form and you only want the parent form to show in the taskbar. The TopMost property tells the form to start in the topmost position in the Z-order of the application. This is true even of the form does not immediately have focus.

In order for users to interact with the application, they must be able to see the form. The Show and ShowDialog methods accomplish this. The Show method just makes the form visible to the user. The following code segment demonstrates how to create a form and show it to the user. Assume that the form you want to display is called MyFormClass.

 MyFormClass myForm = new MyFormClass();  myForm.Show(); 

That's the simple way. The one drawback to this is that there isn't any notification back to the calling code that myForm is finished and has been exited. Sometimes this isn't a big deal and the Show method will work fine. If you do need some type of notification, the ShowDialog is a better option.

When the Show method is called, the code that follows the Show method is executed immediately. When ShowDialog is called, the calling code is blocked and will wait until the form that ShowDialog called is closed. Not only will the calling code be blocked, but the form will optionally return a DialogResult value. The DialogResult enumeration is a list of identifiers that describe the reason for the dialog being closed. These include OK, Cancel, Yes, No, and several others. In order for the form to return a

DialogResult, the form's DialogResult property must be set or the DialogResult property on one of the form's buttons must be set.

For example, suppose that part of application asks for the phone number of a client. The form has a text box for the phone number and two buttons; one is labeled OK and the other is labeled Cancel. If you set the DialogResult of the OK button to DialogResult.OK and the DialogResult property on the Cancel button to DialogResult.Cancel, then when either of these buttons is selected, the form becomes invisible and returns to the calling form the appropriate DialogResult value. Now notice that the form does not get destroyed; only the Visible property is set to false. That's because you still must get values from the form. For this example, you need to phone number. By creating a property on the form for the phone number, the parent form can now get the value and call the Close method on the form. This is what the code for the child form looks like:

 namespace FormsSample.DialogSample { partial class Phone : Form { public Phone() { InitializeComponent(); btnOK.DialogResult = DialogResult.OK; btnCancel.DialogResult = DialogResult.Cancel; } public string PhoneNumber { get { return textBox1.Text; } set { textBox1.Text = value; } } } } 

The first thing to notice is that there isn't code to handle the click events of the buttons. Because the DialogResult property is set for each of the buttons, the form disappears after either the OK or Cancel button is clicked. The only property added is the PhoneNumber property. The following code shows the method in the parent form that calls the Phone dialog:

 Phone frm = new Phone(); frm.ShowDialog(); if (frm.DialogResult == DialogResult.OK) { label1.Text = "Phone number is " + frm.PhoneNumber; } else if (frm.DialogResult == DialogResult.Cancel) { label1.Text = "Form was canceled."; } frm.Close(); 

This looks simple enough. Create the new Phone object (frm). When the frm.ShowDialog() method is called, the code in this method will stop and wait for the Phone form to return. You can then check the DialogResult property of the Phone form. Because it has not been destroyed yet, just made invisible, you can still access the public properties, one of them being the PhoneNumber property. Once you get the data you need, you can call the Close method on the form.

This works well, but what if the returned phone number is not formatted correctly? If you put the ShowDialog inside of the loop, you can just recall it and have the user re-enter the value. This way, you get a proper value. Remember that you must also handle the DialogResult.Cancel if the user clicks the Cancel button.

 Phone frm = new Phone(); while (true) { frm.ShowDialog(); if (frm.DialogResult == DialogResult.OK) { label1.Text = "Phone number is " + frm.PhoneNumber; if (frm.PhoneNumber.Length == 8 | frm.PhoneNumber.Length == 12) { break; } else { MessageBox.Show("Phone number was not formatted correctly. Please correct entry."); } } else if (frm.DialogResult == DialogResult.Cancel) { label1.Text = "Form was canceled."; break; } } frm.Close(); 

Now if the phone number does not pass a simple test for length, the Phone form appears so the user can correct the error. The ShowDialog box does not create a new instance of the form. Any text entered on the form will still be there, so if the form has to be reset, it will be up to you to do that.

Appearance

The first thing that the user sees is the form for the application. It should be first and foremost functional. If the application doesn't solve a business problem, it really doesn't matter how it looks. This is not to say that the form and application's overall GUI design should not be pleasing to the eye. Simple things like color combinations, font sizing, and window sizing can make an application much easier for the user.

Sometimes you don't want the user to have access to the system menu. This is the menu that appears when you click the icon on the top-left corner of a window. Generally it has items such as Restore, Minimize, Maximize, and Close on it. The ControlBox property allows you to set the visibility of the system menu. You can also set the visibility of the Maximize and Minimize buttons with the

} else { 

MaximizeBox and MinimizeBox properties. If you remove all of the buttons and then set the Text property to an empty string (""), the title bar disappears completely.

If you set the Icon property of a form and you don't set the ControlBox property to false, the icon will appear in the top-left corner of the form. It's common to set this to the app.ico. This makes eachform's icon the same as the application icon.

The FormBorderStyle property sets the type of border that appears around the form. This uses the FormBorderStyle enumeration. The values can be as follows:

  • Fixed3D

  • FixedDialog

  • FixedSingle

  • FixedToolWindow

  • None

  • Sizable

  • SizableToolWindow

Most of these are self-explanatory, with the exception of the two tool window borders. A Tool window will not appear in the taskbar, regardless of how ShowInTaskBar is set. Also a Tool window will not show in the list of windows when the user presses Alt+Tab. The default setting is Sizable.

Unless a requirement dictates otherwise, colors for most GUI elements should be set to system colors and not to specific colors. This way if some users like to have all of their buttons green with purple text, the application will follow along with the same colors. To set a control to use a specific system color, you must call the FromKnownColor method of the System.Drawing.Color class. The FromKnownColor method takes a KnownColor enumeration value. Many colors are defined in the enumeration, as well as the various GUI element colors, such as Control, ActiveBorder, and Desktop. So, for example, if the Background color of the form should always match the Desktop color, the code would look like this:

 myForm.BackColor = Color.FromKnownColor(KnownColor.Desktop); 

Now if users change the color of their desktops, the background of the form changes as well. This is a nice, friendly touch to add to an application. Users might pick out some strange color combinations for their desktops, but it is their choice.

Windows XP introduced a feature called visual styles. Visual styles change the way buttons, text boxes, menus, and other controls look and react when the mouse pointer is either hovering or clicking. You can enable visual styles for your application by calling the Application.EnableVisualStyles method. This method has to be called before any type of GUI is instantiated. Because of this, it is generally called in the Main method, as demonstrated in this example:

 [STAThread] static void Main() { Application.EnableVisualStyles(); Application.Run(new Form1()); } 

This code allows the various controls that support visual styles to take advantage of them. Because of an issue with the EnableVisualStyles method, you might have to add an Application.DoEvents() method right after the call to EnableVisualStyles. This should resolve the problem if icons on toolbars begin to disappear at runtime. Also, EnableVisualStyles is available only in .NET Framework 1.1.

You have to accomplish one more task pertaining to the controls. Most controls expose FlatStyle property that takes a FlatStyle enumeration as its value. This property can take one of four different values:

  • Flat — Similar to flat, except that when the mouse pointer hovers over the control, it appears in 3D.

  • Standard — The control appears in 3D.

  • System — The look of the control is controlled by the operating system.

To enable visual styles, the control's FlatStyle property should be set to FlatStyle.System. The application will now take on the XP look and feel and will support XP themes.

Multiple Document Interface (MDI)

MDI-type applications are used when you have an application that can show either multiple instances of the same type of form or different forms that must be contained in some way. An example of multiple instances of the same type of form is a text editor that can show multiple edit windows at the same time. An example of the second type of application is Microsoft Access. You can have query windows, design windows, and table windows all open at the same time. The windows never leave the boundaries of the main Access application.

The project that contains the examples for this chapter is an example of an MDI application. The form mdiParent in the project is the MDI parent form. Setting the IsMdiContainer to true will make any form an MDI parent form. If you have the form in the designer you'll notice that the background turns a dark gray color. This is to let you know that this is an MDI parent form. You can still add controls to the form, but it is generally not recommended.

For the child forms to behave like MDI children, the child form needs to know what form the parent is. This is done by setting the MdiParent property to the parent form. In the example, all children forms are created using the ShowMdiChild method. It takes a reference to the child form that is to be shown. After setting the MdiParent property to this, which is referencing the mdiParent form, the form is shown. Here is the code for the ShowMdiParent method:

 private void ShowMdiChild(Form childForm) { childForm.MdiParent = this; childForm.Show(); } 

One of the issues with MDI applications is that there may be several child forms open at any given time. A reference to the current active child can be retrieved by using the ActiveMdiChild property on the parent form. This is demonstrated on the Current Active menu choice on the Window menu. This will show a message box with the form's name and text value.

The child forms can be arranged by calling the LayoutMdi method. The LayoutMdi method takes an MdiLayout enumeration value as a parameter. The possible values include Cascade, TileHorizontal, and TileVertical.

Custom Controls

Using controls and components is a big part of what makes developing with a forms package such as Windows Forms so productive. The ability to create your own controls, components, and user controls makes it even more productive. By creating controls, functionality can be encapsulated into packages that can be reused over and over.

You can create a control in a number of ways. You can start from scratch, deriving your class from either Control, ScrollableControl, or ContainerControl. You will have to override the Paint event and do all of your drawing, not to mention adding the functionality that your control is supposed to provide. If the control is supposed to be an enhanced version of a current control, the thing to do is to derive from the control that is being enhanced. For example, if a TextBox control is needed that changes background color if the ReadOnly property is set, creating a completely new TextBox control would be a waste of time. Derive from the TextBox control and override the ReadOnly property. Because the ReadOnly property of the TextBox control is not marked override, you have to use the new clause. The following code shows the new ReadOnly property:

 public new bool ReadOnly { get  { return base.ReadOnly;} set  {  if(value) this.BackgroundColor = Color.Red; else this.BackgroundColor = Color.FromKnowColor(KnownColor.Window); base.ReadOnly = value; } } 

For the property get, you return what the base object is set to. The way that the property handles the process of making a text box read-only is not relevant here, so you just pass that functionality to the base object. In the property set, check to see if the passed-in value is true or false. If it is true, change the color to the read-only color (Red in this case); if it is false, set the BackgroundColor to the default. Finally, pass the value down to the base object so that the text box actually does become read-only. As you can see, by overriding one simple property, you can add new functionality to a control.

Control attributes

You can add attributes to the custom control that will enhance the design-time capabilities of the control. The following table describes some of the more useful attributes.

Attribute Name

Description

BindableAttribute

Used at design time to determine if the property supports two- way data binding.

BrowsableAttribute

Determines if the property is shown in the visual designer.

CategoryAttribute

Determines under what category the property is displayed in the Property window. Use on predefined categories or create new ones. Default is Misc.

DefaultEventAttribute

Specifies the default event for a class.

DefaultPropertyAttribute

Specifies the default property for a class.

DefaultValueAttribute

Specifies the default value for a property. Typically, this is the initial value.

DecriptionAttribute

This is the text that appears at the bottom of the designer win- dow when the property is selected.

DesignOnlyAttribute

This marks the property as being editable in design mode only.

Other attributes are available that relate to the editor that the property uses in design time and other advanced design-time capabilities. The Category and Description attributes should almost always be added. This helps other developers who use the control to better understand the property's purpose. To add IntelliSense support, you should add XML comments for each property, method, and event. When the control is compiled with the /doc option, the XML file of comments that is generated will provide IntelliSense for the control.

TreeView-based custom control

This section shows you how to develop a custom control based on the TreeView control. This control displays the file structure of a drive. You'll add properties that set the base or root folder and determine whether files and folders will be displayed. You also use the various attributes discussed in the previous section.

As with any new project, requirements for the control have to be defined. Here is a list of basic requirements that have to be implemented:

  • Read folders and files and display to user.

  • Display folder structure in a tree-like hierarchical view.

  • Optionally hide files from view.

  • Define what folder should be the base or root folder.

  • Return the currently selected folder.

  • Provide the ability to delay loading of the file structure.

This should be a good starting point. One requirement has been satisfied by the fact the TreeView control will be the base of the new control.

The TreeView control displays data in a hierarchical format. It displays text describing the object in the list and optionally an icon. This list can be expanded and contracted by clicking an object or using the arrows keys.

Create a new Windows Control Library project in Visual Studio .NET named FolderTree, and delete the class UserControl1. Add a new class and call it FolderTree. Because FolderTree will be derived from TreeView, change the class declaration from

 public class FolderTree 

to

 public class FolderTree : System.Windows.Forms.TreeView 

At this point, you actually have a fully functional and working FolderTree control. It will do everything that the TreeView can do, and nothing more.

The TreeView control maintains a collection of TreeNode objects. You can't load files and folders directly into the control. You have a couple of ways to map the TreeNode that is loaded into the Nodes collection of the TreeView and the file or folder that it represents.

For example, when each folder is processed, a new TreeNode object is created, and the text property is set to the name of the file or folder. If at some point additional information about the file or folder is needed, you have to make another trip to the disk to gather that information or store additional data regarding the file or folder in the Tag property.

Another method is to create a new class that is derived from TreeNode. New properties and methods can be added and the base functionality of the TreeNode is still there. This is the path that you use in this example. It allows for a more flexible design. If you need new properties, you can add them easily without breaking the existing code.

You must load two types of objects into the control: folders and files. Each has its own characteristics. For example, folders have a DirectoryInfo object that contains additional information, and files have a FileInfo object. Because of these differences you use two separate classes to load the TreeView control: FileNode and FolderNode. You add these two classes to the project; each is derived from TreeNode. This is the listing for FileNode:

 namespace FormsSample.SampleControls { public class FileNode : System.Windows.Forms.TreeNode { string _fileName = ""; FileInfo _info; public FileNode(string fileName) { _fileName = fileName; _info = new FileInfo(_fileName); base.Text = _info.Name; if (_info.Extension.ToLower() == ".exe") this.ForeColor = System.Drawing.Color.Red; } public string FileName { get { return _fileName; } set { _fileName = value; } } public FileInfo FileNodeInfo { get { return _info; } } } } 

The name of the file being processed is passed into the constructor of FileNode. In the constructor the FileInfo object for the file is created and set to the member variable _info. The base.Text property is set to the name of the file. Because you are deriving from TreeNode, this sets the TreeNode's Text property. This is the text displayed in the TreeView control.

Two properties are added to retrieve the data. FileName returns the name of the file and FileNodeInfo returns the FileInfo object for the file.

Here is the code for the FolderNode class. It is very similar in structure to the FileNode class. The differences are that you have a DirectoryInfo property instead of FileInfo, and instead of FileName you have FolderPath:

 namespace FormsSample.SampleControls { public class FolderNode : System.Windows.Forms.TreeNode { string _folderPath = ""; DirectoryInfo _info; public FolderNode(string folderPath) { _folderPath = folderPath; _info = new DirectoryInfo(folderPath); this.Text = _info.Name; } public string FolderPath { get { return _folderPath; } set { _folderPath = value; } } public DirectoryInfo FolderNodeInfo { get { return _info; } } } } 

Now you can construct the FolderTree control. Based on the requirements, you need a property to read and set the RootFolder. You also need a ShowFiles property for determining if files should be shown in the tree. A SelectedFolder property returns the currently highlighted folder in the tree. This is what the code looks like so far for the FolderTree control:

 using System; using System.Windows.Forms; using System.IO; using System.ComponentModel; namespace FolderTree { /// <summary> /// Summary description for FolderTreeCtrl. /// </summary> public class FolderTree :  System.Windows.Forms.TreeView { string _rootFolder = ""; bool _showFiles = true; bool _inInit = false; public FolderTree() { } [Category("Behavior"), Description("Gets or sets the base or root folder of the tree"), DefaultValue("C:\\")] public string RootFolder { get {return _rootFolder;} set { _rootFolder = value; if(!_inInit) InitializeTree(); } } [Category("Behavior"), Description("Indicates whether files will be seen in the list."), DefaultValue(true)] public bool ShowFiles { get {return _showFiles;} set {_showFiles = value;} } [Browsable(false)] public string SelectedFolder { get { if(this.SelectedNode is FolderNode) return ((FolderNode)this.SelectedNode).FolderPath; return ""; } } } } 

Three properties were added: ShowFiles, SelectedFolder, and RootFolder. Notice the attributes that have been added. You set Category, Description, and DefaultValues for the ShowFiles and RootFolder. These two properties will appear in the property browser in design mode. The SelectedFolder really has no meaning at design time, so you select the Browsable=false attribute. SelectedFolder does not appear in the property browser. However, because it is a public property, it will appear in IntelliSense and is accessible in code.

Next, you have to initialize the loading of the file system. Initializing a control can be tricky. Both design time and runtime initializing must be well thought out. When a control is sitting on a designer, it is actually running. If there is a call to a database in the constructor, for example, this call will execute when you drop the control on the designer. In the case of the FolderTree control, this can be an issue.

Here's a look at the method that is actually going to load the files:

 private void LoadTree(FolderNode folder) { string[] dirs = Directory.GetDirectories(folder.FolderPath); foreach(string dir in dirs) { FolderNode tmpfolder = new FolderNode(dir); folder.Nodes.Add(tmpfolder); LoadTree(tmpfolder); } if(_showFiles) { string[] files = Directory.GetFiles(folder.FolderPath); foreach(string file in files) { FileNode fnode = new FileNode(file); folder.Nodes.Add(fnode); } } } 

showFiles is a Boolean member variable that is set from the ShowFiles property. If true, files are also shown in the tree. The only question now is when LoadTree should be called. You have several options. It can be called when the RootFolder property is set. That is desirable in some situations, but not at design time. Remember that the control is "live" on the designer so when the RootNode property is set, the control will attempt to load the file system.

What you can do to solve this is to check the DesignMode property. This returns true if the control is in the designer. Now you can write the code to initialize the control:

 private void InitializeTree() { if(!this.DesignMode && _rootFolder != "") { FolderNode rootNode = new FolderNode(_rootFolder); LoadTree(rootNode); this.Nodes.Clear(); this.Nodes.Add(rootNode); } } 

If the control is not in design mode and _rootFolder is not an empty string, the loading of the tree will begin. The Root node is created first and this is passed into the LoadTree method.

Another option is to implement a public Init method. In the Init method the call to LoadTree can happen. The problem with this option is that the developer who uses your control is required to make the Init call. Depending on the situation, this might be an acceptable solution.

For added flexibility the ISupportInitialize interface can be implemented. ISupportInitialize has two methods, BeginInit and EndInit. When a control implements ISupportInitialize the BeginInit and EndInit methods are called automatically in the generated code in Initialize Component. This allows the initialization process to be delayed until all of the properties are set. ISupportInitialize allows the code in the parent form to delay initialization as well. If the RootNode property is being set in code, a call to BeginInit first will allow the RootNode property as well as other properties to be set or actions to be performed before the control loads the file system. When EndInit is called, the control initializes. This is what BeginInit and EndInit look like:

 #region ISupportInitialize Members void ISupportInitialize.BeginInit() { _inInit = true; } void ISupportInitialize.EndInit() { if(_rootFolder != "") { InitializeTree(); } _inInit = false; } #endregion 

In the BeginInit method, all that is done is that a member variable _inInit is set to true. This flag is used to determine if the control is in the initialization process and is used in the RootFolder property. If the RootFolder property is set outside of the InitializeComponent class, the tree will need to be reinitialized. In the RootFolder property you check to see if _inInit is true or false. If it is true, then you don't want to go through the initialization process. If inInit is false, you call InitializeTree. You could also have a public Init method and accomplish the same task.

In the EndInit method you check to see if the control is in design mode and if _rootFolder has a valid path assigned to it. Only then is InitializeTree called.

To add a final professional-looking touch, you have to add a bitmap image. This is the icon that shows up in the Toolbox when the control is added to a project. The bitmap image should be 1616 pixels and 16 colors. You can create this image file with any graphics editor as long as the size and color depth are set properly. You can even create this file in Visual Studio .NET: right-click the project and select Add New Item. From the list select Bitmap File to open the graphics editor. After you have created the bitmap file, add it to the project, making sure it is in the same namespace and has the same name as the control. Finally, set the Build Action of the bitmap to Embedded Resource: Right-click the bitmap file in the Solution Explorer and select Properties. Select Embedded Resource form the Build Action property.

To test the control, create a TestHarness project in the same solution. The TestHarness is a simple Windows Forms application with a single form. In the references section add a reference to the FolderTreeCtl project. In the Toolbox window, add a reference to the FolderTreeCtl.DLL. FolderTreeCtl should now show up in the toolbox with the bitmap added as the icon. Click the icon and drag it to the TestHarness form. Set the RootFolder to an available folder and run the solution.

This is by no means a complete control. Several things could be enhanced to make this a full-featured, production-ready control. For example, you could add the following:

  • Exceptions — If the control tries to load a folder that the user does not have access to, an exception is raised.

  • Background loading — Loading a large folder tree can take a long time. Enhancing the initialization process to take advantage of a background thread for loading is a good idea.

  • Color codes — You can make the text of certain file types a different color.

  • Icons — You can add an ImageList control and add an icon to each file or folder as it is loaded.

User control

User controls are one of the more powerful features of Windows Forms. They allow encapsulating user interface designs into nice reusable packages that can be plugged into project after project. It is not uncommon for an organization to have a couple of libraries of frequently used user controls. Not only can user interface functionality be contained in user controls, but common data validation can be incorporated in them as well, such as formatting phone numbers or id numbers. A predefined list of items can be in the user control for fast loading of a list box or combo box. State codes or country codes fit into this category. Incorporating as much functionality that does not depend on the current application as possible into a user control makes the control that much more useful in the organization.

In this section, you create a simple address user control. You also will add the various events that make the control ready for data binding. The address control will have text entry for two address lines, city, state, and zip code.

To create a user control in a current project, just right-click the project in Solution Explorer and select Add, and then select Add New User Control. You can also create a new Control Library project and add user controls to it. After a new user control has been started, you will see a form without any borders on the designer. This is where you drop the controls that make up the user control. Remember that a user control is actually one or more controls added to a container control, so it is somewhat like creating a form. For the address control there are five TextBox controls and three Label controls. The controls can be arranged any way that seems appropriate (see Figure 23-5).

image from book
Figure 23-5

The TextBox controls in this example are named as follows:

  • txtAddress1

  • txtAddress2

  • txtCity

  • txtState

  • txtZip

After the TextBox controls are in place and have valid names, add the public properties. You might be tempted to set the visibility of the TextBox controls to public instead of private. However, this is not a good idea, because it defeats the purpose of encapsulating the functionality that you might want to add to the properties. Here is a listing of the properties that must be added:

 public string AddressLine1 { get{return txtAddress1.Text;} set{ if(txtAddress1.Text != value) { txtAddress1.Text = value; if(AddressLine1Changed != null) AddressLine1Changed(this, PropertyChangedEventArgs.Empty); } } } public string AddressLine2 { get{return txtAddress2.Text;} set{ if(txtAddress2.Text != value) { txtAddress2.Text = value; if(AddressLine2Changed != null) AddressLine2Changed(this, PropertyChangedEventArgs.Empty); } } } public string City { get{return txtCity.Text;} set{ if(txtCity.Text != value) { txtCity.Text = value; if(CityChanged != null) CityChanged(this, PropertyChangedEventArgs.Empty); } } } public string State { get{return txtState.Text;} set{ if(txtState.Text != value) { txtState.Text = value; if(StateChanged != null) StateChanged(this, PropertyChangedEventArgs.Empty); } } } public string Zip { get{return txtZip.Text;} set{ if(txtZip.Text != value) { txtZip.Text = value; if(ZipChanged != null) ZipChanged(this, PropertyChangedEventArgs.Empty); } } } 

The property gets are fairly straightforward. They return the value of the corresponding TextBox control's text property. The property sets, however, are doing a bit more work. All of the property sets work the same way. A check is made to see whether or not the value of the property is actually changing. If the new value is the same as the current value, then a quick escape can be made. If there is a new value sent in, set the text property of the TextBox to the new value and test to see if an event has been instantiated. The event to look for is the changed event for the property. It has a specific naming format, propertynameChanged where propertyname is the name of the property. In the case of the AddressLine1 property, this event is called AddressLine1Changed. The properties are declared as follows:

 public event EventHandler AddressLine1Changed; public event EventHandler AddressLine2Changed; public event EventHandler CityChanged; public event EventHandler StateChanged; public event EventHandler ZipChanged; 

The purpose of the events is to notify binding that the property has changed. Once validation occurs, binding will make sure that the new value makes its way back to the object that the control is bound to. One other step should be done to support binding. A change to the text box by the user will not set the property directly. So the propertynameChanged event must be raised when the text box changes as well. The easiest way to do this is to monitor the TextChanged event of the TextBox control. This example has only one TextChanged event handler and all of the text boxes use it. The control name is checked to see which control raised the event and the appropriate propertynameChanged event is raised. Here is the code for the event handler:

 private void TextBoxControls_TextChanged( object sender, System.EventArgs e) { switch(((TextBox)sender).Name) { case "txtAddress1"  : if(AddressLine1Changed != null) AddressLine1Changed(this, EventArgs.Empty); break; case "txtAddress2" : if(AddressLine2Changed != null) AddressLine2Changed(this, EventArgs.Empty); break; case "txtCity" : if(CityChanged != null) CityChanged(this, EventArgs.Empty); break; case "txtState" : if(StateChanged != null) StateChanged(this, EventArgs.Empty); break; case "txtZip" : if(ZipChanged != null) ZipChanged(this, EventArgs.Empty); break; } } 

This example uses a simple switch statement to determine which text box raised the TextChanged event. Then a check is made to verify that the event is valid and not equal to null. Then the Changed event is raised. One thing to note is that an empty EventArgs is sent (EventArgs.Empty). Because these events have been added to the properties to support data binding does not mean that the only way to use the control is with data binding. The properties can be set in and read from code without using data binding. They have been added so that the user control is able to use binding if it is available. This is just one way of making the user control as flexible as possible so that it might be used in as many situations as possible.

Remembering that a user control is essentially a control with some added features, all of the design-time issues discussed in the previous section apply here as well. Initializing user controls can bring on the same issues that you saw in the FolderTree example. Care must be taken in the design of user controls so that access to data stores that might not be available to other developers using your control is avoided.

The other thing that is similar to the control creation is the attributes that can be applied to user controls. The public properties and methods of the user control are displayed in the Properties Window when the control is placed on the designer. In the example of the address user control it is a good idea to add Category, Description, and DefaultValue attributes to the address properties. A new AddressData category can be created and the default values would all be "". Here is an example of these attributes applied to the AddressLine1 property:

 [Category("AddressData"),  Description("Gets or sets the AddressLine1 value"), DefaultValue("")] public string AddressLine1 { get{return txtAddress1.Text;} set{ if(txtAddress1.Text != value) { txtAddress1.Text = value; if(AddressLine1Changed != null) AddressLine1Changed(this, EventArgs.Empty); } } } 

As you can see, all that needs to be done to add a new category is to set the text in the Category attribute. The new category is automatically added.

There still is a lot of room for improvement. For example, you could include a list of state names and abbreviations in the control. Instead of just the state property, the user control could expose both the state name and state abbreviation properties. Exception handling should also be added. You could also add validation for the address lines. Making sure the casing is correct, you might ask yourself whether AddressLine1 could be optional or whether apartment and suite numbers should be entered on AddressLine2 and not on AddressLine1.




Professional C# 2005
Pro Visual C++ 2005 for C# Developers
ISBN: 1590596080
EAN: 2147483647
Year: 2005
Pages: 351
Authors: Dean C. Wills

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net