TaskLoaderView: async init made easy

TaskLoaderView: async init made easy

So after the HorizontalListView, I'm now introducing you to the TaskLoaderView, which will handle all your UI state in sync with your async loading code.

This component will remove the async initialization pain from your view models (try catch / async void / IsBusy / IsInitialized / base view models / ...), and the IsVisible properties from your Views, by using Composition.

With some outrageous nice features such as:

  • Display of ActivityLoader while loading
  • Error handling with custom messages and icons
  • Empty states handling
  • Don't show activity loader for refresh scenarios (if data is already shown)
  • Retry with button
  • Pure Xamarin Forms view: no renderers

You can see it as a container for your views relying on async loading.

It's available in Nuget:

  • Sharpnado.Presentation.Forms (which include several others components like the amaaaazing HorizontalListView)

You can also find the source code here:

https://github.com/roubachof/Sharpnado.Presentation.Forms

User manual - the Silly! app

Always the test app demonstrating the TaskLoaderView's features:

https://github.com/roubachof/Xamarin-Forms-Practices

The TaskLoaderView

<customViews:TaskLoaderView 
    Grid.Row="2"
    Style="{StaticResource SillyTaskLoader}"
    ViewModelLoader="{Binding SillyPeopleLoader}">

    <Grid ColumnSpacing="0" RowSpacing="0">

        <Grid.RowDefinitions>
            <RowDefinition 
                x:Name="SillyOfTheDayHeader" 
                Height="{StaticResource HeaderHeight}" />
            <RowDefinition Height="144" />
            <RowDefinition 
                x:Name="ListViewHeader" 
                Height="{StaticResource HeaderHeight}" />
            <!--  ItemHeight + VerticalMargin + VerticalPadding  -->
            <RowDefinition Height="176" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <!--  ROW 0: Silly Of The Day Header  -->
        <Label 
            Grid.Row="0"
            Style="{StaticResource TextHeader}"
            Text="{loc:Translate SillyPeople_SillyOfTheDay}" />

        <!-- etc -->

    </Grid>

</customViews:TaskLoaderView>

You can see that the magic happens thanks to the ViewModelLoader property bound to the ViewModel's SillyPeopleLoader.
The task loader is in fact a ContentView processing all the states of your asynchrounous initialization code wraped in a "loader". You know, the one you put shamelessly in an a async void IntializeAsync method ? uh ? remember? Right. Stop that, this is silly. But we'll go into details later.

Styling

Now the style applied to this loader that can be applied to all my TaskLoaderView through my app:

<Style x:Key="SillyTaskLoader" TargetType="customViews:TaskLoaderView">
    <Setter Property="AccentColor" 
            Value="{StaticResource Accent}" />
    <Setter Property="FontFamily" 
            Value="{StaticResource FontItalic}" />
    <Setter Property="RetryButtonText" 
            Value="{localization:Translate ErrorButton_Retry}" />
    <Setter Property="TextColor" 
            Value="{StaticResource TextPrimaryColor}" />
    <Setter Property="EmptyStateImageUrl" 
            Value="dougal.png" />
    <Setter Property="ErrorImageConverter" 
            Value="{StaticResource ExceptionToImageSourceConverter}" />
</Style>

So we have some styling stuff like the custom font FontFamily, TextColor, AccentColor you want to use. We also have the images you want to display when you are in Error or Empty state, but we'll discuss it later.

TaskLoaderView states

So your async initialization code state could be either equal to:

  • Loading (async code is running)
  • Success (async code ran succesfully)
  • Empty (returned null or a empty collection)
  • Error (the async code threw an exception)
  • Refreshing (last async code execution was succesfull)

Wait but how does it work ??

Remark: Well if you don't want to know how it works, but just use it, just goto: Man, just tell me how to use the thing

Let's get back to our initialization code in our ViewModel...

So, here is our shameless initialization code (spoiler alert: this is wrong):

public async void Initialize(object parameter)
{
    await InitializationCodeAsync((int)parameter);
}

This is a little better:

public async void Initialize(object parameter)
{
    try
    {
        await InitializationCodeAsync((int)parameter);
    }
    catch (Exception exception)
    {
        ExceptionHandler.Handle(exception);
    }
}

But wait, I want to give a UI feedback to the user:

public async void Initialize(object parameter)
{
    IsBusy = true;
    HasErrors = false;
    try
    {
        await InitializationCodeAsync((int)parameter);
    }
    catch (Exception exception)
    {
        ExceptionHandler.Handle(exception);
        HasErrors = true;
        ErrorMessage =
    }
    finally
    {
        IsBusy = false;
    }
}

