Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Bind a WPF's control to an F# Option

Suppose I have a business object (without an AllowNullLiteralAttribute).

type Person(name: string) =
    member val Name = name
    override x.ToString() = name

And a view model, with the selected person optionally set.

type MainWindowModel() =

    let mutable selectedPerson: Person option = None
        :
    member val People = ObservableCollection<Person>()
    member x.SelectedPerson
        with get() = selectedPerson
        and set(v) =
            if selectedPerson <> v then
                selectedPerson <- v
                x.RaisePropertyChanged("SelectedPerson")

What is the best way to bind a WPF control's SelectedItem property to an F# option property (without using AllowNullLiteralAttribute)?

If I do this...

<StackPanel>
    <ListBox ItemsSource="{Binding People}"
             SelectedItem="{Binding SelectedPerson}"
             DisplayMemberPath="Name" />
    <TextBlock Text="{Binding SelectedPerson}" />
</StackPanel>

...it results in an error, Cannot convert 'George' from type 'Person' to type 'Microsoft.FSharp.Core.FSharpOption`1[Person]'

like image 783
Wallace Kelly Avatar asked Jul 09 '13 19:07

Wallace Kelly


2 Answers

The approach I am currently taking is to write my own IValueConverter.

open System
open System.Globalization
open System.Windows.Data

type OptionsTypeConverter() =

    // from http://stackoverflow.com/questions/6289761
    let (|SomeObj|_|) =
      let ty = typedefof<option<_>>
      fun (a:obj) ->
        let aty = a.GetType()
        let v = aty.GetProperty("Value")
        if aty.IsGenericType && aty.GetGenericTypeDefinition() = ty then
          if a = null then None
          else Some(v.GetValue(a, [| |]))
        else None

    interface IValueConverter with

        member x.Convert(value: obj, targetType: Type, parameter: obj, culture: CultureInfo) =
            match value with
            | null -> null
            | SomeObj(v) -> v
            | _ -> value

        member x.ConvertBack(value: obj, targetType: Type, parameter: obj, culture: CultureInfo) =
            match value with
            | null -> None :> obj
            | x -> Activator.CreateInstance(targetType, [| x |])

And then my XAML looks like this:

<StackPanel>
    <ListBox ItemsSource="{Binding People}"
             SelectedItem="{Binding SelectedPerson, Converter={StaticResource OptionsTypeConverter1}}"
             DisplayMemberPath="Name" />
    <TextBlock Text="{Binding SelectedPerson, Converter={StaticResource OptionsTypeConverter1}}" />
</StackPanel>

There may be a simpler way. This converter may already exist in the framework. There may be better implementations out there.

like image 160
Wallace Kelly Avatar answered Oct 16 '22 05:10

Wallace Kelly


I think using an IValueConverter is probably the cleanest approach.

Alternatively, you could use the fact that option type has Value member (which returns the value for Some or throws an exception for None). So, if you know that there is always a value, or if the exception does not break anything (I have not tried), you might be able to write:

<ListBox ItemsSource="{Binding People}"
         SelectedItem="{Binding SelectedPerson.Value}"
         DisplayMemberPath="Name" />

If the exception is a problem, then this past SO question suggests to use PriorityBinding:

<ListBox ItemsSource="{Binding People}" DisplayMemberPath="Name">
  <ListBox.SelectedItem>
    <PriorityBinding>
        <Binding Path="SelectedPerson.Value" />
        <Binding Source="{x:Null}" />  <!-- or some other default... -->
    </PriorityBinding>
  </ListBox.SelectedItem>
</ListBox>
like image 30
Tomas Petricek Avatar answered Oct 16 '22 05:10

Tomas Petricek