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

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

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

1112 CHAPTER 29 PROGRAMMING WITH WPF CONTROLS

<ListBox Grid.Column="0"

Grid.Row="2" Name="allCars" SelectionChanged="ListItemSelected" Background="LightBlue" ItemsSource="{Binding}"> <ListBox.ItemTemplate>

<DataTemplate>

<StackPanel Orientation="Horizontal">

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

<TextBlock FontStyle="Italic" FontSize="14" Text="{Binding Path=PetName}"/> </StackPanel>

</DataTemplate>

</ListBox.ItemTemplate>

</ListBox>

Here we connect our <DataTemplate> to the ListBox using the <ListBox.ItemTemplate> element. Before we see the result of this data template, implement the SelectionChanged handler of your ListBox to display the ToString() of the current selection within the rightmost TextBlock:

private void ListItemSelected(object sender, SelectionChangedEventArgs e)

{

//Get correct car from the ObservableCollection based

//on the selected item in the list box. Then call toString(). txtCarStats.Text = myCars[allCars.SelectedIndex].ToString();

}

With this update, you should now see a more stylized display of our data, as shown Figure 29-34.

Figure 29-34. Data binding with a custom data template

Binding UI Elements to XML Documents

The next task is to build a custom dialog box that will use data binding to display the content of an external XML file within a stylized ListView object. First, insert the Inventory.xml file you created in Chapter 24 during the NavigationWithLinqToXml project using the Project Add Existing Item menu option. Select this item within the Solution Explorer, and using the Properties window, set the Copy to Output Directory option to Copy Always. This will ensure that when you compile your application, the Inventory.xml file will be copied to your \bin\Debug folder.

CHAPTER 29 PROGRAMMING WITH WPF CONTROLS

1113

Building a Custom Dialog

Insert a new WPF Window type into your project (named AddNewCarDialog) using the Project Add Window menu option of Visual Studio 2008. This new Window will display the content of the Inventory.xml file within a customized ListView type, via data binding. The first step is to author the XAML to define the look and feel of this new window. Here is the full markup, with analysis to follow:

<Window x:Class="CarViewerApp.AddNewCarDialog" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="AddNewCarDialog" Height="234" Width="529" ResizeMode="NoResize" WindowStartupLocation="CenterScreen" >

<Grid>

<Grid.RowDefinitions> <RowDefinition Height="144" /> <RowDefinition Height="51" />

</Grid.RowDefinitions>

<!-- Use the XmlDataProvider-->

<Grid.Resources>

<XmlDataProvider x:Key="CarsXmlDoc" Source="Inventory.xml"/>

</Grid.Resources>

<!-- Now, build a grid of data, mapping attributes/elements to columns using XPath expressions -->

<ListView Name="lstCars" Grid.Row="0"ItemsSource=

"{Binding Source={StaticResource CarsXmlDoc}, XPath=/Inventory/Car}"

>

<ListView.View>

<GridView>

<GridViewColumn Width="100" Header="ID" DisplayMemberBinding="{Binding XPath=@carID}"/>

<GridViewColumn Width="100" Header="Make" DisplayMemberBinding="{Binding XPath=Make}"/>

<GridViewColumn Width="100" Header="Color" DisplayMemberBinding="{Binding XPath=Color}"/>

<GridViewColumn Width="150" Header="Pet Name" DisplayMemberBinding="{Binding XPath=PetName}"/>

</GridView>

</ListView.View>

</ListView>

<WrapPanel Grid.Row="1">

<Label Content="Select a Row to Add to your car collection" Margin="10" /> <Button Name="btnOK" Content="OK" Width="80" Height="25"

Margin="10" IsDefault="True" TabIndex="1" Click="btnOK_Click"/> <Button Name="btnCancel" Content="Cancel" Width="80" Height="25"

Margin="10" IsCancel="True" TabIndex="2"/> </WrapPanel>

</Grid>

</Window>

Starting at the top, notice that the opening <Window> element has been defined by specifying a value of NoResize to the ResizeMode attribute, given that most dialog boxes do not allow the user to alter the size of the window’s dimensions.

1114 CHAPTER 29 PROGRAMMING WITH WPF CONTROLS

