プログラミング Windows 第6版 第6章 WPF編


この記事では、「プログラミング Windows 第6版」を使って WPF XAML の学習を支援することを目的にしています。この目的から、書籍と併せて読まれることをお勧めします。

第6章 WinRT と MVVM

この章では、MVVM(Model View ViewModel) パターンを説明する意欲的な内容になっています。書籍では説明がされていませんが、MVVM というデザイン パターンは、Composite Application Guidance(別名として、PRISM) というドキュメント(日本語英語WinRT)で解説されたデザイン パターンです。WinRT XAML や WPF XAML において、MVVM パターンの使用が必須ではありませんが、データ バインディングを有効活用するという観点ではビューモデル(ViewModel)を使用した方が望ましいので、PRISM ではないにしても MVVM パターンを暗黙的に利用するようになることでしょう。PRISM では、MVVM パターンだけでなく依存性の注入(Dependency Injection)だったり、ナビゲーション フレームワークなどの多くの技術要素を盛り込んだガイダンスとなっています。ここでは、簡単になぜビューモデルという概念が導入されたのかという点を説明します。

マイクロソフトの開発ツールは、古く(Visual Basic 6.0 以前)からデータ バインディングという技術をサポートしてきました。.NET Framework が提供されても、データ バインディングのサポートは続いており、元になるデータに対するアクセス技術はデータベース、Web サービス、REST サービスや Entity Framework など多岐に渡りながら機能強化や進化を続けています。一般的にデータ バインディングは、データを表現するオブジェクトと UI を表示するコントロールとをコーディング レスで繋げるものです。コーディング レスでデータと UI コントロールが繋がるということは、プログラムの中からコントロールとデータを関連付ける大量のコードが削減できることに他なりません。コード削減に繋がる技術がデータ バインディングなのですが、利用は非常に限定的だったと私は考えています。その理由は、次のようなものがあります。

  • データベースなどのデータ ストア上のデータ表現と UI コントロールの表現が、直接的にならない(値の変換などの操作が必要になる)。
  • データの表現には、データを参照するのは良くても、データの変更を許可しないなどのビジネス ルール表現が含まれており、UI コントロールと対応しない(対応しないというよりも、ビジネス ルールの表現方法が異なる)。
  • データ バインディングしたコントロールでは、要求される機能(参照のウィンドウ表示など)の実現が難しい。
  • などなど

つまり、データ表現とコントロールとしての UI 表現、そしてビジネス ルールや要求される機能などによって、木目細かな制御が可能なコードを使って実装する方が、データ バインディングを使うよりも実装がしやすいという判断に基づいた結果として、データ バインディングの利用が限定的になっていたのはないでしょうか。
ch06 view model

この問題を解決するために、ビューモデルを導入したらどのようになるでしょうか。
ch06 view viewmodel

ビューモデルに、実装する主要な機能を次のようにします。

  • データ バインディングを前提にする(UI コントロールと 1:1 に対応)。
  • データ モデルとビューの差異を吸収する(モデルが持つビジネス ルールに対する表現も吸収)。

ビューモデルは、UI コントロールとのバインディングを前提に考えますから、データ表現と UI コントロール上の表現が異なる場合は一致させるためにコンバーター(第4章第5章で説明しています)の利用を前提とします。つまり、バインディングを利用する上で問題となっていたモデルとビューのギャップを解消することを主眼としたモデルをビューモデルと呼んでいます。こうしたビューモデルを導入することで、バインディングを利用していなかった場合に使用していたモデルとビュー間の接着剤的(グル)コードを削減することになります。また、書籍にも記述されていますが、ユーザー コントロールやカスタム コントロールなどにバインディング可能なプロパティを実装する場合は、依存関係プロパティとして実装する必要があります。つまり、依存関係プロパティは、 XAML 系の UI 技術において、UI 定義のシリアライズ/デシリアライズだけでなくデータ バインディングを活用する上でも重要なプロパティ システムになっています。是非、依存関係プロパティの実装をマスターするようにしてください。

6.1(P215) 速習:MVVM

本節では、モデル、ビュー、ビューモデルという階層の役割を説明していますので、熟読をお願いします。

6.2(P216) データ バインディングの通知

