wpfmvvmf#fsharp.viewmodule

Closing a dialog with FSharp.ViewModule


In my previous question "Enabling dialog OK button with FSharp.ViewModule", I got to the point where a dialog's OK button was enabled only when the validators for the dialog's fields were true, and ViewModule's IsValid property became true. But I ran into a couple more problems after that:

1) Clicking on the OK button didn't close the dialog, even if I set IsDefault="true" in XAML.

2) When the OK button is clicked, sometimes I want to do more checks than provided by the ViewModule validators (eg, checking an email address). Then I want to stop the dialog from closing if this custom validation fails.

But I don't know how to do both when using F# and MVVM. First I tried putting the XAML into a C# project and the view model code in an F# library. And then I used the OK button's Click handler in code behind to close the window. This fixed 1), but not 2).

So this is my XAML:

<TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Text="{Binding Email, UpdateSourceTrigger=PropertyChanged}" />
<Button Content="OK" IsEnabled="{Binding IsValid}" IsDefault="true" Command="{Binding OkCommand}" 
    <!--Click="OnOK"--> />

And my view model - with a comment in the validate function to show what I want to do when the OK button is clicked:

let name = self.Factory.Backing( <@ self.Name @>, "", notNullOrWhitespace)
let email = self.Factory.Backing( <@ self.Email @>, "", notNullOrWhitespace)
let dialogResult = self.Factory.Backing( <@ self.DialogResult @>, false )

let isValidEmail (e:string) = e.Length >= 5

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.DialogResult
    with get() = dialogResult.Value
    and set value = dialogResult.Value <- value

member self.OkCommand = self.Factory.CommandSync(fun () ->
                        if not <| isValidEmail(email.Value) then
                            MessageBox.Show("Invalid Email") |> ignore
                        else
                            dialogResult.Value <- true
                        )

Solution

  • It's worth pointing out that MVVM and code-behind aren't best friends.

    The C# event handler you're referring to is located in the Window's code-behind file (i.e. partial class). Although code-behind is considered ok for view related logic, it's frowned upon by MVVM purists. So instead of specifying event handlers in XAML, MVVM prefers the use of Commands.

    Option A - Doing it in code-behind, being pragmatic.

    Note that FsXaml doesn't provide direct wiring of events (specifying handlers in XAML), but you can wire up the events yourself in code-behind.

    After you name a control in XAML, you can get a hold on it in the corresponding source file.

    UserDialog.xaml

    <Button x:Name="butt" ... >
    

    UserDialog.xaml.fs

    namespace Views
    
    open FsXaml
    
    type UserDialogBase = XAML<"UserDialog.xaml">
    
    type UserDialog() as dlg =
        inherit UserDialogBase()
    
        do dlg.butt.Click.Add( fun _ -> dlg.DialogResult <- System.Nullable(true) )
    

    Validation is best handled in the ViewModel, e.g. using custom validation for the email adress:

    Option B - You can follow MVVM pattern using a DialogCloser.

    First add a new source file at the top of your solution (Solution Explorer)

    DialogCloser.fs

    namespace Views
    
    open System.Windows
    
    type DialogCloser() =    
        static let dialogResultProperty =
            DependencyProperty.RegisterAttached("DialogResult", 
                typeof<bool>, typeof<DialogCloser>, 
                    new PropertyMetadata(DialogCloser.DialogResultChanged))
    
        static member SetDialogResult (a:DependencyObject) (value:string) = 
            a.SetValue(dialogResultProperty, value)
    
        static member DialogResultChanged 
            (a:DependencyObject) (e:DependencyPropertyChangedEventArgs) =
            match a with
            | :? Window as window
                -> window.DialogResult <- System.Nullable (e.NewValue :?> bool)
            | _ -> failwith "Not a Window"
    

    Say our solution is called WpfApp (referenced in XAML header), we can then implement the DialogCloser like this:

    UserDialog.xaml

    <Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:views="clr-namespace:Views;assembly=WpfApp"
        xmlns:fsxaml="http://github.com/fsprojects/FsXaml"
        views:DialogCloser.DialogResult="{Binding DialogResult}"
        >
    
            ...
    
    </Window>
    

    Now in the UserDialog's ViewModel you can hook up a Command and close the dialog by setting the dialogResult to true.

    member __.OkCommand = __.Factory.CommandSync(fun () ->
                             if not <| isValidEmail(email.Value) then
                                 System.Windows.MessageBox.Show ("...") |> ignore                       
                             else 
                                 // do stuff (e.g. saving data)
    
                                 ...
    
                                 // Terminator
                                 dialogResult.Value <- true 
                             )
    

    You could also skip the if / else clause and validate the email using custom validation.

    To wrap it up, you can call the dialog from MainViewModel using this helper function:

    UserDialog.xaml.fs

    namespace Views
    
    open FsXaml
    
    type UserDialog = XAML<"UserDialog.xaml">
    
    module UserDialogHandling =    
        /// Show dialog and return result
        let getResult() = 
            let win = UserDialog()
            match win.ShowDialog() with
            | nullable when nullable.HasValue
                -> nullable.Value
            | _ -> false
    

    Note that there's no 'code-behind' in this case (no code within the UserDialog type declaration).