Поделиться через


Sharing, and not sharing, Setter.Value in a Style or Template

Setter values in a style or template get shared, which is good for performance, but impacts how some features work. Take this example with a Button style:

 

<Page xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation" >

  <Page.Resources>

    <Style TargetType="Button">

      <Setter Property="Background" Value="Red" />

    </Style>

  </Page.Resources>

  <StackPanel>

  <Button>Click</Button>

  <Button>Clack</Button>

  </StackPanel>

</Page>

 

Here, the Setter.Value will be a SolidColorBrush, and both buttons will use the same instance of that brush. (The same holds if the Setter was in a Trigger, and similarly if the Trigger is part of a ControlTemplate or DataTemplate).

 

As I said, sharing the brush here is good for performance. But sharing can be a problem for types that don’t like to share. In WPF, this is mostly Freezable (depending on what's in it) and element types.

Freezable Objects as Setter Values

 

Freezable types (such as brushes) have the concept of “freezing”, or “sealing”, or “make yourself immutable, please”. That is, if you call the Freeze method, the object tries to become immutable, and therefore is fine for sharing in a Setter. In fact, Setter understands this, and when you put a Freezable in a style/template setter, Freeze is automatically called.

 

Sometimes, though, a Freezable can’t be frozen, such as when there’s a binding or dynamic resource reference on one of its properties, as in this example:

 

<Page xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">

  <Page.Resources>

    <Color x:Key="Color1">Blue</Color>

    <Style TargetType="Button">

      <Setter Property="Background" >

        <Setter.Value>

          <SolidColorBrush Color="{DynamicResource Color1}" />

        </Setter.Value>

      </Setter>

    </Style>

  </Page.Resources>

  <StackPanel>

    <Button>Clack</Button>

    <Button>

      <Button.Resources>

        <Color x:Key="Color1">Red</Color>

      </Button.Resources>

      Clack

    </Button>

  </StackPanel>

</Page>

 

Here the Setter.Value has a SolidColorBrush, but that brush can’t be frozen. (And the dynamic resource reference resolves differently depending on the context – in the case of the first button it uses the “Color1” resource from the Page, but for the second button Color1 is re-defined.)

 

But this still works because of two things: Freezable has a Clone method, and Setter knows how to use it. So in the above example, each of the buttons internally gets its own copy of a SolidColorBrush. This cloning solution isn’t limited to the scenario of dynamic resources and bindings; any Freezable object set as a Setter.Value that can’t be frozen will be cloned once for each element to which the style or template is applied.

 

Elements as Setter Values

Setting an element, such as a shape or control, as a setter value has another problem. These objects typically live in a tree, the kind of tree that only wants a single parent. As a result, these types of objects are simply not allowed as Setter values. Specifically, objects of type Visual or ContentElement, and derived types, are not supported as a setter value.

 

For example, this markup:

 

<Page xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">

  <Page.Resources>

    <Style TargetType="Button">

      <Setter Property="Content" >

        <Setter.Value>

          <Rectangle Width="20" Height="20" Fill="Red" />

        </Setter.Value>

      </Setter>

    </Style>

  </Page.Resources>

  <StackPanel>

    <Button>Click</Button>

    <Button>Clack</Button>

  </StackPanel>

</Page>

… results in the error: Cannot add content of type 'System.Windows.Shapes.Rectangle' to an object of type 'System.Object'. Error at object 'System.Windows.Shapes.Rectangle', Line 6 Position 5. (Not, unfortunately, the best error message.) The error is because Rectangle is a Visual, being set as a Setter.Value.

 

Note, however, that sometimes a template can be used as a workaround. So the above example can be accomplished with:

 

<Page xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">

  <Page.Resources>

    <Style TargetType="Button">

      <Setter Property="ContentTemplate" >

        <Setter.Value>

          <DataTemplate DataType="Button">

            <Rectangle Width="20" Height="20" Fill="Red" />

          </DataTemplate>

        </Setter.Value>

      </Setter>

    </Style>

  </Page.Resources>

  <StackPanel>

    <Button>Click</Button>

    <Button>Clack</Button>

  </StackPanel>

</Page>