本節では、データ バインディング機能を支えるための仕組みを説明しています。データ バインディングは、1回限り(OneTime)、一方向(OneWay、モデルからビュー)、双方向(TwoWay)というバインディングの方法があります。一方向、双方向のバインディングを支えるのが、通知メカニズムです。この通知メカニズムを実装する契約として、INotificationPropertyChanged インターフェースがあります。これらの具体的な説明が記述されていまし、WPF XAML にも適用できますので、書籍を熟読してください。

6.3(P218) ColorScroll のビューモデル

本節では、第5章の ColorScroll サンプルを題材にバインディングが可能かを説明しています。バインディングを可能にするために導入したビューモデルである RgbViewModel.cs (ColorScrollWithViewModel プロジェクト)を示します。

using System.ComponentModel;
using System.Windows.Media;

namespace ColorScrollWithViewModel
{
    public class RgbViewModel : INotifyPropertyChanged
    {
        double red, green, blue;
        Color color = Color.FromArgb(255, 0, 0, 0);

        public event PropertyChangedEventHandler PropertyChanged;

        public double Red
        {
            set
            {
                if (red != value)
                {
                    red = value;
                    OnPropertyChanged("Red");
                    Calculate();
                }
            }
            get
            {
                return red;
            }
        }

        public double Green
        {
            set
            {
                if (green != value)
                {
                    green = value;
                    OnPropertyChanged("Green");
                    Calculate();
                }
            }
            get
            {
                return green;
            }
        }

        public double Blue
        {
            set
            {
                if (blue != value)
                {
                    blue = value;
                    OnPropertyChanged("Blue");
                    Calculate();
                }
            }
            get
            {
                return blue;
            }
        }

        public Color Color
        {
            protected set
            {
                if (color != value)
                {
                    color = value;
                    OnPropertyChanged("Color");
                }
            }
            get
            {
                return color;
            }
        }

        void Calculate()
        {
            this.Color = Color.FromArgb(255, (byte)this.Red, (byte)this.Green, (byte)this.Blue);
        }

        protected void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

コードは、WinRT と同じものになっています。この ViewModel は、Red、Green、Blue プロパティが変更されると PropertyChanged イベントが発生します。PropertyChanged イベントは、OnPropertyChanged メソッドが発生させていますが、実装コードを見れば PropertyChanged という PropertyChnagedEventHandler 型のフィールドを使用していることがわかります。このフィールドに対して、購読(Subscribe)するのがバインディング定義を記述した UI コントロールの役割でもあります。今度は、作成した RgbViewModel を使用するための MainWindow.xaml よりリソース定義を抜粋して示します。

<Window.Resources>
    <local:RgbViewModel x:Key="rgbViewModel" />
    ...
</Window.Resources>

今度は Slider コントロールの定義を MainWindow.xaml より抜粋して示します。

<Slider Grid.Column="0"
        Grid.Row="1"
        Value="{Binding Source={StaticResource rgbViewModel}, 
                        Path=Red, 
                        Mode=TwoWay}"
        Foreground="Red" />
<TextBlock Text="{Binding Source={StaticResource rgbViewModel}, 
                          Path=Red, 
                          Converter={StaticResource hexConverter}}"
           Grid.Column="0"
           Grid.Row="2"
           Foreground="Red" />

コード自体は、WinRT XAML と同じですが、Binding に 「Mode=TwoWay」と記述されていることに注目してください。バインディングで Mode が記述されていない場合は、「OneWay(一方向)」バインディングになりますので、ここでは双方向を示すためにバインディング モードを記述しています。

今度は、第5章で検討した Color プロパティに対するバインディングを記述した箇所を MainWindow.xaml より抜粋して示します。

<Rectangle Grid.Column="3"
           Grid.Row="0"
           Grid.RowSpan="3">
  <Rectangle.Fill>
    <SolidColorBrush Color="{Binding Source={StaticResource rgbViewModel}, 
                                     Path=Color}" />
  </Rectangle.Fill>
</Rectangle>

RgbViewModel を導入したことで、バインディングを記述できるようになりました(コードは、WinRT XAML と同じです)。バインディングで記述することができましたので、分離コードファイル(MainWindow.xaml.cs) に記述していた SolodColorBrush 操作は必要がなくなり、Rectangle の定義から x:Name 属性も削除しています。ColorScrollWithViewModel プロジェクトを WPF XAML へ移植する場合、既に説明していますが、MainWindow.xaml のリソース定義で一部のコードの書き換え化が必要になります。それは、ThumbToolTipValueConverter の定義です。ThumbToolTipValueConverter が、WPF XAML に含まれないことから削除するだけになります。もちろん、動きは第5章のものと同じとなります。

