Chapter 09

航空券予約

eagle
eagle
2021.12.26に更新

目標

ここまでの集大成として、航空券の予約を模した入力画面を作成します。

  • ユーザーは往復または片道を選択します。
  • 往復の場合は行きの日付と帰りの日付を入力します。
  • 片道の場合は行きの日付のみを入力し、帰りの日付は使用しないため入力できないようにします。
  • 入力が正しくない場合は「予約」ボタンを押すことができないようにします。
  • 予約ボタンを押した後はメッセージを表示します。


実行結果

アプリケーションとして見ると、予約処理はおろか出発地や目的地の入力欄すらありませんが、演習にはぴったりの題材です。
というのも、この程度の単純なアプリケーションとは言え、行き当たりばったりで実装をすると正しく動作させるのが比較的困難になるからです。

例えば、予約ボタンの有効・無効が切り替わるタイミングを考えましょう。
あり得るのは、往復・片道の変更タイミングや、行きと帰りの日付の変更タイミングです。
これらの変更に対して漏れなく有効・無効を切り替える必要があります。

それでは始めましょう。

フォームの追加

これまで作成してきたカウンターはそのままにして、新しいフォームを作成しましょう。
ソリューションエクスプローラーでDesignerプロジェクトを右クリックし、「追加」から「フォーム(Windowsフォーム)」を選択します。


フォームの追加

フォーム名をFlightBookingForm.csとし、追加します。


FlightBookingForm.csの追加

フォームデザイナを開くと真っ新な状態のフォームが表示されるはずです。


フォームデザイナ

F#コードの追加

次は先ほど作成したフォームをF#コードで扱うためのファイルを作成します。
ソリューションエクスプローラーでMainプロジェクトを右クリックし、「追加」から「新しい項目」を選択します。


新しい項目の追加

ソースファイル名をFlightBookingForm.fsとし、追加します。


FlightBookingForm.fsの追加

今作成したFlightBookingForm.fsファイルを開き、次のように書き換えます。

FlightBookingForm.fs
module FlightBookingForm

open System
open App.Designer

type Model = unit

type Msg = unit

let initialModel = ()

let update msg model = model

type View() as this =
    inherit FlightBookingForm()

    let mutable model = None

    let setModel nextModel = model <- Some nextModel

    do setModel initialModel

    let dispatch msg =
        model
        |> Option.map (update msg)
        |> Option.iter setModel

    let subscriptions: IDisposable array = [||]

    override this.Dispose(disposing) =
        if disposing then
            subscriptions
            |> Array.iter (fun subscription -> subscription.Dispose())

        base.Dispose disposing

次に、ソリューションエクスプローラーでMainプロジェクトをダブルクリックし、App.Main.fsprojファイルを開きます。
FlightBookingForm.fsMainForm.fsよりも先に定義されていればそのまま閉じて構いません。
もし順番が異なっていれば、次のようにApp.Main.fsprojの内容を編集します。

App.Main.fsproj
<Project Sdk="Microsoft.NET.Sdk">

	<PropertyGroup>
		<OutputType>WinExe</OutputType>
		<TargetFramework>net6.0-windows</TargetFramework>
	</PropertyGroup>

	<ItemGroup>
		<Compile Include="FlightBookingForm.fs" />
		<Compile Include="MainForm.fs" />
		<Compile Include="Program.fs" />
	</ItemGroup>

	<ItemGroup>
		<ProjectReference Include="..\Designer\App.Designer.csproj" />
	</ItemGroup>

</Project>

最後にProgram.fsファイルを変更し、表示するフォームを変更します。

Program.fs
  module Program
  
  open System
  open System.Drawing
  open System.Windows.Forms
  
  [<STAThread; EntryPoint>]
  let main argv =
      Application.EnableVisualStyles()
      Application.SetCompatibleTextRenderingDefault false
  
      new Font("メイリオ", 14.25f)
      |> Application.SetDefaultFont
  
