2

I'm trying to move some XAML defining MenuItem into a Style.

I've got the following working XAML:

<Menu x:Name="menu" Height="19" Margin="10,10,10.333,0" VerticalAlignment="Top">
        <MenuItem Header="_Open">
            <MenuItem.Icon>
                <Viewbox>
                    <ContentControl Content="{DynamicResource appbar.folder.open}" RenderTransformOrigin="0.5,0.5">
                        <ContentControl.RenderTransform>
                            <TransformGroup>
                                <ScaleTransform ScaleX="2" ScaleY="2"/>
                            </TransformGroup>
                        </ContentControl.RenderTransform>
                    </ContentControl>
                </Viewbox>
            </MenuItem.Icon>
        </MenuItem>
</Menu>

where the resource looks like this:

<?xml version="1.0" encoding="utf-8"?>
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
            x:Key="appbar.folder.open" Width="76" Height="76" Clip="F1 M 0,0L 76,0L 76,76L 0,76L 0,0">
        <Path Width="44" Height="26" Canvas.Left="19" Canvas.Top="24" Stretch="Fill" Fill="#FF000000" Data="F1 M 19,50L 28,34L 63,34L 54,50L 19,50 Z M 19,28.0001L 35,28C 36,25 37.4999,24.0001 37.4999,24.0001L 48.75,24C 49.3023,24 50,24.6977 50,25.25L 50,28L 53.9999,28.0001L 53.9999,32L 27,32L 19,46.4L 19,28.0001 Z "/>
    </Canvas>
</ResourceDictionary>

As a sidenote, i also don't know how to get rid of the explicit scaling. It seems to be a minor problem but i would appreciate if this could be solved as well.

Anyway, to move as much as possible of this somewhat too expressive definition into a Style, I created code for an attached property of type Visual

namespace extensions
{
    public class AttachedProperties
    {
        public static readonly DependencyProperty VisualIconProperty =
            DependencyProperty.RegisterAttached("VisualIcon",
                typeof(System.Windows.Media.Visual), typeof(AttachedProperties),
                new PropertyMetadata(default(System.Windows.Media.Visual)));

        public static void SetVisualIcon(UIElement element, System.Windows.Media.Visual value)
        {
            element.SetValue(VisualIconProperty, value);
        }
        public static System.Windows.Media.Visual GetVisualIcon(UIElement element)
        {
            return (System.Windows.Media.Visual)element.GetValue(VisualIconProperty);
        }
    }
}

redefined the menu item

<MenuItem Header="_Open"
          Style="{StaticResource MenuItemStyle}"
          extensions:AttachedProperties.VisualIcon="{DynamicResource appbar.folder.open}" />

and tried to create a Style which reads the VisualIcon property to set the Icon content

<Style x:Key="MenuItemStyle" TargetType="MenuItem">
    <Setter Property="MenuItem.Icon">
        <Setter.Value>
            <Viewbox>
                <ContentControl RenderTransformOrigin="0.5,0.5">
                    <ContentControl.Content>
                        <!-- this would work but doesn't reference the VisualIcon property.. <DynamicResource ResourceKey="appbar.folder.open"/> -->
                        <!-- these do not -->
                        <Binding Path="(extensions:AttachedProperties.VisualIcon)" RelativeSource="{RelativeSource FindAncestor, AncestorType=MenuItem}"/>-->
                        <!--<Binding Path="(extensions:AttachedProperties.VisualIcon)" RelativeSource="{RelativeSource TemplatedParent}"/>-->
                        <!--<Binding Path="(extensions:AttachedProperties.VisualIcon)" RelativeSource="{RelativeSource Self}"/>-->
                    </ContentControl.Content>
                    <ContentControl.RenderTransform>
                        <TransformGroup>
                            <ScaleTransform ScaleX="2" ScaleY="2"/>
                        </TransformGroup>
                    </ContentControl.RenderTransform>
                </ContentControl>
            </Viewbox>
        </Setter.Value>
    </Setter>
</Style>

but failed. Referencing the resource with a DynamicResource and a static key works, but i cannot get any binding using the attached property to work.

Since I'm a WPF beginner, I'm not sure if this is a good approach anyway.

[EDIT1]

