2021年9月30日木曜日

WPFのRoutedEventへコマンドをバインディングできる添付プロパティとマークアップ拡張をつくってみた

つい最近WPFのマークアップ拡張について話をする機会があったのですが、
自分はコンバーターは使っていてもマークアップ拡張を実装したことはありませんでした。
せっかくならちゃんとしたサンプルを用意したいと思い、
今困っていたことをそのまま解決するサンプルをつくってみることにしました。

困っていたことはRoutedEventにコマンドをバインディングする方法についてです。
Blend SDKを使えばできることは知っているのですが、
開発中のプロジェクトがタイミング的に参照を追加したくなかったのです。
それにBlend SDKの方法は結構行数を使うので、
スマートにバインディングできるのであれば今後も使い続けられるかもしれません。
ということでつくってみたのが以下のソースコードです。

using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Windows.Input;
namespace WpfSandbox
{
/// <summary>
/// サンプルの ViewModel です。
/// </summary>
public class SampleViewModel : INotifyPropertyChanged
{
/// <summary>
/// ログを出力するコマンド。
/// </summary>
private ICommand logCommand = new RelayCommand(_ => Debug.WriteLine("LogCommand is executed.")); // RelayCommand はよくあるやつ
/// <summary>
/// プロパティ値が変更するときに発生するイベント。
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// ログを出力するコマンドを取得または設定します。
/// </summary>
/// <value>ログを出力するコマンド。</value>
public ICommand LogCommand
{
get
{
return logCommand;
}
set
{
logCommand = value;
OnPropertyChanged();
}
}
/// <summary>
/// プロパティ値が変更したときの処理です。
/// </summary>
/// <param name="propertyName">値が変更されたプロパティの名前。</param>
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
<Window x:Class="WpfSandbox.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:l="clr-namespace:WpfSandbox"
mc:Ignorable="d"
Title="MainWindow"
Height="450"
Width="800">
<Window.DataContext>
<l:SampleViewModel/>
</Window.DataContext>
<Grid>
<TextBlock Text="マウスのボタンを押してみてね!" l:CommandService.RoutedEventBinding="{l:RoutedEventBinding UIElement.MouseDown, {Binding LogCommand}}"/>
</Grid>
</Window>
using System;
using System.Windows;
using System.Windows.Data;
using System.Windows.Markup;
namespace WpfSandbox
{
/// <summary>
/// <see cref="RoutedEvent"/> に紐づいたコマンドを設定するマークアップ拡張です。
/// </summary>
[MarkupExtensionReturnType(typeof(RoutedEventBinding))]
public class RoutedEventBinding : MarkupExtension
{
/// <summary>
/// コマンドと紐づける <see cref="RoutedEvent"/> を取得または設定します。
/// </summary>
/// <value>コマンドと紐づける <see cref="RoutedEvent"/>。</value>
public RoutedEvent RoutedEvent { get; set; }
/// <summary>
/// 実行されるコマンドを取得または設定します。
/// </summary>
/// <value>実行されるコマンド。</value>
public BindingBase Command { get; set; }
/// <summary>
/// コマンドの実行時にコマンドへ渡すことのできるユーザー定義データの値を取得または設定します。
/// </summary>
/// <value>コマンドの実行時にコマンドへ渡すことのできるユーザー定義データの値。</value>
public BindingBase CommandParameter { get; set; }
/// <summary>
/// コマンドが実行されているオブジェクトを取得または設定します。
/// </summary>
/// <value>コマンドが実行されているオブジェクト。</value>
public BindingBase CommandTarget { get; set; }
/// <summary>
/// <see cref="RoutedEventCommandBinding"/>クラスの新しいインスタンスを初期化します。
/// </summary>
public RoutedEventBinding()
: this(null, null, null, null)
{
// 何もしません。
}
/// <summary>
/// <see cref="RoutedEventCommandBinding"/>クラスの新しいインスタンスを初期化します。
/// </summary>
/// <param name="routedEvent">コマンドと紐づける <see cref="RoutedEvent"/>。</param>
public RoutedEventBinding(RoutedEvent routedEvent)
: this(routedEvent, null, null, null)
{
// 何もしません。
}
/// <summary>
/// <see cref="RoutedEventCommandBinding"/>クラスの新しいインスタンスを初期化します。
/// </summary>
/// <param name="routedEvent">コマンドと紐づける <see cref="RoutedEvent"/>。</param>
/// <param name="command">実行されるコマンド。</param>
public RoutedEventBinding(RoutedEvent routedEvent, BindingBase command)
: this(routedEvent, command, null, null)
{
// 何もしません。
}
/// <summary>
/// <see cref="RoutedEventCommandBinding"/>クラスの新しいインスタンスを初期化します。
/// </summary>
/// <param name="routedEvent">コマンドと紐づける <see cref="RoutedEvent"/>。</param>
/// <param name="command">実行されるコマンド。</param>
/// <param name="commandParameter">コマンドの実行時にコマンドへ渡すことのできるユーザー定義データの値。</param>
public RoutedEventBinding(RoutedEvent routedEvent, BindingBase command, BindingBase commandParameter)
: this(routedEvent, command, commandParameter, null)
{
// 何もしません。
}
/// <summary>
/// <see cref="RoutedEventCommandBinding"/>クラスの新しいインスタンスを初期化します。
/// </summary>
/// <param name="routedEvent">コマンドと紐づける <see cref="RoutedEvent"/>。</param>
/// <param name="command">実行されるコマンド。</param>
/// <param name="commandParameter">コマンドの実行時にコマンドへ渡すことのできるユーザー定義データの値。</param>
/// <param name="commandTarget">コマンドが実行されているオブジェクト。</param>
public RoutedEventBinding(RoutedEvent routedEvent, BindingBase command, BindingBase commandParameter, BindingBase commandTarget)
{
RoutedEvent = routedEvent;
Command = command;
CommandParameter = commandParameter;
CommandTarget = commandTarget;
}
/// <summary>
/// マークアップ拡張の設定先のプロパティに提供される値を返します。
/// </summary>
/// <param name="serviceProvider">サービスプロバイダー。</param>
/// <returns>プロパティに設定する値。</returns>
public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
}
}
using System.Diagnostics;
using System.Windows;
using System.Windows.Data;
using System.Windows.Input;
namespace WpfSandbox
{
/// <summary>
/// コマンドに関するサービスを提供します。
/// </summary>
public class CommandService : DependencyObject
{
/// <summary>
/// <see cref="RoutedEventBinding"/> に紐づいたコマンドの添付プロパティを識別します。
/// </summary>
public static readonly DependencyProperty RoutedEventBindingProperty =
DependencyProperty.RegisterAttached(
"RoutedEventBinding",
typeof(RoutedEventBinding),
typeof(CommandService),
new PropertyMetadata(null, OnRoutedEventChanged));
/// <summary>
/// 実行されるコマンドの添付プロパティを識別します。
/// </summary>
private static readonly DependencyProperty CommandProperty =
DependencyProperty.Register(
"Command",
typeof(ICommand),
typeof(CommandService),
new PropertyMetadata(null));
/// <summary>
/// コマンドの実行時にコマンドへ渡すことのできるユーザー定義データの値の添付プロパティを識別します。
/// </summary>
private static readonly DependencyProperty CommandParameterProperty =
DependencyProperty.Register(
"CommandParameter",
typeof(object),
typeof(CommandService),
new PropertyMetadata(null));
/// <summary>
/// コマンドが実行されているオブジェクトの添付プロパティを識別します。
/// </summary>
private static readonly DependencyProperty CommandTargetProperty =
DependencyProperty.Register(
"CommandTarget",
typeof(IInputElement),
typeof(CommandService),
new PropertyMetadata(null));
/// <summary>
/// <see cref="RoutedEventBinding"/> に紐づいたコマンドを実行する処理の添付プロパティを識別します。
/// </summary>
private static readonly DependencyProperty RoutedEventActionProperty =
DependencyProperty.Register(
"RoutedEventAction",
typeof(RoutedEventHandler),
typeof(CommandService),
new PropertyMetadata(null));
/// <summary>
/// <see cref="RoutedEvent"/> に紐づいたコマンドを取得します。
/// </summary>
/// <param name="d">対象の要素。</param>
/// <returns><see cref="RoutedEvent"/> に紐づいたコマンド。</returns>
[AttachedPropertyBrowsableForType(typeof(UIElement))]
public static RoutedEventBinding GetRoutedEventBinding(DependencyObject d)
{
return (RoutedEventBinding)d.GetValue(RoutedEventBindingProperty);
}
/// <summary>
/// <see cref="RoutedEvent"/> に紐づいたコマンドを設定します。
/// </summary>
/// <param name="d">対象の要素。</param>
/// <param name="routedEventCommand"><see cref="RoutedEvent"/> に紐づいたコマンド。</param>
[AttachedPropertyBrowsableForType(typeof(UIElement))]
public static void SetRoutedEventBinding(DependencyObject d, RoutedEventBinding routedEventBinding)
{
d.SetValue(RoutedEventBindingProperty, routedEventBinding);
}
/// <summary>
/// <see cref="RoutedEvent"/> に紐づいたコマンドの更新時の処理です。
/// </summary>
/// <param name="d">プロパティの値が変更されたオブジェクト。</param>
/// <param name="e">イベント引数。</param>
private static void OnRoutedEventChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
UIElement element = d as UIElement;
if (element == null)
{
return;
}
RoutedEventBinding oldRoutedEventBinding = e.OldValue as RoutedEventBinding;
if (oldRoutedEventBinding != null)
{
RemoveHandler(oldRoutedEventBinding, element);
}
RoutedEventBinding newRoutedEventBinding = e.NewValue as RoutedEventBinding;
if (newRoutedEventBinding != null)
{
AddHandler(newRoutedEventBinding, element);
}
}
/// <summary>
/// イベントハンドラを追加します。
/// </summary>
/// <param name="routedEventBinding"><see cref="RoutedEvent"/> に紐づいたコマンド。</param>
/// <param name="element">イベントハンドラを追加する要素。</param>
/// <exception cref="ArgumentNullException"><paramref name="element"/>が<c>null</c>です。</exception>
private static void AddHandler(RoutedEventBinding routedEventBinding, UIElement element)
{
Debug.Assert(routedEventBinding != null, "routedEventBinding != null");
Debug.Assert(element != null, "element != null");
if (routedEventBinding.RoutedEvent == null)
{
return;
}
if (routedEventBinding.Command != null)
{
BindingOperations.SetBinding(element, CommandProperty, routedEventBinding.Command);
}
if (routedEventBinding.CommandParameter != null)
{
BindingOperations.SetBinding(element, CommandParameterProperty, routedEventBinding.CommandParameter);
}
if (routedEventBinding.CommandTarget != null)
{
BindingOperations.SetBinding(element, CommandTargetProperty, routedEventBinding.CommandTarget);
}
RoutedEventHandler routedEventHandler = (sender, e) =>
{
ICommand command = GetValue<ICommand>(element, CommandProperty);
if (command == null)
{
return;
}
object parameter = GetValue<object>(element, CommandParameterProperty);
RoutedCommand routed = command as RoutedCommand;
if (routed != null)
{
IInputElement target = GetValue<IInputElement>(element, CommandTargetProperty) ?? e.Source as IInputElement;
if (routed.CanExecute(parameter, target))
{
routed.Execute(parameter, target);
}
}
else
{
if (command.CanExecute(parameter))
{
command.Execute(parameter);
}
}
};
element.SetValue(RoutedEventActionProperty, routedEventHandler);
element.AddHandler(routedEventBinding.RoutedEvent, routedEventHandler);
}
/// <summary>
/// イベントハンドラを削除します。
/// </summary>
/// <param name="routedEventBinding"><see cref="RoutedEvent"/> に紐づいたコマンド。</param>
/// <param name="element">イベントハンドラを削除する要素。</param>
/// <exception cref="ArgumentNullException"><paramref name="element"/>が<c>null</c>です。</exception>
private static void RemoveHandler(RoutedEventBinding routedEventBinding, UIElement element)
{
Debug.Assert(routedEventBinding != null, "routedEventBinding != null");
Debug.Assert(element != null, "element != null");
if (routedEventBinding.RoutedEvent == null)
{
return;
}
if (routedEventBinding.Command != null)
{
BindingOperations.ClearBinding(element, CommandProperty);
}
if (routedEventBinding.CommandParameter != null)
{
BindingOperations.ClearBinding(element, CommandParameterProperty);
}
if (routedEventBinding.CommandTarget != null)
{
BindingOperations.ClearBinding(element, CommandTargetProperty);
}
RoutedEventHandler routedEventAction = element.GetValue(RoutedEventActionProperty) as RoutedEventHandler;
if (routedEventAction == null)
{
return;
}
element.RemoveHandler(routedEventBinding.RoutedEvent, routedEventAction);
}
/// <summary>
/// プロパティの値を取得します。
/// </summary>
/// <typeparam name="T">プロパティの値の型。</typeparam>
/// <param name="d">プロパティの値を取得するオブジェクト。</param>
/// <param name="dp">プロパティの値を取得する添付プロパティ。</param>
/// <returns></returns>
private static T GetValue<T>(DependencyObject d, DependencyProperty dp)
{
object value = d.GetValue(dp);
if (value is T)
{
return (T)value;
}
return default;
}
}
}

やっていることはCommandService.RoutedEventBindingという添付プロパティを用意し、
RoutedEventBindingというマークアップ拡張を設定できるようにしました。
RoutedEventBindingにはRoutedEventとコマンドを設定できるので、
だいたい1行で実装することができます。
(テストはしてませんが)たぶんRoutedCommandにも対応しています。

ほぼ車輪の再発明なのでこれを使おうとする方は少ないと思いますが、
何かの参考にはなるんじゃないでしょうか。

0 件のコメント:

コメントを投稿