-     new MainForm.View() |> Application.Run
+     new FlightBookingForm.View() |> Application.Run
      0

さて、アプリケーションを実行してみましょう。空のフォームが表示されれば成功です。


実行結果

画面の作成

それではフォームデザイナを用いてコントロールを配置していきましょう。


フォームデザイナ

往復、片道の入力欄にはRadioButtonコントロールを使いました。
日付の選択にはDateTimePickerコントロールを使いました。

コントロールの名前(Nameプロパティ)は次のようにしました。

  • 往復ラジオボタンradioButtonRoundtrip
  • 片道ラジオボタンradioButtonOneway
  • 行きラベルlabelGoThere
  • 行き日付dateTimePickerGoThere
  • 帰りラベルlabelGoBack
  • 帰り日付dateTimePickerGoBack
  • 予約ボタンbuttonFlightBooking

モデルの定義

画面が完成したので、早速ロジックとなるコードを書いていきましょう。

まずはこのアプリケーションの状態を定義します。
「この情報さえあれば画面を描画するのに十分」というデータを考えます。

  • 往復か片道か?
  • 行きの日付は何か?
  • 帰りの日付は何か?

この3つがあればとりあえず十分でしょうか。
それでは早速モデリングしていきましょう。

上から順にフィールド名をBookingKindGoThereDateGoBackDateとしてみました。
問題は型は何かということです。

type Model =
    { BookingKind: // ?
      GoThereDate: // ?
      GoBackDate:  // ?
    }

BookingKindはユーザーが往復を選択しているか片道を選択しているかを表すフィールドです。
これは安易にboolを選びがちですが、新たに判別共用体を定義するのがF#流でしょう。

type BookingKind =
    | Roundtrip
    | Oneway

GoThereDateGoBackDateには日付の情報を格納します。
日付をどのように表示するか?(例えば2000/01/012000年1月1日など)というのはモデルの主眼ではありません。
.NET 6からはDateOnlyという日付を格納するための構造体が導入されましたので、今回はこれを使いましょう。

なお、今回は行きと帰りのどちらも入力されていることを前提として、型は共にDateOnlyとします。
というのも、片道が選択されているときは帰りの日付は入力不可になるわけなので、場合によっては帰りの型にはoptionを付けるのがふさわしいこともあり得るのです。
ただし、入力不可ではありつつも画面には日付を表示するので今回はoptionは付けないことにします。

最終的に次のようにモデルを定義できます。

type BookingKind =
    | Roundtrip
    | Oneway

type Model =
    { BookingKind: BookingKind
      GoThereDate: DateOnly
      GoBackDate: DateOnly }

モデルの初期値も決める必要があります。
今回は往復を初期値とし、各日付は端末の日付とすることにします。

DateTime.Todayで端末の現在日付を取得できますが、これは歴史的事情からDateTime型であるので、明示的にDateOnly型へ変換する必要があります。
そのためにDateOnly.FromDateTimeメソッドを使用します。

let initialModel =
    { BookingKind = Roundtrip
      GoThereDate = DateTime.Today |> DateOnly.FromDateTime
      GoBackDate = DateTime.Today |> DateOnly.FromDateTime }

メッセージの定義

次に考えるべきはモデルを変更する方法、すなわちメッセージです。

  • ユーザーは往復か片道かを選択できる
  • ユーザーは行きの日付を選択できる
  • ユーザーは帰りの日付を選択できる
  • ユーザーは予約できる

とりあえずこの4つでしょうか。

最後の予約メッセージに関してはモデルのデータがあれば十分ですが、それ以外の3つのメッセージに関しては「どの値に変えるのか?」というモデル外の情報が必要です。

type Msg =
    | SetBookingKind of BookingKind
    | SetGoThereDate of DateOnly
    | SetGoBackDate of DateOnly
    | BookFlight

更新関数の定義

次に、現在のモデルとそれに加える変更を表すメッセージから、新たなモデルを計算するためのupdate関数を定義します。

