2

My application (using MahApps.Metro) has a lot of <Button> elements with Content that all have identical markup except for varying attributes on the <Button> itself, as well as a single Button.Content child element attribute that I want to parameterize through a new attribute (an Attached Property?) on the <Button>.

I have about 20 instances of this XAML for a button:

<Button
    Style="{StaticResource MetroCircleButtonStyle}"
    Margin="0,-4,-4,2"
    DockPanel.Dock="Right"
    Width="32"
    Height="32"
    Name="targetSystemAutoconfigureButton"
    ToolTip="{x:Static me:Resources.Settings_AutoconfigureTargetSystem}"
    Command="{Binding AutoconfigureTargetSystemCommand}"
>
    <Button.Content>
        <Rectangle Fill="{Binding Foreground, ElementName=targetSystemAutoconfigureButton}" Width="12" Height="12">
            <Rectangle.OpacityMask>
                <DrawingBrush Drawing="{StaticResource appbar_magnify}" Stretch="Uniform" />
            </Rectangle.OpacityMask>
        </Rectangle>
    </Button.Content>
</Button>

I've already shrunk it down by moving the constant properties to a derived style:

<!-- In a <ResourceDictionary> located elsewhere -->
<Style x:Key="inputSplitIconButton" TargetType="Button" BasedOn="{StaticResource MetroCircleButtonStyle}">
    <Setter Property="Margin" Value="0,-4,-4,2" />
    <Setter Property="DockPanel.Dock" Value="Right" />
    <Setter Property="Width" Value="32" />
    <Setter Property="Height" Value="32" />
</Style>

<!-- Button XAML is now: -->
<Button
    Style="{StaticResource inputSplitIconButton}"
    Name="targetSystemAutoconfigureButton"
    ToolTip="{x:Static me:Resources.Settings_AutoconfigureTargetSystem}"
    Command="{Binding AutoconfigureTargetSystemCommand}"
>
    <Button.Content>
        <Rectangle Fill="{Binding Foreground, ElementName=targetSystemAutoconfigureButton}" Width="12" Height="12">
            <Rectangle.OpacityMask>
                <DrawingBrush Drawing="{StaticResource appbar_magnify}" Stretch="Uniform" />
            </Rectangle.OpacityMask>
        </Rectangle>
    </Button.Content>
</Button>

But I want to have just this:

<Button
    Style="{StaticResource inputSplitIconButton}"
    Name="targetSystemAutoconfigureButton"
    ToolTip="{x:Static me:Resources.Settings_AutoconfigureTargetSystem}"
    Command="{Binding AutoconfigureTargetSystemCommand}"
    ImageMask="{StaticResource appbar_magnify}"           <--- this property
/>

A problem with WPF is that there's at least three different ways to accomplish the same end result, however I don't know WPF well enough to choose the best at this point. I know my options are:

  • Set the Content property in the <Style x:Key="inputSplitIconButton">.
    • But how do I parameterize the Drawing="" attribute?
  • Set the DataTemplate property in the <Style x:Key="inputSplitIconButton"> and use a DataContext binding for the Drawing="" attribute, and pass that in as the new DataContext for that button instance
    • But this means I can't use my existing Bindings.
  • Along the lines of adding a DataTemplate, there are variations-on-a-theme:
    • Use an Attached Property to set the attribute in the DataTemplate
    • Abuse the Tag property to store the StaticResource name.
  • Subclass Button and add my own properties there and create the content structure in code.
    • This will be very painful and is very non-idiomatic WPF.
  • Define an Attached Property ("me:Drawing=""" for example), that when set on an element automatically adds the <Rectangle>... etc child content.
    • Is this "correct" and idiomatic WPF? How does an Attached Property get to manipulate the markup of its applied element?
Dai
  • 141,631
  • 28
  • 261
  • 374

2 Answers2

1

I'd create a UserControl with your button.

  • All of your button xaml moves into the UserControl.
  • Any parameters that need to be set dynamically become dependency properties on the UserControl.

I'd add example code to this answer, but I'm not sure it would benefit you at all. Hopefully right now you're having a lightbulb moment, and see how this works out. I've employed this pattern multiple times to solve exactly the problem you're describing, and it fits perfectly.

