From 315df2541e8b0f46529c3b136870b6ed0a5b26b1 Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Tue, 9 Apr 2024 14:55:20 -0600 Subject: [PATCH 01/11] chore: centralizing props --- Directory.Build.props | 2 ++ Directory.Build.targets | 5 +++++ src/MauiMicroMvvm.Rx/MauiMicroMvvm.Rx.csproj | 3 --- src/MauiMicroMvvm.Templates/MauiMicroMvvm.Templates.csproj | 2 +- src/MauiMicroMvvm/MauiMicroMvvm.csproj | 3 --- 5 files changed, 8 insertions(+), 7 deletions(-) create mode 100644 Directory.Build.targets diff --git a/Directory.Build.props b/Directory.Build.props index 572dfe7..f6d9bd9 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -15,7 +15,9 @@ https://github.com/AvantiPoint/mauimicromvvm en enable + enable true + $(NoWarn);NU1507 diff --git a/Directory.Build.targets b/Directory.Build.targets new file mode 100644 index 0000000..becdd4d --- /dev/null +++ b/Directory.Build.targets @@ -0,0 +1,5 @@ + + + AvantiPoint.$(AssemblyName) + + \ No newline at end of file diff --git a/src/MauiMicroMvvm.Rx/MauiMicroMvvm.Rx.csproj b/src/MauiMicroMvvm.Rx/MauiMicroMvvm.Rx.csproj index 8a6c3ed..cc6b1c6 100644 --- a/src/MauiMicroMvvm.Rx/MauiMicroMvvm.Rx.csproj +++ b/src/MauiMicroMvvm.Rx/MauiMicroMvvm.Rx.csproj @@ -2,12 +2,9 @@ $(DotNetVersion) - true - enable True MauiMicroMvvm.Rx is the perfect companion for people who love Reactive design & want to couple it with MauiMicro. With MauiMicro Rx you get an Observables first base ViewModel that let's you design your code around an observable for the App & View lifecycles. dotnet-maui;mvvm;mauimicro;reactive;rx - AvantiPoint.$(AssemblyName) $(AssemblyName) diff --git a/src/MauiMicroMvvm.Templates/MauiMicroMvvm.Templates.csproj b/src/MauiMicroMvvm.Templates/MauiMicroMvvm.Templates.csproj index e3f9158..9c4cccd 100644 --- a/src/MauiMicroMvvm.Templates/MauiMicroMvvm.Templates.csproj +++ b/src/MauiMicroMvvm.Templates/MauiMicroMvvm.Templates.csproj @@ -7,9 +7,9 @@ false false True + true Project Template for Maui Micro by AvantiPoint dotnet-maui;mauimicro;mauimicromvvm;mauimicrotemplates;templates;mvvm;maui; - AvantiPoint.$(AssemblyName) MauiMicroMvvm Templates $(NoWarn);NU5128 diff --git a/src/MauiMicroMvvm/MauiMicroMvvm.csproj b/src/MauiMicroMvvm/MauiMicroMvvm.csproj index 3cda0f4..b318b04 100644 --- a/src/MauiMicroMvvm/MauiMicroMvvm.csproj +++ b/src/MauiMicroMvvm/MauiMicroMvvm.csproj @@ -2,12 +2,9 @@ $(DotNetVersion) - true - enable True MauiMicroMvvm is a micro Mvvm Framework built specifically for use with .NET MAUI Shell applications. It's built in a way that gives you the flexibility to do what you need with a proper decoupling between the View & ViewModel that many frameworks seems to mess up. dotnet-maui;mvvm;mauimicro; - AvantiPoint.$(AssemblyName) MauiMicroMvvm From 20377617417fcc59dbda8777aeff9c8dd8b4b2b7 Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Tue, 9 Apr 2024 14:55:55 -0600 Subject: [PATCH 02/11] feat: introduce IDestructible --- src/MauiMicroMvvm/IDestructible.cs | 6 ++++++ src/MauiMicroMvvm/IDestructibleAsync.cs | 6 ++++++ src/MauiMicroMvvm/MauiMicroViewModel.cs | 24 ++++++++++++++++-------- 3 files changed, 28 insertions(+), 8 deletions(-) create mode 100644 src/MauiMicroMvvm/IDestructible.cs create mode 100644 src/MauiMicroMvvm/IDestructibleAsync.cs diff --git a/src/MauiMicroMvvm/IDestructible.cs b/src/MauiMicroMvvm/IDestructible.cs new file mode 100644 index 0000000..23bb149 --- /dev/null +++ b/src/MauiMicroMvvm/IDestructible.cs @@ -0,0 +1,6 @@ +namespace MauiMicroMvvm; + +public interface IDestructible +{ + void Destroy(); +} diff --git a/src/MauiMicroMvvm/IDestructibleAsync.cs b/src/MauiMicroMvvm/IDestructibleAsync.cs new file mode 100644 index 0000000..5a54522 --- /dev/null +++ b/src/MauiMicroMvvm/IDestructibleAsync.cs @@ -0,0 +1,6 @@ +namespace MauiMicroMvvm; + +public interface IDestructibleAsync +{ + Task DestroyAsync(); +} diff --git a/src/MauiMicroMvvm/MauiMicroViewModel.cs b/src/MauiMicroMvvm/MauiMicroViewModel.cs index 189ae8a..17d0f1b 100644 --- a/src/MauiMicroMvvm/MauiMicroViewModel.cs +++ b/src/MauiMicroMvvm/MauiMicroViewModel.cs @@ -25,8 +25,8 @@ protected MauiMicroViewModel(ViewModelContext context) protected IPageDialogs PageDialogs { get; } - public event PropertyChangedEventHandler PropertyChanged; - public event PropertyChangingEventHandler PropertyChanging; + public event PropertyChangedEventHandler? PropertyChanged; + public event PropertyChangingEventHandler? PropertyChanging; public bool IsBusy { @@ -50,17 +50,25 @@ public virtual void OnSleep() { } protected virtual void OnParametersSet() { } - protected T Get(T defaultValue = default, [CallerMemberName]string propertyName = null) + protected T Get(T? defaultValue = default, [CallerMemberName]string? propertyName = null) { - if (_properties.ContainsKey(propertyName)) - return (T)_properties[propertyName]; + ArgumentException.ThrowIfNullOrEmpty(propertyName); + if (_properties.TryGetValue(propertyName, out var value) && value is T valueAsT) + return valueAsT; - return defaultValue; + if (defaultValue is null && typeof(T).IsValueType) + defaultValue = Activator.CreateInstance(); + + if (Nullable.GetUnderlyingType(typeof(T)) != null) + ArgumentNullException.ThrowIfNull(defaultValue); + + return defaultValue!; } - protected bool Set(T value, [CallerMemberName]string propertyName = null) + protected bool Set(T value, [CallerMemberName]string? propertyName = null) { - if(EqualityComparer.Default.Equals(Get(propertyName: propertyName), value)) + ArgumentException.ThrowIfNullOrEmpty(propertyName); + if (EqualityComparer.Default.Equals(Get(propertyName: propertyName), value)) return false; PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(propertyName)); From b21da6d5ee86970dd06ac54abdca14fed9fc4c34 Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Tue, 9 Apr 2024 14:56:05 -0600 Subject: [PATCH 03/11] chore: adding MvvmHelpers --- src/MauiMicroMvvm/Common/MvvmHelpers.cs | 47 +++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/MauiMicroMvvm/Common/MvvmHelpers.cs diff --git a/src/MauiMicroMvvm/Common/MvvmHelpers.cs b/src/MauiMicroMvvm/Common/MvvmHelpers.cs new file mode 100644 index 0000000..51b065e --- /dev/null +++ b/src/MauiMicroMvvm/Common/MvvmHelpers.cs @@ -0,0 +1,47 @@ +using Microsoft.Maui.Controls; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MauiMicroMvvm.Common; + +public static class MvvmHelpers +{ + public static void InvokeViewViewModelAction(object? value, Action action) + { + if (value is T valueAsT) + { + action(valueAsT); + } + + if (value is BindableObject bindable) + { + InvokeViewViewModelAction(bindable.BindingContext, action); + } + } + + public static async Task InvokeViewViewModelActionAsync(object? value, Func action) + { + if (value is T valueAsT) + { + await action(valueAsT); + } + + if (value is BindableObject bindable) + { + await InvokeViewViewModelActionAsync(bindable.BindingContext, action); + } + } + + public static void Destroy(object? page) + { + InvokeViewViewModelAction(page, x => x.Destroy()); + } + + public static Task DestroyAsync(object? page) + { + return InvokeViewViewModelActionAsync(page, x => x.DestroyAsync()); + } +} From 03e3bfbf362d38511f5e2254151dacfc96393fa0 Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Tue, 9 Apr 2024 15:40:45 -0600 Subject: [PATCH 04/11] chore: updating IsPackable property --- Directory.Build.props | 1 + 1 file changed, 1 insertion(+) diff --git a/Directory.Build.props b/Directory.Build.props index f6d9bd9..4d4d0a8 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -18,6 +18,7 @@ enable true $(NoWarn);NU1507 + $(MSBuildProjectName.Contains('MauiMicroMvvm')) From f4cb8fabaaea167d2c4cea9198366c5c46dcfbb3 Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Tue, 9 Apr 2024 15:41:01 -0600 Subject: [PATCH 05/11] chore: adding Packages.props and targets --- MauiMicroMvvm.sln | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MauiMicroMvvm.sln b/MauiMicroMvvm.sln index c005917..d368dd1 100644 --- a/MauiMicroMvvm.sln +++ b/MauiMicroMvvm.sln @@ -18,6 +18,8 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{58DDD03C-E4B7-431A-8BC4-B8A8199C3DCD}" ProjectSection(SolutionItems) = preProject Directory.Build.props = Directory.Build.props + Directory.Build.targets = Directory.Build.targets + Directory.Packages.props = Directory.Packages.props EndProjectSection EndProject Global From e462b3638be6660a33d45bcc33c870a92f81491a Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Tue, 9 Apr 2024 16:02:58 -0600 Subject: [PATCH 06/11] chore: cleaning up nullable properties --- src/MauiMicroMvvm.Rx/RxMauiMicroViewModel.cs | 4 +- .../Internals/AppLifecycleBehavior.cs | 52 ++++++++++++------- .../MauiMicroBuilderExtensions.cs | 10 ++-- 3 files changed, 39 insertions(+), 27 deletions(-) diff --git a/src/MauiMicroMvvm.Rx/RxMauiMicroViewModel.cs b/src/MauiMicroMvvm.Rx/RxMauiMicroViewModel.cs index f2e4adf..012ec52 100644 --- a/src/MauiMicroMvvm.Rx/RxMauiMicroViewModel.cs +++ b/src/MauiMicroMvvm.Rx/RxMauiMicroViewModel.cs @@ -12,7 +12,7 @@ public class RxMauiMicroViewModel : ReactiveObject, IViewModelActivation, IViewL private readonly Subject _viewLifecycleState; private readonly Subject> _queryParameters; private readonly Lazy _lazyLogger; - protected ObservableAsPropertyHelper IsBusyHelper; + protected ObservableAsPropertyHelper? IsBusyHelper; private readonly ObservableAsPropertyHelper _isNotBusyHelper; protected readonly CompositeDisposable Disposables; @@ -21,7 +21,7 @@ public RxMauiMicroViewModel(ViewModelContext context) _applifecycleState = new Subject(); _viewLifecycleState = new Subject(); _queryParameters = new Subject>(); - Disposables = new CompositeDisposable(); + Disposables = []; Navigation = context.Navigation; PageDialogs = context.PageDialogs; _lazyLogger = new Lazy(() => context.Logger.CreateLogger(GetType().Name)); diff --git a/src/MauiMicroMvvm/Internals/AppLifecycleBehavior.cs b/src/MauiMicroMvvm/Internals/AppLifecycleBehavior.cs index 5345a33..406c9b6 100644 --- a/src/MauiMicroMvvm/Internals/AppLifecycleBehavior.cs +++ b/src/MauiMicroMvvm/Internals/AppLifecycleBehavior.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using MauiMicroMvvm.Common; using MauiMicroMvvm.Xaml; namespace MauiMicroMvvm.Internals; @@ -8,13 +9,15 @@ public class AppLifecycleBehavior : Behavior { private bool _didAppear; private bool _isVisible; - private Window _window; - public Page Page { get; set; } + private Window? _window; + public Page? Page { get; set; } - public BindableObject View { get; set; } + public BindableObject? View { get; set; } protected override void OnAttachedTo(BindableObject bindable) { + ArgumentNullException.ThrowIfNull(Page); + ArgumentNullException.ThrowIfNull(View); base.OnAttachedTo(bindable); Page.Appearing += OnAppearing; Page.Disappearing += OnDisappearing; @@ -34,13 +37,19 @@ protected override void OnAttachedTo(BindableObject bindable) } } - protected override void OnDetachingFrom(BindableObject bindable) + protected override async void OnDetachingFrom(BindableObject bindable) { + ArgumentNullException.ThrowIfNull(Page); + ArgumentNullException.ThrowIfNull(View); + base.OnDetachingFrom(bindable); Page.Appearing -= OnAppearing; Page.Disappearing -= OnDisappearing; Page.PropertyChanged -= OnPagePropertyChanged; View.PropertyChanged -= OnViewPropertyChanged; + + MvvmHelpers.Destroy(View); + await MvvmHelpers.DestroyAsync(View); if (_window is not null) { _window.Resumed -= OnResumed; @@ -50,8 +59,11 @@ protected override void OnDetachingFrom(BindableObject bindable) Page = null; } - private void OnViewPropertyChanged(object sender, PropertyChangedEventArgs e) + private void OnViewPropertyChanged(object? sender, PropertyChangedEventArgs e) { + ArgumentNullException.ThrowIfNull(Page); + ArgumentNullException.ThrowIfNull(View); + if (e.PropertyName != MauiMicro.SharedContextProperty.PropertyName) return; @@ -60,8 +72,11 @@ private void OnViewPropertyChanged(object sender, PropertyChangedEventArgs e) MauiMicro.SetSharedContext(Page, value); } - private void OnPagePropertyChanged(object sender, PropertyChangedEventArgs e) + private void OnPagePropertyChanged(object? sender, PropertyChangedEventArgs e) { + ArgumentNullException.ThrowIfNull(Page); + ArgumentNullException.ThrowIfNull(View); + if (e.PropertyName != MauiMicro.SharedContextProperty.PropertyName) return; @@ -70,32 +85,29 @@ private void OnPagePropertyChanged(object sender, PropertyChangedEventArgs e) MauiMicro.SetSharedContext(View, value); } - private void OnResumed(object sender, EventArgs e) + private void OnResumed(object? sender, EventArgs e) { - if (_isVisible && View.BindingContext is IAppLifecycle lifecycle) - lifecycle.OnResume(); + MvvmHelpers.InvokeViewViewModelAction(View, x => x.OnResume()); } - private void OnStopped(object sender, EventArgs e) + private void OnStopped(object? sender, EventArgs e) { - if (_isVisible && View.BindingContext is IAppLifecycle lifecycle) - lifecycle.OnSleep(); + MvvmHelpers.InvokeViewViewModelAction(View, x => x.OnSleep()); } - private void OnAppearing(object sender, EventArgs e) + private void OnAppearing(object? sender, EventArgs e) { - if (!_didAppear && View.BindingContext is IViewModelActivation initialize) - initialize.OnFirstLoad(); + if (!_didAppear) + MvvmHelpers.InvokeViewViewModelAction(View, x => x.OnFirstLoad()); _didAppear = true; - if (View.BindingContext is IViewLifecycle lifecycle) - lifecycle.OnAppearing(); + + MvvmHelpers.InvokeViewViewModelAction(View, x => x.OnAppearing()); _isVisible = true; } - private void OnDisappearing(object sender, EventArgs e) + private void OnDisappearing(object? sender, EventArgs e) { - if (View.BindingContext is IViewLifecycle lifecycle) - lifecycle.OnDisappearing(); + MvvmHelpers.InvokeViewViewModelAction(View, x => x.OnDisappearing()); _isVisible = false; } diff --git a/src/MauiMicroMvvm/MauiMicroBuilderExtensions.cs b/src/MauiMicroMvvm/MauiMicroBuilderExtensions.cs index 0d586e2..94b68a4 100644 --- a/src/MauiMicroMvvm/MauiMicroBuilderExtensions.cs +++ b/src/MauiMicroMvvm/MauiMicroBuilderExtensions.cs @@ -21,9 +21,9 @@ public static MauiAppBuilder UseMauiMicroMvvm(this MauiAppBuilder .AddSingleton() .AddSingleton(sp => { - var app = sp.GetRequiredService(); + var app = sp.GetRequiredService(); - if (mergedDictionaries.Any()) + if (mergedDictionaries.Length != 0) { var assembly = typeof(TShell).Assembly; var qualifiedResources = mergedDictionaries.Select(x => @@ -42,10 +42,10 @@ public static MauiAppBuilder UseMauiMicroMvvm(this MauiAppBuilder "; - app.Resources.LoadFromXaml(xaml); + app.Resources.LoadFromXaml(xaml); } - if (resources != null && resources.Keys.Any()) + if (resources != null && resources.Keys.Count != 0) { app.Resources.Add(resources); } @@ -71,7 +71,7 @@ public static MauiAppBuilder UseMauiMicroMvvm(this MauiAppBuilder builde where TShell : Shell => builder.UseMauiMicroMvvm(resources, mergedDictionaries); - public static IServiceCollection MapView(this IServiceCollection services, string key = null) + public static IServiceCollection MapView(this IServiceCollection services, string? key = null) where TView : VisualElement where TViewModel : class { From 0715216e94b4087ef0284606151a623aeee5db81 Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Tue, 9 Apr 2024 16:21:04 -0600 Subject: [PATCH 07/11] feat: adding MAUI WindowCreator --- sample/MauiMicroSample/App.xaml | 13 ++++++++ sample/MauiMicroSample/App.xaml.cs | 9 +++++ sample/MauiMicroSample/MauiProgram.cs | 5 ++- .../Internals/AppLifecycleBehavior.cs | 14 ++++++-- src/MauiMicroMvvm/Internals/WindowCreator.cs | 15 +++++++++ .../MauiMicroBuilderExtensions.cs | 33 ++++++++++++------- 6 files changed, 73 insertions(+), 16 deletions(-) create mode 100644 sample/MauiMicroSample/App.xaml create mode 100644 sample/MauiMicroSample/App.xaml.cs create mode 100644 src/MauiMicroMvvm/Internals/WindowCreator.cs diff --git a/sample/MauiMicroSample/App.xaml b/sample/MauiMicroSample/App.xaml new file mode 100644 index 0000000..dae79e2 --- /dev/null +++ b/sample/MauiMicroSample/App.xaml @@ -0,0 +1,13 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/sample/MauiMicroSample/App.xaml.cs b/sample/MauiMicroSample/App.xaml.cs new file mode 100644 index 0000000..0d5a419 --- /dev/null +++ b/sample/MauiMicroSample/App.xaml.cs @@ -0,0 +1,9 @@ +namespace MauiMicroSample; + +public partial class App : Application +{ + public App() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/sample/MauiMicroSample/MauiProgram.cs b/sample/MauiMicroSample/MauiProgram.cs index c3672ea..9ed66f1 100644 --- a/sample/MauiMicroSample/MauiProgram.cs +++ b/sample/MauiMicroSample/MauiProgram.cs @@ -13,9 +13,8 @@ public static MauiApp CreateMauiApp() { var builder = MauiApp.CreateBuilder(); builder - .UseMauiMicroMvvm( - "Resources/Styles/Colors.xaml", - "Resources/Styles/Styles.xaml") + .UseMauiApp() + .UseMauiMicroMvvm() .ConfigureFonts(fonts => { fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); diff --git a/src/MauiMicroMvvm/Internals/AppLifecycleBehavior.cs b/src/MauiMicroMvvm/Internals/AppLifecycleBehavior.cs index 406c9b6..0fecae2 100644 --- a/src/MauiMicroMvvm/Internals/AppLifecycleBehavior.cs +++ b/src/MauiMicroMvvm/Internals/AppLifecycleBehavior.cs @@ -98,16 +98,26 @@ private void OnStopped(object? sender, EventArgs e) private void OnAppearing(object? sender, EventArgs e) { if (!_didAppear) + { MvvmHelpers.InvokeViewViewModelAction(View, x => x.OnFirstLoad()); + } + _didAppear = true; - MvvmHelpers.InvokeViewViewModelAction(View, x => x.OnAppearing()); + if (!_isVisible) + { + MvvmHelpers.InvokeViewViewModelAction(View, x => x.OnAppearing()); + } + _isVisible = true; } private void OnDisappearing(object? sender, EventArgs e) { - MvvmHelpers.InvokeViewViewModelAction(View, x => x.OnDisappearing()); + if (_isVisible) + { + MvvmHelpers.InvokeViewViewModelAction(View, x => x.OnDisappearing()); + } _isVisible = false; } diff --git a/src/MauiMicroMvvm/Internals/WindowCreator.cs b/src/MauiMicroMvvm/Internals/WindowCreator.cs new file mode 100644 index 0000000..86a904c --- /dev/null +++ b/src/MauiMicroMvvm/Internals/WindowCreator.cs @@ -0,0 +1,15 @@ +namespace MauiMicroMvvm.Internals; + +internal class WindowCreator(TShell Shell) : IWindowCreator + where TShell : Shell +{ + private Window? _window; + + public Window CreateWindow(Application app, IActivationState? activationState) + { + return _window ??= new Window + { + Page = Shell + }; + } +} diff --git a/src/MauiMicroMvvm/MauiMicroBuilderExtensions.cs b/src/MauiMicroMvvm/MauiMicroBuilderExtensions.cs index 94b68a4..ee56ec8 100644 --- a/src/MauiMicroMvvm/MauiMicroBuilderExtensions.cs +++ b/src/MauiMicroMvvm/MauiMicroBuilderExtensions.cs @@ -6,18 +6,33 @@ namespace Microsoft.Maui.Hosting; public static class MauiMicroBuilderExtensions { + public static MauiAppBuilder UseMauiMicroMvvm(this MauiAppBuilder builder) + where TShell : Shell + { + builder.Services + .AddSingleton() + .AddSingleton>() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddScoped(); + return builder; + } + + [Obsolete("Use `UseMauiMicroMvvm()` instead.")] public static MauiAppBuilder UseMauiMicroMvvm(this MauiAppBuilder builder, params string[] mergedDictionaries) where TApp : Application where TShell : Shell => - builder.UseMauiMicroMvvm(new ResourceDictionary(), mergedDictionaries); + builder.UseMauiMicroMvvm([], mergedDictionaries); + [Obsolete("Use `UseMauiMicroMvvm()` instead.")] public static MauiAppBuilder UseMauiMicroMvvm(this MauiAppBuilder builder, ResourceDictionary resources, params string[] mergedDictionaries) where TApp : Application where TShell : Shell { builder.UseMauiApp(); - builder.Services.AddSingleton() + builder.Services .AddSingleton() .AddSingleton(sp => { @@ -50,23 +65,19 @@ public static MauiAppBuilder UseMauiMicroMvvm(this MauiAppBuilder app.Resources.Add(resources); } - var shell = sp.GetRequiredService(); - app.MainPage = shell; return app; }); - builder.Services - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddScoped(); - return builder; + return builder + .UseMauiMicroMvvm(); } + [Obsolete("Use `UseMauiMicroMvvm()` instead.")] public static MauiAppBuilder UseMauiMicroMvvm(this MauiAppBuilder builder, params string[] mergedDictionaries) where TShell : Shell => - builder.UseMauiMicroMvvm(new ResourceDictionary(), mergedDictionaries); + builder.UseMauiMicroMvvm([], mergedDictionaries); + [Obsolete("Use `UseMauiMicroMvvm()` instead.")] public static MauiAppBuilder UseMauiMicroMvvm(this MauiAppBuilder builder, ResourceDictionary resources, params string[] mergedDictionaries) where TShell : Shell => builder.UseMauiMicroMvvm(resources, mergedDictionaries); From 42bc5bd584e69a15ee63e50ebca39c0f4f14b012 Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Tue, 9 Apr 2024 16:21:25 -0600 Subject: [PATCH 08/11] feat: adding Behavior Factory --- .../Behaviors/BehaviorFactory.cs | 27 +++++++++++++++++++ .../Behaviors/DelegateViewBehavior.cs | 24 +++++++++++++++++ .../Behaviors/IBehaviorFactory.cs | 6 +++++ .../Behaviors/IRegisteredBehavior.cs | 7 +++++ .../Behaviors/RegisteredBehavior.cs | 10 +++++++ src/MauiMicroMvvm/Internals/ViewFactory.cs | 6 ++++- .../MauiMicroBuilderExtensions.cs | 24 +++++++++++++++++ 7 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 src/MauiMicroMvvm/Behaviors/BehaviorFactory.cs create mode 100644 src/MauiMicroMvvm/Behaviors/DelegateViewBehavior.cs create mode 100644 src/MauiMicroMvvm/Behaviors/IBehaviorFactory.cs create mode 100644 src/MauiMicroMvvm/Behaviors/IRegisteredBehavior.cs create mode 100644 src/MauiMicroMvvm/Behaviors/RegisteredBehavior.cs diff --git a/src/MauiMicroMvvm/Behaviors/BehaviorFactory.cs b/src/MauiMicroMvvm/Behaviors/BehaviorFactory.cs new file mode 100644 index 0000000..9b15d88 --- /dev/null +++ b/src/MauiMicroMvvm/Behaviors/BehaviorFactory.cs @@ -0,0 +1,27 @@ +namespace MauiMicroMvvm.Behaviors; + +public sealed class BehaviorFactory : IBehaviorFactory +{ + private readonly IEnumerable _behaviors; + private readonly IServiceProvider _services; + + public BehaviorFactory(IServiceProvider services, IEnumerable behaviors) + { + ArgumentNullException.ThrowIfNull(services); + _behaviors = behaviors ?? []; + _services = services; + } + + public void ApplyBehaviors(VisualElement element) + { + foreach (var registration in _behaviors) + { + if (!registration.ViewType.IsAssignableFrom(registration.ViewType)) + continue; + + var behavior = registration.GetBehavior(); + if (behavior is not null) + element.Behaviors.Add(behavior); + } + } +} diff --git a/src/MauiMicroMvvm/Behaviors/DelegateViewBehavior.cs b/src/MauiMicroMvvm/Behaviors/DelegateViewBehavior.cs new file mode 100644 index 0000000..07c129b --- /dev/null +++ b/src/MauiMicroMvvm/Behaviors/DelegateViewBehavior.cs @@ -0,0 +1,24 @@ +namespace MauiMicroMvvm.Behaviors; + +internal sealed class DelegateViewBehavior(Action onAttached, Action onDetached) : Behavior + where TView : VisualElement +{ + private readonly Action _onAttached = onAttached; + private readonly Action _onDetached = onDetached; + + protected override void OnAttachedTo(TView bindable) + { + base.OnAttachedTo(bindable); + var serviceProvider = bindable.Handler?.MauiContext?.Services; + ArgumentNullException.ThrowIfNull(serviceProvider); + _onAttached(serviceProvider, bindable); + } + + protected override void OnDetachingFrom(TView bindable) + { + base.OnDetachingFrom(bindable); + var serviceProvider = bindable.Handler?.MauiContext?.Services; + ArgumentNullException.ThrowIfNull(serviceProvider); + _onDetached(serviceProvider, bindable); + } +} diff --git a/src/MauiMicroMvvm/Behaviors/IBehaviorFactory.cs b/src/MauiMicroMvvm/Behaviors/IBehaviorFactory.cs new file mode 100644 index 0000000..69a0afc --- /dev/null +++ b/src/MauiMicroMvvm/Behaviors/IBehaviorFactory.cs @@ -0,0 +1,6 @@ +namespace MauiMicroMvvm.Behaviors; + +public interface IBehaviorFactory +{ + void ApplyBehaviors(VisualElement element); +} diff --git a/src/MauiMicroMvvm/Behaviors/IRegisteredBehavior.cs b/src/MauiMicroMvvm/Behaviors/IRegisteredBehavior.cs new file mode 100644 index 0000000..e499636 --- /dev/null +++ b/src/MauiMicroMvvm/Behaviors/IRegisteredBehavior.cs @@ -0,0 +1,7 @@ +namespace MauiMicroMvvm.Behaviors; + +public interface IRegisteredBehavior +{ + Type ViewType { get; } + Behavior GetBehavior(); +} diff --git a/src/MauiMicroMvvm/Behaviors/RegisteredBehavior.cs b/src/MauiMicroMvvm/Behaviors/RegisteredBehavior.cs new file mode 100644 index 0000000..f50df28 --- /dev/null +++ b/src/MauiMicroMvvm/Behaviors/RegisteredBehavior.cs @@ -0,0 +1,10 @@ +namespace MauiMicroMvvm.Behaviors; + +internal class RegisteredBehavior(IServiceProvider Services) : IRegisteredBehavior + where TView : VisualElement + where TBehavior : Behavior +{ + public Type ViewType => typeof(TView); + + public Behavior GetBehavior() => Services.GetRequiredService(); +} diff --git a/src/MauiMicroMvvm/Internals/ViewFactory.cs b/src/MauiMicroMvvm/Internals/ViewFactory.cs index b9b3a06..3bc6d95 100644 --- a/src/MauiMicroMvvm/Internals/ViewFactory.cs +++ b/src/MauiMicroMvvm/Internals/ViewFactory.cs @@ -1,7 +1,9 @@ #nullable enable +using MauiMicroMvvm.Behaviors; + namespace MauiMicroMvvm.Internals; -public class ViewFactory(IServiceProvider services, IEnumerable mappings) : IViewFactory +public class ViewFactory(IServiceProvider services, IEnumerable mappings, IBehaviorFactory behaviorFactory) : IViewFactory { public static readonly BindableProperty NavigationKeyProperty = BindableProperty.CreateAttached("NavigationKey", typeof(string), typeof(ViewFactory), null); @@ -38,6 +40,8 @@ public virtual TView Configure(TView view) if(view.BindingContext is null && (!view.IsSet(Xaml.MauiMicro.AutowireProperty) || Xaml.MauiMicro.GetAutowire(view))) SetBindingContext(view); + behaviorFactory.ApplyBehaviors(view); + if (view is Shell || view is Window || view.Behaviors.OfType().Any()) return view; diff --git a/src/MauiMicroMvvm/MauiMicroBuilderExtensions.cs b/src/MauiMicroMvvm/MauiMicroBuilderExtensions.cs index ee56ec8..70d6c2d 100644 --- a/src/MauiMicroMvvm/MauiMicroBuilderExtensions.cs +++ b/src/MauiMicroMvvm/MauiMicroBuilderExtensions.cs @@ -1,4 +1,5 @@ using MauiMicroMvvm; +using MauiMicroMvvm.Behaviors; using MauiMicroMvvm.Internals; using INavigation = MauiMicroMvvm.INavigation; @@ -13,6 +14,7 @@ public static MauiAppBuilder UseMauiMicroMvvm(this MauiAppBuilder builde .AddSingleton() .AddSingleton>() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddScoped(); @@ -102,4 +104,26 @@ public static IServiceCollection MapView(this IServiceCollect .AddSingleton(new ViewMapping(key, typeof(TView), typeof(TViewModel))) .AddTransient(); } + + public static IServiceCollection ApplyBehavior(this IServiceCollection services) + where TView : VisualElement + where TBehavior : Behavior + { + return services.AddTransient() + .AddSingleton>(); + } + + public static IServiceCollection ApplyBehavior(this IServiceCollection services, Action onAttached, Action? onDetached = null) + where TView : VisualElement + { + onDetached ??= delegate { }; + return services.AddSingleton(new DelegateViewBehavior(onAttached, onDetached)); + } + + public static IServiceCollection ApplyBehavior(this IServiceCollection services, Action onAttached, Action? onDetached = null) + where TView : VisualElement + { + onDetached ??= delegate { }; + return services.AddSingleton(new DelegateViewBehavior((_, view) => onAttached(view), (_, view) => onDetached(view))); + } } \ No newline at end of file From bca905145bd02f2bd5b166f0061b843ffd816189 Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Tue, 9 Apr 2024 16:35:16 -0600 Subject: [PATCH 09/11] feat: adding RaisePropertyChang(ing|ed) --- src/MauiMicroMvvm/MauiMicroViewModel.cs | 29 +++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/MauiMicroMvvm/MauiMicroViewModel.cs b/src/MauiMicroMvvm/MauiMicroViewModel.cs index 17d0f1b..4e68e01 100644 --- a/src/MauiMicroMvvm/MauiMicroViewModel.cs +++ b/src/MauiMicroMvvm/MauiMicroViewModel.cs @@ -68,16 +68,37 @@ protected T Get(T? defaultValue = default, [CallerMemberName]string? property protected bool Set(T value, [CallerMemberName]string? propertyName = null) { ArgumentException.ThrowIfNullOrEmpty(propertyName); - if (EqualityComparer.Default.Equals(Get(propertyName: propertyName), value)) + + var isSet = _properties.ContainsKey(propertyName); + if (isSet && EqualityComparer.Default.Equals(Get(propertyName: propertyName), value)) return false; + RaisePropertyChanging(propertyName); + + if (value is null && isSet) + { + _properties.Remove(propertyName); + } + else if (value is not null) + { + _properties[propertyName] = value; + } + + RaisePropertyChanged(propertyName); + return true; + } + + protected virtual void RaisePropertyChanging(string propertyName) + { PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(propertyName)); - _properties[propertyName] = value; + } + + protected virtual void RaisePropertyChanged(string propertyName) + { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - return true; } - protected bool Set(T value, Action callback, [CallerMemberName]string propertyName = null) + protected bool Set(T value, Action callback, [CallerMemberName]string? propertyName = null) { if (Set(value, propertyName)) { From 3f338b25be5d65a13cf6a7c5df00dd1ea69c1cf8 Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Tue, 9 Apr 2024 16:45:31 -0600 Subject: [PATCH 10/11] feat: prefer use of IDisposable --- src/MauiMicroMvvm/Common/MvvmHelpers.cs | 9 ++------- src/MauiMicroMvvm/IDestructible.cs | 6 ------ src/MauiMicroMvvm/IDestructibleAsync.cs | 6 ------ src/MauiMicroMvvm/MauiMicroViewModel.cs | 25 ++++++++++++++++++++++++- 4 files changed, 26 insertions(+), 20 deletions(-) delete mode 100644 src/MauiMicroMvvm/IDestructible.cs delete mode 100644 src/MauiMicroMvvm/IDestructibleAsync.cs diff --git a/src/MauiMicroMvvm/Common/MvvmHelpers.cs b/src/MauiMicroMvvm/Common/MvvmHelpers.cs index 51b065e..fecc3e5 100644 --- a/src/MauiMicroMvvm/Common/MvvmHelpers.cs +++ b/src/MauiMicroMvvm/Common/MvvmHelpers.cs @@ -1,9 +1,4 @@ using Microsoft.Maui.Controls; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace MauiMicroMvvm.Common; @@ -37,11 +32,11 @@ public static async Task InvokeViewViewModelActionAsync(object? value, Func(page, x => x.Destroy()); + InvokeViewViewModelAction(page, x => x.Dispose()); } public static Task DestroyAsync(object? page) { - return InvokeViewViewModelActionAsync(page, x => x.DestroyAsync()); + return InvokeViewViewModelActionAsync(page, x => x.DisposeAsync().AsTask()); } } diff --git a/src/MauiMicroMvvm/IDestructible.cs b/src/MauiMicroMvvm/IDestructible.cs deleted file mode 100644 index 23bb149..0000000 --- a/src/MauiMicroMvvm/IDestructible.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace MauiMicroMvvm; - -public interface IDestructible -{ - void Destroy(); -} diff --git a/src/MauiMicroMvvm/IDestructibleAsync.cs b/src/MauiMicroMvvm/IDestructibleAsync.cs deleted file mode 100644 index 5a54522..0000000 --- a/src/MauiMicroMvvm/IDestructibleAsync.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace MauiMicroMvvm; - -public interface IDestructibleAsync -{ - Task DestroyAsync(); -} diff --git a/src/MauiMicroMvvm/MauiMicroViewModel.cs b/src/MauiMicroMvvm/MauiMicroViewModel.cs index 4e68e01..3656746 100644 --- a/src/MauiMicroMvvm/MauiMicroViewModel.cs +++ b/src/MauiMicroMvvm/MauiMicroViewModel.cs @@ -6,10 +6,11 @@ namespace MauiMicroMvvm; -public abstract class MauiMicroViewModel : INotifyPropertyChanging, INotifyPropertyChanged, IViewModelActivation, IViewLifecycle, IAppLifecycle, IQueryAttributable +public abstract class MauiMicroViewModel : INotifyPropertyChanging, INotifyPropertyChanged, IViewModelActivation, IViewLifecycle, IAppLifecycle, IQueryAttributable, IDisposable { private readonly Dictionary _properties = []; private readonly Lazy _lazyLogger; + private readonly object _locker = new (); protected MauiMicroViewModel(ViewModelContext context) { @@ -19,6 +20,8 @@ protected MauiMicroViewModel(ViewModelContext context) QueryParameters = new Dictionary(); } + protected bool IsDisposed { get; private set; } + protected ILogger Logger => _lazyLogger.Value; protected INavigation Navigation { get; } @@ -129,4 +132,24 @@ public void ApplyQueryAttributes(IDictionary query) OnParametersSet(); } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting + /// unmanaged resources. + /// + protected virtual void Dispose() { } + + void IDisposable.Dispose() + { + lock(_locker) + { + if (!IsDisposed) + { + Dispose(); + } + IsDisposed = true; + } + + GC.SuppressFinalize(this); + } } From 5c930e7d831000346564da9e2260c3ca7a280935 Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Tue, 9 Apr 2024 16:45:43 -0600 Subject: [PATCH 11/11] chore: update template for new API --- .../content/MauiMicroApp.1/App.xaml | 13 +++++++++++++ .../content/MauiMicroApp.1/App.xaml.cs | 9 +++++++++ .../content/MauiMicroApp.1/MauiProgram.cs | 5 ++--- 3 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 src/MauiMicroMvvm.Templates/content/MauiMicroApp.1/App.xaml create mode 100644 src/MauiMicroMvvm.Templates/content/MauiMicroApp.1/App.xaml.cs diff --git a/src/MauiMicroMvvm.Templates/content/MauiMicroApp.1/App.xaml b/src/MauiMicroMvvm.Templates/content/MauiMicroApp.1/App.xaml new file mode 100644 index 0000000..da2fb5c --- /dev/null +++ b/src/MauiMicroMvvm.Templates/content/MauiMicroApp.1/App.xaml @@ -0,0 +1,13 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/MauiMicroMvvm.Templates/content/MauiMicroApp.1/App.xaml.cs b/src/MauiMicroMvvm.Templates/content/MauiMicroApp.1/App.xaml.cs new file mode 100644 index 0000000..fbb4295 --- /dev/null +++ b/src/MauiMicroMvvm.Templates/content/MauiMicroApp.1/App.xaml.cs @@ -0,0 +1,9 @@ +namespace MauiMicroApp._1; + +public partial class App : Application +{ + public App() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/MauiMicroMvvm.Templates/content/MauiMicroApp.1/MauiProgram.cs b/src/MauiMicroMvvm.Templates/content/MauiMicroApp.1/MauiProgram.cs index 451222e..4ea3742 100644 --- a/src/MauiMicroMvvm.Templates/content/MauiMicroApp.1/MauiProgram.cs +++ b/src/MauiMicroMvvm.Templates/content/MauiMicroApp.1/MauiProgram.cs @@ -10,9 +10,8 @@ public static MauiApp CreateMauiApp() { var builder = MauiApp.CreateBuilder(); builder - .UseMauiMicroMvvm( - "Resources/Styles/Colors.xaml", - "Resources/Styles/Styles.xaml") + .UseMauiApp() + .UseMauiMicroMvvm() .ConfigureFonts(fonts => { fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");