Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Pro CSharp 2008 And The .NET 3.5 Platform [eng]

.pdf
Скачиваний:
78
Добавлен:
16.08.2013
Размер:
22.5 Mб
Скачать

1072 CHAPTER 29 PROGRAMMING WITH WPF CONTROLS

}

protected void repeatAddValueButton_Click(object sender, RoutedEventArgs e)

{

// Add 1 to the current value and show in label. currValue++;

lblCurrentValue.Content = currValue;

}

protected void repeatRemoveValueButton_Click(object sender, RoutedEventArgs e)

{

// Subtract 1 from the current value and show in label. currValue--;

lblCurrentValue.Content = currValue;

}

}

As you can see, when the user clicks either RepeatButton, we increment or decrement the private currValue accordingly, and set the Content property of the Label type. Figure 29-6 shows our custom spin button UI in action.

Figure 29-6. Building a spin button using RepeatButton as a starting point

Source Code The CustomSpinButtonApp project is included under the Chapter 29 subdirectory.

Working with CheckBoxes and RadioButtons

As mentioned previously, CheckBox “is-a” ToggleButton, which “is-a” ButtonBase, which may seem very odd given that the UI of a button looks very different from that of a check box. However, a CheckBox type, like a Button, can be clicked, responds to mouse and keyboard input, and follows the WPF content model. Given all of these similarities, it turns out that the CheckBox type simply overrides various virtual members of ToggleButton to establish a check box look and feel (recall that a major motivator of WPF is to decouple the display of a control from its functionality). Consider the following <CheckBox> declarations, which yield the output shown in Figure 29-7:

<StackPanel>

<!-- CheckBox types -->

<CheckBox Name ="checkInfo" >Send me more information</CheckBox> <CheckBox Name ="checkPhoneContact" >Contact me over the phone</CheckBox>

</StackPanel>

Figure 29-7. Simple CheckBox types

CHAPTER 29 PROGRAMMING WITH WPF CONTROLS

1073

RadioButton is another type that “is-a” ToggleButton. Unlike the CheckBox type, however, it has the innate ability to ensure all RadioButtons in the same container (such as a StackPanel, Grid, or whatnot) are mutually exclusive without any additional work on your part. Consider the following:

<StackPanel>

<!--

RadioButton types for music selection --

>

<Label FontSize = "15" Content = "Select Your Music Media"/> <RadioButton>CD Player</RadioButton>

<RadioButton>MP3 Player</RadioButton> <RadioButton>8-Track</RadioButton>

<!--

RadioButton types for color selection --

>

<Label FontSize = "15" Content = "Select Your Color Choice"/> <RadioButton>Red</RadioButton> <RadioButton>Green</RadioButton> <RadioButton>Blue</RadioButton>

</StackPanel>

If you were to test this XAML, you would find that you can only select one of the six options, which is probably not what is intended, as there seem to be two separate groups within the mix (radio options and color options).

Establishing Logical Groupings

When you wish to have a single container with multiple RadioButton types, which behave as distinct physical groupings, you can do so setting the GroupName property on the opening element of the

RadioButton type:

<StackPanel>

<!-- The Music group -->

<Label FontSize = "15" Content = "Select Your Music Media"/> <RadioButton GroupName = "Music" >CD Player</RadioButton> <RadioButton GroupName = "Music" >MP3 Player</RadioButton> <RadioButton GroupName = "Music" >8-Track</RadioButton>

<!—The Color group (optional for this example, see Note below) -->

<Label FontSize = "15" Content = "Select Your Color Choice"/> <RadioButton GroupName = "Color">Red</RadioButton> <RadioButton GroupName = "Color">Green</RadioButton> <RadioButton GroupName = "Color">Blue</RadioButton>

</StackPanel>

With this, we will now be able to set each logical grouping independently, even though they are in the same physical container.

Note By default, all RadioButtons in a container that do not have a GroupName value work as a single physical group. Therefore, in the previous example, the color-centric buttons would have been mutually exclusive, even with the GroupName omitted, given the presence of the Music group.

