F#でMVVMパターン。はじめてのWPFプログラミング。ModelとViewModelをF#で、ViewはXAMLとC#で。
遅ればせながら「WindowsForm終了のお知らせ」を感知
WPFに関しては、仕事で使う機会もなく、自宅に満足な開発環境もなかったという理由で、関連記事の流し読み程度はしていましたが、基本華麗にスルーしてきました。
しかし、Windows7が好感触だったり、今後のSilverlightに期待できそうだったり、
WPFで作られているというVisualStudio2010 Bata2を実際に触ってみて、Microsoftの心意気を感じてみたりで、
世間の流れ的にXAMLなUIについてそろそろ無視できなくなってきました。
自宅に環境も整ったことだし、今後はWindowsFormではなく、なるべくWPFを使うようにしたい。
はじめてのWPFプログラミング
「はじめてのWPFプログラミング」と書きましたが、厳密には初めてではないです。以前、WeakEventパターンを学習した際に、ちょろっとハリー・ボッテーな画面を作ったことがあります。
あれはちょうど、わんくま同盟などを中心に各所でMVVMパターンの話題で盛り上がっていた時期でした。
/) ///) /,.=゙''"/ / i f ,.r='"-‐'つ____ こまけぇこたぁいいんだよ!! / / _,.-‐'~/⌒ ⌒\ / ,i ,二ニ⊃( ●). (●)\ / ノ il゙フ::::::⌒(__人__)⌒::::: \ ,イ「ト、 ,!,!| |r┬-| | / iトヾヽ_/ィ"\ `ー'´ /
MVVMパターンはMVCパターンやMVPパターンのお友達
MVVMパターンはMVCやMVPの流れを組むパターンということで、その概念は難しくありません。ですが、まだ実際にMVVMパターンを用いてWPFなアプリケーションを作ったことがありません。
なので、遅ればせながら「MVVMパターン」をやってみようと思います。
でも、ただC#で実装するというだけでは芸がなさ過ぎなので、今回はF#を絡めてみます。
今のところ、F#はGUI作成の面で他の.NET言語に比べてかなり不利な状況。
VとVMについて疎な結合で表現するMVVMパターンとF#は、幸いなことに相性ばっちりです。
ModelとViewModelをF#で、ViewはXAMLとC#で実装してみたいと思います。こんなの↓
XAMLとC#でViewを作ろう
まずはViewを作ります。ViewはUIを表すクラスです。画面デザインです。例えば、WPFでは、MainWindow.xamlです。
Silverlightでは MainPage.xaml や Page.xamlなどがそれにあたります。
Viewはコントロールやアニメーション、あるいはナビゲーションなど
様々な表示用の対話機能を持っているクラスを表します。
WPF や SilverlightにおけるMVVMパターンでは、データバインディングもViewに含まれます。
バインディングはデータのどのプロパティを使うかだけを指定していて、
プロパティがどこからくるのか 、つまりどのインスタンスにバインドされるのかは一切意識しないようにします。
データソースがViewのDataContextにセットされた時に、バインディングがアクティブになります。
XAMLは完全に感覚的なものだけで書きました。実際どうすれば良いのかよくわかっていません。
わんくま同盟でどなたかが、WPFの基本はGridでと言っていたのでGrid使ってみました。
View.xaml
<UserControl x:Class="WpfApplication1.View" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Height="150" Width="255"> <Grid Height="150" Width="255"> <Grid.ColumnDefinitions> <ColumnDefinition Width="50"/> <ColumnDefinition Width="100"/> <ColumnDefinition Width="100"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="25"/> <RowDefinition Height="25"/> <RowDefinition Height="25"/> <RowDefinition Height="25"/> </Grid.RowDefinitions> <Grid Grid.Column="0" Grid.Row="0" Grid.ColumnSpan="3"> <TextBlock Text="{Binding ElementName=textBoxAge, Path=(Validation.Errors).CurrentItem.ErrorContent}" Foreground="Red"/> </Grid> <Grid Grid.Column="0" Grid.Row="1"> <TextBlock Text="年齢:" TextAlignment="Center"/> </Grid> <Grid Grid.Column="1" Grid.Row="1" Grid.ColumnSpan="2"> <TextBox Name="textBoxAge" Text="{Binding Age, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}"/> </Grid> <Grid Grid.Column="0" Grid.Row="2"></Grid> <Grid Grid.Column="1" Grid.Row="3" Grid.ColumnSpan="2"> <Button Content="判 定" Command="{Binding Judge}"/> </Grid> </Grid> </UserControl>
View.xaml.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; 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 WpfApplication1 { /// <summary> /// View.xaml の相互作用ロジック /// </summary> public partial class View : UserControl { public View() { InitializeComponent(); } } }
ViewがUserContorlになっている点に注目です。
また、このViewではビジネスロジック的な実装を一切もちません。
F#でModelを作ろう
次にModelを作りましょう。
Modelは特定のエンティティとして表現されるデータを表すクラスです。
例えば、CompanyName や CustomerId のようなプロパティを持った Customer クラスです。
Modelの目的はデータを表現することであり、表示場所や表示方法に関する情報は一切持ちません。
今回は年齢プロパティを持つだけの単純なModelを用意することにします。
Model.fs
namespace Library1 open System open System.Diagnostics open System.ComponentModel /// Model [<Sealed>] [<DebuggerStepThrough>] type Model () = let mutable _age = "" member this.Age with get() = _age and set(value) = _age <- value
F#でViewModelを作ろう
続いてViewModelです。
ViewModelはViewと他のもの(Modelなど)との間の接着剤の役割を果たします。
ただし、ViewとViewModelは互いに直接的に参照はせず、クラスはあくまで疎結合を保つことに注意します。
MVVMパターンでは、ViewのDataContext に ViewModel のインスタンスを
設定することでデータバインディングを行って関連づけます。
ViewModelは Model や、View から ViewModel のアクションを実行するための Command プロパティ、
View にバインドされるその他のプロパティを持ち、INotifyPropertyChangedインターフェイスを通じて
互いのプロパティが変更されたことを通知し合います。
ViewModel.fs
namespace Library1 open System open System.Diagnostics open System.Windows open System.Windows.Forms open System.Windows.Input open System.ComponentModel /// ViewModel type ViewModel (model : Model) = let mutable _model = model let mutable _age = "" let mutable _message = "" let mutable _error = "" let mutable (_judge : ICommand) = null let _propertyChanged = Event<_,_>() interface INotifyPropertyChanged with [<CLIEvent>] member self.PropertyChanged = _propertyChanged.Publish member this.Age with get() = _age and set(v) = _age <- v _propertyChanged.Trigger(this, PropertyChangedEventArgs("Age")) member this.Message with get() = _message and set(v) = _message <- v _propertyChanged.Trigger(this, PropertyChangedEventArgs("Message")) interface IDataErrorInfo with member this.Error with get() = null ///インデクサ member this.Item with get (columnName : string) = try if (columnName <> "Age") then null elif (System.String.IsNullOrEmpty(this.Age)) then "年齢を入力してください。" else let mutable age = 0 if not(System.Int32.TryParse(this.Age,ref age)) then "年齢は数値で入力してください。" else null finally CommandManager.InvalidateRequerySuggested () member this.Judge with get() = if not(_judge = null) then _judge else let executeAction (param : obj) = if (Convert.ToInt32(this.Age) < 20) then let mr = MessageBox.Show("おこちゃまは、けえんな。") () else let mr = MessageBox.Show("アダルティなページが閲覧できます。") () let canExecuteAction (param : obj) = let (a : IDataErrorInfo) = this :> IDataErrorInfo a.["Age"] = null _judge <- new SampleCommand(executeAction,canExecuteAction) _judge
ViewModelで利用するSampleCommandを作る
ViewからViewModelで実行する処理および実行可能かどうかの判断について、
関数で指定するCommandクラスを作成します。
SampleCommand.fs
namespace Library1 open System open System.Diagnostics open System.Windows open System.Windows.Forms open System.Windows.Input open System.ComponentModel /// SampleCommand type SampleCommand (executeAction : (obj -> unit), canExecuteAction : (obj -> bool)) = let _executeAction = executeAction let _canExecuteAction = canExecuteAction let mutable handler : EventHandler = null interface ICommand with member this.Execute param = _executeAction param member this.CanExecute param = _canExecuteAction param member this.add_CanExecuteChanged(eh : EventHandler) = handler <- (Delegate.Combine(handler, eh) :?> EventHandler) CommandManager.RequerySuggested.AddHandler(eh) member this.remove_CanExecuteChanged(eh : EventHandler) = handler <- (Delegate.Remove(handler, eh) :?> EventHandler) CommandManager.RequerySuggested.RemoveHandler(eh)
今回はCommadManager.RequerySuggestedに関連付けています。
ICommandインターフェイスのCanExecuteChangedイベントのaddとremoveの実装のしかたで、
ちょっと詰まりました。F#でイベント関係を書くのは、ちょっと慣れが必要かもしれません。
Viewを表示するMainWindowを作る
今回のサンプルのメインウィンドウにして、唯一のウィンドウです。
MainWindow.xaml
<Window x:Class="WpfApplication1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="年齢認証" Height="255" Width="300" WindowStartupLocation="CenterScreen"> <Grid> <ContentPresenter Content="{Binding}" /> </Grid> </Window>
MainWindow.xaml.cs
using System.Windows; namespace WpfApplication1 { /// <summary> /// MainWindow.xaml の相互作用ロジック /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } } }
最後の仕上げ
いよいよアプリケーションの開始です。
ViewModelとViewの関連付けを行って、MainWindowを表示します。
App.xaml
<Application x:Class="WpfApplication1.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:l="clr-namespace:WpfApplication1" xmlns:m="clr-namespace:Library1;assembly=Library1" Startup="Application_Startup"> <Application.Resources> <!-- ViewModelとViewの関連付けをします --> <DataTemplate DataType="{x:Type m:ViewModel}"> <l:View /> </DataTemplate> </Application.Resources> </Application>
App.xaml.cs
using System; using System.Windows; using Library1; namespace WpfApplication1 { /// <summary> /// App.xaml の相互作用ロジック /// </summary> public partial class App : Application { private void Application_Startup(object sender, StartupEventArgs e) { // ウィンドウとViewModelの初期化 var window = new MainWindow { DataContext = new ViewModel(new Model()) }; window.Show(); } } }
以上です。F#でMVVMパターンできました。お疲れ様でした。
F#で画面作るのは面倒くせーぞ!やってらんねーぜこん畜生!という方は、
WPFでMVVMパターンしちゃうという方法を選ぶのも良いかもしれません。
多少違いはあるにせよ、たぶんSilverlightでも似たような感じでいけんじゃないかな。
どうでもいい事ですが、MVVMパターンって、
ビジュアル的にちょっと草植えときますねパターンかもかも。