From f809f3d221a2bfe88ed31fbe70e8e53741862a24 Mon Sep 17 00:00:00 2001 From: TheXamlGuy Date: Tue, 8 Oct 2024 15:12:50 +0100 Subject: [PATCH] introduce a TransientNavigationStore to share objects from one state to the next state --- Toolkit.Avalonia/FrameHandler.cs | 50 ++-- .../IServiceCollectionExtensions.cs | 7 + Toolkit.Avalonia/ITransientNavigationStore.cs | 15 + Toolkit.Avalonia/TransientNavigationStore.cs | 36 +++ Toolkit.Foundation/ContentFactory.cs | 7 +- Toolkit.Foundation/IContentFactory.cs | 4 +- Toolkit.Foundation/IKeepAlive.cs | 3 - Toolkit.Foundation/Navigation.cs | 2 +- Toolkit.Foundation/ObservableCollection.cs | 273 +++++++++--------- 9 files changed, 224 insertions(+), 173 deletions(-) create mode 100644 Toolkit.Avalonia/ITransientNavigationStore.cs create mode 100644 Toolkit.Avalonia/TransientNavigationStore.cs delete mode 100644 Toolkit.Foundation/IKeepAlive.cs diff --git a/Toolkit.Avalonia/FrameHandler.cs b/Toolkit.Avalonia/FrameHandler.cs index 97e0264..147a146 100644 --- a/Toolkit.Avalonia/FrameHandler.cs +++ b/Toolkit.Avalonia/FrameHandler.cs @@ -7,7 +7,7 @@ using Toolkit.UI.Controls.Avalonia; namespace Toolkit.Avalonia; -public class FrameHandler : +public class FrameHandler(ITransientNavigationStore navigationStore) : INotificationHandler>, INotificationHandler> { @@ -18,19 +18,16 @@ public class FrameHandler : frame.NavigationPageFactory ??= new NavigationPageFactory(); if (args.Template is Control control) { - void NavigatedTo(Control sender) + void Navigated(Control sender) { - async void HandleNavigatedTo(object? _, - NavigationEventArgs __) + async void HandleNavigatedTo(object? _, NavigationEventArgs __) { - async void HandleNavigatingFrom(object? _, - NavigatingCancelEventArgs args) + async void HandleNavigatingFrom(object? _, NavigatingCancelEventArgs args) { sender.RemoveHandler(Frame.NavigatingFromEvent, HandleNavigatingFrom); control.Unloaded -= HandleUnloaded; - async void HandleNavigatedFrom(object? _, - NavigationEventArgs args) + async void HandleNavigatedFrom(object? _, NavigationEventArgs args) { sender.RemoveHandler(Frame.NavigatedFromEvent, HandleNavigatedFrom); if (sender.DataContext is object content) @@ -40,9 +37,11 @@ public class FrameHandler : await deactivated.OnDeactivated(); } - if (content is not IKeepAlive) + if (content is IDisposable disposable) { - if (content is IDisposable disposable) + FrameNavigationOptions? options = navigationStore.Get(frame); + if (options is not FrameNavigationOptions frameOptions || + !frameOptions.IsNavigationStackEnabled) { disposable.Dispose(); } @@ -51,9 +50,10 @@ public class FrameHandler : } sender.AddHandler(Frame.NavigatedFromEvent, HandleNavigatedFrom); + if (sender.DataContext is object content) { - if (content is IConfirmation confirmation && + if (content is IConfirmation confirmation && !await confirmation.Confirm()) { args.Cancel = true; @@ -106,7 +106,6 @@ public class FrameHandler : } control.DataContext = args.Content; - NavigatedTo(control); FrameNavigationOptions navigationOptions = new(); List postNavigateActions = []; @@ -133,31 +132,27 @@ public class FrameHandler : if (args.Parameters is not null) { - if (args.Parameters.TryGetValue("Transition", - out object? transition)) + if (args.Parameters.TryGetValue("Transition", out object? transition)) { switch ($"{transition}") { case "Suppress": - navigationOptions.TransitionInfoOverride = - new SuppressNavigationTransitionInfo(); + navigationOptions.TransitionInfoOverride = new SuppressNavigationTransitionInfo(); break; case "FromLeft": case "FromRight": case "FromTop": case "FromBottom": - navigationOptions.TransitionInfoOverride = - new SlideNavigationTransitionInfo - { - Effect = Enum.Parse($"{transition}") - }; + navigationOptions.TransitionInfoOverride = new SlideNavigationTransitionInfo + { + Effect = Enum.Parse($"{transition}") + }; break; } } - if (args.Parameters.TryGetValue("IsBackStackEnabled", - out object? isBackStackEnabled)) + if (args.Parameters.TryGetValue("IsBackStackEnabled", out object? isBackStackEnabled)) { if (isBackStackEnabled is bool value) { @@ -165,8 +160,7 @@ public class FrameHandler : } } - if (args.Parameters.TryGetValue("ClearBackStack", - out object? clearBackStack)) + if (args.Parameters.TryGetValue("ClearBackStack", out object? clearBackStack)) { if (clearBackStack is bool clearBool) { @@ -178,8 +172,7 @@ public class FrameHandler : if (clearBackStack is string clearString) { - if (clearString.StartsWith('[') && clearString.EndsWith(']') && - clearString.Contains('-')) + if (clearString.StartsWith('[') && clearString.EndsWith(']') && clearString.Contains('-')) { string range = clearString.Trim('[', ']'); string[] parts = range.Split('-'); @@ -202,6 +195,9 @@ public class FrameHandler : } } + Navigated(control); + + navigationStore.Set(frame, navigationOptions); frame.NavigateFromObject(control, navigationOptions); foreach (Action postAction in postNavigateActions) diff --git a/Toolkit.Avalonia/IServiceCollectionExtensions.cs b/Toolkit.Avalonia/IServiceCollectionExtensions.cs index b3957c7..1f92754 100644 --- a/Toolkit.Avalonia/IServiceCollectionExtensions.cs +++ b/Toolkit.Avalonia/IServiceCollectionExtensions.cs @@ -33,7 +33,10 @@ public static class IServiceCollectionExtensions services.AddHandler(nameof(IClassicDesktopStyleApplicationLifetime)); services.AddHandler(nameof(ISingleViewApplicationLifetime)); services.AddHandler(nameof(ContentControl)); + services.AddHandler(nameof(Frame)); + services.TryAddSingleton, TransientNavigationStore>(); + services.AddHandler(nameof(ContentDialog)); services.AddHandler(nameof(TaskDialog)); @@ -66,7 +69,11 @@ public static class IServiceCollectionExtensions services.AddHandler(); services.AddHandler(nameof(ContentControl)); + services.AddHandler(nameof(Frame)); + + services.TryAddSingleton(provider.GetRequiredService>()); + services.AddHandler(nameof(ContentDialog)); services.AddHandler(nameof(TaskDialog)); }))); diff --git a/Toolkit.Avalonia/ITransientNavigationStore.cs b/Toolkit.Avalonia/ITransientNavigationStore.cs new file mode 100644 index 0000000..035db3c --- /dev/null +++ b/Toolkit.Avalonia/ITransientNavigationStore.cs @@ -0,0 +1,15 @@ +namespace Toolkit.Avalonia; + +public interface ITransientNavigationStore + where TControl : class +{ + void Clear(); + + T? Get(TControl control) + where T : class; + + void Remove(TControl control); + + void Set(TControl control, + object parameters); +} \ No newline at end of file diff --git a/Toolkit.Avalonia/TransientNavigationStore.cs b/Toolkit.Avalonia/TransientNavigationStore.cs new file mode 100644 index 0000000..456f7be --- /dev/null +++ b/Toolkit.Avalonia/TransientNavigationStore.cs @@ -0,0 +1,36 @@ +namespace Toolkit.Avalonia; + +public class TransientNavigationStore : ITransientNavigationStore + where TControl : class +{ + private readonly Dictionary> controlDataMap = []; + + public void Set(TControl control, + object parameters) + { + if (!controlDataMap.TryGetValue(control, out var typeMap)) + { + typeMap = new Dictionary(); + controlDataMap[control] = typeMap; + } + + typeMap[parameters.GetType()] = parameters; + } + + public T? Get(TControl control) + where T : class + { + if (controlDataMap.TryGetValue(control, out var typeMap) && + typeMap.TryGetValue(typeof(T), out var parameters)) + { + typeMap.Remove(typeof(T)); + return parameters as T; + } + + return default; + } + + public void Remove(TControl control) => controlDataMap.Remove(control); + + public void Clear() => controlDataMap.Clear(); +} diff --git a/Toolkit.Foundation/ContentFactory.cs b/Toolkit.Foundation/ContentFactory.cs index e34b782..1b9d231 100644 --- a/Toolkit.Foundation/ContentFactory.cs +++ b/Toolkit.Foundation/ContentFactory.cs @@ -1,9 +1,10 @@ namespace Toolkit.Foundation; public class ContentFactory(IServiceProvider provider, - IServiceFactory factory) : IContentFactory + IServiceFactory factory) : + IContentFactory { - public Task CreateAsync(IContentTemplateDescriptor descriptor, + public object? Create(IContentTemplateDescriptor descriptor, object[] parameters) { object? content = parameters is { Length: > 0 } @@ -22,6 +23,6 @@ public class ContentFactory(IServiceProvider provider, } }, descriptor.Key); - return Task.FromResult(content); + return content; } } \ No newline at end of file diff --git a/Toolkit.Foundation/IContentFactory.cs b/Toolkit.Foundation/IContentFactory.cs index 4706a4c..cb172d2 100644 --- a/Toolkit.Foundation/IContentFactory.cs +++ b/Toolkit.Foundation/IContentFactory.cs @@ -2,7 +2,7 @@ { public interface IContentFactory { - Task CreateAsync(IContentTemplateDescriptor descriptor, - object[] resolvedArguments); + object? Create(IContentTemplateDescriptor descriptor, + object[] parameters); } } \ No newline at end of file diff --git a/Toolkit.Foundation/IKeepAlive.cs b/Toolkit.Foundation/IKeepAlive.cs deleted file mode 100644 index 82e6e0b..0000000 --- a/Toolkit.Foundation/IKeepAlive.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Toolkit.Foundation; - -public interface IKeepAlive; \ No newline at end of file diff --git a/Toolkit.Foundation/Navigation.cs b/Toolkit.Foundation/Navigation.cs index 7de1453..02a3c24 100644 --- a/Toolkit.Foundation/Navigation.cs +++ b/Toolkit.Foundation/Navigation.cs @@ -62,7 +62,7 @@ public class Navigation(IServiceProvider provider, if (region is not null) { - object? content = await contentFactory.CreateAsync(descriptor, resolvedArguments); + object? content = contentFactory.Create(descriptor, resolvedArguments); if (content is not null) { Type navigationType = region is Type type ? type : region.GetType(); diff --git a/Toolkit.Foundation/ObservableCollection.cs b/Toolkit.Foundation/ObservableCollection.cs index 31a9804..5453673 100644 --- a/Toolkit.Foundation/ObservableCollection.cs +++ b/Toolkit.Foundation/ObservableCollection.cs @@ -46,6 +46,8 @@ public partial class ObservableCollection : [ObservableProperty] private int count; + private Func? defaultSelectionFactory; + [ObservableProperty] private bool isActivated; @@ -145,86 +147,27 @@ public partial class ObservableCollection : } } - private Func? defaultSelectionFactory; - - public void SetSource(IList source, - Func? defaultSelectionFactory) + public void Activate(Func activateDelegate, + bool reset = false) { - foreach (TViewModel item in source) + if (reset) { - Add(item); + Clear(); } - if (defaultSelectionFactory is not null) - { - this.defaultSelectionFactory = defaultSelectionFactory; - SelectedItem = defaultSelectionFactory.Invoke(); - } - - if (source is INotifyCollectionChanged observableSource) - { - observableSource.CollectionChanged -= SourceCollectionChanged; - observableSource.CollectionChanged += SourceCollectionChanged; - } + ActivationBuilder builder = activateDelegate.Invoke(); + Publisher.Publish(builder.Value, builder.Key); } - private void SourceCollectionChanged(object? sender, - NotifyCollectionChangedEventArgs args) + public void Activate(bool reset = false) { - switch (args.Action) + if (reset) { - case NotifyCollectionChangedAction.Add: - if (args.NewItems is not null) - { - foreach (TViewModel newItem in args.NewItems) - { - Add(newItem); - } - } - break; - - case NotifyCollectionChangedAction.Remove: - if (args.OldItems is not null) - { - foreach (TViewModel oldItem in args.OldItems) - { - if (this.FirstOrDefault(x => x.Equals(oldItem)) is TViewModel removedItem) - { - Remove(removedItem); - } - } - } - break; - - case NotifyCollectionChangedAction.Reset: - - Clear(); - if (sender is IEnumerable collection) - { - foreach (TViewModel item in collection) - { - Add(item); - } - - if (defaultSelectionFactory is not null) - { - SelectedItem = defaultSelectionFactory.Invoke(); - } - } - break; - } - } - - public virtual Task OnActivated() - { - IsActivated = true; - while (pendingEvents.Count > 0) - { - object current = pendingEvents.Dequeue(); - Handle((dynamic)current); + Clear(); } - return Task.CompletedTask; + ActivationBuilder builder = ActivationBuilder(); + Publisher.PublishUI(builder.Value, builder.Key); } public TViewModel Add(params object?[] parameters) @@ -285,14 +228,6 @@ public partial class ObservableCollection : } } - public void Reset(Action> factory, bool disposeItems = true) - { - SelectedItem = default; - - Clear(disposeItems); - factory.Invoke(this); - } - public void Clear(bool disposeItems = false) { isClearing = true; @@ -342,33 +277,12 @@ public partial class ObservableCollection : void ICollection.CopyTo(Array array, int index) => collection.CopyTo((TViewModel[])array, index); - public virtual Task OnDeactivated() - { - IsActivated = false; - return Task.CompletedTask; - } - - public virtual Task OnDeactivating() => - Task.CompletedTask; - public virtual void Dispose() { GC.SuppressFinalize(this); Disposer.Dispose(this); } - public void Activate(Func activateDelegate, - bool reset = false) - { - if (reset) - { - Clear(); - } - - ActivationBuilder builder = activateDelegate.Invoke(); - Publisher.Publish(builder.Value, builder.Key); - } - public IEnumerator GetEnumerator() => collection.GetEnumerator(); @@ -505,10 +419,6 @@ public partial class ObservableCollection : IsCompatibleObject(value) ? IndexOf((TViewModel)value!) : -1; - public virtual void OnInitialize() - { - } - [RelayCommand] public virtual void Initialize() { @@ -519,37 +429,12 @@ public partial class ObservableCollection : IsInitialized = true; Subscriber.Subscribe(this); + OnInitialize(); Activate(); } - public bool Replace(int index, - params object?[] parameters) - where T : - TViewModel - { - if (index <= Count - 1) - { - RemoveItem(index); - } - else - { - index = Count; - } - - T? item = Factory.Create(args => - { - if (args is IInitialization initialization) - { - initialization.Initialize(); - } - }, parameters); - - Insert(index, item); - return true; - } - public TViewModel Insert(int index = 0, params object?[] parameters) where T : @@ -634,6 +519,31 @@ public partial class ObservableCollection : return true; } + public virtual Task OnActivated() + { + IsActivated = true; + while (pendingEvents.Count > 0) + { + object current = pendingEvents.Dequeue(); + Handle((dynamic)current); + } + + return Task.CompletedTask; + } + + public virtual Task OnDeactivated() + { + IsActivated = false; + return Task.CompletedTask; + } + + public virtual Task OnDeactivating() => + Task.CompletedTask; + + public virtual void OnInitialize() + { + } + public bool Remove(TViewModel item) { int index = collection.IndexOf(item); @@ -669,6 +579,32 @@ public partial class ObservableCollection : public void RemoveAt(int index) => RemoveItem(index); + public bool Replace(int index, + params object?[] parameters) + where T : + TViewModel + { + if (index <= Count - 1) + { + RemoveItem(index); + } + else + { + index = Count; + } + + T? item = Factory.Create(args => + { + if (args is IInitialization initialization) + { + initialization.Initialize(); + } + }, parameters); + + Insert(index, item); + return true; + } + public bool Replace(int index, TViewModel item) { @@ -685,6 +621,14 @@ public partial class ObservableCollection : return true; } + public void Reset(Action> factory, bool disposeItems = true) + { + SelectedItem = default; + + Clear(disposeItems); + factory.Invoke(this); + } + public void Revert() { foreach (object trackedProperty in trackedProperties.Values) @@ -693,15 +637,24 @@ public partial class ObservableCollection : } } - public void Activate(bool reset = false) + public void SetSource(IList source, Func? defaultSelectionFactory) { - if (reset) + foreach (TViewModel item in source) { - Clear(); + Add(item); } - ActivationBuilder builder = ActivationBuilder(); - Publisher.PublishUI(builder.Value, builder.Key); + if (defaultSelectionFactory is not null) + { + this.defaultSelectionFactory = defaultSelectionFactory; + SelectedItem = defaultSelectionFactory.Invoke(); + } + + if (source is INotifyCollectionChanged observableSource) + { + observableSource.CollectionChanged -= SourceCollectionChanged; + observableSource.CollectionChanged += SourceCollectionChanged; + } } public void Track(string propertyName, Func getter, Action setter) @@ -725,7 +678,7 @@ public partial class ObservableCollection : Disposer.Add(this, item); Disposer.Add(item, Disposable.Create(() => { - if (item is IRemovable && !isClearing) + if (item is IDisposable && !isClearing) { if (item is IList collection) { @@ -739,6 +692,10 @@ public partial class ObservableCollection : collection.Insert(index > Count ? Count : index, item); } + protected virtual void OnSelectedItemChanged() + { + } + protected virtual void RemoveItem(int index) => collection.RemoveAt(index); @@ -782,10 +739,52 @@ public partial class ObservableCollection : OnSelectedItemChanged(); } - protected virtual void OnSelectedItemChanged() + private void SourceCollectionChanged(object? sender, + NotifyCollectionChangedEventArgs args) { - } + switch (args.Action) + { + case NotifyCollectionChangedAction.Add: + if (args.NewItems is not null) + { + foreach (TViewModel newItem in args.NewItems) + { + Add(newItem); + } + } + break; + case NotifyCollectionChangedAction.Remove: + if (args.OldItems is not null) + { + foreach (TViewModel oldItem in args.OldItems) + { + if (this.FirstOrDefault(x => x.Equals(oldItem)) is TViewModel removedItem) + { + Remove(removedItem); + } + } + } + break; + + case NotifyCollectionChangedAction.Reset: + + Clear(); + if (sender is IEnumerable collection) + { + foreach (TViewModel item in collection) + { + Add(item); + } + + if (defaultSelectionFactory is not null) + { + SelectedItem = defaultSelectionFactory.Invoke(); + } + } + break; + } + } private void UpdateSelection(TViewModel item) { if (item is ISelectable newSelection)