Search code examples
c#wpfbuttonstylescontentpresenter

ContentPresenter steals resources?


In a simple view I want to display two buttons, that have as contents images, that are provided via resources in an extra style.xaml that is loaded in the App.xaml.

<BitmapImage x:Key="AddIcon"  UriSource="pack://application:,,,/WpfTestBench;component/Images/plus.png"></BitmapImage>

<BitmapImage x:Key="RemoveIcon"  UriSource="pack://application:,,,/WpfTestBench;component/Images/minus.png"></BitmapImage>

<Style x:Key="AddButtonWithIconStyle" TargetType="{x:Type Button}">
  <Setter Property="Content">
    <Setter.Value>
      <Image Source="{DynamicResource AddIcon}" Stretch="None" 
           VerticalAlignment="Center" HorizontalAlignment="Center"/>
    </Setter.Value>
  </Setter>
</Style>

<Style x:Key="RemoveButtonWithIconStyle" TargetType="{x:Type Button}">
  <Setter Property="Content">
    <Setter.Value>
      <Image Source="{DynamicResource RemoveIcon}" Stretch="None" 
           VerticalAlignment="Center" HorizontalAlignment="Center"/>
    </Setter.Value>
  </Setter>
</Style>

In my view I use the styles like this:

<DockPanel Margin="0,0,5,0" DockPanel.Dock="Bottom" LastChildFill="False" >
   <Button Command="{Binding DeleteCommand}" DockPanel.Dock="Right"  
              Style="{StaticResource RemoveButtonWithIconStyle}"/>
   <Button Command="{Binding AddCommand}" DockPanel.Dock="Right" Margin="5,0" 
              Style="{StaticResource AddButtonWithIconStyle}"/>
</DockPanel>

So far this looks good. But when I add another view into this view via a ContentPresenter, that has basically the same content (again with the above described button styles) the display of the first two buttons (of the parent view) does not work anymore. All I get ist two small circles with the button functionality.

<ContentPresenter Content="{Binding SomeEmbeddedViewModel, UpdateSourceTrigger=PropertyChanged}"/>

The upper list plus buttons is the part of the parent view, the second list and buttons are added with the content presenter.

Why does this happen? Does the ContentPresenter somehow prevent to share the resources?


Solution

  • Diagnosis

    The core reason of this behavior is that an element can only appear once in the visual tree.

    First of all, resources in a ResourceDictionary are by default shared, i.e. every time you fetch a resource with a particular key, you always get the same instance. In your case, you always get the same instance of Style, which also means that there's always only one instance of Image (for each style).

    So if you apply the same style to two buttons, the framework tries to put the same Image instance in two different places, which is not allowed. To avoid that, upon attempt to load the image into the visual tree, if it already is in the visual tree, it is unloaded from the previous location first.

    That's why the image is only visible in the last button (in order in which they were loaded).

    Solution

    There are basically two solutions to this problem.

    1. Disable resource sharing

    You can disable the resource sharing so that each time you fetch a resource from a ResourceDictionary you get a new instance of the resource. In order to do that, you set x:Shared="False" on your resource:

    <Style x:Key="AddButtonWithIconStyle" x:Shared="False" TargetType="{x:Type Button}">
        (...)
    </Style>
    

    2. Use ContentTemplate in your style

    Instead of putting the image as the content of the button, you could define a ContentTemplate, which is realized separately for each button it is applied to (so each button gets its own instance of the image):

    <Style x:Key="RemoveButtonWithIconStyle" TargetType="{x:Type Button}">
        <Setter Property="ContentTemplate">
            <Setter.Value>
                <DataTemplate>
                    <Image Source="{DynamicResource RemoveIcon}" Stretch="None" 
                           VerticalAlignment="Center" HorizontalAlignment="Center"/>
                </DataTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    

    Personally I'd advise you to use the second solution, since that's exactly the purpose of templates in WPF.