WPFプログラミング備忘録

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

WPF

はじめに

この記事では以下の記事でコマンドの確認をしたアプリケーションをMVVMパターンライクなものに置き換えてみます。

>> MVVMパターンを使わずにコマンドを理解する

MVVMパターンライクと書いたのはModelが必要なかったためです。View、ViewModelおよびCommandを実装します。

今回はRoutedUICommandクラスとCommandBindingクラスは使わず、ICommandインターフェースを実装することにします。

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

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

アプリケーションの仕様

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

起動時の画面

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

テキストボックスに入力

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

コピーボタンを押す

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

貼り付けボタンを押す

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

リセットボタンを押す

プロジェクトのフォルダ構成とファイル

WPFアプリケーションのプロジェクト作成したら、プロジェクト内にViewsフォルダ、ViewModelsフォルダ、およびCommandsフォルダを作成します。

フォルダの作成手順 「プロジェクトを右クリック -> 追加 -> 新しいフォルダー」

自動的に生成されるMainWindow.xamlファイルは削除します。

そして下図のように各フォルダに必要なファイルを作成します。

①Commandsフォルダ

  • CopyCommand.cs
  • PasteCommans.cs
  • ResetCommand.cs

作成手順 「Commandsフォルダを右クリック -> 追加 -> クラス」

②ViewModelsフォルダ

  • MainWindowViewModel.cs

作成手順 「ViewModelsフォルダを右クリック -> 追加 -> クラス」

③Viewsフォルダ

  • MainWindow.xaml

作成手順 「Viewsフォルダを右クリック -> 追加 -> ウィンドウ(WPF)」

Viewを起動するためにApp.xamlファイルのStartupUri属性の値を以下のとおり変更してください。

<Application x:Class="WpfMVVMSample.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:WpfMVVMSample"
             StartupUri="Views\MainWindow.xaml">
    <Application.Resources>
         
    </Application.Resources>
</Application>

コマンドの定義

3つのコマンドの定義は以下のとおりです。ICommandインターフェースのメンバーを実装しています。

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

ResetCommand.csは以下のとおりです。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using WpfMVVMSample.ViewModels;

namespace WpfMVVMSample.Commands
{
    class ResetCommand : ICommand
    {
        MainWindowViewModel _vm;
        public ResetCommand(MainWindowViewModel vm)
        {
            _vm = vm;
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public bool CanExecute(object parameter)
        {
            // 常に有効化
            return true;
        }

        public void Execute(object parameter)
        {
            // リセット処理をする
            Clipboard.Clear();
            _vm.CopyText = string.Empty;
            _vm.PasteText = string.Empty;
        }
    }
}

コンストラクタでViewModelのインスタンスを取得しています。

リセットボタンは常に有効化しておくためCanExecuteメソッドはtrueを返すようにしています。

CanExecuteChangedイベントの定義は定石のようです。

CopyCommand.csとPasteCommand.csは以下のとおりです。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using System.Windows;
using WpfMVVMSample.ViewModels;

namespace WpfMVVMSample.Commands
{
    class CopyCommand : ICommand
    {
        MainWindowViewModel _vm;
        public CopyCommand(MainWindowViewModel vm)
        {
            _vm = vm;
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public bool CanExecute(object parameter)
        {
            if (_vm.CopyText == null)
            {
                return false;
            }

            // テキストボックスに空白以外の文字列が入力されていた場合はコピーボタンを有効化する
            return !string.IsNullOrWhiteSpace(_vm.CopyText);
        }

        public void Execute(object parameter)
        {
            string text = _vm.CopyText;

            if (!string.IsNullOrWhiteSpace(text))
            {
                // クリップボードにテキストボックスの文字列を保存する
                Clipboard.SetData(DataFormats.Text, text);
            }
        }
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using System.Windows;
using WpfMVVMSample.ViewModels;

namespace WpfMVVMSample.Commands
{
    class PasteCommand : ICommand
    {
        MainWindowViewModel _vm;
        public PasteCommand(MainWindowViewModel vm)
        {
            _vm = vm;
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public bool CanExecute(object parameter)
        {
            string text = Clipboard.GetText();

            // クリップボードにテキストが保存されていた場合は貼り付けボタンを有効化する
            return !string.IsNullOrWhiteSpace(text);
        }

        public void Execute(object parameter)
        {
            string text = Clipboard.GetText();
            _vm.PasteText = text;

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

CopyCommand.csとPasteCommand.csは特に問題は無いでしょう。

ここでもCanExecuteChangedイベントの定義は定石のようです。

ViewModelの定義

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

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using WpfMVVMSample.Commands;

namespace WpfMVVMSample.ViewModels
{
    class MainWindowViewModel : INotifyPropertyChanged
    {
        ICommand _resetCommand, _copyCommand, _pasteCommand;

        public MainWindowViewModel()
        {
            _resetCommand = new ResetCommand(this);
            _copyCommand = new CopyCommand(this);
            _pasteCommand = new PasteCommand(this);
        }

        public event PropertyChangedEventHandler PropertyChanged;

        private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        string _copyText;

        string _pasteText;

        public string CopyText
        {
            get { return _copyText; }
            set
            {
                if(value != _copyText)
                {
                    _copyText = value;
                    NotifyPropertyChanged();
                }
            }
        }
        public string PasteText
        {
            get { return _pasteText; }
            set
            {
                if (value != _pasteText)
                {
                    _pasteText = value;
                    NotifyPropertyChanged();
                }
            }
        }
        public ICommand ResetCommand
        {
            get { return _resetCommand; }
        }

        public ICommand CopyCommand
        {
            get { return _copyCommand; }
        }

        public ICommand PasteCommand
        {
            get { return _pasteCommand; }
        }
    }
}

コンストラクタでは3つのコマンドのインスタンスを生成しています。

またViewの中で各コマンドをバインドするためICommand型のプロパティとして公開しています。

Viewの定義

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

<Window x:Class="WpfMVVMSample.Views.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:WpfMVVMSample.Views"
        mc:Ignorable="d"
        Title="Basic MVVM Sample" Height="160" Width="400">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <StackPanel Orientation="Horizontal" Margin="6">
            <Button Command="{Binding CopyCommand}" Content="コピー" Width="60" Margin="6"/>
            <TextBox Text="{Binding CopyText, UpdateSourceTrigger=PropertyChanged}" Width="240" Height="30" VerticalContentAlignment="Center"/>
        </StackPanel>
        <StackPanel Grid.Row="1" Orientation="Horizontal" Margin="6">
            <Button Command="{Binding PasteCommand}" Content="貼り付け" Width="60" Margin="6"/>
            <TextBlock Text="{Binding PasteText}" Width="240" Height="30" />
        </StackPanel>
        <Button Command="{Binding ResetCommand}" Grid.Row="2" Content="リセット" 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.Shapes;

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

            Clipboard.Clear();

            DataContext = new WpfMVVMSample.ViewModels.MainWindowViewModel();
        }
    }
}

コンストラクタの中ではDataContextにViewModelのインスタンスを設定しています。

繰り返しになりますが、各コマンドをXAMLファイルの中でバインドできるようにするためです。

以上の実装で仕様どおりの動作をすることが確認できます。

MVVMパターンの各パーツの関係性は下図のとおりです。

Windows Presentation Foundation 4.5 Cookbook P251より

今回はMVVMパターンライクな実装を検証しました。

なお、本アプリケーションではコピー、貼り付け、およびリセットの各コマンド用のファイルを3つに分けましたが、コマンドパラメータを使用すれば1つのファイルで処理を振り分けることも可能です。そのような実装例については以下の記事をご確認ください。

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

以上です。

コメント