こんにちは、働くC#プログラマーのさんさめです。
本記事では、RoutedCommandという、
WPFが提供するICommand実装クラスを自分で作ってみるやり方を紹介します。
MVVMといえば、コードビハインドには何も書かないのが主流(?)ですが、
「Viewで完結する処理」なら、
むしろViewModelに処理を持たせない方がすっきりします。
RoutedCommandはそんなView向けの実装をするときに使えます。
そもそもRoutedCommandとは?
WPFが提供するICommandインタフェース実装クラスです。
- LivetでいうViewModelCommand
- PrismでいうDelegateCommand
の仲間みたいなものです(目的とするところはだいぶ違いますが…)
ともあれ使い方を見ていきましょう。
RoutedCommandを用意して呼び出すまでの手順
以下の手順で呼び出すところまで実装できます。
- RoutedCommandインスタンスをViewのクラスに用意
- ViewのCommandBindingsプロパティにRoutedCommandを追加
- Commandを使いたい要素(例:Button)に作ったRoutedCommandを設定
入力された文字の先頭文字を、
MessageBoxに表示する機能を持つ自家製TextBoxを作る、
という例を題材に1つずつ見ていきましょう。
自家製TextBoxを作るためにUserControlを作成
「追加」→「UserControl(WPF)」を選びます。
ファイル名はシンプルに「MyTextBox」で。
そして、中に名前を付けたTextBoxだけを配置します。
<UserControl x:Class="RoutedCommandSample.MyTextBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:RoutedCommandSample"
mc:Ignorable="d" >
<TextBox x:Name="InnerTextBox">
</TextBox>
</UserControl>
これで、配置すると結果的にTextBoxが置かれるだけの独自コントロールが作れました。
RoutedCommandインスタンスをViewのクラスに用意
RoutedCommandクラスのインスタンスを宣言します。
Viewで済む操作を実装しようとしているので、
インスタンスの置き場所も当然Viewのコードビハインドです。
MVVMに慣れた方だとこの時点で「ヒエッ」となるかもしれませんが、
もう少々お付き合いください。
public partial class MyTextBox : UserControl
{
// RoutedCommandのインスタンス。宣言と同時に作ってしまう
public static RoutedCommand MyCommand { get; }
= new RoutedCommand(nameof(MyCommand), typeof(MainWindow));
public MyTextBox()
{
InitializeComponent();
}
}
後からインスタンスを差し替えることは無いので、
staticなゲッターのみのプロパティとして宣言してしまった方が良いでしょう。
これで1ステップ目終了です。
ViewのCommandBindingsプロパティにRoutedCommandを追加
コンストラクタのInitializeComponentの後か、
もしくはxaml上でCommandBindingsプロパティに、
RoutedCommandを追加します。
より正確には、RoutedCommandとそれに対応する処理の紐づけを行います。
次のコードは、コンストラクタのInitializeComponentの後、
つまり、コードビハインドで行っている例です。
public MyTextBox()
{
InitializeComponent();
\\ RoutedCommandに処理を紐づける
CommandBindings.Add(new CommandBinding(
MyCommand, // どのコマンドに処理を紐づけるか
OnMyCommand, // コマンド実行時の処理
CanMyCommand // コマンド実行可能かを判定する処理
));
}
private void OnMyCommand(object sender, RoutedEventArgs eventArgs)
{
// 先頭1文字をMessageBoxに表示
MessageBox.Show(InnerTextBox.Text.Substring(0, 1));
}
private void CanMyCommand(object sender, CanExecuteRoutedEventArgs eventArgs)
{
// 何かしら入力があれば実行可能
eventArgs.CanExecute = !string.IsNullOrWhiteSpace(InnerTextBox.Text);
}
xamlで記述する例を後述しているので、あえてメソッドに分離していますが、
コードビハインドで書くと決めているならラムダ式にしてしまってもよいでしょう。
一応書いておくと以下のようになります。
public MyTextBox()
{
InitializeComponent();
CommandBindings.Add(new CommandBinding(
MyCommand, // どのコマンドに処理を紐づけるか
(s, e) => MessageBox.Show(InnerTextBox.Text.Substring(0, 1)),
(s, e) => e.CanExecute = !string.IsNullOrWhiteSpace(InnerTextBox.Text)
));
}
一方、xamlで書く場合は、次の通りです。
上記のコードビハインドの例の、CommandBinding.Addの部分が不要になります。
<UserControl x:Class="RoutedCommandSample.MyTextBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:RoutedCommandSample"
mc:Ignorable="d" >
<UserControl.CommandBindings>
<CommandBinding Command="{x:Static local:MyTextBox.MyCopyCommand}"
Executed="OnMyCopy"
CanExecute="CanMyCopy"/>
</UserControl.CommandBindings>
<TextBox x:Name="InnerTextBox">
</TextBox>
</UserControl>
ちなみに、ユーザーコントロールを追加したばかりの段階で、
上記のxamlを書こうとすると、「そんなの無いよ!」と怒られてしまいますが、
ビルドできるのでご安心を。一度ビルドを通すと警告も出なくなります。
これで何が行われたかというと、
このMyTextBoxに対して、MyCommandが呼ばれたときに、
OnMyCommandの処理…つまり、先頭文字をコピーする処理が呼ばれるようになった、
ということです。
これをしないと、ただMyCommandという名前の受け皿を用意しただけになります。
イメージとしては処理を記述していないButtonを置いたようなものですね。
これで2ステップ目は終わりです。
Commandを使いたい要素(例:Button)に作ったRoutedCommandを設定
いよいよ作ったCommandを呼び出してみます。
挙動確認するためにMainWindowに、
MyTextBoxコントロールとButtonを置いてみます。
<Window x:Class="RoutedCommandSample.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:RoutedCommandSample"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<DockPanel>
<Button DockPanel.Dock="Top"
Content="先頭文字表示"
Command="{x:Static local:MyTextBox.MyCommand}"
CommandTarget="{Binding ElementName=MyText}"/>
<local:MyTextBox x:Name="MyText"/>
</DockPanel>
</Window>
CommandTargetという普段なかなか見ないプロパティを使っていますね。
ButtonにただCommandを設定しただけでは、
そのCommandをどのコントロールに適用させれば良いかわからないため、
しっかり指定してあげる必要があります。
さて、これですべてのステップが終了しました。
さっそく実行して挙動を確かめてみましょう。
実行した結果を見てみる
起動した直後はボタンが押せませんが…
何かしら文字を入力するとボタンが有効化しました。
そして、ボタンを押すと、MessageBoxが表示されます
注目すべきは、CanExecuteの呼出しタイミングですね。
特にPropertyChanged的なことを実装したわけでもないのに、
必要十分なタイミングでButtonに通知が行われています。
RoutedCommandの「Routed」とは
ちなみに、以下のようにContextMenuに設定した場合は、
CommandTargetを指定する必要がありません。
<Window x:Class="RoutedCommandSample.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:RoutedCommandSample"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<DockPanel>
<DockPanel.ContextMenu>
<ContextMenu>
<MenuItem Header="先頭文字表示"
Command="{x:Static local:MyTextBox.MyCommand}"/>
</ContextMenu>
</DockPanel.ContextMenu>
<Button DockPanel.Dock="Top"
Content="先頭文字表示"
Command="{x:Static local:MyTextBox.MyCommand}"
CommandTarget="{Binding ElementName=MyText}"/>
<local:MyTextBox x:Name="MyText"/>
</DockPanel>
</Window>
これは、DockPanelとMyTextBoxがビジュアルツリー上で親子関係にあるためです。
「Routed」Commandというだけあって、ルーティングしてくれているわけですね。
RoutedUICommandとの違いは?
RoutedUICommandというコマンドもあります。
RoutedCommandの派生クラスです。
こちらは、ContextMenu(右クリックメニュー)に配置したときに、
デフォルトで表示される文言を指定できます。
たとえば、以下のように宣言を変えます。
public static RoutedUICommand MyCommand { get; }
= new RoutedUICommand("先頭文字表示", nameof(MyCommand), typeof(MainWindow));
すると、ContextMenuで利用するときに、
Headerを省略するとあらかじめ設定していた文字を代わりに表示してくれます。
…ただし、残念ながらButtonでは効きません(Contentを省略できない)
なんだかどっちつかずですね。
ICommandはButtonで使いたいことが多いので、
あえてRoutedUICommandを使う必要はそこまでないでしょう。
WPFがフレームワーク側で提供しているRoutedCommand
わざわざ独自にRoutedCommandを作らなくても、
実はWPFではアプリケーションの操作でよくありそうなものを、
RoutedCommandという形で提供しています。
たとえば、ApplicationCommandsクラスでは、
アプリケーションを代表する処理を表すコマンドが提供されています。
例えば、以下のようなものです。
- Copy
- Undo
- Close
たとえば、TextBoxというコントロールにはCopyやCutなどのコマンドに、
処理が紐づけされています。
そのため、配置したTextBoxに対してApplicationCommands.Copyを設定すると、
ちゃんと範囲選択された文字がクリップボードにコピーされます。
<DockPanel>
<Button Content="Copy"
DockPanel.Dock="Left"
Command="{x:Static ApplicationCommands.Copy}"
CommandTarget="{Binding ElementName=InnerTextBox}"/>
<TextBox x:Name="InnerTextBox"/>
</DockPanel>
実行結果はこんな感じ。範囲選択していないとボタンは有効化しません。
とはいえ、TextBoxではたまたま実装がありましたが、
すべてのコントロールにすべてのRoutedCommandの処理の実装があるわけではありません。
提供されているのはあくまでもRoutedCommandなのでそれ自体には実装はなく、
CommandBindingする側、つまりVew要素を作る側が実際の処理内容を
実装することになります。
その上、あるコントロールに実装があるかないかは、
そのコントロールを使う側がCommand指定してみないと分からないという欠点があります。
正直フレームワーク側が提供しているRoutedCommandを積極活用する理由は、
あんまり無いのかもしれません。
そのコントロール専用の独自コマンドだったら、
そりゃ実装あるやろ、ってなるから、まだ安心して使えるんだけどね…
まとめ
本記事ではRoutedCommandを紹介し、その作り方を解説しました。
- RoutedCommandとは
- RoutedCommandの作り方
- WPFはRoutedCommandを提供しているがスベり気味
1つのView要素として完結しているカスタムコントロールなどを作るときは、
特に便利に使えるので、RoutedCommandを覚えていただければ幸いです。
関連記事
Viewだけで完結する処理の例です。
複数のDataGridに対して、
キーボードだけでフォーカスを移動させる方法です。
コメント