Beyond carving up our <Grid> into two rows of a given size, the next point of interest is that we are placing into the grid’s resource dictionary a new object of type XmlDataProvider. This type can be connected to an external *.xml file (or an XML data island within the XAML file) via the Source attribute. As we have configured the Inventory.xml file to be located within the application directory of our current project, we have no need to worry about hard-coding a fixed path.

The real bulk of this markup takes place within the definition of the ListView type. First of all, notice that the ItemsSource attribute has been assigned to the CarsXmlDoc resource, which is qualified using the XPath attribute. Based on your experience, you may know that XPath is an XML technology that allows you to navigate within an XML document using a query-like syntax. Here we are saying that our initial data-binding path begins with the <Car> element of the <Inventory> root.

To inform the ListView type to display a grid-like front end, we next make use of the <ListView.View> element to define a <GridView> consisting of four <GridViewColumns>. Each of these types specifies a Header value (for display purposes) and most importantly a DisplayMemberBinding data-binding value. Given that the <ListView> itself has already specified the initial path within the XML document to be the <Car> subelement of <Inventory>, each of the XPath bindings for the column types use this as a starting point.

The first <GridViewColumn> is displaying the ID attribute of the <Car> element using an XPath- specific syntax for plucking our attribute values (@caID). The remaining columns simply further qualify the path within the XML document by appending the next subelement using the XPath qualifier of the {Binding} markup extension.

Last but not least, the final row of the <Grid> contains a <WrapPanel> that contains two Buttons (and a descriptive Label) to complete the UI. The only points of interest here would be that we are handling the Click event of the OK button and the use of the IsDefault and IsCancel properties.

These establish which button on a window should respond to the Click event when the Enter key or Esc key is pressed.

Finally, note that these Button types specify a TabIndex value and a Margin value, the latter of which allows you to define spacing around each item in the <WrapPanel>.

Assigning the DialogResult Value

Before we display this new dialog box, we need to implement the Click handler for the OK button. Similar to Windows Forms (see Chapter 27), WPF dialog boxes can inform the caller which button has been clicked via the DialogResult property. However, unlike the DialogResult property found in Windows Forms, in the WPF model, this property operates on a nullable Boolean value, rather than a strongly typed enumeration. Thus, if you wish to inform the caller the user wishes to employ the data in the dialog box for use within the program (typically indicated by clicking an OK, a Yes, or an Accept button), set the inherited DialogResult property to true in the Click handler of said button:

private void btnOK_Click(object sender, RoutedEventArgs e)

{

DialogResult = true;

}

As the default value of DialogResult is false, we have no need to do anything if the user clicks the Cancel button.

Obtaining the Current Selection

Finally, add a custom read-only property to your AddNewCarDialog named SelectedCar, which returns a new Car object to the caller based on the values of the selected row of the grid:

public Car SelectedCar

{

get

Note

CHAPTER 29 PROGRAMMING WITH WPF CONTROLS

1115

{

//Cast selected item on grid to an XmlElement.

System.Xml.XmlElement carRow = (System.Xml.XmlElement)lstCars.SelectedItem;

//Make sure the user selected something!

if (carRow == null)

{

return null;

}

else

{

//Generate a random speed.

Random r = new Random(); int speed = r.Next(100);

//Return new Car based on the data in selected XmlElement/speed. return new Car(speed, carRow["Make"].InnerText,

carRow["Color"].InnerText, carRow["PetName"].InnerText);

}

}

}

Notice we cast the return value of the SelectedItem property (which is of type System.Object) into an XmlElement type. This is possible because our ListView is indeed connected to the Inventory.xml file via our data-binding operation. Once we nab the current XmlElement, we are able to access the Make, Color, and PetName elements (using the type indexer) and extract out the values by calling InnerText.

If you have never worked with the types of the System.Xml namespace, simply know that the InnerText property obtains the value between the opening and closing elements of an XML node. For example, the inner text of <Make>Ford</Make> would be Ford.

Displaying a Custom Dialog Box

Now that our dialog box is complete, we are able to launch it from the Click handler of the File Add New Car menu option:

private void AddNewCarWizard(object sender, RoutedEventArgs e)

{

AddNewCarDialog dlg = new AddNewCarDialog(); if (true == dlg.ShowDialog())

{

if (dlg.SelectedCar != null)

{

myCars.Add(dlg.SelectedCar);

}

}

}

