WPFプログラミング備忘録

WPF MVVMパターンを使わずにルーティングコマンドを理解する

WPF

はじめに

この記事ではコマンドの簡単な使い方を確認します。

コマンドはMVVMパターンには欠かせない存在で、実際MVVMパターンと一緒に説明されることが多いです。

しかし、それだと私はコマンドが理解し辛かったため今回はMVVMパターンは使用せずにルーティングコマンドに焦点を絞ります。

今回の開発環境は以下のとおりです。

オペレーティングシステムWindows10 x64
Visual StudioMicrosoft Visual Studio Community 2019 Version 16.11.2
.NET Framewrok4.7.2

コマンドに関連するクラスとインターフェース

登場人物をざっと列挙します。

ICommandインターフェース

まずコマンドと呼ばれるクラスはICommandインターフェースを実装している必要があります。

ICommandインターフェースは以下の2つのメソッドと1つのイベントをメンバーに持ちます。

public interface ICommand
{
    bool CanExecute(object parameter);
    void Execute(object parameter);
    event EventHandler CanExecuteChanged;
}

実際にこれらの実装をすることもありますが、今回はWPFが提供しているルーティングコマンドを使うのでこれらの実装はしません。

CommandBindingクラス

CommandBindingクラスにはCommandプロパティやユーザーに実装の責任があるイベントがあります。

その中で今回はCanExecuteイベントとExecutedイベントを実装します。

CommandBinding クラス (System.Windows.Input)
RoutedCommand を、コマンドを実装するイベント ハンドラーにバインドします。

RoutedCommandクラスとRoutedUICommandクラス

WPFが提供しているのがRoutedCommandクラスとRoutedUICommandクラスです。

定義は以下のとおりです。

public class RoutedCommand : System.Windows.Input.ICommand

public class RoutedUICommand : System.Windows.Input.RoutedCommand

今回はRoutedUICommandを使用します。

RoutedUICommandとCommandBindingの動作については後ほど説明します。

ApplicationCommandsクラス

頻繁に使用する操作に対応するためにWPFから以下のクラスが提供されています。

  • ApplicationCommands
  • MediaCommands
  • NavigationCommands
  • ComponentCommands
  • EditingCommands

ApplicationCommandsクラスもWPFが提供しているクラスで、多くの静的なRoutedUICommand型のプロパティを持っています。

今回はその中のCopyプロパティとPasteプロパティを使用します。

ApplicationCommands クラス (System.Windows.Input)
一連の標準的なアプリケーション関連コマンドを提供します。

アプリケーションの仕様

まず起動時の画面は以下のとおりです。コピーボタンと貼り付けボタンは無効化されています。

起動時の画面

コピーボタンの隣のテキストボックスに文字を入力するとコピーボタンが有効化されるようにします。

テキストボックスに入力

コピーボタンを押すと貼り付けボタンが有効化されるようにします。

コピーボタンを押す

貼り付けボタンを押すと右側のテキストブロックにコピーした文字列を表示します。貼り付けが終わると貼り付けボタンは再度無効化されるようにします。

貼り付けボタンを押す

リセットボタンを押すと起動時の画面に戻るようにします。

リセットボタンを押す

プログラムの実装

XAMLファイルは以下のとおりです。

<Window x:Class="WpfCommandSample.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:WpfCommandSample"
        mc:Ignorable="d"
        Title="Command Sample" Height="160" Width="400">
    <Window.CommandBindings>
        <CommandBinding Command="Copy" Executed="OnCopy" CanExecute="OnIsExistText"/>
        <CommandBinding Command="Paste" Executed="OnPaste" CanExecute="OnIsPasteEnabled"/>
        <CommandBinding Command="local:Commands.ResetCommand" Executed="OnReset"/>
    </Window.CommandBindings>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <StackPanel Orientation="Horizontal" Margin="6">
            <Button Content="コピー" Command="Copy" Width="60" Margin="6"/>
            <TextBox x:Name="txtBox" Width="240" Height="30" VerticalContentAlignment="Center"/>
        </StackPanel>
        <StackPanel Grid.Row="1" Orientation="Horizontal" Margin="6">
            <Button Content="貼り付け" Command="Paste" Width="60" Margin="6"/>
            <TextBlock x:Name="txtBlock" Width="240" Height="30" />
        </StackPanel>
        <Button x:Name="btnReset" Grid.Row="2" Content="リセット" Command="local:Commands.ResetCommand" Width="60" Margin="6"/>
    </Grid>
