SourceChord

C#とXAML好きなプログラマの備忘録。最近はWPF系の話題が中心です。

Windows Template Studio雛形コードの使い方~Blank & MVVM Basicなプロジェクト~

今度は、MVVM Basicな構成のプロジェクトを見ていきます。
この雛形は、シンプルなMVVM構成でコードを書く時には、割と使い勝手がいいかなと思います。

準備

まずは、以下の設定でプロジェクトを作成します。

  • ProjectType: Blank
  • Framework: MVVM Basic
  • Pages, Features: デフォルトのまま

プロジェクトの全体構成はこんな感じです。
f:id:minami_SC:20170527232606p:plain

Code BehindとMVVM Basicのプロジェクトの違い

アプリの起動処理や、一部の各種ヘルパー関数などは、前回のCode Behindなプロジェクトと共通です。

違いは以下のような点です。

  • Helpersフォルダに、以下のようなMVVM設計のための基底クラスが用意されている
    • Observableクラス
    • RelayCommandクラス
  • Views/ViewModelsフォルダ
    • Observableクラスを継承した、MainViewModelクラス
    • MainPage.xaml.csは、↑をプロパティとして持つだけのシンプルな構成

MVVMな設計のためのヘルパークラス類

MVVM Basicでは、Helpersフォルダに以下のようなObservable/RelayCommandといったクラスが追加されています。
これらは、MVVMな設計をする上での必需品となる、INotifyPropertyChangedとICommandインターフェースの実装クラスです。

C#7.0な書き方が多用されていますが、↓に書いたようなコードとだいたい同じことが書かれています。
http://sourcechord.hatenablog.com/entry/20130303/1362315081
http://sourcechord.hatenablog.com/entry/2014/01/13/200039

Observable.cs

    public class Observable : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        protected void Set<T>(ref T storage, T value, [CallerMemberName]string propertyName = null)
        {
            if (Equals(storage, value))
            {
                return;
            }

            storage = value;
            OnPropertyChanged(propertyName);
        }

        protected void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

RelayCommand.cs

    public class RelayCommand : ICommand
    {
        private readonly Action _execute;
        private readonly Func<bool> _canExecute;

        public event EventHandler CanExecuteChanged;
        public RelayCommand(Action execute) : this(execute, null) { }

        public RelayCommand(Action execute, Func<bool> canExecute)
        {
            this._execute = execute ?? throw new ArgumentNullException("execute");
            this._canExecute = canExecute;
        }

        public bool CanExecute(object parameter) => _canExecute == null || _canExecute();
        public void Execute(object parameter) => _execute();
        public void OnCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
    }

    public class RelayCommand<T> : ICommand
    {
        private readonly Action<T> _execute;
        private readonly Func<T, bool> _canExecute;

        public event EventHandler CanExecuteChanged;
        public RelayCommand(Action<T> execute) : this(execute, null) { }

        public RelayCommand(Action<T> execute, Func<T, bool> canExecute)
        {
            this._execute = execute ?? throw new ArgumentNullException("execute");
            this._canExecute = canExecute;
        }

        public bool CanExecute(object parameter) => _canExecute == null || _canExecute((T)parameter);
        public void Execute(object parameter) => _execute((T)parameter);
        public void OnCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
    }

ViewsとViewModelsフォルダ

MainPage.xaml
MainPageのxamlは、前回のコードビハインドを用いたプロジェクトとほぼ同様のものです。
ということで、ここではコードは省略します。

MainPage.xaml.cs
MainPageのコードビハインドは、ViewModelのプロパティを持つだけのシンプルなものになっています。
前回と違い、MVVMなスタイルでViewとViewModelを分離しているので、コードビハインドはINotifyPropertyChangedの実装などはせず、シンプルな形になっています。

    public sealed partial class MainPage : Page
    {
        public MainViewModel ViewModel { get; } = new MainViewModel();
        public MainPage()
        {
            InitializeComponent();
        }
    }

MainViewModel.cs
ViewModelは、Observableを継承しただけのシンプルなものが用意されています。

    public class MainViewModel : Observable
    {
        public MainViewModel()
        {
        }
    }

サンプル

ObservableやRelayCommandどのクラスを利用し、ちょっとしたサンプルコード書いてみます。

