テストタスクの例に関するGjallarhorn.Bindable.WPF(F#)の知識

ロシア語の記事では、 F#



WPF



併用するというトピックに少し注意が払われています。







今日は、 F#



ライブラリの1つを紹介します。これにより、このような開発が大幅に簡素化されます。







デモとして、知識をテストするジュニア開発者のポジションを申請者に与えるWPF



テストの割り当ての1つを見てみましょう。







タスク自体はこのように聞こえます







Students.xmlファイルで提供されるデータを使用してアプリケーションを開発する必要があります。



指定されたファイルには、学生に関する次の情報が含まれています:姓、名、年齢、性別。

もちろん、追加の推奨事項と実装制限がありますが、それらを完全にコピーすることはしません。 必要に応じて本文に本文が記載されます。完全版はこちらから入手できます







最初に、Visual Studio(またはその他の優先IDE)で.NET Frameworkの空のコンソールプロジェクトを作成します。 デバッグコンソールを表示したくない場合は、プロジェクト設定で出力のタイプを変更する必要があります。







メインステップ(メインデータ型(ユーザーインターフェイスに依存しないデータ型)の決定)を使用して、単純なアプリケーションの作業を開始します。







F#-(これまで)C#に類縁体を持たないタイプを使用します-レコード( Record



)およびマークされたユニオン( Discriminated Union



)。







 type Gender = |Male |Female type Student = {FirstName : string; LastName : string; Age : int; Gender : Gender}
      
      





おそらくここに滞在する価値があります。 割り当てが強調しなかったもう1つのポイントがあります-記録の一意性です。 理論的には、リストされているすべてのフィールドに一致する学生が存在する場合があります。







しかし、サンプルのxmlファイルを見ると







 <?xml version="1.0" encoding="utf-8"?> <Students> <Student Id="0"> <FirstName>Robert</FirstName> <Last>Jarman</Last> <Age>21</Age> <Gender>0</Gender> </Student> ... </Students>
      
      





次に、IDが属性として指定されているため、別のフィールドを追加するだけです。







 type Student = {ID:int; FirstName : string; LastName : string; Age : int; Gender : Gender; }
      
      





このようなアナウンスには1つの重大な欠点があります-IDの存在はレコードの一意性を保証しません。







つまり、理論的には、同じ識別子を持つレコードをいくつでも追加できます。







F#は、個々のフィールドへのアクセス修飾子の割り当てを許可しませんが、型に対して行うことを許可します。







自分自身を保護したい場合は、 Student



タイプをprivateにしたいという明示的な指示を置くことができます。







 type Student = private {ID:int; FirstName : string; LastName : string; Age : int; Gender : Gender; }
      
      





オブジェクトを作成するための補助関数を作成します







 let create firstname lastname age gender = let id = getNextId() { ID = id FirstName = firstname ... }
      
      





有効なフィールド値を制限するための要件を考慮してください。







名、姓、性別のフィールドは必須です。

年齢を負にすることはできず、範囲は[16、100]でなければなりません。

論理的な質問がすぐに発生します-入力されたパラメーターの正確性を確認する場所







Student



タイプが保護されている場合、以下を返すtryCreate



関数を作成できます。

None



/ Error<string>



またはSome<Student>



/ Ok<Student>



チェックの結果に応じて。







Result



、学生を登録する試みが失敗したことを知らせるだけでなく、問題が発生した場所を具体的に示すために必要な場合に使用するResult



便利です。

実装をわかりやすくするために、この関数のコードを記事に追加しません。







上記のアプローチを覚えておきましょうが、データ制御の責任をビューとモデルの間のリンクに割り当てます。







プレゼンテーションを担当する部分に移る前に、アプリケーションの主な機能のトピックを閉じます







  • 新しいアイテムを作成してリストに追加します。
  • リスト内のエントリを編集します。
  • リストから1つ以上のエントリを削除します。


すでに作成がわかっているので、さらにいくつかの機能を追加します







 //  ; let add xs student = student :: xs //   let remove students = List.filter (fun student -> Seq.contains student students |> not) //    ; let editFirstName firstname student = { student with FirstName = firstname } let editLastName lastname student = { student with LastName = lastname } let editAge age student = { student with Age = age} let editGender gender student = { student with Gender = gender } let editId student id = {student with ID = id} let edit student = List.map (fun st -> if st.ID = student.ID then student else st)
      
      





次のステップに進みます。







XMLでの読み取りと書き込み



F#でデータを処理するには、タイププロバイダーと呼ばれる優れたメカニズムがあります(データプロバイダー(for)とも呼ばれますが、将来の普及率が高いため、最初のオプションを使用します)。