</Window>

コードビハインドは以下のとおりです。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfCommandSample
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            Clipboard.Clear();
        }

        private void OnCopy(object sender, ExecutedRoutedEventArgs e)
        {
            string text = txtBox.Text;

            if (!string.IsNullOrWhiteSpace(text))
            {
                // クリップボードにテキストボックスの文字列を保存する
                Clipboard.SetData(DataFormats.Text, text);
            }
        }

        private void OnIsExistText(object sender, CanExecuteRoutedEventArgs e)
        {
            if(txtBox == null)
            {
                return;
            }
            
            // テキストボックスに空白以外の文字列が入力されていた場合はコピーボタンを有効化する
            e.CanExecute = !string.IsNullOrWhiteSpace(txtBox.Text);
        }

        private void OnPaste(object sender, ExecutedRoutedEventArgs e)
        {
            string text = Clipboard.GetText();
            txtBlock.Text = text;

            // テキストブロックに貼り付けた後はクリップボードをクリアする
            // クリップボードが空なので貼り付けボタンは無効化される
            Clipboard.Clear();
        }

        private void OnIsPasteEnabled(object sender, CanExecuteRoutedEventArgs e)
        {
            string text = Clipboard.GetText();
            
            // クリップボードにテキストが保存されていた場合は貼り付けボタンを有効化する
            e.CanExecute = !string.IsNullOrWhiteSpace(text);
        }

        private void OnReset(object sender, ExecutedRoutedEventArgs e)
        {
            // リセット処理をする
            Clipboard.Clear();
            txtBox.Text = string.Empty;
            txtBlock.Text = string.Empty;
        }
    }
}

ユーザー定義のコマンド(リセット)の実装は以下のとおりです。コマンド用に別ファイルを作成しています。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input; // RoutedUICommand

namespace WpfCommandSample
{
    // ユーザー定義のコマンド
    static class Commands
    {
        static readonly RoutedUICommand _resetCommand = new RoutedUICommand();

        public static RoutedUICommand ResetCommand
        {
            get { return _resetCommand; }
        }
    }
}

以下説明していきます。

まずRouted(UI)Commandオブジェクトはユーザーが独自の実装を含めることはできません。

ユーザーが実装するのはCommandBindingクラスのイベントです。

そのためRouted(UI)CommandオブジェクトはCommandBindingオブジェクトをビジュアルツリーをさかのぼって探しに行きます。

<CommandBinding Command="Copy" Executed="OnCopy" CanExecute="OnIsExistText"/>

XAMLパーサーは文字列CopyをRoutedUICommand型の静的プロパティApplicationCommands.Copyに型変換します。

<Button Content="コピー" Command="Copy" Width="60" Margin="6"/>

ボタンがクリックされるとICommandのExecuteメソッドがApplicationCommands.Copyコマンドインスタンスに対して呼ばれます。

RoutedUICommandはデフォルトでコマンドソース自身(ここではButton)からスタートして親要素へ向かって対応するCommandBindingを探します。

もし見つからなかった場合は、そのコマンド(Button)は無効化されます。

見つかった場合はCommandBindingクラスのExecutedイベントハンドラ(OnCopyなど)が呼ばれます。

しかしこの時RoutedUICommandインスタンス(ApplicationCommands.Copyなど)に対してCommandBindingクラスのCanExecuteイベントハンドラ(OnIsExistTextなど)が呼ばれます。

もしCanExecuteイベントハンドラが存在しなくてExecutedイベントハンドラが存在していた場合、そのコマンドは常に有効化されています。今回のアプリケーションでは以下のとおり、リセットボタンがこれに相当します。

<CommandBinding Command="local:Commands.ResetCommand" Executed="OnReset"/>

