I'm Andrew Hoefling, and I work for FileOnQ as a Lead Software Engineer building mobile technologies for Government, Financial and First Responders using Xamarin. 

 

Why You Shouldn't Use async void With Inheritance


C# developers experienced with WPF, UWP, Xamarin.Forms, Uno Platform and anything that involes the ICommand interface are familiar with async void. A quick trick to call Task based asynchronous methods that interact with some type of User Interface interaction. We all know this is bad, but still implement code this way because it doesn't cause immediate problems.

STOP IT! async void is bad

Basic Command Usage

When using a basic ICommand implementation it is easy to use add the async keyword to the method signature to allow your code to run asynchronously. To get started let's analyze a simple MainViewModel in a MVVM (Model-View-ViewModel) Application.

public class MainViewModel
{
    public MainViewModel()
    {
        MyCommand = new Command(OnMyCommand);
    }

    public ICommand MyCommand { get; }

    void OnMyCommand()
    {
        Debug.WriteLine("MyCommand Executed!");
    }
}

Whenever the user interface triggers the MyCommand it will print to the output window. This will work great and serve your application well for almost anything you want to do. Until your OnMyCommand method needs to invoke an asynchronous method.

Enter async void

Suppose we have a LongRunningJob method that returns a Task and this method takes 5000ms to execute. It is easy enough to just add async void to the method signature and let it work.

public class MainViewModel
{
    public MainViewModel()
    {
        MyCommand = new Command(OnMyCommand);
    }

    public ICommand MyCommand { get; }

    async void OnMyCommand()
    {
        Debug.WriteLine("MyCommand Executed!");
        await LongRunningJobAsync();
    }

    // new method - LongRunningJobAsync
    public async Task LongRunningJobAsync()
    {
        // Simulate work that takes 5000ms to complete
        await Task.Delay(5000);

        Debug.WriteLine("LongRunningJob Completed!");
    }
}

This will work, but it is not recommended.

The method does not return a Task and the other methods on the call stack will lose the state as the Task executes and completes. Some of the other problems you will run into

  • Timing Issues & Race Conditions
  • Exceptions are lost

Real World Problem - async void

A real world production problem I have personally ran into dealt with a small inheritance tree where we have 2 classes. A ViewModelBase which defines the ICommand and an abstract method that is invoked when the command fires. There is also a child class called MainViewModel which implements the method.

ViewModelBase

public class ViewModelBase
{
    public MainViewModel()
    {
        MyCommand = new Command(OnMyCommand);
    }
 
    public ICommand MyCommand { get; }
 
    void OnMyCommand()
    {
        LongRunningJob();
    }
 
    protected abstract void LongRunningJob();
}

MainViewModel

public class MainViewModel : ViewModelBase
{
    protected override async void LongRunningJob()
    {
        // Simulate work
        await Task.Delay(5000);

        Debug.WriteLine("Long Job Completed!");
    }
}

This will work since there is no timing issues introduced, but if exceptions occur in the LongRunningJob they will not be reported creating since the calling thread has already completed.

Add a Timing Issue

Let's add a timing problem to demonstrate where our code will start to get into trouble. So far we have used async void and haven't really gotten into any trouble. In MVVM development it is typical to add an IsBusy variable to stop execution of a method if some code is processing. For example if your app is requesting data from a web service.

Add _isBusy variable to stop execution in ViewModelBase

public class ViewModelBase
{
    public MainViewModel()
    {
        MyCommand = new Command(OnMyCommand);
    }
  
    public ICommand MyCommand { get; }
  
    bool _isBusy = false;

    void OnMyCommand()
    {
        if (_isBusy)
            return;

        _isBusy = true;

        LongRunningJob();

        _isBusy = false;
    }
  
    protected abstract void LongRunningJob();
}

In theory after introducing this code the user can tap the button 5 times within the 5000ms and it will only execute once. FALSE. This will execute 5 times because the calling thread continues over the LongRunningJob method and doesn't wait until the execution is completed.

This is where async void causes problems

Now imagine if your LongRunningJob is saving records to a database, you may run into database locks that cause your app to crash. If you are dealing with financial data it wouldn't be good to charge a customer multiple times because they pressed the button several times. The list of problems goes on and on, and it can be prevented by managing your Task state.

Fix the Timing Issue

Since we attempted using async void and it hurt us in the end, let's fix our code following the same inheritance tree.

Start by updating the abstract method in ViewModelBase to return a Task

protected abstract Task LongRunningJob();

Update override to return a Task in MainViewModel

protected override async Task LongRunningJob()
{
    // Simulate work
    await Task.Delay(5000);

    Debug.WriteLine("Long Job Completed!");
}

Update the OnMyCommand to return a Task, this will ensure the method understands when to wait and when to execute code.

async Task OnMyCommand()

The last step is to update the Command instantiation to correctly use the new OnMyCommand.

MyCommand = new Command(async () => await OnMyCommand());

Putting it all together, our ViewModelBase

public class ViewModelBase
{
    public MainViewModel()
    {
        MyCommand = new Command(async () => await OnMyCommand());
    }
  
    public ICommand MyCommand { get; }
  
    bool _isBusy = false;

    async Task OnMyCommand()
    {
        if (_isBusy)
            return;

        _isBusy = true;

        await LongRunningJob();

        _isBusy = false;
    }
  
    protected abstract Task LongRunningJob();
}

Now that the ViewModelBase properly supports invoking the Command method that returns a Task, it will know that it needs to wait until the LongRunningJob method finishes executing. If the user taps the button 5 times in 5000ms, it will only print to the output window once. This means our _isBusy lock is working correctly and our app no longer has a timing issur also known as a race condition.

Further Research

If you want to really expand your commanding capabilities for your project, there are techniques called TaskCommand or AsyncCommand. They implement Task based methods that will work in the Command instantiation better than our implementation. 

Our version

MyCommand = new Command(async () => await OnMyCommand());

AsyncCommand

// The parameter is a Task based method instead of void
// this allows us to pass our method with no lambda expression
MyCommand = new AsynCommand(OnMyCommand);

Conclusion

There is a lot to unpack with async void and how to resolve problems with it. This is not just a problem to MVVM architecture applications, but can happen in any .NET application. I hope you learned the importance of keeping your Task and have the tools to start learning more on how to create real solutions to this problem.

-Happy Coding


Share

Tags

.NET.NET Core.NET FrameworkC#XamarinXamarin.FormsUWPMVVMMemoryTaskAsyncWPF