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?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With