SetXXX系のメッセージでは対応するモデルのフィールドを更新するだけですが、BookFlightメッセージは少し困ります。
とりあえず今のところはモデルに変更を加えないことにしましょう。

let update msg model =
    match msg with
    | SetBookingKind bookingKind -> { model with BookingKind = bookingKind }
    | SetGoThereDate goThereDate -> { model with GoThereDate = goThereDate }
    | SetGoBackDate goBackDate -> { model with GoBackDate = goBackDate }
    | BookFlight -> model

モデルの描画

それではモデルを画面へ反映させるための関数setState関数を作成しましょう。
ひとまず入力不可にする処理は後回しにし、単にモデルの各フィールドをフォームの各コントロールにセットするに留めます。

DateTimePickerコントロールでは時刻は選べませんが、これもやはり歴史的事情からValueプロパティの型はDateTimeになっています。
DateOnly型の値をDateTime型の値に変えるために、DateOnly.ToDateTimeメソッドを使用します。
時刻部分は0時0分0秒で良いので、TimeOnly.MinValueを使用しておきます。

let setModel nextModel =
    this.radioButtonRoundtrip.Checked <- nextModel.BookingKind = Roundtrip
    this.radioButtonOneway.Checked <- nextModel.BookingKind = Oneway
    this.dateTimePickerGoThere.Value <- nextModel.GoThereDate.ToDateTime TimeOnly.MinValue
    this.dateTimePickerGoBack.Value <- nextModel.GoBackDate.ToDateTime TimeOnly.MinValue

    model <- Some nextModel

イベントの購読

最後に、コントロールのイベントを購読し、メッセージと関連付けましょう。

基本的にはdispatch関数を使ってメッセージを発信するだけですが、予約ボタンをクリックしたときはそれに加えてメッセージボックスを表示しておきます。
このメッセージボックスはデバッグ目的のもので、次のように現在のアプリケーションのモデルを表示させます。


デバッグ用メッセージボックス

メッセージボックスを表示させる最も簡単な方法はMessageBox.Showメソッドを使うことです。
このメソッドはユーザーが選択したボタンに応じてDialogResultを返しますが、今回はこの値を無視します。

System.Windows.Forms.MessageBox.Show $"%A{model}" |> ignore

イベントの購読は次のようになります。

let subscriptions =
    [| this.radioButtonRoundtrip.CheckedChanged
        |> Observable.filter (fun e -> this.radioButtonRoundtrip.Checked)
        |> Observable.subscribe (fun e -> dispatch (SetBookingKind Roundtrip))

        this.radioButtonOneway.CheckedChanged
        |> Observable.filter (fun e -> this.radioButtonOneway.Checked)
        |> Observable.subscribe (fun e -> dispatch (SetBookingKind Oneway))

        this.dateTimePickerGoThere.ValueChanged
        |> Observable.subscribe
            (fun e ->
                dispatch (
                    SetGoThereDate(
                        this.dateTimePickerGoThere.Value
                        |> DateOnly.FromDateTime
                    )
                ))

        this.dateTimePickerGoBack.ValueChanged
        |> Observable.subscribe
            (fun e ->
                dispatch (
                    SetGoBackDate(
                        this.dateTimePickerGoBack.Value
                        |> DateOnly.FromDateTime
                    )
                ))

        this.buttonFlightBooking.Click
        |> Observable.subscribe
            (fun e ->
                dispatch BookFlight

                System.Windows.Forms.MessageBox.Show $"%A{model}"
                |> ignore) |]

ここまでのコード

ここまでで全体のコードは次のようになります。

FlightBookingForm.fs
module FlightBookingForm

open System
open App.Designer

type BookingKind =
    | Roundtrip
    | Oneway

type Model =
    { BookingKind: BookingKind
      GoThereDate: DateOnly
      GoBackDate: DateOnly }

