Skip to content

Instantly share code, notes, and snippets.

@SlyZ
Last active January 4, 2019 13:02
Show Gist options
  • Save SlyZ/ca7b03931412115cc5fb1416180ad1b4 to your computer and use it in GitHub Desktop.
Save SlyZ/ca7b03931412115cc5fb1416180ad1b4 to your computer and use it in GitHub Desktop.
MVVM-friendly RoutedCommands. Command to command binding can be used to redirect one command to another. The primary use case for it is a redirection of a RoutedCommand to a different ICommand implementation (e.g. DelegateCommand, RelayCommand, etc.). Unlike the standard WPF CommandBinding, CommandToCommandBinding is a DependencyObject with Depe…
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
namespace Sly.Toolkit
{
/// <summary>
/// Command to command binding can be used to redirect one command to another. The primary use case for it
/// is a redirection of a RoutedCommand to a different ICommand implementation (e.g. DelegateCommand, RelayCommand, etc.).
/// Unlike the standard WPF CommandBinding, CommandToCommandBinding is a DependencyObject with DependencyProperties,
/// meaning that the source and target commands can be easily bound to in Xaml, and the commands can be implemented in the View Model.
/// Example usage:
/// <![CDATA[
/// <Window xmlns:st="clr-namespace:Sly.Toolkit" ...>
/// <st:CommandToCommand.Bindings>
/// <st:CommandToCommandBinding SourceCommand="Save" TargetCommand="{Binding MySaveDelegateCommand}" />
/// </st:CommandToCommand.Bindings>
/// </Window>
/// ]]>
/// </summary>
public static class CommandToCommand
{
/// <summary>
/// Identifies the CommandToCommand.Bindings attached property.
/// </summary>
public static readonly DependencyProperty BindingsProperty =
DependencyProperty.RegisterAttached("CommandToCommandBindingsInternal", typeof(CommandToCommandBindingCollection), typeof(CommandToCommand),
new UIPropertyMetadata(null));
/// <summary>
/// Gets the value of the CommandToCommand.Bindings attached property from a given System.Windows.UIElement.
/// </summary>
/// <param name="uielement">The element from which to read the property value.</param>
/// <returns>The value of the CommandToCommand.Bindings attached property.</returns>
public static CommandToCommandBindingCollection GetBindings(UIElement uielement)
{
if (uielement == null)
{
throw new ArgumentNullException(nameof(uielement));
}
var bindings = (CommandToCommandBindingCollection)uielement.GetValue(BindingsProperty);
if (bindings == null)
{
bindings = new CommandToCommandBindingCollection(uielement);
uielement.SetValue(BindingsProperty, bindings);
}
return bindings;
}
/// <summary>
/// Sets the value of the CommandToCommand.Bindings attached property to a given System.Windows.UIElement.
/// </summary>
/// <param name="uielement">The element on which to set the attached property.</param>
/// <param name="value">The property value to set.</param>
public static void SetBindings(UIElement uielement, CommandToCommandBindingCollection value)
{
if (uielement == null)
{
throw new ArgumentNullException(nameof(uielement));
}
var bindings = (CommandToCommandBindingCollection)uielement.GetValue(BindingsProperty);
if (bindings != null)
{
bindings.Unhook();
}
uielement.SetValue(BindingsProperty, value);
}
}
/// <summary>
/// A command binding encapsulating a single command-to-command mapping.
/// Unlike <see cref="System.Windows.Input.CommandBinding"/>, this binding is a DependencyObject,
/// and hence can be bound to in Xaml.
/// </summary>
public class CommandToCommandBinding : Freezable
{
/// <summary>
/// A dummy command used as a placeholder when source command is not set.
/// </summary>
private static readonly RoutedCommand m_dummyCommand = new RoutedCommand();
/// <summary>
/// Identifies the <see cref="SourceCommand"/> dependency property.
/// </summary>
public static readonly DependencyProperty SourceCommandProperty =
DependencyProperty.Register(nameof(SourceCommand), typeof(ICommand), typeof(CommandToCommandBinding),
new FrameworkPropertyMetadata(null, SourceCommandChanged));
/// <summary>
/// Identifies the <see cref="TargetCommand"/> dependency property.
/// </summary>
public static readonly DependencyProperty TargetCommandProperty =
DependencyProperty.Register(nameof(TargetCommand), typeof(ICommand), typeof(CommandToCommandBinding),
new FrameworkPropertyMetadata(null));
/// <summary>
/// Initializes a new instance of the <see cref="CommandToCommandBinding"/> class.
/// </summary>
public CommandToCommandBinding()
{
CommandBinding = new CommandBinding(m_dummyCommand, OnSourceExecuted, OnSourceCanExecute);
}
/// <summary>
/// Gets a <see cref="System.Windows.Input.CommandBinding"/> instance representing command-to-command mapping
/// that will be added to the <see cref="System.Windows.UIElement.CommandBindings"/> collection to enable
/// listening to routed commands.
/// </summary>
public CommandBinding CommandBinding { get; private set; }
/// <summary>
/// Gets or sets the source command for the binding.
/// </summary>
public ICommand SourceCommand
{
get { return (ICommand)GetValue(SourceCommandProperty); }
set { SetValue(SourceCommandProperty, value); }
}
/// <summary>
/// Gets or sets the target command for the binding.
/// </summary>
public ICommand TargetCommand
{
get { return (ICommand)GetValue(TargetCommandProperty); }
set { SetValue(TargetCommandProperty, value); }
}
private static void SourceCommandChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
var c2cBinding = (CommandToCommandBinding)obj;
c2cBinding.WritePreamble();
c2cBinding.CommandBinding.Command = ((ICommand)e.NewValue) ?? m_dummyCommand;
c2cBinding.WritePostscript();
}
private void OnSourceCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
var targetCommand = TargetCommand;
if (targetCommand != null)
{
e.CanExecute = targetCommand.CanExecute(e.Parameter);
e.Handled = true;
}
}
private void OnSourceExecuted(object sender, ExecutedRoutedEventArgs e)
{
var targetCommand = TargetCommand;
if (targetCommand != null)
{
targetCommand.Execute(e.Parameter);
e.Handled = true;
}
}
protected override Freezable CreateInstanceCore()
{
return new CommandToCommandBinding();
}
}
/// <summary>
/// A specialised collection used by the CommandToCommand.Bindings property as a storage for the command mappings.
/// This collection automatically adds and removes its own CommandToCommandBindings to the
/// <see cref="System.Windows.UIElement.CommandBindings"/> collection of the UIElement to which CommandToCommand.Bindings is attached.
/// </summary>
public sealed class CommandToCommandBindingCollection : FreezableCollection<CommandToCommandBinding>
{
private UIElement m_uielement;
/// <summary>
/// Initializes a new instance of the <see cref="CommandToCommandBindingCollection"/> class.
/// </summary>
/// <param name="uielement">UIElement to which this collection should add command bindings.</param>
internal CommandToCommandBindingCollection(UIElement uielement)
{
m_uielement = uielement;
Hook();
}
private void Hook()
{
((INotifyCollectionChanged)this).CollectionChanged += OnCollectionChanged;
}
internal void Unhook()
{
((INotifyCollectionChanged)this).CollectionChanged -= OnCollectionChanged;
if (m_uielement != null)
{
for (int i = 0; i < this.Count; ++i)
{
m_uielement.CommandBindings.Remove(this.ElementAt(i).CommandBinding);
}
}
}
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (m_uielement != null)
{
if (e.OldItems != null)
{
foreach (CommandToCommandBinding c2cBinding in e.OldItems)
{
m_uielement.CommandBindings.Remove(c2cBinding.CommandBinding);
}
}
if (e.NewItems != null)
{
foreach (CommandToCommandBinding c2cBinding in e.NewItems)
{
m_uielement.CommandBindings.Add(c2cBinding.CommandBinding);
}
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment