如何将 Style 与 ContentControl 重用以使代码更紧凑?

How to reuse a Style with ContentControl to make code more compact?

我有以下(工作)代码:

<StackPanel>
    <Menu>
        <Menu.Resources>
            <Style TargetType="{x:Type MenuItem}" x:Key="MenuItemStyle">
                <Style.Triggers>
                    <Trigger Property="IsEnabled" Value="False">
                        <Setter Property="Background" Value="LightGray" />
                        <Setter Property="Foreground" Value="LightSlateGray" />
                    </Trigger>
                    <Trigger Property="IsEnabled" Value="True">
                        <Setter Property="Background" Value="LightGray" />
                        <Setter Property="Foreground" Value="Black" />
                    </Trigger>
                </Style.Triggers>
            </Style>
            <Style TargetType="{x:Type MenuItem}" x:Key="DeleteMenuStyle" BasedOn="{StaticResource MenuItemStyle}">
                <Setter Property="Icon">
                    <Setter.Value>
                        <ContentControl Style="{StaticResource CrossIconScalable}"
                                Width="15"
                                Height="15"/>
                    </Setter.Value>
                </Setter>
            </Style>
            <Style TargetType="{x:Type MenuItem}" x:Key="SaveMenuStyle" BasedOn="{StaticResource MenuItemStyle}">
                <Setter Property="Icon">
                    <Setter.Value>
                        <ContentControl Style="{StaticResource SaveButtonScalable}"
                                Width="15"
                                Height="15"/>
                    </Setter.Value>
                </Setter>
            </Style>
        </Menu.Resources>
        <MenuItem>
            <MenuItem.Header>
               <!-- ... -->
            </MenuItem.Header>
            <MenuItem Name="SaveImageMenu" Header="{Binding MenuItemSaveTxt}"
                      Click="SaveImageMenu_OnClick" Style="{StaticResource SaveMenuStyle}" />
            <MenuItem Name="DeleteViewMenu" Header="{Binding MenuItemCancTxt}"
                      Click="DeleteViewMenu_OnClick" Style="{StaticResource DeleteMenuStyle}" />
        </MenuItem>
    </Menu>
</StackPanel>

<!-- StaticResources definition -->
<Style TargetType="ContentControl" x:Key="SaveButtonScalable">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ContentControl">
                <Viewbox Stretch="Uniform">
                    <Canvas Name="Capa_1" Width="32" Height="32">
                        <Canvas.RenderTransform>
                            <TranslateTransform X="0" Y="0" />
                        </Canvas.RenderTransform>
                        <Canvas.Resources />
                        <Canvas Name="g3">
                            <Path Name="path5" Fill="{TemplateBinding Foreground}">
                                <Path.Data>
                                    <PathGeometry Figures="M26 0h-2v13H8V0H0v32h32V6L26 0z M28 30H4V16h24V30z"
                                                  FillRule="NonZero" />
                                </Path.Data>
                            </Path>
                            <Rectangle Canvas.Left="6" Canvas.Top="18" Width="20" Height="2" Name="rect7"
                                       Fill="{TemplateBinding Foreground}" />
                            <Rectangle Canvas.Left="6" Canvas.Top="22" Width="20" Height="2" Name="rect9"
                                       Fill="{TemplateBinding Foreground}" />
                            <Rectangle Canvas.Left="6" Canvas.Top="26" Width="20" Height="2" Name="rect11"
                                       Fill="{TemplateBinding Foreground}" />
                            <Rectangle Canvas.Left="18" Canvas.Top="2" Width="4" Height="9" Name="rect13"
                                       Fill="{TemplateBinding Foreground}" />
                        </Canvas>
                    </Canvas>
                </Viewbox>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<Style TargetType="ContentControl" x:Key="CrossIconScalable">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ContentControl">
                <Viewbox Stretch="Uniform">
                    <Canvas Name="svg2" Width="32" Height="32">
                        <Canvas.RenderTransform>
                            <TranslateTransform X="0" Y="0"/>
                        </Canvas.RenderTransform>
                        <Canvas.Resources/>
                        <Path Name="path4">
                            <Path.Data>
                                <PathGeometry Figures="m0 0h32v32h-32z" FillRule="NonZero"/>
                            </Path.Data>
                        </Path>
                        <Path Name="path6" Fill="{TemplateBinding Foreground}">
                            <Path.Data>
                                <PathGeometry Figures="m2 26 4 4 10-10 10 10 4-4-10-10 10-10-4-4-10 10-10-10-4 4 10 10z" FillRule="NonZero"/>
                            </Path.Data>
                        </Path>
                    </Canvas>
                </Viewbox>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