特定の形式での便利な作業のための多くの実装があります。

このパートでは、 FSharp.Data



のみが必要です( FSharp.Data



ライブラリから)。

このパッケージをプロジェクトに追加します







Install-Package FSharp.Data









XmlProvider



タイプはXmlProvider



内で使用されるため、まだへのリンクが必要です。

System.Xml.Linq









 open FSharp.Data let [<Literal>] Sample = """ <Students> <Student Id="0"> <FirstName>Robert</FirstName> <Last>Jarman</Last> <Age>21</Age> <Gender>0</Gender> </Student> <Student Id="2"> <FirstName>Leona</FirstName> <Last>Menders</Last> <Age>20</Age> <Gender>1</Gender> </Student> </Students>""" type Students = XmlProvider<Sample>
      
      





使用されているサンプルでは、 Id



はファイルの{0, 1}



なく{0, 1}



{0, 2}



として示されているため、型はbool



ではなくint



として定義されています。







一般に、データソース形式の型をアプリケーションで受け入れられる型に変換するには、複雑なロジックが必要になる場合があります。 ただし、これらのデータ構造はほとんど同じであるため、 bool



型のbool



とラベル付きユニオンの対応を確立するために追加する関数は1つだけです。







 let fromBool = function | true -> Female | false -> Male
      
      





記録function | true -> Female | false -> Male



function | true -> Female | false -> Male



function | true -> Female | false -> Male



は次とまったく同じ意味

match x with



が、短い形式のみです。 この形式は、サンプルと簡単に比較するときに便利です。







次の部分も問題を引き起こしません-すべてが簡単で明確です。







 let toCoreStudent (student:Students.Student) = student.Gender |> fromBool |> create student.Id student.FirstName student.Last student.Age let readFromFile (path : string) = Students.Load path |> fun x -> x.Students |> Seq.map toCoreStudent
      
      





しかし、これだけではありません。ユーザーがリストにデータを追加できることを考慮する必要があります。したがって、ファイルからデータを抽出できるだけでなく、書き込みもできる必要があります。







変換が異なる方向に進むことを除いて、コードはまったく同じです。







 let toBool = function | Male -> false | Female -> true let fromCoreStudent (student:Student) = Students.Student(student.ID, student.FirstName, student.LastName, student.Age, toBool student.Gender) let toXmlStudents data = data |> Seq.map fromCoreStudent |> Seq.toArray |> Students.Students let writeToFile (path : string) data = let students = data |> toXmlStudents students.XElement.Save path
      
      





これまで検討されてきたすべてがWPFに依存せず、この部分に変更(たとえば、別の種類のインターフェイスへの)が生じることはないことを強調します。







通常の状況では、このようなコードをクラスライブラリに転送することは理にかなっていますが、特定の形式(.xml)に関連しない機能部分が小さすぎるため、完全に独立したモジュールを作成するために独立したプロジェクトは使用されません。







ユーザーインターフェース



私たちの目標は、プロジェクトを完全にF#でFsXAML



です。 FsXAML



、インターフェイスの問題については、 FsXAML



の助けをFsXAML



ます。

C#でパーツを書くことには何の問題もありませんが、それほど面白くないことに同意します。







FsXAML



は、 FsXAML



ファイルを便利に使用できるようにする型プロバイダーです。 NuGetを使用してプロジェクトに追加できます。







Install-Package FsXaml.Wpf









XamlReader



を超える利点は、 StackOverFlowの別の回答(英語)に記載されています。







その欠点の1つはドキュメントがないことです。そのため、コンバーターが存在することや、独自のものを作成するための便利なラッパーがあることをすぐに知ることはできません。







ここでは、年齢と検証エラーを正しく表示するためのコンバーターが必要です。







 type AgeToStringConverter() = inherit ConverterBase (fun value _ _ _ -> match value with | :? int -> value |> unbox |> AgeToStringConverter.ageToStr |> box | _ -> null ) static member ageToStr age = ...
      
      





ConverterBase



は、 ConverterBase



を作成するためのFsXAML



の基本クラスです。







アプリケーションの基本的な要件を繰り返しましょうが、今度は外観の観点からそれらを見てみましょう。







  • 既存のアイテムのリストを表示します。
  • 新しいアイテムを作成してリストに追加します。
  • リスト内のエントリを編集します。
  • リストから1つ以上のエントリを削除します。


アイテムのリストを表示するには、 ListView



を使用すると便利です。