Like Windows Forms, a WPF dialog box may be shown as a modal dialog box (by calling ShowDialog()) or as a modaless dialog (by calling Show()). If the return value of ShowDialog() is true, we ask the dialog box for the new Car object and add it to our ObservableCollection<T>. Because this collection type sends out notifications when its contents are altered, you will find your ListBox

1116 CHAPTER 29 PROGRAMMING WITH WPF CONTROLS

will automatically refresh itself as you insert new items. Figure 29-35 shows the UI of our custom dialog box.

Figure 29-35. A custom grid of data, bound to an XML document

Source Code The CarViewerApp project can be found under the Chapter 29 subdirectory.

That wraps up our look at the WPF data-binding engine and the core controls found within this UI API. In the next chapter, you will complete your investigation of Windows Presentation Foundation by examining the role of graphical rendering, resource management, and the construction of custom themes.

Summary

This chapter examined several aspects of WPF controls, beginning with a discussion of dependency properties and routed events. These WPF mechanisms are very important for several aspects of WPF programming including data binding, animation services, and a slew of other features. Over the course of this chapter, you have had a chance to configure and tweak several controls and learned to arrange your UI content in various panel types.

More importantly, you examined the use of WPF commands. Recall that these control-agnostic events can be attached to a UI element or an input gesture to automatically inherit out-of-the-box services (such as clipboard operations). You also dove into the mechanics of the WPF data-binding engine and learned how to bind property values, custom objects, and XML documents to your UI layer. At this time, you also learned how to build WPF dialog boxes and discovered the role of the

IValueConverter and ObservableCollection<T> types.

C H A P T E R 3 0

WPF 2D Graphical Rendering,

Resources, and Themes

The purpose of this chapter is to examine three ultimately independent, yet interrelated topics, which will enable you to build more sophisticated Windows Presentation Foundation (WPF) applications than shown in the previous two chapters. The first order of business is to investigate the WPF 2D graphical programming APIs. Here you will examine numerous ways to render 2D geometric images (via shapes, drawings, and visuals) and work with graphical primitives such as brushes and pens. Along the way, you will also be introduced to the topic of WPF animation services and the types of the System.Windows.Media.Animation namespace.

Once you understand the basic 2D graphical rendering/animation primitives of WPF, we will then move on to an examination of how WPF allows you to define, embed, and reference application resources. Generally speaking, the term “application resources” refers to string tables, image files, icons, and other non-code-based entities used by an application. While this is still true under WPF, a “resource” can also represent custom graphical and UI objects you wish to embed into an assembly for later use.

The final topic of this chapter closes the gap between these two seemingly unrelated topics by examining how to define styles and templates for your control types. As you will see, creating styles and templates almost always involves making extensive use of WPF’s graphical rendering/animation services and packaging them into your assembly as application resources.

Note You may recall from Chapter 28 that WPF provides comprehensive support for 3D graphics programming, which is beyond the scope of this text. If you require details regarding this aspect of WPF, check out Pro WPF in C# 2008: Windows Presentation Foundation with .NET 3.5, Second Edition by Matthew MacDonald (Apress, 2008).

The Philosophy of WPF Graphical Rendering

Services

WPF makes use of a particular flavor of graphical rendering that goes by the term retained mode graphics. Simply put, this means that as you are using XAML or procedural code to generate graphical renderings, it is the responsibility of WPF to persist these visual items and ensure they are correctly redrawn and refreshed in an optimal manner. Thus, when you render graphical data, it is always present regardless of whether the end user hides the image by resizing the window, minimizing the window, covering the window with another, and so forth.

In stark contrast, previous Microsoft graphical rendering APIs (including GDI+) were immedi-

ate mode graphics systems. Under this model, it is up to the programmer to ensure that rendered

1117

1118 CHAPTER 30 WPF 2D GRAPHICAL RENDERING, RESOURCES, AND THEMES

visuals are correctly “remembered” and updated during the life of the application. For example, recall from Chapter 27 that under GDI+, rendering a rectangle involves handling the Paint event (or overriding the virtual OnPaint() method), obtaining a Graphics object to draw the rectangle and, most important, adding the infrastructure to ensure that the image is persisted when the user resizes the window (e.g., create member variables to represent the position of the rectangle, call Invalidate() throughout your program, etc.). This conceptual shift from immediate mode to retained mode graphics is indeed a good thing, as programmers have much less grungy graphics code to author and maintain.