またCanExecuteイベントハンドラでは、CanExecuteRoutedEventArgs.CanExecuteプロパティがtrueの場合はコマンドは有効化され、falseの場合は無効化されます。

例えば以下のとおりCopyコマンドのOnIsExistTextイベントハンドラでは、空白でない文字が1文字でも入力されていた場合はコピーボタンは押せる状態になりますが、そうでない場合は押せない状態になります。

private void OnIsExistText(object sender, CanExecuteRoutedEventArgs e)
{
    if(txtBox == null)
    {
        return;
    }
            
    // テキストボックスに空白以外の文字列が入力されていた場合は
    // コピーボタンを有効化する
    e.CanExecute = !string.IsNullOrWhiteSpace(txtBox.Text);
}

RoutedUICommandのキーボードショートカット

Commands.csファイルのリセットコマンドに対応するRoutedUICommandを次の形式で生成すると「Alt + R」を押すことでもリセットできるようになります。

static readonly RoutedUICommand _resetCommand =
        new RoutedUICommand("Reset Command", "Reset", typeof(Commands), 
            new InputGestureCollection(new[] { new KeyGesture(Key.R, ModifierKeys.Alt)}));

CommandParameterプロパティ

プログラムの結合度を疎にするために、基本的には1つのコマンドでは1つの処理を実装することが望ましいです。

しかし似たような処理が複数あって場合分けによって振り分けたいこともあります。

こういった時はCommandParameterプロパティに値を渡すことによって処理の振り分けが実装できます。

良い例ではありませんがMainWindow.xamlを以下のように書き換えてみます。

<Window x:Class="WpfCommandSample.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:WpfCommandSample"
        mc:Ignorable="d"
        Title="Command Sample" Height="160" Width="400">
    <Window.CommandBindings>
        <CommandBinding Command="Copy" Executed="OnCopy" CanExecute="OnIsExistText"/>
        <CommandBinding Command="Paste" Executed="OnPaste" CanExecute="OnIsPasteEnabled"/>
        <CommandBinding Command="local:Commands.ResetCommand" Executed="OnReset"/>
    </Window.CommandBindings>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <StackPanel Orientation="Horizontal" Margin="6">
            <Button Content="コピー" Command="Copy" Width="60" Margin="6"/>
            <TextBox x:Name="txtBox" Width="240" Height="30" VerticalContentAlignment="Center"/>
        </StackPanel>
        
        <StackPanel Grid.Row="1" Orientation="Horizontal" Margin="6">
            <Button Content="貼り付け" Command="Paste" Width="60" Margin="6"/>
            <TextBlock x:Name="txtBlock" Width="240" Height="30" />
        </StackPanel>
   
        <StackPanel Grid.Row="2" Orientation="Horizontal" Margin="6">
            <Button x:Name="btnReset" Content="リセット" Command="local:Commands.ResetCommand" Width="60" Margin="6"/>
            <Button Content="リセット無効" Command="local:Commands.ResetCommand" CommandParameter="noReset" Width="65" Margin="6"/>
        </StackPanel>
    </Grid>
</Window>
<Button Content="リセット無効" Command="local:Commands.ResetCommand" CommandParameter="noReset" Width="60" Margin="6"/>

ここではCommandParameterに文字列noResetを渡しています。

そしてOnResetイベントハンドラを以下のように実装し直します。

private void OnReset(object sender, ExecutedRoutedEventArgs e)
{
    string param = e.Parameter as string;

    if (param != null && param == "noReset")
    {
        // リセットボタンを隠す
        btnReset.Visibility = Visibility.Collapsed;
    }
    else
    {
        // リセット処理をする
        Clipboard.Clear();
        txtBox.Text = string.Empty;
        txtBlock.Text = string.Empty;
    }
}

こうするとリセット無効ボタンを押すとリセットボタンが非表示になります。

今回はMVVMパターンは考えずに簡単なコマンドの使い方について確認しました。

MVVMパターンとICommandインターフェースの実装については以下の記事をご確認ください。

>> WPF MVVMパターンとコマンドについて

以上です。

コメント