WPF用 ViewModelの基底クラスサンプル

WPFでアプリケーション開発する場合、MVVMパターンを利用しての開発が推奨されています。アプリケーションを Model, View, ViewModel の3つの層に分割して開発する手法です。

WPFではMVVMパターンを用いて開発するためにデータバインドとかいろいろ機能が用意されているのですが、それらを利用するためには幾つかのお約束があって、ViewModelでは`INotifyPropertyChanged`と`IDataErrorInfo`という2つのインターフェイスを実装することが求めらます（別に必須ではないですが）。また、ViewからViewModelへの通知を受け取る場合、`ICommand`インターフェイスを介して行われるため、この実装も必要となります。

これらの実装はほぼ定形ですので、ViewModelの基底クラスとして作成しておくのが普通です。最低限の機能を実装した基底クラスを以下に示します。

## ViewModelの基底クラスのサンプル

`INotifyPropertyChanged`と`IDataErrorInfo`の実装、及び、`ICommand`を実装するためのヘルパークラスを実装しています。

```csharp:ViewModelBase.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;

namespace WpfTest
{
    #region ** Class : ViewModelBase
    /// <summary>
    /// ViewModelの基底クラス
    /// INotifyPropertyChanged と IDataErrorInfo を実装する
    /// </summary>
    public abstract class ViewModelBase : INotifyPropertyChanged, IDataErrorInfo
    {
        #region == implement of INotifyPropertyChanged ==

        // INotifyPropertyChanged.PropertyChanged の実装
        public event PropertyChangedEventHandler PropertyChanged;

        // INotifyPropertyChanged.PropertyChangedイベントを発生させる。
        protected virtual void RaisePropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        #endregion

        #region == implemnt of IDataErrorInfo ==

        // IDataErrorInfo用のエラーメッセージを保持する辞書
        private Dictionary<string, string> _ErrorMessages = new Dictionary<string, string>();

        // IDataErrorInfo.Error の実装
        string IDataErrorInfo.Error
        {
            get { return (_ErrorMessages.Count > 0) ? "Has Error" : null; }
        }

        // IDataErrorInfo.Item の実装
        string IDataErrorInfo.this[string columnName]
        {
            get
            {
                if (_ErrorMessages.ContainsKey(columnName))
                    return _ErrorMessages[columnName];
                else
                    return null;
            }
        }

        // エラーメッセージのセット
        protected void SetError(string propertyName, string errorMessage)
        {
            _ErrorMessages[propertyName] = errorMessage;
        }

        // エラーメッセージのクリア
        protected void ClearErrror(string propertyName)
        {
            if (_ErrorMessages.ContainsKey(propertyName))
                _ErrorMessages.Remove(propertyName);
        }

        #endregion

        #region == implemnt of ICommand Helper ==

        #region ** Class : _DelegateCommand
        // ICommand実装用のヘルパークラス
        private class _DelegateCommand : ICommand
        {
            private Action<object> _Command;        // コマンド本体
            private Func<object, bool> _CanExecute;  // 実行可否

            // コンストラクタ
            public _DelegateCommand(Action<object> command, Func<object, bool> canExecute = null)
            {
                if (command == null)
                    throw new ArgumentNullException();

                _Command = command;
                _CanExecute = canExecute;
            }

            // ICommand.Executeの実装
            void ICommand.Execute(object parameter)
            {
                _Command(parameter);
            }

            // ICommand.Executeの実装
            bool ICommand.CanExecute(object parameter)
            {
                if (_CanExecute != null)
                    return _CanExecute(parameter);
                else
                    return true;
            }

            // ICommand.CanExecuteChanged の実装
            event EventHandler ICommand.CanExecuteChanged
            {
                add { CommandManager.RequerySuggested += value; }
                remove { CommandManager.RequerySuggested -= value; }
            }
        }
        #endregion

        // コマンドの生成
        protected ICommand CreateCommand(Action<object> command, Func<object, bool> canExecute = null)
        {
            return new _DelegateCommand(command, canExecute);
        }

        #endregion
    }
    #endregion
}
```

## 上記コードを利用したサンプル

```csharp:ViewModel.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;

namespace WpfTest
{
    #region ** Class : ViewModel
    /// <summary>
    /// ViewModel
    /// </summary>
    class ViewModel : ViewModelBase
    {
        // プロパティの値を保持するメンバ変数
        private int _property1 = 0;

        // プロパティ
        public int Property1
        {
            get { return _property1; }
            set
            {
                // 値が変更されたら、INotifyPropertyChanged.PropertyChanged を発生させる。
                if (_property1 != value)
                {
                    _property1 = value;

                    // 値は0以上、10以下。それ以外はエラー
                    if (_property1 >= 0 && _property1 <= 10)
                        ClearErrror("Property1");
                    else
                        SetError("Property1", "Error: argument out of range");

                    RaisePropertyChanged("Property1");

                }
            }
        }

        public ICommand Add1Command { get; private set; }

        // コンストラクタ
        public ViewModel()
        {
            Property1 = 0;
            Add1Command = CreateCommand(v => { Property1 = Property1 + 1; });
        }
    }
    #endregion
}
```
```xml:MainWindow.xaml
<Window x:Class="WpfTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfTest"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <local:ViewModel x:Key="ViewModel"/>
    </Window.Resources>
    <Grid DataContext="{StaticResource ViewModel}">
        <Grid.RowDefinitions>
            <RowDefinition Height="1*" />
            <RowDefinition Height="1*" />
        </Grid.RowDefinitions>
        <StackPanel Grid.Row="0" Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
            <TextBlock Text="数値を入力してください:" FontSize="14" Margin="10" />
            <TextBox Text="{Binding Property1, ValidatesOnDataErrors=True, Mode=TwoWay}" FontSize="14" Width="50" Margin="10" HorizontalContentAlignment="Right" />
        </StackPanel>
        <Button Grid.Row="1" Content="1を加算" FontSize="14" Padding="20,10,20,10" VerticalAlignment="Center" HorizontalAlignment="Center" Command="{Binding Add1Command}" />
    </Grid>
</Window>
```
```csharp:MainWindow.xaml.cs
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 WpfTest
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }
}
```