Framing Related Elements in GroupBoxes

When you design a collection of radio buttons or check boxes, it is common to surround them with a visual container to denote that they behave as a group. The most common way to do so is using a GroupBox control. As the Header property is prototyped to operate on a System.Object, you are able

1074 CHAPTER 29 PROGRAMMING WITH WPF CONTROLS

to assign any object to function as the header (a simple string, a colored rectangle, a button, etc.). Consider the following two GroupBox declarations, which frame the previous RadioButtons in various manners:

<StackPanel>

<GroupBox Header = "Select Your Music Media" BorderBrush ="Black">

<StackPanel>

<RadioButton GroupName = "Music" >CD Player</RadioButton> <RadioButton GroupName = "Music" >MP3 Player</RadioButton> <RadioButton GroupName = "Music" >8-Track</RadioButton>

</StackPanel>

</GroupBox>

<GroupBox BorderBrush ="Black">

<GroupBox.Header>

<Label Background = "Blue" Foreground = "White"

FontSize = "15" Content = "Select your color choice"/> </GroupBox.Header>

<StackPanel>

<RadioButton>Red</RadioButton>

<RadioButton>Green</RadioButton>

<RadioButton>Blue</RadioButton>

</StackPanel>

</GroupBox>

</StackPanel>

The output can be seen in Figure 29-8.

Figure 29-8. GroupBox types framing RadioButton types

Framing Related Elements in Expanders

In addition to the customary group box, WPF ships with a new UI element that can group a collection of UI elements that can be hidden or shown via a toggle. This element, the Expander type, allows you to define the direction elements will be displayed (up, down, left, or right) using the ExpandDirection property. Consider the following XAML (which basically just changes <GroupBox> to <Expander>):

<StackPanel>

<Expander Header = "Select Your Music Media" BorderBrush ="Black">

<StackPanel>

<RadioButton GroupName = "Music" >CD Player</RadioButton> <RadioButton GroupName = "Music" >MP3 Player</RadioButton> <RadioButton GroupName = "Music" >8-Track</RadioButton>

</StackPanel>

</Expander>

<Expander BorderBrush ="Black">

<Expander.Header>

<Label Background = "Blue" Foreground = "White"

CHAPTER 29 PROGRAMMING WITH WPF CONTROLS

1075

FontSize = "15" Content = "Select your color choice"/> </Expander.Header>

<StackPanel>

<RadioButton>Red</RadioButton>

<RadioButton>Green</RadioButton>

<RadioButton>Blue</RadioButton>

</StackPanel> </Expander >

</StackPanel>

Figure 29-9 shows each Expander in the collapsed state.

Figure 29-9. Collapsed Expanders

Figure 29-10 shows each Expander (pardon the redundancy) expanded.

Figure 29-10. Expanded Expanders

Source Code The CheckRadioGroup.xaml file is included under the Chapter 29 subdirectory.

Working with the ListBox and ComboBox Types

As you would hope, WPF provides types that contain a group of selectable items, such as ListBox and ComboBox, both of which derive from the ItemsControl abstract base class. Most importantly, this parent class defines a property named Items, which returns a strongly typed ItemCollection object that holds onto the subitems. As it turns out, the ItemCollection type has been constructed to operate on System.Object types, and therefore it can contain anything whatsoever. If you wish to fill an ItemsControl-derived type with simply textual data via markup, you can do so using a set of <ListBoxItem> types. For example, consider the following XAML:

<!-- Simple list box -->

<ListBox Name = "lstVideoGameConsoles"> <ListBoxItem>Microsoft XBox 360</ListBoxItem> <ListBoxItem>Sony Playstation 3</ListBoxItem> <ListBoxItem>Nintendo Wii</ListBoxItem> <ListBoxItem>Sony PSP</ListBoxItem> <ListBoxItem>Nintendo DS</ListBoxItem>

</ListBox>