Pfew, this is a lot of copy paste on each of my VM, I will create a base VM for this, and all my VM will inherit from that.

Then stop it, stop that nonsense. Just use Composition over Inheritance.

The idea is simply to wrap our initialization code in an object responsible for its asynchronous loading.

Introducing the ViewModelLoader

Now for the loading part, the issue has been tackled years ago by Stephen Cleary. You should use a NotifyTask object to wrap your async initialization. It garantees that the exception is correctly caught, and it will notify you (it implements INotifyPropertyChanged).

Start by reading this: https://msdn.microsoft.com/en-us/magazine/dn605875.aspx.

So the ViewModelLoader is using a NotifyTask, and fills the gap between asynchrounous code state and ui states.

It will basically handles all the code states and notify the TaskLoaderView through INotifyPropertyChanged. So you bind a ViewModelLoader to a TaskLoaderView.

Man, just tell me how to use the thing

public class SillyPeopleVm : ANavigableViewModel
{
    private readonly ISillyDudeService _sillyDudeService;

    public SillyPeopleVm(
        INavigationService navigationService, 
        ISillyDudeService sillyDudeService, 
        ErrorEmulator errorEmulator)
        : base(navigationService)
    {
        _sillyDudeService = sillyDudeService;
        InitCommands();

        SillyPeopleLoader = new 
            ViewModelLoader<ObservableCollection<SillyDudeVmo>>( 
                ApplicationExceptions.ToString, 
                SillyResources.Empty_Screen);
    }

    public ErrorEmulatorVm ErrorEmulator { get; }

    public SillyDudeVmo SillyOfTheDay { get; private set; }

    public ViewModelLoader<ObservableCollection<SillyDudeVmo>> 
        SillyPeopleLoader { get; }

    public override void Load(object parameter)
    {
        SillyPeopleLoader.Load(LoadSillyPeopleAsync);
    }

    private async Task<ObservableCollection<SillyDudeVmo>> LoadSillyPeopleAsync()
    {
        SillyOfTheDay = new SillyDudeVmo(
            await _sillyDudeService.GetRandomSilly(), GoToSillyDudeCommand);
        RaisePropertyChanged(nameof(SillyOfTheDay));

        return new ObservableCollection<SillyDudeVmo>(
            (await _sillyDudeService.GetSillyPeople())
                .Select(dude => 
                    new SillyDudeVmo(dude, GoToSillyDudeCommand)));
    }
}

You have to provide 3 things to the ViewModelLoader.
In the constructor:

ViewModelLoader(
    Func<Exception, string> errorHandler = null, 
    string emptyStateMessage = null)
  1. An error handler that will translate your exception to readable messages
  2. The message to be displayed when a empty state is displayed
  3. And of course, your async initialization code in the Load method

Example of error handler:

public static class ApplicationExceptions
{
    public static string ToString(Exception exception)
    {
        switch (exception)
        {
            case ServerException serverException:
                // "Couldn't reach the Internet."
                return SillyResources.Error_Business; 
            case NetworkException networkException:
                // "Couldn't reach the Internet."
                return SillyResources.Error_Network; 
            default:
                // "An ill wind is blowing..."
                return SillyResources.Error_Unknown; 
        }
    }
}

Pro tip: If you're returning a collection, you can directly bind the view to the SillyPeopleLoader.Result property.

And that's it...

Cherry on the cake

As you could see in the app, we display some nice images to illustrate our states. For example Douglas for empty state, or the Mighty internet on network error.

For this we use two properties of TaskLoaderView.

  • EmptyStateImageUrl: which is just the url of the image in case of empty state,
  • ErrorImageConverter: which is a ValueConverter converting our Exception to ImageSource.
public class ExceptionToImageSourceConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value == null)
        {
            return null;
        }

        string imageName;

        switch (value)
        {
            case ServerException serverException:
                imageName = "server.png";
                break;
            case NetworkException networkException:
                imageName = "the_internet.png";
                break;
            default:
                imageName = "richmond.png";
                break;
        }

        return ImageSource.FromFile(imageName);
    }
}

But if your are just lazy, you can ignore those properties. You just won't have images displayed.

Open Source licenses and inspirations

I greet his grace Stephen Cleary (https://github.com/StephenCleary) who cast his holy words on my async soul. https://www.youtube.com/watch?v=jjaqrPpdQYc.