書籍では、RgbViewModel のプロパティ実装や様々な解説が記述されていますので、ビューモデル実装の知識を学習するために熟読をお願いします。

6.4(P224) 構文のショートカット

本節では、ビューモデルを INotifyPropertyChanged インターフェースを使って実装することが面倒ではないかという投げ掛けに始まって、Windows 8 用の Windows ストア アプリ プロジェクトに含まれる BindableBase 抽象クラスを使って、ビューモデルを実装することを説明しています。

BindaleBase 抽象クラスとは、Visual Studio 2012 の Windows ストア アプリ プロジェクト テンプレートに含まれているクラスになります。また、Visual Studio 2013 の Windows ストア アプリ(Windows 8.1 用)プロジェクト テンプレートには含まれていないクラスになります。が、このクラスは、WPF アプリでも使用ができます。私は、WPF アプリでビュー モデルを作成する時に使用していたりもします。皆さんも、ビュー モデルの作成が面倒だなと感じたのならば、自分でビュー モデルを作成しやすいような部品を用意することを考えてみては如何でしょうか。

それでは、BindableBase 抽象クラスの考え方を利用した ColorScrollWithDataContext プロジェクトの RgbViewModel.cs を示します。

using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Media;

namespace ColorScrollWithDataContext
{
    public class RgbViewModel : INotifyPropertyChanged
    {
        double red, green, blue;
        Color color = Color.FromArgb(255, 0, 0, 0);

        public event PropertyChangedEventHandler PropertyChanged;

        public double Red
        {
            set
            {
                if (SetProperty<double>(ref red, value, "Red"))
                    Calculate();
            }
            get
            {
                return red;
            }
        }

        public double Green
        {
            set
            {
                if (SetProperty<double>(ref green, value))
                    Calculate();
            }
            get
            {
                return green;
            }
        }

        public double Blue
        {
            set
            {
                if (SetProperty<double>(ref blue, value))
                    Calculate();
            }
            get
            {
                return blue;
            }
        }

        public Color Color
        {
            set
            {
                if (SetProperty<Color>(ref color, value))
                {
                    this.Red = value.R;
                    this.Green = value.G;
                    this.Blue = value.B;
                }
            }
            get
            {
                return color;
            }
        }

        void Calculate()
        {
            this.Color = Color.FromArgb(255, (byte)this.Red, (byte)this.Green, (byte)this.Blue);
        }

        protected bool SetProperty<T>(ref T storage, T value,
                                      [CallerMemberName] string propertyName = null)
        {
            if (object.Equals(storage, value))
                return false;

            storage = value;
            OnPropertyChanged(propertyName);
            return true;
        }

        protected void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

コード自体は、WinRT と同じになります。

6.5(P228) DataContext プロパティ

本節では、DataContext プロパティについて説明しています。DataContext プロパティは、バインディングを使用する場合のバインディング ソースを指定する方法の 1つです。それでは、ColorScrollWithDataContext プロジェクトの MainWindow.xaml.cs を抜粋して示します。

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        this.DataContext = new RgbViewModel();

        // Initialize to highlight color
        (this.DataContext as RgbViewModel).Color = SystemColors.HighlightColor;
    }
}

WPF XAML へ移植するために、Color プロパティのハイライト カラー設定だけを書き換えています。これは、既に説明していますが、WinRT XAML と WPF XAML でハイライト カラーの定義が異なっているためです。そして、DataContext プロパティを使用していることから、MainWindow.xaml を抜粋して示します。

<Window.Resources>
    ...
        <!--<Setter Property="ThumbToolTipValueConverter" Value="{StaticResource hexConverter}" />-->
    </Style>
</Window.Resources>

<Grid>
    ...
    <!-- Red -->
    <TextBlock Text="Red"
               Grid.Column="0"
               Grid.Row="0"
               Foreground="Red" />

    <Slider Grid.Column="0"
            Grid.Row="1"
            Value="{Binding Red, Mode=TwoWay}"
            Foreground="Red" />

    <TextBlock Text="{Binding Red, Converter={StaticResource hexConverter}}"
               Grid.Column="0"
               Grid.Row="2"
               Foreground="Red" />
    ...
    <!-- Result -->
    <Rectangle Grid.Column="3"
               Grid.Row="0"
               Grid.RowSpan="3">
        <Rectangle.Fill>
            <SolidColorBrush Color="{Binding Color}" />
        </Rectangle.Fill>
    </Rectangle>