1076 CHAPTER 29 PROGRAMMING WITH WPF CONTROLS

<!-- Simple combo box -->

<ComboBox Name = "comboVideoGameConsoles"> <ListBoxItem>Microsoft XBox 360</ListBoxItem> <ListBoxItem>Sony Playstation 3</ListBoxItem> <ListBoxItem>Nintendo Wii</ListBoxItem> <ListBoxItem>Sony PSP</ListBoxItem> <ListBoxItem>Nintendo DS</ListBoxItem>

</ComboBox>

Note ComboBox types can also be populated using <ComboBoxItem> elements, rather than <ListBoxItem>. By doing so, you gain access to the IsHighlighted property, which is not used by the ListBoxItem type.

Not surprisingly, we find the rendering shown in Figure 29-11.

Figure 29-11. A simple ListBox and ComboBox

Filling List Controls Programmatically

Oftentimes, the data contained within a list control is not known until runtime; for example, you may need to fill items in a list box based on values returned from a database read, invoking a WCF service, or reading an external file. When you need to populate a ListBox or ComboBox control programmatically, simply use the members of the ItemCollection type to do so (Add(), Remove(), etc.). Assume you have a new Visual Studio 2008 WPF Application project named ListControls. The previous XAML declaration of the lstVideoGameConsole type could be defined in XAML as follows:

<Window x:Class="ListControls.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="ListControls" Height="300" Width="300" >

<StackPanel>

<!-- This is filled via code -->

<ListBox Name = "lstVideoGameConsoles"> </ListBox>

</StackPanel>

</Window>

and populated in a related code file as follows:

public partial class MainWindow : System.Windows.Window

{

public MainWindow()

{

InitializeComponent();

CHAPTER 29 PROGRAMMING WITH WPF CONTROLS

1077

FillListBox();

}

private void FillListBox()

{

// Add items to the list box. lstVideoGameConsoles.Items.Add("Microsoft XBox 360"); lstVideoGameConsoles.Items.Add("Sony Playstation 3"); lstVideoGameConsoles.Items.Add("Nintendo Wii"); lstVideoGameConsoles.Items.Add("Sony PSP"); lstVideoGameConsoles.Items.Add("Nintendo DS");

}

}

One thing that might strike you as odd is that in the XAML description of the ListBox, we made use of <ListBoxItem> types to populate the items; however, here we have made use of string types when calling the Add() method. The short explanation is that when using XAML, <ListBoxItem> types are more convenient in that they are defined within the http://schemas.microsoft.com/ winfx/2006/xaml/presentation XML namespace, and therefore we have a direct reference to them.

Under the hood, ToString() is called on each <ListBoxItem> type, so the end result is identical. If you truly wanted to use a System.String to fill the ListBox (or ComboBox) type in XAML, you would need to define a new XML namespace to bring in mscorlib.dll (see Chapter 28 for more details):

<StackPanel xmlns:CorLib = "clr-namespace:System;assembly=mscorlib"> <ListBox Name = "lstVideoGameConsoles">

<CorLib:String>Microsoft XBox 360</CorLib:String> <CorLib:String>Sony Playstation 3</CorLib:String> <CorLib:String>Nintendo Wii</CorLib:String> <CorLib:String>Sony PSP</CorLib:String> <CorLib:String>Nintendo DS</CorLib:String>

</ListBox>

</StackPanel>

Conversely, if you really wanted to, you could programmatically populate an ItemsControl- derived type using strongly typed ListBoxItem objects; however, you really gain nothing for the current example and have in fact created additional work for yourself (as the ListBoxItem does not have a constructor to set the Content property!).

Adding Arbitrary Content

Because ListBox and ComboBox both have ContentControl in their inheritance chain, they can contain data well beyond a simple string. Consider the following ComboBox, which contains various <StackPanels> containing 2D graphical objects and a descriptive label:

<StackPanel>

<!-- A ListBox with content! -->

<ListBox Name = "lstColors"> <StackPanel Orientation ="Horizontal">

