WPF ViewModel of an F# Record
How does one expose an F# record in a WPF view model? My F# code uses immutable records. But, I need to bind those records to a WPF user interface in a view model. What is the right way to do that?
Background
I have an F# record that looks something like this:
1:
2:
3:
|
type MyOptions =
{ FreezePanes : bool
ShowHeaders : bool }
|
I want to bind this to an WPF user interface with two check boxes.
I do not want my view model to look like this:
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
|
type MyOptionsViewModelVerbose(opts : MyOptions) =
let propertyChangedEvent = Event<PropertyChangedEventHandler, PropertyChangedEventArgs>()
let mutable freezePanes = opts.FreezePanes
let mutable showHeaders = opts.ShowHeaders
member x.FreezePanes
with get() = freezePanes
and set(v) =
if v <> freezePanes then
freezePanes <- v
propertyChangedEvent.Trigger(x, PropertyChangedEventArgs("FreezePanes"))
member x.ShowHeaders
with get() = showHeaders
and set(v) =
if v <> showHeaders then
showHeaders <- v
propertyChangedEvent.Trigger(x, PropertyChangedEventArgs("ShowHeaders"))
interface INotifyPropertyChanged with
[<CLIEvent>]
member x.PropertyChanged : IEvent<PropertyChangedEventHandler, PropertyChangedEventArgs> =
propertyChangedEvent.Publish
|
That is too much code! And in my real project, my record has a dozen members.
Proposed Solution
Instead, bind directly to a copy of the record.
First, decorate the record definition to allow WPF to modify the immutable type. That's the purpose of the [<CLIMutable>]
attribute. Also add Default
and Clone()
members to the record. The reason for these two members becomes apparent in the next step.
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
|
[<CLIMutable>]
type MyOptions =
{ FreezePanes : bool
ShowHeaders : bool }
static member Default =
{ FreezePanes = false
ShowHeaders = false }
member x.Clone() = { x with FreezePanes = x.FreezePanes }
|
Define a ViewModel class which publicly exposes a copy of the record value.
1:
2:
3:
|
type MyOptionsViewModel(options : MyOptions) =
new() = MyOptionsViewModel(MyOptions.Default)
member val Options = options.Clone()
|
The opts
record is cloned because WPF is going to be changing the values of the record. We do not want to break the immutability assumptions that the calling code makes about the opts
record.
Sample Usage
Here is some code to test this approach.
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
|
let winXaml = "
<Window xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"
xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"
xmlns:mbo=\"clr-namespace:MyBusinessObjects;assembly=MyOptionsTest\">
<Window.DataContext>
<mbo:MyOptionsViewModel />
</Window.DataContext>
<StackPanel>
<CheckBox IsChecked=\"{Binding Options.FreezePanes}\"
Content=\"Freeze Headers\" />
<CheckBox IsChecked=\"{Binding Options.ShowHeaders}\"
Content=\"Show Headers\" />
</StackPanel>
</Window>"
[<EntryPoint; STAThread>]
let main argv =
let win = XamlReader.Parse(winXaml) :?> Window
let options =
{ FreezePanes = true
ShowHeaders = true }
win.DataContext <- MyOptionsViewModel(options)
let app = new Application()
app.Run(win) |> ignore
let vm = win.DataContext :?> MyOptionsViewModel
printfn "Before: %A" options
printfn "After: %A" vm.Options
Console.ReadKey(false) |> ignore
0
|
Final note
Keep in mind that you may need to think about your own implementation of Clone()
. It may need to be a deep copy, depending on what the user interface could potentially modify.
type MyOptions =
{FreezePanes: bool;
ShowHeaders: bool;}
Full name: wpfviewmodelofanfrecord.MyOptions
MyOptions.FreezePanes: bool
type bool = System.Boolean
Full name: Microsoft.FSharp.Core.bool
MyOptions.ShowHeaders: bool
Multiple items
type MyOptionsViewModelVerbose =
interface obj
new : opts:MyOptions -> MyOptionsViewModelVerbose
override add_PropertyChanged : obj -> obj
member FreezePanes : bool
override PropertyChanged : obj
member ShowHeaders : bool
override remove_PropertyChanged : obj -> obj
member FreezePanes : bool with set
member ShowHeaders : bool with set
Full name: wpfviewmodelofanfrecord.MyOptionsViewModelVerbose
--------------------
new : opts:MyOptions -> MyOptionsViewModelVerbose
val opts : MyOptions
val propertyChangedEvent : obj
Multiple items
module Event
from Microsoft.FSharp.Control
--------------------
type Event<'T> =
new : unit -> Event<'T>
member Trigger : arg:'T -> unit
member Publish : IEvent<'T>
Full name: Microsoft.FSharp.Control.Event<_>
--------------------
type Event<'Delegate,'Args (requires delegate and 'Delegate :> Delegate)> =
new : unit -> Event<'Delegate,'Args>
member Trigger : sender:obj * args:'Args -> unit
member Publish : IEvent<'Delegate,'Args>
Full name: Microsoft.FSharp.Control.Event<_,_>
--------------------
new : unit -> Event<'T>
--------------------
new : unit -> Event<'Delegate,'Args>
val mutable freezePanes : bool
val mutable showHeaders : bool
val x : MyOptionsViewModelVerbose
member MyOptionsViewModelVerbose.FreezePanes : bool with set
Full name: wpfviewmodelofanfrecord.MyOptionsViewModelVerbose.FreezePanes
val set : elements:seq<'T> -> Set<'T> (requires comparison)
Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.set
val v : bool
member MyOptionsViewModelVerbose.ShowHeaders : bool with set
Full name: wpfviewmodelofanfrecord.MyOptionsViewModelVerbose.ShowHeaders
Multiple items
type CLIEventAttribute =
inherit Attribute
new : unit -> CLIEventAttribute
Full name: Microsoft.FSharp.Core.CLIEventAttribute
--------------------
new : unit -> CLIEventAttribute
override MyOptionsViewModelVerbose.PropertyChanged : obj
Full name: wpfviewmodelofanfrecord.MyOptionsViewModelVerbose.PropertyChanged
type IEvent<'T> = IEvent<Handler<'T>,'T>
Full name: Microsoft.FSharp.Control.IEvent<_>
Multiple items
type CLIMutableAttribute =
inherit Attribute
new : unit -> CLIMutableAttribute
Full name: Microsoft.FSharp.Core.CLIMutableAttribute
--------------------
new : unit -> CLIMutableAttribute
Multiple items
type MyOptionsViewModel =
new : unit -> MyOptionsViewModel
new : options:MyOptions -> MyOptionsViewModel
member Options : obj
Full name: wpfviewmodelofanfrecord.MyOptionsViewModel
--------------------
new : unit -> MyOptionsViewModel
new : options:MyOptions -> MyOptionsViewModel
val options : MyOptions
val winXaml : string
Full name: wpfviewmodelofanfrecord.winXaml
Multiple items
type EntryPointAttribute =
inherit Attribute
new : unit -> EntryPointAttribute
Full name: Microsoft.FSharp.Core.EntryPointAttribute
--------------------
new : unit -> EntryPointAttribute
val main : argv:string [] -> int
Full name: wpfviewmodelofanfrecord.main
val argv : string []
val win : obj
val app : obj
val ignore : value:'T -> unit
Full name: Microsoft.FSharp.Core.Operators.ignore
val vm : MyOptionsViewModel
val printfn : format:Printf.TextWriterFormat<'T> -> 'T
Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.printfn
property MyOptionsViewModel.Options: obj
I need to sign a ClickOnce manifest using mage.exe
.
The certificate is in the Windows Certificate Store.
How do I do that?
Read more...
How does one expose an F# record in a WPF view model? My F# code uses immutable records. But, I need to bind those records to a WPF user interface in a view model. What is the right way to do that?
Read more...
I was recently debugging a ClickOnce deployment. I needed a way to quickly add and remove the *.deploy
extension from all the files in a deployment folder. Well, not all the files. The *.manifest
file is treated differently.
Read more...
I was working on a project that required calculating nice ranges for chart axes. I found a useful discussion here.
Here is what I used in C# and so far it is working adequately.
Read more...
I needed a menu docked to the bottom of an application's window. Docking to the bottom of the window is no problem. However, it did not work well, because by default, WPF menus drop down. I needed it to drop up.
Read more...