6.1. Creating and Using Resources
The term resource has a very broad meaningany object can
be a resource. A brush or a color used in various parts of a user interface
could be a resource. Snippets of graphics or text can be resources
. There is nothing special an object has to do to qualify as a resource. The
resource-handling infrastructure is entirely dedicated to making it possible to
get hold of the resource you require, and it doesn't care what the resource is.
It simply provides a mechanism for identifying and locating objects.
At the heart of resource management is the ResourceDictionary
class. This is a fairly simple collection class. It behaves much like an
ordinary Hashtable; it allows objects to be associated with keys, and
provides an indexer that lets you retrieve those objects using these keys. So
you could, in principle, use the ResourceDictionary like a Hashtable,
as Example 6-1 shows.
Example 6-1. ResourceDictionary programming
ResourceDictionary myDictionary = new ResourceDictionary( );
myDictionary.Add("Foo", "Bar");
myDictionary.Add("HW", "Hello, world");
Console.WriteLine(myDictionary["Foo"]);
Console.WriteLine(myDictionary["HW"]);
In practice, you will not often create your own ResourceDictionary
like this. Instead, you will normally use ones provided by WPF. For example,
the FrameworkElement base class, from which most user-interface
elements derive, provides a resource dictionary in its Resources property.
Moreover, this dictionary can be populated from markup, as
Example 6-2 shows.
Example 6-2. Populating a ResourceDictionary from XAML
<?Mapping XmlNamespace="urn:System" ClrNamespace="System" Assembly="mscorlib" ?>
<Window x:Class="ResourcePlay.Window1" Text="ResourcePlay"
xmlns="http://schemas.microsoft.com/winfx/avalon/2005"
xmlns:x="http://schemas.microsoft.com/winfx/xaml/2005"
xmlns:s="urn:System">
<Window.Resources>
<SolidColorBrush x:Key="Foo" Color="Green" />
<s:String x:Key="HW">Hello, world</s:String>
</Window.Resources>
<Grid Name="myGrid">
</Grid>
</Window>
The x:Key attribute specifies the key to be used for
the resource in the dictionary. In principle, you can use anything as a key,
but in practice, strings are the most common choice, although distinct object
instances stored in public properties are used for system resources.
 |