I tested all the provided solutions. Grek40's answer didn't display any icons for me, neither in design view nor during runtime; maybe I did something completely wrong.

The second approach in grx70's second answer it the most promising for me as it reliably displays the icon in the design view as well as during runtime. However, it seems like there's a difference between Win7 and Win10 i don't understand. I tested the identical source (on a external drive) on Win7 and Win10. For Win10, it looks nice, but Win7 draws the icons way too big.

(Note: The reason is give in this comment)

Difference Win7/Win10

Here's the window testcode:

<Window.Resources>
    <Style x:Key="MenuItemStyle" TargetType="controls:MenuItemEx">
        <Setter Property="MenuItem.Icon">
            <Setter.Value>
                <Viewbox>
                    <ContentControl RenderTransformOrigin="0.5,0.5">
                        <ContentControl.Content>
                            <Binding Path="(extensions:AttachedProperties.VisualIcon)" RelativeSource="{RelativeSource FindAncestor, AncestorType=MenuItem}"/>
                        </ContentControl.Content>
                        <ContentControl.RenderTransform>
                            <TransformGroup>
                                <ScaleTransform ScaleX="2" ScaleY="2"/>
                            </TransformGroup>
                        </ContentControl.RenderTransform>
                    </ContentControl>
                </Viewbox>
            </Setter.Value>
        </Setter>
    </Style>
    <Style x:Key="MenuItemStyle2" TargetType="MenuItem">
        <Setter Property="uihelpers:MenuItemHelper.IsEnabled" Value="True" />
        <Setter Property="MenuItem.Icon">
            <Setter.Value>
                <Viewbox>
                    <ContentControl RenderTransformOrigin="0.5,0.5"
                        Content="{Binding
                        Path=(uihelpers:MenuItemHelper.MenuItem).(extensions:AttachedProperties.VisualIcon),
                        RelativeSource={RelativeSource AncestorType=Viewbox}}">
                        <ContentControl.RenderTransform>
                            <TransformGroup>
                                <ScaleTransform ScaleX="2" ScaleY="2"/>
                            </TransformGroup>
                        </ContentControl.RenderTransform>
                    </ContentControl>
                </Viewbox>
            </Setter.Value>
        </Setter>
    </Style>

    <Style x:Key="MenuItemStyle3" TargetType="{x:Type MenuItem}">
        <Style.Resources>
            <DataTemplate x:Key="MenuItemStyle3dt" DataType="{x:Type Style}">
                <Path Style="{Binding}"
                      Stretch="Uniform"
                      HorizontalAlignment="Center"
                      VerticalAlignment="Center" />
            </DataTemplate>
        </Style.Resources>
    </Style>
    <Style x:Key="appbar.folder.open.style3.local" TargetType="{x:Type Path}">
        <Setter Property="Fill" Value="Black" />
        <Setter Property="Data" Value="M0,26 L9,10 L44,10 L35,26 Z M0,4 L16,4 C17,1 18.5,0 18.5,0 L29.75,0 C30.3,0 31,0.7 31,1.25 L31,4 L34,4 L34,8 L8,8 L0,22.4 Z" />
    </Style>
    <extensions:PathStyle x:Key="appbar.folder.open.style3b">
        <Setter Property="Path.HorizontalAlignment" Value="Center" />
        <Setter Property="Path.VerticalAlignment" Value="Center" />
        <Setter Property="Path.Stretch" Value="Uniform" />
        <Setter Property="Path.Fill" Value="Black" />
        <Setter Property="Path.Data" Value="M0,26 L9,10 L44,10 L35,26 Z M0,4 L16,4 C17,1 18.5,0 18.5,0 L29.75,0 C30.3,0 31,0.7 31,1.25 L31,4 L34,4 L34,8 L8,8 L0,22.4 Z" />
    </extensions:PathStyle>
    <DataTemplate DataType="{x:Type extensions:PathStyle}">
        <Path Style="{Binding}" />
    </DataTemplate>

    <Canvas x:Key="appbar.folder.open.style4.local" x:Shared="False" Width="76" Height="76" Clip="F1 M 0,0L 76,0L 76,76L 0,76L 0,0">
        <Path Width="44" Height="26" Canvas.Left="19" Canvas.Top="24" Stretch="Fill" Fill="#FF000000" Data="F1 M 19,50L 28,34L 63,34L 54,50L 19,50 Z M 19,28.0001L 35,28C 36,25 37.4999,24.0001 37.4999,24.0001L 48.75,24C 49.3023,24 50,24.6977 50,25.25L 50,28L 53.9999,28.0001L 53.9999,32L 27,32L 19,46.4L 19,28.0001 Z "/>
    </Canvas>
    <Viewbox x:Key="MenuItemStyle4.Icon" x:Shared="False">
        <ContentControl Content="{Binding Path=Tag,RelativeSource={RelativeSource AncestorType=MenuItem}}" RenderTransformOrigin="0.5,0.5">
            <ContentControl.RenderTransform>
                <TransformGroup>
                    <ScaleTransform ScaleX="2" ScaleY="2"/>
                </TransformGroup>
            </ContentControl.RenderTransform>
        </ContentControl>
    </Viewbox>
    <Style x:Key="MenuItemStyle4" TargetType="MenuItem">
        <Setter Property="Icon" Value="{StaticResource MenuItemStyle4.Icon}"/>
    </Style>
