diff --git a/Maui.ServerDrivenUI/Abstractions/IServerDrivenVisualElement.cs b/Maui.ServerDrivenUI/Abstractions/IServerDrivenVisualElement.cs index 1a4978f..ef77b49 100644 --- a/Maui.ServerDrivenUI/Abstractions/IServerDrivenVisualElement.cs +++ b/Maui.ServerDrivenUI/Abstractions/IServerDrivenVisualElement.cs @@ -2,21 +2,17 @@ public interface IServerDrivenVisualElement { - public string? ServerKey - { - get; set; - } + string? ServerKey { get; set; } - public UIElementState State - { - get; set; - } - public Action OnLoaded - { - get; - set; - } + UIElementState State { get; set; } + + Action? OnLoaded { get; set; } + + DataTemplate LoadingTemplate { get; set; } + + DataTemplate ErrorTemplate { get; set; } void OnStateChanged(UIElementState newState); + void OnError(Exception ex); } diff --git a/Maui.ServerDrivenUI/Views/ServerDrivenContentPage.cs b/Maui.ServerDrivenUI/Views/ServerDrivenContentPage.cs index f576cb0..577476f 100644 --- a/Maui.ServerDrivenUI/Views/ServerDrivenContentPage.cs +++ b/Maui.ServerDrivenUI/Views/ServerDrivenContentPage.cs @@ -18,6 +18,18 @@ public abstract class ServerDrivenContentPage : ContentPage, IServerDrivenVisual typeof(ServerDrivenContentPage), UIElementState.None, propertyChanged: ServerDrivenVisualElement.OnStatePropertyChanged); + + public static readonly BindableProperty LoadingTemplateProperty = BindableProperty.Create( + nameof(LoadingTemplate), + typeof(DataTemplate), + typeof(ServerDrivenView), + null); + + public static readonly BindableProperty ErrorTemplateProperty = BindableProperty.Create( + nameof(ErrorTemplate), + typeof(DataTemplate), + typeof(ServerDrivenView), + null); #endregion @@ -35,12 +47,24 @@ public UIElementState State set => SetValue(StateProperty, value); } - public Action OnLoaded + public Action? OnLoaded { get; set; } + public DataTemplate LoadingTemplate + { + get => (DataTemplate)GetValue(LoadingTemplateProperty); + set => SetValue(LoadingTemplateProperty, value); + } + + public DataTemplate ErrorTemplate + { + get => (DataTemplate)GetValue(ErrorTemplateProperty); + set => SetValue(ErrorTemplateProperty, value); + } + #endregion #region Constructors diff --git a/Maui.ServerDrivenUI/Views/ServerDrivenView.cs b/Maui.ServerDrivenUI/Views/ServerDrivenView.cs index 68a2ebc..8e8145a 100644 --- a/Maui.ServerDrivenUI/Views/ServerDrivenView.cs +++ b/Maui.ServerDrivenUI/Views/ServerDrivenView.cs @@ -19,6 +19,18 @@ public class ServerDrivenView : ContentView, IServerDrivenVisualElement UIElementState.None, propertyChanged: ServerDrivenVisualElement.OnStatePropertyChanged); + public static readonly BindableProperty LoadingTemplateProperty = BindableProperty.Create( + nameof(LoadingTemplate), + typeof(DataTemplate), + typeof(ServerDrivenView), + null); + + public static readonly BindableProperty ErrorTemplateProperty = BindableProperty.Create( + nameof(ErrorTemplate), + typeof(DataTemplate), + typeof(ServerDrivenView), + null); + #endregion #region Properties @@ -35,6 +47,18 @@ public UIElementState State set => SetValue(StateProperty, value); } + public DataTemplate LoadingTemplate + { + get => (DataTemplate)GetValue(LoadingTemplateProperty); + set => SetValue(LoadingTemplateProperty, value); + } + + public DataTemplate ErrorTemplate + { + get => (DataTemplate)GetValue(ErrorTemplateProperty); + set => SetValue(ErrorTemplateProperty, value); + } + public Action? OnLoaded { get; diff --git a/Maui.ServerDrivenUI/Views/ServerDrivenVisualElement.cs b/Maui.ServerDrivenUI/Views/ServerDrivenVisualElement.cs index e038e81..62652e0 100644 --- a/Maui.ServerDrivenUI/Views/ServerDrivenVisualElement.cs +++ b/Maui.ServerDrivenUI/Views/ServerDrivenVisualElement.cs @@ -9,8 +9,10 @@ internal class ServerDrivenVisualElement internal static async Task InitializeComponentAsync(IServerDrivenVisualElement element, int attempt = 0) { + var errorMessage = string.Empty; try { + ShowLoadingView(element); MainThread.BeginInvokeOnMainThread(() => element.State = UIElementState.Loading); var serverDrivenUiService = ServiceProviderHelper @@ -60,6 +62,8 @@ internal static async Task InitializeComponentAsync(IServerDrivenVisualElement e } else { + errorMessage = SERVICE_NOT_FOUND; + MainThread.BeginInvokeOnMainThread(() => { element.State = UIElementState.Error; element.OnError(new DependencyRegistrationException(SERVICE_NOT_FOUND)); @@ -68,11 +72,17 @@ internal static async Task InitializeComponentAsync(IServerDrivenVisualElement e } catch (Exception ex) { + errorMessage = ex.Message; MainThread.BeginInvokeOnMainThread(() => { element.State = UIElementState.Error; element.OnError(ex); }); } + + if (!string.IsNullOrWhiteSpace(errorMessage)) + { + ShowErrorView(element, errorMessage); + } } private static bool IsXamlLoaded(IServerDrivenVisualElement element, int attempt) @@ -96,6 +106,50 @@ private static bool IsXamlLoaded(IServerDrivenVisualElement element, int attempt return true; } + private static void ShowLoadingView(IServerDrivenVisualElement element) + { + View loadingView = (element.LoadingTemplate?.CreateContent() as View) + ?? CreateDefaultLoadingTemplate(); + + SetContent(element, loadingView); + } + + private static void ShowErrorView(IServerDrivenVisualElement element, string errorMessage) + { + try + { + View errorView = (element.ErrorTemplate?.CreateContent() as View) + ?? CreateDefaultErrorTemplate(errorMessage); + + SetContent(element, errorView); + } + catch (Exception ex) + { + MainThread.BeginInvokeOnMainThread(() => { + element.State = UIElementState.Error; + element.OnError(ex); + }); + } + } + + private static void SetContent(IServerDrivenVisualElement element, View template) + { + if (element is ContentView contentView && template != null) + { + MainThread.BeginInvokeOnMainThread(() => { + template.BindingContext = contentView.BindingContext; + contentView.Content = template; + }); + } + else if (element is ContentPage contentPage && template != null) + { + MainThread.BeginInvokeOnMainThread(() => { + template.BindingContext = contentPage.BindingContext; + contentPage.Content = template; + }); + } + } + internal static void OnStatePropertyChanged(BindableObject bindable, object oldValue, object newValue) { if (bindable is not IServerDrivenVisualElement view @@ -106,4 +160,34 @@ internal static void OnStatePropertyChanged(BindableObject bindable, object oldV if (newState is UIElementState.Loaded) view.OnLoaded?.Invoke(); } + + internal static View CreateDefaultLoadingTemplate() => + new Frame() { + HorizontalOptions = LayoutOptions.Center, + VerticalOptions = LayoutOptions.Center, + BackgroundColor = Colors.Black, + BorderColor = Colors.Transparent, + Content = new ActivityIndicator() { + IsRunning = true, + IsEnabled = true, + Color = Colors.White, + HorizontalOptions = LayoutOptions.Center, + VerticalOptions = LayoutOptions.Center + } + }; + + internal static View CreateDefaultErrorTemplate(string errorMessage) => + new Frame() { + HorizontalOptions = LayoutOptions.Center, + VerticalOptions = LayoutOptions.Center, + BackgroundColor = Colors.Red, + BorderColor = Colors.Transparent, + Content = new Label { + Text = errorMessage, + HorizontalOptions = LayoutOptions.Center, + VerticalOptions = LayoutOptions.Center, + HorizontalTextAlignment = TextAlignment.Center, + VerticalTextAlignment = TextAlignment.Center + } + }; } diff --git a/README.md b/README.md index c181f73..b1d9f50 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,10 @@ public static class MauiProgram } ``` -- You can now use the ServerDrivenUI Elements +- You can now use the ServerDrivenUI Elements, defining the key that will be used to get the UI from the API +- You can also define a LoadingTemplate and an ErrorTemplate to be shown while the UI is being fetched from the API + +```csharp ```xaml @@ -53,7 +56,23 @@ public static class MauiProgram xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"> - + + + + + + + + + + + + + + + @@ -67,4 +86,4 @@ We are currently doing a [workaround](https://github.com/felipebaltazar/Maui.Ser ## Repo Activity -![Alt](https://repobeats.axiom.co/api/embed/e3457a9dc9131c33ca38ceb2203bfffa67864080.svg "Repobeats analytics image") +![Alt](https://repobeats.axiom.co/api/embed/e3457a9dc9131c33ca38ceb2203bfffa67864080.svg "Repo activity analytics image") diff --git a/samples/Maui.ServerDrivenUI.Sample/MainPage.xaml b/samples/Maui.ServerDrivenUI.Sample/MainPage.xaml index 1340a34..02cdc37 100644 --- a/samples/Maui.ServerDrivenUI.Sample/MainPage.xaml +++ b/samples/Maui.ServerDrivenUI.Sample/MainPage.xaml @@ -4,6 +4,22 @@ xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"> - + + + + + + + + + + + + + + + diff --git a/samples/Maui.ServerDrivenUI.Sample/ViewModelBase.cs b/samples/Maui.ServerDrivenUI.Sample/ViewModelBase.cs index 0c0c921..4e5629a 100644 --- a/samples/Maui.ServerDrivenUI.Sample/ViewModelBase.cs +++ b/samples/Maui.ServerDrivenUI.Sample/ViewModelBase.cs @@ -11,7 +11,6 @@ protected void OnPropertyChanged(string propertyName) => protected void Set(ref T field, T value, string propertyName) { - if (!EqualityComparer.Default.Equals(field, value)) { field = value;