Search code examples
wpfasynchronousmvvmprogress-barstatusbar

How do I make sure the UI is updated during long running processes in a WPF application?


In a WPF app that follows the MVVM pattern, I've run across a common issue where a user clicks on a button which fires an event in the ViewModel. This event should enable a "Please Wait" spinner animation, do some processing which may take a few seconds, then hide the spinner. I'm not really sure of a good pattern I can use to make sure the spinner animation always appears.

As an example, I have a login process which does the following:

  • Displays spinner (set property on VM to true, spinner is bound to it)
  • Attempt to connect to server (can take a few seconds depending on connection)
  • On a failure, display a failure message
  • On success, save off some info about the user so it's available to the rest of the app.

What I'm finding is that the spinner never actually appears. I have tried wrapping the longer-running process in a Task.Run call, but that hasn't seemed to help.

Here's an approximation of what the code looks like:

// When true, spinner should be visible
protected bool _authenticatingIsVisible = false;
public bool AuthenticatingIsVisible
{
    get { return _authenticatingIsVisible; }
    set
    {
        _authenticatingIsVisible = value;
        NotifyOfPropertyChange(() => AuthenticatingIsVisible);
    }
}

public void Login()
{
    try
    {
        AuthenticationIsVisible = true;
        AuthCode result = AuthCode.NoAuthenticated;

        Task.Run(() => { result = _client.Authenticate() }).Wait();

        AuthenticationIsVisible = false;

        if (result == AuthCode.Authenticated)
        {
           // Bit of misc. code to set up the environment

           // Another check to see if something has failed
           // If it has, displays a dialog.
           // ex.
           var error = new Error("Something Failed", "Details Here", Answer.Ok);
           var vm = new DialogViewModel() { Dialog = error };
           _win.ShowDialog(vm);
           return;
        }
        else
        {
           DisplayAuthMessage(result);
        }
    }
    finally
    {
        AuthenticationIsVisible = false;
    } 
}

Solution

  • The proper way would be not to block the UI thread (which is what you are doing right now with .Wait()), and use AsyncAwait instead.

    private Task<AuthCode> Authenticate()
    {
        return Task.Run<AuthCode>(()=> 
        {
            return _client.Authenticate(); 
        });
    }
    
    public async void Login()
    {
        AuthenticationIsVisible = true;
        AuthCode result = await Authenticate();
        AuthenticationIsVisible = false;
    }