</Window.Resources>
<Grid>
    <Menu x:Name="menu" Height="19" Margin="10,10,10.333,0" VerticalAlignment="Top">
        <MenuItem Header="_File">
            <controls:MenuItemEx Header="_Open"
                      Style="{StaticResource MenuItemStyle}"
                      extensions:AttachedProperties.VisualIcon="{DynamicResource appbar.folder.open}" />
            <MenuItem Header="_Open"
                      Style="{StaticResource MenuItemStyle2}"
                      extensions:AttachedProperties.VisualIcon="{DynamicResource appbar.folder.open}" />
            <MenuItem Header="_Open"
                      Style="{StaticResource MenuItemStyle3}"
                      Icon="{DynamicResource appbar.folder.open.style3}" />
            <MenuItem Header="_Open"
                      Style="{StaticResource MenuItemStyle3}"
                      Icon="{DynamicResource appbar.folder.open.style3.local}" />
            <MenuItem Header="_Open" Icon="{DynamicResource appbar.folder.open.style3b}" />
            <MenuItem Header="_Open"
                      Style="{StaticResource MenuItemStyle4}"
                      Tag="{DynamicResource appbar.folder.open}" />
            <MenuItem Header="_Open"
                      Style="{StaticResource MenuItemStyle4}"
                      Tag="{DynamicResource appbar.folder.open.style4.local}" />
            <MenuItem Header="_Save">
                <MenuItem.Icon>
                    <Viewbox>
                        <ContentControl Content="{DynamicResource appbar.save}" RenderTransformOrigin="0.5,0.5">
                            <ContentControl.RenderTransform>
                                <TransformGroup>
                                    <ScaleTransform ScaleX="2" ScaleY="2"/>
                                </TransformGroup>
                            </ContentControl.RenderTransform>
                        </ContentControl>
                    </Viewbox>
                </MenuItem.Icon>
            </MenuItem>
        </MenuItem>
    </Menu>
    <Menu x:Name="menu2" Height="19" Margin="9,32,11.333,0" VerticalAlignment="Top" ItemContainerStyle="{StaticResource MenuItemStyle4}">
        <MenuItem Header="_File">
            <MenuItem Header="_Open"
                       Tag="{DynamicResource appbar.folder.open.style4.local}" />
            <MenuItem Header="_Open"
                       Tag="{DynamicResource appbar.folder.open}" />
        </MenuItem>
    </Menu>
</Grid>

and here are the global resources:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
            x:Key="appbar.folder.open" x:Shared="False" Width="76" Height="76" Clip="F1 M 0,0L 76,0L 76,76L 0,76L 0,0">
        <Path Width="44" Height="26" Canvas.Left="19" Canvas.Top="24" Stretch="Fill" Fill="#FF000000" Data="F1 M 19,50L 28,34L 63,34L 54,50L 19,50 Z M 19,28.0001L 35,28C 36,25 37.4999,24.0001 37.4999,24.0001L 48.75,24C 49.3023,24 50,24.6977 50,25.25L 50,28L 53.9999,28.0001L 53.9999,32L 27,32L 19,46.4L 19,28.0001 Z "/>
    </Canvas>
    <Style x:Key="appbar.folder.open.style3" TargetType="{x:Type Path}">
        <Setter Property="Fill" Value="Black" />
        <Setter Property="Data" Value="M0,26 L9,10 L44,10 L35,26 Z M0,4 L16,4 C17,1 18.5,0 18.5,0 L29.75,0 C30.3,0 31,0.7 31,1.25 L31,4 L34,4 L34,8 L8,8 L0,22.4 Z" />
    </Style>