type Msg =
    | SetBookingKind of BookingKind
    | SetGoThereDate of DateOnly
    | SetGoBackDate of DateOnly
    | BookFlight

let initialModel =
    { BookingKind = Roundtrip
      GoThereDate = DateTime.Today |> DateOnly.FromDateTime
      GoBackDate = DateTime.Today |> DateOnly.FromDateTime }

let update msg model =
    match msg with
    | SetBookingKind bookingKind -> { model with BookingKind = bookingKind }
    | SetGoThereDate goThereDate -> { model with GoThereDate = goThereDate }
    | SetGoBackDate goBackDate -> { model with GoBackDate = goBackDate }
    | BookFlight -> model

type View() as this =
    inherit FlightBookingForm()

    let mutable model = None

    let setModel nextModel =
        this.radioButtonRoundtrip.Checked <- nextModel.BookingKind = Roundtrip
        this.radioButtonOneway.Checked <- nextModel.BookingKind = Oneway
        this.dateTimePickerGoThere.Value <- nextModel.GoThereDate.ToDateTime TimeOnly.MinValue
        this.dateTimePickerGoBack.Value <- nextModel.GoBackDate.ToDateTime TimeOnly.MinValue

        model <- Some nextModel

    do setModel initialModel

    let dispatch msg =
        model
        |> Option.map (update msg)
        |> Option.iter setModel

    let subscriptions =
        [| this.radioButtonRoundtrip.CheckedChanged
           |> Observable.filter (fun e -> this.radioButtonRoundtrip.Checked)
           |> Observable.subscribe (fun e -> dispatch (SetBookingKind Roundtrip))

           this.radioButtonOneway.CheckedChanged
           |> Observable.filter (fun e -> this.radioButtonOneway.Checked)
           |> Observable.subscribe (fun e -> dispatch (SetBookingKind Oneway))

           this.dateTimePickerGoThere.ValueChanged
           |> Observable.subscribe
               (fun e ->
                   dispatch (
                       SetGoThereDate(
                           this.dateTimePickerGoThere.Value
                           |> DateOnly.FromDateTime
                       )
                   ))

           this.dateTimePickerGoBack.ValueChanged
           |> Observable.subscribe
               (fun e ->
                   dispatch (
                       SetGoBackDate(
                           this.dateTimePickerGoBack.Value
                           |> DateOnly.FromDateTime
                       )
                   ))

           this.buttonFlightBooking.Click
           |> Observable.subscribe
               (fun e ->
                   dispatch BookFlight

                   System.Windows.Forms.MessageBox.Show $"%A{model}"
                   |> ignore) |]

    override this.Dispose(disposing) =
        if disposing then
            subscriptions
            |> Array.iter (fun subscription -> subscription.Dispose())

        base.Dispose disposing

次のように、予約ボタンをクリックすると現在のアプリケーションのモデルが表示されます。
各コントロールに対する変更が適切にモデルへ反映されていることを確かめることができます。


実行結果

適切な入力制限

現在は片道を選択していても帰りの日付を入力できてしまいます。
帰りの日付は往復選択時にしか意味を成さないものなので、利便性の観点から片道が選択されているときは帰りの日付入力欄を操作できないようにしたほうが良いでしょう。
これを達成するためにはコントロールのEnabledプロパティをfalseに設定します。

この処理を追加する場所は次のうちどれでしょうか?

  • Model型の定義
  • Msg型の定義
  • モデルの初期値initialModel
  • 更新関数update
  • モデルの描画関数setModel
  • メッセージ発信関数dispatch
  • イベントの購読subscriptions

もちろんsetModel関数ですね。
帰りの日付入力欄は往復選択時のみ有効であれば良いので、次の1行を追加します。

this.dateTimePickerGoBack.Enabled <- nextModel.BookingKind = Roundtrip


実行結果

さて、次は往復選択時の入力制限を考えます。
行きの日付よりも前に帰るというのはおかしいです。
行きと帰りが同日であるのはあり得ますが、行き > 帰りはあり得ませんね。
そこで、そのような入力の時は予約ボタンを無効にしてしまいましょう。

