I have the following (working) code:
<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>
As you can see there is some repetition of code here:
Style
elements (DeleteMenuStyle
and SaveMenuStyle
) are identical except for the <Style>
used for each ContenControl
.SaveButtonScalable
and CrossIconScalable
Styles are identical for the parent tag but they differs in the <Canvas>
tag inside.I'd like to refactor this in order to create code which is more compact and without any repetition.
How can I do this?
Okay, that was close enough to a good MCVE that I think I can provide some useful information. :)
In your particular case, it seems you want to be able to propagate the Foreground
value down into the template, so DataTemplate
isn't going to work, at least not without creating a new helper data structure to do that work. So, sticking with the ControlTemplate
idea, you can consolidate the XAML you posted with something like this:
<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>
The idea being that you define the ControlTemplate
only once, and then reference the part that's different — i.e. the geometry — when you actually declare a ContentControl
using the template.
I'll note that in your example, your CrossIconScalable
style included a path4
element that appeared to be unused. It did not specify any fill, and so while you had a geometry there, it didn't have any effect on the visual appearance. So I just left it out. But doing so does "cheat" a little; you could not use exactly the above approach if you really did have two different parts in your template that needed to be filled differently, because there's no direct way to declare two different DataContext
values (i.e. two different geometries, one for each fill value you want to use).
In your example, where you want to be able to use {TemplateBinding}
to refer to the parent's property value, you would need to work around this by making a helper class to represent the geometries, e.g. something like:
class TemplateGeometry
{
public Geometry ForegroundGeometry { get; set; }
public Geometry BackgroundGeometry { get; set; }
}
Then you'd declare your resource something like this:
<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>
The template could look (in part) like this:
<Canvas>
<Path Fill="{TemplateBinding Background}" Data="{Binding BackgroundGeometry}"/>
<Path Fill="{TemplateBinding Foreground}" Data="{Binding ForegroundGeometry}"/>
</Canvas>
And then of course you'd set the DataContext
to the helper class instance instead of a geometry directly:
<ContentControl Template="{StaticResource geometryContentTemplate}"
DataContext="{StaticResource templateGeometry1}"
Width="15" Height="15"/>
The above is just the basic technique. As you can probably see, there are lots of variations you could apply to accomplish exactly what you want.
Finally, I'll just mention that the DataTemplate
approach is very similar. The main issue is that all a DataTemplate
has access to is the binding context object's members and not {TemplateBinding}
members. One of the very nice things about DataTemplate
is that you can set it up, through the use of the DataType
property of the template, such that you don't even need to reference the template explicitly. A ContentControl
will automatically find the correct template and apply it, according to the type of the context object used.