</ResourceDictionary>

and

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
            x:Key="appbar.save" Width="76" Height="76" Clip="F1 M 0,0L 76,0L 76,76L 0,76L 0,0">
        <Path Width="34.8333" Height="34.8333" Canvas.Left="20.5833" Canvas.Top="20.5833" Stretch="Fill" Fill="#FF000000" Data="F1 M 20.5833,20.5833L 55.4167,20.5833L 55.4167,55.4167L 45.9167,55.4167L 45.9167,44.3333L 30.0833,44.3333L 30.0833,55.4167L 20.5833,55.4167L 20.5833,20.5833 Z M 33.25,55.4167L 33.25,50.6667L 39.5833,50.6667L 39.5833,55.4167L 33.25,55.4167 Z M 26.9167,23.75L 26.9167,33.25L 49.0833,33.25L 49.0833,23.75L 26.9167,23.75 Z "/>
    </Canvas>
</ResourceDictionary>

which are merged in app.xaml:

<Application.Resources>
    <ResourceDictionary >
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="icons/appbar.folder.open.xaml"/>
            <ResourceDictionary Source="icons/appbar.save.xaml"/>
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Application.Resources>

The reason the icons are offset is that i took them more or less 1:1 from github.com/Templarian/WindowsIcons and was hoping that because they're provided in this format (i also tried to save them as brushes, first with Inkscape and then with Expression Design) it would be common to use them like this.

wonko realtime
  • 545
  • 9
  • 26
  • Are you getting any binding-related errors in the output window? – Grx70 Aug 05 '17 at 20:33
  • @Grx70 for the three different approaches, only FindAncestor logs: System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='System.Windows.Controls.MenuItem', AncestorLevel='1''. BindingExpression:Path=(0); DataItem=null; target element is 'ContentControl' (Name=''); target property is 'Content' (type 'Object'). Strangely, the "Self" approach shows "(0)" as the icon in the design view but not during runtime. – wonko realtime Aug 06 '17 at 11:22
  • 1
    According to [this QA](https://stackoverflow.com/questions/15725729/wpf-menuitem-icon-dimensions) `MenuItem` height differs depending on used theme. Unfortunately, there's no universal way of determining "optimal" icon size - e.g. in _Aero_ theme (the default for _Windows 7_) there are no limitations at all (there's always infinite space available during the measure pass of the layout - that stems form the `MenuItem` temlpate), hence your results (icons are getting "as big as they want"). There might be some workarounds to get things more-less working, but that begs for a different question. – Grx70 Aug 08 '17 at 17:47
  • @Grx70 many thanks! Really appreciate your help. I think the best and most versatile solution is the one with the derived PathStyle and the implicit DataTemplate and i will accept that answer. Btw, how did you translate the Path? Is there a tool which can do this in batch mode? – wonko realtime Aug 08 '17 at 19:36
  • I did it manually (because it took me like 2 mins), but you could easily write such a tool in half an hour, so depending on the complexity of your project it may or may not be worth it. BTW, in the light of that you've got the icons from [this link](https://github.com/Templarian/WindowsIcons), it is understandable why the icons are so "overly complicated". As I take it, they were designed in _MS Expression Design_ (judging by the .design extension), which generates _XAML_ code representing an image, which explains the unnecessary complexity of the code itself. – Grx70 Aug 08 '17 at 19:49

3 Answers3

1

Diagnosis

The reasons why your bindings do not work are these:

  1. For RelativeSourceMode.Self - because VisualIcon attached property is not explicitly set on the ContentControl and have its default value of null.
  2. For RelativeSourceMode.TemplatedParent - because the ContentControl is not part of a template (and if it is, which is not apparent from the code you've provided, probably the value of VisualIcon for the templated parent is null)
  3. For RelativeSourceMode.FindAncestor - because MenuItem does not set the value of the MenuItem.Icon property as its logical child, so until the ContentControl is loaded into the visual tree, there's no way to establish the relationship between the two. And, apparently, in this case bindings are resolved prior to that, and for some reason are not re-resolved when the ContentControl is finally loaded.

Solution(s)

There are several approaches you can take to solve this problem, following are some of them.

I. Set the binding only when the ContentControl is loaded

You could subscribe the following handler to the ContentControl.Loaded event:

private void ContentControl_Loaded(object sender, RoutedEventArgs e)
{
    var control = (ContentControl)sender;
    control.SetBinding(ContentControl.ContentProperty, new Binding
    {
        Path = new PropertyPath(AttachedProperties.VisualIconProperty),
        RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor)
        {
            AncestorType = typeof(MenuItem),
        },
    });
}

Then in your XAML:

<ContentControl
    Content="{Binding
        Path=(extensions:AttachedProperties.VisualIcon),
        RelativeSource={RelativeSource AncestorType=MenuItem}}"
    Loaded="ContentControl_Loaded" (...)>
    (...)
