Using logical resources
WPF introduces the concept of logical resources, objects that can be shared (and reused) across some part of a visual tree or an entire application. Logical resources can be anything, from WPF objects such as brushes, geometries, or styles, to other objects defined by the .NET Framework or the developer, such as string
, List<>
, or some custom typed object. This gives a whole new meaning to the term resources. These objects are typically placed inside a ResourceDictionary
and located at runtime using a hierarchical search, as demonstrated in this recipe.
Getting ready
Make sure Visual Studio is up and running.
How to do it...
We'll create a simple application that demonstrates creating and using logical resources:
- Create a new WPF Application named
CH02.SimpleResources
. - Open
MainWindow.xaml
and replace theGrid
element with aStackPanel
. - Add a
Rectangle
element to theStackPanel
, as shown in the following code snippet:<Rectangle Height="100" Stroke="Black"> <Rectangle.Fill> <LinearGradientBrush> <GradientStop Offset="0" Color="Yellow" /> <GradientStop Offset="1" Color="Brown" /> </LinearGradientBrush> </Rectangle.Fill> </Rectangle>
- Now we want to add an
Ellipse
element, whoseStroke
is the same as the previousLinearGradientBrush
. One way to do that is to simply make a copy:<Ellipse StrokeThickness="20" Height="100"> <Ellipse.Stroke> <LinearGradientBrush> <GradientStop Offset="0" Color="Yellow" /> <GradientStop Offset="1" Color="Brown" /> </LinearGradientBrush> </Ellipse.Stroke> </Ellipse>
This is wasteful – duplication of code causes a maintenance headache (and eventually a nightmare). More subtly, two brushes are created instead of one. Using a logical resource can solve this. Cut the
LinearGradientBrush
tag and paste it into theResources
property of the Window, as follows:<Window.Resources> <LinearGradientBrush x:Key="brush1"> <GradientStop Offset="0" Color="Yellow" /> <GradientStop Offset="1" Color="Brown" /> </LinearGradientBrush> </Window.Resources>
The
Resources
property is a dictionary, so a key must be provided using thex:Key
XAML attriute. - To use the brush in XAML, we need the
StaticResource
markup extension. Here's the revised markup for the rectangle and ellipse:<Rectangle Height="100" Stroke="Black" Fill="{StaticResource brush1}" /> <Ellipse StrokeThickness="20" Height="100" Stroke="{StaticResource brush1}" />
- Running the application shows the following:
How it works...
Every element (deriving from FrameworkElement
) has a Resources
property of type ResourceDictionary
. This means that every element can have resources associated with it. In XAML, the x:Key
attribute must be specified (most of the time; exceptions to this rule will be discussed in relation to styles and data templates). The preceding defined resource looks as follows inside the Resources
collection of the Window
:
<Window.Resources> <LinearGradientBrush x:Key="brush1"> <GradientStop Offset="0" Color="Yellow" /> <GradientStop Offset="1" Color="Brown" /> </LinearGradientBrush> </Window.Resources>
Using this resource in XAML requires the StaticResource
markup extension (the "static" part will become clear after the next recipe, Dynamically binding to a logical resource) with the resource key povided:
<Rectangle Fill="{StaticResource brush1}" />
This causes a search from the current element (the Rectangle
) up the element tree, looking for a resource with the key brush1
; if found, its associated object is used as the property's value. If not found on any element up to the root Window
, the search continues within the resources of Application
(typically located in the App.xaml
file). If not found even there, a runtime exception is thrown. This is depicted in the following diagram:
There's more...
The same lookup effect can be achieved in code by using the FrameworkElement.FindResource
method, as follows:
Brush brush = (Brush)x.FindResource("brush1");
This has the same effect as using {StaticResource}
. If an exception is undesirable in case of a non-existent resource, the TryFindResource
can be used instead:
Brush brush = (Brush)x.TryFindResource("brush1"); if(brush == null) { // not found }
This method returns null
if the specified resource does not exist.
A resource can also be directly accessed using an indexer, provided we know on which object it's actually defined. In our example, the brush is defined on the resources of Window
, so can be accessed directly as follows:
Brush brush = (Brush)this.Resources["brush1"];
The Resources
dictionary can be manipulated at runtime, by adding or removing resources. Here's how to add a new resource:
this.Resources.Add("brush2", new SolidColorBrush( Color.FromRgb(200, 10, 150)));
This adds a new resource (a Brush
) to the Window's resources collection. Later, calls to FindResource
can locate the new resource. A resource can be removed as well:
this.Resources.Remove("brush1");
It's important to realize that any object bound to the resource with {StaticResource}
does not lose the resource in any way—it's still being referenced by it. However, future FindResource
calls will fail to find that resource.
All {StaticResource}
(or FindResource
) lookups with a specific key use the same object instance. This means that modifying the resource properties (not replacing the resource with a different one), impacts automatically all properties using that resource.
For example, if we modify the brush resource as follows:
var brush = (LinearGradientBrush)this.Resources["brush1"]; brush.GradientStops.Add(new GradientStop(Colors.Blue, .5));
This causes immediate changes to the output:
A resource can use (as part of its definition) another resource. Here's an example:
<LinearGradientBrush x:Key="brush3"> <GradientStop Offset="0" Color="Red" /> <GradientStop Offset="1" Color="Orange" /> </LinearGradientBrush> <DataTemplate x:Key="temp1"> <Rectangle Fill="{StaticResource brush3}" StrokeThickness="4" Stroke="DarkBlue" /> </DataTemplate>
The Rectangle
inside the DataTemplate
uses the LinearGradientBrush
defined just above it. The important point is that the resource, brush3
must be declared before referencing it using {StaticResource}
; this is due to the way the XAML parser hunts down resources.
Resources are shared by default, meaning there's only one instance created no matter how many lookups exist for that resource. Sometimes it's useful to get new instances for every lookup of a particular resource. To do that, we can add the attribute x:Shared="False"
in defining the resource. Note there's no intellisense for that, but it works.
Elements are not the only objects that have the Resources
property (a ResourceDictionary
). Other objects that are not elements may have them as well. The canonical example is the template types (deriving from FrameworkTemplate
): DataTemplate
, ControlTemplate
, and ItemsPanelTemplate
. They can have resources that are available when those templates are used (the exact way this works will be explained in Chapter 6, Data Binding, and Chapter 8, Style, Triggers, and Control Templates, where templates are discussed).