Visual Basic 2005: A Developer's Notebook/Windows Applications

From WikiContent

< Visual Basic 2005: A Developer's Notebook(Difference between revisions)
Jump to: navigation, search
(Initial conversion from Docbook)
Current revision (22:57, 11 March 2008) (edit) (undo)
(Initial conversion from Docbook)
 
(One intermediate revision not shown.)

Current revision

Visual Basic 2005: A Developer's Notebook

.NET 1.0 introduced a whole new toolkit for writing Windows applications. This toolkit—called Windows Forms—quickly won the hearts of developers with its rich features for creating self-sizing windows, customized controls, and dynamic graphics. But for all its strengths, the Windows Forms toolkit left out a few features that many VB 6 developers had come to expect, including a masked edit control and a way to display HTML web pages. The Windows Forms toolkit also lacked some of the frills found in modern Windows applications, like Office XP-style toolbars and menus with thumbnail images. As you'll see in this chapter, .NET 2.0 includes all of these elements and more.

Contents

Use Office-Style Toolbars

With .NET 1.0 and 1.1, VB developers have had to content themselves with either the woefully out-of-date ToolBar control, or draw their own custom toolbars by hand. In .NET 2.0, the situation improves with a rich new ToolStrip control that sports a modern, flat look, correctly handles Windows XP themes, and supports a wide range of graphical widgets, such as buttons, labels, drop-down lists, drop-down menus, text boxes, and more.

How do I do that?

To use the System.Windows.Forms.ToolStrip control, just drag the ToolStrip from the Menus & Toolbars section of the Visual Studio toolbox onto a form. To control which side of the form the ToolStrip lines up with, set the Docking property. For example, Figure 3-1 shows a form, Form1, with two ToolStrip controls, one docked to the top of the form and the other to the right side.

Figure 3-1. Three ToolStrip objects in one RaftingContainer

Three ToolStrip objects in one RaftingContainer

Note

Finally, a ToolStrip control whose looks are worthy of a modern Windows application.

To add buttons to the ToolStrip, you can use the Visual Studio designer. Just click the ToolStrip smart tag and select Edit Items. You can choose new items from a drop-down list and configure their properties in a window like the one shown in Figure 3-2. Or, select Insert Standard Items to create standard ToolStrip buttons for document management (new, open, save, close) and editing (cut, copy, paste).

Figure 3-2. The ToolStrip designer

The ToolStrip designer

The key to mastering the ToolStrip control is learning about all the different widgets you can put inside it. These include:

ToolStripButton
Represents an item on the toolbar that the user can click. It can include text or an image (or both). This is the most common ToolStrip item.
ToolStripLabel
Represents a non-selectable item on the ToolStrip. It can include text or an image (or both).
ToolStripSeparator
Divides adjacent items in a ToolStrip with a thin engraved line.
ToolStripDropDownButton and ToolStripSplitButton
Represent a drop-down menu with items. The only difference is how the drop-down list is drawn. The ToolStripDropDownButton shows its items as a menu, with a thumbnail margin and the ability to check items. In both cases, the menu items are ToolStripMenuItem objects that are added to the collection exposed by the DropDownItems property.
ToolStripComboBox, ToolStripTextBox, and ToolStripProgressBar
Allow you to add familiar .NET controls to a ToolStrip, such as ComboBox, TextBox, and ProgressBar. All of these items derive from ToolStripControlHost, which you can use to create your own ToolStrip controls (as described in the next section, "Add Any Control to a ToolStrip").

All the ToolStrip items derive from the ToolStripItem class. That means they all support a few basic properties (the most important include Text, Image, and ImageAlign, all of which set the display content). ToolStrip items all provide a Click event you can use to detect when the user clicks a toolbar button.

For example, if you want to react to a click of a ToolStrip item that you've named TestToolStripButton, you can use the following code:

Note

When the user clicks a button on the ToolStrip, that button's Click event fires. This is different than the legacy ToolBar control, which fired a generic Click event no matter which button was clicked.

Private Sub TestToolStripButton_Click(ByVal sender As Object, _
  ByVal e As System.EventArgs) Handles TestToolStripButton.Click
    
    MessageBox.Show("You clicked " & CType(sender, ToolStripItem).Name)
End Sub

Once you've created a ToolStrip and added at least one item, you can take advantage of a significant amount of out-of-the-box formatting. The following are just a few of the impressive features provided by ToolStrip:

  • It matches the Office XP toolbar look, with a blue gradient background, etched sizing grips, and hot tracking (highlighting an item as the mouse moves over it).
  • It correctly supports Windows XP themes. That means if you change the color scheme to Olive Green or Silver, all ToolStrip controls update themselves automatically, allowing your application to blend in with the scenery.
  • It allows user customization. If you enable the ToolStrip.AllowReorder property, the user can rearrange the orders of buttons in a ToolStrip by holding down the Alt key and dragging items from one place to another, or even drag a button from one ToolStrip to another.
  • It supports overflow menus. If you enable this feature (by setting ToolStrip.CanOverflow to True) and shrink the window so the entire ToolStrip no longer fits, a special drop-down menu appears at the right with all the extra buttons, as shown in Figure 3-3.

Figure 3-3. An overflow menu

An overflow menu

In the previous example, the ToolStrip is fixed in place. If you want, you can give the user the ability to drag a ToolStrip, either to dock it in a different place or to rearrange several that appear together. To make this possible, you need to add a ToolStripContainer to your form, which shows up as a box with a blue gradient background (like the background of the ToolStrip). Although you can use more than one ToolStripContainer, usually you'll just use one and dock it to fill all or a portion of your window.

Note

To add a ToolStripContainer and place a ToolStrip in it in one step, click the ToolStrip smart tag and then click the "Embed in ToolStripContainer" link.

The ToolStripContainer actually wraps four ToolStripPanel objects, one for each side. These objects are exposed through properties such as ToolStripContainer.LeftToolStripPanel, ToolStripContainer.TopToolStripPanel, and so on. Each panel can hold an unlimited number of ToolStrip objects, which are then docked to the corresponding side. The interesting part is that once you place a ToolStrip in a ToolStripContainer, the user gains the ability to drag a ToolStrip freely about its panel at runtime. Users can even drag a ToolStrip from one ToolStripPanel to another to change the side it's docked on (or even to an entirely separate ToolStripContainer in the same window).

Tip

If you want to prevent the user from docking the ToolStrip to the left side of the container, set the ToolStripContainer.LeftToolStripPanelVisible property to false. You can also use similar properties to prevent docking to the right, top, or bottom sides.

What about...

...updating the rest of your interface to look as good as the ToolStrip? .NET 2.0 actually provides four controls that sport the flat, modern look of Windows XP, and support Windows XP theming. These are ToolStrip, StatusStrip, MenuStrip, and ContextMenuStrip, which replace ToolBar, StatusBar, MainMenu, and ContextMenu. You can quickly refresh your application's interface just by updating these old standbys to the new controls.

Tip

In Visual Studio 2005, you won't see the legacy controls like ToolBar and StatusBar, because they're left out of the toolbox by default. If you want to use them, right-click the toolbox, choose Choose Items, and select these controls from the list.

Where can I learn more?

For more information, read about the ToolStrip classes in the MSDN help library reference. You can also refer to a few more recipes in this chapter:

  • "Add Any Control to a ToolStrip" explains how to add other controls to a ToolStrip.
  • "Add Icons to Your Menu" explains how to use the new MenuStrip control.

Add Any Control to a ToolStrip

The ToolStrip supports a wide range of ToolStripItem classes, allowing you to add everything from buttons and drop-down menus to text-boxes and labels. However, in some situations you might want to go beyond the standard options and use other .NET controls, or even place your own custom controls in the ToolStrip. In order to make this work, you need to use the ToolStripControlHost.

Note

Want to outfit a ToolStrip with a custom control? Thanks to the ToolStripControlHost, you can add just about anything.

How do I do that?

There's no way to add standard .NET controls directly to the ToolStrip, because the ToolStrip only supports classes that derive from ToolStripItem. You could create a class that derives from ToolStripItem to implement a custom ToolStrip element, but this approach is fairly complex and tedious. A much simpler approach is to use the ToolStripControlHost, which can wrap just about any .NET control.

To use the ToolStripControlHost with a non-ToolStripItem control, just pass the control object as a constructor argument when you create the ToolStripControlHost. Then, add the ToolStripControlHost object to the ToolStrip. You can use the code in Example 3-1 to add a CheckBox control to the ToolStrip.Items collection. Figure 3-4 shows the result.

Example 3-1. Adding a Checkbox control to a ToolStrip.Items collection

' Create a CheckBox.
Dim CheckStrip As New CheckBox( )
    
' Set the CheckBox so it takes the size of its text.
CheckStrip.AutoSize = True
CheckStrip.Text = "Sample CheckBox in ToolStrip"
    
' Make sure the CheckbBox is transparent (so the 
' ToolStrip gradient background shows through).
CheckStrip.BackColor = Color.FromArgb(0, 255, 0, 0)
    
' Create the ToolStripControlHost that wraps the CheckBox.
Dim CheckStripHost As New ToolStripControlHost(CheckStrip)
    
' Set the ToolStripControlHost to take the full width
' of the control it wraps.
CheckStripHost.AutoSize = True
    
' Add the wrapped CheckBox.
ToolStrip1.Items.Add(CheckStripHost)

Figure 3-4. A ToolStrip with a CheckBox

A ToolStrip with a CheckBox

What about...