<Ellipse Fill ="Yellow" Height ="50" Width ="50"/>

<Label FontSize ="20" HorizontalAlignment="Center" VerticalAlignment="Center">Yellow</Label>

</StackPanel>

<StackPanel Orientation ="Horizontal">

<Ellipse Fill ="Blue" Height ="50" Width ="50"/>

<Label FontSize ="20" HorizontalAlignment="Center" VerticalAlignment="Center">Blue</Label>

</StackPanel>

1078 CHAPTER 29 PROGRAMMING WITH WPF CONTROLS

<StackPanel Orientation ="Horizontal">

<Ellipse Fill ="Green" Height ="50" Width ="50"/>

<Label FontSize ="20" HorizontalAlignment="Center" VerticalAlignment="Center">Green</Label>

</StackPanel>

</ListBox>

</StackPanel>

Figure 29-12 shows the output of our current list types.

Figure 29-12. ItemsControl-derived types can contain any sort of content you desire.

Determining the Current Selection

Once you have populated a ListBox or ComboBox type, the next obvious issue is how to determine at runtime which item the user has selected. As it turns out, you have three ways to do so. If you are interested in finding the numerical index of the item selected, you can use the SelectedIndex property (which is zero based; a value of -1 represents no selection). If you wish to obtain the object within the list that has been selected, the SelectedItem property fits the bill. Finally, the SelectedValue allows you to obtain the value of the selected object (typically obtained via a call to

ToString()).

Sounds simple enough, right? Well, to test how each property behaves, assume you have defined two new Button types for the current window, both of which handle the Click event:

<!-- Buttons to get the selected items -->

<Button Name ="btnGetGameSystem" Click ="btnGetGameSystem_Click"> Get Video Game System

</Button>

<Button Name ="btnGetColor" Click ="btnGetColor_Click"> Get Color

</Button>

The Click handler for btnGetGameSystem will obtain the values of the SelectedIndex,

SelectedItem, and SelectedValue properties of the lstVideoGameConsoles object and display them in a message box:

CHAPTER 29 PROGRAMMING WITH WPF CONTROLS

1079

protected void btnGetGameSystem_Click(object sender, RoutedEventArgs args)

{

string data = string.Empty;

data += string.Format("SelectedIndex = {0}\n", lstVideoGameConsoles.SelectedIndex);

data += string.Format("SelectedItem = {0}\n", lstVideoGameConsoles.SelectedItem);

data += string.Format("SelectedValue = {0}\n", lstVideoGameConsoles.SelectedValue);

MessageBox.Show(data, "Your Game Info");

}

If you were to select “Nintendo Wii” from the list of game consoles and click the related button, you would find the message box shown in Figure 29-13.

Figure 29-13. Finding a selected string

However, what about obtaining the selected color?

Determining the Current Selection for Nested Content

Assume the Click event handler for the btnGetColor Button has implemented btnGetColor_Click() to print out the current selection, index, and value of the lstColors ListBox object. Now, if you were to select the first item in the lstColors list box (and click the related button), you may be surprised to find the output shown in Figure 29-14.

Figure 29-14. Finding a selected . . . StackPanel?

The reason for this output is the fact that the lstColors object is maintaining three StackPanel objects, each of which contains nested content. Therefore, SelectedItem and SelectedValue are simply calling ToString() on the StackPanel type, which returns its fully qualified name.

While you would be able to simply figure out which item was selected using the numerical value returned from SelectedIndex, another approach is to drill into the StackPanel’s child

1080 CHAPTER 29 PROGRAMMING WITH WPF CONTROLS

collection to grab the Content value of the Label using the StackPanel’s internally maintained Children collection as follows:

protected void btnGetColor_Clicked(object sender, RoutedEventArgs args)