如您所见,此处有一些重复代码:

我想重构它以创建更紧凑且没有任何重复的代码。

我该怎么做?

好的,这已经足够接近一个好的 MCVE,我想我可以提供一些有用的信息。 :)

在您的特定情况下,您似乎希望能够将 Foreground 值向下传播到模板中,因此 DataTemplate 不会起作用,至少在不创建新的辅助数据结构来完成这项工作。因此,坚持 ControlTemplate 的想法,您可以合并您发布的 XAML 内容,如下所示:

<Window x:Class="TestSO36775094RefactorStyle.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:p="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
  <!--
      Adding the 'p' namespace qualifier above allows Style elements to be
      formatted correctly on Stack Overflow
  -->
  <Window.Resources>
    <StreamGeometry x:Key="saveButtonGeometry">
      F1 M26,0 h-2 v13 H8 V0 H0 v32 h32 V6 L26,0 z M28,30 H4 V16 h24 V30 z
      M6,18 h20 v2 h-20 z m0,4 h20 v2 h-20 z m0,4 h20 v2 h-20 z M18,2 h4 v9 h-4 z
    </StreamGeometry>

    <StreamGeometry x:Key="crossButtonGeometry">
      F1 m2 26 4 4 10-10 10 10 4-4-10-10 10-10-4-4-10 10-10-10-4 4 10 10z
    </StreamGeometry>

    <ControlTemplate x:Key="geometryContentTemplate" TargetType="ContentControl">
      <Viewbox Stretch="Uniform">
        <Canvas Name="Capa_1" Width="32" Height="32">
          <Canvas.RenderTransform>
            <!--
                Not sure why you set this here, since translating 0,0
                does nothing, but I've left it in :)
            -->
            <TranslateTransform X="0" Y="0" />
          </Canvas.RenderTransform>
          <Canvas.Resources />
          <Canvas Name="g3">
            <Path Name="path5" Fill="{TemplateBinding Foreground}" Data="{Binding}"/>
          </Canvas>
        </Canvas>
      </Viewbox>
    </ControlTemplate>
  </Window.Resources>

  <StackPanel>
    <Menu>
      <Menu.Resources>
        <p:Style TargetType="{x:Type MenuItem}" x:Key="MenuItemStyle">
          <p:Style.Triggers>
            <Trigger Property="IsEnabled" Value="False">
              <Setter Property="Background" Value="LightGray" />
              <Setter Property="Foreground" Value="LightSlateGray" />
            </Trigger>
            <Trigger Property="IsEnabled" Value="True">
              <Setter Property="Background" Value="LightGray" />
              <Setter Property="Foreground" Value="Black" />
            </Trigger>
          </p:Style.Triggers>
        </p:Style>

        <!--
            These styles do nothing other than set the Icon property, so if you
            wanted to, you could just set each of these ContentControl instances
            on the MenuItem.Icon value directly, and then just use MenuItemStyle
            as the actual style for each MenuItem.
        -->
        <p:Style TargetType="{x:Type MenuItem}" x:Key="DeleteMenuStyle" BasedOn="{StaticResource MenuItemStyle}">
          <Setter Property="Icon">
            <Setter.Value>
              <ContentControl Template="{StaticResource geometryContentTemplate}"
                              DataContext="{StaticResource crossButtonGeometry}"/>
            </Setter.Value>
          </Setter>
        </p:Style>
        <p:Style TargetType="{x:Type MenuItem}" x:Key="SaveMenuStyle" BasedOn="{StaticResource MenuItemStyle}">
          <Setter Property="Icon">
            <Setter.Value>
              <ContentControl Template="{StaticResource geometryContentTemplate}"
                              DataContext="{StaticResource saveButtonGeometry}"
                              Width="15" Height="15"/>
            </Setter.Value>
          </Setter>
        </p:Style>
      </Menu.Resources>
      <MenuItem Header="Menu">
        <MenuItem Name="SaveImageMenu" Header="{Binding MenuItemSaveTxt}"
                  Click="SaveImageMenu_OnClick" Style="{StaticResource SaveMenuStyle}" />
        <MenuItem Name="DeleteViewMenu" Header="{Binding MenuItemCancTxt}"
                  Click="DeleteViewMenu_OnClick" Style="{StaticResource DeleteMenuStyle}" />
      </MenuItem>
    </Menu>
  </StackPanel>