</Grid>

WPF XAML に移植する上で変更したのは、リソース定義から ThumbToolTipValueConverter を削除しただけです。これは、WinRT だけがこのプロパティをサポートしているからです。後は、WinRT XAML と一緒になっており、DataContext プロパティを使用したことからバインディングの記述が簡単になっています。Path 記述がなくなっているのは、Path がデフォルトを意味するからです。書籍では、バインディングの記述方法の詳細な説明や、DataContext プロパティをコードではなくリソース定義と組み合わせて使用する方法などを説明していますし、WPF XAML に適用できますので、熟読をお願いします。

6.6(P230) データバインディングと TextBox

本節では、ユーザーが入力に使用する TextBox を使用した場合のバインディングについて説明しています。この目的のために、ColorTextBoxes プロジェクトを使用しています。それでは、ColorTextBoxes プロジェクトの MainWindow.xaml.cs の抜粋を示します。

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        this.DataContext = new RgbViewModel();

        // Initialize to highlight color
        (this.DataContext as RgbViewModel).Color = SystemColors.HighlightColor;

    }
}

このコードは、6.5 で使用した ColorScrollWithDataContext プロジェクトと同じですから、WPF へ移植するためにハイライト カラーを書き換えています。そして、XAML では TextBox へ RgbViewModel に対するバインディングを記述した MainWindow.xaml の抜粋を示します。

<Window 
        ... >
    <Window.Resources>
        <Style TargetType="TextBlock">
            <Setter Property="FontSize" Value="24" />
            <Setter Property="Margin" Value="24 0 0 0" />
            <Setter Property="VerticalAlignment" Value="Center" />
        </Style>
        <Style TargetType="TextBox">
            <Setter Property="Margin" Value="24 48 96 48" />
            <Setter Property="VerticalAlignment" Value="Center" />
        </Style>
    </Window.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Grid Grid.Column="0">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>

            <TextBlock Text="Red: "
                       Grid.Row="0"
                       Grid.Column="0" />
            <TextBox Text="{Binding Red, Mode=TwoWay}"
                     Grid.Row="0"
                     Grid.Column="1" />

            <TextBlock Text="Green: "
                       Grid.Row="1"
                       Grid.Column="0" />
            <TextBox Text="{Binding Green, Mode=TwoWay}"
                     Grid.Row="1"
                     Grid.Column="1" />

            <TextBlock Text="Blue: "
                       Grid.Row="2"
                       Grid.Column="0" />
            <TextBox Text="{Binding Blue, Mode=TwoWay}"
                     Grid.Row="2"
                     Grid.Column="1" />
        </Grid>
        <!-- Result -->
        <Rectangle Grid.Column="1">
            <Rectangle.Fill>
                <SolidColorBrush Color="{Binding Color}" />
            </Rectangle.Fill>
        </Rectangle>
    </Grid>
</Window>

既に説明した内容に基づいて、必要な変更をしただけで WPF XAML 用にしています。それでは、実行結果を示します。
ColorTextBoxes

このサンプルでは、TextBox に対する双方向バインディングにおける TextBox から ビューモデルへデータが反映されるタイミングを確認しています。具体的には、TextBox を値(0から255)を入力してフォーカスを移動したタイミングでビューモデルへ反映されるというものです。

データが更新されるタイミングを改善するために、ColorTextBoxesWithEvents プロジェクトの MainWindow.xaml の抜粋を示します。

<TextBlock Text="Red: "
           Grid.Row="0"
           Grid.Column="0" />
<TextBox x:Name="redTextBox" 
         Grid.Row="0"
         Grid.Column="1"
         Text="0"
         TextChanged="OnTextBoxTextChanged" />

<TextBlock Text="Green: "
           Grid.Row="1"
           Grid.Column="0" />
<TextBox x:Name="greenTextBox"
         Grid.Row="1"
         Grid.Column="1"
         Text="0"
         TextChanged="OnTextBoxTextChanged" />