メインウィンドウの生徒のテーブルに加えて、コントロールボタンもあります。

すべてが一緒になって、アプリケーションのメイン「ページ」を表すUserControl



を形成します。













他のページは予見されないため、ナビゲーションの使用は冗長なソリューションのように思えるかもしれません。







しかし、簡単な例を示すことは完全に適しています。







学生情報の編集と追加は、ダイアログボックスで行われます。







xaml



ファイルを作成したら、それらのタイプを作成する必要があります







 type App = XAML<"App.xaml"> type MainWin = XAML<"MainWindow.xaml"> type StudentsControl = XAML<"StudentsControl.xaml"> type StudentDialogBase = XAML<"StudentDialog.xaml"> type StudentDialog() = inherit StudentDialogBase() override this.CloseClick (_sender, _e) = this.Close()
      
      





3番目のバージョンから、イベント処理の基本的なサポートがFsXAML



追加されFsXAML



。 上記の例では、確認ボタンをクリックするとウィンドウが閉じます。







ジャラルホーン



モデルをビューに接続するには、非常に有望な新しいGjallarhorn.Bindableライブラリを使用します







Install-Package Gjallarhorn.Bindable.Wpf -Version 1.0.0-beta5









最新の利用可能なリリース 、まだベータ版です。







主な概念は、 Elm



アーキテクチャの一種の配置であり、メインのGjallarhornライブラリの上にあるwpf



詳細を考慮してwpf



wpf



バージョンに加えて、 XamarinForms



パッケージもあります







アプリケーションを作成するには、 Framework



モジュールのapplication



関数を使用すると便利です。







 Framework.application model update appComponent nav
      
      





個々のパーツ(モデル、それを更新するための機能、ビューおよびナビゲーターと通信するためのコンポーネント)を接続します









タスクに基づいて、アプリケーションはリストからアイテムを追加、編集、削除できるはずです。







アクションごとに、マークアップされたユニオンの名前付きバリアントの形式で表示される個別のメッセージを開始すると便利です。







 type AppMessages = |Add of Student |Edit of Student |Remove of Student seq |Save
      
      





既にリストされている機能に、ファイルを上書きする別の機能が追加されました。







リストに新しいエントリを追加するときは、一意の識別子の増分( ID



)を考慮する必要があります。







これを行うには、補助関数getId



記述して、リスト内の最大数の次のシリアル番号を返します。







更新機能には他の落とし穴はないので、最終的には次の形式を取ります







 let update message model = match message with |Add student -> model |> getId |> editId student |> add model |Edit newValue -> model |> edit newValue |Remove students -> model |> remove students |Save -> XmlWorker.writeToFile path model model
      
      





ナビゲーション状態を決定するために、ラベル付きユニオンも使用します







 type CollectionNav = | ViewStudents | AddStudent | EditStudent of Student
      
      





これでナビゲーションフレームワークの準備が整い、ナビゲーションメッセージとアプリケーションのメッセージの関連付けに進むことができます。







モデルの更新と同様に、状態の更新もupdate



関数で実装されます







 let updateNavigation (_ : ApplicationCore<Student list,_,_>) request : UIFactory<Student list,_,_> = match request with |ViewStudents -> Navigation.Page.fromComponent StudentsControl id appComponent id |AddStudent -> Navigation.Page.dialog StudentDialog (fun _ -> defaultStudent) studentComponent Add |EditStudent x -> Navigation.Page.dialog StudentDialog (fun _ -> x) studentComponent Edit
      
      





ライブラリで提供される2つの機能がここで使用されます。







Navigation.Page.fromComponent









 fromComponent : (makeElement : unit -> 'UIElement) -> (modelMapper : 'Model -> 'Submodel) -> (comp : IComponent<'Submodel, 'Nav, 'Submsg>) -> (msgMapper : 'Submsg -> 'Message) -> UIFactory<_,_,_>
      
      





そして







Navigation.Page.dialog









 dialog : (makeElement : unit -> 'Win) -> (modelMapper : 'Model -> 'Submodel) -> (comp : IComponent<'Submodel, 'Nav, 'Submsg>) -> (msgMapper : 'Submsg -> 'Message) = -> UIFactory<_,_,_>
      
      





それらは互いに非常に類似しているため、それらの説明を個別に示しません。







最初の引数は関数( makeElement



)であり、表示されるアイテム(ウィンドウ( Window



)またはコントロール( UIElement



))を設定します。







デザイナは本質的に同じ機能であるため、ほとんどの場合、目的の型を渡すだけで十分です。







