ロシア語の記事では、 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
個々のパーツ(モデル、それを更新するための機能、ビューおよびナビゲーターと通信するためのコンポーネント)を接続します
-
model
アプリケーションモデル(トップレベルモデル)-さらなる作業が残るメインデータ。 -
update:('message -> 'model -> 'model)
受信したメッセージ(message
)に応じてモデル(model
)を処理し、新しい値を返す関数。
タスクに基づいて、アプリケーションはリストからアイテムを追加、編集、削除できるはずです。
アクションごとに、マークアップされたユニオンの名前付きバリアントの形式で表示される個別のメッセージを開始すると便利です。
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# .
じゃあね!