However, this point is not to suggest that the WPF graphics API is completely different from earlier rendering toolkits. For example, like GDI+, WPF supports various brush types and pen types, the ability to render graphics at runtime through code, techniques for hit-testing support, and so forth. So to this end, if you currently have a background in GDI+ (or C/C++-based GDI), you already know a good deal about how to perform basic renderings under WPF.

WPF Graphical Rendering Options

Like other aspects of WPF development, you have a number of choices regarding how you will perform your graphical rendering, above and beyond the decision to do so via XAML or procedural C# code. Specifically, WPF provides three distinct ways to render graphical data:

System.Windows.Shapes: This namespace defines a number of types used to render 2D geometric objects (rectangles, ellipses, polygons, etc.). While these types are very simple to use, they do come with a good deal of overhead.

System.Windows.Media.Drawing: This abstract base class defines a more lightweight set of services for a number of derived types (GeometryDrawing, ImageDrawing, etc.).

System.Windows.Media.Visual: This abstract base class provides the most lightweight approach to render graphical data; however, it also requires authoring a fair amount of procedural code.

The motivation behind offering a number of different ways to do the exact same thing (e.g., render graphical data) has to do with memory usage and ultimately application performance. Given that WPF is such a graphically intensive system, it is not unreasonable for an application to render hundreds of different images upon a window’s surface, and your choice of implementation (shapes, drawings, or visuals) could have a huge impact.

To set the stage for the sections to come, let’s begin with a brief overview of each option, from the “heaviest” to the “lightest.” If you wish to try out each option firsthand, create a new WPF Windows Application named WPFGraphicsOptions, change the name of your initial Window type to MainWindow, and replace the window’s initial XAML <Grid> definition with a <StackPanel> type:

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

<StackPanel>

</StackPanel>

</Window>

Use of the Shape-Derived Types

The members of the System.Windows.Shapes namespace (Ellipse, Line, Path, Polygon, Polyline, and

Rectangle) are the absolute simplest way to render a 2D image and are appropriate when you need

CHAPTER 30 WPF 2D GRAPHICAL RENDERING, RESOURCES, AND THEMES

1119

to render infrequently used images (such as a region of a stylized button) or you need a graphical image that can support user interaction. Using these types typically entails selecting a “brush” for the interior fill and a “pen” for the border outline (you’ll examine WPF’s brush and pen options later in this chapter). To illustrate the basic use of shape types, if you add the following to your <StackPanel>, you will find a simple light blue rectangle with a blue outline:

<!-- Draw a rectangle using Shape types -->

<Rectangle Height="55" Width="105" Stroke="Blue" StrokeThickness="5" Fill="LightBlue"/>

While these types are ridiculously simple to use, they are a bit on the bloated side due to the fact that their parent class, Sysem.Windows.Shapes.Shape, attempts to be all things to all people (if you will). Shape inherits a ton of services from its long list of parents in the inheritance chain: Shape is-a FrameworkElement, which is-a UIElement, which is-a Visual, which is-a DependencyObject, DispatcherObject, and Object!

Collectively, these base classes provide the derived types with support for styles and temples, data binding support, resource management, the ability to send numerous events, the ability to monitor keyboard and mouse input, complex layout management, and animation services. Figure 30-1 illustrates the inheritance of the Shape type, as seen through the eyes of the Visual Studio object browser.

Figure 30-1. Shape-derived types gain a ton of functionality from their parent types.

While each parent adds various bits of functionality, UIElement is a key culprit. For example, UIElement defines over 80 events to handle numerous forms of input (mouse, keyboard, and stylus for Tablet PCs). FrameworkElement is the other suspect, in that this type provides members for changing the mouse cursor, various events representing object lifetime, context menus support, and so on. Given this, the Shape-derived types can be as interactive as other UI elements such as Buttons, ProgressBars, and other widgets.

The bottom line is that while the Shape-derived types are very simple to use and quite powerful, this very fact makes them the heaviest option for rendering 2D graphics. Again, using these types is just fine for “occasional rendering” (the definition of which can be more of a gut feel than a hard science) or when you honestly do need a graphical rendering that is responsive to user interaction.

However, if you are designing a more graphically intensive application, you will likely find some performance gains by using the Drawing-derived types.