2番目の引数( modelMapper



)は、最上位モデルから下位モデルへの変換関数です。







編集の場合は、興味のあるオブジェクト( Student



)をパラメーターとして取得するため、単純に渡すことができます。 追加するには、デフォルト値を渡します。







ViewStudents



の基底状態のViewStudents



メインコンポーネントのモデルはアプリケーションモデルになるため、変更する必要はなく、適用できます。

標準F# id



関数







次にコンポーネント( comp



)が来ます。これには、インターフェースと対話するために必要なすべてのバインディングが含まれています。







appComponent



コンポーネントのタイプはIComponent<Student list, CollectionNav, AppMessages>



で、 studentComponent



タイプはIComponent<Student, CollectionNav, Student>



です。







最後の引数( msgMapper



)は、メッセージの逆変換関数です。 studentComponent



コンポーネントは学生を返すため、ここでは正しいメッセージのみを渡すことができます。







最後の部分、つまりコンポーネント自体の検査に進むことができます。







Gjallarhorn.Bindable.WPF



データバインディングの場合は、 Bind



モジュールが責任を負い、このモジュールはいくつかのサブモジュールに分割されます。







メイン(ルート)API(最初のバージョン以降に追加された)はより安全ですが、 時には扱いにくくなり、2つ目は明示的ですExplicit



モジュールの機能)。







ここでは、両方のアプローチを示すために、 Explicit



使用して生徒情報とコアのImplicit



を取得します。







両方のコンポーネントは互いに独立していることに注意してください。







主なappComponent



から始めましょうappComponent









新しいAPIを使用するには、設定されているすべてのプロパティとコマンドを含む必要がある中間タイプを宣言する必要があります。







 type AppViewModel = { Students : Student list Add : VmCmd<CollectionNav> Edit : VmCmd<CollectionNav> Remove : VmCmd<AppMessages> RemoveAll : VmCmd<AppMessages> Save : VmCmd<AppMessages> }
      
      





コマンドは、単にメッセージを保存する特別なタイプのVmCmd



を使用して指定されます。

これは、チームの名前がクォータ(引用と呼ばれることもある)を使用して取得されるという事実につながります。







したがって、誤字による名前の不一致によるエラーのリスクを減らすのに役立つ「マジックライン」を回避します。







コンポーネントを設計する前に、 VM



タイプのベースインスタンス(デフォルト値)を作成する必要がありVM









 let appvd = { Students = [] Edit = Vm.cmd (CollectionNav.EditStudent defaultStudent) Add = Vm.cmd CollectionNav.AddStudent Remove = Vm.cmd (AppMessages.Remove [defaultStudent]) RemoveAll = Vm.cmd (AppMessages.Remove [defaultStudent]) Save = Vm.cmd AppMessages.Save }
      
      





リストが空の場合、最初にいくつかのボタンのロックを考慮する必要があるため、要素の存在に関する情報を含む関数を定義します。







 let hasStudents = List.isEmpty >> not
      
      





(原則として、 ListView



テンプレートを変更するために行われたように、データトリガー( DataTrigger



)を使用できます)。







次に、以下に示すように、すべてのバインディングのリストをComponent.create



関数に渡してコンポーネントを作成します







 let appComponent = let hasStudents = List.isEmpty >> not Component.create<Student list, CollectionNav, AppMessages> [ <@ appvd.Students @> |> Bind.oneWay id <@ appvd.Edit @> |> Bind.cmdParam EditStudent |> Bind.toNav <@ appvd.Add @> |> Bind.cmd |> Bind.toNav <@ appvd.Save @> |> Bind.cmd <@ appvd.Remove @> |> Bind.cmdParamIf hasStudents (Seq.singleton >> Remove) <@ appvd.RemoveAll @> |> Bind.cmdParamIf hasStudents (Seq.cast >> Remove) ]
      
      





Bind.oneWay



単方向バインディングを作成するように設計されています。







Bind.cmd



Bind.cmdParam



、およびBind.cmdParamIf



それぞれコマンド、パラメーター付きコマンド、および実行可能性の追加チェック付きコマンドを作成します。







いくつかの点に注意しましょう-2つの別個のメッセージを開始しないように(1つまたは複数の要素を削除するため)、送信されるオブジェクトは単位長のシーケンスを形成します。







 <@ appvd.Remove @> |> Bind.cmdParamIf hasStudents (Seq.singleton >> Remove)
      
      





残念ながらSelectedItems



は一般化されたコレクションではないため、ここでは追加の変換を適用する必要があります







 <@ appvd.RemoveAll @> |> Bind.cmdParamIf hasStudents (Seq.cast >> Remove)
      
      





