This project is read-only.

Kinkuma Framework F# ドキュメント

Getting Start!

インストール

以下のようなプロジェクト構成を想定しています。
  • WPF アプリケーション(C# or VB) MVVMのVを担当
  • F# ライブラリ(F#) MVVMのVMを担当
  • F# ライブラリ(F#) MVVMのMを担当

そして、VのプロジェクトからはVMとMのプロジェクトを参照し、VMのプロジェクトからはMのプロジェクトを参照します。
ここでは、以下の名前でプロジェクトを作成したものとして説明します。
  • Vのプロジェクト Hello.View
  • VMのプロジェクト Hello.ViewModel
  • Mのプロジェクト Hello.Model
WS000001.JPG

nugetによるインストール

インストールにはnugetを使用します。nugetについてはnugetのプロジェクトのサイトを参照してください。

Hello.Viewプロジェクトの設定

Hello.Viewプロジェクトにnugetを使用してKinkumaFrameworkを参照に追加します。
WS000002.JPG

Hello.ViewModelプロジェクトの設定

次にHello.ViewModelプロジェクトの参照にKinkumaFramework.FSharpを追加します。
WS000003.JPG

Modelの作成

まず、アプリケーションのコアになるModelを作成します。ここでは四則演算を行うアプリケーションを作成します。
Hello.Modelプロジェクトにcalc.fsという名前のファイルを追加して、以下のように記述します。
namespace Hello.Model

module Calculate =
    /// 計算方式
    type CalculateType =
        | Add
        | Sub
        | Mul
        | Div

    /// 計算を行う
    let calculate (lhs : float) (rhs : float) calcType =
        match calcType with
        | Add -> lhs + rhs
        | Sub -> lhs - rhs
        | Mul -> lhs * rhs
        | Div -> lhs / rhs

ViewModelの作成

次に、ViewModelの作成を行います。
ここでは、ViewModelで左辺値と右辺値と計算方式を入力してModelの関数を呼び出すように作成しています。
まず、計算方式を表すViewModelをCalculateTypeViewModelという名前で作成します。
ここで作成するViewModelは、コマンドもプロパティの変更通知も行わないので通常のクラスとして作成しています。
namespace Hello.ViewModel

open Microsoft.FSharp.Reflection
open Hello.Model.Calculate

module private CalculateTypeViewModelUtils =
    /// 計算方式を表示用文字列に変換する
    let toLabel calculateType =
        match calculateType with
        | Add -> "足し算"
        | Sub -> "引き算"
        | Mul -> "掛け算"
        | Div -> "割り算"

open CalculateTypeViewModelUtils

/// 計算方式のViewModel
type CalculateTypeViewModel(calculateType) =
    /// 計算方式の表示用文字列
    member x.Label = toLabel calculateType
    /// 計算方式
    member x.CalculateType : CalculateType = calculateType

    /// 全計算方式に対応したViewModelのリストを取得する
    static member Create() =
        [|
            CalculateTypeViewModel(Add);
            CalculateTypeViewModel(Sub);
            CalculateTypeViewModel(Mul);
            CalculateTypeViewModel(Div);
        |]


次にMainWindowViewModelを作成します。このクラスは画面から計算の左辺値、右辺値、計算方法を受け取るクラスになります。
open System
open System.ComponentModel.DataAnnotations
open System.Collections.ObjectModel
open Okazuki.MVVM.PrismSupport.FSharp
open Microsoft.Practices.Prism.Commands
open Hello.ViewModel.Utils
open Hello.Model
open System.Linq

/// MainWindow用ViewModel
type MainWindowViewModel() =
    inherit FsViewModelBase()

    /// 計算方式
    let calculateTypes = CalculateTypeViewModel.Create()

    /// 計算コマンド
    let mutable calculateCommand = Unchecked.defaultof<DelegateCommand>

    /// 左辺値
    let mutable lhs = String.Empty

    /// 右辺値
    let mutable rhs = String.Empty

    /// 計算結果
    let mutable answer = String.Empty

    /// 現在選択中の計算方式
    let mutable selectedType = calculateTypes.First()

    /// 計算方式を取得する
    member x.CalculateTypes = calculateTypes

    /// 左辺値を取得または設定する
    member x.Lhs 
        with get() = lhs
        and set v = x.set(&lhs, v, <@ x.Lhs @>)

    /// 右辺値を取得または設定する
    member x.Rhs 
        with get() = rhs
        and set v = x.set(&rhs, v, <@ x.Rhs @>)

    /// 計算結果を取得または設定する
    member x.Answer
        with get() = answer
        and set v = x.set(&answer, v, <@ x.Answer @>)

    /// 現在選択中の計算方式を取得または設定する
    member x.SelectedType
        with get() = selectedType
        and set v = x.set(&selectedType, v, <@ x.SelectedType @>)

    /// 計算を行うコマンドを取得する
    member x.CalculateCommand = 
        x.command(
            &calculateCommand, 
            x.CalculateExecute)
                

    /// 計算処理
    member private x.CalculateExecute() =
        // Modelに処理を委譲して結果を取得する
        x.Answer <- Calculate.calculate (double x.Lhs) (double x.Rhs) x.SelectedType.CalculateType |> string

ここでのポイントはViewModelの基本クラスにOkazuki.MVVM.PrismSupport.FSharp.FsViewModelBaseを継承しているところです。このクラスが冗長になりがちなViewModelのクラスの記述を簡略化してくれます。

プロパティの定義

PropertyChangedイベントを発行するプロパティの定義方法は以下のようになります。
let mutable lhs = String.Empty

member x.Lhs
    with get() = lhs
    and set v = x.set(&lhs, v, <@ x.Lhs @>)

FsViewModelBaseで定義しているsetメソッドにプロパティのフィールドの参照と、セットする値と、プロパティ名を示すための式を渡すことで必要な処理が行われるようになっています。

コマンドの定義

次に、DelegateCommandの定義を示します。
/// 計算コマンド
let mutable calculateCommand = Unchecked.defaultof<DelegateCommand>

/// 計算を行うコマンドを取得する
member x.CalculateCommand = 
    x.command(
        &calculateCommand, 
        x.CalculateExecute)
                
/// 計算処理
member private x.CalculateExecute() =
    // Modelに処理を委譲して結果を取得する
    x.Answer <- Calculate.calculate (double x.Lhs) (double x.Rhs) x.SelectedType.CalculateType |> string

コマンドもFsViewModelBaseクラスで定義されているコマンドメソッドにコマンドのフィールドの参照と実行メソッド(必要に応じて実行可否メソッド)を渡すことで適切にコマンドが生成されるようになっています。

Viewの作成

ViewはC#のプロジェクトなので通常通り作成します。今回のサンプルでは以下のようなXAMLになります。
<Window x:Class="Hello.View.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="clr-namespace:Hello.ViewModel;assembly=Hello.ViewModel"
        Title="MainWindow" SizeToContent="WidthAndHeight" ResizeMode="NoResize">
    <Window.DataContext>
        <!-- ViewModelの設定 -->
        <vm:MainWindowViewModel />
    </Window.DataContext>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <TextBox Name="textBox1" Text="{Binding Path=Lhs, UpdateSourceTrigger=PropertyChanged}" Margin="2.5" MinWidth="150" />
        <TextBox Grid.Column="2" Name="textBox2" Text="{Binding Path=Rhs, UpdateSourceTrigger=PropertyChanged}" Margin="2.5" MinWidth="150" />
        <ComboBox Grid.Column="1" Name="comboBox1" ItemsSource="{Binding Path=CalculateTypes}" DisplayMemberPath="Label" SelectedItem="{Binding Path=SelectedType}" Margin="2.5" />
        <Button Content="計算" Grid.Column="3" Name="button1" Command="{Binding Path=CalculateCommand}" Margin="2.5" MinWidth="50" />
        <TextBlock Grid.Column="4" Name="textBlock1" Text="{Binding Path=Answer}" Margin="2.5" MinWidth="150" />
    </Grid>
</Window>

実行

実行すると、以下のような画面が表示されます。
WS000029.JPG

数字を入力して計算方式を入力して計算ボタンを押すと以下のように計算結果が表示されます。
WS000030.JPG

ただし、TextBoxに数字以外のものを入力して計算ボタンを押すとアプリケーションがクラッシュします。
WS000031.JPG

入力値の検証機能

ここでは、Kinkuma Framework F#の機能を使って入力値の検証を行います。
入力値の検証をサポートするのはFsValidatableViewModelBaseクラスになります。このクラスを継承することでSystem.ComponentModel.DataAnnotationsによるアノテーションベースの入力値の検証が行えます。この機能を使用するように書き直したViewModelは以下のようになります。
namespace Hello.ViewModel

open System
open System.ComponentModel.DataAnnotations
open System.Collections.ObjectModel
open Okazuki.MVVM.PrismSupport.FSharp
open Microsoft.Practices.Prism.Commands
open Hello.ViewModel.Utils
open Hello.Model
open System.Linq

/// MainWindowのViewModel
type MainWindowViewModel() as x =
    inherit FsValidatableViewModelBase() // 妥当性検証機能つきViewModelの基本クラス

    /// 計算方式
    let calculateTypes = CalculateTypeViewModel.Create()

    /// 計算コマンド
    let mutable calculateCommand = Unchecked.defaultof<DelegateCommand>

    /// 左辺値
    let mutable lhs = String.Empty

    /// 右辺値
    let mutable rhs = String.Empty

    /// 計算結果
    let mutable answer = String.Empty

    /// 現在選択中の計算方式
    let mutable selectedType = calculateTypes.First()

    do
        /// 全プロパティの妥当性検証
        x.ValidateObject()

    /// 計算方式の配列を取得する
    member x.CalculateTypes = calculateTypes

    /// 左辺値を取得または設定する
    [<Required(ErrorMessage = "入力してください")>]
    [<DoubleFormat(ErrorMessage = "実数を入力してください")>]
    member x.Lhs 
        with get() = lhs
        and set v = x.set(&lhs, v, <@ x.Lhs @>)

    /// 右辺値を取得または設定する
    [<Required(ErrorMessage = "入力してください")>]
    [<DoubleFormat(ErrorMessage = "実数を入力してください")>]
    member x.Rhs 
        with get() = rhs
        and set v = x.set(&rhs, v, <@ x.Rhs @>)

    /// 計算結果を取得または設定する
    member x.Answer
        with get() = answer
        and set v = x.set(&answer, v, <@ x.Answer @>)

    /// 現在選択中の計算方式を取得または設定する
    member x.SelectedType
        with get() = selectedType
        and set v = x.set(&selectedType, v, <@ x.SelectedType @>)

    /// 計算処理を行うコマンドを取得または設定する
    member x.CalculateCommand = 
        x.command(
            &calculateCommand, 
            x.CalculateExecute, 
            fun () -> not (x.HasErrors))
                
    /// 計算処理
    member private x.CalculateExecute() =
        x.Answer <- Calculate.calculate (double x.Lhs) (double x.Rhs) x.SelectedType.CalculateType |> string


ポイントはLhsとRhsプロパティに属性を指定している箇所です。このように属性を指定することでプロパティの値の検証が行われます。
DoubleFormatAttributeは独自に定義した検証属性で文字列からDouble型に変換できない場合にエラーになるように実装しています。以下にコードを示します。
namespace Hello.ViewModel.Utils

open System
open System.ComponentModel.DataAnnotations

type DoubleFormatAttribute() =
    inherit ValidationAttribute()

    override x.IsValid(value, context) =
        match Double.TryParse(string value) with
        | (true, v) -> ValidationResult.Success
        | _ -> ValidationResult(x.ErrorMessage)


次に、初期状態でプロパティの値の妥当性検証を行うようにプライマリコンストラクタでx.ValidateObject()を呼び出して検証処理を行っています。
do
    /// 全プロパティの妥当性検証
    x.ValidateObject()


そして、入力にエラーがあるときはボタンが押せないようにコマンドのCanExecuteに以下のような処理を追加しています。
/// 計算処理を行うコマンドを取得または設定する
member x.CalculateCommand = 
    x.command(
        &calculateCommand, 
        x.CalculateExecute, 
        fun () -> not (x.HasErrors))
                
/// 計算処理
member private x.CalculateExecute() =
    x.Answer <- Calculate.calculate (double x.Lhs) (double x.Rhs) x.SelectedType.CalculateType |> string

実行

この状態で実行すると入力された値が数字じゃない場合はボタンが押せなくなります。
WS000032.JPG

もちろん、数字を入力した場合はボタンを押すことが出来ます。
WS000033.JPG

この状態でももんだいはありませんが、割り算を指定した時に右辺値に0を入力すると結果がInfinityになってしまいます。
WS000034.JPG

このときに、ユーザに0での除算を行ったことをメッセージボックスで示して、必要に応じて入力値をクリアするように変更したいと思います。

Asyncワークフローとコールバック

MVVMでは、通常Messengerパターンというものを使ってViewModelからViewにフィードバックを通知します。PrismではInteractionRequest<T>クラスがこの機能を持っています。このクラスのRaiseメソッドはコールバックでViewでの処理結果を受け取ることが出来るのですが、このコードはとても見通しが悪くなります。
Kinkuma Framework F#では、Asyncワークフローとくみあわせることで処理を同期的に書くことが出来るRaiseAsyncメソッドを提供しています。この機能を使って以下のようにViewModelを書き換えます。

まずは、InteractionRequest型のプロパティをViewModelに追加します。
/// メッセージボックスを表示するリクエストを投げる
let alertRequest = InteractionRequest<ShowMessageBoxConfirmation>()

/// Viewへメッセージボックスを表示するリクエストを投げる
member x.AlertRequest = alertRequest


そして、計算ボタンが押された時の処理を以下のように記述します。ポイントは非同期ワークフローを使っているところです。
/// 計算処理
member private x.CalculateExecute() =
    let r = Calculate.calculate (double x.Lhs) (double x.Rhs) x.SelectedType.CalculateType
    x.Answer <- r |> string

    // 結果がInfinitiyのときはユーザとの対話を含む処理をやる
    if Double.IsInfinity(r) then
        async {
            // RaiseAsyncでShowMessageBoxConfirmationをViewに投げる
            let! result = x.AlertRequest.RaiseAsync(
                            ShowMessageBoxConfirmation(
                                Title = "確認", 
                                Content = "計算結果が実数の範囲を超えました。入力を初期化しますか", 
                                Button = Nullable<_>(MessageBoxButton.OKCancel)))
            // 結果を受けて後続の処理を行う
            if result.Confirmed then
                x.Lhs <- String.Empty
                x.Rhs <- String.Empty
                x.Answer <- String.Empty
                x.SelectedType <- x.CalculateTypes.First()
        } |> Async.StartImmediate


View側ではShowMessageBoxConfirmationを受け取るActionを定義します。
<i:Interaction.Triggers>
    <my:InteractionRequestTrigger SourceObject="{Binding Path=AlertRequest}">
        <my1:ShowMessageBoxAction />
    </my:InteractionRequestTrigger>
</i:Interaction.Triggers>

実行

以上で完成です。実行して、右辺値に0を入れた状態で計算を行うと以下のようにメッセージボックスが表示されます。
WS000035.JPG

OKを押すと、入力が初期化されます。
WS000036.JPG

サンプルプログラムのダウンロード

サンプルプログラムは、ダウンロードページからダウンロードできます。

Last edited Jul 3, 2011 at 8:45 AM by okazuki, version 5

Comments

No comments yet.