Skip to content

Commit

Permalink
Merge pull request #11 from felipebaltazar/feature/api-sample
Browse files Browse the repository at this point in the history
Api sample + Cache Fixes
  • Loading branch information
felipebaltazar authored Feb 18, 2024
2 parents cc131b5 + 9a50071 commit 8095ff6
Show file tree
Hide file tree
Showing 27 changed files with 698 additions and 102 deletions.
2 changes: 2 additions & 0 deletions Maui.ServerDrivenUI/Abstractions/IServerDrivenUISettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

public interface IServerDrivenUISettings
{
string? CacheFilePath { get; set; }

HashSet<string> CacheEntryKeys { get; }

IUIElementResolver? ElementResolver { get; }
Expand Down
52 changes: 27 additions & 25 deletions Maui.ServerDrivenUI/AppBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using EasyCaching.LiteDB;
using Maui.ServerDrivenUI.Models;
using Maui.ServerDrivenUI.Services;
using Microsoft.Extensions.Logging;
using Microsoft.Maui.LifecycleEvents;

namespace Maui.ServerDrivenUI;
Expand All @@ -16,40 +15,43 @@ public static MauiAppBuilder ConfigureServerDrivenUI(this MauiAppBuilder builder
builder.Services.AddSingleton<IServerDrivenUISettings>(settings);
builder.Services.AddSingleton<IServerDrivenUIService, ServerDrivenUIService>();

builder.Services.AddEasyCaching(option =>
{
option.UseLiteDB(config =>
{
var dbFilePath = FileSystem.Current.AppDataDirectory + "sduicache.ldb";

config.DBConfig = new LiteDBDBOptions
{
FileName = dbFilePath,
ConnectionType = LiteDB.ConnectionType.Shared
};
});
});
builder.Services.AddEasyCaching(o =>
o.UseLiteDB(c => ConfigureDb(c, settings)));

builder.ConfigureLifecycleEvents(b =>
{
#if IOS
b.AddiOS(iOS => iOS.FinishedLaunching((app, launchOptions) =>
{
var service = Application.Current!.Handler.MauiContext!.Services.GetService<IServerDrivenUIService>();
_ = Task.Run(service!.FetchAsync);
return false;
}));
b.AddiOS(iOS => iOS.FinishedLaunching((app, launchOptions) => InitServerDrivenUIService()));
#elif ANDROID
b.AddAndroid(android => android.OnCreate(async (activity, bundle) =>
{
var service = Application.Current!.Handler.MauiContext!.Services.GetService<IServerDrivenUIService>();
await service!.FetchAsync().ConfigureAwait(false);
}));
b.AddAndroid(android => android.OnCreate((activity, bundle) => InitServerDrivenUIService()));
#elif MACCATALYST
b.AddiOS(macCatalyst => macCatalyst.FinishedLaunching((app, launchOptions) => InitServerDrivenUIService()));
#elif WINDOWS
b.AddWindows(windows => windows.OnLaunched((app, args) => InitServerDrivenUIService()));
#else
throw new NotImplementedException("Platform not implemented");
#endif
});

return builder;
}

private static void ConfigureDb(LiteDBOptions config, ServerDrivenUISettings settings)
{
var dbFilePath = settings.CacheFilePath
?? FileSystem.Current.AppDataDirectory + "sduicache.ldb";

config.DBConfig = new LiteDBDBOptions {
FileName = dbFilePath,
ConnectionType = LiteDB.ConnectionType.Shared,
};
}

private static bool InitServerDrivenUIService()
{
var service = Application.Current!.Handler.MauiContext!.Services.GetService<IServerDrivenUIService>();
_ = Task.Run(service!.FetchAsync);

return false;
}
}
2 changes: 2 additions & 0 deletions Maui.ServerDrivenUI/Models/ServerDrivenUISettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ namespace Maui.ServerDrivenUI.Models;