予約ボタンが有効であれば良いのは、「片道が選択されている」または「行き <= 帰り」のときですね。

this.buttonFlightBooking.Enabled <-
    nextModel.BookingKind = Oneway
    || nextModel.GoThereDate <= nextModel.GoBackDate

setModel関数への変更をまとめると次のようになります。

  let setModel nextModel =
      this.radioButtonRoundtrip.Checked <- nextModel.BookingKind = Roundtrip
      this.radioButtonOneway.Checked <- nextModel.BookingKind = Oneway
      this.dateTimePickerGoThere.Value <- nextModel.GoThereDate.ToDateTime TimeOnly.MinValue
      this.dateTimePickerGoBack.Value <- nextModel.GoBackDate.ToDateTime TimeOnly.MinValue
+     this.dateTimePickerGoBack.Enabled <- nextModel.BookingKind = Roundtrip
+ 
+     this.buttonFlightBooking.Enabled <-
+         nextModel.BookingKind = Oneway
+         || nextModel.GoThereDate <= nextModel.GoBackDate
  
      model <- Some nextModel


実行結果

メッセージ表示

最後に予約処理を完成させて本章を終わりましょう。
現時点ではデバッグ用にモデルを表示しただけで終わっています。

MessageBoxの代わりに、.NET 5以降で追加されたTaskDialogを使用することでメッセージの細かいカスタマイズが可能になります。
具体的には、表示するメッセージボックスの位置をフォームの中心に合わせたり、メッセージに見出しを付けたりすることができます。
それでは、TaskDialogを使用してユーザーフレンドリーなメッセージを表示してみましょう。

まずはSystem.Windows.Forms開きます。

  open System
+ open System.Windows.Forms
  open App.Designer

MessageBoxの箇所を次のように書き換えます。

model
|> Option.map
    (fun model ->
        let goThereDateString =
            model.GoThereDate.ToString "yyyy'年'MM'月'dd'日'"

        let goBackDateString =
            model.GoBackDate.ToString "yyyy'年'MM'月'dd'日'"

        match model.BookingKind with
        | Roundtrip -> $"行き%s{goThereDateString}、帰り%s{goBackDateString}の往復券を予約しました。"
        | Oneway -> $"%s{goThereDateString}の片道券を予約しました。")
|> Option.iter
    (fun message ->
        TaskDialog.ShowDialog(
            this,
            TaskDialogPage(Caption = "予約完了", Heading = "ご予約ありがとうございます。", Text = message)
        )
|> ignore)

コード全体としては次のようになります。

FlightBookingForm.fs
module FlightBookingForm

open System
open System.Windows.Forms
open App.Designer

type BookingKind =
    | Roundtrip
    | Oneway

type Model =
    { BookingKind: BookingKind
      GoThereDate: DateOnly
      GoBackDate: DateOnly }

type Msg =
    | SetBookingKind of BookingKind
    | SetGoThereDate of DateOnly
    | SetGoBackDate of DateOnly
    | BookFlight

let initialModel =
    { BookingKind = Roundtrip
      GoThereDate = DateTime.Today |> DateOnly.FromDateTime
      GoBackDate = DateTime.Today |> DateOnly.FromDateTime }

let update msg model =
    match msg with
    | SetBookingKind bookingKind -> { model with BookingKind = bookingKind }
    | SetGoThereDate goThereDate -> { model with GoThereDate = goThereDate }
    | SetGoBackDate goBackDate -> { model with GoBackDate = goBackDate }
    | BookFlight -> model