</Window>

想法是您只定义 ControlTemplate 一次,然后在您实际使用模板声明 ContentControl 时引用不同的部分 - 即几何形状。

我会注意到,在您的示例中,您的 CrossIconScalable 样式包含一个似乎未使用的 path4 元素。它没有指定任何填充,所以当你在那里有一个几何体时,它对视觉外观没有任何影响。所以我就把它放在一边了。但是这样做 "cheat" 一点点;如果你的模板中确实有两个不同的部分需要不同地填充,你就不能完全使用上述方法,因为没有直接的方法来声明两个不同的DataContext值(即两种不同的几何形状,一种对应您要使用的每个填充值)。

在您的示例中,您希望能够使用 {TemplateBinding} 来引用父级的 属性 值,您需要通过创建一个助手 class 来解决这个问题表示几何图形,例如类似于:

class TemplateGeometry
{
    public Geometry ForegroundGeometry { get; set; }
    public Geometry BackgroundGeometry { get; set; }
}

然后您将像这样声明您的资源:

<l:TemplateGeometry x:Key="templateGeometry1">
  <l:TemplateGeometry.ForegroundGeometry>
    <StreamGeometry>
      <!-- your foreground geometry here -->
    </StreamGeometry>
  </l:TemplateGeometry.ForegroundGeometry>
  <l:TemplateGeometry.BackgroundGeometry>
    <StreamGeometry>
      <!-- your background geometry here -->
    </StreamGeometry>
  </l:TemplateGeometry.BackgroundGeometry>
</l:TemplateGeometry>

模板可能(部分)如下所示:

          <Canvas>
            <Path Fill="{TemplateBinding Background}" Data="{Binding BackgroundGeometry}"/>
            <Path Fill="{TemplateBinding Foreground}" Data="{Binding ForegroundGeometry}"/>
          </Canvas>

然后你当然要将 DataContext 设置为助手 class 实例而不是直接设置几何体:

              <ContentControl Template="{StaticResource geometryContentTemplate}"
                              DataContext="{StaticResource templateGeometry1}"
                              Width="15" Height="15"/>

以上只是基本技巧。正如您可能看到的那样,您可以应用许多变体来准确地完成您想要的。

最后,我只想提一下 DataTemplate 方法非常相似。主要问题是 DataTemplate 可以访问的只是绑定上下文对象的成员,而不是 {TemplateBinding} 成员。关于 DataTemplate 的一个非常好的事情是您可以通过使用模板的 DataType 属性 来设置它,这样您甚至不需要引用显式模板。 ContentControl 将根据使用的上下文对象的类型自动找到正确的模板并应用它。