Chinook Media Manager: Asynchronous calls

I will show in this article an approach to make an asynchronous call to the model, to prevent the user interface to freeze. If you read about WPF, you will see there is a lot of information and claims “don’t freeze the ui thread”, “build UI more responsiveness” and so on.

What you should know

NHibernate Sessions are not thread safe, so don’t use a single session in multiples threads. Conversation-per-Business-Transaction use the same session for a conversation. The end of the conversation flush the changes and close the session, the abort of the conversation discard changes and close the session.

You can’t update UI controls from a non-ui thread. Some people read this like “don’t set a ViewModel property from a non UI thread”. But this is not true, because it depends where do you raise the property changed notification thank to my friend German Schuager for reminding me this post from Rob Eisenberg. However, I’m not using this trick for now.

Asynchronous code is HARD to unit test. I will like to separate the asynchronous code in few units more testable and test “in-sync”.

The problem

The load of the artist list is very slow and this causes the user interface to freeze. This is very irritating for the end user.

I will break the async problem in the following three steps:

  1. Preview: Before start the operation we want to let the user know that the operation is in-process with some message in the user interface or maybe an hourglass.
  2. Process: The heavy operation.
  3. Completed: The operation has ended and we want to show the result to the UI.

Show me the code

This my generic implementation of ICommand for make async calls.

public class AsyncCommandWithResult<TParameter, TResult> 
        : IAsyncCommandWithResult<TParameter, TResult>
{
    private readonly Func<TParameter, bool> _canAction;
    private readonly Func<TParameter, TResult> _action;

    public AsyncCommandWithResult(Func<TParameter, TResult> action)
    {
        _action = action;
    }

    public AsyncCommandWithResult(
            Func<TParameter, TResult> action, 
            Func<TParameter, bool> canAction)
    {
        _action = action;
        _canAction = canAction;
    }


    public Action<TParameter, TResult> Completed { get; set; }
    public Action<TParameter> Preview { get; set; }
    public bool BlockInteraction { get; set; }

    public void Execute(object parameter)
    {
        //Execute Preview
        Preview((TParameter)parameter);

        //This is the async actions to take... 
        worker.DoWork += (sender, args) =>
            {
                args.Result = _action((TParameter)parameter);
                
            };

        //When the work is complete, execute the postaction.
        worker.RunWorkerCompleted += (sender, args) =>{
            Completed((TParameter) parameter, (TResult) args.Result);
            CommandManager.InvalidateRequerySuggested();
        };

        //Run the async work.
        worker.RunWorkerAsync();
    }

    [DebuggerStepThrough]
    public bool CanExecute(object parameter)
    {
        if(BlockInteraction  && worker.IsBusy )
            return false;

        return _canAction == null ? true : 
                _canAction((TParameter)parameter);
    }

    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }

    public TResult ExecuteSync(TParameter obj)
    {
        return _action(obj);
    }

    private readonly BackgroundWorker worker 
            = new BackgroundWorker();
}

Testing the ViewModel

Here you can see the test of the three steps. None of these test involves asynchronous calls.

[Test]
public void preview_of_load_list_should_show_status_info()
{
    var browseArtistVm = new BrowseArtistViewModel(
                new Mock<IBrowseArtistModel>().Object,
                new Mock<IViewFactory>().Object);
    
    browseArtistVm.LoadListCommand.Preview(null);
    browseArtistVm.Status.Should().Be.EqualTo("Loading artists...");
}

[Test(Description = "Check if the process call the model")]
public void load_list_command_should_load_artists_coll()
{
    var artistModel = new Mock<IBrowseArtistModel>();

    var artists = new List<Artist> {new Artist {Name = "Jose"}};

    artistModel.Setup(am => am.GetAllArtists()).Returns(artists);

    var browseArtistVm = new BrowseArtistViewModel(
                            artistModel.Object, 
                            new Mock<IViewFactory>().Object);

    browseArtistVm.LoadListCommand.ExecuteSync(null);

    artistModel.VerifyAll();
}

[Test]
public void completed_of_load_l_should_load_the_list_and_change_status()
{
    var browseArtistVm = new BrowseArtistViewModel(
                new Mock<IBrowseArtistModel>().Object,
                new Mock<IViewFactory>().Object);

    var artists = new List<Artist>();

    browseArtistVm.LoadListCommand.Completed(null, artists);

    browseArtistVm.Artists.Should().Be.SameInstanceAs(artists);
    browseArtistVm.Status.Should().Be.EqualTo("Finished");
}

Implementing the ViewModel

The LoadListCommand of the ViewModel is:

public virtual IAsyncCommandWithResult<object, IList<Artist>> LoadListCommand
{
    get
    {
        if (_loadListCommand == null)
        {
            _loadListCommand = 
                new AsyncCommandWithResult<object, IList<Artist>>
                        (o => _browseArtistModel.GetAllArtists())
                        {
                            BlockInteraction = true,
                            Preview = o => Status = "Loading artists...",
                            Completed = (o, artists) =>
                                {
                                    Artists = artists;
                                    Status = "Finished";
                                }
                        };
        }
        return _loadListCommand;
    }
}

Final conclusion

You must to remember that an NHibernate Session should be used in only one thread. This model for Browsing Artists has only one method with EndMode = End. This means session-per-call, so each time I click the LoadCommand the model start a new conversation and session. If you have a ViewModel with multiples operations within the same Conversation better you use something else, or use AsyncCommand everywhere within the VM.

There are a lot of alternatives to this approach, here are some;

Use this only when you need it. You don’t have to do this everywhere. Some operations are very fast and inexpensive.

Divide and conquer; I really like this way of testing. Don’t bring asynchronous things to your unit tests.


blog comments powered by Disqus
  • Categories

  • Archives