<TextBlock Text="Blue: "
           Grid.Row="2"
           Grid.Column="0" />
<TextBox x:Name="blueTextBox"
         Grid.Row="2"
         Grid.Column="1"
         Text="0"
         TextChanged="OnTextBoxTextChanged" />

このコードは、WinRT XAML と同じものになり、TextBox の TextChanged イベント ハンドラーを指定しています。そして、入力値の検証も追加した MainWindow.xaml.cs を抜粋して示します。

public partial class MainWindow : Window
{
    RgbViewModel rgbViewModel;
    Brush textBoxTextBrush; 
    Brush textBoxErrorBrush = new SolidColorBrush(Colors.Red);

    public MainWindow()
    {
        InitializeComponent();

        // Get TextBox brush
        //textBoxTextBrush = this.Resources["TextBoxForegroundThemeBrush"] as SolidColorBrush;
        textBoxTextBrush = new SolidColorBrush(Colors.Brown); // 初期値を変更 

        // Create RgbViewModel and save as field
        rgbViewModel = new RgbViewModel();
        rgbViewModel.PropertyChanged += OnRgbViewModelPropertyChanged;
        this.DataContext = rgbViewModel;

        // Initialize to highlight color
        rgbViewModel.Color = SystemColors.HighlightColor;
    }

    void OnRgbViewModelPropertyChanged(object sender, PropertyChangedEventArgs args)
    {
        switch (args.PropertyName)
        {
            case "Red":
                redTextBox.Text = rgbViewModel.Red.ToString("F0");
                break;

            case "Green":
                greenTextBox.Text = rgbViewModel.Green.ToString("F0");
                break;

            case "Blue":
                blueTextBox.Text = rgbViewModel.Blue.ToString("F0");
                break;
        }
    }

    private void OnTextBoxTextChanged(object sender, TextChangedEventArgs e)
    {
        if (rgbViewModel == null) return;   // 追加
        byte value;

        if (sender == redTextBox && Validate(redTextBox, out value))
            rgbViewModel.Red = value;

        if (sender == greenTextBox && Validate(greenTextBox, out value))
            rgbViewModel.Green = value;

        if (sender == blueTextBox && Validate(blueTextBox, out value))
            rgbViewModel.Blue = value;

    }

    bool Validate(TextBox txtbox, out byte value)
    {
        bool valid = byte.TryParse(txtbox.Text, out value);
        txtbox.Foreground = valid ? textBoxTextBrush : textBoxErrorBrush;
        return valid;
    }

}

WPF XAML へ移植する上で変更したのは、3か所になります。

  • textBoxTextBrush の設定で、組み込みスタイルを変更しました。
  • ハイライト カラーを WPF XAML 用に変更しました。
  • OnTextBoxTextChanged イベント ハンドラーで、RgbViewModel が null の条件を追加しました。

WPF XAML 版で注意して欲しいのが、OnTextBoxTextChanged イベント ハンドラーです。このような変更イベントは、コントロールのインスタンスが作成された直後(InitializeComponent メソッド) にも発生します。この理由で、RgbViewModel が null になるケースが発生します。一方で、WinRT XAML ではコントロールのインスタンスが作成された直後に変更イベントが発生していません。これは、WPF XAML と WinRT XAML の違いにもなりますが、変更イベントでは操作するオブジェクトが null かどうかをチェックされることをお勧めします。このようにすれば、同じコードを WinRT XAML と WPF XAML で使用することができるからです。

書籍では、このように ビュー側のコードから ビューモデルを操作した場合に起きる問題などを詳細に説明していますし、WPF XAML にも適用できる内容ですので、熟読をお願いします。

6.7(P236) ボタンと MVVM

本節では、コマンド パターンの説明をしています。コマンド パターンとは、ICommand インターフェースを継承したオブジェクトを使ってボタン コントロールのクリック イベントによって実行されるコマンドを実現するパターンになります。このコマンド パターンを使用する上で必要となる、考え方や知識を説明していますので、書籍を熟読してください。コマンド パターンは、WinRT XAML と WPF XAML で同じように利用できるもので、コマンド実行の概要で説明が記述されています。

6.8(P238) DelegateCommand クラス