...customizing the ToolStripControlHost? If you're using a ToolStripControlHost to host another control, you might want to add properties to the ToolStripControlHost to expose data from the hosted control. For example, you could add a Checked property to the ToolStripControlHost used in this example so that you could easily set or retrieve the checked state of the wrapped CheckBox control. In order to use this technique, you need to create a custom class that derives from ToolStripControlHost.

Where can I learn more?

The MSDN help reference includes an example with a ToolStripControlHost that hosts a date control. For more information, look up the index entry "ToolStrip → wrapping controls in. "

Add Icons to Your Menu

Windows applications have been undergoing a gradual facelift since Windows XP and Office XP first appeared on the scene. Today, many modern Windows applications use a fine-tuned menu that sports a blue shaded margin on its left side, and an optional icon for each menu command. (To see what this looks like, you can jump ahead to Figure 3-5.)

Note

Jazz up your dullest menus with thumbnail images.

If you wanted to create a polished-looking menu with this appearance in .NET 1.0 or 1.1, you needed to draw it yourself using GDI+ code. Although there are several surprisingly good examples of this technique available on the Internet, it's more than a little messy. In .NET 2.0, the situation improves dramatically. Even though the original MainMenu and ContextMenu controls are unchanged, two new controls—MenuStrip and ContextMenuStrip—provide the same functionality but render the menu with the new Office XP look.

How do I do that?

The MenuStrip and ContextMenuStrip classes leverage all the hard work that went into building the ToolStrip class. Essentially, a MenuStrip is a special container for ToolStripItem objects. The MenuStrip.Items property holds a collection of top-level menu headings (like File, Edit, View, and Help), each of which is represented by a ToolStripMenuItem object. Each ToolStripMenuItem has a DropDownItemsProperty, which exposes another collection of ToolStripMenuItem objects, one for each contained menu item.

Example 3-2 shows code that creates the familiar Windows File menu.

Example 3-2. Creating a Windows File menu

' Add the top-level items to the menu.
MenuStrip1.Items.AddRange(New ToolStripItem( ) _
  {fileToolStripMenuItem})
    
' Set the text for the File menu, and set "F" as the 
' quick access key (so that Alt+F will open the menu.)
fileToolStripMenuItem.Text = "&File"
    
' Add the child items to the File menu.
fileToolStripMenuItem.DropDownItems.AddRange(New ToolStripItem( ) _
  {newToolStripMenuItem, openToolStripMenuItem, _
  toolStripSeparator, saveToolStripMenuItem, _
  saveAsToolStripMenuItem, toolStripSeparator1, _
  printToolStripMenuItem, printPreviewToolStripMenuItem, _
  toolStripSeparator2, exitToolStripMenuItem})
    
' Configure the File child items.
' Set the text and shortcut key for the New menu option.
newToolStripMenuItem.ShortcutKeys = CType((Keys.Control Or Keys.N), Keys)
newToolStripMenuItem.Text = "&New"
    
' Set the text and shortcut key for the Open menu option.
openToolStripMenuItem.ShortcutKeys = CType((Keys.Control Or Keys.O), Keys)
openToolStripMenuItem.Text = "&Open"
    
' (Code for configuring other omitted menu items.)

Usually, you won't enter this information by hand—instead, it's part of the designer code that Visual Studio generates automatically as you set the properties in the Properties window. However, it does show you how the menu works and what you'll need to do if you want to dynamically add new items at runtime.

