ContentPresenter 窃取资源?

ContentPresenter steals resources?

在一个简单的视图中,我想显示两个按钮,它们具有内容图像,它们是通过额外 style.xaml 中的资源提供的,该资源加载在 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>

在我看来,我使用这样的样式:

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

到目前为止这看起来还不错。 但是当我通过 ContentPresenter 将另一个视图添加到该视图时,它具有基本相同的内容(再次使用上述按钮样式)(父视图的)前两个按钮的显示不再起作用。我得到的只是两个带有按钮功能的小圆圈。

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

为什么会这样? ContentPresenter 是否以某种方式阻止共享资源?

诊断

这种行为的核心原因是一个元素在可视化树中只能出现一次。

首先,ResourceDictionary 中的资源默认是共享的,即每次使用特定键获取资源时,您总是会得到相同的实例。在你的情况下,你总是得到相同的 Style 实例,这也意味着总是只有一个 Image 实例(对于每种样式)。

因此,如果您将相同的样式应用于两个按钮,框架会尝试将相同的 Image 实例放在两个不同的地方,这是不允许的。为避免这种情况,在尝试将图像加载到可视化树中时,如果它已经在可视化树中,则首先从先前的位置卸载它。

这就是图像仅在最后一个按钮中可见的原因(按照它们的加载顺序)。

解决方案

这个问题基本上有两种解决方案。

1.禁用资源共享

您可以禁用资源共享,这样每次从 ResourceDictionary 获取资源时,您都会得到该资源的一个新实例。为此,您在资源上设置 x:Shared="False"

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

2。在您的风格中使用 ContentTemplate

您可以定义一个 ContentTemplate,而不是将图像作为按钮的内容,它是为应用到它的每个按钮单独实现的(因此每个按钮都有自己的图像实例) :

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

我个人建议您使用第二种解决方案,因为这正是 WPF.

中模板的用途

如其他答案中所述,您应该在 Button 的 ContentTemplate 中拥有 Image 控件。

声明一个简单的图像按钮样式,其图像的来源 属性 绑定到实际内容:

<Style x:Key="ImageButtonStyle" TargetType="Button">
    <Setter Property="ContentTemplate">
        <Setter.Value>
            <DataTemplate>
                <Image Source="{Binding}"/>
            </DataTemplate>
        </Setter.Value>
    </Setter>
</Style>
...
<Button Style="{StaticResource ImageButtonStyle}"
        Content="{DynamicResource RemoveIcon}" .../>

您也可以将样式声明为默认按钮样式,可能在 DockPanel 中:

<DockPanel>
    <DockPanel.Resources>
        <Style TargetType="Button">
            <Setter Property="ContentTemplate">
                <Setter.Value>
                    <DataTemplate>
                        <Image Source="{Binding}" Stretch="None"/>
                    </DataTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </DockPanel.Resources>

    <Button Command="{Binding DeleteCommand}" DockPanel.Dock="Right"  
            Content="{DynamicResource RemoveIcon}"/>

    <Button Command="{Binding AddCommand}" DockPanel.Dock="Right" Margin="5,0" 
            Content="{DynamicResource AddIcon}"/>
</DockPanel>