When you use XAML to populate a resource
dictionary, WPF may choose to defer the creation of the resources. Under
certain circumstances, it may choose to leave sections of the tree in their
serialized form (known as BAML), and only expand this into real objects on
demand. This can significantly improve the startup time for a user interface in
cases where not all of the objects are needed as soon as the UI appears. For
the most part, this optimization will not have any direct effect on your code's
behavior other than speeding it up. However, if there is something wrong with
your markup, this deferred creation can cause the resulting errors to emerge
later than you might have expected.
|
|
Example 6-3 shows
code retrieving the resources defined in
Example 6-2.
Example 6-3. Retrieving resources from an element's
ResourceDictionary
Brush b = (Brush) this.Resources["Foo"];
String s = (String) this.Resources["HW"];
Notice that this code accesses the ResourceDictionary using
this.Resources. This is all very well for the code-behind for the
markup that defined the resources. However it is not always this convenient to
get hold of the right dictionary. What if we want to define resources
accessible to all windows in the application? It would be both tedious and
inefficient to copy the same resources into every window in the application.
And what if we want a custom control to pick up resources specified by its
parent window, rather than baking them into the control? To solve these
problems, and to make it easy to achieve consistency across your user
interface, FrameworkElement extends the basic ResourceDictionary
facilities with a hierarchical resource scope .
6.1.1. Resource Scope
As well as offering a ResourceDictionary for every
element that wants one, FrameworkElement also provides a FindResource
method to retrieve resources. Example
6-4 shows the use of this method to retrieve the same resources as
Example 6-3.
Example 6-4. Using FindResource
Brush b = (Brush) this.FindResource("Foo");
String s = (String) this.FindResource("HW");
This may seem rather pointless: why does this FindResource
method exist when we could just use the dictionary's indexer as we did in
Example 6-3? The reason is that FindResource doesn't give up
if the resource is not in the specified element's resource dictionary. It will
search elsewhere.
The code in Example 6-5
uses the myGrid element from Example
6-2 instead of this. The Grid does not have any
resources, so this code will set the b1 variable to null.
However, because b2 is set using FindResources
instead of the resource dictionary indexer, WPF will consider all of the
resources in scope, not just those directly set on the Grid. It will
start at the Grid element, but will then examine the parent, the
parent's parent, and so on, all the way to the root element. (In this case, the
parent happens to be the root element, so this is a short search. But in
general, it will search as many elements as it needs to.) The result is that
the b2 variable is set to the same Brush object as was
retrieved in Example 6-3 and
Example 6-4.
Example 6-5. FrameworkElement.Resources versus
FindResource
// Returns null
Brush b1 = (Brush) myGrid.Resources["Foo"];
// Returns SolidColorBrush from Window.Resources
Brush b2 = (Brush) myGrid.FindResource("Foo");
It doesn't stop here. If FindResource gets all the way
to the root of the UI without finding the specified resource, it will then look
in the application. Not only do all framework elements have a Resources
property, so does the Application object.
Example 6-6 shows how to define application-scope resources in markup.
(If you are using the normal Visual Studio 2005 Avalon project template, you
would put this in the MyApp.xaml file.)
Example 6-6. Resources at application scope
<Application x:Class="ResourcePlay.MyApp"
xmlns="http://schemas.microsoft.com/winfx/avalon/2005"
xmlns:x="http://schemas.microsoft.com/winfx/xaml/2005"
StartingUp="AppStartingUp"
>
<Application.Resources>
<LinearGradientBrush x:Key="shady" StartPoint="0,0" EndPoint="1,1">
<LinearGradientBrush.GradientStops>
<GradientStop Offset="0" Color="Red"/>
<GradientStop Offset="1" Color="Black"/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Application.Resources>
</Application>
The application scope is handy for anything used throughout your
application. For example, if you use styles or control templates, you would
typically put these in the application resources, to ensure that you get a
consistent look across all the windows in your application.
Resource searching doesn't stop at
the application level. If a resource is not present in the UI tree or the
application, FindResource will finally consult the system scope, which
contains resources that represent system-wide settings, such as the configured
color for selected items or the scrollbar width.
Figure 6-1 shows
a typical hierarchy of resource sources. Several applications are running, each
application may have several windows, and each window has a tree consisting of
multiple elements. If FindResource is called on the element labeled
"1" in the figure, it will first look in that element's resource dictionary. If
that fails, it will keep working its way up the hierarchy through the numbered
items in order, until it reaches the system resources.
WPF uses the system scope to define brushes, fonts, and metrics
that the user can configure at a system-wide level. The keys for these are
provided as static properties of the SystemColors, SystemFonts,
and SystemParameters classes, respectively. (Between them, these
classes define over 400 resources, so they are not listed hereconsult the SDK
documentation for each class to see the complete set.)
Example 6-7 uses the system scope to retrieve a brush for the currently
configured tool tip background color. (See
Chapter 7 for more information on brushes.)
Example 6-7. Retrieving a system scope resource
Brush toolTipBackground = (Brush) myGrid.FindResource(SystemColors.InfoBrushKey);
 |
