Wallace Kelly

writes about F#, WPF, async, and the like.

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

MAGE.EXE and the Certificate Store

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...

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?

Read more...

Script for adding and removing the ClickOnce .deploy extension

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...

Calculating nice chart axis ranges

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...

WPF menus that go up

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...

Subscribe