</ContentControl>

Note though that if the style is placed in a resource dictionary in a separate file, you'll have to follow these instructions to get things working.

You could also create custom MarkupExtension (e.g. named DeferredBinding) to do the job.

II. Subclass MenuItem and set icon as a logical child

There's not much code to write:

public class MyMenuItem : MenuItem
{
    protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
    {
        base.OnPropertyChanged(e);
        if (e.Property == IconProperty)
        {
            if (e.OldValue != null)
                RemoveLogicalChild(e.OldValue);
            if (e.NewValue != null)
                AddLogicalChild(e.NewValue);
        }
    }
}

But the downside is that you always have to remember to use MyMenuItem instead of MenuItem:

<local:MyMenuItem Header="_Open"
      Style="{StaticResource MenuItemStyle}"
      extensions:AttachedProperties.VisualIcon="{DynamicResource appbar.folder.open}" />

and:

<Style x:Key="MenuItemStyle" TargetType="local:MyMenuItem">
    (...)
</Style>

In this case you'd also have bind in FindAncestor mode.

III. Create a helper class that would allow to access the related MenuItem from the ContentControl through an attached property

The following helper class contains two dependency properties - IsEnabled (I believe it's self explanatory), and MenuItem, which is read-only and holds the actual MenuItem, on which the target icon is set:

public static class MenuItemHelper
{
    /**** Here are the important parts: ****/

    //When IsEnabled changes we need to either hook things up or do the cleanup
    private static void HandleIsEnabledChanged(
        DependencyObject d,
        DependencyPropertyChangedEventArgs e)
    {
        var item = (MenuItem)d;
        if ((bool)e.NewValue)
            //We set MenuItem attached property for current Icon
            HandleIconChanged(null, item, EventArgs.Empty);
        else
            //We clear the value of MenuItem attached property
            HandleIconChanged(item.Icon, item, EventArgs.Empty);
    }

    //By using an extension method we get hold of the old value without the need
    //to maintain any kind of dictionary, so we don't need to worry about memory leaks
    private static void HandleIconChanged(
        this object oldValue,
        object sender,
        EventArgs e)
    {
        var item = (MenuItem)sender;
        if (oldValue is DependencyObject oldIcon)
            SetMenuItem(oldIcon, null);
        if (item.Icon is DependencyObject newIcon)
            SetMenuItem(newIcon, item);
        //We need to remove the old handler, because it relates to the old icon
        DependencyPropertyDescriptor
            .FromProperty(MenuItem.IconProperty, item.GetType())
            .RemoveValueChanged(item, item.Icon.HandleIconChanged);
        //We add new handler, so that when the icon changes we get correct old icon
        DependencyPropertyDescriptor
            .FromProperty(MenuItem.IconProperty, item.GetType())
            .AddValueChanged(item, item.Icon.HandleIconChanged);
    }

    /**** The rest is just DP boilerplate code ****/

    private static readonly DependencyPropertyKey MenuItemPropertyKey =
        DependencyProperty.RegisterAttachedReadOnly(
            name: "MenuItem",
            propertyType: typeof(MenuItem),
            ownerType: typeof(MenuItemHelper),
            defaultMetadata: new PropertyMetadata(null));

    public static readonly DependencyProperty MenuItemProperty =
        MenuItemPropertyKey.DependencyProperty;

    public static MenuItem GetMenuItem(DependencyObject d)
        => (MenuItem)d.GetValue(MenuItemProperty);

    private static void SetMenuItem(DependencyObject d, MenuItem value)
        => d.SetValue(MenuItemPropertyKey, value);

    public static readonly DependencyProperty IsEnabledProperty =
        DependencyProperty.RegisterAttached(
            name: "IsEnabled",
            propertyType: typeof(bool),
            ownerType: typeof(MenuItemHelper),
            defaultMetadata: new PropertyMetadata(false, HandleIsEnabledChanged));

    public static bool GetIsEnabled(MenuItem item)
        => (bool)item.GetValue(IsEnabledProperty);

    public static void SetIsEnabled(MenuItem item, bool value)
        => item.SetValue(IsEnabledProperty, value);
}

Then you only need to set MenuItemHelper.IsEnabled="True" on the MenuItem and you can use MenuItemHelper.MenuItem for the binding (remember that it will be set on the root element of the icon - Viewbox in your case):

<Style x:Key="MenuItemStyle" TargetType="MenuItem">
    <Setter Property="extensions:MenuItemHelper.IsEnabled" Value="True" />
    <Setter Property="MenuItem.Icon">
        <Setter.Value>
            <Viewbox>
                <ContentControl
                    Content="{Binding
                        Path=(extensions:MenuItemHelper.MenuItem).(extensions:AttachedProperties.VisualIcon),
                        RelativeSource={RelativeSource AncestorType=Viewbox}}" (...)>
                    (...)
                </ContentControl>
            </Viewbox>
        </Setter.Value>
    </Setter>
