やっていることは、
クリックしたマウスの座標をMessageBoxで表示するだけです。
コードビハインドでは、
XAML側でCanvasにx:Nameを付けて、
Clickイベントに対応するメソッドを記述するだけなので、
ほんの数行のコードで済みます。
では、なぜAttachedPropertyを使うのかと言いますと、
ViewModelでは直接View上のイベントを捕まえることが出来ない(やってはいけない)ので、
AttachedProperyを使い、ViewModelまで導いて上げる必要があります。
CanvasClick.cs
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace AttachedPropertyDemo;
public static class CanvasClick
{
// Commandという名前のDPを生やす
public static readonly DependencyProperty CommandProperty =
DependencyProperty.RegisterAttached(
"Command", // 名前
typeof(ICommand), // バインド元の型 ... 今回はコマンド
typeof(CanvasClick), // 所有するクラス。
new PropertyMetadata(null, OnCommandChanged)); // バインド時呼び出す
// コードビハインド用Setter
public static void SetCommand(DependencyObject element, ICommand value)
=> element.SetValue(CommandProperty, value);
// コードビハインド用Getter
public static ICommand GetCommand(DependencyObject element)
=> (ICommand)element.GetValue(CommandProperty);
// バインド時の処理
private static void OnCommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// キャンバスで無ければ返る
if (d is not Canvas canvas)
return;
// OldValueがある場合はイベント解除
if (e.OldValue != null)
canvas.MouseLeftButtonDown -= Canvas_MouseLeftButtonDown;
// NewValueがある場合はイベントの登録
if (e.NewValue != null)
canvas.MouseLeftButtonDown += Canvas_MouseLeftButtonDown;
}
// マウスのクリックイベント(イベントドリブン)
private static void Canvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
// キャンバスで無い場合返る。
if (sender is not Canvas canvas)
return;
// コマンドが取得できなければ返る。
var command = GetCommand(canvas);
if (command == null)
return;
// クリック座標取得
Point pos = e.GetPosition(canvas);
// コマンドが実行可能なら実行する。
if (command.CanExecute(pos))
{
// 引数にクリック座標を渡す。
command.Execute(pos);
}
}
}
using System.Windows.Input;
namespace AttachedPropertyDemo;
public class RelayCommand<T> : ICommand
{
private readonly Action<T?> execute;
public RelayCommand(Action<T?> execute)
{
this.execute = execute;
}
public bool CanExecute(object? parameter) => true;
public void Execute(object? parameter)
=> execute((T?)parameter);
public event EventHandler? CanExecuteChanged;
}
ICommandの簡易実装ヘルパー
MainWindow.xaml
<Window x:Class="AttachedPropertyDemo.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:local="clr-namespace:AttachedPropertyDemo"
mc:Ignorable="d"
Title="CanvasClickSample" Height="400" Width="300">
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Grid>
<Canvas Background="LightGray"
local:CanvasClick.Command="{Binding CanvasClickCommand}" />
<!-- CanvasClick.Command(AttachedProperty) に
ViewModelの CanvasClickCommand をバインド -->
</Grid>
</Window>
using System.Windows;
namespace AttachedPropertyDemo;
public class MainWindowViewModel
{
// キャンバスクリックコマンドの定義
public RelayCommand<Point> CanvasClickCommand { get; }
public MainWindowViewModel()
{
// コマンドオブジェクトを生成
CanvasClickCommand = new RelayCommand<Point>(OnCanvasClick);
}
// コマンド実行時に呼ばれる処理
private void OnCanvasClick(Point p)
{
MessageBox.Show($"Click : {p.X:F0}, {p.Y:F0}");
}
}
本来、ViewModelはINotifyPropertyChangedを実装し、
プロパティの変更を通知する必要がありますが、
今回はコマンドのサンプルコードという事で、
バインドされた初回のみて、以降変更される予定が無いので、
INotifyPropertyChangedの実装は省略しています。
プロパティのバイドの場合は必須だと思いますが、
コマンドだけならこのままでも問題ないと思います。
実行時のイメージ

今回は ICommand を AttachedProperty として定義しましたが、
同じ方法で通常のプロパティも DependencyProperty として追加することが出来ます。
ただし、すべての状態を DependencyProperty にする必要はありません。
AttachedProperty の実装では、
コントロールごとに内部状態(フィールドのようなもの)を持たせたい場合があります。
しかし AttachedProperty は static クラスとして実装するため、
インスタンスフィールドを持つことが出来ません。
そのため、コントロールごとに状態を保持する仕組みが必要になります。
ベタな方法としては、コントロールの Tag プロパティに
オブジェクトを格納する方法もあります。
しかし Tag は基本的に一つしか値を持つことが出来ないため、
この用途で使ってしまうのは少しもったいない気もします。
そのような場合は ConditionalWeakTable を使うと、
DependencyObject に対して安全にデータを紐付けることが出来ます。

これで、プロパティ、コマンド、フィールド、イベントを
AttachedProperty を使って扱えることが確認出来ました。
このように考えると、
多くの機能は AttachedProperty とその関連機能を組み合わせることで
実現することが出来そうです。
ここで一つ注意点があります。
このサンプルコードではクリック座標をそのまま ViewModel に渡しているだけで、
AttachedProperty の利点があまり活かされていません。
AttachedProperty の本来の役割は、
XAML のプロパティ変更やイベントをフックし、
共通処理を実行したうえで ViewModel に処理を渡すことにあります。
つまり View と ViewModel の間に処理を挟み込むことで、
View 側の振る舞いを拡張することが出来ます。
例えば TextBox を Button のようにクリック可能なコントロールにすることも理屈上は可能です。
AttachedProperty を使えば既存コントロールに任意の振る舞いを追加出来るため、
極端な話、ほとんどのコントロールを Button のように扱うことも出来ます。
もっとも、そのような使い方は UI としてはあまり良い設計とは言えないでしょう。
他のアイディアとしては Slider を ProgressBar の代わりに使えば、
処理を一時停止して手順を戻すような UI を作ることも理屈上は可能です。
いわゆる動画プレイヤーなどで使われる シークバー のような UI です。
WPF には基本的なコントロールは一通り揃っていますが、
機能が豊富なコントロールはそれほど多くありません。
そのため、必要な機能がある場合は自作する必要があります。
しかし、アプリごとにコードビハインドで実装してしまうと
同じようなコードを何度も書くことになり効率が良くありません。
AttachedProperty を使ってコントロールを拡張すれば、
既存コントロールに振る舞いを追加し、その機能を再利用することが出来ます。
これが AttachedProperty の最大の利点だと思います。
WPF では、カスタムコントロールやユーザーコントロール、
ビヘイビアなどを使って機能を拡張する方法もあります。
しかし、既存コントロールをそのまま利用しながら
振る舞いだけを追加出来るという点では、
AttachedProperty が最も柔軟で使いやすい方法だと思います。

コメント