{

// Get the Content value in the selected Label in the StackPanel.

StackPanel selectedStack = (StackPanel)lstColors.Items[lstColors.SelectedIndex];

string color = ((Label)(selectedStack.Children[1])).Content.ToString();

string data = string.Empty;

data += string.Format("SelectedIndex = {0}\n", lstColors.SelectedIndex); data += string.Format("Color = {0}", color);

MessageBox.Show(data, "Your Game Info");

}

While this does the trick, this solution is very fragile in that we have hard-coded positions within the StackPanel (the second child, being the Label) and are required to perform numerous casting operations. Another alternative is to set the Tag property of each StackPanel, which is defined in the FrameworkElement base class:

<ListBox Name = "lstColors">

<StackPanel Orientation ="Horizontal" Tag ="Yellow">

...

</StackPanel>

<StackPanel Orientation ="Horizontal" Tag ="Blue">

...

</StackPanel>

<StackPanel Orientation ="Horizontal" Tag ="Green">

...

</StackPanel>

</ListBox>

Using this approach, our code cleans up considerably, as we can pluck out the value assigned to Tag programmatically as follows:

protected void btnGetColor_Clicked(object sender, RoutedEventArgs args)

{

string data = string.Empty;

data += string.Format("SelectedIndex = {0}\n", lstColors.SelectedIndex); data += string.Format("SelectedItem = {0}\n", lstColors.SelectedItem); data += string.Format("SelectedValue = {0}",

(lstColors.Items[lstColors.SelectedIndex] as StackPanel).Tag);

MessageBox.Show(data, "Your Color Info");

}

While this approach is a bit cleaner than our first attempt, there are other manners in which you can capture values from a complex control using data templates. To do so requires an understanding of the WPF data-binding engine, which you will examine at the conclusion of this chapter.

Source Code The ListControls project is included under the Chapter 29 subdirectory.

CHAPTER 29 PROGRAMMING WITH WPF CONTROLS

1081

Working with Text Areas

WPF ships with a number of UI elements that allow you to gather textual-based user input. The most primitive types would be TextBox and PasswordBox, which we will examine here using a new Visual Studio 2008 WPF Application named TextControls.

Working with the TextBox Type

Like other TextBox types you have used in the past, the WPF TextBox type can be configured to hold a single line of text (the default setting) or multiple lines of text if the AcceptReturn property is set to true. Information within a TextBox will always be treated as character data, and therefore the “content” is always a string type that can be set and retrieved using the Text property:

<TextBox Name ="txtData" Text = "Hello!" BorderBrush ="Blue" Width ="100"/>

One aspect of the WPF TextBox type that is very unique is that it has the built-in ability to check the spelling of the data entered within it by setting the SpellCheck.IsEnabled property to true. When you do so, you will notice that like Microsoft Office, misspelled words are underlined in a red squiggle. Even better, there is an underlying programming model that gives you access to the spellchecker engine, which allows you to get a list of suggestions for misspelled words.

Update your current window XAML definition to make use of a Label, TextBox, and Button as follows (notice this TextBox supports multiple lines of text and has enabled spell checking):

<Window x:Class="TextControls.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="TextControls" Height="204" Width="292" >

<StackPanel>

<Label FontSize ="15">Is this word spelled correctly?</Label> <TextBox SpellCheck.IsEnabled ="True" AcceptsReturn ="True"

Name ="txtData" FontSize ="12" BorderBrush ="Blue" Height ="100">

</TextBox>

<Button Name ="btnOK" Content ="Get Selections" Width = "100" Click ="btnOK_Click"/>

</StackPanel>

</Window>

With just this much functionality, you will already notice that when you type misspelled words into your TextBox, errors are marked as such. To complete our simple spell checker, update the Click event handler for the Button type as follows:

protected void btnOK_Click(object sender, RoutedEventArgs args)

{

string spellingHints = string.Empty;

// Try to get a spelling error at the current caret location.

SpellingError error = txtData.GetSpellingError(txtData.CaretIndex); if (error != null)

{

// Build a string of spelling suggestions. foreach (string s in error.Suggestions)

{

spellingHints += string.Format("{0}\n", s);