</Style>

My personal favorite is #III, because it's most versatile of the three, but perhaps in your particular case other solutions might prove to be more applicable.

Community
  • 1
  • 1
Grx70
  • 10,041
  • 1
  • 40
  • 55
  • If you're interested in code for `DeferredBinding` let me know. – Grx70 Aug 06 '17 at 14:56
  • Thanks a lot for the detailed explanation! I tried 2 and 3 and both work (had to fix old/new-Icon in MenuItemHelper though; or is that a syntax i don't know about yet?). Sorry that I didn't mention it before, but it would be great if the solution would also work in the design view. Is there any chance DeferredBinding could do that? Or does there exist another approach, even if it's an ugly one, which would also work in the design view? – wonko realtime Aug 06 '17 at 19:37
  • 1
    Right, I was about to address that as well - it seems that you have your project code disabled in the designer (hence the `(0)` you've mentioned). Look for an "Enable/Disable project code" toggle button (I believe it's the rightmost one) in the panel at the bottom of the designer, and toggle it on. It might take a restart to take effect. – Grx70 Aug 06 '17 at 20:23
  • As for the code that does not work - I used some C# 7.0 syntax, which by default is only available in Visual Studio 2017, so if you're using an earlier version, it won't compile. See [What's new in C# 7.0](https://blogs.msdn.microsoft.com/dotnet/2016/08/24/whats-new-in-csharp-7-0/), especially [Pattern matching](https://blogs.msdn.microsoft.com/dotnet/2016/08/24/whats-new-in-csharp-7-0/#user-content-pattern-matching). – Grx70 Aug 06 '17 at 20:24
  • Thanks a lot! Found the toggle button, but it's disabled. After some try and error i found out that it get's disabled for projects referencing some c++/cli libs; oh MS.. Maybe that's solved in VS2017 but i cannot use that at work:( Anyway, if there's no other solution to get a working design view (the Style could be totally redefined, i don't care as long as it works, and maybe also removes the need for the manual scale as a bonus) I'll accept your answer. Also, great to read about c# 7.0. Thanks for the link! – wonko realtime Aug 06 '17 at 21:04
1

Since it somewhat looks like an XY problem, I'll give you alternative approach to accomplishing your goal.

First of all, your appbar.folder.open resource is overly complicated. The Canvas is completely redundant (you can offset your Path by setting its Margin). The figures are offset within the Path by 19,24, which combined with the Path being offset in Canvas leads to the necessity of using Viewbox together with ScaleTransform. Moreover, your resource is not reusable, since it can only be loaded into the visual tree once, so it would only be visible in the last place it was referenced (it would be unloaded in all the previous places). My advice would be to create a Style for a Path instead - not only is it reusable, but also extensible in the sense that you can modify other properties on the target Path after applying the style. Here's a minimal style to do the job (I've translated your data so that it is no longer offset):

<Style x:Key="appbar.folder.open" TargetType="{x:Type Path}">
    <Setter Property="Fill" Value="Black" />
    <Setter Property="Data" Value="M0,26 L9,10 L44,10 L35,26 Z M0,4 L16,4 C17,1 18.5,0 18.5,0 L29.75,0 C30.3,0 31,0.7 31,1.25 L31,4 L34,4 L34,8 L8,8 L0,22.4 Z" />
</Style>

Secondly, I don't quite understand the purpose of your AttachedProperties.VisualIcon attached property. In my approach it is completely redundant. Also, I believe that this is the part that makes the designer not display your icon properly with project code disabled. The only problem is that if we set MenuItem.Icon="{DynamicResource appbar.folder.open}" we'll get System.Windows.Style text displayed instead of the icon. And DynamicResourceExtension does not support converting the referenced resource out-of-the-box. But, there's a clever trick we can use to make things work - simply provide an implicit DataTemplate with DataType="{x:Type Style}" which will be automatically applied:

<Style x:Key="MenuItemStyle" TargetType="{x:Type MenuItem}">
    <Style.Resources>
        <DataTemplate DataType="{x:Type Style}">
            <Path Style="{Binding}"
                  Stretch="Uniform"
                  HorizontalAlignment="Center"
                  VerticalAlignment="Center" />
        </DataTemplate>
    </Style.Resources>
</Style>

We set some additional properties on the Path so that it fits well into the icon area.

All we need to do now is to reference the style and the icon to have it displayed on a MenuItem:

<Menu (...)>
    <MenuItem Header="_Open"
              Style="{StaticResource MenuItemStyle}"
              Icon="{DynamicResource appbar.folder.open}" />
</Menu>

This approach has the additional advantage of that it works in the designer even with project code disabled (at least it does for me).

Edit

If you want to completely separate and automate things, you could subclass Style:

public class PathStyle : Style
{
    public PathStyle()
    {
        TargetType = typeof(Path);
    }
}

And provide implicit DataTemplate in your resources dictionary:

<local:PathStyle x:Key="appbar.folder.open">
    <Setter Property="Path.HorizontalAlignment" Value="Center" />
    <Setter Property="Path.VerticalAlignment" Value="Center" />
    <Setter Property="Path.Stretch" Value="Uniform" />
    <Setter Property="Path.Fill" Value="Black" />
    <Setter Property="Path.Data" Value="M0,26 L9,10 L44,10 L35,26 Z M0,4 L16,4 C17,1 18.5,0 18.5,0 L29.75,0 C30.3,0 31,0.7 31,1.25 L31,4 L34,4 L34,8 L8,8 L0,22.4 Z" />
</local:PathStyle>
<DataTemplate DataType="{x:Type local:PathStyle}">
    <Path Style="{Binding}" />
</DataTemplate>

I moved all the properties from the DataTemplate to the Style so that it can be overridden in other styles. Note though that you need to fully qualify property names, i.e. use Path.Data instead of simply Data.

Now all you need to is to reference the resource in your view:

<MenuItem Icon="{DynamicResource appbar.folder.open}" (...) />

Or even:

<ContentPresenter Content="{DynamicResource appbar.folder.open}" />

And all the magic is done by the framework. The beauty of this approach is that you can replace your ResourceDictionary with one containing for example:

<Border x:Key="appbar.folder.open" x:Shared="False" Background="Red" />

And everything still works without the need to modify the views.

Community
  • 1
  • 1
Grx70
  • 10,041
  • 1
  • 40
  • 55
  • I've never seen `` before... do you have any source that explains this approach (with any justified usage scenario)? It looks **really** strange to me... – grek40 Aug 07 '17 at 07:52
  • 1
    @grek40 I know it looks a little bit odd, and I've never seen it before either. As for explanation - we have a `Style` for a `Path` that, when applied, creates an icon, and we want to set it to `MenuItem.Icon`. Since `DynamicResourceExtension` does not support conversion, and `MenuItem` does not contain `IconTemplate` nor `IconTemplateSelector` properties, we need to fallback to using implicit `DataTemplate` to "convert" the `Style` into a `Path`. That's pretty much standard approach. Do you think there are some risks with data type being `Style` instead of some arbitrary `FooViewModel`? – Grx70 Aug 07 '17 at 08:05
  • edited the question as there was too much to post for a comment – wonko realtime Aug 08 '17 at 07:30
0

The problem with your style approach is, that Viewbox will be shared between multiple items when the same style is applied. This can be avoided by creating the Viewbox as a separate resource with x:Shared="False". If you want to stick to your current Canvas-Path approach, you should turn it into a non-shared resource too, so it becomes re-usable

For demonstration purpose, I set the MenuItem.Tag property, but the same should be possible with your attached property idea.

Resources:

<!--Notice the added x:Shared-->
<Canvas x:Key="appbar.folder.open" x:Shared="False" Width="76" Height="76" Clip="F1 M 0,0L 76,0L 76,76L 0,76L 0,0">
    <Path Width="44" Height="26" Canvas.Left="19" Canvas.Top="24" Stretch="Fill" Fill="#FF000000" Data="F1 M 19,50L 28,34L 63,34L 54,50L 19,50 Z M 19,28.0001L 35,28C 36,25 37.4999,24.0001 37.4999,24.0001L 48.75,24C 49.3023,24 50,24.6977 50,25.25L 50,28L 53.9999,28.0001L 53.9999,32L 27,32L 19,46.4L 19,28.0001 Z "/>
</Canvas>

<!--Another icon for purpose of demonstration-->
<Canvas x:Key="appbar.folder.close" x:Shared="False" Width="76" Height="76" Clip="F1 M 0,0L 76,0L 76,76L 0,76L 0,0">
    <Path Width="44" Height="26" Canvas.Left="19" Canvas.Top="24" Stretch="Fill" Fill="#FFFF0000" Data="F1 M 19,50L 28,34L 63,34L 54,50L 19,50 Z M 19,28.0001L 35,28C 36,25 37.4999,24.0001 37.4999,24.0001L 48.75,24C 49.3023,24 50,24.6977 50,25.25L 50,28L 53.9999,28.0001L 53.9999,32L 27,32L 19,46.4L 19,28.0001 Z "/>
</Canvas>

<Viewbox x:Key="MenuItemStyle.Icon" x:Shared="False">
    <ContentControl Content="{Binding Path=Tag,RelativeSource={RelativeSource AncestorType=MenuItem}}" RenderTransformOrigin="0.5,0.5">
        <ContentControl.RenderTransform>
            <TransformGroup>
                <ScaleTransform ScaleX="2" ScaleY="2"/>
            </TransformGroup>
        </ContentControl.RenderTransform>
    </ContentControl>
</Viewbox>

<Style x:Key="MenuItemStyle" TargetType="MenuItem">
    <Setter Property="Icon" Value="{StaticResource MenuItemStyle.Icon}"/>
</Style>

Usage:

<Menu x:Name="menu" Height="19" Margin="10,10,10.333,0" VerticalAlignment="Top" ItemContainerStyle="{StaticResource MenuItemStyle}">
    <MenuItem Header="_Open" Tag="{DynamicResource appbar.folder.open}"/>
    <MenuItem Header="_Open 2" Tag="{DynamicResource appbar.folder.open}"/>
    <MenuItem Header="_Close" Tag="{DynamicResource appbar.folder.close}"/>
</Menu>
grek40
  • 13,113
  • 1
  • 24
  • 50
  • I tried some combinations (see the updated question) but none did display any icons for me, neither in design view nor during runtime; maybe I did something completely wrong. – wonko realtime Aug 08 '17 at 12:02
  • @wonkorealtime Its just a little misunderstanding... each sub-menu has its own `ItemContainerStyle`, so you need to add it to the File menu or use an implicit style resource in order to apply to the sub-items: ``. However, it's not the most designer-friendly version, the icons will only work properly at runtime. – grek40 Aug 08 '17 at 19:57