You could even go so far as to drop in a ContentPresenter control inside of the content of the button, and expose that control's Content property as another dependency property for your binding pleasure. Bind whatever you like to it: more text, a rectangle, etc... or nothing at all.

Feel free to comment and I'm happy to flesh this out more...

Lynn Crumbling
  • 12,985
  • 8
  • 57
  • 95
  • One more note: I'm a regular in the [WPF Chat Room](https://chat.stackoverflow.com/rooms/18165/wpf), which is an excellent resource most of the work day for WPF devs. Activity usually kicks up there when Europe comes online for the day and starts to dwindle off when the east coast heads home for the day. – Lynn Crumbling Jun 27 '18 at 02:48
  • Thank you for the advice and chat-room info :) – Dai Jul 01 '18 at 08:02
0

I came to a solution by using a DataTemplate, with an Attached Property to set the image name as a string, with IValueConverter to map from string names to Drawing object resources for the binding to the OpacityMask.

With thanks to these resources:

Resources.xaml:

<!-- Note the ordering of elements is important -->
<me:StringToStaticResourceConverter x:Key="ssr" />

<DataTemplate x:Key="inputSplitIconButtonContentTemplate">
    <Rectangle Fill="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Button}}" Width="12" Height="12">
        <Rectangle.OpacityMask>
            <DrawingBrush Drawing="{Binding Path=(me:Buttons.Image), Mode=OneWay, Converter={StaticResource ssr}, RelativeSource={RelativeSource AncestorType=Button}}" Stretch="Uniform" />
        </Rectangle.OpacityMask>
    </Rectangle>
</DataTemplate>

<Style x:Key="inputSplitIconButton" TargetType="Button" BasedOn="{StaticResource MetroCircleButtonStyle}">
    <Setter Property="Margin" Value="0,-4,-4,2" />
    <Setter Property="DockPanel.Dock" Value="Right" />
    <Setter Property="Width" Value="32" />
    <Setter Property="Height" Value="32" />
    <Setter Property="TabIndex" Value="10" />
    <Setter Property="ContentTemplate" Value="{StaticResource inputSplitIconButtonContentTemplate}" />
</Style>

AttachedProperties.cs:

using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;

namespace MyProject
{
    public static class Buttons
    {
        public static readonly DependencyProperty ImageProperty = DependencyProperty.RegisterAttached
        (
            name           : "Image",
            propertyType   : typeof(String),
            ownerType      : typeof(Buttons),
            defaultMetadata: new FrameworkPropertyMetadata( defaultValue: null, flags: FrameworkPropertyMetadataOptions.AffectsRender )
        );

        public static void SetImage(UIElement element, String value)
        {
            element.SetValue( ImageProperty, value );
        }

        public static String GetImage(UIElement element)
        {
            return (String)element.GetValue( ImageProperty );
        }
    }

    public class StringToStaticResourceConverter : IValueConverter
    {
        public Object Convert(Object value, Type targetType, Object parameter, CultureInfo culture)
        {
            return Application.Current.FindResource( value );
        }

        public Object ConvertBack(Object value, Type targetType, Object parameter, CultureInfo culture)
        {
            return null;
        }
    }
}

Window.xaml:

<Button
    Style="{StaticResource inputSplitIconButton}"
    ToolTip="{x:Static me:Resources.Settings_BrowseFile}" 
    me:Buttons.Image="appbar_folder_ellipsis_drawing"
/>

It even works in the WPF XAML designer too!

I think this could be simplified by using {StaticResource appbar_folder_ellipsis_drawing} in Window.xaml and just passing that through... time to experiment!

Update: Simplified:

I was able to simplify my solution further and eliminate the StringToStaticResourceConverter when I realised that the {StaticResource key} works fine with Attached Properties too.

  1. In Resources.xaml:
    1. Remove this line: <me:StringToStaticResourceConverter x:Key="ssr" />
    2. The <DrawingBrush line becomes this: <DrawingBrush Drawing="{Binding Path=(me:Buttons.Image), Mode=OneWay, RelativeSource={RelativeSource AncestorType=Button}}" Stretch="Uniform" />
  2. In Window.xaml:
    1. Change the attached property attribute value to the {StaticResource} like so: me:Buttons.Image="{StaticResource appbar_folder_ellipsis_drawing}".
Dai
  • 141,631
  • 28
  • 261
  • 374