Reputation: 34609
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=""/>
<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
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="" />
<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
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