type View() as this =
    inherit FlightBookingForm()

    let mutable model = None

    let setModel nextModel =
        this.radioButtonRoundtrip.Checked <- nextModel.BookingKind = Roundtrip
        this.radioButtonOneway.Checked <- nextModel.BookingKind = Oneway
        this.dateTimePickerGoThere.Value <- nextModel.GoThereDate.ToDateTime TimeOnly.MinValue
        this.dateTimePickerGoBack.Value <- nextModel.GoBackDate.ToDateTime TimeOnly.MinValue
        this.dateTimePickerGoBack.Enabled <- nextModel.BookingKind = Roundtrip

        this.buttonFlightBooking.Enabled <-
            nextModel.BookingKind = Oneway
            || nextModel.GoThereDate <= nextModel.GoBackDate

        model <- Some nextModel

    do setModel initialModel

    let dispatch msg =
        model
        |> Option.map (update msg)
        |> Option.iter setModel

    let subscriptions =
        [| this.radioButtonRoundtrip.CheckedChanged
           |> Observable.filter (fun e -> this.radioButtonRoundtrip.Checked)
           |> Observable.subscribe (fun e -> dispatch (SetBookingKind Roundtrip))

           this.radioButtonOneway.CheckedChanged
           |> Observable.filter (fun e -> this.radioButtonOneway.Checked)
           |> Observable.subscribe (fun e -> dispatch (SetBookingKind Oneway))

           this.dateTimePickerGoThere.ValueChanged
           |> Observable.subscribe
               (fun e ->
                   dispatch (
                       SetGoThereDate(
                           this.dateTimePickerGoThere.Value
                           |> DateOnly.FromDateTime
                       )
                   ))

           this.dateTimePickerGoBack.ValueChanged
           |> Observable.subscribe
               (fun e ->
                   dispatch (
                       SetGoBackDate(
                           this.dateTimePickerGoBack.Value
                           |> DateOnly.FromDateTime
                       )
                   ))

           this.buttonFlightBooking.Click
           |> Observable.subscribe
               (fun e ->
                   dispatch BookFlight

                   model
                   |> Option.map
                       (fun model ->
                           let goThereDateString =
                               model.GoThereDate.ToString "yyyy'年'MM'月'dd'日'"

                           let goBackDateString =
                               model.GoBackDate.ToString "yyyy'年'MM'月'dd'日'"

                           match model.BookingKind with
                           | Roundtrip -> $"行き%s{goThereDateString}、帰り%s{goBackDateString}の往復券を予約しました。"
                           | Oneway -> $"%s{goThereDateString}の片道券を予約しました。")
                   |> Option.iter
                       (fun message ->
                           TaskDialog.ShowDialog(
                               this,
                               TaskDialogPage(Caption = "予約完了", Heading = "ご予約ありがとうございます。", Text = message)
                           )
                           |> ignore)) |]

    override this.Dispose(disposing) =
        if disposing then
            subscriptions
            |> Array.iter (fun subscription -> subscription.Dispose())

        base.Dispose disposing

以上により、本章の冒頭で紹介したようなアプリケーションが完成です。


実行結果

テスト容易性の担保

最後に、現時点でのModelMsginitialModelupdateのテスト容易性を考えましょう。
これらはF#インタラクティブで容易にテストできたことを思い出してください。

この中で、initialModelに関してはDateTime.Todayを使用しているため、これを実行するときの環境に応じて処理結果が変わってしまいます。
これはテスト容易性の観点からは好ましくありません。

そこで、この依存性を外部から注入することにしましょう。
具体的には、モデルの初期値を単なる値として定義するのではなく、モデルの初期値を返すような関数initを定義します。

let init today =
    { BookingKind = Roundtrip
      GoThereDate = today
      GoBackDate = today }

モデルの初期値はinit関数に必要な依存(今回は端末の日付)を与えることで得られます。

let initialModel =
    DateTime.Today |> DateOnly.FromDateTime |> init

最終的なコードは次のようになります。

FlightBookingForm.fs
module FlightBookingForm

open System
open System.Windows.Forms
open App.Designer

type BookingKind =
    | Roundtrip
    | Oneway

