C#
WPF
MVVM

[C# / WPF] 最新のC# 6.0でMVVMパターンを実装する

More than 1 year has passed since last update.

こんにちは、Niaです。
C#の最新バージョン「6.0」になって、自動実装プロパティの強化やnull条件演算子など、便利な機能が追加されましたね。

今回はC# 6.0でWPFアプリでよく使うMVVM(Model-View-ViewModel)パターンを実装したプログラムを作成し、C# 5.0と比べてコードがどう変化するか見ていきましょう。

ここでは例として、RSSフィードを取得するプログラムを作成します。
cw-01.PNG

cw-02.PNG

1. C# 5.0でMVVMパターンを実装

まずはC# 5.0でRSSフィードを取得するプログラムを以下に示します。

1.1. Model(RSSModelクラス)

RSSModel.cs
// *** 中略 ***

// RSSリーダーのModelです。
class RSSModel : INotifyPropertyChanged {

    // HTMLタグを取り除くための文字列です。
    private const string patternStr = @"<.*?>";

    // コンストラクタ
    public RSSModel() {
        Items = new ObservableCollection<RSSContent>();
        Url = "";
        RSSProvider = new RSSProviderInfo();
        BindingOperations.EnableCollectionSynchronization( Items, new object() );
    }

    #region 2.4. 自動実装プロパティで初期値を設定する」の対象

    // RSSフィードのコンテンツを格納するコレクションです。
    public ObservableCollection<RSSContent> Items { get; private set; }

    // RSSフィードの配信元情報です。
    public RSSProviderInfo RSSProvider { get; private set; }

    // RSSフィードのURL( ViewModel、Viewから設定できるようにします。 )
    public string Url { get; set; } 

    #endregion *******************************

    // RSSフィードを取得します。( 非同期メソッド )
    public Task<TaskResult> GetRSS() {
        // *** 中略 ***
    }

    // RSSの取得完了後に発生させるイベントハンドラです。
    public event TaskResultEventHandler GetRSSCompleted;

    // プロパティ変更後に発生させるイベントハンドラです。
    public event PropertyChangedEventHandler PropertyChanged;

    #region 2.1. null条件演算子で変更通知処理をコンパクトに」の対象

    // プロパティ変更を通知します。
    private void NotifyPropertyChanged( [CallerMemberName]string propertyName = null ) {
        if( PropertyChanged != null ) {
            PropertyChanged( this, new PropertyChangedEventArgs( propertyName ) );
        }
    }

    #endregion *******************************

    #region 2.1. null条件演算子で変更通知処理をコンパクトに」の対象

    // RSSフィードを取得します。
    public async Task GetRSSAsync() {
        // RSSフィードを非同期で取得します。
        TaskResult result = await Task.Run( () => GetRSS() );
        // RSSフィードの取得が完了したことをViewModel側に通知します。
        if( GetRSSCompleted != null ) {
            GetRSSCompleted( this, new TaskResultEventArgs( result ) );
        }
    } 

    #endregion *******************************
}

// *** 中略 ***

1.2. ViewModel(RSSViewModelクラス)

RSSViewModel.cs
// *** 中略 ***

// RSSリーダーのViewModelです。
class RSSViewModel : INotifyPropertyChanged {

    // Model
    RSSModel rssModel = new RSSModel();

    #region 2.1. null条件演算子で変更通知処理をコンパクトに」の対象

    // コンストラクタ
    public RSSViewModel() {

        rssModel.PropertyChanged += ( sender, e ) => {
            if( PropertyChanged != null ) {
                PropertyChanged( this, new PropertyChangedEventArgs( e.PropertyName ) );
            }
        };
        // RSSフィードの取得が完了したことをView側に通知します。
        rssModel.GetRSSCompleted += ( sender, e ) => {
            // RSSフィード取得中のフラグをオフにします。
            IsProgress = false;
            if( GetRSSCompleted != null )
                // RSSフィード取得完了したことをView側に通知します。
                GetRSSCompleted( this, e );
        };
    }

    #endregion *******************************

    #region 2.3. プロパティ / メソッドの本体をラムダ式で簡潔に」の対象

    // RSSフィードのタイトル
    public string Title {
        get { return IsProgress ? "RSSフィールドを取得中 ..." : rssModel.RSSProvider.Title; }
    }

    // RSSフィードの説明
    public string Description {
        get { return IsProgress ? "Recieving ..." : rssModel.RSSProvider.Description; }
    }

    // RSSフィードの最終更新日
    public DateTime LastUpdatedTime {
        get { return IsProgress ? DateTime.MinValue : rssModel.RSSProvider.LastUpdatedTime; }
    }

    // RSSフィードのコンテンツ
    public ObservableCollection<RSSContent> Items {
        get { return rssModel.Items; }
    }

    #endregion *******************************

    // RSSフィードのURL
    public string Url {
        get { return rssModel.Url; }
        set { rssModel.Url = value; }
    }

    #region 2.2. nameof演算子でリファクタリングが捗る!?」の対象

    // RSSフィード取得中のフラグ
    private bool isProgress = false;
    public bool IsProgress {
        get { return isProgress; }
        set {
            isProgress = value;
            NotifyPropertyChanged();
            NotifyPropertyChanged( "Title" );
            NotifyPropertyChanged( "Description" );
            NotifyPropertyChanged( "LastUpdatedTime" );
        }
    }

    #endregion *******************************

    // RSSの取得完了後に発生させるイベントハンドラです。
    public event TaskResultEventHandler GetRSSCompleted;

    // プロパティ変更後に発生させるイベントハンドラです。
    public event PropertyChangedEventHandler PropertyChanged;

    #region 2.1. null条件演算子で変更通知処理をコンパクトに」の対象

    // プロパティ変更を通知します。
    private void NotifyPropertyChanged( [CallerMemberName]string propertyName = null ) {
        if( PropertyChanged != null ) {
            PropertyChanged( this, new PropertyChangedEventArgs( propertyName ) );
        }
    }

    #endregion *******************************

    #region 2.3. プロパティ / メソッドの本体をラムダ式で簡潔に」の対象

    // RSSフィード取得のコマンド
    private ICommand getRSS;
    public ICommand GetRSS {
        get { return getRSS ?? ( getRSS = new GetRSSCommand( this ) ); }
    }

    #endregion *******************************

    // RSSフィードを取得するコマンドです。
    private class GetRSSCommand : ICommand {

        // ViewModel
        private RSSViewModel rssViewModel;

        #region 2.1. null条件演算子で変更通知処理をコンパクトに」の対象

        // コンストラクタ
        public GetRSSCommand( RSSViewModel viewModel ) {
            rssViewModel = viewModel;

            // コマンド実行の可否の変更を通知します。
            rssViewModel.PropertyChanged += ( sender, e ) => {
                if( CanExecuteChanged != null )
                    CanExecuteChanged( sender, e );
            };
        }

        #endregion *******************************

        #region 2.3. プロパティ / メソッドの本体をラムダ式で簡潔に」の対象

        // コマンドを実行できるかどうかを取得します。
        public bool CanExecute( object parameter ) {
            return !rssViewModel.IsProgress;
        }

        #endregion *******************************

        // コマンド実行の可否の変更した時のイベントハンドラです。
        public event EventHandler CanExecuteChanged;

        // コマンドを実行し、RSSフィードを取得します。
        public void Execute( object parameter ) {
            // *** 中略 ***
        }
    }
}

// *** 中略 ***

Viewも含めた完全なプログラムはGistにアップロードしています。
https://gist.github.com/Nia-TN1012/a9762b547fcf644691fd

2. C# 6.0の新機能を使って、MVVMパターンの実装するためのコードを改良してみよう

2.1. null条件演算子で変更通知処理をコンパクトに

MVVMパターンの実装でよく使われる、「INotifyPropertyChanged」インターフェースを介した変更通知ですが、「PropertyChanged」イベントを発生させる前にnullでないかどうかを確認する必要があります。

C#5.0
// プロパティ変更を通知します。
private void NotifyPropertyChanged( [CallerMemberName]string propertyName = null ) {
    if( PropertyChanged != null ) {
        PropertyChanged( this, new PropertyChangedEventArgs( propertyName ) );
    }
}

そこでC# 6.0のnull条件演算子を使って、オブジェクトがnullでない時、メソッドやプロパティを呼び出すようにします。
但し、今回扱う「PropertyChanged」はデリゲート型のため、?.Invoke()の形にします。

C#6.0
// プロパティ変更を通知します。
private void NotifyPropertyChanged( [CallerMemberName]string propertyName = null ) {
    PropertyChanged?.Invoke( this, new PropertyChangedEventArgs( propertyName ) );
}

RSSフィードの取得後に発生させるGetRSSCompletedイベントなどにも、null条件演算子を使ってnullチェックをします。

  • RSSModelクラスのGetRSSAsyncメソッドにあるGetRSSCompletedイベント
C#5.0
// RSSフィードを取得します。
public async void GetRSSAsync() {
    TaskResult result = await Task.Run( () => GetRSS() );
    if( GetRSSCompleted != null ) {
        GetRSSCompleted( this, new TaskResultEventArgs( result ) );
    }
} 
C#6.0
// RSSフィードを取得します。
public async void GetRSSAsync() {
    TaskResult result =  await Task.Run( () => GetRSS() );
    GetRSSCompleted?.Invoke( this, new TaskResultEventArgs( result ) );
}
  • RSSViewModelクラスのコンストラクタにあるPropertyChangedイベントとGetRSSCompletedイベント
C#5.0
// コンストラクタ
public RSSViewModel() {
    rssModel.PropertyChanged += ( sender, e ) => {
        if( PropertyChanged != null ) {
            PropertyChanged( this, new PropertyChangedEventArgs( e.PropertyName ) );
        }
    };
    rssModel.GetRSSCompleted += ( sender, e ) => {
        IsProgress = false;
        if( GetRSSCompleted != null )
            GetRSSCompleted( this, e );
    };
}
C#6.0
// コンストラクター
public RSSViewModel() {
    rssModel.PropertyChanged += ( sender, e ) =>
        PropertyChanged?.Invoke( this, new PropertyChangedEventArgs( e.PropertyName ) );
    rssModel.GetRSSCompleted += ( sender, e ) => {
        IsProgress = false;
        GetRSSCompleted?.Invoke( this, e );
    };
}
  • GetRSSCommandクラス(RSSViewModelクラス内)のコンストラクタにあるCanExecuteChangedイベント
C#5.0
// コンストラクタ
public GetRSSCommand( RSSViewModel viewModel ) {
    rssViewModel = viewModel;
    rssViewModel.PropertyChanged += ( sender, e ) => {
        if( CanExecuteChanged != null )
            CanExecuteChanged( sender, e );
    };
}
C#6.0
// コンストラクタ
public GetRSSCommand( RSSViewModel viewModel ) {
    rssViewModel = viewModel;
    rssViewModel.PropertyChanged += ( sender, e ) =>
        CanExecuteChanged?.Invoke( sender, e );
}

これでイベントを発生させる時のデリゲートのnullチェックをするためのif文を省略することができ、コードが簡潔になりました。

◆ Visual Studio 2015 Update 2のクイックアクション機能で簡単に変換

Visual Studio 2015 Update 2では、クイックアクション機能を使って、if文を使ったデリゲートのnullチェックと呼び出し処理のコードを、null条件演算子を使った簡潔なコードに変換することができます。

cw-03.png

関連記事 :

2.2. nameof演算子でリファクタリングが捗る!?

「INotifyPropertyChanged」インターフェースを介してプロパティの変更を通知する時に、対象となるプロパティの名前を指定しますが、C# 5.0以前ではプロパティ名を文字列で直接入力していました。

C#5.0
// RSSフィード取得中のフラグ
private bool isProgress = false;
public bool IsProgress {
    get { return isProgress; }
    set {
        isProgress = value;
        NotifyPropertyChanged();
        NotifyPropertyChanged( "Title" );
        NotifyPropertyChanged( "Description" );
        NotifyPropertyChanged( "LastUpdatedTime" );
    }
}

しかし、この実装ではVisual Studioのリファクタリング機能を使ってプロパティの名前を変更した時に、リネームの対象にならないという問題があります。(C# 5.0で呼び出し元のプロパティを対象にするならば、「CallerMemberName」属性を使い、呼び出し元では引数を省略する手があります)

そこでC# 6.0のnameof演算子を使って、プロパティから名前を取得します。

C#6.0
// RSSフィード取得中のフラグ
private bool isProgress = false;
public bool IsProgress {
    get { return isProgress; }
    set {
        isProgress = value;
        NotifyPropertyChanged();
        NotifyPropertyChanged( nameof( Title ) );
        NotifyPropertyChanged( nameof( Description ) );
        NotifyPropertyChanged( nameof( LastUpdatedTime ) );
    }
}

これでVisual Studioのリファクタリング機能を使った時にリネームの対象にすることができるので、コードの保守がしやすくなります。それだけでなく、プロパティ名のスペルミスによるバグを未然に防いだり、Visual Studioのコード補完機能を活用したりすることができます。

2.3. プロパティ / メソッドの本体をラムダ式で簡潔に

C# 6.0からはプロパティ、メソッドの本体をラムダ式(式形式)で記述できるようになりました。

そこで、RSSViewModelクラスにあるGetterのみのプロパティの本体をラムダ式で表してみます。

C#5.0
// RSSフィードのタイトル
public string Title {
    get { return IsProgress ? "RSSフィールドを取得中 ..." : rssModel.RSSProvider.Title; }
}
// RSSフィードの説明
public string Description {
    get { return IsProgress ? "Recieving ..." : rssModel.RSSProvider.Description; }
}
// RSSフィードの最終更新日
public DateTime LastUpdatedTime {
    get { return IsProgress ? DateTime.MinValue : rssModel.RSSProvider.LastUpdatedTime; }
}
// RSSフィードのコンテンツ
public ObservableCollection<RSSContent> Items {
    get { return rssModel.Items; }
}

// RSSフィード取得のコマンド
public ICommand GetRSS {
    get { return getRSS ?? ( getRSS = new GetRSSCommand( this ) ); }
}

※GetRSSプロパティにある「??」は「null合体演算子」です。??以前の値がnullでなければそれを、nullであれば??以降の値を返します。

C#6.0
// RSSフィードのタイトル
public string Title => IsProgress ? "RSSフィールドを取得中 ..." : rssModel.RSSProvider.Title;
// RSSフィードの説明
public string Description => IsProgress ? "Recieving ..." : rssModel.RSSProvider.Description;
// RSSフィードの最終更新日
public DateTime LastUpdatedTime => IsProgress ? DateTime.MinValue : rssModel.RSSProvider.LastUpdatedTime;
// RSSフィードのコンテンツ
public ObservableCollection<RSSContent> Items => rssModel.Items;

// RSSフィード取得のコマンド
public ICommand GetRSS => getRSS ?? ( getRSS = new GetRSSCommand( this ) );

これで、単なるGetterを実装する時に「get」や「return」を入力する手間が省けますね。

また、GetRSSCommandクラスにあるCanExecuteメソッドの中身はreturn文1つだけなので、ラムダ式で簡潔に表すことができます。

C#5.0
// コマンドを実行できるかどうかを取得します。
public bool CanExecute( object parameter ) {
    return !rssViewModel.IsProgress;
}
C#6.0
// コマンドを実行できるかどうかを取得します。
public bool CanExecute( object parameter ) => !rssViewModel.IsProgress;

2.4. 自動実装プロパティで初期値を設定する

C# 5.0のコードでは、RSSModelクラスで定義した自動実装プロパティをコンストラクタ内で初期値を設定していました。

C#5.0
// RSSフィードのコンテンツを格納するコレクションです。
public ObservableCollection<RSSContent> Items { get; private set; }

// RSSフィードの配信元情報です。
public RSSProviderInfo RSSProvider { get; private set; }

// RSSフィードのURL( ViewModel、Viewから設定できるようにします。 )
public string Url { get; set; } 

// ※RSSModelクラスのコンストラクタで初期化します。

ここでC# 6.0の新機能、自動実装プロパティの初期化子を使って、各プロパティに初期値を設定します。

C#6.0
// RSSフィードのコンテンツを格納するコレクションです。
public ObservableCollection<RSSContent> Items { get; private set; } = new ObservableCollection<RSSContent>();

// RSSフィードの配信元情報です。
public RSSProviderInfo RSSProvider { get; private set; } = new RSSProviderInfo();

// RSSフィードのURL( ViewModel、Viewから設定できるようにします。 )
public string Url { get; set; } = "";

1つ気を付けておきたいのが、自動実装プロパティの初期化子を使って初期化した場合、プロパティのSetterが実行されるのではなく、バッキングフィールドに直接代入されることです。

3. C# 6.0でMVVMパターンを実装

C# 6.0の新機能を使って、1章のコードを改良したものを以下に示します。

3.1. Model(RSSModelクラス)

RSSModel.cs
// *** 中略 ***

// RSSリーダーのModelです。
class RSSModel : INotifyPropertyChanged {

    // HTMLタグを取り除くための文字列です。
    private const string patternStr = @"<.*?>";

    // コンストラクタ
    public RSSModel() {
        BindingOperations.EnableCollectionSynchronization( Items, new object() );
    }

    #region 2.4. 自動実装プロパティで初期値を設定する」の対象

    // RSSフィードのコンテンツを格納するコレクションです。
    public ObservableCollection<RSSContent> Items { get; private set; } = new ObservableCollection<RSSContent>();

    // RSSフィードの配信元情報です。
    public RSSProviderInfo RSSProvider { get; private set; } = new RSSProviderInfo();

    // RSSフィードのURL( ViewModel、Viewから設定できるようにします。 )
    public string Url { get; set; } = "";

    #endregion *******************************

    // RSSフィードを取得します。( 非同期メソッド )
    public Task<TaskResult> GetRSS() {
        // *** 中略 ***
    }

    // RSSの取得完了後に発生させるイベントハンドラです。
    public event TaskResultEventHandler GetRSSCompleted;

    // プロパティ変更後に発生させるイベントハンドラです。
    public event PropertyChangedEventHandler PropertyChanged;

    #region 2.1. null条件演算子で変更通知処理をコンパクトに」の対象

    // プロパティ変更を通知します。
    private void NotifyPropertyChanged( [CallerMemberName]string propertyName = null ) {
        PropertyChanged?.Invoke( this, new PropertyChangedEventArgs( propertyName ) );
    }

    #endregion *******************************

    #region 2.1. null条件演算子で変更通知処理をコンパクトに」の対象

    // RSSフィードを取得します。
    public async Task GetRSSAsync() {
        // RSSフィードを非同期で取得します。
        TaskResult result =  await Task.Run( () => GetRSS() );
        // RSSフィードの取得が完了したことをViewModel側に通知します。
        GetRSSCompleted?.Invoke( this, new TaskResultEventArgs( result ) );
    }

    #endregion *******************************
}

3.2. ViewModel(RSSViewModelクラス)

RSSViewModel.cs
// *** 中略 ***

// RSSリーダーのViewModelです。
class RSSViewModel : INotifyPropertyChanged {

    // Model
    RSSModel rssModel = new RSSModel();

    #region 2.1. null条件演算子で変更通知処理をコンパクトに」の対象

    // コンストラクタ
    public RSSViewModel() {
        rssModel.PropertyChanged += ( sender, e ) =>
            PropertyChanged?.Invoke( this, new PropertyChangedEventArgs( e.PropertyName ) );
        // RSSフィードの取得が完了したことをView側に通知します。
        rssModel.GetRSSCompleted += ( sender, e ) => {
            // RSSフィード取得中のフラグをオフにします。
            IsProgress = false;
            // RSSフィード取得完了したことをView側に通知します。
            GetRSSCompleted?.Invoke( this, e );
        };
    }

    #endregion *******************************

    #region 2.3. プロパティ / メソッドの本体をラムダ式で簡潔に」の対象

    // RSSフィードのタイトル
    public string Title => IsProgress ? "RSSフィールドを取得中 ..." : rssModel.RSSProvider.Title;

    // RSSフィードの説明
    public string Description => IsProgress ? "Recieving ..." : rssModel.RSSProvider.Description;

    // RSSフィードの最終更新日
    public DateTime LastUpdatedTime => IsProgress ? DateTime.MinValue : rssModel.RSSProvider.LastUpdatedTime;

    // RSSフィードのコンテンツ
    public ObservableCollection<RSSContent> Items => rssModel.Items;

    #endregion *******************************

    // RSSフィードのURL
    public string Url {
        get { return rssModel.Url; }
        set { rssModel.Url = value; }
    }

    #region 2.2. nameof演算子でリファクタリングが捗る!?」の対象

    // RSSフィード取得中のフラグ
    private bool isProgress = false;
    public bool IsProgress {
        get { return isProgress; }
        set {
            isProgress = value;
            NotifyPropertyChanged();
            NotifyPropertyChanged( nameof( Title ) );
            NotifyPropertyChanged( nameof( Description ) );
            NotifyPropertyChanged( nameof( LastUpdatedTime ) );
        }
    }

    #endregion *******************************

    // RSSの取得完了後に発生させるイベントハンドラです。
    public event TaskResultEventHandler GetRSSCompleted;

    // プロパティ変更後に発生させるイベントハンドラです。
    public event PropertyChangedEventHandler PropertyChanged;

    #region 2.1. null条件演算子で変更通知処理をコンパクトに」の対象

    // プロパティ変更を通知します。
    private void NotifyPropertyChanged( [CallerMemberName]string propertyName = null ) {
        PropertyChanged?.Invoke( this, new PropertyChangedEventArgs( propertyName ) );
    }

    #endregion *******************************

    #region 2.3. プロパティ / メソッドの本体をラムダ式で簡潔に」の対象

    // RSSフィード取得のコマンド
    private ICommand getRSS;
    public ICommand GetRSS => getRSS ?? ( getRSS = new GetRSSCommand( this ) );

    #endregion *******************************

    // RSSフィードを取得するコマンドです。
    private class GetRSSCommand : ICommand {

        // ViewModel
        private RSSViewModel rssViewModel;

        #region 2.1. null条件演算子で変更通知処理をコンパクトに」の対象

        // コンストラクタ
        public GetRSSCommand( RSSViewModel viewModel ) {
            rssViewModel = viewModel;
            // コマンド実行の可否の変更を通知します。
            rssViewModel.PropertyChanged += ( sender, e ) =>
                CanExecuteChanged?.Invoke( sender, e );
        }

        #endregion *******************************

        #region 2.3. プロパティ / メソッドの本体をラムダ式で簡潔に」の対象

        // コマンドを実行できるかどうかを取得します。
        public bool CanExecute( object parameter ) => !rssViewModel.IsProgress;

        #endregion *******************************

        // コマンド実行の可否の変更した時のイベントハンドラです。
        public event EventHandler CanExecuteChanged;

        // コマンドを実行し、RSSフィードを取得します。
        public void Execute( object parameter ) {
            // *** 中略 ***
        }
    }
}

// *** 中略 ***

Viewも含めた完全なプログラムはGistにアップロードしています。
https://gist.github.com/Nia-TN1012/0265cafca6dc50a211a3

4. おわりに

今回はC# 6.0の新機能の内、以下の4つを使ってWPFのMVVMパターンを実装していきました。

  • null条件演算子
  • nameof演算子
  • プロパティ / メソッドの本体をラムダ式で記述
  • 自動実装プロパティの初期化子

特に前者2つはプロパティの変更通知処理の実装でよく使うので、C# 5.0よりコーディングやメンテナンスが捗るのではないでしょうか。

それでは、See you next time!