0

I have a class used as a parent to all my ViewModels. It contains a specific method used to call others methods, and show loading messages and message boxes on error (mainly):

public class BaseViewModel
{
    async public void Try(Func<Task> action, string errorMessage = null, string waitMessage = null)
    {
        try
        {
            if (waitMessage != null)
                ShowLoading(waitMessage);
            await action();
        }
        catch (Exception e)
        {
            ShowError(errorMessage, e);
        }
        finally
        {
            HideLoading();
        }
    }
}

It is asynchronous, so my ShowLoading can be animated and stuff like that.

  1. Is it correctly implemented?

It will always get anonymous (lambda) parameterless Tasks. My main issue is on how to actually construct these Tasks. Let's say I have a Command in a ViewModelBase's child, which call the following method when executed:

private void OnMyCommandExecute()
{
    Try(() =>
    {
        Thread.Sleep(5000);
    }, "error", "please wait");
}

It does not compile because Not all code paths return a value in lambda expression of type 'System.Func<System.Threading.Tasks.Task>'. Obvious, since we await this Func. Which leads me to the second question:

  1. What should I put inside my Try call in this example for it to work?

I tried some really ugly things, and I really hope the answer is way different, else it will be a pain of readability:

Try(async () =>
{
    return await Task.Factory.StartNew(() =>
    {
        SharePointService.Connect(Connection);
        IsConnected = true;
    });
}

It does not compile, but at this point, it's better like that. Error on return: Since 'System.Func<System.Threading.Tasks.Task>' is anasyncmethod that returns 'Task', a return keyword must not be followed by an object expression. Did you intend to return 'Task<T>'?

Servy
  • 202,030
  • 26
  • 332
  • 449
Kilazur
  • 3,089
  • 1
  • 22
  • 48
  • 1
    `Try` should almost certainly not be `async void`. It should be `async Task`. – Servy Jan 30 '15 at 15:23
  • @Servy I blindly believe you, I never had any experience with async/await. I would just like for `Try` to be usable as transparently as possible. – Kilazur Jan 30 '15 at 15:34
  • Your update still has `async void` and offloads everything to a `ThreadPool` thread (If that's the point it's fine, but you should be aware of that) – i3arnon Jan 30 '15 at 15:52
  • @I3arnon That can't be the point, I have no idea of what it is :p. What is the issue with this? I update my "solution" so you can see what I'm trying to do. `Try` is not necessarily calling asynchronous methods. – Kilazur Jan 30 '15 at 15:55
  • 1
    You should not be adding entirely new questions to your question, nor should you be adding your own answers to your question. If you want to ask an entirely new question, then ask a new question. – Servy Jan 30 '15 at 15:57
  • Your question *may* already be answered many times. I read it as "how to correctly implement fire-and-forget in UI handlers" and in that way it would be duplicate of http://stackoverflow.com/questions/12803012/fire-and-forget-with-async-vs-old-async-delegate. – Alexei Levenkov Jan 30 '15 at 16:14
  • Originally no, my question wasn't about fire and forget; it was about firing a method and showing a loading window until the method is completed. But, as my solution shows, I also want it to be able to fire and forget, but it's a secondary concern. – Kilazur Jan 30 '15 at 19:03
  • Related: [Exception handling inside “async void” WPF command handlers](http://stackoverflow.com/q/21231739/1768303). – noseratio Jan 30 '15 at 23:32

3 Answers3

3

Try accepts a method that returns a Task. In your first example you're providing a method that is void.

In your second example you're providing a method that returns a Task<Task>, but trying to use it in a context where a Task (non-generic) is expected.

If you want to use a non-async lambda, then just have that lambda return the Task that you want to use:

Try(()=>Task.Factory.StartNew(() =>
    {
        SharePointService.Connect(Connection);
        IsConnected = true;
    }));

If you want to use an async lambda, then you need to await the task without returning it:

Try(async () => await Task.Factory.StartNew(() =>
    {
        SharePointService.Connect(Connection);
        IsConnected = true;
    }));

Note that there's no real purpose to having an async lambda here. These two snippets will both perform identically, but the second adds some extra overhead in code bloat as well as a whole state machine that just isn't actually needed at runtime.

Servy
  • 202,030
  • 26
  • 332
  • 449
1

What should I put inside my Try call in this example for it to work?

You need to make that lambda expression async by adding (surprisingly) async:

Try(async () =>
{
    Thread.Sleep(5000);
}, "error", "please wait");

However, while this will enable you to create an async delegate there's nothing actually asynchronous about it (it blocks the calling thread with Thread.Sleep). If this is just an example then:

Try(async () =>
{
    await Task.Delay(5000);
}, "error", "please wait");

is a better one. If it isn't don't use async at all.

Is it correctly implemented?

Not really. async void should almost always be avoided (unless in a UI event handler). Use async Task instead and make sure to await the returned task in some point to ensure the operation completed without any exceptions.

i3arnon
  • 113,022
  • 33
  • 324
  • 344
0

In order for Try to be as transparent as possible, I ended up with this.

async public Task Try(Action action, string errorMessage = null, string waitMessage = null)
{
    try
    {
        if (waitMessage != null)
        {
            ShowLoading(waitMessage);
            await Task.Factory.StartNew(() => action());
        }
        else
            action();
    }
    catch (Exception e)
    {
        ShowError(errorMessage, e);
    }
    finally
    {
        HideLoading();
    }
}

Therefore, you don't have to work with Task.Factory.StartNew or async/await when you call it:

Try(() =>
{
    Thread.Sleep(5000);
}, "error", "please wait");
Kilazur
  • 3,089
  • 1
  • 22
  • 48
  • The method can now *only* be used to work with synchronous CPU bound operations, and it cannot work with any other types of asynchronously operations. Additionally, you're executing the method synchronously, not asynchronously, when `waitMessage` is `null`, which is almost certainly wrong. – Servy Jan 30 '15 at 16:40
  • No, that's the idea. If it's something you know is short in execution time, you probably don't want to have a big "Please wait" window flashing in your face. – Kilazur Jan 30 '15 at 18:30
  • But it may not be CPU bound work that is being performed. It might be IO, such as a database request, accessing a web service, file IO, etc. There are all sorts of things that one can do that will take a long time but not use the CPU in the process. CPU bound work is just *one* of *many* possibilities. – Servy Jan 30 '15 at 18:32
  • I don't understand; if I have a method that is going to write a 100Mo text file (for the IO example), the method itself won't end before the file is written, therefore my Task will be awaited until its end, isn't it? Same goes for a database request (which is, incidentally, what my program will do)? I mean, my `Try` works as it is in that extend. – Kilazur Jan 30 '15 at 19:06
  • Not if it's asynchronous. If it's asynchronous the method will end almost immediately, and simply return a `Task` that will be completed when the operation has finished. Creating a thread that's just going to sit there waiting for that operation to finish is just wasteful. It's what it means to have an actually asynchronous application, rather than a synchronous one. – Servy Jan 30 '15 at 19:08
  • Oooh, OK, you are right indeed. So yes, my `Try` is only intended for synchronous methods. Asynchronous ones (even though I don't have any in my API right now) would be handled another way. – Kilazur Jan 30 '15 at 19:10