internal sealed class ServerDrivenUISettings : IServerDrivenUISettings
{
public string? CacheFilePath { get; set; }

public IUIElementResolver? ElementResolver { get; private set; }

public HashSet<string> CacheEntryKeys { get; } = [];
Expand Down
40 changes: 24 additions & 16 deletions Maui.ServerDrivenUI/Services/ServerDrivenUIService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public class ServerDrivenUIService

private readonly IEasyCachingProvider _cacheProvider = cacheProvider;
private readonly IServerDrivenUISettings _settings = settings;

private readonly TaskCompletionSource<bool> _fetchFinished = new();

#endregion
Expand All @@ -20,17 +21,7 @@ public Task ClearCacheAsync() =>

public async Task FetchAsync()
{
ServerUIElement[] elements;

try
{
elements = await DownloadServerElementsAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
_fetchFinished.TrySetResult(false);
throw new FetchException("Error while downloading server elements", ex);
}
var elements = await GetElementsAsync().ConfigureAwait(false);

try
{
Expand All @@ -47,16 +38,20 @@ public async Task FetchAsync()

public async Task<string> GetXamlAsync(string elementKey)
{
if (await _fetchFinished.Task.ConfigureAwait(false))
// Try to get the value from cache
var element = await _cacheProvider.GetAsync<ServerUIElement>(elementKey)
.ConfigureAwait(false);

// If the value is not in cache wait fetch finish, then try to get it from the cache again
if (!(element?.HasValue ?? true) && await _fetchFinished.Task.ConfigureAwait(false))
{
var element = await _cacheProvider.GetAsync<ServerUIElement>(elementKey).ConfigureAwait(false)
element = await _cacheProvider.GetAsync<ServerUIElement>(elementKey).ConfigureAwait(false)
?? throw new KeyNotFoundException($"Visual element not found for specified key: '{elementKey}'");

var xaml = element.Value.ToXaml();
return xaml;
}

return string.Empty;
return element?.Value?.ToXaml()
?? string.Empty;
}

#endregion
Expand All @@ -74,5 +69,18 @@ private Task<ServerUIElement[]> DownloadServerElementsAsync()
return Task.WhenAll(_settings.CacheEntryKeys.Select(_settings.ElementResolver.GetElementAsync));
}

private async Task<ServerUIElement[]> GetElementsAsync()
{
try
{
return await DownloadServerElementsAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
_fetchFinished.TrySetResult(false);
throw new FetchException("Error while downloading server elements", ex);
}
}

#endregion
}
2 changes: 1 addition & 1 deletion Maui.ServerDrivenUI/Views/ServerDrivenView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public UIElementState State
set => SetValue(StateProperty, value);
}

