F# async/await in a viewmodel

295 Views Asked by At

My ViewModel in F#

I'm trying to use F# instead of C# to implement my ViewModel. I'm following this article (btw, is there something newer or any better suggestion?).

So let's say that I have my view model base implementation (MVVM.ViewModel, it's in C# but I can reference it from F#) and a simple Status property.

namespace FuncViewModel
open MVVM.ViewModel
open System

    type MyFuncViewModel() = 
        inherit ViewModelBase()

        let mutable status=""

        member this.RunSetStatus() =
            status <- "Reset @" + DateTime.Now.ToString "yy.MM.dd hh:mm:ss"
            base.OnPropertyChanged("Status")

        member this.SetStatus = new DelegateCommand(fun _ -> this.RunSetStatus() )


    member this.Status 
        with get() =
            status
        and set(value) =
             status <- value
             base.OnPropertyChanged(fun () -> this.Status)

Everything works as expected, so far so good (but let me know if you spot any conceptual error or if you find a more idiomatic version for the above code)

Introducing the async/await pattern

This is where I'm going wrong: I know how to do that in C# but I'm not good at it in F#.

I've tried with the following.

member this.RunSetStatus() =
    status <- "Start resetting @" + DateTime.Now.ToString "yy.MM.dd hh:mm:ss"
    base.OnPropertyChanged("Status")
    let task = async {

        do! Async.Sleep (30 * 1000) 

    }
    Async.StartImmediate(task)
    status <- "Reset done @" + DateTime.Now.ToString "yy.MM.dd hh:mm:ss"
    base.OnPropertyChanged("Status")

The problem is that - when I run the full WPF application - I can't see the expected delay: the final status goes straight to the output.

If I change the above Async.StartImmediate(task) to Async.RunSynchronously(task), of course I see the delay in progress, but the application gets freezed, so this is not what I want.

If I rearrange it as

member this.RunSetStatus() =
    status <- "Start resetting @" + DateTime.Now.ToString "yy.MM.dd hh:mm:ss"
    base.OnPropertyChanged("Status")
    let task = async {

        do! Async.Sleep (30 * 1000) 

        status <- "Reset done @" + DateTime.Now.ToString "yy.MM.dd hh:mm:ss"
        base.OnPropertyChanged("Status")

    }
    Async.StartImmediate(task)

I get an error

The member or object constructor 'OnPropertyChanged' is not accessible. Private members may only be accessed from within the declaring type. Protected members may only be accessed from an extending type and cannot be accessed from inner lambda expressions.

Edit (Continuation)

Finally, I've also tried this

member this.RunSetStatus() =
    status <- "Start resetting @" + DateTime.Now.ToString "yy.MM.dd hh:mm:ss"
    base.OnPropertyChanged("Status")
    let task = async {

        do! Async.Sleep (30 * 1000) 

    }
    Async.StartWithContinuations(task, 
        (fun _ -> this.Status <- "Reset done @" + DateTime.Now.ToString "yy.MM.dd hh:mm:ss"),
        (fun _ -> this.Status <- "Operation failed."),
        (fun _ -> this.Status <- "Operation canceled."))

but the application crashes with an ArgumentException

Stack Trace

Application: MyFuncWPF.exe Framework Version: v4.0.30319 Description: The process was terminated due to an unhandled exception. Exception Info: System.ArgumentException Stack: at MVVM.ViewModel.ViewModelBase.OnPropertyChanged[System.__Canon, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089] at FuncViewModel.MyFuncViewModel.set_Status(System.String) at [email protected](Microsoft.FSharp.Core.Unit) at Microsoft.FSharp.Control.CancellationTokenOps+StartWithContinuations@1274[[System.__Canon, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]].Invoke(System.__Canon) at .$Control.loop@430-52(Microsoft.FSharp.Control.Trampoline, Microsoft.FSharp.Core.FSharpFunc2<Microsoft.FSharp.Core.Unit,Microsoft.FSharp.Control.FakeUnitValue>) at Microsoft.FSharp.Control.Trampoline.ExecuteAction(Microsoft.FSharp.Core.FSharpFunc2) at [email protected](System.Object) at System.Windows.Threading.ExceptionWrapper.InternalRealCall(System.Delegate, System.Object

Edit 2 - found the issue

I had to use the following - more simple - overload of OnPropertyChanged (they are both implemented and working in C# as per this source code)

    member this.Status 
        with get() =
            status
        and set(value) =
             status <- value
             base.OnPropertyChanged("Status")
1

There are 1 best solutions below

2
On BEST ANSWER

The reason of the exception is that the function of F# cannot be represented as MemberExpression:

    protected virtual void OnPropertyChanged<T>(Expression<Func<T>> selectorExpression)
    {
        if (selectorExpression == null)
            throw new ArgumentNullException("selectorExpression");
        MemberExpression body = selectorExpression.Body as MemberExpression;
        if (body == null)
            throw new ArgumentException("The body must be a member expression");
        OnPropertyChanged(body.Member.Name);
    }

In the debugger you'll see that the exception you get is actually - "The body must be a member expression".

Your first code :

member this.RunSetStatus() =
    status <- "Reset @" + DateTime.Now.ToString "yy.MM.dd hh:mm:ss"
    base.OnPropertyChanged("Status")

works since you are not using the setter of property Status.

So you need to use a different overload.