ナビゲーションメッセージの送信は、 Bind.toNav



を使用して行われます。







ここでは、代わりに、コンポーネントを「クリーン」のままにする別のアプローチを使用できることに注意してください(ナビゲーションの副作用はありません)。







その本質は、 update



機能のすべての変更だけでなく、変更要求自体も実行することです。







私たちの場合、それらは学生に関する情報を追加および編集するためのリクエストです。







つまり、 Bind.toNav



呼び出しを削除するか、コンポーネントのBind.toNav



を介して直接Bind.toNav



する必要があります(明示的なAPIを使用する場合)。







このメソッドを例で見てみましょう。







必要な要求を反映するAddRequest



およびEditRequest



AppMessages



タイプに追加します。







 type AppMessages = |Add of Student |Edit of Student |Remove of Student seq |Save |AddRequest |EditRequest of Student
      
      





次に、次の部分がコンポーネントの追加と編集を担当するように、 AppViewModel



のタイプを書き換えます







 <@ appvd.Edit @> |> Bind.cmdParam EditRequest <@ appvd.Add @> |> Bind.cmd
      
      





次に、 update



機能の前に、ディスパッチャーを作成します







 let disp = Dispatcher<CollectionNav>()
      
      





メッセージを受信するときにリクエストを送信する関数で使用します







 |AddRequest -> AddStudent |> disp.Dispatch model |EditRequest st -> EditStudent st |> disp.Dispatch model
      
      





ディスパッチャー(この場合、ナビゲーションの管理を担当)を接続するには、 Framework.withNavigation



関数を使用します







 let app = Framework.application model update appComponent nav.Navigate |> Framework.withNavigation disp
      
      





はい、この場合、コードはより多くのスペースを占有しますが、コンポーネントは「クリーン」であることがわかります。







ここでは、 studentComponent



に渡しますが、ここでは完全には渡しません。主要部分のみを残します







 type StudentUpdate = |FirstName of string |LastName of string |Age of int |Gender of Gender let studentBind _ source model = let mstudent = model |> Signal.get |> Mutable.create [Female; Male] |> Signal.constant |> Bind.Explicit.oneWay source "Genders" let first = mstudent |> Signal.map (fun student -> student.FirstName) |> Bind.Explicit.twoWayValidated source "FirstName" (Validators.notNullOrWhitespace >> Validators.noSpaces) |> Observable.toMessage FirstName //    let upd msg = match msg with | FirstName name -> Mutable.step (editFirstName name) mstudent | LastName name -> Mutable.step (editLastName name) mstudent | Age age -> Mutable.step (editAge age) mstudent | Gender gender -> Mutable.step (editGender gender) mstudent [last; age; gender] |> List.fold Observable.merge first |> Observable.subscribe upd |> source.AddDisposable [ Bind.Explicit.createCommandChecked "SaveCommand" source.Valid source |> Observable.map(fun _ -> mstudent.Value) ] let studentComponent : IComponent<_,CollectionNav,_> = Component.fromExplicit studentBind
      
      





ここでは、データをリンクするときに、 Gjallarhorn



ライブラリの機能を使用して検証も実行されます(検証)。







パラメーターの有効性の状態を追跡するには、 source.Valid



信号がsource.Valid



ます。







Validators



モジュールには、簡単に相互に組み合わせることができるいくつかのヘルパー関数が含まれています。







たとえば、名前フィールドに空の文字列や空白を含めないようにします。

これを行うには、両方の機能に互換性がある







 Validators.notNullOrWhitespace >> Validators.noSpaces
      
      





標準機能では不十分な場合は、いつでも独自の機能を記述してチェックチェーンに追加できます。

これを行う方法、およびGjallarhorn



データ検証に関するその他の詳細については、 ドキュメントを参照してください。







Validators.noValidation



関数は、チェックが不要な場合に役立ちます。







その結果、生徒を追加するためのダイアログボックスは次のようになります。













注目のアプローチ







 mstudent |> Signal.map (fun student -> student.FirstName) |> Bind.Explicit.twoWayValidated source "FirstName" (Validators.notNullOrWhitespace >> Validators.noSpaces)
      
      





おそらく誰かが冗長すぎるようです。 しかし、解決策がありますBind.Explicit.memberToFromView



関数を使用して、記録を少し短くすることができます。







, :









, . F# ;)







.







F# , ( ). F# Slack ( F# Software Foundation)







Reed Copsey ( F#), API .







, , F# .







じゃあね!








All Articles