diff --git a/src/SharedMauiCoreLibrary/Hosting/AppHostBuilderExtensions.cs b/src/SharedMauiCoreLibrary/Hosting/AppHostBuilderExtensions.cs index 694b232..b192b04 100644 --- a/src/SharedMauiCoreLibrary/Hosting/AppHostBuilderExtensions.cs +++ b/src/SharedMauiCoreLibrary/Hosting/AppHostBuilderExtensions.cs @@ -1,7 +1,11 @@ -using AndreasReitberger.Shared.Core.Interfaces; +using AndreasReitberger.Shared.Core.Dispatch; +using AndreasReitberger.Shared.Core.Events; +using AndreasReitberger.Shared.Core.Interfaces; using AndreasReitberger.Shared.Core.Localization; using AndreasReitberger.Shared.Core.NavigationManager; using CommunityToolkit.Maui; +using Microsoft.Extensions.DependencyInjection.Extensions; +using System.Diagnostics; using System.Runtime.Versioning; namespace AndreasReitberger.Shared.Core.Hosting @@ -12,9 +16,42 @@ namespace AndreasReitberger.Shared.Core.Hosting [SupportedOSPlatform(SPlatforms.WindowsVersion)] public static class AppHostBuilderExtensions { + /// + /// Configures the core library features for a .NET MAUI application using the specified builder. + /// Included features: + /// - UseMauiCommunityToolkit() + /// - ConfigureDispatching() + /// + /// This method adds essential services and features from the core library, including the + /// .NET MAUI Community Toolkit and dispatching support. Call this method during application startup to ensure + /// required functionality is available throughout the app. + /// The instance to configure. Cannot be null. + /// The same instance, enabling method chaining. public static MauiAppBuilder ConfigureCoreLibrary(this MauiAppBuilder builder) { - builder.UseMauiCommunityToolkit(); + builder + .UseMauiCommunityToolkit() + .ConfigureDispatching() + .RegisterDispatcher() + ; + return builder; + } + + public static MauiAppBuilder RegisterDispatcher(this MauiAppBuilder builder, IDispatcher? dispatcher = null) + { + dispatcher ??= builder.Services.BuildServiceProvider().GetService(); + if (dispatcher is not null) + { + DispatchManager manager = new(dispatcher); +#if DEBUG + manager.Error += (a, b) => + { + if (b is DispatchErrorEventArgs args) + Debug.WriteLine($"{nameof(DispatchManager)}: Dispatch error occured: \n{args}"); + }; +#endif + builder.Services.TryAddSingleton(manager); + } return builder; } @@ -25,7 +62,7 @@ public static MauiAppBuilder ConfigureLocalizationManager(this MauiAppBuilder bu BaseFlagImageUri = flagImageBaseDir }; manager.SetLanguages(languages); - builder.Services.AddSingleton(manager); + builder.Services.TryAddSingleton(manager); return builder; } @@ -35,7 +72,7 @@ public static MauiAppBuilder ConfigureLocalizationManager(this MauiAppBuilder bu { BaseFlagImageUri = flagImageBaseDir }; - builder.Services.AddSingleton(manager); + builder.Services.TryAddSingleton(manager); return builder; } @@ -44,7 +81,7 @@ public static MauiAppBuilder ConfigureShellNavigator(this MauiAppBuilder builder dispatcher ??= builder.Services.BuildServiceProvider().GetService(); ShellNavigator navigator = dispatcher is not null ? new(rootPage, dispatcher) : new(rootPage); navigator.AvailableEntryPages = [.. entryPages ?? [ rootPage]]; - builder.Services.AddSingleton(navigator); + builder.Services.TryAddSingleton(navigator); return builder; } } diff --git a/src/SharedMauiCoreLibrary/Interfaces/IDispatchManager.cs b/src/SharedMauiCoreLibrary/Interfaces/IDispatchManager.cs new file mode 100644 index 0000000..5e0bd1b --- /dev/null +++ b/src/SharedMauiCoreLibrary/Interfaces/IDispatchManager.cs @@ -0,0 +1,28 @@ +namespace AndreasReitberger.Shared.Core.Interfaces +{ + public interface IDispatchManager + { + #region Instance + public static IDispatchManager? Instance { get; private set; } + #endregion + + #region Properties + public IDispatcher? Dispatcher { get; set; } + #endregion + + #region Methods + public void Dispatch(IDispatcher? dispatcher, Action action, bool forceUiThread = false); + public void Dispatch(Action action, bool forceUiThread = false); + public void Dispatch(IDispatcher? dispatcher, Func action, bool forceUiThread = false); + public void Dispatch(Func action, bool forceUiThread = false); + public Task DispatchAsync(IDispatcher? dispatcher, Action action, bool forceUiThread = false); + public Task DispatchAsync(Action action, bool forceUiThread = false); + public Task DispatchAsync(IDispatcher? dispatcher, Func action, bool forceUiThread = false); + public Task DispatchAsync(Func action, bool forceUiThread = false); + #endregion + + #region Events + public event EventHandler? Error; + #endregion + } +} diff --git a/src/SharedMauiCoreLibrary/Models/Dispatch/DispatchManager.Events.cs b/src/SharedMauiCoreLibrary/Models/Dispatch/DispatchManager.Events.cs new file mode 100644 index 0000000..3d83390 --- /dev/null +++ b/src/SharedMauiCoreLibrary/Models/Dispatch/DispatchManager.Events.cs @@ -0,0 +1,26 @@ +using AndreasReitberger.Shared.Core.Events; +using AndreasReitberger.Shared.Core.Interfaces; + +namespace AndreasReitberger.Shared.Core.Dispatch +{ + public partial class DispatchManager : ObservableObject, IDispatchManager + { + #region Events + public event EventHandler? Error; + + protected virtual void OnError() + { + Error?.Invoke(this, EventArgs.Empty); + } + protected virtual void OnError(DispatchErrorEventArgs e) + { + Error?.Invoke(this, e); + } + protected virtual void OnError(UnhandledExceptionEventArgs e) + { + Error?.Invoke(this, e); + } + #endregion + + } +} diff --git a/src/SharedMauiCoreLibrary/Models/Dispatch/DispatchManager.cs b/src/SharedMauiCoreLibrary/Models/Dispatch/DispatchManager.cs new file mode 100644 index 0000000..c3ca1d3 --- /dev/null +++ b/src/SharedMauiCoreLibrary/Models/Dispatch/DispatchManager.cs @@ -0,0 +1,233 @@ +using AndreasReitberger.Shared.Core.Interfaces; +using System.Diagnostics; + +namespace AndreasReitberger.Shared.Core.Dispatch +{ + public partial class DispatchManager : ObservableObject, IDispatchManager + { + + #region Instance + static IDispatchManager? _instance = null; +#if NET9_0_OR_GREATER + static readonly Lock Lock = new(); +#else + static readonly object Lock = new(); +#endif + public static IDispatchManager Instance + { + get + { + lock (Lock) + { + _instance ??= new DispatchManager(); + } + return _instance; + } + set + { + if (_instance == value) return; + lock (Lock) + { + _instance = value; + } + } + } + #endregion + + #region Properties + [ObservableProperty] + public partial IDispatcher? Dispatcher { get; set; } + #endregion + + #region Ctor + public DispatchManager() + { + Dispatcher ??= DispatcherProvider.Current.GetForCurrentThread(); + } + public DispatchManager(IDispatcher? dispatcher) : this() + { + Dispatcher = dispatcher; + } + #endregion + + #region Methods + + public void Dispatch(IDispatcher? dispatcher, Action action, bool forceUiThread = false) + { + dispatcher ??= DispatcherProvider.Current.GetForCurrentThread(); + if (dispatcher is null) + { +#if DEBUG + // Show when a dispatching was done or not + Debug.WriteLine($"{nameof(DispatchManager)}: Dispatcher => The `Dispatcher` property was null!"); +#endif + return; + } +#if DEBUG + // Show when a dispatching was done or not + Debug.WriteLine($"{nameof(DispatchManager)}: Dispatcher => '{(dispatcher.IsDispatchRequired ? "dispatched" : "not dispatched")}'"); + if (dispatcher.IsDispatchRequired) + { + // Just for a breaking point + } +#endif + if (dispatcher.IsDispatchRequired || forceUiThread) + { + dispatcher.Dispatch(() => + { + try + { + action.Invoke(); + } + catch (Exception exc) + { + OnError(new Events.DispatchErrorEventArgs(exc) + { + DispatchRequired = dispatcher.IsDispatchRequired + }); + } + }); + } + else + { + action.Invoke(); + } + } + public void Dispatch(Action action, bool forceUiThread = false) + => Dispatch(dispatcher: Dispatcher, action: action, forceUiThread: forceUiThread); + + public void Dispatch(IDispatcher? dispatcher, Func action, bool forceUiThread = false) + { + dispatcher ??= DispatcherProvider.Current.GetForCurrentThread(); + if (dispatcher is null) + { +#if DEBUG + // Show when a dispatching was done or not + Debug.WriteLine($"{nameof(DispatchManager)}: Dispatcher => The `Dispatcher` property was null!"); +#endif + return; + } +#if DEBUG + // Show when a dispatching was done or not + Debug.WriteLine($"{nameof(DispatchManager)}: Dispatcher => '{(dispatcher.IsDispatchRequired ? "dispatched" : "not dispatched")}'"); + if (dispatcher.IsDispatchRequired) + { + // Just for a breaking point + } +#endif + if (dispatcher.IsDispatchRequired || forceUiThread) + { + dispatcher.Dispatch(() => + { + try + { + action.Invoke(); + } + catch (Exception exc) + { + OnError(new Events.DispatchErrorEventArgs(exc) + { + DispatchRequired = dispatcher.IsDispatchRequired + }); + } + }); + } + else + { + action.Invoke(); + } + } + public void Dispatch(Func action, bool forceUiThread = false) + => Dispatch(dispatcher: Dispatcher, action: action, forceUiThread: forceUiThread); + + public async Task DispatchAsync(IDispatcher? dispatcher, Action action, bool forceUiThread = false) + { + dispatcher ??= DispatcherProvider.Current.GetForCurrentThread(); + if (dispatcher is null) + { +#if DEBUG + // Show when a dispatching was done or not + Debug.WriteLine($"{nameof(DispatchManager)}: Dispatcher => The `Dispatcher` property was null!"); +#endif + return; + } +#if DEBUG + // Show when a dispatching was done or not + Debug.WriteLine($"{nameof(DispatchManager)}: Dispatcher => '{(dispatcher.IsDispatchRequired ? "dispatched" : "not dispatched")}'"); + if (dispatcher.IsDispatchRequired) + { + // Just for a breaking point + } +#endif + if (dispatcher.IsDispatchRequired || forceUiThread) + { + await dispatcher.DispatchAsync(() => + { + try + { + action.Invoke(); + } + catch (Exception exc) + { + OnError(new Events.DispatchErrorEventArgs(exc) + { + DispatchRequired = dispatcher.IsDispatchRequired + }); + } + }); + } + else + { + action.Invoke(); + } + } + public Task DispatchAsync(Action action, bool forceUiThread = false) + => DispatchAsync(dispatcher: Dispatcher, action: action, forceUiThread: forceUiThread); + + public async Task DispatchAsync(IDispatcher? dispatcher, Func action, bool forceUiThread = false) + { + dispatcher ??= DispatcherProvider.Current.GetForCurrentThread(); + if (dispatcher is null) + { +#if DEBUG + // Show when a dispatching was done or not + Debug.WriteLine($"{nameof(DispatchManager)}: Dispatcher => The `Dispatcher` property was null!"); +#endif + return; + } +#if DEBUG + // Show when a dispatching was done or not + Debug.WriteLine($"{nameof(DispatchManager)}: Dispatcher => '{(dispatcher.IsDispatchRequired ? "dispatched" : "not dispatched")}'"); + if (dispatcher.IsDispatchRequired) + { + // Just for a breaking point + } +#endif + if (dispatcher.IsDispatchRequired || forceUiThread) + { + await dispatcher.DispatchAsync(async () => + { + try + { + await action.Invoke(); + } + catch (Exception exc) + { + OnError(new Events.DispatchErrorEventArgs(exc) + { + DispatchRequired = dispatcher.IsDispatchRequired + }); + } + }); + } + else + { + await action.Invoke(); + } + } + public Task DispatchAsync(Func action, bool forceUiThread = false) + => DispatchAsync(dispatcher: Dispatcher, action: action, forceUiThread: forceUiThread); + #endregion + + } +} diff --git a/src/SharedMauiCoreLibrary/Models/Events/DispatchErrorEventArgs.cs b/src/SharedMauiCoreLibrary/Models/Events/DispatchErrorEventArgs.cs new file mode 100644 index 0000000..5f3de31 --- /dev/null +++ b/src/SharedMauiCoreLibrary/Models/Events/DispatchErrorEventArgs.cs @@ -0,0 +1,19 @@ +namespace AndreasReitberger.Shared.Core.Events +{ + public partial class DispatchErrorEventArgs : EventArgs + { + #region Properties + public bool DispatchRequired { get; set; } + public Exception? Exception { get; set; } + + #endregion + + #region Ctor + public DispatchErrorEventArgs() { } + public DispatchErrorEventArgs(Exception exception) + { + Exception = exception; + } + #endregion + } +} diff --git a/src/SharedMauiCoreLibrary/SharedMauiCoreLibrary.csproj b/src/SharedMauiCoreLibrary/SharedMauiCoreLibrary.csproj index f87a37b..d1ac2d9 100644 --- a/src/SharedMauiCoreLibrary/SharedMauiCoreLibrary.csproj +++ b/src/SharedMauiCoreLibrary/SharedMauiCoreLibrary.csproj @@ -1,7 +1,7 @@  - - + + MAUI, Utilites, Core, Library, Helper, C#, dotnet