These system-resource classes use objects rather
than strings as resource keys. This avoids the risk of naming collisionsbecause
system resources are always identified by a specific object, there will never
be any ambiguity between system resources and your own named resources.
|
|
The system-resource classes also define static properties that
let you retrieve the relevant object directly rather than having to go via the
resource system. For example, SystemColors defines an InfoBrush
property, which returns the same value that FindResource returns when
passed SystemColors.InfoBrushKey. So rather than writing the code in
Example 6-7, we could have written the code in
Example 6-8.
Example 6-8. Retrieving a system resource through its
corresponding property
Brush toolTipBackground = SystemColors.InfoBrush;
When writing code, these properties are likely to be simpler to
use than the resource system. However, using the resource-key properties offers
three advantages. First, if you want to let the user change your application's
color scheme away from the system-wide default, you can override these system
settings by putting resources into the application scope.
Example 6-9 shows an application resource section that defines a new
application-wide value for the InfoBrushKey resource.
Example 6-9. Application overriding system colors
// (Hypothetical function for retrieving settings)
Color col = GetColorFromUserSettings( );
Application.Current.Resources[SystemColors.InfoBrushKey] =
new SolidColorBrush(Colors.Red);
This replacement value would be returned in
Example 6-7, but not in Example
6-8. This is because in Example
6-8, SystemColors has no way of knowing what scope you would
like to use, so it always goes straight to the system scope.
The second advantage offered by resource keys is that they
provide a straightforward way of using system-defined resources from markup.
Third, you can make your application respond automatically to changes in system
resources. Both of these last two benefits come from using resource references.
6.1.2. Resource References
So far, we have seen how to retrieve the current value of a
named resource in code. Since we usually use resource values to set element
properties, we will now look at how to set an element's property to the value
of a resource. This may seem like a ridiculously trivial step. You might expect
it to look like Example 6-10.
Example 6-10. How not to use a system resource value
this.Background = (Brush) this.FindResource(SystemColors.ControlBrushKey);
This will work up to a pointit will successfully set the Background
property to a brush that paints with whatever the currently selected color for
control backgrounds is at the moment when this line of code runs. However, if
the user changes this setting using the Display Properties control panel
applet, this Background property will not be updated automatically.
The code in Example 6-10 effectively
takes a snapshot of the resource value.
The code in Example
6-11 does not suffer from this problem. Instead of taking a snapshot,
it associates the Background property with the resource.
Example 6-11. Self-updating system resource reference
this.SetResourceReference(Window.BackgroundProperty, SystemColors.ControlBrushKey);
Unlike Example 6-10,
if the system resource value changes, the property will automatically receive
the new value. The practical upshot of this is that if the user changes the
color scheme using the Display Properties applet,
Example 6-11 will ensure that your user interface is updated
automatically.
WPF defines markup extensions that are the XAML equivalent of
the code in the previous two examples. (See
Appendix A for more information on markup extensions.) These are the StaticResource
and DynamicResource extensions. If you are using a system resource, or
any other resource that might change at runtime, choose DynamicResource.
If you know the resource will never change, use StaticResource, which
takes a snapshot, avoiding the cost associated with tracking the resource for
changes. (The cost is small, but you may as well avoid it for resources that
never change.) Example 6-12 shows
the use of both resource reference types.
Example 6-12. Using resources from markup
<Window x:Class="ResourcePlay.Window1" Text="ResourcePlay"
xmlns="http://schemas.microsoft.com/winfx/avalon/2005"
xmlns:x="http://schemas.microsoft.com/winfx/xaml/2005">
<Window.Resources>
<SolidColorBrush x:Key="Foo" Color="LightGreen" />
</Window.Resources>
<Grid Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}">
<TextBlock FontSize="36" Width="200" Height="200"
Background="{StaticResource Foo}">Hello!</TextBlock>
</Grid>
</Window>
The top-level Window defines a brush as a resource. The
TextBlock uses this for its Background property via a StaticResource
reference. This has a similar effect as the code in
Example 6-10. It takes a snapshot and is appropriate for resources that
will not change while the application runs.
The grid's Background has been set to the system
"control" color. (This is typically battleship greythe color often used as the
background for dialogs.) Since this is a user-configurable color, and could
therefore change at runtime, we've used a DynamicResource, which has
the same effect as the call to SetResourceReference in
Example 6-11.
The syntax here is a little more complex than for the StaticResource.
This complexity is not because we are using DynamicResource. It is
because the resource we wish to use is identified by an object, returned by the
static SystemColors.ControlBrushKey property. We could have tried
this:
<!-- This will not work as intended -->
<Grid Background="{DynamicResource SystemColors.ControlBrushKey}">
This is syntactically correct, but doesn't do what we want. It
will be interpreted as a dynamic reference to a resource named by the
string SystemColors.ControlBrushKey. However, there is no
such resource, so the background will not be set. In order to retrieve the real
resource key (the object returned by the ControlBrushKey static
property) we have to use the x:Static markup extension as
Example 6-12 doesthis tells the XAML compiler that the text should be
treated as the name of a static property, not as a string.
6.1.3. Reusing Drawings
It is often useful to put drawings and shapes into resources.
There are two main reasons for doing so. One is that drawings can be quite
complex, and putting them inline as part of the main markup for a user
interface can make the XAML hard to read. By putting drawings into the
resources section, the overall UI structure can be clearer. The other main
reason is to enable reuseyou may want to use the same graphic in multiple
places.
There are many different ways in which you can represent shapes
and drawings, which are shown in
Chapter 7. All of them can be used as resources, although there are
some restrictions with using certain types. In particular, if you use any
element that derives from FrameworkElement as a resource, you can only
reference it once. The reason for this restriction is that FrameworkElement
is the basis of the user-interface tree. An element knows what its parent is
and what children it has, so it is not possible for it to be in more than one
place in the tree. (When you use a resource, you are not using a copy of the
object, you are using the object itself.)
So although you make an Ellipse a resource, or even a
whole drawing in a Canvas, you should do this only if you intend to
use the drawing exactly once, because Ellipse and Canvas both
derive from FrameworkElement. This is sometimes a useful thing to
doyou may want to turn a graphic into a resource just to move it out of the
main part of your markup so as to reduce clutter. In this case, the single-use
restriction isn't a big problem. Example
6-13 uses this to define an Ellipse resource called shape.
It also shows how to use the resource.
Example 6-13. Using a FrameworkElement resource
<Window.Resources>
<Ellipse x:Key="shape" Fill="Blue" Width="100" Height="80" />
</Window.Resources>
...
<StackPanel>
<Button>Foo</Button>
<StaticResource ResourceKey="shape" />
<Button>Bar</Button>
</StackPanel>
The StaticResource element here will be replaced at
runtime with the resource it names. The result will look like
Figure 6-2.
The Drawing classes, such as a GeometryDrawing
or a DrawingGroup, are better candidates for storing drawings as
resources. Since Drawing does not derive from FrameworkElement,
you are free to use one instance in as many places as you like. DrawingGroup
lets you put as many shapes and images into a single drawing as needed, and the
various other types derived from Drawing provide access to all of
WPF's graphics facilities. See
Chapter 7 for more details.
Example 6-14 shows
how to define and use a drawing resourcea Drawing is typically used in
conjunction with a DrawingBrush.
Figure 6-3 shows the result.
Example 6-14. Using a Drawing resource
<Window.Resources>
<GeometryDrawing x:Key="drawing" Brush="Green">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="200" RadiusY="10" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
</Window.Resources>
...
<Rectangle Width="250" Height="50">
<Rectangle.Fill>
<DrawingBrush Drawing="{StaticResource drawing}" />
</Rectangle.Fill>
</Rectangle>
Alternatively, you could just define a DrawingBrush resource.
This moves some of the complexity into the Resources section, making
the markup considerably simpler at the point at which you use the resource, as
Example 6-15 shows. The results are the same as the previous example (Figure
6-3), but the markup that uses the resource is just one line long
instead of five.
Example 6-15. Using a DrawingBrush resource
<Window.Resources>
<DrawingBrush x:Key="dbrush" Drawing="{StaticResource drawing}" />
<GeometryDrawing x:Key="drawing" Brush="Green">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="200" RadiusY="10" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
</Window.Resources>
...
<Rectangle Width="250" Height="50" Fill="{StaticResource dbrush}" />
If you want the same shape to crop up in multiple drawings, you
might want to drop down a level, and use individual geometry objects as
resources. These can then be referred to from within drawings.
Example 6-16 shows the use of a DrawingBrush with a GeometryDrawing
that uses a Geometry resource. (Since this is yet another ellipse, we
won't waste your time with another pictureit'll look much like
Figure 6-3, only in cyan.)
Example 6-16. Using a Geometry resource
<Window.Resources>
<EllipseGeometry x:Key="geom" RadiusX="200" RadiusY="30" />
</Window.Resources>
...
<Rectangle Width="250" Height="50">
<Rectangle.Fill>
<DrawingBrush>
<DrawingBrush.Drawing>
<GeometryDrawing Brush="Cyan" Geometry="{StaticResource geom}" />
</DrawingBrush.Drawing>
</DrawingBrush>
</Rectangle.Fill>
</Rectangle>
In this particular example, the use of resources may seem a
little extremeit would probably have been less effort just to create a new
geometry from scratch. However, some geometries, such as PathGeometry,
can become quite complex, in which case this kind of reuse makes more sense.
While drawings and geometries are powerful, reusable, and
lightweight, they have one disadvantage. Because they are not proper framework
elements, they cannot take advantage of WPF's layout system. You can scale them
using the brush scaling features described in
Chapter 7, but you cannot make them adapt their layout intelligently
using the panels described in
Chapter 2, because panels are all framework elements. So you might
think that you have to choose between the ability to use framework element
features such as layout and the ability to be reuse the resource. However, you
can get the best of both worlds by using a ControlTemplate.
Example 6-17 shows
markup that uses a ControlTemplate resource. This uses the same Ellipse
element multiple times, something we could not do by making the Ellipse
element itself a resource. As you can see in
Figure 6-4, each Ellipse has been positioned and sized
individually by the Grid. Templates are discussed in more detail in
Chapter 5.
Example 6-17. Reusing arbitrary markup with templates
<Window.Resources>
<ControlTemplate x:Key="shapeTemplate">
<Ellipse Fill="Blue" Margin="3" />
</ControlTemplate>
</Window.Resources>
...
<Grid Width="300" Height="150">
<Grid.RowDefinitions>
<RowDefinition Height="2*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Control Template="{StaticResource shapeTemplate}"
Grid.Row="0" Grid.Column="0" />
<Control Template="{StaticResource shapeTemplate}"
Grid.Row="0" Grid.Column="1" />
<Control Template="{StaticResource shapeTemplate}"
Grid.Row="1" Grid.Column="0" />
<Control Template="{StaticResource shapeTemplate}"
Grid.Row="1" Grid.Column="1" />
</StackPanel>
Control templates offer a good way of reusing arbitrary markup,
but if you don't need to use FrameworkElement-based types in your
drawing, using the more lightweight DrawingBrush class is more
efficient. And if you are creating lots of drawings all containing similar
shapes, you can even go as far as sharing individual geometry objects as
resources. All of these drawing mechanisms are described in more detail in
Chapter 7.
|