Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using WPF and MVVM to edit F# records

Tags:

mvvm

wpf

xaml

f#

In my previous question, I asked how to use WPF and MVVM to make a closeable dialog for adding new Person records in F#. Now my next step is to make another dialog to edit those records. But I haven't worked out how to pass an existing record to the ViewModel and use it to populate the dialog's fields. I get exceptions because F# records are immutable, whereas the ViewModel seems to expect a mutable object.

I'll show you the code for my existing Add dialog - assume that the Edit dialog will look the same.

This is the Person record:

type Person = { Name: string; Email: string }

Here's the XAML for the Add dialog:

<Window 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:fsxaml="http://github.com/fsprojects/FsXaml"
    xmlns:local="clr-namespace:ViewModels;assembly=Test3"
    local:DialogCloser.DialogResult="{Binding DialogResult}"
    Title="Add Person" Height="150" Width="210" ResizeMode="NoResize" >
    <Window.DataContext>
        <local:PersonAddVM />
    </Window.DataContext>
    <StackPanel>
        <Grid FocusManager.FocusedElement="{Binding ElementName=_name}">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="50" />
                <ColumnDefinition Width="140" />
            </Grid.ColumnDefinitions>
            <Label Content="_Name" Target="_name" Grid.Row="0" Grid.Column="0" Margin="2" />
            <TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}" x:Name="_name" 
                     Grid.Row="0" Grid.Column="1" Margin="4" />
            <Label Content="_Email" Target="_email" Grid.Row="2" Grid.Column="0" Margin="2" HorizontalAlignment="Left" />
            <TextBox Text="{Binding Email, UpdateSourceTrigger=PropertyChanged}" x:Name="_email" 
                     Grid.Row="1" Grid.Column="1" Margin="4" />
        </Grid>
        <UniformGrid Rows="1" Columns="2" VerticalAlignment="Center" Margin="2,20,2,2" >
            <Button Content="OK" IsDefault="True" IsEnabled="{Binding IsValid}" Command="{Binding OkCmd}"
                    HorizontalAlignment="Right" Margin="6,0" Width="50" />
            <Button Content="Cancel" IsCancel="True" HorizontalAlignment="Left" Margin="6,0" Width="50" />
        </UniformGrid>
    </StackPanel>
</Window>

This is its (simplified) ViewModel:

type PersonAddVM() as self =
    inherit DialogVMBase()  // ViewModelBase with a DialogResult property

    let name = self.Factory.Backing( <@ self.Name @>, "", hasLengthAtLeast 4 )
    let email = self.Factory.Backing( <@ self.Email @>, "", hasLengthAtLeast 5 )

    let makePerson () = { Name = name.Value; Email = email.Value }

    member self.Name with get() = name.Value and set value = name.Value <- value
    member self.Email with get() = email.Value and set value = email.Value <- value

    member self.OkCmd = self.Factory.CommandSync( fun () ->
                            PersonCache.Add (makePerson())  // PersonCache is based on an Observable Dictionary
                            self.DialogResult <- true )

And the Add dialog is opened from a PersonList dialog that lists all the Persons and has this ViewModel code:

    type PersonListView = XAML<"PersonListView.xaml">
    type PersonAddView = XAML<"PersonAddView.xaml">

    module PersonViewHandling =
        let OpenList() = DialogHelper.OpenDialog (PersonListView())
        let OpenAdd() = DialogHelper.OpenDialog (PersonAddView())  // Calls ShowDialog on the view and handles the result

    type PersonListVM() as self =
        inherit DialogVMBase()  

        // I want to use the next 3 lines to access the Person selected in the list, 
        // to pass it to the Edit dialog
        let emptyPerson = { Name = ""; Email = "" }
        let selectedPerson = self.Factory.Backing( <@ self.SelectedPerson @>, emptyPerson )    
        member self.SelectedPerson with get() = selectedPerson.Value and set value = selectedPerson.Value <- value

        member self.AddCmd = self.Factory.CommandSync (fun _ -> PersonViewHandling.OpenAdd() |> ignore)

So how can I use this approach (or similar) to open an identical Edit dialog and use the SelectedPerson to populate its fields?

like image 976
DenisV Avatar asked Dec 14 '16 08:12

DenisV


1 Answers

Your current approach is reasonable, but I would recommend one change.

Instead of having the VM constructed from the View within your dialog, I would construct it manually. This would allow you to pass the selected person to the VM, which could then be edited:

type PersonAddVM (initial: Person) as self =
    // Then "fill in" based off the selection here...
    let name = self.Factory.Backing( <@ self.Name @>, initial.Name, hasLengthAtLeast 4 )
    // ...

    // Add an easy way to fetch the person:
    member this.EditedPerson with get () = makePerson ()

You can then remove the VM from the XAML, and construct and fetch it from the main VM:

  member self.AddCmd = 
      self.Factory.CommandSync 
         (fun _ -> 
              // Build this out manually:
              let dlg = PersonAddView()
              dlg.DataContext <- PersonAddVM(self.SelectedPerson)
              if  dlg.ShowDialog () = true then
                  let newPerson = dlg.Person
                  // Do something with newPerson here
         )

This also eliminates the need for the cache to exist, allowing you to directly push the selection, and fetch the edit (and use it).

If you want to keep the dialog "service", you could also easily wrap this into a method like you had, but that returned Person option from the dialog, ie:

module PersonViewHandling =
    let OpenAdd initial = 
        let vm = PersonAddVM(initial)
        let win = PersonAddView(DataContext = vm)
        if DialogHelper.OpenDialog (win) then
             Some win.Person
        else 
             None

You could then handle the result as a Person option instead of dealing with dialog results.

like image 108
Reed Copsey Avatar answered Nov 17 '22 16:11

Reed Copsey