【お勉強記録】WPF + MVVMでTodoアプリを作ってみた。
先週コマンドバインディングを学び、WPFのバインディングに関しては一応基礎的な部分はお抑えられたのかなと思い、今度はMVVMにのっとって実際に何か作ってみようということで今回Todoアプリを簡単に書いてみました。
MVVMパターン
MVVMは役割に応じてModel・View・ViewModelの3つにアプリケーションを分割するパターンで、ロジックを担当するModelと画面の表示などを担当するView、それぞれを橋渡しするViewModelという役割をそれぞれ担当しているとのこと。
とはいったものの、現状、Viewの部分をXamlが担当しており、ViewModelを担当するクラスとXamlがバインディングされて、ViewModel内でModelのクラスを使うというイメージしか持てていません。
利点として役割ごとに分けることで、再利用性やテストしやすさが高まったり、デザインとロジックでの担当者(コーディングする人)の分離が可能であったりするようです。
これは結構わかります。日常業務ではバリバリイベント駆動で全てをコードビハインドに書き込むような状況で開発を行っていて、依存度がすごすぎて、再利用性・テスト可能性・可読性が損なわれている状況を体験しているので、役割を分けることでこれらの問題に対処しようというのは理解しやすかったです。(レガシー環境にいてよかった~)
ViewModelとModelの役割の違いが現時点でいまいちな部分が多いのですが、今の時点でもとりあえず書いてみようということで今回遣ってみました。このあと色々他のサンプルなどを参考にして、自分の中に落とし込んでいくつもりです。
Todoアプリ
タスクと期限を登録して、終わったら消すというだけのシンプルなアプリ。
「追加ボタン」で追加したタスクが下に表示され、完了したものにはチェックを付けて「完了ボタン」押下で一覧から消えます。
まずはModelから。タスクを管理するのに必要なプロパティのみ持っています。
using System; namespace TodoMVVM { public class Task { public string Title { get; set; } public DateTime Deadline { get; set; } public bool Done { get; set; } } }
Modelってこんなにシンプルで平気なの?心配です。
続いてViewModelです。
using System; using System.Collections.ObjectModel; using System.ComponentModel; namespace TodoMVVM { public class ViewModel:INotifyPropertyChanged { #region INotifyPropertyChanged public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged(string name) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(name)); } } #endregion public ViewModel() { taskList = new ObservableCollection<Task>(); addCommand = new Command(ExecuteAdd); doneCommand = new Command(ExecuteDone); deadline = DateTime.Today; } private ObservableCollection<Task> taskList; public ObservableCollection<Task> TaskList { get { return taskList; } set { taskList = value; OnPropertyChanged("TaskList"); } } private Command addCommand; public Command AddCommand { get { return addCommand; } } private Command doneCommand; public Command DoneCommand { get { return doneCommand; } } private string title; public string Title { get { return title; } set { title = value; OnPropertyChanged("Title"); } } private DateTime deadline; public DateTime Deadline { get { return deadline; } set { deadline = value; OnPropertyChanged("Deadline"); } } /// <summary> /// 追加処理。 /// </summary> private void ExecuteAdd() { var task = new Task(); task.Title = title; task.Deadline = deadline; task.Done = false; taskList.Add(task); } /// <summary> /// 完了済みのタスクの削除 /// </summary> private void ExecuteDone() { for (int i = taskList.Count - 1; i >= 0 ; i--) { if (taskList[i].Done) { taskList.RemoveAt(i); } } } } }
Commandクラスは別に書いていて、コンストラクタでActionデリゲートに実処理を渡しています。
using System; using System.Windows.Input; namespace TodoMVVM { public class Command : ICommand { #region ICommand public bool CanExecute(Object parameter) { return true; } public event EventHandler CanExecuteChanged; public void Execute(object parameter) { action(); } #endregion private Action action; public Command(Action action) { this.action = action; } } }
Viewです、ViewModelをDataContextに指定して、Bindingしています。
<Window x:Class="TodoMVVM.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm ="clr-namespace:TodoMVVM" Title="MainWindow" Height="400" Width="400"> <Window.DataContext> <vm:ViewModel x:Name="viewModel"></vm:ViewModel> </Window.DataContext> <Grid> <Grid.RowDefinitions> <RowDefinition Height="auto"/> <RowDefinition Height="auto"/> <RowDefinition Height="16"/> <RowDefinition Height="auto"/> <RowDefinition Height="16"/> <RowDefinition Height="auto"/> <RowDefinition Height="auto"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="70"></ColumnDefinition> <ColumnDefinition Width="300"></ColumnDefinition> </Grid.ColumnDefinitions> <TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Background="#FFEEAA00" Text="TODO"/> <TextBox Grid.Row="0" Grid.Column="1" Width="250" HorizontalAlignment="Left" Text="{Binding Title}"></TextBox> <TextBlock Grid.Row="1" Grid.Column="0" Text="DeadLine" VerticalAlignment="Center" Background="#FFEEAA00"></TextBlock> <DatePicker Grid.Row="1" Grid.Column="1" Width="200" HorizontalAlignment="Left" SelectedDate="{Binding Deadline}"></DatePicker> <Button Grid.Row="3" Grid.Column="1" Command="{Binding AddCommand}" Height="20" Width="70" Content="追加" HorizontalAlignment="Right"></Button> <ListBox Grid.Row="5" Grid.Column="0" Grid.ColumnSpan="2" x:Name="listbox" Height="200" Width="350" HorizontalAlignment="Center" ItemsSource="{Binding TaskList}"> <ListBox.ItemTemplate> <DataTemplate> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="250"></ColumnDefinition> <ColumnDefinition Width="auto"></ColumnDefinition> </Grid.ColumnDefinitions> <CheckBox Grid.Column="0" IsChecked="{Binding Done}" Content="{Binding Title}"/> <TextBlock Grid.Column="1" Text="{Binding Deadline ,StringFormat=yyyy/MM/dd}" /> </Grid> </DataTemplate> </ListBox.ItemTemplate> </ListBox> <Button Grid.Row="6" Grid.Column="1" Command="{Binding DoneCommand}" Height="20" Width="70" Content="完了" HorizontalAlignment="Right"></Button> </Grid> </Window>
Xamlも結構難しいですねぇー 各コントロールがどのような機能やプロパティをもっていてどのように記述するのか、まだまだ勉強です。
最後に
ということで、一応Todoアプリを作成してみたのですが、いまいちまだまだわかりません! Viewとその他の分離はまぁわかってきたのですが、ViewModelとModelでの役割の分担の理解がいまいちです。
上のままでいくとさらに機能を盛り込んでいくことになると、どんどんViewModelが肥大していき容易にスパゲッティになりそうです。 MVVMのためのフレームワークもあるようなので、上にも書きましたが、色々自分で試しつつ、サンプルコードを当たりつつ、色々な人の意見を聞きつつ、自分の中で理解を進めていきます。
あ、あとアプリケーションを一から作る際のフォルダ階層のいい感じのルール見たいのも学びたい…。
【お勉強記録】WPF コマンド
久しぶりにWPF +MVVM関係です。 データバインディングについては基本的な部分は以前やりまして、今回はコマンドについて勉強しました。
コマンド
コマンドはXamlでの操作に対応して、実行される処理を定義したもの?みたいです。 コマンドとバインディングするにはICommandインターフェースの実装したクラスが必要なようです。
そのICommandインターフェースを見てみると以下のようになっています。
// 概要: // コマンドを定義します。 public interface ICommand { // 概要: // コマンドを実行するかどうかに影響するような変更があった場合に発生します。 event EventHandler CanExecuteChanged; // 概要: // 現在の状態でこのコマンドを実行できるかどうかを判断するメソッドを定義します。 // // パラメーター: // parameter: // コマンドで使用されたデータ。 コマンドにデータを渡す必要がない場合は、このオブジェクトを null に設定できます。 // // 戻り値: // このコマンドを実行できる場合は true。それ以外の場合は false。 bool CanExecute(object parameter); // // 概要: // コマンドの起動時に呼び出されるメソッドを定義します。 // // パラメーター: // parameter: // コマンドで使用されたデータ。 コマンドにデータを渡す必要がない場合は、このオブジェクトを null に設定できます。 void Execute(object parameter); }
コマンドで実行される実処理を担当する部分としてExecuteとコマンドが実行可能かを判断する他の部分に分かれています。
サンプル
理解のために実際に書いてみました。 「テキストボックスに何かしらの値が入っていれば、実行ボタンが押せる。(=何か入れないと実行できない。)」
ますはcs側から…。
ExecuteCommandクラスでICommandを実装しています。 CanExecuteでは、実行可能になる条件を、Executeでは実行処理をそれぞれ実装しています。 実装したExexuteCommandクラスを、実際にXamlと結ばれるCommandViewModelでプロパティとして使用しています。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.ComponentModel; using System.Runtime.CompilerServices; using System.Windows; using System.Windows.Input; namespace Command { public class CommandViewModel : INotifyPropertyChanged { public string Text { get { return text; } set { text = value; NotifyPropertyChange("Text"); } } private string text; public ICommand Command { get { return command ?? (command = new ExecuteCommand(this)); } } private ICommand command; public event PropertyChangedEventHandler PropertyChanged; protected void NotifyPropertyChange([CallerMemberName]string propertyName = null) { if (this.PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } private class ExecuteCommand: ICommand { private CommandViewModel commandViewModel; //コンストラクタ public ExecuteCommand(CommandViewModel viewModel) { commandViewModel = viewModel; commandViewModel.PropertyChanged += (sender, e) => { if (CanExecuteChanged != null) { CanExecuteChanged(sender, e); } }; } public bool CanExecute(object parameter) { return !String.IsNullOrEmpty(commandViewModel.Text); } public event EventHandler CanExecuteChanged; public void Execute(object param) { MessageBox.Show("実行"); } } } }
Xaml側では、DataContextプロパティで先ほどのCommandViewModelをセットして、 ButtonのCommandプロパティにバインドしています。
<Window x:Class="Command.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:Command" Title="MainWindow" Height="350" Width="525"> <Window.DataContext> <local:CommandViewModel x:Name="viewModel"/> </Window.DataContext> <StackPanel> <TextBox Text="{Binding Text}"></TextBox> <Button Content="Focus用"></Button> <Button Content="実行" Command="{Binding Command}"></Button> </StackPanel> </Window>
以下実行結果です。
実行と書いてあるボタンが押下不可状態になっています。
テキストボックスに何らか入力すると実行可能に!(今回はサンプルだしフォーカスが移動必要なままです。)
ちなみに押すと「実行」とメッセージボックスが出ます。
次回以降はMVVM
データバインディングとコマンドを一通り見られたので、いよいよ今後はMVVMについて学んでいく予定です。 まずはTodoアプリでも作ってみたいと思います。