DelegateCommand クラスとは、KeypadWithViewModel プロジェクトで使用するために用意したクラスになります。日本語では委譲コマンドであり、ICommand インターフェースと KeypadWithViewModel プロジェクトで使用するコマンドで必要となるインターフェースを実装したクラスになります。このDelegateCommand を実装するためのインターフェースとして、KeypadWithViewModel プロジェクトの IDelegateCommand cs を示します。

using System.Windows.Input;

namespace KeypadWithViewModel
{
    public interface IDelegateCommand : ICommand
    {
        void RaiseCanExecuteChanged();
    }
}

IDelegateCommand インターフェースは、DelegateCommand を実装するために定義したインターフェースになります。それでは、DelegateCommand.cs を示します。

using System;

namespace KeypadWithViewModel
{
    public class DelegateCommand : IDelegateCommand
    {
        Action<object> execute;
        Func<object, bool> canExecute;

        // Event required by ICommand
        public event EventHandler CanExecuteChanged;

        // Two constructors
        public DelegateCommand(Action<object> execute, Func<object, bool> canExecute)
        {
            this.execute = execute;
            this.canExecute = canExecute;
        }
        public DelegateCommand(Action<object> execute)
        {
            this.execute = execute;
            this.canExecute = this.AlwaysCanExecute;
        }

        // Methods required by ICommand
        public void Execute(object param)
        {
            execute(param);
        }
        public bool CanExecute(object param)
        {
            return canExecute(param);
        }

        // Method required by IDelegateCommand
        public void RaiseCanExecuteChanged()
        {
            if (CanExecuteChanged != null)
                CanExecuteChanged(this, EventArgs.Empty);
        }

        // Default CanExecute method
        bool AlwaysCanExecute(object param)
        {
            return true;
        }
    }
}

 

DelegateCommand クラスは、コンストラクターで execute という Action<object> デリゲートと canExecute という Func<object, bool> デリゲートを受け取ります。execute が、コマンド実行に使用するデリゲートであり、canExecute がキャンセル時に実行するデリゲートになります。定義した DelegateCommand クラスを使って、ビューモデルでコマンドを定義しますので、KyepadViewModel.cs を示します。

