The TaskLoaderView is a UI component that handles all your UI loading state (Loading, Error, Result, Notification), and removes all the pain of async loading from your view models (try catch / async void / IsBusy / HasErrors / base view models / ...) thanks to its brother the TaskLoaderNotifier.
| MAUI Supported platforms |
|---|
![]() |
| ✔️ Android |
| ✔️ iOS |
| ✔️ Windows |
| ✔️ macOS |
Featuring:
- Default layout for all loading states (
Loading,Error,Success,Notification,Refresh) - Stylable layouts including fonts, accent colors, error images, ...
- Support for async
ICommandwithTaskLoaderCommandandCompositeTaskLoaderNotifier - Any states are overridable with user custom views and easily positioned with AbsoluteLayout properties
- Support for
Xamarin.Forms.Skeletonnuget package - Support for refresh scenarios, and error while refreshing with the
ErrorNotificationView - Support loading task on demand with the
NotStartedstate TaskLoaderNotifierfor theViewModelside taking care of all the error handling and theIsBusynonsense
It has been tested on Android, iOS, Windows and MacOS platforms through the Retronado sample app.
It uses the Sharpnado's TaskMonitor.
Add the Sharpnado.Maui.TaskLoaderView nuget package.
And call the initializer in your MauiProgram.cs file:
publicstaticclassMauiProgram{publicstaticMauiAppCreateMauiApp(){varbuilder=MauiApp.CreateBuilder();builder.UseMauiApp<App>().ConfigureTaskLoader(true)// logger enabled.ConfigureFonts(fonts =>{fonts.AddFont("OpenSans-Regular.ttf","OpenSansRegular");fonts.AddFont("OpenSans-Semibold.ttf","OpenSansSemibold");});returnbuilder.Build();}}Version 2.5.0 now supports .Net MAUI.
New TemplatedTaskLoader: it does the same job as the TaskLoaderView but using ControlTemplate instead of a absolute layout of views.
<cv:TemplatedTaskLoaderx:Name="LoaderView"
Grid.Row="1"Style="{StaticResource TemplatedLoaderLongLoading}"TaskLoaderNotifier="{Binding Loader}">
<cv:TemplatedTaskLoader.ResultControlTemplate>
<ControlTemplate>
...
</ControlTemplate>
</cv:TemplatedTaskLoader.ResultControlTemplate>
<cv:TemplatedTaskLoader.ErrorControlTemplate>
<ControlTemplate>
...
</ControlTemplate>
</cv:TemplatedTaskLoader.ErrorControlTemplate>
<cv:TemplatedTaskLoader.LoadingControlTemplate>
<ControlTemplate>
...
</ControlTemplate>
</cv:TemplatedTaskLoader.LoadingControlTemplate>
</cv:TemplatedTaskLoader>
...
<Stylex:Key="TemplatedLoaderLongLoading"TargetType="customViews:TemplatedTaskLoader">
<SetterProperty="LoadingControlTemplate"Value="{StaticResource LottieRocketControlTemplate}" />
<SetterProperty="ErrorControlTemplate"Value="{StaticResource ErrorViewControlTemplate}" />
</Style>
<ControlTemplatex:Key="LottieRocketControlTemplate">
<forms:AnimationViewHorizontalOptions="Center"VerticalOptions="Center"HeightRequest="200"WidthRequest="200"Animation="delivery_truck_animation.json"IsAnimating="{Binding Source={RelativeSource AncestorType={x:Type customViews:TemplatedTaskLoader}}, Path=TaskLoaderNotifier.ShowLoader}"RepeatMode="Infinite" />
<ControlTemplatex:Key="ErrorViewControlTemplate">
<StackLayoutHorizontalOptions="Center"VerticalOptions="Center"BindingContext="{Binding Source={RelativeSource AncestorType={x:Type customViews:TemplatedTaskLoader}}, Path=TaskLoaderNotifier}"IsVisible="False"Orientation="Vertical"Spacing="10">
<FrameStyle="{StaticResource FrameCircle}"WidthRequest="{StaticResource SizeTaskLoaderIcon}"HeightRequest="{StaticResource SizeTaskLoaderIcon}"Margin="0,0,0,10"BackgroundColor="{StaticResource ColorPrimary}">
<ImageHorizontalOptions="Center"VerticalOptions="Center"Source="{Binding Error, Converter={converters:ExceptionToImageSourceConverter}}" />
</Frame>
<LabelStyle="{StaticResource TextBodySecondary}"WidthRequest="300"Margin="0,0,0,20"HorizontalTextAlignment="Center"LineBreakMode="WordWrap"MaxLines="2"Text="{Binding Error, Converter={converters:ExceptionToErrorMessageConverter}}" />
<sho:ShadowsCornerRadius="10"Shades="{StaticResource ShadowAccentBottom}">
<ButtonStyle="{StaticResource ButtonAccent}"HorizontalOptions="Center"VerticalOptions="End"Command="{Binding ReloadCommand}"Text="{x:Static loc:GlobalResources.Common_Retry}" />
</sho:Shadows>
</StackLayout>
</ControlTemplate>For all new developments, I recommend now to use the TemplatedTaskLoader, and use it with a Snackbar to handle the error notifications.
There is an detailed example on how to use it here:
and here:
You can use the new builder for the CompositeTaskLoader that will wire for you the ShowErrorNotification of your loaders, and the errors from your TaskLoaderCommand:
CompositeNotifier=CompositeTaskLoaderNotifier.ForCommands().WithLoaders(Loader).WithCommands(BuyGameCommand,PlayTheGameCommand).Build();And bind the ShowLastError and LastError property to your Snackbar:
<ContentPage.Content>
<Grid>
<Grid.RowDefinitions>
<RowDefinitionHeight="60" />
<RowDefinitionHeight="*" />
</Grid.RowDefinitions>
<views:NavigationToolBarTitle="{loc:Translate Commands_Title}"
Grid.Row="0"BackgroundColor="{StaticResource TopElementBackground}"Theme="Standard" />
<RefreshView Grid.Row="1"IsRefreshing="{Binding Loader.ShowRefresher}"RefreshColor="{StaticResource AccentColor}"Command="{Binding Loader.RefreshCommand}">
<ScrollView>
<tlv:TemplatedTaskLoaderTaskLoaderNotifier="{Binding Loader}">
<tlv:TemplatedTaskLoader.ResultControlTemplate>
<ControlTemplate>
<GridRowDefinitions="300,*"x:DataType="viewModels:CommandsPageViewModel"BindingContext="{Binding Source={RelativeSource AncestorType={x:Type viewModels:CommandsPageViewModel}}}">
<Image Grid.Row="0"
skeleton:Skeleton.BackgroundColor="{StaticResource GreyBackground}"
skeleton:Skeleton.IsBusy="{Binding Loader.ShowLoader}"Aspect="Fill"Source="{Binding Loader.Result.ScreenshotUrl}" />
<sho:MaterialFrame Grid.Row="0"Padding="15,5,15,30"VerticalOptions="End"CornerRadius="0"MaterialBlurStyle="Dark"MaterialTheme="AcrylicBlur">
<GridPadding="0"ColumnDefinitions="*,60"RowDefinitions="40,20,20"RowSpacing="0">
<Label Grid.Row="0"
Grid.Column="0"Style="{StaticResource GameName}"Margin="0,0,10,0"
skeleton:Skeleton.BackgroundColor="{StaticResource GreyBackground}"
skeleton:Skeleton.IsBusy="{Binding Loader.ShowLoader}"Text="{Binding Loader.Result.Name}" />
<Label Grid.Row="1"Style="{StaticResource GameCompany}"
skeleton:Skeleton.BackgroundColor="{StaticResource GreyBackground}"
skeleton:Skeleton.IsBusy="{Binding Loader.ShowLoader}"Text="{Binding Loader.Result.MajorCompany}" />
<Label Grid.Row="2"Style="{StaticResource GameGenre}"Text="{Binding Loader.Result.MajorGenre}" />
<Image Grid.Row="0"
Grid.RowSpan="3"
Grid.Column="1"Margin="0,10,0,0"Source="{Binding Loader.Result.CoverUrl}" />
</Grid>
</sho:MaterialFrame>
<Button Grid.Row="1"HeightRequest="40"Margin="0,-20,100,0"Padding="10"HorizontalOptions="Center"VerticalOptions="Start"BackgroundColor="{StaticResource AccentColor}"Command="{Binding BuyGameCommand}"FontFamily="{StaticResource FontAtariSt}"Text="BUY IT"TextColor="{StaticResource TopElementBackground}" />
<Button Grid.Row="1"HeightRequest="40"Margin="100,-20,0,0"Padding="10"HorizontalOptions="Center"VerticalOptions="Start"BackgroundColor="{StaticResource AccentColor}"Command="{Binding PlayTheGameCommand}"FontFamily="{StaticResource FontAtariSt}"Text="PLAY IT"TextColor="{StaticResource TopElementBackground}" />
<Label Grid.Row="1"Style="{StaticResource GameGenre}"Margin="15,45,15,0"Text="{Binding Loader.Result.Summary}"TextColor="White" />
</Grid>
</ControlTemplate>
</tlv:TemplatedTaskLoader.ResultControlTemplate>
<tlv:TemplatedTaskLoader.LoadingControlTemplate>
<ControlTemplate>
<ActivityIndicatorColor="{StaticResource AccentColor}"IsRunning="True"HorizontalOptions="Center"VerticalOptions="Center" />
</ControlTemplate>
</tlv:TemplatedTaskLoader.LoadingControlTemplate>
<tlv:TemplatedTaskLoader.ErrorControlTemplate>
<ControlTemplatex:DataType="{x:Null}">
<StackLayoutHorizontalOptions="Center"VerticalOptions="Center"BindingContext="{Binding Source={RelativeSource AncestorType={x:Type tlv:TemplatedTaskLoader}}, Path=TaskLoaderNotifier}"Orientation="Vertical"Spacing="10">
<FrameCornerRadius="50"HorizontalOptions="Center"VerticalOptions="Center"BackgroundColor="Transparent"HasShadow="False"IsClippedToBounds="True"WidthRequest="100"Padding="0"HeightRequest="100"Margin="0,0,0,10">
<ImageHorizontalOptions="Center"VerticalOptions="Center"WidthRequest="100"HeightRequest="100"Source="{Binding Error, Converter={converters:ExceptionToImageSourceConverter}}" />
</Frame>
<LabelStyle="{StaticResource TextBodySecondary}"WidthRequest="300"Margin="0,0,0,20"HorizontalTextAlignment="Center"LineBreakMode="WordWrap"MaxLines="2"Text="{Binding Error, Converter={converters:ExceptionToErrorMessageConverter}}" />
<ButtonBackgroundColor="{StaticResource AccentColor}"CornerRadius="10"Padding="10,0"HorizontalOptions="Center"VerticalOptions="End"Command="{Binding ReloadCommand}"TextColor="Black"Text="{x:Static loc:SampleResources.ErrorButton_Retry}" />
</StackLayout>
</ControlTemplate>
</tlv:TemplatedTaskLoader.ErrorControlTemplate>
</tlv:TemplatedTaskLoader>
</ScrollView>
</RefreshView>
<AbsoluteLayout Grid.Row="1"BackgroundColor="#77002200"IsVisible="{Binding CompositeNotifier.ShowLoader}">
<Gridx:Name="ErrorNotificationView"
AbsoluteLayout.LayoutFlags="PositionProportional"
AbsoluteLayout.LayoutBounds="0.5, 0.5, 300, 150"RowDefinitions="*,*">
<Grid.Behaviors>
<tlv:TimedVisibilityBehaviorVisibilityInMilliseconds="4000" />
</Grid.Behaviors>
<Image Grid.RowSpan="2"Aspect="Fill"Source="{inf:ImageResource Sample.Images.window_border.png}" />
<Imagex:Name="BusyImage"Margin="15,30,15,0"Aspect="AspectFit"Source="{inf:ImageResource Sample.Images.busy_bee_white_bg.png}" />
<Label Grid.Row="1"Style="{StaticResource TextBody}"Margin="{StaticResource ThicknessLarge}"VerticalOptions="Center"HorizontalTextAlignment="Center"Text="{Binding LoadingText}" />
</Grid>
</AbsoluteLayout>
<tlv:Snackbar Grid.Row="1"Margin="15"VerticalOptions="End"BackgroundColor="White"FontFamily="{StaticResource FontAtariSt}"IsVisible="{Binding CompositeNotifier.ShowLastError, Mode=TwoWay}"Text="{Binding CompositeNotifier.LastError, Converter={StaticResource ExceptionToErrorMessageConverter}}"TextColor="{StaticResource TextPrimaryColor}"TextHorizontalOptions="Start" />
</Grid>
</ContentPage.Content>The task source given to TaskLoaderNotifier is now a Func<bool, Task> (or a Func<bool, Task<T>>) instead of a Func<Task>.
You can simply change your calls from Loader.Load(() => InitializeAsync()) to Loader.Load(_ => InitializeAsync())
The boolean passed now to your task source is a boolean indicating if the notifier is refreshing.
You can use it for invalidating your cache for example.
Loader.Load(LoadItems);publicTaskLoadItems(boolisRefreshing){if(isRefreshing||!_cache.ContainsItems()){_cache.InvalidateItems();varitems=_httpService.GetItems()
_cache.PutItems(items);returnitems;}return_cache.GetItems();}When the RefreshCommand will be called, it will pass automatically the isRefreshing boolean set to true to your task source.
The TaskLoaderView implements default view for all the task loading states:
- Loading, a simple ActivityLoader is displayed
- Error, an image with a retry button
- Success, your xaml inside the
TaskLoaderView - Notification, when an error occurs whereas some information has already been displayed
- Refresh, with a
RefreshViewfor example
| LoadingView | Result | ErrorView | ErrorNotificationView |
|---|---|---|---|
![]() | ![]() | ![]() | ![]() |
It is highly stylable, and you can even provide converters to translate an Exception to a meaningful image and message.
<ContentPage.Resources>
<ResourceDictionary>
<Stylex:Key="TaskLoaderStyle"TargetType="customViews:TaskLoaderView">
<SetterProperty="AccentColor"Value="{StaticResource AccentColor}" />
<SetterProperty="FontFamily"Value="{StaticResource FontAtariSt}" />
<SetterProperty="EmptyStateMessage"Value="{loc:Translate Empty_Screen}" />
<SetterProperty="EmptyStateImageSource"Value="{inf:ImageResource Sample.Images.dougal.png}" />
<SetterProperty="RetryButtonText"Value="{loc:Translate ErrorButton_Retry}" />
<SetterProperty="TextColor"Value="{StaticResource OnDarkColor}" />
<SetterProperty="ErrorImageConverter"Value="{StaticResource ExceptionToImageSourceConverter}" />
<SetterProperty="ErrorMessageConverter"Value="{StaticResource ExceptionToErrorMessageConverter}" />
<SetterProperty="BackgroundColor"Value="{StaticResource LightGreyBackground}" />
<SetterProperty="NotificationBackgroundColor"Value="{StaticResource TosWindows}" />
<SetterProperty="NotificationTextColor"Value="{StaticResource TextPrimaryColor}" />
</Style>
</ResourceDictionary>
</ContentPage.Resources>
...
<customViews:TaskLoaderView Grid.Row="2"Style="{StaticResource TaskLoaderStyle}"TaskLoaderNotifier="{Binding Loader}">
<RefreshViewCommand="{Binding Loader.RefreshCommand}"IsRefreshing="{Binding Loader.ShowRefresher}"RefreshColor="{StaticResource AccentColor}">
<ListViewBackgroundColor="Transparent"CachingStrategy="RecycleElementAndDataTemplate"Header=""ItemTemplate="{StaticResource GameDataTemplate}"ItemsSource="{Binding Loader.Result}"RowHeight="140"SelectionMode="None"SeparatorVisibility="None" />
</RefreshView>
</customViews:TaskLoaderView>publicclassExceptionToImageSourceConverter:IValueConverter{publicobjectConvert(objectvalue,TypetargetType,objectparameter,CultureInfoculture){if(value==null){returnnull;}stringimageName;switch(value){caseServerExceptionserverException:imageName="server.png";break;caseNetworkExceptionnetworkException:imageName="the_internet.png";break;default:imageName="richmond.png";break;}returnImageSource.FromFile(imageName);}}You can also override any state views to implement your own:
| LoadingView (busy bee) | Result | ErrorView (atari st bombs) | ErrorNotificationView (retro alert) |
|---|---|---|---|
![]() | ![]() | ![]() | ![]() |
<sharpnado:TaskLoaderViewx:Name="TaskLoaderView"
Grid.Row="3"Style="{StaticResource TaskLoaderStyle}"TaskLoaderNotifier="{Binding Loader}">
<sharpnado:TaskLoaderView.LoadingView>
<Imagex:Name="BusyImage"
AbsoluteLayout.LayoutFlags="PositionProportional"
AbsoluteLayout.LayoutBounds="0.5, 0.5, 60, 60"Aspect="AspectFit"Source="{img:ImageResource Sample.Images.busy_bee_white_bg.png}" />
</sharpnado:TaskLoaderView.LoadingView>
<sharpnado:TaskLoaderView.ErrorView>
<Grid AbsoluteLayout.LayoutFlags="PositionProportional"
AbsoluteLayout.LayoutBounds="0, 0.5, 150, 90"Padding="15,0,0,0"BackgroundColor="White">
<Grid.RowDefinitions>
<RowDefinitionHeight="60" />
<RowDefinitionHeight="30" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinitionWidth="*" />
<ColumnDefinitionWidth="*" />
<ColumnDefinitionWidth="*" />
</Grid.ColumnDefinitions>
<Image Grid.Row="0"
Grid.Column="0"Style="{StaticResource ErrorBombStyle}" />
<Image Grid.Row="0"
Grid.Column="1"Style="{StaticResource ErrorBombStyle}" />
<Image Grid.Row="0"
Grid.Column="2"Style="{StaticResource ErrorBombStyle}" />
<Label Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="3"Style="{StaticResource TextBody}"Text="{Binding Loader.Error, Converter={StaticResource ExceptionToErrorMessageConverter}}" />
</Grid>
</sharpnado:TaskLoaderView.ErrorView>
<sharpnado:TaskLoaderView.ErrorNotificationView>
<Gridx:Name="ErrorNotificationView"
AbsoluteLayout.LayoutFlags="PositionProportional"
AbsoluteLayout.LayoutBounds="0.5, 0.5, 300, 150"Scale="0">
<Grid.Behaviors>
<behaviors:TimedVisibilityBehaviorVisibilityInSeconds="4" />
</Grid.Behaviors>
<ImageAspect="Fill"Source="{img:ImageResource Sample.Images.window_border.png}" />
<LabelStyle="{StaticResource TextBody}"Margin="{StaticResource ThicknessLarge}"VerticalOptions="Center"HorizontalTextAlignment="Center"Text="{Binding Loader.Error, Converter={StaticResource ExceptionToErrorMessageConverter}}" />
</Grid>
</sharpnado:TaskLoaderView.ErrorNotificationView>
<RefreshViewCommand="{Binding Loader.RefreshCommand}"IsRefreshing="{Binding Loader.ShowRefresher}"RefreshColor="{StaticResource AccentColor}">
<ListViewBackgroundColor="Transparent"CachingStrategy="RecycleElementAndDataTemplate"Header=""ItemTemplate="{StaticResource GameDataTemplate}"ItemsSource="{Binding Loader.Result}"RowHeight="140"SelectionMode="None"SeparatorVisibility="None" />
</RefreshView>
</sharpnado:TaskLoaderView>You can see that the TaskLoaderView uses an AbsoluteLayout internally. So you can use AbsoluteLayout bounds and flags to position your views.
You can now add your customs views as View or DataTemplate.
If you choose the latest option, you can now specify your custom views in a ResourceDictionary and set them in your TaskLoaderView style.
<ContentPage.Resources>
<ResourceDictionary>
<DataTemplatex:Key="LoadingLottieDataTemplate">
<lottie:AnimationViewx:Name="LoadingLottie"
AbsoluteLayout.LayoutFlags="PositionProportional"
AbsoluteLayout.LayoutBounds="0.5, 0.4, 120, 120"HorizontalOptions="FillAndExpand"VerticalOptions="FillAndExpand"Animation="{Binding Loader.ShowLoader, Converter={StaticResource CyclicLoadingLottieConverter}}"IsPlaying="True"Loop="True" />
</DataTemplate>
<DataTemplatex:Key="EmptyLottieDataTemplate">
<StackLayout AbsoluteLayout.LayoutFlags="PositionProportional"
AbsoluteLayout.LayoutBounds="0.5, 0.4, 300, 180"BindingContext="{Binding Source={RelativeSource AncestorType={x:Type customViews:TaskLoaderView}}, Path=TaskLoaderNotifier}">
<lottie:AnimationViewHorizontalOptions="FillAndExpand"VerticalOptions="FillAndExpand"Animation="empty_state.json"IsPlaying="True"Loop="True" />
<LabelStyle="{StaticResource TextBody}"HorizontalOptions="Center"VerticalOptions="Center"Text="{loc:Translate Empty_Screen}"TextColor="White" />
<ButtonStyle="{StaticResource TextBody}"HeightRequest="40"Margin="0,20,0,0"Padding="25,0"HorizontalOptions="Center"BackgroundColor="{StaticResource TopElementBackground}"Command="{Binding ReloadCommand}"Text="{loc:Translate ErrorButton_Retry}"TextColor="White" />
</StackLayout>
</DataTemplate>
<DataTemplatex:Key="ErrorLottieDataTemplate">
<StackLayout AbsoluteLayout.LayoutFlags="PositionProportional"
AbsoluteLayout.LayoutBounds="0.5, 0.4, 300, 180"BindingContext="{Binding Source={RelativeSource AncestorType={x:Type customViews:TaskLoaderView}}, Path=TaskLoaderNotifier}">
<lottie:AnimationViewHorizontalOptions="FillAndExpand"VerticalOptions="FillAndExpand"Animation="{Binding Error, Converter={StaticResource ExceptionToLottieConverter}}"IsPlaying="True"Loop="True" />
<LabelStyle="{StaticResource TextBody}"HorizontalOptions="Center"VerticalOptions="Center"Text="{Binding Error, Converter={StaticResource ExceptionToErrorMessageConverter}}"TextColor="White" />
<ButtonStyle="{StaticResource TextBody}"HeightRequest="40"Margin="0,20,0,0"Padding="25,0"HorizontalOptions="Center"BackgroundColor="{StaticResource TopElementBackground}"Command="{Binding ReloadCommand}"Text="{loc:Translate ErrorButton_Retry}"TextColor="White" />
</StackLayout>
</DataTemplate>
<Stylex:Key="TaskLoaderStyle"TargetType="customViews:TaskLoaderView">
<SetterProperty="AccentColor"Value="{StaticResource AccentColor}" />
<SetterProperty="FontFamily"Value="{StaticResource FontAtariSt}" />
<SetterProperty="EmptyStateMessage"Value="{loc:Translate Empty_Screen}" />
<SetterProperty="EmptyStateImageSource"Value="{inf:ImageResource Sample.Images.dougal.png}" />
<SetterProperty="RetryButtonText"Value="{loc:Translate ErrorButton_Retry}" />
<SetterProperty="TextColor"Value="{StaticResource OnDarkColor}" />
<SetterProperty="ErrorImageConverter"Value="{StaticResource ExceptionToImageSourceConverter}" />
<SetterProperty="ErrorMessageConverter"Value="{StaticResource ExceptionToErrorMessageConverter}" />
<SetterProperty="BackgroundColor"Value="{StaticResource LightGreyBackground}" />
<SetterProperty="NotificationBackgroundColor"Value="{StaticResource TosWindows}" />
<SetterProperty="NotificationTextColor"Value="{StaticResource TextPrimaryColor}" />
<SetterProperty="LoadingView"Value="{StaticResource LoadingLottieDataTemplate}" />
<SetterProperty="EmptyView"Value="{StaticResource EmptyLottieDataTemplate}" />
<SetterProperty="ErrorView"Value="{StaticResource ErrorLottieDataTemplate}" />
</Style>
</ResourceDictionary>
</ContentPage.Resources>With custom views and Lottie, you can add all the amazing animations made by your best designer \o/
Use Lottie json animations for subtle app branding and dazzle your guests in your mundane diner parties!
<customViews:TaskLoaderView Grid.Row="2"Style="{StaticResource TaskLoaderStyle}"TaskLoaderNotifier="{Binding Loader}">
<customViews:TaskLoaderView.LoadingView>
<lottie:AnimationViewx:Name="LoadingLottie"
AbsoluteLayout.LayoutFlags="PositionProportional"
AbsoluteLayout.LayoutBounds="0.5, 0.4, 120, 120"HorizontalOptions="FillAndExpand"VerticalOptions="FillAndExpand"Animation="{Binding Loader.ShowLoader, Converter={StaticResource CyclicLoadingLottieConverter}}"AutoPlay="True"Loop="True" />
</customViews:TaskLoaderView.LoadingView>
<customViews:TaskLoaderView.EmptyView>
<StackLayout AbsoluteLayout.LayoutFlags="PositionProportional" AbsoluteLayout.LayoutBounds="0.5, 0.4, 300, 180">
<lottie:AnimationViewHorizontalOptions="FillAndExpand"VerticalOptions="FillAndExpand"Animation="empty_state.json"AutoPlay="True"Loop="True" />
<LabelStyle="{StaticResource TextBody}"HorizontalOptions="Center"VerticalOptions="Center"Text="{loc:Translate Empty_Screen}"TextColor="White" />
<ButtonStyle="{StaticResource TextBody}"HeightRequest="40"Margin="0,20,0,0"Padding="25,0"HorizontalOptions="Center"BackgroundColor="{StaticResource TopElementBackground}"Command="{Binding Loader.ReloadCommand}"Text="{loc:Translate ErrorButton_Retry}"TextColor="White" />
</StackLayout>
</customViews:TaskLoaderView.EmptyView>
<customViews:TaskLoaderView.ErrorView>
<StackLayout AbsoluteLayout.LayoutFlags="PositionProportional" AbsoluteLayout.LayoutBounds="0.5, 0.4, 300, 180">
<lottie:AnimationViewHorizontalOptions="FillAndExpand"VerticalOptions="FillAndExpand"Animation="{Binding Loader.Error, Converter={StaticResource ExceptionToLottieConverter}}"AutoPlay="True"Loop="True" />
<LabelStyle="{StaticResource TextBody}"HorizontalOptions="Center"VerticalOptions="Center"Text="{Binding Loader.Error, Converter={StaticResource ExceptionToErrorMessageConverter}}"TextColor="White" />
<ButtonStyle="{StaticResource TextBody}"HeightRequest="40"Margin="0,20,0,0"Padding="25,0"HorizontalOptions="Center"BackgroundColor="{StaticResource TopElementBackground}"Command="{Binding Loader.ReloadCommand}"Text="{loc:Translate ErrorButton_Retry}"TextColor="White" />
</StackLayout>
</customViews:TaskLoaderView.ErrorView>
...
</customViews:TaskLoaderView>Have you tried the Skeleton loading properties from Horus?
https://github.com/HorusSoftwareUY/Xamarin.Forms.Skeleton
It's brilliant! The TaskLoaderView is supporting a simpler use case of the properties by binding directly to the TaskLoaderNotifier. With this method you don't have to create fake item view models in your page view model.
In case of a list: you just have to create a static array of item view models.
<customViews:TaskLoaderViewx:Name="GamesTaskLoader"
Grid.Row="2"Style="{StaticResource TaskLoaderStyle}"TaskLoaderNotifier="{Binding Loader}">
<customViews:TaskLoaderView.LoadingView>
<ListViewStyle="{StaticResource ListGameStyle}"
sk:Skeleton.Animation="Fade"
sk:Skeleton.IsBusy="{Binding Loader.ShowLoader}"
sk:Skeleton.IsParent="True"ItemTemplate="{StaticResource GameSkeletonViewCell}"ItemsSource="{x:Static views:Skeletons.Games}"VerticalScrollBarVisibility="Never" />
</customViews:TaskLoaderView.LoadingView>
<RefreshViewCommand="{Binding Loader.RefreshCommand}"IsRefreshing="{Binding Loader.ShowRefresher}"RefreshColor="{StaticResource AccentColor}">
<ListViewStyle="{StaticResource ListGameStyle}"CachingStrategy="RecycleElementAndDataTemplate"ItemTemplate="{StaticResource GameSkeletonViewCell}"ItemsSource="{Binding Loader.Result}" />
</RefreshView>
</customViews:TaskLoaderView>publicstaticclassSkeletons{publicstaticGame[]Games=new[]{newGame(0,null,null,DateTime.Now,newList<Genre>{newGenre(1,"Genre genre")},newList<Company>{newCompany(1,"The Company")},"Name name name",null),newGame(0,null,null,DateTime.Now,newList<Genre>{newGenre(1,"Genre genre")},newList<Company>{newCompany(1,"The Company")},"Name name name",null),newGame(0,null,null,DateTime.Now,newList<Genre>{newGenre(1,"Genre genre")},newList<Company>{newCompany(1,"The Company")},"Name name name",null),}}If you are not loading a list but a simple object, you don't even have to use a custom LoadingView, you can just use the TaskLoaderType="ResultAsLoadingView" property.
<sharpnado:TaskLoaderView Grid.Row="2"
Grid.Column="0"
Grid.ColumnSpan="2"AccentColor="{StaticResource AccentColor}"ErrorImageConverter="{StaticResource ExceptionToImageSourceConverter}"ErrorMessageConverter="{StaticResource ExceptionToErrorMessageConverter}"FontFamily="{StaticResource FontAtariSt}"TaskLoaderNotifier="{Binding RandomGameLoader}"TaskLoaderType="ResultAsLoadingView"TextColor="Black">
<FrameStyle="{StaticResource CardStyle}"Margin="-15,0,-15,-15"Padding="0"
skeleton:Skeleton.Animation="Beat"
skeleton:Skeleton.IsBusy="{Binding RandomGameLoader.ShowLoader}"
skeleton:Skeleton.IsParent="True"BackgroundColor="{DynamicResource CellBackgroundColor}"CornerRadius="10"IsClippedToBounds="True">
<Grid>
<Grid.RowDefinitions>
<RowDefinitionHeight="160" />
<RowDefinitionHeight="40" />
<RowDefinitionHeight="20" />
<RowDefinitionHeight="20" />
</Grid.RowDefinitions>
<Image Grid.Row="0"
skeleton:Skeleton.BackgroundColor="{StaticResource GreyBackground}"
skeleton:Skeleton.IsBusy="{Binding RandomGameLoader.ShowLoader}"Aspect="AspectFill"Source="{Binding RandomGameLoader.Result.ScreenshotUrl}" />
<Label Grid.Row="1"Style="{StaticResource GameName}"Margin="15,0"
skeleton:Skeleton.BackgroundColor="{StaticResource GreyBackground}"
skeleton:Skeleton.IsBusy="{Binding RandomGameLoader.ShowLoader}"Text="{Binding RandomGameLoader.Result.Name}" />
<Label Grid.Row="2"Style="{StaticResource GameCompany}"Margin="15,0"
skeleton:Skeleton.BackgroundColor="{StaticResource GreyBackground}"
skeleton:Skeleton.IsBusy="{Binding RandomGameLoader.ShowLoader}"Text="{Binding RandomGameLoader.Result.MajorCompany}" />
<Label Grid.Row="3"Style="{StaticResource GameGenre}"Margin="15,0"Text="{Binding RandomGameLoader.Result.MajorGenre}" />
</Grid>
</Frame>
</sharpnado:TaskLoaderView>You probably read many blogs about how you should implement a AsyncCommand instead of a regular Command for your Task based code.
Of course, like all async mvvm patterns, the original idea comes from Stephen Cleary.
But what if we could merge the power of TaskLoaderNotifier with the classic ICommand interface?
The TaskLoaderCommand will take as parameter a function returning a Task, and will wrap it in a NotifyTaskNotifier. You can then bind your TaskLoaderView to the NotifyTaskNotifier exposed by your TaskLoaderCommand.
It's in fact the missing piece to have a complete UI feedback to all our actions.
We at Sharpnado, created a simple Snackbar component very simple with timer and customization:
<forms:Snackbar Grid.Row="1"Margin="15"VerticalOptions="End"BackgroundColor="White"DisplayDurationMilliseconds="3000"FontFamily="{StaticResource FontAtariSt}"IsVisible="{Binding BuyGameCommand.Notifier.ShowError, Mode=TwoWay}"Text="{Binding BuyGameCommand.Notifier.Error, Converter={StaticResource ExceptionToErrorMessageConverter}}"TextColor="{StaticResource TextPrimaryColor}"TextHorizontalOptions="Start" />You can then very simply bind a TaskLoaderCommand to your Snackbar:
BuyGameCommand=newTaskLoaderCommand(BuyGame);privateasyncTaskBuyGame(){LoadingText="Proceeding to payment";RaisePropertyChanged(nameof(LoadingText));awaitTask.Delay(2000);thrownewLocalizedException($"Sorry, we only accept DogeCoin...{Environment.NewLine}BTW GameStop are still opened");}Then when pressing "BUY IT", you will get this
As you can see you can change the auto dismiss timer as you like, but also the font size, color, family, etc...
So with the TaskLoaderCommand you knew you could bind any action to a UI component and have a great UI feedback.
But thanks to the CompositeTaskLoaderNotifier you can now bind SEVERAL async Command states to ONE UI Component.
It makes all your commands UI feedback sooooo easy.
You can bind 1, 2, 3, 4, 423 commands to a unique Snackbar or/and a unique loading dialog \o/
Now look at that you just pass as parameters all the notifiers from your commands to the composite constructor:
publicclassCommandsPageViewModel:ANavigableViewModel{privatereadonlyIRetroGamingService_retroGamingService;publicCommandsPageViewModel(INavigationServicenavigationService,IRetroGamingServiceretroGamingService):base(navigationService){_retroGamingService=retroGamingService;Loader=newTaskLoaderNotifier<Game>();BuyGameCommand=newTaskLoaderCommand(BuyGame);PlayTheGameCommand=newTaskLoaderCommand(PlayTheGame);CompositeNotifier=newCompositeTaskLoaderNotifier(BuyGameCommand.Notifier,PlayTheGameCommand.Notifier);}publicCompositeTaskLoaderNotifierCompositeNotifier{get;}publicTaskLoaderCommandBuyGameCommand{get;}publicTaskLoaderCommandPlayTheGameCommand{get;}publicTaskLoaderNotifier<Game>Loader{get;}publicstringLoadingText{get;set;}publicoverridevoidOnNavigated(objectparameter){Loader.Load(()=>GetRandomGame());}privateasyncTask<Game>GetRandomGame(){awaitTask.Delay(TimeSpan.FromSeconds(2));returnawait_retroGamingService.GetRandomGame(true);}privateasyncTaskBuyGame(){LoadingText="Proceeding to payment";RaisePropertyChanged(nameof(LoadingText));awaitTask.Delay(2000);thrownewLocalizedException($"Sorry, we only accept DogeCoin...{Environment.NewLine}BTW GameStop are still opened");}privateasyncTaskPlayTheGame(){LoadingText="Loading the game...";RaisePropertyChanged(nameof(LoadingText));awaitTask.Delay(2000);thrownewLocalizedException("AHAHAHA! Yeah right...");}}...bind the composite notifier to your loading dialog and snackbar...
<!-- Loading dialog -->
<AbsoluteLayout Grid.Row="1"BackgroundColor="#77002200"IsVisible="{Binding CompositeNotifier.ShowLoader}">
<Gridx:Name="ErrorNotificationView"
AbsoluteLayout.LayoutFlags="PositionProportional"
AbsoluteLayout.LayoutBounds="0.5, 0.5, 300, 150"RowDefinitions="*,*">
<Grid.Behaviors>
<forms:TimedVisibilityBehaviorVisibilityInMilliseconds="4000" />
</Grid.Behaviors>
<Image Grid.RowSpan="2"Aspect="Fill"Source="{inf:ImageResource Sample.Images.window_border.png}" />
<Imagex:Name="BusyImage"Margin="15,30,15,0"Aspect="AspectFit"Source="{inf:ImageResource Sample.Images.busy_bee_white_bg.png}" />
<Label Grid.Row="1"Style="{StaticResource TextBody}"Margin="{StaticResource ThicknessLarge}"VerticalOptions="Center"HorizontalTextAlignment="Center"Text="{Binding LoadingText}" />
</Grid>
</AbsoluteLayout>
<!-- SNACKBAR -->
<forms:Snackbar Grid.Row="1"Margin="15"VerticalOptions="End"BackgroundColor="White"FontFamily="{StaticResource FontAtariSt}"IsVisible="{Binding CompositeNotifier.ShowError, Mode=TwoWay}"Text="{Binding CompositeNotifier.LastError, Converter={StaticResource ExceptionToErrorMessageConverter}}"TextColor="{StaticResource TextPrimaryColor}"TextHorizontalOptions="Start" />...and BAM, just chill and click.
Another example of using TaskLoaderCommand is the ability to load views on demand.
A NotStartedView state is also available so you can display a view before loading the Task.
It is quite useful for load-on-demand.
Here TaskLoaderType="ResultAsLoadingView" is set cause we are using the skeleton loading for just one object.
<sharpnado:TaskLoaderView Grid.Row="2"
Grid.Column="0"
Grid.ColumnSpan="2"AccentColor="{StaticResource AccentColor}"ErrorImageConverter="{StaticResource ExceptionToImageSourceConverter}"ErrorMessageConverter="{StaticResource ExceptionToErrorMessageConverter}"FontFamily="{StaticResource FontAtariSt}"TaskLoaderNotifier="{Binding RandomGameLoaderCommand.Notifier}"TaskLoaderType="ResultAsLoadingView"TextColor="Black">
<sharpnado:TaskLoaderView.NotStartedView>
<Button AbsoluteLayout.LayoutFlags="PositionProportional"
AbsoluteLayout.LayoutBounds="0.5, 0.5, 120, 50"Style="{StaticResource ButtonTextIt}"Command="{Binding RandomGameLoaderCommand}" />
</sharpnado:TaskLoaderView.NotStartedView>
<FrameStyle="{StaticResource CardStyle}"Margin="-15,0,-15,-15"Padding="0"
skeleton:Skeleton.Animation="Fade"
skeleton:Skeleton.IsBusy="{Binding ShowLoader}"
skeleton:Skeleton.IsParent="True"BackgroundColor="{DynamicResource CellBackgroundColor}"BindingContext="{Binding RandomGameLoaderCommand.Notifier}"CornerRadius="10"IsClippedToBounds="True">
...
</Frame>
</sharpnado:TaskLoaderView>publicclassLoadOnDemandViewModel:Bindable{privatereadonlyIRetroGamingService_retroGamingService;publicLoadOnDemandViewModel(IRetroGamingServiceretroGamingService){_retroGamingService=retroGamingService;RandomGameLoaderCommand=newTaskLoaderCommand<object,Game>(_ =>GetRandomGame());}publicTaskLoaderCommand<object,Game>RandomGameLoaderCommand{get;}privateasyncTask<Game>GetRandomGame(){awaitTask.Delay(TimeSpan.FromSeconds(4));if(DateTime.Now.Millisecond%2==0){thrownewNetworkException();}returnawait_retroGamingService.GetRandomGame();}}We tend to forget a state in our Task loading cycle: the notification view.
Consider this scenario:
- we are loading a list of retro game
- loading is successful: the list is displayed
- we are refreshing the list
- oops an error occurs
- do we want to see the error view although the items were correctly loaded before?
NO! We just want to see a nice snackbar warning the user about it.
The ErrorNotificationView is also customizable if you like. It's brought to you with a nice TimedVisibilityBehavior so that you can specify how much time it needs to be shown to the user.
| Default view | User custom view |
|---|---|
![]() | ![]() |
<sharpnado:TaskLoaderView.ErrorNotificationView>
<Gridx:Name="ErrorNotificationView"
AbsoluteLayout.LayoutFlags="PositionProportional"
AbsoluteLayout.LayoutBounds="0.5, 0.5, 300, 150"Scale="0">
<Grid.Behaviors>
<behaviors:TimedVisibilityBehaviorVisibilityInSeconds="4" />
</Grid.Behaviors>
<ImageAspect="Fill"Source="{img:ImageResource Sample.Images.window_border.png}" />
<LabelStyle="{StaticResource TextBody}"Margin="{StaticResource ThicknessLarge}"VerticalOptions="Center"HorizontalTextAlignment="Center"Text="{Binding Loader.Error, Converter={StaticResource ExceptionToErrorMessageConverter}}" />
</Grid>
</sharpnado:TaskLoaderView.ErrorNotificationView>Just bind the RefreshCommand to the RefreshView and IsRefreshing to the ShowRefresher property.
<RefreshViewCommand="{Binding Loader.RefreshCommand}"IsRefreshing="{Binding Loader.ShowRefresher}"RefreshColor="{StaticResource AccentColor}">
<ListViewStyle="{StaticResource ListGameStyle}"CachingStrategy="RecycleElementAndDataTemplate"ItemTemplate="{StaticResource GameSkeletonViewCell}"ItemsSource="{Binding Loader.Result}" />
</RefreshView>For more about that, you can read this post on Sharpnado: https://www.sharpnado.com/taskloaderview-async-init-made-easy/.
For deeper understanding of Composition vs Inheritance, my session at XamlExpertDay: https://github.com/roubachof/SLIDES_FreeYourselfFromIsBusy.
The TaskLoaderNotifier is a loading component for your tasks, and is commonly used in your view models.
publicclassRetroGamesViewModel:ANavigableViewModel{privatereadonlyIRetroGamingService_retroGamingService;publicRetroGamesViewModel(INavigationServicenavigationService,IRetroGamingServiceretroGamingService):base(navigationService){_retroGamingService=retroGamingService;RefreshCommand=newCommand(()=>Load(null));Loader=newTaskLoaderNotifier<List<Game>>();}publicTaskLoaderNotifier<List<Game>>Loader{get;}publicICommandRefreshCommand{get;}publicoverridevoidLoad(objectparameter){// TaskStartMode = Manual (Default mode)Loader.Load(InitializeAsync);}privateasyncTask<List<Game>>InitializeAsync(){
...}}And that's all. It wraps all the states of the task (NotStarted, Loading, Fault, Success).
You can just stop worrying about IsBusy, HasErrors, ErrorMessage, IsRefreshing...
You bind your TaskLoaderNotifier to your TaskLoaderView, and the magic happens.
<customViews:TaskLoaderView Grid.Row="2"Style="{StaticResource TaskLoaderStyle}"TaskLoaderNotifier="{Binding Loader}">
<RefreshViewCommand="{Binding Loader.RefreshCommand}"IsRefreshing="{Binding Loader.ShowRefresher}"RefreshColor="{StaticResource AccentColor}">
<ListViewBackgroundColor="Transparent"CachingStrategy="RecycleElementAndDataTemplate"Header=""ItemTemplate="{StaticResource GameDataTemplate}"ItemsSource="{Binding Loader.Result}"RowHeight="140"SelectionMode="None"SeparatorVisibility="None" />
</RefreshView>
</customViews:TaskLoaderView>And just with those 2 chunks of code you are now handling all the loading states of your view model :)
The sample highlighting the possibilities of the TaskLoaderView is a tribute to the TOS of the Atari ST and its famous "busy bee".
It includes a random collection of retro games provided by the IGDB v3 API.



















