James Ko
James Ko

Reputation: 34609

How to set Button.Command from a ResourceDictionary?

I'm trying to implement a hamburger button by myself in a Windows 10 app. I'm running into a little trouble with my ResourceDictionary when trying to set the Command property of a Button (via a style). Here is my code:

Hamburger.xaml

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    x:Class="Octopie.Styles.Hamburger"
    xmlns:local="using:Octopie.Styles">

    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="Square.xaml"/>
    </ResourceDictionary.MergedDictionaries>

    <Style x:Key="HamburgerStyle" TargetType="Button" BasedOn="{StaticResource SquareStyle}">
        <Setter Property="Background" Value="Transparent"/>
        <Setter Property="Command" Value="{Binding OnClicked}"/> <!--This is the part that's having issues-->
        <Setter Property="Content" Value="&#xE700;"/>
        <Setter Property="FontFamily" Value="Segoe MDL2 Assets"/>
    </Style>
</ResourceDictionary>

Hamburger.xaml.cs

namespace Octopie.Styles
{
    public sealed partial class Hamburger : ResourceDictionary
    {
        public Hamburger()
        {
            this.InitializeComponent();
        }

        public ICommand OnClicked => new ClickedCommand();

        private class ClickedCommand : ICommand
        {
            public event EventHandler CanExecuteChanged;

            public bool CanExecute(object parameter) =>
                parameter is Button;

            public void Execute(object parameter)
            {
                var button = (Button)parameter;

                // Walk up the tree until we reach a SplitView
                FrameworkElement parent = button;
                do
                    parent = parent.Parent as FrameworkElement;
                while (!(parent is SplitView));

                var splitView = (SplitView)parent;
                splitView.IsPaneOpen = !splitView.IsPaneOpen;
            }
        }
    }
}

For some reason the binding for the Command property doesn't seem to be working; when I set a breakpoint inside the Execute method and click the button, the breakpoint is never hit. I tried adding a DataContext="{Binding RelativeSource={RelativeSource Self}}" to the top of the XAML file, but for some reason ResourceDictionary doesn't seem to support DataContext.

tl;dr: What can I do to make the Button.Command property bind correctly to OnClicked within the setter?

Upvotes: 2

Views: 3433

Answers (2)

Jay Zuo
Jay Zuo

Reputation: 15758

Like Mike said, usually we won't set Button.Command in ResourceDictionary. A hamburger button may not only be in SplitView but can be in another place and then you may need bind another command. So you can refer to Mike's suggestion.

But if you do want to set it in ResourceDictionary, you can try like following:

Firstly, in your case, your command is fixed, you can declare your ClickedCommand as a public class, then in the Style,set the Command like:

<Setter Property="Command">
    <Setter.Value>
        <local:ClickedCommand />
    </Setter.Value>
</Setter>

After this, you can use your command, but this won't fix your problem as in ClickedCommand, you use parameter to retrieve the Button, but the parameter is not the "sender" of the Command, but the object passed with CommandParameter property. So we need set this in the Style.

However, Bindings in Style Setters are not supported in UWP Apps. See Remarks in Setter class:

The Windows Runtime doesn't support a Binding usage for Setter.Value (the Binding won't evaluate and the Setter has no effect, you won't get errors, but you won't get the desired result either).

A workaround for this is using attached property to set up the binding in code behind for you. For example:

public class BindingHelper
{
    public static readonly DependencyProperty CommandParameterBindingProperty =
        DependencyProperty.RegisterAttached(
            "CommandParameterBinding", typeof(bool), typeof(BindingHelper),
            new PropertyMetadata(null, CommandParameterBindingPropertyChanged));

    public static bool GetCommandParameterBinding(DependencyObject obj)
    {
        return (bool)obj.GetValue(CommandParameterBindingProperty);
    }

    public static void SetCommandParameterBinding(DependencyObject obj, bool value)
    {
        obj.SetValue(CommandParameterBindingProperty, value);
    }

    private static void CommandParameterBindingPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if ((bool)e.NewValue)
        {
            BindingOperations.SetBinding(d, Button.CommandParameterProperty, new Binding { RelativeSource = new RelativeSource() { Mode = RelativeSourceMode.Self } });
        }
    }
}

Then in Style, using

<Setter Property="local:BindingHelper.CommandParameterBinding" Value="True" />

will set the Button as CommandParameter. Your Hamburger.xaml may like:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:Octopie.Styles">

    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="Square.xaml" />
    </ResourceDictionary.MergedDictionaries>

    <Style x:Key="HamburgerStyle" TargetType="Button" BasedOn="{StaticResource SquareStyle}">
        <Setter Property="Background" Value="Transparent" />
        <Setter Property="Command">
            <Setter.Value>
                <local:ClickedCommand />
            </Setter.Value>
        </Setter>
        <Setter Property="local:BindingHelper.CommandParameterBinding" Value="True" />
        <Setter Property="Content" Value="&#xE700;" />
        <Setter Property="FontFamily" Value="Segoe MDL2 Assets" />
    </Style>
</ResourceDictionary>

I delete x:Class="Octopie.Styles.Hamburger" and Hamburger.xaml.cs as there is no need to use code-behind for your ResourceDictionary.

Now we can use this ResourceDictionary in our page like:

<Page.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="Hamburger.xaml" />
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Page.Resources>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <SplitView DisplayMode="CompactOverlay" IsPaneOpen="True">
        <SplitView.Pane>
            <StackPanel>
                <Button Style="{StaticResource HamburgerStyle}" />
            </StackPanel>
        </SplitView.Pane>
    </SplitView>
</Grid>

But there is another problem in Execute method of ClickedCommand. In this method, you've used FrameworkElement.Parent to retrieve the SplitView. But

Parent can be null if an object was instantiated, but is not attached to an object that eventually connects to a page object root.

Most of the time, Parent is the same value as returned by VisualTreeHelper APIs. However, there may be cases where Parent reports a different parent than VisualTreeHelper does.

And in your case, you need use VisualTreeHelper.GetParent to get the SplitView. We can use a helper method to do this:

public static T FindParent<T>(DependencyObject child) where T : DependencyObject
{
    //get parent item
    DependencyObject parentObject = VisualTreeHelper.GetParent(child);

    //we've reached the end of the tree
    if (parentObject == null) return null;

    //check if the parent matches the type we're looking for
    T parent = parentObject as T;
    if (parent != null)
        return parent;
    else
        return FindParent<T>(parentObject);
}

Then in Execute method using:

public void Execute(object parameter)
{
    var button = (Button)parameter;
    var splitView = FindParent<SplitView>(button);
    splitView.IsPaneOpen = !splitView.IsPaneOpen;
}

Now the HamburgerStyle will work as you want.

Upvotes: 4

Mike Eason
Mike Eason

Reputation: 9723

What the hell?

You're going about this all wrong. You don't need to declare a new ICommand in a ResourceDictionary, it simply doesn't belong there. It belongs in your View Model, or whatever the Button.DataContext is set to.

The purpose of a Style is to control the look and feel of your controls, they should not explicitly set their own behaviours (commands).

Let me show you an example. You should declare your button like this:

<Button Style="{StaticResource HamburgerStyle}" Command="{Binding ClickedCommand}"/>

Where ClickedCommand is an object in your View Model.

Your HamburgerStyle should not set it's own Command property, otherwise you are limiting your Button to one single implementation of ICommand, this is unwise.

Upvotes: 2

Related Questions