public Action OnLoaded
public Action? OnLoaded
{
get;

Check warning on line 40 in Maui.ServerDrivenUI/Views/ServerDrivenView.cs

View workflow job for this annotation

GitHub Actions / Build and publish package

Nullability of reference types in return type of 'Action? ServerDrivenView.OnLoaded.get' doesn't match implicitly implemented member 'Action IServerDrivenVisualElement.OnLoaded.get' (possibly because of nullability attributes).
set;
Expand Down
66 changes: 42 additions & 24 deletions Maui.ServerDrivenUI/Views/ServerDrivenVisualElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,66 +4,84 @@ namespace Maui.ServerDrivenUI.Views;

internal class ServerDrivenVisualElement
{
private const string SERVICE_NOT_FOUND = "IServerDrivenUIService not found, make sure you are calling 'ConfigureServerDrivenUI(s=> s.RegisterElementGetter((k)=> yourApiCall(k)))'";

internal static async Task InitializeComponentAsync(IServerDrivenVisualElement element)
{
try
{
MainThread.BeginInvokeOnMainThread(() => element.State = UIElementState.Loading);

var serverDrivenUiService = ServiceProviderHelper.ServiceProvider?.GetService<IServerDrivenUIService>();
var serverDrivenUiService = ServiceProviderHelper
.ServiceProvider?
.GetService<IServerDrivenUIService>();

if (serverDrivenUiService != null)
{
var xaml = await serverDrivenUiService.GetXamlAsync(element.ServerKey ?? element.GetType().Name).ConfigureAwait(false);
MainThread.BeginInvokeOnMainThread(() => {
var xaml = await serverDrivenUiService
.GetXamlAsync(element.ServerKey ?? element.GetType().Name)
.ConfigureAwait(false);

MainThread.BeginInvokeOnMainThread(() =>
{
var onLoaded = element.OnLoaded;
(element as VisualElement)?.LoadFromXaml(xaml);
var visualElement = (element as VisualElement);
var currentBindingContext = visualElement?.BindingContext;

if (element is ServerDrivenContentPage page)
{
if (page.Content is null)
{
_ = Task.Run(() => InitializeComponentAsync(element));
return;
}
}
else if (element is ServerDrivenView view)
{
if (view.Content is null)
{
_ = Task.Run(() => InitializeComponentAsync(element));
return;
}
}
visualElement?.LoadFromXaml(xaml);

if (!IsXamlLoaded(element))
return;

if(visualElement != null)
visualElement.BindingContext = currentBindingContext;

element.OnLoaded = onLoaded;
element.State = UIElementState.Loaded;
});
}
else
{
MainThread.BeginInvokeOnMainThread(() => {
MainThread.BeginInvokeOnMainThread(() =>
{
element.State = UIElementState.Error;
element.OnError(new DependencyRegistrationException("IServerDrivenUIService not found, make sure you are calling 'ConfigureServerDrivenUI(s=> s.RegisterElementGetter((k)=> yourApiCall(k)))'"));
element.OnError(new DependencyRegistrationException(SERVICE_NOT_FOUND));
});
}
}
catch (Exception ex)
{
MainThread.BeginInvokeOnMainThread(() => {
MainThread.BeginInvokeOnMainThread(() =>
{
element.State = UIElementState.Error;
element.OnError(ex);
});
}
}

private static bool IsXamlLoaded(IServerDrivenVisualElement element)
{
switch (element)
{
case ServerDrivenContentPage page when page.Content is null:
_ = Task.Run(() => InitializeComponentAsync(element));
return false;
case ServerDrivenView view when view.Content is null:
_ = Task.Run(() => InitializeComponentAsync(element));
return false;
}

return true;
}

internal static void OnStatePropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is not IServerDrivenVisualElement view
|| newValue is not UIElementState newState)
return;

view.OnStateChanged(newState);
if(newState is UIElementState.Loaded)
if (newState is UIElementState.Loaded)
view.OnLoaded?.Invoke();
}
}
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Server Driven UI library for dotnet MAUI. New features to be deployed on all pla
[![Build and publish packages](https://github.com/felipebaltazar/Maui.ServerDrivenUI/actions/workflows/PackageCI.yml/badge.svg)](https://github.com/felipebaltazar/Maui.ServerDrivenUI/actions/workflows/PackageCI.yml)


Sample Api Response: https://serverdrivenui.azurewebsites.net/ServerDrivenUI?key=MyView

## Getting started

- Install the ServerDrivenUI.Maui package
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Microsoft.AspNetCore.Mvc;
using System.Reflection;
using System.Text.Json;

namespace Maui.ServerDrivenUI.ApiSample.Controllers
{
[ApiController]
[Route("[controller]")]
public class ServerDrivenUIController : ControllerBase
{
private readonly ILogger<ServerDrivenUIController> _logger;

public ServerDrivenUIController(ILogger<ServerDrivenUIController> logger)
{
_logger = logger;
}

[HttpGet(Name = "GetUIElement")]
public async Task<ServerUIElement> Get(string key)
{
var assembly = Assembly.GetExecutingAssembly();
var resourceName = $"{key}.json";
using var stream = assembly.GetManifestResourceStream(resourceName);

if (stream is null)
throw new Exception("Resource not found");

var result = await JsonSerializer.DeserializeAsync<ServerUIElement>(stream) ?? throw new Exception("Fake api error");

return result;
}
}
}
14 changes: 14 additions & 0 deletions samples/Maui.ServerDrivenUI.ApiSample/CustomNamespace.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace Maui.ServerDrivenUI.ApiSample;

/// <summary>
/// Represents a xaml namespace eg.: xmlns:alias="clr-namespace:namespace;assembly=assembly"
/// </summary>
/// <param name="alias">Define the alias from custom namespaces</param>
/// <param name="namespace">clr-namespace</param>
/// <param name="assembly">Assembly where the custom control is located</param>
public class CustomNamespace(string alias, string @namespace, string? assembly = null)
{
public string Alias { get; private set; } = alias;
public string Namespace { get; private set; } = @namespace;
public string? Assembly { get; private set; } = assembly;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>

<ItemGroup>
<Content Remove="Resources\MyView.json" />
</ItemGroup>

<ItemGroup>
<EmbeddedResource Include="Resources\MyView.json">
<LogicalName>%(RecursiveDir)%(Filename)%(Extension)</LogicalName>
</EmbeddedResource>
</ItemGroup>

<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@Maui.ServerDrivenUI.ApiSample_HostAddress = http://localhost:5102

GET {{Maui.ServerDrivenUI.ApiSample_HostAddress}}/weatherforecast/
Accept: application/json

###
18 changes: 18 additions & 0 deletions samples/Maui.ServerDrivenUI.ApiSample/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

app.UseSwagger();
app.UseSwaggerUI();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
Loading

0 comments on commit 8095ff6

Please sign in to comment.