ようこそ。睡眠不足なプログラマのチラ裏です。

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はXAMLC#で実装してみたいと思います。こんなの↓


 
 
  


XAMLC#でViewを作ろう
まずはViewを作ります。
ViewはUIを表すクラスです。画面デザインです。例えば、WPFでは、MainWindow.xamlです。
Silverlightでは MainPage.xaml や Page.xamlなどがそれにあたります。


Viewはコントロールやアニメーション、あるいはナビゲーションなど
様々な表示用の対話機能を持っているクラスを表します。
WPFSilverlightにおける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#で画面作るのは面倒くせーぞ!やってらんねーぜこん畜生!という方は、
WPFMVVMパターンしちゃうという方法を選ぶのも良いかもしれません。
多少違いはあるにせよ、たぶんSilverlightでも似たような感じでいけんじゃないかな。


どうでもいい事ですが、MVVMパターンって、
ビジュアル的にちょっと草植えときますねパターンかもかも。