type Model =
    { BookingKind: BookingKind
      GoThereDate: DateOnly
      GoBackDate: DateOnly }

type Msg =
    | SetBookingKind of BookingKind
    | SetGoThereDate of DateOnly
    | SetGoBackDate of DateOnly
    | BookFlight

let init today =
    { BookingKind = Roundtrip
      GoThereDate = today
      GoBackDate = today }

let update msg model =
    match msg with
    | SetBookingKind bookingKind -> { model with BookingKind = bookingKind }
    | SetGoThereDate goThereDate -> { model with GoThereDate = goThereDate }
    | SetGoBackDate goBackDate -> { model with GoBackDate = goBackDate }
    | BookFlight -> model

type View() as this =
    inherit FlightBookingForm()

    let mutable model = None

    let setModel nextModel =
        this.radioButtonRoundtrip.Checked <- nextModel.BookingKind = Roundtrip
        this.radioButtonOneway.Checked <- nextModel.BookingKind = Oneway
        this.dateTimePickerGoThere.Value <- nextModel.GoThereDate.ToDateTime TimeOnly.MinValue
        this.dateTimePickerGoBack.Value <- nextModel.GoBackDate.ToDateTime TimeOnly.MinValue
        this.dateTimePickerGoBack.Enabled <- nextModel.BookingKind = Roundtrip

        this.buttonFlightBooking.Enabled <-
            nextModel.BookingKind = Oneway
            || nextModel.GoThereDate <= nextModel.GoBackDate

        model <- Some nextModel

    do
        let initialModel =
            DateTime.Today |> DateOnly.FromDateTime |> init

        setModel initialModel

    let dispatch msg =
        model
        |> Option.map (update msg)
        |> Option.iter setModel

    let subscriptions =
        [| this.radioButtonRoundtrip.CheckedChanged
           |> Observable.filter (fun e -> this.radioButtonRoundtrip.Checked)
           |> Observable.subscribe (fun e -> dispatch (SetBookingKind Roundtrip))

           this.radioButtonOneway.CheckedChanged
           |> Observable.filter (fun e -> this.radioButtonOneway.Checked)
           |> Observable.subscribe (fun e -> dispatch (SetBookingKind Oneway))

           this.dateTimePickerGoThere.ValueChanged
           |> Observable.subscribe
               (fun e ->
                   dispatch (
                       SetGoThereDate(
                           this.dateTimePickerGoThere.Value
                           |> DateOnly.FromDateTime
                       )
                   ))

           this.dateTimePickerGoBack.ValueChanged
           |> Observable.subscribe
               (fun e ->
                   dispatch (
                       SetGoBackDate(
                           this.dateTimePickerGoBack.Value
                           |> DateOnly.FromDateTime
                       )
                   ))

           this.buttonFlightBooking.Click
           |> Observable.subscribe
               (fun e ->
                   dispatch BookFlight

                   model
                   |> Option.map
                       (fun model ->
                           let goThereDateString =
                               model.GoThereDate.ToString "yyyy'年'MM'月'dd'日'"

                           let goBackDateString =
                               model.GoBackDate.ToString "yyyy'年'MM'月'dd'日'"

                           match model.BookingKind with
                           | Roundtrip -> $"行き%s{goThereDateString}、帰り%s{goBackDateString}の往復券を予約しました。"
                           | Oneway -> $"%s{goThereDateString}の片道券を予約しました。")
                   |> Option.iter
                       (fun message ->
                           TaskDialog.ShowDialog(
                               this,
                               TaskDialogPage(Caption = "予約完了", Heading = "ご予約ありがとうございます。", Text = message)
                           )
                           |> ignore)) |]

    override this.Dispose(disposing) =
        if disposing then
            subscriptions
            |> Array.iter (fun subscription -> subscription.Dispose())

        base.Dispose disposing

追加課題

現時点ではパフォーマンスに問題はありませんが、setModelでのコントロールの更新処理を必要最低限にするのも良い練習問題になるでしょう。