Use of the Drawing-Derived Types

The System.Windows.Media.Drawing abstract base class represents the bare bones of a twodimensional surface. The derived types (such as GeometryDrawing, ImageDrawing, and VideoDrawing) are more lightweight than the Shape-derived types in that neither UIElement nor FrameworkElement is

1120 CHAPTER 30 WPF 2D GRAPHICAL RENDERING, RESOURCES, AND THEMES

in the inheritance chain. Given this, Drawing-derived types do not have intrinsic support for handling input events (although it is possible to programmatically perform hit-testing logic); however, they can be animated due to the fact that Animatable is in the family (see Figure 30-2).

Figure 30-2. Drawing-derived types are significantly more lightweight than Shape-derived types.

Another key difference between Drawing-derived types and Shape-derived types is that Drawing-derived types have no ability to render themselves, as they do not derive from UIElement! Rather, derived types must be placed within a hosting object (such as DrawingImage, DrawingBrush, or DrawingVisual) to display their content. This decoupling of composition from display makes the Drawing-derived types much more lightweight than the Shape-derived types, while still retaining key services.

Without getting too hung up on the details for the time being, consider how the previous Rectangle could be rendered using the drawing-centric types (add this markup directly after your previous <Rectangle> if you are following along):

<!-- Draw a rectangle using Drawing types -->

<Image Height="55" Width="105"> <Image.Source>

<DrawingImage>

<DrawingImage.Drawing> <GeometryDrawing Brush="LightBlue">

<GeometryDrawing.Pen>

<Pen Brush="Blue" Thickness="5"/> </GeometryDrawing.Pen> <GeometryDrawing.Geometry>

<RectangleGeometry Rect="0,0,100,50"/> </GeometryDrawing.Geometry>

</GeometryDrawing>

</DrawingImage.Drawing>

</DrawingImage>

</Image.Source>

</Image>

While the output is identical to the previous <Rectangle>, it is clearly more verbose. What we have here is the classic “more code for better performance” dilemma. Thankfully, when you make use of XAML graphical design tools (such as Microsoft Expression Blend or Microsoft Expression Design), the underlying markup is rendered behind the scenes (see Chapter 28 for details of the Microsoft Expression product family).

Use of the Visual-Derived Types

The abstract System.Windows.Media.Visual class type provides a minimal and complete set of services to render a derived type (rendering, hit testing, transformation), but it does not provide support for addition nonvisual services, which can lead to code bloat (input events, layout services, styles, and data binding). Given this, the Visual-derived types (DrawingVisual, Viewport3DVisual, and

CHAPTER 30 WPF 2D GRAPHICAL RENDERING, RESOURCES, AND THEMES

1121

ContainerVisual) are the most lightweight of all graphical rendering options and offer the best performance. Notice the simple inheritance chain of the Visual type, as shown in Figure 30-3.

Figure 30-3. The Visual type provides basic hit testing, coordinate transformation, and bounding box calculations.

Because the Visual type exposes the lowest level of functionality, it has limited support for direct XAML definitions (unless you use a Visual within a type that can be expressed in XAML). Using these types feels a bit closer to making use of GDI/GDI+ rendering APIs, in that they are often manipulated through procedural code. When doing so, you are manually populating the object graph representing your window with custom Visual-derived types. Furthermore, you are required to override various virtual methods that will be called by the WPF graphics system to figure out how many items you are rendering, and the Visual item itself to be rendered.

To illustrate how you can use the Visual-derived types to render 2D data, open the code file for your main window type and comment out the entire definition (so you can restore it shortly, with minimal effort):

/*

public partial class MainWindow : System.Windows.Window

{

public MainWindow()

{

InitializeComponent();

}

}

*/

Now create the following Window-derived type that renders a rectangle directly on the surface of the window, bypassing any content defined in the XAML markup (your previous XAML descriptions will be ignored and not displayed):

public partial class MainWindow : System.Windows.Window

{

// Our single drawing visual.

private DrawingVisual rectVisual = new DrawingVisual(); private const int NumberOfVisualItems = 1;

public MainWindow()

{

InitializeComponent();

// Helper function to create the rectangle.

CreateRectVisual();

}

private void CreateRectVisual()

{

using (DrawingContext drawCtx = rectVisual.RenderOpen())

{

// The top, left, bottom, and right position of the rectangle.