Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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
                        )
like image 399
DenisV Avatar asked Dec 06 '16 08:12

DenisV


1 Answers

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:

  • SimpleMVVMDemo
  • FSharp.ViewModule.Validation.Validators.custom

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

like image 134
Funk Avatar answered Sep 30 '22 07:09

Funk