テキストボックスの文字列をViewModelのNameプロパティとバインドしておき、
Clearボタンが押されたら、RelayCommandを用いて上記Nameプロパティをクリアします。

f:id:minami_SC:20170527233040p:plain

View/ViewModelのコードは、それぞれ以下のような感じ。

MainPage.xaml

<Page
    x:Class="Blank_MvvmBasic.Views.MainPage"
    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"
    mc:Ignorable="d">
    <Grid
        x:Name="ContentArea"
        Margin="12,0,12,0">

        <Grid.RowDefinitions>
            <RowDefinition x:Name="TitleRow" Height="48"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <TextBlock
            x:Name="TitlePage"
            x:Uid="Main_Title"
            FontSize="28" FontWeight="SemiLight" TextTrimming="CharacterEllipsis" TextWrapping="NoWrap" VerticalAlignment="Center"
            Margin="0,0,12,7"/>

        <Grid 
            Grid.Row="1" 
            Background="{ThemeResource SystemControlPageBackgroundChromeLowBrush}">
            <StackPanel>
                <TextBox Margin="5" Width="200" Text="{x:Bind ViewModel.Name, Mode=TwoWay}" HorizontalAlignment="Left" />
                <Button Content="Clear" HorizontalAlignment="Left" Margin="5" Command="{x:Bind ViewModel.ClearCommand}" />
            </StackPanel>

        </Grid>
    </Grid>
</Page>

MainPage.xaml.cs
雛形コードのまま変更なし。

MainViewModel.cs

    public class MainViewModel : Observable
    {
        public MainViewModel()
        {
        }

        // Observableクラスで定義されているSetメソッドを使うことで、
        // プロパティの値が更新されたら、INotifyPropertyChangedでの更新通知イベントが呼び出されます。
        private string name;
        public string Name
        {
            get { return name; }
            set { this.Set(ref this.name, value); }
        }

        // 以下、RelayCommandクラスを用いたコマンドの実装
        private RelayCommand clearCommand;
        public RelayCommand ClearCommand
        {
            get { return clearCommand = clearCommand ?? new RelayCommand(Clear); }
        }
        private void Clear()
        {
            this.Name = string.Empty;
        }
    }

コマンド実行のEnable/Disable制御

RelayCommandのCanExecuteを利用して、コマンド実行可否の制御をしてみます。

UWPではx:Bindによってイベントから関数へと直接バインドすることができます。
なので、RelayCommandクラスを用いずとも、前述のサンプルと同等の事は実装できます。

しかし、RelayCommandではコマンドが実行可能か否かをコマンド自身が示すことができるようになっています。
コマンドが実行可能かどうかを判断するチェック処理を定義しておくと、コマンドをボタンにバインドしたときに、自動でEnable/Disable制御をしてくれるようになります。

ということで、コマンドの実行可否の制御をしてみます。
修正点は以下の二つ。

  • RelayCommand作成時に、第二引数でコマンド実行できるかどうかのチェック処理を渡す
  • NameプロパティのSetterで、ClearCommandのOnCanExecuteChangedを呼び出す
    • Nameプロパティが変わる = Clearボタンの実行可否が変わる可能性があるため

こうすると、ClearCommandをボタンのCommandプロパティにバインドしておくだけで、
CanExecuteがfalseの時には、ボタンが勝手にDisable状態へと変化します。

    public class MainViewModel : Observable
    {
        public MainViewModel()
        {
        }

        // Observableクラスで定義されているSetメソッドを使うことで、
        // プロパティの値が更新されたら、INotifyPropertyChangedでの更新通知イベントが呼び出されます。
        private string name;
        public string Name
        {
            get { return name; }
            set { this.Set(ref this.name, value); this.ClearCommand.OnCanExecuteChanged(); }
        }

        // 以下、RelayCommandクラスを用いたコマンドの実装
        private RelayCommand clearCommand;
        public RelayCommand ClearCommand
        {
            get { return clearCommand = clearCommand ?? new RelayCommand(Clear, () => !string.IsNullOrWhiteSpace(this.Name)); }
        }
        private void Clear()
        {
            this.Name = string.Empty;
        }
    }