using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace KeypadWithViewModel
{
    public class KeypadViewModel : INotifyPropertyChanged
    {
        string inputString = "";
        string displayText = "";
        char[] specialChars = { '*', '#' };

        public event PropertyChangedEventHandler PropertyChanged;

        // Constructor
        public KeypadViewModel()
        {
            this.AddCharacterCommand = new DelegateCommand(ExecuteAddCharacter);
            this.DeleteCharacterCommand =
                new DelegateCommand(ExecuteDeleteCharacter, CanExecuteDeleteCharacter);
        }

        // Public properties
        public string InputString
        {
            protected set
            {
                bool previousCanExecuteDeleteChar = this.CanExecuteDeleteCharacter(null);

                if (this.SetProperty<string>(ref inputString, value))
                {
                    this.DisplayText = FormatText(inputString);

                    if (previousCanExecuteDeleteChar != this.CanExecuteDeleteCharacter(null))
                        this.DeleteCharacterCommand.RaiseCanExecuteChanged();
                }
            }

            get { return inputString; }
        }

        public string DisplayText
        {
            protected set { this.SetProperty<string>(ref displayText, value); }
            get { return displayText; }
        }

        // ICommand implementations
        public IDelegateCommand AddCharacterCommand { protected set; get; }

        public IDelegateCommand DeleteCharacterCommand { protected set; get; }

        // Execute and CanExecute methods
        void ExecuteAddCharacter(object param)
        {
            this.InputString += param as string;
        }

        void ExecuteDeleteCharacter(object param)
        {
            this.InputString = this.InputString.Substring(0, this.InputString.Length - 1);
        }

        bool CanExecuteDeleteCharacter(object param)
        {
            return this.InputString.Length > 0;
        }

        // Private method called from InputString
        string FormatText(string str)
        {
            bool hasNonNumbers = str.IndexOfAny(specialChars) != -1;
            string formatted = str;

            if (hasNonNumbers || str.Length < 4 || str.Length > 10)
            {
            }
            else if (str.Length < 8)
            {
                formatted = String.Format("{0}-{1}", str.Substring(0, 3),
                                                     str.Substring(3));
            }
            else
            {
                formatted = String.Format("({0}) {1}-{2}", str.Substring(0, 3),
                                                           str.Substring(3, 3),
                                                           str.Substring(6));
            }
            return formatted;
        }

        protected bool SetProperty<T>(ref T storage, T value,
                                      [CallerMemberName] string propertyName = null)
        {
            if (object.Equals(storage, value))
                return false;

            storage = value;
            OnPropertyChanged(propertyName);
            return true;
        }

        protected void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

KeyPadViewModel が、AddCharacterCommand と DeleteCharacterCommand プロパティを実装している点に注意してください。それぞれのコマンドが、入力した文字列を追加するコマンドであり、文字列を削除するコマンドになっています。
それでは、コマンドを定義した KeypadWithViewModel プロジェクトの MainWindow.xaml の抜粋を示します。

<Window ...
        xmlns:local="clr-namespace:KeypadWithViewModel"
        ... >
    <Window.Resources>
        <local:KeypadViewModel x:Key="viewModel" />
    </Window.Resources>
    <Grid DataContext="{StaticResource viewModel}">
        <Grid HorizontalAlignment="Center"
              VerticalAlignment="Center"
              Width="288">
            <Grid.Resources>
                <Style TargetType="Button">
                    <Setter Property="ClickMode" Value="Press" />
                    <Setter Property="HorizontalAlignment" Value="Stretch" />
                    <Setter Property="Height" Value="72" />
                    <Setter Property="FontSize" Value="36" />
                </Style>
            </Grid.Resources>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
            <Grid Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*" />
                    <ColumnDefinition Width="Auto" />
                </Grid.ColumnDefinitions>
                <Border Grid.Column="0"
                        HorizontalAlignment="Left">

                    <TextBlock Text="{Binding DisplayText}"
                               HorizontalAlignment="Right"
                               VerticalAlignment="Center"
                               FontSize="24" />
                </Border>
                <Button Content="&#x21E6"
                        Command="{Binding DeleteCharacterCommand}"
                        Grid.Column="1"
                        FontFamily="Segoe Symbol"
                        HorizontalAlignment="Left"
                        Padding="0"
                        BorderThickness="0" />
            </Grid>

            <Button Content="1"
                    Command="{Binding AddCharacterCommand}"
                    CommandParameter="1"
                    Grid.Row="1" Grid.Column="0" />
            ...
            <Button Content="#"
                    Command="{Binding AddCharacterCommand}"
                    CommandParameter="#"
                    Grid.Row="4" Grid.Column="2" />
        </Grid>
    </Grid>
</Window>

Button の Command 属性に、バインディングによって DeleteCharacterCommand と AddCharacterCommand を指定しています。AddCharacterCommand に対しては、CommandParameter によってコマンドに渡すパラメーターを指定しています。このバインディングによって、MainWindow.xaml.cs の分離コードに対すて記述する必要性がなくなります。それでは、実行結果を示します。
KeypadWithViewModel

 

このように、ビュー モデル と コマンド パターンを組み合わせることで、必要なコードを分離コードなどから削減する可能性が生まれます。これらの記述が何を意味しているかなどは、書籍で解説していますので、書籍を熟読してください。

ViewModel について

ビュー モデルは、ビューとモデルのセマンティック ギャップを埋めるために導入されたビュー専用のモデルです。つまり、ビューと 1:1 に対応するモデルであり、その実装は退屈なものになりがちです。なぜなら、INotifyPropertyChanged インターフェースを実装し、ビューと 1:1 になるようにプロパティを記述し、ビュー表現とプロパティ表現が異なればコンバーターを実装するというように、極論するればビューが100種類あれば、ビュー モデルも 100種類あるという状況になりかねません。しかしながら、ビュー モデルを作成することでデータ バインディングを活用できるようになり、ビューとモデルを対応付けるグル(接着剤)コードが無くなり、実装すべき本来の機能へ集中できるようになります。その一方で、ユーザーのキー入力をチェックするようなビュー モデルは、現実的ではありません。なぜなら、可能限り、ビュー モデル から ビューを操作することを避けるべきだからです。色々な考え方があると思いますが、キー入力を何らかの形で処理する必要があるのであれば、ユーザー コントロールやカスタム コントロールなどを使うのが望ましいのではないでしょうか。

ここまで説明してた内容を意識しながら、第6章を読むことで WPF にも書籍の内容を応用することができるようになることでしょう。

ch06.zip

Comments (0)

Skip to main content