Windows Presentation Foundation 4.5 Cookbook
上QQ阅读APP看书,第一时间看更新

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:

  1. Create a new WPF Application named CH02.SimpleResources.
  2. Open MainWindow.xaml and replace the Grid element with a StackPanel.
  3. Add a Rectangle element to the StackPanel, 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>
  4. Now we want to add an Ellipse element, whose Stroke is the same as the previous LinearGradientBrush. 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 the Resources 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 the x:Key XAML attriute.

  5. 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}" />
  6. Running the application shows the following:
    How to do it...

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:

How it works...

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"];

Adding or deleting resources dynamically

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.

Modifying resources

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:

Modifying resources

Resources that use other resources

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.

Non-shared 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.

Other locations for resources

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).