As Example 3-2 reveals, the structure of a MenuStrip control is the same as the structure of its predecessor, the MainMenu control, with menu objects containing other menu objects. The only difference is in the type of object used to represent menu items (it's now ToolStripMenuItem instead of MenuItem) and the name of the property used to hold the collection of contained menu items (ToolStripMenuItem.DropDownItems instead of MenuItem.ChildItems).

To reap the real benefits of the new ToolStripMenuItem, you need to use one property that wasn't available with ordinary MenuItem objects: the Image property, which sets the thumbnail icon that appears in the menu margin.

newToolStripMenuItem.Image = CType( _
  resources.GetObject("newToolStripMenuItem.Image"), _
  System.Drawing.Image)

Figure 3-5 shows the standard File menu.

Figure 3-5. The new MenuStrip

The new MenuStrip

Usually, you'll load all your images using the Visual Studio Properties Window at design time. In that case, they'll be embedded as a resource inside your assembly. Another option is to load them into an ImageList and then set the ImageKey or IndexProperty of ToolStripMenuItem to point to an image in the ImageList.

Note

To quickly generate a basic menu framework (including the standard menu commands for the File, Edit, Tools, and Help menu), click the MenuStrip smart tag and select Insert Standard Items.

What about...

...painting a menu from scratch? Hopefully, you won't need to. The ToolStripMenuItem gives you a little bit more flexibility than the original MenuItem class—not only can you insert images, but you can also choose a nonstandard font by setting the ToolStripMenuItem.Font property. Here's an example:

fileToolStripMenuItem.Font = New Font("Verdana", 10, FontStyle.Regular)

This technique is useful when you want to show a list of fonts in some sort of document editing application, and you want to render the font names in their corresponding typefaces in the menu.

If you need to perform more radical alterations to how a menu is drawn, you'll need to use another renderer. The MenuStrip, like all the "strip" controls, provides a RenderMode and a Renderer property. The RenderMode property allows you to use one of the built-in renderers by choosing a value from the ToolStripRenderMode enumeration (such as Professional, System, and Custom). If you want to use a renderer of your own, select Custom and then supply a new renderer object in the Renderer property. This renderer could be an instance of a third-party class or an instance of a class you've created (just derive from ToolStripRenderer and override the methods to supply your specialized painting logic).

Put the Web in a Window

There's no shortage of reasons why you might want to integrate a web page window into your application. Maybe you want to show your company web site, create a customized browser, or display HTML product documentation. In .NET 1.0 and .NET 1.1, you could use a web browser window through COM interop, but there were a number of quirky or missing features. The new WebBrowser control in .NET 2.0 addresses these issues with easy web integration, support for printing and saving documents, and the ability to stop a user from navigating to the wrong web site.

Note

. NET's new managed WebBrowser control lets you show an HTML page or allow a user to browse a web site from inside your Windows application—with no interop headaches.

How do I do that?

The System.Windows.Forms.WebBrowser control wraps an Internet Explorer window. You can drop the WebBrowser control onto any Windows form straight from the Visual Studio .NET toolbox.

To direct the WebBrowser to show a page, you simply set the Url property to the target web page. All navigation in the WebBrowser is asynchronous, which means your code continues running while the page is downloading. To check if the page is complete, verify that the ReadyState property is Completed or, better yet, react to a WebBrowser event.

Note

The WebBrowser control supports everything IE does, including JavaScript, ActiveX controls, and plug-ins.

The WebBrowser events unfold in this order:

Note

WebBrowser provides methods that duplicate the browser functions every web surfer is familiar with, such as Stop( ), Refresh( ), GoBack( ), GoForward( ), GoHome( ), GoSearch( ), Print( ), ShowPrintDialog( ), and ShowSave-AsDialog( ).

  1. Navigating fires when you set a new Url or the user clicks a link. This is your chance to cancel the navigation before anything happens.
  2. Navigated fires after Navigating, just before the web browser begins downloading the page.
  3. The ProgressChanged event fires periodically during a download and gives you information about how many bytes have been downloaded and how many are expected in total.
  4. DocumentCompleted fires when the page is completely loaded. This is your chance to process the page.

Example 3-3 shows the event-handling code for a form, WebForm, which hosts a WebBrowser along with a simple status bar and progress bar. The WebBrowser displays a local HTML file (note how the URL starts with file:///, not http://) and ensures that any external web links are opened in standalone Internet Explorer windows.

Example 3-3. Building a basic browser window

Public Class WebForm
    
    Private Sub WebForm_Load(ByVal sender As Object, ByVal e As EventArgs) _
      Handles MyBase.Load
        ' Prevent the user from dragging and dropping links onto this browser.
        Browser.AllowWebBrowserDrop = False
    
        ' Go to the local documentation page.
        Browser.Url = new Uri("file:///" & _
          My.Application.StartupPath & "\Doc.html")
    End Sub
    
    Private Sub Browser_Navigating(ByVal sender As Object, _
      ByVal e As WebBrowserNavigatingEventArgs) Handles Browser.Navigating
        If Not e.Url.IsFile Then
            ' Don't resolve this external link.
            ' Instead, use the Navigate( ) method to open a
            ' standalone IE window.
            e.Cancel = True
            Browser.Navigate(e.Url, True)
        End If
    End Sub
    
    Private Sub Browser_Navigated(ByVal sender As Object, _
      ByVal e As WebBrowserNavigatedEventArgs) Handles Browser.Navigated
        ' Show the progress bar.
        Progress.Visible = True
    End Sub
    
    Private Sub Browser_ProgressChanged(ByVal sender As Object, _
      ByVal e As WebBrowserProgressChangedEventArgs) _
      Handles Browser.ProgressChanged
        ' Update the progress bar.
        Progress.Maximum = e.MaximumProgress
        Progress.Value = e.CurrentProgress
    End Sub
    
    Private Sub Browser_DocumentCompleted(ByVal sender As Object, _
      ByVal e As WebBrowserDocumentCompletedEventArgs) _
      Handles Browser.DocumentCompleted
        ' Hide the progress bar.
        Progress.Visible = False
    End Sub
    
    Private Sub Browser_StatusTextChanged(ByVal sender As Object, _
      ByVal e As EventArgs) Handles Browser.StatusTextChanged
        ' Display the text that IE would ordinarily show
        ' in the status bar.
        Status.Text = Browser.StatusText
    End Sub
    
End Class

Figure 3-6 shows the form with its customized WebBrowser window. The window also includes a StatusStrip to display status text and a progress indicator when pages are being loaded.

Figure 3-6. An embedded web window

An embedded web window

Note

The WebBrowser window is stripped to the bare minimum and doesn't include a toolbar, address bar, or status bar (although you can add other controls to your form).

What about...

...other web surfing tricks? WebBrowser gives you almost all of the power of IE to use in your own applications. Here are a few more tricks you might want to try:

  • Instead of setting the Url property, call the Navigate( ) method, which has two useful overloads. The first (shown in the previous example), allows you to launch a standalone browser window. The second allows you to load a document into a specific frame in the current page.
  • Instead of using URLs, you can load an HTML document directly from another resource, using the DocumentStream or DocumentText property. The DocumentStream accepts a reference to any Stream object, while the DocumentText property accepts a string that contains the HTML data.
  • Once you've loaded a document, you can explore it using the HTML document model that's built into .NET. The jumping-off point is the Document property, which returns an HtmlDocument object that models the current document, including its tags and content.
  • You can direct the WebBrowser to a directory to give the user quick-and-dirty file browsing abilities. Keep in mind, however, that you won't be able to prevent them from copying, moving, or deleting files!

Where can I learn more?

For the full set of properties, look up the System.Windows.Forms.WebBrowser class in the MSDN class library reference.

Validate Input While the User Types

Visual Basic 6 and Access both provide developers with masked editing controls: text input controls that automatically format your input as you type it in based on a specific mask. For example, if you type 1234567890 into a masked input control that uses a telephone-number mask, the number is displayed as the string (123) 456-7890.

Note

VB 6 programmers accustomed to the ActiveX MaskedEdit control were disappointed to find . NET did not include a replacement. In . NET 2.0, the new MaskedTextBox fills the gap.

Masked input controls not only improve the presentation of certain values—they also prevent errors. Choosing the right mask ensures that certain characters will be rejected outright (for example, a telephone- number mask will not accept letters). Masked input controls also neatly avoid canonicalization errors, which occur when there is more than one way of representing the same information. For example, with the telephone number mask, the user will immediately realize that an area code is required, even if you don't specifically explain this requirement.

How do I do that?

.NET 2.0 includes a new control named MaskedTextBox that extends the TextBox control. Once you've added a MaskedTextBox to a form, you can set the mask in two ways:

  • You can choose one of the prebuilt masks.
  • You can define your own custom mask.

To set a mask, click the MaskedTextBox smart tag and select Set Mask. The Input Mask dialog box appears, with a list of commonly used masks, including masks for phone numbers, zip codes, dates, and so on. When you select a mask from the list, the mask is displayed in the Mask text box. You can now customize the mask. You can also try the mask out using the Try It text box, as shown in Figure 3-7.

Figure 3-7. Selecting a mask for the MaskedTextBox

Selecting a mask for the MaskedTextBox

Note

Thanks to the wonders of COM Interop, it's still possible to use the VB 6 MaskedEdit control in . NET. However, the . NET MaskedTextBox control improves on several limitations and quirks in the MaskedEdit control, so it's still superior.

The mask you choose will be stored in the MaskTextBox.Mask property. Once you've chosen a mask, it will be applied whenever the user types in the MaskedTextBox. If you want to respond to user mistakes (like invalid characters) to provide more information, you can respond to the MaskInputRejected event.

If you want to build a custom mask, you need to understand a little more about how masking works. Essentially, a mask is built out of two types of characters: placeholders, which designate where the user must supply a character; and literals, which are used to format the value. For example, in the phone number mask (999)-000-000, the hyphens and brackets are literals. These characters are always present and can't be deleted, modified, or moved by the user. The number 0 is a placeholder that represents any number character, while the number 9 is a placeholder that represents an optional numeric character.

Table 3-1 lists and explains all the characters you can use to create a mask. You can use this as a reference to build your own masks.

Table 3-1. Mask characters

Character Description
0 Required digit (0-9).
9 Optional digit or space. If left blank, a space is inserted automatically.
# Optional digit, space, or plus/minus symbol. If left blank, a space is inserted automatically.
L Required ASCII letter (a-z or A-Z).
 ? Optional ASCII letter.
& Required Unicode character. Allows anything that isn't a control key, including punctuation and symbols.
C Optional Unicode character.
A Required alphanumeric character (allows letter or number but not punctuation or symbols).
a Optional alphanumeric character.
. Decimal placeholder.
, Thousands placeholder.
 : Time separator.
/ Date separator.
$ Currency symbol.
< All the characters that follow will be converted automatically to lowercase as the user types them in. (There is no way to switch a subsequent portion of the text back to mixed-case entry mode once you use this character.)
> All the characters that follow will be converted automatically to uppercase as the user types them in.
\ Escapes a masked character, turning it into a literal. Thus, if you use \& it is interpreted as a literal character &, which will be inserted in the text box.
All other characters All other characters are treated as literals, and are shown in the text box.


Finally, there are a few more properties that the MaskedTextBox provides (and you might want to take advantage of). These include:

BeepOnError
If the user inputs an invalid character and BeepOnError is True, the MaskedTextBox will play the standard error chime.
PromptChar
When the text box is empty, every required value is replaced with a prompt character. By default, the prompt character is the underscore (_), so a mask for a telephone number will display (_ _ _)-_ _ _-_ _ _ _ while empty.
MaskCompleted
Returns True if there are no empty characters in the text box (meaning the user has entered the required value).
InputText
InputText returns the data in the MaskedTextBox without any literal characters. For example, in a MaskedTextBox that allows the user to enter a telephone number, the Text property will return the fully formatted number, like (123)-456-7890, while InputText returns just the numeric content, or 1234567890.

What about...

...using masked editing in other input controls? It is possible, but not easy. The MaskedTextBox relies on a special MaskedEditProvider class in the System.ComponentModel namespace.

To create a different type of masked control, you need to create a custom control that uses the MaskedEditProvider internally. When your control receives a key press, you need to determine the attempted action and pass it on to the MaskedEditProvider using methods like Add( ), Insert( ), Remove( ), and Replace( ). Then, you can retrieve the new display value by calling MaskedEditProvider.ToDisplayString( ), and refresh your custom control appropriately. The hard part is handling all of this low-level editing without causing flicker or losing the user's place in the input string. For more information, you can refer to the full example that's included with the downloadable code in the MaskedEditing project.

Create Text Boxes thatAuto-Complete

In many of the nooks and crannies of the Windows operating system, you'll find AutoComplete text boxes. These text boxes suggest one or more values as you type.

Note

With . NET's new auto-complete features, you can create intelligent text boxes able to suggest possible values based on recent entries or a default list.

Usually, AutoComplete values are drawn from your recent history. For example, when you type a URL into Internet Explorer's address bar, you'll see a list that includes URLs you've surfed to in the past. Now with .NET 2.0, you can harness the same AutoComplete features with your own custom lists or one of the lists maintained by the operating system.

How do I do that?

The TextBox and the ComboBox controls both support the AutoComplete feature in .NET 2.0. To use AutoComplete, first set the control's AutoCompleteMode property to one of the following values:

Append
In this mode, the AutoComplete value is automatically inserted into the control as you type. However, the added portion is selected so that the new portion will be replaced if you continue typing. (Alternatively, you can just click delete to remove it.)
Suggest
This is the friendliest mode. As you type, a drop-down list of matching AutoComplete values appears underneath the control. If one of these entries matches what you want, you can select it.
SuggestAppend
This mode combines Append and Suggest. As with Suggest, a list of candidate matches is shown in a drop-down list. However, the first match is also inserted into the control and selected.

After choosing the type of AutoComplete, you need to specify what list will be used for suggestions. Do this by setting the AutoCompleteSource property to one of the following values:

FileSystem
Includes recently entered file paths. Use FileSystemDirectories instead to include only directory paths.
HistoryList
Includes URLs from Internet Explorer's history list.
RecentlyUsedList
Includes all the documents in the user's "most recently used list," which appears in the Start menu (depending on system settings).
AllUrl
Includes the URLs of all sites that the current user has visited recently, whether they were typed in manually by the user or linked to from a web page.
AllSystemSources
Includes the full list of URLs and file paths.
ListItems
Includes the items in the ComboBox.Items collection. This choice isn't valid with the TextBox.
CustomSource
Includes the items in the AutoCompleteCustomSource collection. You need to add these items yourself.

Figure 3-8 shows an AutoComplete text box using AutoSuggestAppend as the AutoCompleteMode and AllUrl as the AutoCompleteSource.

Figure 3-8. An AutoComplete text box

An AutoComplete text box

The TextBox and ComboBox controls both provide the same functionality. If you use AutoSuggest or AutoSuggestAppend with a ComboBox, the list of matches is displayed in a list under the control. However, this list shouldn't be confused with the list of entries that you've added to the ComboBox.Items property. When you click the drop-down arrow for the ComboBox, you'll see your list of items, not the list of AutoComplete suggestions. Both lists are completely separate, and there is no programmatic way for you to interact with the AutoComplete list. The only exception is if you create a ComboBox with an AutoCompleteSource of CustomSource or ListItems.

What about...

...using AutoComplete in other controls? Unfortunately, there's no managed way to do it in .NET. However, you can retrieve the information you need directly from the registry. For example, if you look in the Software\Microsoft\Internet Explorer\TypedURLs section of the HKEY_CURRENT_USER registry key, you'll find the list of recently typed in URLs. To retrieve these items programmatically, refer to classes like the RegistryKey in the Microsoft.Win32 namespace.

Play a Windows System Sound

The Windows operating system alerts users to system events by mapping them to sounds recorded in specific audio files. The problem is that these files are stored in different locations on different computers. In .NET 1.0 and 1.1, there's no easy way to find the default system sounds and play them in your own application. A new SystemSounds class in .NET 2.0 addresses this problem, allowing you to play the most common sounds with a single line of code.

Note

Need to sound the infamous Windows chime? With the new SystemSounds class, these audio files are right at your fingertips.

How do I do that?

The SystemSounds class in the System.Windows.Forms namespace provides five shared properties. Each of these properties is a separate SystemSound object that represents a specific operating-system event. Here's the full list:

  • Asterisk
  • Beep
  • Exclamation
  • Hand
  • Question

Once you decide which sound you want to use, you simply need to call its Play( ) method to play the sound. Here's an example:

Note

To configure which WAV files are used for each sound, select the Sounds and Audio Devices icon in the Control Panel.

SystemSounds.Beep.Play( )

What about...

...playing arbitrary WAV files? The SystemSounds class works best if you just need an easy way to add a sound for simple user feedback. If you need to play an audio file of your own choosing, you need to use the SoundPlayer, as discussed in the next lab, "Play Simple WAV Audio."

Play Simple WAV Audio

Neither .NET 1.0 or .NET 1.1 provided a managed way to play audio. This shortcoming is finally addressed in .NET 2.0 with the new SoundPlayer class, which allows you to play audio synchronously or asynchronously.

Note

Using the SoundPlayer class, you can play WAV files without diving into the Windows API.

How do I do that?

You can instantiate a SoundPlayer object programmatically, or you can add one to the component tray by dragging it from the toolbox at design time. Once you've created the SoundPlayer, you need to point it to the sound content you want to play. You do this by setting one of two properties:

SoundLocation
If you have a file path or URL that points to a WAV file, specify this information in the SoundLocation property.
Stream
If you have a Stream-based object that contains WAV audio content, use the Stream property.

Once you've set the Stream or SoundLocation property, you need to tell SoundPlayer to actually load the audio data by calling the Load( ) or LoadAsync( ) method. The Load( ) method pauses your code until all the audio is loaded into memory. On the other hand, LoadAsync( ) carries out its work on another thread and fires the LoadCompleted event once it's finished and the audio's available. Usually, you'll use Load() unless you have an extremely large audio file or it takes a long time to read the whole audio file (for example, when retrieving the audio over a slow network or Internet connection).

Finally, once the audio is available, you can call one of the following methods:

PlaySync( )
Pauses your code until the audio playback is finished.
Play( )
Plays the audio on another thread, allowing your code to continue with other tasks and making sure that your application's interface remains responsive.
PlayLooping( )
Similar to Play( ), except that it loops the audio, repeating it continuously.

To halt asynchronous playback at any time, just call Stop( ).

The following code snippet shows an example that plays a sample sound synchronously:

Dim Player As New SoundPlayer( )
Player.SoundLocation = Application.StartupPath & "\mysound.wav"
Try
    Player.Load( )
    Player.PlaySync( )
Catch Err As Exception
    ' An error will occur here if the file can't be read
    ' or if it has the wrong format.
End Try

What about...

...other types of audio? Unfortunately, the SoundPlayer can only play the WAV audio format. If you want to play other types of multimedia, like MP3 or WMA files, you need to use a different solution, and there are no managed classes to help you out.

Two options include:

Create a Windows Explorer-like Split Window

.NET 1.0 gave developers the tools they needed to create split windows of the kind seen in Windows Explorer with the Splitter control. Unfortunately, creating these windows wasn't always easy, because it commonly required a combination of a Splitter and three Panel controls, all of which needed to be docked in the correct order. If you needed to split a window in more than one way, the task became even more awkward. Thankfully, .NET 2.0 streamlines the process with a SplitContainer control.

Note

Split windows are easier than ever now that the SplitContainer control replaces the bare-bones Splitter.

How do I do that?

Essentially, the SplitContainer control represents two panels separated by a splitter bar. The user can drag the bar to one side or another to change the relative amount of space given to each section. To help signal the availability of this functionality, the mouse pointer switches from a single- to a double-headed arrow icon when the user mouses over the splitter bar.

Note

A SplitContainer control is often used when the content in the two panels is related. When the user makes a selection in the first panel, the content in the second is refreshed.

To create a simple interface with the SplitContainer, you should first decide how much screen real estate the SplitContainer will occupy. For example, if you need to reserve some space below the SplitContainer, start by docking a Panel to the bottom of the form. When you add the SplitContainer, its Dock property will automatically be set to DockStyle.Fill so that it fills whatever space is left over.

The SplitContainer always consists of two panels. If you set the Orientation property to Orientation.Vertical (the default), the splitter runs from top to bottom, creating left and right panels. The other option is Orientation.Horizontal, which creates top and bottom panels with a splitter bar running from left to right between them.

Once you've set the appropriate orientation, the next step is to add controls to each side of the SplitContainer. If you want a single control on each side, you simply need to drag the control to the appropriate panel in the SplitContainer and set the Dock property of the control to DockStyle.Fill, so that it fills all the available space between the splitter bar and the edges of the SplitContainer.

If you need to add more than one control in the same region of the SplitContainer, start by adding a Panel and setting the Dock property to DockStyle.Fill. Then, you can anchor other controls inside the Panel.

Once you've set up the SplitContainer, you don't need to write any code to manage the control resizing or user interaction. Figure 3-9 shows an example. (The complete SplitWindow project is available with the downloadable samples.)

Figure 3-9. A vertically split window

A vertically split window

Note

You can also nest a SplitContainer inside another SplitContainer. This is most useful if you are using different orientations (for example, dividing a window into left and right regions and then dividing the region on the right into top and bottom compartments).

What about...

...restricting how a SplitContainer can be resized? The SplitContainer provides several properties tailored for this purpose. For example, you can set the Panel1MinSize and Panel2MinSize properties with the minimum pixel width of the appropriate panels. Once you set these properties, the user won't be able to move the splitter bar to a position that shrinks the panel to less than its minimum allowed size. You can also stop resizing altogether by setting the IsSplitterFixed property to False (in which case you can still adjust the position of the splitter bar by programmatically modifying the SplitterDistance property).

Additionally, you can configure how the SplitContainer behaves when the whole form is resized. By default, the panels are sized proportionately. However, you can designate one of the panels as a fixed panel by setting the FixedPanel property. In this case, that panel won't be modified when the form is resized. (For example, in Windows Explorer the directory tree is in a fixed panel, and it doesn't change size when you expand or shrink the window.) Finally, you can even hide a panel temporarily by setting the Panel1Collapsed or Panel2Collapsed property to True.

Where can I learn more?

For more details on the SplitContainer and some how-to tips, look up "SplitContainer → Overview" in the index of the MSDN help reference.

Take Control of Window Layout

.NET 2.0 includes two new container controls that can lay out all the controls they contain in a set pattern. Both of these controls extend the Panel class with additional layout logic. The FlowLayoutPanel arranges controls evenly over several rows (from left to right), or in multiple columns (from top to bottom). The TableLayoutPanel places its controls into a grid of invisible cells, allowing to you to keep consistent column widths and row heights.

Note

The new . NET layout controls give you a way to lay out controls in set patterns automatically, which can save a good deal of effort with highly dynamic or configurable interfaces.

How do I do that?

The layout controls are used most often in the following two scenarios:

  • You have a dynamic interface that generates some of its elements programmatically. Using the layout controls, you can arrange a group of controls neatly without calculating a position for each control (and then setting the Location property accordingly).
  • You have a localized interface that must adapt to different languages that require vastly different amounts of on-screen real estate. As a result, when the display text changes, the controls must also adjust their size. In this case, layout controls can help you make sure the controls remain properly arranged even when their size varies.

Example 3-4 demonstrates an implementation of the first scenario. It starts with a form that includes an empty FlowLayoutPanel. The FlowLayoutPanel has its BorderStyle set to BorderStyle.Fixed3D so the border is visible.

No controls are added to the FlowLayoutPanel at design time. Instead, several new buttons are added programmatically when a cmdGenerate button is clicked.

Example 3-4. Laying out buttons dynamically

Private Sub Button1_Click(ByVal sender As System.Object, _
  ByVal e As System.EventArgs) Handles Button1.Click
    For i As Integer = 0 To 10
        ' Create a new button.
        Dim Button As New Button
        Button.Text = "Dynamic Button #" & String.Format("{0:00}", i)
    
        ' Size the button the width of the text.
        Button.AutoSize = True
    
        ' Add the button to the layout panel.
        FlowLayoutPanel1.Controls.Add(Button)
    Next
End Sub

Note that the code doesn't set the Location property for each button. That's because the FlowLayoutPanel won't use this information. Instead, it will arrange the buttons in the order they are added, spacing each button out from left to right and then top to bottom. (To reverse this order, change the FlowLayoutPanel.FlowDirection property.)

There is one piece of information that the FlowLayoutPanel does use. That's the Margin property of each container control. This sets the minimum border required between this control and the next. The code above doesn't change the Button.Margin property, because the default setting of 3 pixels is perfectly adequate.

Figure 3-10 shows what the buttons look like once they've been added.

Figure 3-10. Laying out buttons dynamically

Laying out buttons dynamically

Note

There are actually four different components of the Margin property: Margin.Left, Margin.Right, Margin.Top, and Margin.Bottom. You can set these individually to specify different margins for the control on each side.

.NET also includes a TableLayoutPanel. This panel works like the FlowLayoutPanel, laying out controls automatically, but it aligns them according to invisible column and row grid lines. For example, if you have a number of controls that were sized differently, you can use the TableLayoutPanel to ensure that each control is spaced out evenly in an imaginary cell.

What about...

...more advanced layout examples? There's a lot more you can do with a creative use of layout controls. Of course, just because you can doesn't mean you should. Microsoft architects recommend you use layout controls only in specialized scenarios where the anchoring and docking features of Windows Forms aren't enough. If you don't have a highly dynamic interface, layout managers may introduce more complexity than you need.

Where can I learn more?

To get started with more advanced uses of layout controls, refer to some of the information in the MSDN help library reference. Look up "TableLayoutPanel control → about" in the index of the MSDN help reference. This displays general information about the TableLayoutPanel control and provides a link to two walkthroughs that show how the TableLayoutPanel can work in a complex localizable application.

Control When Your Application Shuts Down

In Visual Studio 2005, a new "Shutdown mode" option lets you control when your application should end. You can wrap up as soon as the main window is closed (the window that's designated as the startup object), or you can wait until all the application windows are closed. And if neither of these choices offers what you want, you can take complete control with the Application class.

Note

In . NET 2.0, it's easier than ever to specify when your Windows application should call it quits.

How do I do that?

In Visual Studio, double-click the My Project item in the Solution Explorer. A tabbed window with application settings will appear, as shown in Figure 3-11. Click the Application tab, and look at the Windows Application Properties section at the bottom of the tab.

Figure 3-11. Application settings in Visual Studio 2005

Application settings in Visual Studio 2005

You have two out-of-the-box choices for the "Shutdown mode" box:

When startup form closes
This option matches the standard behavior of an application in .NET 1.0 and 1.1. As soon as the startup form is closed, the entire program shuts down, taking any other open windows with it. (The startup form is the form identified in the "Startup object" box.)
When last form closes
This option matches the behavior of Visual Basic 6. Your application keeps on rolling as long as a window is open. When the last window is closed, the application follows suit, and shuts itself down.

If neither of these options is suitable, you can take matters into your own hands. First, select the "Startup with custom Sub Main" checkbox. Now, you need to add a subroutine named Main( ) to your application. You can place this subroutine in an existing form or class, as long as you make sure to add the Shared accessibility keyword. Here's an example:

Public Class MyForm
    
    Public Shared Sub Main
        ' (Startup code goes here.)
               End Sub
   
End Class

Shared methods are always available, even if there isn't a live instance of the containing class. For example, if you add a Main( ) method to a form, the .NET runtime can call your Main( ) method even though there isn't a form object.

Another choice is to add the Main( ) method to a module. In a module, every method, function, property, and variable acts as though it's shared, so you won't need to add the Shared keyword. Here's an example:

Public Module MyModule
    
    Public Sub Main
        ' (Startup code goes here.)
               End Sub
   
End Module

Whatever you choose, make sure the class or module that contains the Main( ) method is selected in the "Startup object" box.

Note

Using a module is a great choice if you have extensive initialization to perform, because it separates your startup code from your form code.

When you use a Main( ) method to start your application, the application only runs as long as the Main( ) method is active. As soon as Main( ) ends, your application finishes. Here's an example of a prematurely terminated application:

Public Sub Main
    ' Show one form modelessly (without blocking the code).
    Form1.Show( )
    
    ' Show another form modelessly (at the same time as the first).
    Form2.Show( )
    
    ' After this line, the Main method ends, the application shuts
    ' itself down, and both windows close (after only being open a
    ' for a few milliseconds of screen time).
End Sub

And here's the correct code that shows two windows in sequence:

Public Sub Main
    ' Show one form modally (code stops until the window is closed).
    Form1.ShowDialog( )
    
    ' After the first window is closed, show the second modally.
    Form2.ShowDialog( )
    
    ' Now the application ends.
End Sub

In some cases, you might want to start your application with a Main( ) method to perform some basic initialization and show a few forms. Then, you might want to wait until all the forms are closed before the application ends. This pattern is easy to implement, provided you use the Application class. The basic idea is to call Application.Run( ) to keep your application alive indefinitely, and call Application.Exit( ) at some later point to end it. Here's how you could start the application with two visible windows:

Public Sub Main
    ' Show two forms modelessly (and at the same time).
    Form1.Show( )
    Form2.Show( )
    
    ' Keep the application going until you say otherwise.
    Application.Run( )
End Sub

To specify that the application should end when either window closes, use this code in the Form.Unload event handler of both forms:

Private Sub Form1_FormClosed(ByVal sender As Object, _
  ByVal e As FormClosedEventArgs) Handles Me.FormClosed
    Application.Exit( )
End Sub

What about...

...cleaning up when the application calls it quits? When your application ends you might want to release unmanaged resources, delete temporary files, or save some final settings. The Application class provides a solution with its ApplicationExit event. All you need to do is attach the event to a suitable event handler in the Main( ) method. Here's an example that uses a method named Shutdown( ):

Note

The ApplicationExit Event always fires (and the code in an event handler for it always runs), even if the application has been derailed by an unhandled exception.

AddHandler Application.ApplicationExit, AddressOf Shutdown

And here's the Shutdown( ) method that runs automatically just before the application ends:

Public Sub Shutdown(ByVal sender As Object, ByVal e As EventArgs)
    MessageBox.Show("Cleaning up.")
End Sub

Where can I learn more?

For more information, refer to the Application class in the MSDN class library reference (it's in the System.Windows.Forms namespace).

Tip

This lab uses the Application class from the System.Windows.Forms namespace. This item is similar to, but different from, the My.Application object. Technically, the My.Application object is a dynamically created class (generated by Visual Studio and hidden from view), which inherits from WindowsFormsApplicationBase. Overall, the My.Application object usually acts as a slightly simplified version of the System.Windows.Forms.Application class. This allows .NET to offer one class to programmers who want simplicity, and another to those who want the full set of features. In other words, .NET lets VBers have their cake and eat it too (but only by creating two different cakes).

Prevent Your Application from Starting Twice

Want to make sure that the user can run no more than one copy of your application on the same computer? In VB .NET 1.0, you'd need to go through the awkward task of searching all the loaded processes to make sure your program wasn't already in memory. In VB 2005, the work is done for you.

Note

There's no longer a need to write code to check whether your application is already running. VB 2005 will perform the check for you.

How do I do that?

In Visual Studio, double-click the My Project item in the Solution Explorer. A tabbed window with application settings will appear. Click the Application tab, and look at the Windows Application Properties section at the bottom of the tab. Now click the "Make single instance application" checkbox and build the project.

If you try to start the application while it's already running, it will ignore you completely, and nothing will happen.

What about...

...showing a custom error message? If you need to show an error message, check for other instances without stopping the application, or otherwise tweak the code, then you'll need to perform the check youself by using the System.Diagnostics.Process class. Here's the code to get you started:

' Get the full name of the process for the current application.
Dim ModuleName, ProcessName As String
ModuleName = Process.GetCurrentProcess.MainModule.ModuleName
ProcessName = System.IO.Path.GetFileNameWithoutExtension(ModuleName)
    
' Check for other processes with this name.
Dim Proc( ) As System.Diagnostics.Process
Proc = Process.GetProcessesByName(ProcessName)
If Proc.Length > 1 Then
    ' (There is another instance running.)
Else
    ' (There are no other instances running.)
End If

Where can I learn more?

For more information, look up the "ProcessInfo class" index entry in the MSDN help, or look up "Process class sample" index entry for a full-fledged example.

Communicate Between Forms

In previous versions of .NET, you were responsible for tracking every open form. If you didn't, you might unwittingly strand a window, leaving it open but cut off from the rest of your application. VB 2005 restores the beloved approach of VB 6 developers, where there's always a default instance of your form ready, waiting, and accessible from anywhere else in your application.

Note

VB 2005 makes it easy for forms to interact, thanks to the new default instances. This feature is a real timesaver—and a potential stumbling block.

How do I do that?

To access the default instance of a form, just use its class name. In other words, if you've created a form that's named (unimaginatively) Form1, you can show its default instance like this:

Form1.Show( )

This automatically creates an instance of Form1 and then displays it. This instance of Form1 is designated as the default instance.

To communicate between forms, you simply add dedicated public methods. For example, if Form1 needs to be able to refresh Form2, you could add a RefreshData( ) method to Form2, like this:

Public Class Form2
    Private Sub RefreshData( )
        MessageBox.Show("I've been refreshed!")
    End Sub
End Class

You could then call it like this:

Form2.RefreshData( )

This calls the RefreshData( ) method of the default instance of Form2. The fact that RefreshData( ) is a method you added (not an inherited method, like the Show( ) method) makes no difference in how you use it.

You can also get at the forms using the My collection. For example, the code above is equivalent to this slightly longer statement:

My.Forms.Form2.RefreshData( )

You can always access the default instance of a form, even if it isn't currently visible. In fact, .NET creates the default instance of the form as soon as you access one of its properties or methods. If you only want to find out what forms are currently open, you're better off using the My.Application.OpenForms collection. Here's an example that iterates through the collection and displays the caption of each form:

For Each frm As Form In My.Application.OpenForms
    MessageBox.Show(frm.Text)
Next

This handy trick just wasn't possible in earlier versions of .NET without writing your own code to manually track forms.

What about...

...potential problems? Conveniences such as default instances come at a price. In this case, you don't need to worry about wasted memory or any performance slowdown, since .NET is clever enough to create the forms as you need them. The real problem that you might face results from the fact that default instances confuse the concepts of classes and objects, making it all too easy to accidentally refer to different instances of the same form in different parts of your application.

Note

You can also get a reference to the application's startup form using the My.Application.StartupForm property.

For example, imagine you use this code to show a form:

Dim FormObject As New Form1
FormObject.Show( )

In this example, the form you've shown is an instance of Form1, but it isn't the default instance. That means that if another part of your code uses code like this:

Form1.Refresh( )

it won't have the effect you expect. The visible instance of Form1 won't be refreshed. Instead, the default instance (which probably isn't even visible) will be refreshed. Watch out for this problem—it can lead to exasperating headaches! (In all fairness to .NET, this isn't a new problem. Visual Basic 6 developers encountered the same headaches when creating forms dynamically. The difference is that Visual Basic 6 developers almost always rely on default instances, while .NET developers—until now—haven't.)

Improve Redraw Speeds for GDI+

Every control and form in .NET inherits from the base Control class. In .NET 2.0, the Control class sports a new property named DoubleBuffered. If you set this property to True, the form or control will automatically use double-buffering, which dramatically reduces flicker when you add custom drawing code.

Note

Need to turbocharge your GDI+ animations? In . NET 2.0, the Form class can do the double-buffering for you.

How do I do that?

In some applications you need to repaint a window or control frequently. For example, you might refresh a window every 10 milliseconds to create the illusion of a continuous animation. Every time the window is refreshed, you need to erase the current contents and draw the new frame from scratch.

In a simple application, your drawing logic might draw a single shape. In a more complex animation, you could easily end up rendering dozens of different graphical elements at a time. Rendering these elements takes a small but significant amount of time. The problem is that if you paint each graphical element directly on the form, the animation will flicker as the image is repeatedly erased and reconstructed. To avoid this annoying problem, developers commonly use a technique known as double-buffering. With double-buffering, each new frame is fully assembled in memory, and only painted on the form when it's complete.

.NET 2.0 completely saves you the hassle of double-buffering. All you need to do is set the DoubleBuffered property of the form or control to True. For example, imagine you create a form and handle the Paint event to supply your own custom painting logic. If the form is set to use double-buffering, it won't be refreshed until the Paint event handler has finished, at which point it will copy the completed image directly onto the form. If DoubleBuffered is set to False, every time you draw an individual element onto the form in the Paint event handler, the form will be refreshed. As a result, the form will be refreshed dozens of times for anything but the simplest operations.

Example 3-5 features a form that makes use of custom drawing logic. When the user clicks the cmdStart button, a timer is switched on. This timer fires every few milliseconds and invalidates the form by calling its Invalidate( ) method. In response, Windows asks the application to repaint the window, triggering the OnPaint( ) method with the custom drawing code.

Example 3-5. An animated form

Public Class AnimationForm
    
    ' Indicates whether the animation is currently being shown.
    Private IsAnimating As Boolean = False
    
    ' Track how long the animation has been going on.
    Private StartTime As DateTime
    
    Private Sub Form_Paint(ByVal sender As Object, _
      ByVal e As System.Windows.Forms.PaintEventArgs) Handles MyBase.Paint
    
        ' Check if the animation is in progress.
        If IsAnimating Then
    
            ' Get reading to draw the current frame.
            Dim g As Graphics = e.Graphics
            g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality
    
            ' Paint the background.
            Dim BackBrush As New LinearGradientBrush( _
              New Point(0, 0), New Point(100, 100), _
              Color.Blue, Color.LightBlue)
            g.FillRectangle(BackBrush, New Rectangle(New Point(0, 0), _
              Me.ClientSize))
            g.FillRectangle(Brushes.LightPink, New Rectangle(New Point(10, 10), _
              New Point(Me.Width - 30, Me.Height - 50)))
    
            ' Calculate elapsed time.
            Dim Elapsed As Double = DateTime.Now.Subtract(StartTime).TotalSeconds
    
            Dim Pos As Double = (-100 + 24 * Elapsed ^ 2) / 10
    
            ' Draw some moving objects.
            Dim Pen As New Pen(Color.Blue, 10)
            Dim Brush As Brush = Brushes.Chartreuse
            g.DrawEllipse(Pen, CInt(Elapsed * 100), CInt(Pos), 10, 10)
            g.FillEllipse(Brush, CInt(Elapsed * 100), CInt(Pos), 10, 10)
    
            g.DrawEllipse(Pen, CInt(Elapsed * 50), CInt(Pos), 10, 10)
            g.FillEllipse(Brush, CInt(Elapsed * 50), CInt(Pos), 10, 10)
    
            g.DrawEllipse(Pen, CInt(Elapsed * 76), CInt(Pos) * 2, 10, 10)
            g.FillEllipse(Brush, CInt(Elapsed * 55), CInt(Pos) * 3, 10, 10)
    
            g.DrawEllipse(Pen, CInt(Elapsed * 66), CInt(Pos) * 4, 10, 10)
            g.FillEllipse(Brush, CInt(Elapsed * 72), CInt(Pos) * 3, 10, 10)
    
            If Elapsed > 10 Then
                ' Stop the animation.
                tmrInvalidate.Stop( )
                IsAnimating = False
            End If
    
        Else
            ' There is no animation underway. Paint the background.
            MyBase.OnPaintBackground(e)
        End If
    
    End Sub
    
    
    Private Sub tmrInvalidate_Tick(ByVal sender As System.Object, _
      ByVal e As System.EventArgs) Handles tmrInvalidate.Tick
        ' Invalidate the form, which will trigger a refresh.
        Me.Invalidate( )
    End Sub
    
    Private Sub cmdStart_Click(ByVal sender As System.Object, _
      ByVal e As System.EventArgs) Handles cmdStart.Click
        ' Start the timer, which will trigger the repainting process
        ' at regular intervals.
        Me.DoubleBuffered = True
        IsAnimating = True
        StartTime = DateTime.Now
        tmrInvalidate.Start( )
    End Sub
    
    ' Ensure that the form background is not repainted automatically
    ' when the form is invalidated. This isn't necessary, because the
    ' Paint event will handle the painting for the form.
    ' If you don't override this method, every time the form is painted
    ' the window will be cleared and the background color will be painted
    ' on the surface, which causes extra flicker.
    Protected Overrides Sub OnPaintBackground( _
      ByVal pevent As System.Windows.Forms.PaintEventArgs)
        ' Do nothing.
    End Sub
    
End Class

Try running this example with and without double-buffering. You'll see a dramatic difference in the amount of flicker.

What about...

...owner-drawn controls? Double-buffering works exactly the same way with owner-drawn controls as with forms, because both the Form and Control classes provide the DoubleBuffered property and the Paint event. Of course, there's no point in double-buffering both a form and its controls, since that will only cause your application to consume unnecessary extra memory.

Where can I learn more?

Overall, the GDI+ drawing functions remain essentially the same in .NET 2.0. To learn more about drawing with GDI+, look up "GDI+ → Examples" in the index of the MSDN help library. You may also be interested in the "GDI+ Images" and "GDI+ Text" entries.

Handle Asynchronous Tasks Safely

One of .NET's most impressive features is its extensive support for multithreaded programming. However, as most programmers discover at some point in their lives, multithreaded programming isn't necessarily easy.

Note

Need to conduct a time-consuming task in the background without dealing with threading issues? The new BackgroundWorker class makes it easy.

One of the main challenges with Windows applications is that it's not safe to modify a form or control from a background thread, which means that after your background task is finished, there's no straightforward way to update your application's interface. You can use the Control.Invoke( ) method to marshal a method to the correct thread, but other problems then appear, such as transferring the information you need to make the update. Fortunately, all of these headaches can be avoided thanks to the new BackgroundWorker component.

How do I do that?

The BackgroundWorker component gives you a foolproof way to run a time-consuming task on a separate, dedicated thread. This ensures that your application interface remains responsive, and it allows your code to carry out other tasks in the foreground. Best of all, the underlying complexities of multithreaded programming are hidden. Once the background process is complete, you simply handle an event, which fires on the main thread. In addition, the BackgroundWorker supports progress reporting and canceling.

You can either create a BackgroundWorker object programmatically, or you can drag it onto a form from the Components tab of the toolbox. To start your background operation, you call the RunWorkerAsync( ) method. If you need to pass an input value to this process, you can supply it as an argument to this method (any type of object is allowed):

Worker.RunWorkerAsync(inputValue)

Next, you need to handle the DoWork event to perform the background task. The DoWork event fires on the background thread, which means at this point you can't interact with any other part of your application (unless you're willing to use locks or other techniques to safeguard access). Typically, the DoWork event handler retrieves the input value from the DoWorkEventArgs.Argument property and then carries out the time-consuming operation. Once the operation is complete, you simply set the DoWorkEventArgs.Result property with the result. You can use any data type or even a custom object. Here's the basic pattern you'll use:

Private Sub backgroundWorker1_DoWork(ByVal sender As Object, _
  ByVal e As DoWorkEventArgs) Handles backgroundWorker1.DoWork
    
    ' Get the information that was supplied.
    Dim Input As Integer = CType(e.Argument, Integer)
    
    ' (Perform some time consuming task.)
    
    ' Return the result.
    e.Result = Answer
    
End Sub

Finally, the BackgroundWorker fires a RunWorkerCompleted event to notify your application that the process is complete. At this point, you can retrieve the result from RunWorkerCompletedEventArgs and update the form accordingly:

Private Sub backgroundWorker1_RunWorkerCompleted( _
  ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs) _
  Handles backgroundWorker1.RunWorkerCompleted
    
    result.Text = "Result is: " & e.Result.ToString( )
    
End Sub

Example 3-6 shows a form that puts all of these parts together. It performs a time-limited loop for a number of seconds that you specify. This example also demonstrates two more advanced techniques: cancellation and progress. To cancel the operation, you simply need to call the BackgroundWorker.CancelAsync( ) method. Your DoWork event-handling code can then check to see if the main form is attempting to cancel the operation and exit gracefully. To maintain progress information, your DoWork event-handling code needs to call the BackgroundWorker.ReportProgress( ) method and provide an estimated percent complete (where 0% means "just started" and 100% means "completely finished"). The form code can respond to the ProgressChanged event to read the new progress percentage and update another control, such as a ProgressBar. Figure 3-12 shows this application in action.

Example 3-6. An asynchronous form with the BackgroundWorker

Public Class AsyncForm
    
    Private Sub startAsyncButton_Click(ByVal sender As System.Object, _
      ByVal e As System.EventArgs) Handles startAsyncButton.Click
    
        ' Disable the Start button until 
        ' the asynchronous operation is done.
        startAsyncButton.Enabled = False
    
        ' Enable the Cancel button while 
        ' the asynchronous operation runs.
        cancelAsyncButton.Enabled = True
    
        ' Start the asynchronous operation.
        backgroundWorker1.RunWorkerAsync(Int32.Parse(txtWaitTime.Text))
    End Sub
    
    ' This event handler is where the actual work is done.
    Private Sub backgroundWorker1_DoWork(ByVal sender As Object, _
      ByVal e As DoWorkEventArgs) Handles backgroundWorker1.DoWork
    
        ' Get the information that was supplied.
        Dim Worker As BackgroundWorker = CType(sender, BackgroundWorker)
    
        Dim StartTime As DateTime = DateTime.Now
        Dim SecondsToWait As Integer = CType(e.Argument, Integer)
        Dim Answer As Single = 100
        Do
            ' Check for any cancellation requests.
            If Worker.CancellationPending Then
                e.Cancel = True
                Return
            End If
    
            ' Continue calculating the answer.
            Answer *= 1.01
    
            ' Report the current progress (percentage complete).
            Worker.ReportProgress(( _
              DateTime.Now.Subtract(StartTime).TotalSeconds / SecondsToWait) * 100)
    
            Thread.Sleep(50)
        Loop Until DateTime.Now > (StartTime.AddSeconds(SecondsToWait))
    
        e.Result = Answer
    End Sub
    
    ' This event handler fires when the background work
    ' is complete.
    Private Sub backgroundWorker1_RunWorkerCompleted( _
      ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs) _
      Handles backgroundWorker1.RunWorkerCompleted
    
        ' Check what the result was, and update the form.
        If Not (e.Error Is Nothing) Then
            ' An exception was thrown.
            MessageBox.Show(e.Error.Message)
        ElseIf e.Cancelled Then
            ' Check if the user cancelled the operation.
            result.Text = "Cancelled"
        Else
            ' The operation succeeded.
            result.Text = "Result is: " & e.Result.ToString( )
        End If
    
        startAsyncButton.Enabled = True
        cancelAsyncButton.Enabled = False
    End Sub
    
    ' This event handler updates the progress bar.
    Private Sub backgroundWorker1_ProgressChanged( _
      ByVal sender As Object, ByVal e As ProgressChangedEventArgs) _
      Handles backgroundWorker1.ProgressChanged
    
        Me.progressBar1.Value = e.ProgressPercentage
    End Sub
    
    Private Sub cancelAsyncButton_Click( _
      ByVal sender As System.Object, ByVal e As System.EventArgs) _
      Handles cancelAsyncButton.Click
    
        ' Cancel the asynchronous operation.
        Me.backgroundWorker1.CancelAsync( )
    
        cancelAsyncButton.Enabled = False
    End Sub 
    
End Class

Figure 3-12. Monitoring a background task

Monitoring a background task

What about...

...other scenarios where you can use the BackgroundWorker? This example used the BackgroundWorker with a long-running background calculation. Other situations in which the BackgroundWorker proves to be just as indispensable include:

  • Contacting a web service
  • Downloading a file over the Internet
  • Retrieving data from a database
  • Reading or writing large amounts of data

Where can I learn more?

The MSDN reference includes a detailed walkthrough for using the BackgroundWorker, and other topics that tackle multithreaded programming in detail. Look up the "background operations" index entry to see a slightly different approach that uses the BackgroundWorker to calculate Fibonacci numbers.

Use a Better Data-Bound Grid

The DataGrid that shipped with .NET 1.0 and 1.1 had a slew of limitations. It was difficult to customize, nearly impossible to extend, and had no support for important features like modifying or filling the DataGrid programmatically, accessing individual cells, or applying per-cell formatting. In many cases, VB developers avoided the DataGrid altogether and used third-party grids or even older COM-based controls like the MSFlexGrid. (In fact, third-party component developers regularly thanked Microsoft for making enhanced grid components an easy sell.)

Note

. NET's DataGrid was a significant disappointment in an otherwise state-of-the-art framework. Now the Windows Forms team fills in the gaps with a first-rate grid.

In designing .NET 2.0, the Windows Forms team decided it would be nearly impossible to remedy the shortcomings without breaking backward compatibility. So, they chose to introduce an entirely new DataGridView control with support for all the missing features and more.

How do I do that?

You can bind the DataGridView to a DataTable object in the same way that you would bind a DataGrid. Here's the bare minimum code you might use to bind a table named Customers:

DataGridView1.DataSource = ds
DataGridView.DataMember = "Customers"

Of course, to put this code to work, you need to create the DataSet object ds and fill it with information. For a complete example that adds the necessary ADO.NET code for this step, refer to the downloadable content for this chapter.

When you use this code, the DataGridView creates one column for each field in the data source, and titles it using the field name. The grid also has a significant amount of out-of-the-box functionality. Some of the characteristics you'll notice include:

  • The column headers are frozen. That means they won't disappear as you scroll down the list.
  • You can edit values. Just double-click a cell or press F2 to put it in edit mode. (You can disable this feature by setting the DataColumn.ReadOnly property to True in the underlying DataTable.)
  • You can sort columns. Just click the column header once or twice.
  • You can automatically size columns. Just double-click on the column divider between headers to expand a column (the one on the left) to fit the current content.
  • You can select a range of cells. You can highlight one or more cells, or multiple rows, by clicking and dragging. To select the entire table, click the square at the top-left corner.
  • You can add rows by scrolling to the end of the grid and entering new values. To disable this feature, set the AllowUserToAddRows property to False.
  • You can delete rows by selecting the full row (click the row button at the left) and pressing the Delete key. To disable this feature, set the AllowUserToDeleteRows property to False.

Before going any further with the DataGridView, there are two methods you'll want to consider using right away: AutoResizeColumns( ) and AutoResizeRows( ). AutoResizeColumns( ) extends all columns to fit header text and cell data. AutoResizeRows( ) enlarges the row with multiple lines to fit header text and cell data (the DataGridView supports automatic wrapping). Both of these methods accept a value from an enumeration that allows you to specify additional options (such as extending the column just to fit all the columns, or just the header text):

' Create wider columns to fit data.
DataGridView1.AutoResizeColumns( _
  DataGridViewAutoSizeColumnsMode.AllCells)
    
' Create multi-line columns to fit data.
DataGridView1.AutoResizeRows( _
 DataGridViewAutoSizeRowsMode.HeaderAndColumnsAllCells)

You can also use the AutoResizeColumn( ) and AutoResizeRow( ) methods to change just a single column or row (specified as an index number).

Once you have created a DataGridView and populated it with data, you can interact with it through two useful collections: Columns and Rows. The Columns collection exposes a collection of DataGridViewCell objects, one for each column in the grid. You can set the order in which columns are displayed (by setting an index number in the DisplayIndex property), hide a column altogether (set Visible to false), or freeze a column so that it always remains visible even as the user scrolls to the side (set Frozen to true). You can also modify the column header text (HeaderText), the size (Width), and make it non-editable (ReadOnly). To look up a column, use the index number or the corresponding field name.

For example, here's the code you need to change some column properties in the OrderID column of a bound DataGridView:

' Keep this column visible on the left at all times.
DataGridView1.Columns("CustomerID").Frozen = True
DataGridView1.Columns("CustomerID").DisplayIndex = 0
    
' Configure the column appearance.
DataGridView1.Columns("CustomerID").HeaderText = "ID"
DataGridView1.Columns("CustomerID").Resizable = DataGridViewTriState.True
DataGridView1.Columns("CustomerID").MinimumWidth = 50
DataGridView1.Columns("CustomerID").Width = 50
    
' Don't allow the values in this column to be edited.
DataGridView1.Columns("CustomerID").ReadOnly = True

The Rows collection allows you to access individual DataGridViewRow objects by index number. Once you have a DataGridViewRow, you can examine its Cells collection to look up individual values in that row.

However, it's more likely that you'll want to access just those rows that correspond to the current user selection. The DataGridView actually provides three related properties that can help you:

SelectedRows
Provides a collection with one DataGridViewRow for each fully selected row. This makes sense if the SelectionMode only allows full row selection.
SelectedColumns
Provides a collection with one DataGridViewColumn for each fully selected column. This makes sense if the SelectionMode only allows full column selection.
SelectedCells
Always provides a collection with one DataGridViewCell for each selected cell, regardless of the selection mode. You can use this property if your selection mode allows individual cell selection or if you just want to process each cell separately.

For example, if you're using DataGridViewSelectionMode.FullRowSelect, you can use the following code to retrieve the current selection and display a specific field from each selected row when the user clicks a button:

Note

You can control the type of selection that's allowed by setting the DataGridView.SelectionMode property. Different values allow selection for individual cells, rows, or columns. DataGridView.MultiSelect determines whether more than one item can be selected at a time.

Private Sub cmdSelection_Click(ByVal sender As System.Object, _
  ByVal e As System.EventArgs) Handles cmdSelection.Click
    
    For Each SelectedRow As DataGridViewRow In DataGridView1.SelectedRows
        MessageBox.Show(SelectedRow.Cells("CustomerID").Value)
    Next
    
End Sub

For a full example that puts all of these ingredients together, refer to the BetterDataGrid example in the downloadable samples.

What about...

...doing more with the DataGridView? The features described so far provide a snapshot of DataGridView basics, but they only scratch the surface of its customizability features. For more information, refer to the following two labs in this chapter (Section 3.17 and Section 3.18).

Format the DataGridView

Formatting the .NET 1.x DataGrid ranges from awkward to nearly impossible. However, thanks to its multi-layered model, formatting the DataGridView is far easier. This model builds on a single class, the DataGridViewCellStyle, which encapsulates key formatting properties. You can assign different DataGridViewCellStyle objects to separate rows, columns, or even distinct cells.

Note

By using a few simple style properties, you can configure the appearance of the entire grid, individual columns, or rows with important data.

How do I do that?

The DataGridView already looks better than the DataGrid in its default state. For example, you'll notice that the column headers have a modern, flat look and become highlighted when the user moves the mouse over them. However, there's much more you can do with the help of the DataGridViewCellStyle class.

The DataGridViewCellStyle collects all the formatting properties of the DataGridView. It defines appearance-related settings (e.g., color, font), and data formatting (e.g., currency, date formats). All in all, the DataGridViewCellStyle provides the following key properties:

Alignment
Sets how text is justified inside the cell.
BackColor and ForeColor
Set the color of the cell background and the color of the cell text.
Font
Sets the font used for the cell text.
Format
A format string that configures how numeric or date data values will be formatted as strings. You can use the standard .NET format specifiers and your own custom format strings. For example, C designates a currency value. (For more information, look up the index entry "numeric format strings" in the MSDN help.)
NullText
A string of text that will be substituted for any null (missing) values.
SelectionBackColor and SelectionForeColor
Set the cell background colors and text colors for selected cells.
WrapMode
Determines if text will flow over multiple lines (if the row is high enough to accommodate it) or if it will be truncated. By default, cells will wrap.

The interesting part is that you can create and set DataGridViewCellStyle objects at different levels. When the DataGridView displays a cell, it looks for style information in several places. Here's the order from highest to lowest importance:

  1. DataGridViewCell.Style
  2. DataGridViewRow.DefaultCellStyle
  3. DataGridView.AlternatingRowsDefaultCellStyle
  4. DataGridView.RowsDefaultCellStyle
  5. DataGridViewColumn.DefaultCellStyle
  6. DataGridView.DefaultCellStyle

In other words, if DataGridView finds a DataGridViewCellStyle object assigned to the current cell (option 1), it always uses it. If not, it checks the DataGridViewCellStyle for the row, and so on.

The following code snippet performs column-specific formatting. It ensures that all the values in the CustomerID column are given a different font, alignment, and set of colors. Figure 3-13 shows the result.

Note

If you use the design-time data-binding features of Visual Studio, you can avoid writing this code altogether. Just click the Edit Columns link in the Properties Window and use the designer to choose the formatting.

Dim Style As DataGridViewCellStyle = _
  DataGridView1.Columns("CustomerID").DefaultCellStyle
Style.Font = New Font(DataGridView1.Font, FontStyle.Bold)
Style.Alignment = DataGridViewContentAlignment.MiddleRight
Style.BackColor = Color.LightYellow
Style.ForeColor = Color.DarkRed

Figure 3-13. A DataGridView with a formatted column

A DataGridView with a formatted column

What about...

...the easiest way to apply custom cell formatting? Sometimes, you want to call attention to cells with certain values. You could handle this task by iterating over the entire grid, looking for those cells that interest you. However, you can save time by responding to the DataGridView.CellFormatting event. This event occurs as the grid is being filled. It gives you the chance to inspect the cell and change its style before it appears.

Here's an example that formats a cell to highlight high prices:

Private Sub DataGridView1_CellFormatting(ByVal sender As System.Object, _
  ByVal e As System.Windows.Forms.DataGridViewCellFormattingEventArgs) _
  Handles DataGridView1.CellFormatting
    
    ' Check if this is the right column.
    If DataGridView1.Columns(e.ColumnIndex).Name = "Price" Then
        ' Check if this is the right value.
        If e.Value > 100 Then
            e.CellStyle.ForeColor = Color.Red
            e.CellStyle.BackColor = Color.Yellow
        End If
    End If
    
End Sub

Keep in mind that you should reuse style objects if at all possible. If you assign a new style object to each cell, you'll consume a vast amount of memory. A better approach is to create one style object, and assign it to multiple cells that use the same formatting.

Add Images and Controls to the DataGridView

To create a custom column with the DataGrid, you needed to implement the functionality yourself by deriving a custom DataGridColumnStyle class that would need dozens of lines of code. The DataGridView provides a much simpler model. In fact, you can add new columns right alongside your data-bound columns!

Note

There's a lot more that you can do with theDataGridView, including adding static buttons and images.

How do I do that?

In many scenarios, it's useful to display a button next to each row in a grid. Clicking this button can then remove a record, add an item to a shopping cart, or call up another window with more information. The DataGridView makes this easy with the DataGridViewButtonColumn class. You simply need to create a new instance, specify the button text, and add it to the end of the grid:

' Create a button column.
Dim Details As New DataGridViewButtonColumn( )
Details.Name = "Details"
    
' Turn off data-binding and show static text.
' (You could use a property from the table by setting
' the DataPropertyName property instead.)
Details.UseColumnTextForButtonValue = False
Details.Text = "Details..."
    
' Clear the header.
Details.HeaderText = ""
    
' Add the column.
DataGridView1.Columns.Insert(DataGridView1.Columns.Count, Details)

Once you've performed this easy task, you can intercept the CellClick event to perform another action (Figure 3-14 shows the result of this simple test):

Private Sub DataGridView1_CellClick(ByVal sender As System.Object, _
  ByVal e As System.Windows.Forms.DataGridViewCellEventArgs) _
  Handles DataGridView1.CellClick
    
    If DataGridView1.Columns(e.ColumnIndex).Name = "Details" Then
        MessageBox.Show("You picked " & _
        DataGridView1.Rows(e.RowIndex).Cells("CustomerID").Value)
    End If
    
End Sub

Figure 3-14. Using a button column

Using a button column

Creating an image column is just as easy. In this case, you simply create and add a new DataGridViewImageColumn object. If you want to show the same static image in each cell, simply set the Image property with the Image object you want to use.

A more sophisticated technique is to show a separate image for each record. You can draw this record from a binary field in the database, or read it from a file specified in a string field. In either case, the technique is basically the same. First of all, you hide the column that contains the real data (the raw binary information for the picture, or the path to the file) by setting its Visible property to False. Then, you create a new DataGridViewImageColumn:

DataGridView1.DataSource = ds
DataGridView1.DataMember = "pub_info"
    
' Hide the binary data.
DataGridView1.Columns("logo").Visible = False
    
' Add an image column.
Dim ImageCol As New DataGridViewImageColumn( )
ImageCol.Name = "Image"
ImageCol.Width=200
DataGridView1.Columns.Add(ImageCol)

Finally, you can set the binary picture data you need:

For Each Row As DataGridViewRow In DataGridView1.Rows
    ' First, you must convert the binary data to a memory stream.
    ' Then, you can use the memory stream to create an Image object.
    Try
        Dim ImageBytes( ) As Byte = Row.Cells("logo").Value
    
        Dim ms As New MemoryStream(ImageBytes)
        Dim img As Image = Image.FromStream(ms)
    
        ' Finally, bind the image column.
        Dim ImageCell As DataGridViewImageCell = CType(Row.Cells("Image"), _
          DataGridViewImageCell)
        ImageCell.Value = img
    
        ' Now you can release the original information to save space.
        Row.Cells("logo").Value = New Byte( ) {  }
    
        Row.Height = 100
    Catch
        ' Ignore errors from invalid images.
    End Try
    
Next

Figure 3-15 shows the DataGridView with image data.

Figure 3-15. Using an image column

Using an image column

Note

In many cases, DataGridView is intelligent enough to recognize image data types and use them seamlessly in image columns, with no conversion required. However, if any extra work is required (e.g., converting or removing extra header information), you need to use the technique shown here.

Where can I learn more?

So, you want to do even more with the DataGridView control? Because it is one of the key showpieces of the new .NET Windows Forms toolkit, there's a lot of online documentation for the DataGridView. Look up the index entry "DataGridView control (Windows Forms)" in the MSDN help, and you'll find nearly 100 entries detailing distinct features you can add to solutions that use the DataGridView!

Personal tools