diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 52df96d..8b6dead 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -75,7 +75,7 @@ jobs: - name: Publish WPF NuGet package run: | if (Test-Path "./nugets/*.nupkg") { - dotnet nuget push "./nugets/*.nupkg" --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate + dotnet nuget push ".\nugets\*.nupkg" --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate } - name: Upload WPF NuGet Artifact diff --git a/src/Lemon.ModuleNavigation.Avaloniaui/AssemblyInfo.cs b/src/Lemon.ModuleNavigation.Avaloniaui/AssemblyInfo.cs index b41cdfb..b93bbf1 100644 --- a/src/Lemon.ModuleNavigation.Avaloniaui/AssemblyInfo.cs +++ b/src/Lemon.ModuleNavigation.Avaloniaui/AssemblyInfo.cs @@ -2,4 +2,5 @@ [assembly: XmlnsPrefix("https://github.com/NeverMorewd/Lemon.ModuleNavigation", "lm")] [assembly: XmlnsDefinition("https://github.com/NeverMorewd/Lemon.ModuleNavigation", "Lemon.ModuleNavigation")] -[assembly: XmlnsDefinition("https://github.com/NeverMorewd/Lemon.ModuleNavigation", "Lemon.ModuleNavigation.Avaloniaui")] \ No newline at end of file +[assembly: XmlnsDefinition("https://github.com/NeverMorewd/Lemon.ModuleNavigation", "Lemon.ModuleNavigation.Avaloniaui")] +[assembly: XmlnsDefinition("https://github.com/NeverMorewd/Lemon.ModuleNavigation", "Lemon.ModuleNavigation.Avaloniaui.Regions")] \ No newline at end of file diff --git a/src/Lemon.ModuleNavigation.Avaloniaui/DialogService.cs b/src/Lemon.ModuleNavigation.Avaloniaui/DialogService.cs index 8768985..6d527dd 100644 --- a/src/Lemon.ModuleNavigation.Avaloniaui/DialogService.cs +++ b/src/Lemon.ModuleNavigation.Avaloniaui/DialogService.cs @@ -1,7 +1,7 @@ using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Lemon.ModuleNavigation.Abstractions; -using Lemon.ModuleNavigation.Dialogs; +using Lemon.ModuleNavigation.Core; using Microsoft.Extensions.DependencyInjection; namespace Lemon.ModuleNavigation.Avaloniaui; diff --git a/src/Lemon.ModuleNavigation.Avaloniaui/Lemon.ModuleNavigation.Avaloniaui.csproj b/src/Lemon.ModuleNavigation.Avaloniaui/Lemon.ModuleNavigation.Avaloniaui.csproj index eced45c..ce7dc98 100644 --- a/src/Lemon.ModuleNavigation.Avaloniaui/Lemon.ModuleNavigation.Avaloniaui.csproj +++ b/src/Lemon.ModuleNavigation.Avaloniaui/Lemon.ModuleNavigation.Avaloniaui.csproj @@ -15,9 +15,13 @@ + + + + diff --git a/src/Lemon.ModuleNavigation.Avaloniaui/NavigationExtension.cs b/src/Lemon.ModuleNavigation.Avaloniaui/NavigationExtension.cs index 19f3fcc..685acac 100644 --- a/src/Lemon.ModuleNavigation.Avaloniaui/NavigationExtension.cs +++ b/src/Lemon.ModuleNavigation.Avaloniaui/NavigationExtension.cs @@ -36,7 +36,7 @@ void LoadedHandler(object? sender, RoutedEventArgs e) { var serviceProvider = navigationProvider!.ServiceProvider; var handler = serviceProvider.GetRequiredService(); - handler.RegionManager.AddRegion(currentValue, control.ToContainer(currentValue)); + handler.RegionManager.AddRegion(currentValue, control.ToRegion(currentValue)); } control.Loaded -= LoadedHandler; } @@ -112,7 +112,7 @@ public static void SetModuleContainerName(Control control, string value) #region CanUnloadProperty public static readonly AttachedProperty CanUnloadProperty = AvaloniaProperty.RegisterAttached("CanUnload", - defaultValue: true, + defaultValue: false, coerce: CoerceCanUnload); private static bool CoerceCanUnload(AvaloniaObject targetObject, bool currentValue) @@ -160,17 +160,23 @@ private static void UnloadModule(object? sender, RoutedEventArgs e) { if (sender is Button button) { + var view = button.FindLogicalAncestorOfType(includeSelf: false); + if (view != null) + { + } var tabItem = button.FindLogicalAncestorOfType(includeSelf: false) ?? throw new InvalidOperationException($"There is no 'TabItem' found in parents of {button}"); var tabContainer = tabItem.FindLogicalAncestorOfType(includeSelf: false); if (tabContainer != null) { - IModule item = tabItem.DataContext as IModule ?? throw new InvalidOperationException($"The DataContext of tabItem is not derived from IModule"); - if (item.CanUnload) + if (tabItem.DataContext is IModule item) { - if (tabContainer.DataContext is IServiceAware serviceAware) + if (item.CanUnload) { - var handler = serviceAware.ServiceProvider.GetRequiredService(); - handler.ModuleManager.ActiveModules.Remove(item); + if (tabContainer.DataContext is IServiceAware serviceAware) + { + var handler = serviceAware.ServiceProvider.GetRequiredService(); + handler.ModuleManager.ActiveModules.Remove(item); + } } } } diff --git a/src/Lemon.ModuleNavigation.Avaloniaui/Regions/ContentRegion.cs b/src/Lemon.ModuleNavigation.Avaloniaui/Regions/ContentRegion.cs new file mode 100644 index 0000000..806709a --- /dev/null +++ b/src/Lemon.ModuleNavigation.Avaloniaui/Regions/ContentRegion.cs @@ -0,0 +1,117 @@ +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using Avalonia.Data; +using Avalonia.Markup.Xaml.Templates; +using Lemon.ModuleNavigation.Abstractions; +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace Lemon.ModuleNavigation.Avaloniaui.Regions; + +public class ContentRegion : Region, IContentRegionContext +{ + private readonly ContentControl _contentControl; + public ContentRegion(string name, ContentControl contentControl) : base(name) + { + _contentControl = contentControl; + SetBindingContentTemplate(); + SetBindingContent(); + } + + private object? _content; + public object? Content + { + get => _content; + set + { + _content = value; + OnPropertyChanged(); + } + } + + private IDataTemplate? _contentTemplate; + public IDataTemplate? ContentTemplate + { + get => _contentTemplate; + set + { + _contentTemplate = value; + OnPropertyChanged(); + } + } + + public event PropertyChangedEventHandler? PropertyChanged; + + /// + /// When Views with same ViewName were found, the latest one will be picked. + /// + /// + public override void Activate(NavigationContext target) + { + if (ViewCache.TryGetValue(target, out IView? accurateView)) + { + target.View = accurateView; + Content = target; + } + else if (ViewNameCache.TryGetValue(target.ViewName, out IView? view) + && view.DataContext is INavigationAware navigationAware + && navigationAware.IsNavigationTarget(target)) + { + var context = Contexts.First(c => c.ViewName == target.ViewName); + context.View = view; + Content = context; + } + else + { + Contexts.Add(target); + Content = target; + } + } + + public override void DeActivate(string regionName) + { + if (Content is NavigationContext current) + { + if (current.ViewName == regionName) + { + Contexts.Remove(current); + Content = null; + } + } + } + public override void DeActivate(NavigationContext navigationContext) + { + if (Content is NavigationContext current) + { + if (NavigationContext.ViewNameComparer.Equals(current, navigationContext)) + { + Contexts.Remove(current); + Content = null; + } + } + } + + protected virtual void SetBindingContentTemplate() + { + ContentTemplate = RegionContentTemplate; + _contentControl.Bind(ContentControl.ContentTemplateProperty, + new Binding(nameof(ContentTemplate)) + { + Source = this, + Mode = BindingMode.TwoWay + }); + } + protected virtual void SetBindingContent() + { + _contentControl.Bind(ContentControl.ContentProperty, + new Binding(nameof(Content)) + { + Source = this, + Mode = BindingMode.TwoWay + }); + } + protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} diff --git a/src/Lemon.ModuleNavigation.Avaloniaui/Regions/ItemsRegion.cs b/src/Lemon.ModuleNavigation.Avaloniaui/Regions/ItemsRegion.cs new file mode 100644 index 0000000..3ac3d7d --- /dev/null +++ b/src/Lemon.ModuleNavigation.Avaloniaui/Regions/ItemsRegion.cs @@ -0,0 +1,134 @@ +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.Data; +using Avalonia.Markup.Xaml.Templates; +using Lemon.ModuleNavigation.Abstractions; +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace Lemon.ModuleNavigation.Avaloniaui.Regions; + +public class ItemsRegion : Region, IItemsRegionDataContext +{ + private readonly ItemsControl _itemsControl; + public ItemsRegion(string name, ItemsControl itemsControl) : base(name) + { + _itemsControl = itemsControl; + SetBindingItemTemplate(); + SetBindingSelectedItem(); + SetBindingItemsSource(); + } + private object? _selectItem; + public object? SelectedItem + { + get + { + return _selectItem; + } + set + { + _selectItem = value; + OnPropertyChanged(); + } + } + + private IDataTemplate? _itemsTemplate; + public IDataTemplate? ItemTemplate + { + get => _itemsTemplate; + set + { + _itemsTemplate = value; + OnPropertyChanged(); + } + } + public event PropertyChangedEventHandler? PropertyChanged; + + public override void ScrollIntoView(int index) + { + _itemsControl.ScrollIntoView(index); + } + public override void ScrollIntoView(NavigationContext item) + { + _itemsControl.ScrollIntoView(item); + } + + /// + /// When Views with same ViewName were found, the earliest one will be picked. + /// + /// + public override void Activate(NavigationContext target) + { + try + { + if (ViewCache.TryGetValue(target, out IView? accurateView)) + { + target.View = accurateView; + SelectedItem = target; + return; + } + var context = Contexts.FirstOrDefault(c => c.ViewName == target.ViewName); + if (context is not null + && context.View is not null + && context.View.DataContext is INavigationAware navigationAware + && navigationAware.IsNavigationTarget(target)) + { + SelectedItem = context; + return; + } + Contexts.Add(target); + SelectedItem = target; + } + finally + { + ScrollIntoView((SelectedItem as NavigationContext)!); + } + } + public override void DeActivate(string viewName) + { + Contexts.Remove(Contexts.Last(c => c.ViewName == viewName)); + } + public override void DeActivate(NavigationContext navigationContext) + { + Contexts.Remove(navigationContext); + } + public void Add(NavigationContext item) + { + Contexts.Add(item); + } + + protected virtual void SetBindingItemsSource() + { + _itemsControl.Bind(ItemsControl.ItemsSourceProperty, + new Binding(nameof(Contexts)) + { + Source = this + }); + } + protected virtual void SetBindingItemTemplate() + { + ItemTemplate = RegionContentTemplate; + _itemsControl.Bind(ItemsControl.ItemTemplateProperty, + new Binding(nameof(ItemTemplate)) + { + Source = this + }); + } + protected virtual void SetBindingSelectedItem() + { + if (_itemsControl is SelectingItemsControl selector) + { + selector.Bind(SelectingItemsControl.SelectedItemProperty, + new Binding(nameof(SelectedItem)) + { + Source = this, + Mode = BindingMode.TwoWay + }); + } + } + protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} diff --git a/src/Lemon.ModuleNavigation.Avaloniaui/Regions/Region.cs b/src/Lemon.ModuleNavigation.Avaloniaui/Regions/Region.cs new file mode 100644 index 0000000..58377d5 --- /dev/null +++ b/src/Lemon.ModuleNavigation.Avaloniaui/Regions/Region.cs @@ -0,0 +1,151 @@ +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using Lemon.ModuleNavigation.Abstractions; +using Lemon.ModuleNavigation.Core; +using Microsoft.Extensions.DependencyInjection; +using System.Collections.Concurrent; +using System.Collections.ObjectModel; +using System.Collections.Specialized; + +namespace Lemon.ModuleNavigation.Avaloniaui.Regions; + +public abstract class Region : IRegion +{ + public Region(string name) + { + Name = name; + Current = new(); + ViewCache = []; + Contexts = []; + RegionContentTemplate = CreateRegionDataTemplate(); + Contexts.CollectionChanged += Contexts_CollectionChanged; + } + protected ConcurrentItem<(IView View, INavigationAware NavigationAware)> Current + { + get; + } + public string Name + { + get; + } + protected ConcurrentDictionary ViewCache + { + get; + } + + protected ConcurrentDictionary ViewNameCache + { + get + { + return new(Contexts + .GroupBy(kv => kv.ViewName) + .Select(group => new KeyValuePair( + group.Key, + group.Last().View!))); + } + } + public ObservableCollection Contexts + { + get; + } + + public IDataTemplate? RegionContentTemplate + { + get; + } + public virtual void ScrollIntoView(int index) + { + throw new NotImplementedException(); + } + public virtual void ScrollIntoView(NavigationContext item) + { + throw new NotImplementedException(); + } + public abstract void Activate(NavigationContext target); + public abstract void DeActivate(string viewName); + public abstract void DeActivate(NavigationContext target); + + protected IView? ResolveView(NavigationContext context) + { + var view = context.View; + if (view is null) + { + view = context.ServiceProvider.GetRequiredKeyedService(context.ViewName); + var navigationAware = context.ServiceProvider.GetRequiredKeyedService(context.ViewName); + + if (Current.TryTakeData(out var previousData)) + { + previousData.NavigationAware.OnNavigatedFrom(context); + } + + view.DataContext = navigationAware; + navigationAware.OnNavigatedTo(context); + navigationAware.RequestUnload += () => + { + DeActivate(context); + }; + Current.SetData((view, navigationAware)); + context.View = view; + ViewCache.AddOrUpdate(context, view, (key, value) => view); + } + return view; + } + + protected virtual void WhenContextsAdded(IEnumerable contexts) + { + + } + protected virtual void WhenContextsRemoved(IEnumerable contexts) + { + + } + private void Contexts_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Add) + { + if (e.NewItems is not null) + { + WhenContextsAdded(e.NewItems.Cast()); + } + } + if (e.Action == NotifyCollectionChangedAction.Remove) + { + if (e.OldItems is not null) + { + WhenContextsRemoved(e.OldItems.Cast()); + } + } + } + private IDataTemplate CreateRegionDataTemplate() + { + return new FuncDataTemplate((context, np) => + { + if (context is null) + { + return null; + } + var view = context.View; + if (view is null) + { + view = context.ServiceProvider.GetRequiredKeyedService(context.ViewName); + var navigationAware = context.ServiceProvider.GetRequiredKeyedService(context.ViewName); + + if (Current.TryTakeData(out var previousData)) + { + previousData.NavigationAware.OnNavigatedFrom(context); + } + + view.DataContext = navigationAware; + navigationAware.OnNavigatedTo(context); + navigationAware.RequestUnload += () => + { + DeActivate(context); + }; + Current.SetData((view, navigationAware)); + context.View = view; + ViewCache.AddOrUpdate(context, view, (key, value) => view); + } + return view as Control; + }); + } +} diff --git a/src/Lemon.ModuleNavigation.Avaloniaui/Regions/TabRegion.cs b/src/Lemon.ModuleNavigation.Avaloniaui/Regions/TabRegion.cs new file mode 100644 index 0000000..015d89c --- /dev/null +++ b/src/Lemon.ModuleNavigation.Avaloniaui/Regions/TabRegion.cs @@ -0,0 +1,48 @@ +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using Avalonia.Data; +using Avalonia.Markup.Xaml.Templates; +using Lemon.ModuleNavigation.Abstractions; + +namespace Lemon.ModuleNavigation.Avaloniaui.Regions; + +public class TabRegion : ItemsRegion, IContentRegionContext +{ + private readonly TabControl _tabControl; + public TabRegion(string name, TabControl tabControl) : base(name, tabControl) + { + _tabControl = tabControl; + SetBindingContentTemplate(); + } + + public object? Content + { + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); + } + + private IDataTemplate? _contentTemplate; + public IDataTemplate? ContentTemplate + { + get => _contentTemplate; + set + { + _contentTemplate = value; + OnPropertyChanged(); + } + } + protected override void SetBindingItemTemplate() + { + //base.SetBindingItemTemplate(); + } + protected virtual void SetBindingContentTemplate() + { + ContentTemplate = RegionContentTemplate; + _tabControl.Bind(TabControl.ContentTemplateProperty, + new Binding(nameof(ContentTemplate)) + { + Mode = BindingMode.TwoWay, + Source = this + }); + } +} diff --git a/src/Lemon.ModuleNavigation.Avaloniaui/RegionsExtension.cs b/src/Lemon.ModuleNavigation.Avaloniaui/RegionsExtension.cs index 19782bc..93cf0eb 100644 --- a/src/Lemon.ModuleNavigation.Avaloniaui/RegionsExtension.cs +++ b/src/Lemon.ModuleNavigation.Avaloniaui/RegionsExtension.cs @@ -1,17 +1,18 @@ using Avalonia.Controls; using Lemon.ModuleNavigation.Abstractions; +using Lemon.ModuleNavigation.Avaloniaui.Regions; namespace Lemon.ModuleNavigation.Avaloniaui; public static class RegionsExtension { - public static IRegion ToContainer(this Control control, string name) + public static IRegion ToRegion(this Control control, string name) { return control switch { - TabControl tabControl => new TabRegion(tabControl, name), - ItemsControl itemsControl => new ItemsRegion(itemsControl, name), - ContentControl contentControl => new ContentRegion(contentControl, name), + TabControl tabControl => new TabRegion(name, tabControl), + ItemsControl itemsControl => new ItemsRegion(name, itemsControl), + ContentControl contentControl => new ContentRegion(name, contentControl), _ => throw new NotSupportedException($"Unsupported control:{control.GetType()}"), }; } diff --git a/src/Lemon.ModuleNavigation.Avaloniaui/ContentRegion.cs b/src/Lemon.ModuleNavigation.Avaloniaui/RegionsOld/ContentRegion.cs similarity index 56% rename from src/Lemon.ModuleNavigation.Avaloniaui/ContentRegion.cs rename to src/Lemon.ModuleNavigation.Avaloniaui/RegionsOld/ContentRegion.cs index c2973b3..9b045c1 100644 --- a/src/Lemon.ModuleNavigation.Avaloniaui/ContentRegion.cs +++ b/src/Lemon.ModuleNavigation.Avaloniaui/RegionsOld/ContentRegion.cs @@ -1,11 +1,16 @@ using Avalonia.Controls; +using Lemon.ModuleNavigation.Abstractions; +using Lemon.ModuleNavigation.Core; +using System.Collections.Concurrent; using System.Collections.ObjectModel; using System.Collections.Specialized; -namespace Lemon.ModuleNavigation.Avaloniaui; +namespace Lemon.ModuleNavigation.Avaloniaui.RegionsOld; -public class ContentRegion : AvaloniauiRegion +public class ContentRegion : RegionBak { + private readonly ConcurrentDictionary _viewCache = new(); + private readonly ConcurrentItem<(IView View, INavigationAware NavigationAware)> _current = new(); private readonly ContentControl _contentControl; public ContentRegion(ContentControl contentControl, string name) : base() { @@ -37,18 +42,37 @@ public override void Activate(NavigationContext target) { if(Content is NavigationContext current) { - if (target.TargetViewName == current.TargetViewName + if (target.ViewName == current.ViewName && !target.RequestNew) { return; } } Content = target; + Contexts.Add(target); } - public override void DeActivate(NavigationContext target) + public override void DeActivate(string regionName) { - Content = null; + if (Content is NavigationContext current) + { + if (current.ViewName == regionName) + { + Contexts.Remove(current); + Content = null; + } + } + } + public override void DeActivate(NavigationContext navigationContext) + { + if (Content is NavigationContext current) + { + if (current == navigationContext) + { + Contexts.Remove(current); + Content = null; + } + } } private void ViewContents_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { diff --git a/src/Lemon.ModuleNavigation.Avaloniaui/ItemsRegion.cs b/src/Lemon.ModuleNavigation.Avaloniaui/RegionsOld/ItemsRegion.cs similarity index 87% rename from src/Lemon.ModuleNavigation.Avaloniaui/ItemsRegion.cs rename to src/Lemon.ModuleNavigation.Avaloniaui/RegionsOld/ItemsRegion.cs index 30d4eea..65702b5 100644 --- a/src/Lemon.ModuleNavigation.Avaloniaui/ItemsRegion.cs +++ b/src/Lemon.ModuleNavigation.Avaloniaui/RegionsOld/ItemsRegion.cs @@ -3,9 +3,9 @@ using System.Collections.ObjectModel; using System.Collections.Specialized; -namespace Lemon.ModuleNavigation.Avaloniaui; +namespace Lemon.ModuleNavigation.Avaloniaui.RegionsOld; -public class ItemsRegion : AvaloniauiRegion +public class ItemsRegion : RegionBak { private readonly ItemsControl _itemsControl; public ItemsRegion(ItemsControl itemsControl, string name) @@ -84,9 +84,13 @@ public override void Activate(NavigationContext target) SelectedItem = target; } } - public override void DeActivate(NavigationContext target) + public override void DeActivate(string viewName) { - Contexts.Remove(target); + Contexts.Remove(Contexts.Last(c => c.ViewName == viewName)); + } + public override void DeActivate(NavigationContext navigationContext) + { + Contexts.Remove(navigationContext); } public void Add(NavigationContext item) { @@ -110,7 +114,7 @@ private void ViewContents_CollectionChanged(object? sender, NotifyCollectionChan { foreach (var item in e.OldItems) { - _itemsControl.Items.Remove(e.OldItems); + _itemsControl.Items.Remove(item); } } } diff --git a/src/Lemon.ModuleNavigation.Avaloniaui/AvaloniauiRegion.cs b/src/Lemon.ModuleNavigation.Avaloniaui/RegionsOld/RegionBak.cs similarity index 79% rename from src/Lemon.ModuleNavigation.Avaloniaui/AvaloniauiRegion.cs rename to src/Lemon.ModuleNavigation.Avaloniaui/RegionsOld/RegionBak.cs index a07d3c0..4eb43f9 100644 --- a/src/Lemon.ModuleNavigation.Avaloniaui/AvaloniauiRegion.cs +++ b/src/Lemon.ModuleNavigation.Avaloniaui/RegionsOld/RegionBak.cs @@ -6,14 +6,14 @@ using System.Collections.Concurrent; using System.Collections.ObjectModel; -namespace Lemon.ModuleNavigation.Avaloniaui; +namespace Lemon.ModuleNavigation.Avaloniaui.RegionsOld; -public abstract class AvaloniauiRegion : IRegion +public abstract class RegionBak : IRegion { private readonly ConcurrentDictionary _viewCache = new(); private readonly ConcurrentItem<(IView View, INavigationAware NavigationAware)> _current = new(); - public AvaloniauiRegion() + public RegionBak() { RegionTemplate = CreateRegionDataTemplate(); } @@ -24,6 +24,7 @@ public AvaloniauiRegion() public IDataTemplate? RegionTemplate { get; set; } public abstract void Activate(NavigationContext target); + public abstract void DeActivate(string viewName); public abstract void DeActivate(NavigationContext target); private IDataTemplate CreateRegionDataTemplate() @@ -34,12 +35,12 @@ private IDataTemplate CreateRegionDataTemplate() return null; bool needNewView = context.RequestNew || - !_viewCache.TryGetValue(context.TargetViewName, out IView? view); + !_viewCache.TryGetValue(context.ViewName, out IView? view); if (needNewView) { - view = context.ServiceProvider.GetRequiredKeyedService(context.TargetViewName); - var navigationAware = context.ServiceProvider.GetRequiredKeyedService(context.TargetViewName); + view = context.ServiceProvider.GetRequiredKeyedService(context.ViewName); + var navigationAware = context.ServiceProvider.GetRequiredKeyedService(context.ViewName); if (_current.TryTakeData(out var previousData)) { @@ -51,11 +52,11 @@ private IDataTemplate CreateRegionDataTemplate() view.DataContext = navigationAware; navigationAware.OnNavigatedTo(context); _current.SetData((view, navigationAware)); - _viewCache.TryAdd(context.TargetViewName, view); + _viewCache.TryAdd(context.ViewName, view); } else { - view = _viewCache[context.TargetViewName]; + view = _viewCache[context.ViewName]; } return view as Control; diff --git a/src/Lemon.ModuleNavigation.Avaloniaui/TabRegion.cs b/src/Lemon.ModuleNavigation.Avaloniaui/RegionsOld/TabRegion.cs similarity index 54% rename from src/Lemon.ModuleNavigation.Avaloniaui/TabRegion.cs rename to src/Lemon.ModuleNavigation.Avaloniaui/RegionsOld/TabRegion.cs index 5b5b460..c5b5070 100644 --- a/src/Lemon.ModuleNavigation.Avaloniaui/TabRegion.cs +++ b/src/Lemon.ModuleNavigation.Avaloniaui/RegionsOld/TabRegion.cs @@ -1,40 +1,64 @@ using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Data; using System.Collections.ObjectModel; using System.Collections.Specialized; +using System.ComponentModel; +using System.Runtime.CompilerServices; -namespace Lemon.ModuleNavigation.Avaloniaui; +namespace Lemon.ModuleNavigation.Avaloniaui.RegionsOld; -public class TabRegion : AvaloniauiRegion +public class TabRegion : RegionBak, INotifyPropertyChanged { private readonly TabControl _tabControl; - public TabRegion(TabControl tabControl, string name) + public TabRegion(TabControl tabControl, string name) { _tabControl = tabControl; - _tabControl.ContentTemplate = RegionTemplate; Contexts = []; - Contexts.CollectionChanged += ViewContents_CollectionChanged; + //Contexts.CollectionChanged += ViewContents_CollectionChanged; + + + _tabControl.Bind(SelectingItemsControl.SelectedItemProperty, + new Binding(nameof(SelectedItem)) + { + Mode = BindingMode.TwoWay, + Source = this + }); + _tabControl.Bind(ItemsControl.ItemsSourceProperty, + new Binding(nameof(Contexts)) + { + Source = this + }); + _tabControl.ContentTemplate = RegionTemplate; + Name = name; } public override string Name { get; } - public object? SelectedItem + private NavigationContext? _selectItem; + public NavigationContext? SelectedItem { get { - return _tabControl.SelectedItem; + return _selectItem; } set { - _tabControl.SelectedItem = value; + _selectItem = value; + OnPropertyChanged(); } } public override ObservableCollection Contexts { get; } - + public event PropertyChangedEventHandler? PropertyChanged; + protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } public override void Activate(NavigationContext target) { if (!target.RequestNew) @@ -63,9 +87,13 @@ public override void Activate(NavigationContext target) SelectedItem = target; } } - public override void DeActivate(NavigationContext target) + public override void DeActivate(string viewName) + { + Contexts.Remove(Contexts.Last(c => c.ViewName == viewName)); + } + public override void DeActivate(NavigationContext navigationContext) { - Contexts.Remove(target); + Contexts.Remove(navigationContext); } public void Add(NavigationContext item) { @@ -89,7 +117,7 @@ private void ViewContents_CollectionChanged(object? sender, NotifyCollectionChan { foreach (var item in e.OldItems) { - _tabControl.Items.Remove(e.OldItems); + _tabControl.Items.Remove(item); } } } diff --git a/src/Lemon.ModuleNavigation.Sample/ModuleAs/ViewModelA.cs b/src/Lemon.ModuleNavigation.Sample/ModuleAs/ViewModelA.cs index 46305a9..08513b2 100644 --- a/src/Lemon.ModuleNavigation.Sample/ModuleAs/ViewModelA.cs +++ b/src/Lemon.ModuleNavigation.Sample/ModuleAs/ViewModelA.cs @@ -5,7 +5,7 @@ namespace Lemon.ModuleNavigation.Sample.ModuleAs; -public class ViewModelA : SampleViewModelBase, IModuleNavigationAware +public class ViewModelA : BaseNavigationViewModel, IModuleNavigationAware { private readonly NavigationService _navigationService; public ViewModelA(NavigationService navigationService) diff --git a/src/Lemon.ModuleNavigation.Sample/ModuleBs/ViewModelB.cs b/src/Lemon.ModuleNavigation.Sample/ModuleBs/ViewModelB.cs index 7d1a96c..ca58f6a 100644 --- a/src/Lemon.ModuleNavigation.Sample/ModuleBs/ViewModelB.cs +++ b/src/Lemon.ModuleNavigation.Sample/ModuleBs/ViewModelB.cs @@ -5,7 +5,7 @@ namespace Lemon.ModuleNavigation.Sample.ModuleBs; -public class ViewModelB : SampleViewModelBase, IModuleNavigationAware +public class ViewModelB : BaseNavigationViewModel, IModuleNavigationAware { private readonly NavigationService _navigationService; public ViewModelB(NavigationService navigationService) diff --git a/src/Lemon.ModuleNavigation.Sample/ModuleCs/ViewModelC.cs b/src/Lemon.ModuleNavigation.Sample/ModuleCs/ViewModelC.cs index accbfb9..a66c653 100644 --- a/src/Lemon.ModuleNavigation.Sample/ModuleCs/ViewModelC.cs +++ b/src/Lemon.ModuleNavigation.Sample/ModuleCs/ViewModelC.cs @@ -6,7 +6,7 @@ namespace Lemon.ModuleNavigation.Sample.ModuleCs { - public class ViewModelC : SampleViewModelBase, IModuleNavigationAware, IServiceAware + public class ViewModelC : BaseNavigationViewModel, IModuleNavigationAware, IServiceAware { private readonly IModuleNavigationService _navigationService; private readonly IServiceProvider _moduleServiceProvider; diff --git a/src/Lemon.ModuleNavigation.Sample/ModuleCs/ViewModels/SubViewModel01.cs b/src/Lemon.ModuleNavigation.Sample/ModuleCs/ViewModels/SubViewModel01.cs index 60d7fb1..07e14c0 100644 --- a/src/Lemon.ModuleNavigation.Sample/ModuleCs/ViewModels/SubViewModel01.cs +++ b/src/Lemon.ModuleNavigation.Sample/ModuleCs/ViewModels/SubViewModel01.cs @@ -6,7 +6,7 @@ namespace Lemon.ModuleNavigation.Sample.ModuleCs.ViewModels; -public class SubViewModel01 : SampleViewModelBase, IModuleNavigationAware +public class SubViewModel01 : BaseNavigationViewModel, IModuleNavigationAware { private readonly ILogger _logger; public SubViewModel01(IServiceProvider serviceProvider, IServiceProviderDecorator appServiceProvider) diff --git a/src/Lemon.ModuleNavigation.Sample/ModuleCs/ViewModels/SubViewModel02.cs b/src/Lemon.ModuleNavigation.Sample/ModuleCs/ViewModels/SubViewModel02.cs index ce32cef..309c335 100644 --- a/src/Lemon.ModuleNavigation.Sample/ModuleCs/ViewModels/SubViewModel02.cs +++ b/src/Lemon.ModuleNavigation.Sample/ModuleCs/ViewModels/SubViewModel02.cs @@ -4,7 +4,7 @@ namespace Lemon.ModuleNavigation.Sample.ModuleCs.ViewModels; -public class SubViewModel02 : SampleViewModelBase, IModuleNavigationAware +public class SubViewModel02 : BaseNavigationViewModel, IModuleNavigationAware { public SubViewModel02(IServiceProvider serviceProvider) { diff --git a/src/Lemon.ModuleNavigation.Sample/ViewModels/BaseNavigationViewModel.cs b/src/Lemon.ModuleNavigation.Sample/ViewModels/BaseNavigationViewModel.cs new file mode 100644 index 0000000..4eb964d --- /dev/null +++ b/src/Lemon.ModuleNavigation.Sample/ViewModels/BaseNavigationViewModel.cs @@ -0,0 +1,56 @@ +using Lemon.ModuleNavigation.Abstractions; +using ReactiveUI; +using System; +using System.Diagnostics; +using System.Reactive; + +namespace Lemon.ModuleNavigation.Sample.ViewModels; + +public class BaseNavigationViewModel : ReactiveObject, INavigationAware, IDisposable +{ + public virtual string Greeting => $"Welcome to {GetType().Name}[{Environment.ProcessId}][{Environment.CurrentManagedThreadId}]{Environment.NewLine}{DateTime.Now:yyyy-MM-dd HH-mm-ss.ffff}"; + + + public BaseNavigationViewModel() + { + UnloadViewCommand = ReactiveCommand.Create(() => + { + var code = this.GetHashCode(); + Debug.WriteLine(code); + RequestUnload?.Invoke(); + }); + } + public ReactiveCommand UnloadViewCommand + { + get; + } + + public event Action? RequestUnload; + + public virtual void Dispose() + { + + } + + public virtual bool IsNavigationTarget(NavigationContext navigationContext) + { + if (navigationContext.Parameters is not null) + { + if (navigationContext.Parameters.TryGetValue("requestNew", out bool requestNew)) + { + return !requestNew; + } + } + return true; + } + + public virtual void OnNavigatedFrom(NavigationContext navigationContext) + { + + } + + public virtual void OnNavigatedTo(NavigationContext navigationContext) + { + + } +} diff --git a/src/Lemon.ModuleNavigation.Sample/ViewModels/MainViewModel.cs b/src/Lemon.ModuleNavigation.Sample/ViewModels/MainViewModel.cs index 7f50b2a..f29c86b 100644 --- a/src/Lemon.ModuleNavigation.Sample/ViewModels/MainViewModel.cs +++ b/src/Lemon.ModuleNavigation.Sample/ViewModels/MainViewModel.cs @@ -1,6 +1,5 @@ using Lemon.ModuleNavigation.Abstractions; using Lemon.ModuleNavigation.Core; -using Lemon.ModuleNavigation.Dialogs; using Lemon.ModuleNavigation.Extensions; using Microsoft.Extensions.Logging; using ReactiveUI; @@ -11,7 +10,7 @@ namespace Lemon.ModuleNavigation.Sample.ViewModels; -public class MainViewModel : SampleViewModelBase, IServiceAware +public class MainViewModel : ReactiveObject, IServiceAware { private readonly NavigationService _navigationService; private readonly IServiceProvider _serviceProvider; @@ -31,9 +30,9 @@ public MainViewModel(IEnumerable modules, _dialogService = dialogService; _regionManager = regionManager; // default views for different regions - _navigationService.RequestViewNavigation("ContentRegion", "ViewAlpha", false); - _navigationService.RequestViewNavigation("TransitioningContentRegion", "ViewAlpha", false); - Modules = new ObservableCollection(modules); + _navigationService.RequestViewNavigation("ContentRegion", "ViewAlpha"); + _navigationService.RequestViewNavigation("TransitioningContentRegion", "ViewAlpha"); + Modules = [.. modules]; ToViewCommand = ReactiveCommand.Create(content => { var viewName = content; @@ -44,10 +43,18 @@ public MainViewModel(IEnumerable modules, requestNew = true; } - _navigationService.RequestViewNavigation("ContentRegion", viewName, requestNew); - _navigationService.RequestViewNavigation("TabRegion", viewName, requestNew); - _navigationService.RequestViewNavigation("ItemsRegion", viewName, requestNew); - _navigationService.RequestViewNavigation("TransitioningContentRegion", viewName, requestNew); + _navigationService.RequestViewNavigation("ContentRegion", + viewName, + new NavigationParameters { { "requestNew", requestNew } }); + _navigationService.RequestViewNavigation("TabRegion", + viewName, + new NavigationParameters { { "requestNew", requestNew } }); + _navigationService.RequestViewNavigation("ItemsRegion", + viewName, + new NavigationParameters { { "requestNew", requestNew } }); + _navigationService.RequestViewNavigation("TransitioningContentRegion", + viewName, + new NavigationParameters { { "requestNew", requestNew } }); }); ShowCommand = ReactiveCommand.Create(content => { @@ -94,15 +101,25 @@ await _dialogService.ShowDialog(content, _logger.LogDebug($"ShowDialog over:{result}"); }); + + UnloadViewCommand = ReactiveCommand.Create((context) => + { + _regionManager.RequestViewUnload(context); + }); + _regionManager.NavigationSubscribe(n => { - _logger.LogDebug($"Request to : {n.RegionName}.{n.TargetViewName}"); + _logger.LogDebug($"Request to : {n.RegionName}.{n.ViewName}"); }); _regionManager.NavigationSubscribe(r => { _logger.LogDebug($"New region : {r.Name}"); }); } + public ReactiveCommand UnloadViewCommand + { + get; + } public ReactiveCommand ToViewCommand { get; diff --git a/src/Lemon.ModuleNavigation.Sample/ViewModels/SampleViewModelBase.cs b/src/Lemon.ModuleNavigation.Sample/ViewModels/SampleViewModelBase.cs deleted file mode 100644 index 1fd699c..0000000 --- a/src/Lemon.ModuleNavigation.Sample/ViewModels/SampleViewModelBase.cs +++ /dev/null @@ -1,13 +0,0 @@ -using ReactiveUI; -using System; - -namespace Lemon.ModuleNavigation.Sample.ViewModels; - -public class SampleViewModelBase : ReactiveObject, IDisposable -{ - public virtual string Greeting => $"Welcome to {GetType().Name}[{Environment.ProcessId}][{Environment.CurrentManagedThreadId}]{Environment.NewLine}{DateTime.Now:yyyy-MM-dd HH-mm-ss.ffff}"; - public virtual void Dispose() - { - - } -} diff --git a/src/Lemon.ModuleNavigation.Sample/ViewModels/ViewAlphaViewModel.cs b/src/Lemon.ModuleNavigation.Sample/ViewModels/ViewAlphaViewModel.cs index a9e0370..7ea4585 100644 --- a/src/Lemon.ModuleNavigation.Sample/ViewModels/ViewAlphaViewModel.cs +++ b/src/Lemon.ModuleNavigation.Sample/ViewModels/ViewAlphaViewModel.cs @@ -1,67 +1,48 @@ using Lemon.ModuleNavigation.Abstractions; using Lemon.ModuleNavigation.Core; -using Lemon.ModuleNavigation.Dialogs; using Microsoft.Extensions.Logging; using ReactiveUI; using System; using System.Reactive; -namespace Lemon.ModuleNavigation.Sample.ViewModels +namespace Lemon.ModuleNavigation.Sample.ViewModels; + +public class ViewAlphaViewModel : BaseNavigationViewModel, IDialogAware { - public class ViewAlphaViewModel : SampleViewModelBase, - IDialogAware, - INavigationAware + private readonly ILogger _logger; + public ViewAlphaViewModel(ILogger logger) { - private readonly ILogger _logger; - public ViewAlphaViewModel(ILogger logger) - { - _logger = logger; - CloseCommand = ReactiveCommand.Create(() => - { - var param = new DialogParameters - { - { "from", nameof(ViewAlphaViewModel) } - }; - RequestClose?.Invoke(new DialogResult(ButtonResult.OK, param)); - }); - } - private bool _isDialog = false; - public bool IsDialog + _logger = logger; + CloseCommand = ReactiveCommand.Create(() => { - get => _isDialog; - set + var param = new DialogParameters { - this.RaiseAndSetIfChanged(ref _isDialog, value); - } - } - public ReactiveCommand CloseCommand { get; } - public string Title => nameof(ViewAlphaViewModel); - public event Action? RequestClose; - - public void OnDialogClosed() - { - _logger.LogInformation("OnDialogClosed"); - } - - public void OnDialogOpened(IDialogParameters? parameters) - { - _logger.LogInformation($"OnDialogOpened:{parameters?.ToString()}"); - IsDialog = true; - } - - public void OnNavigatedTo(NavigationContext navigationContext) + { "from", nameof(ViewAlphaViewModel) } + }; + RequestClose?.Invoke(new DialogResult(ButtonResult.OK, param)); + }); + } + private bool _isDialog = false; + public bool IsDialog + { + get => _isDialog; + set { - //throw new NotImplementedException(); + this.RaiseAndSetIfChanged(ref _isDialog, value); } + } + public ReactiveCommand CloseCommand { get; } + public string Title => nameof(ViewAlphaViewModel); + public event Action? RequestClose; - public bool IsNavigationTarget(NavigationContext navigationContext) - { - return true; - } + public void OnDialogClosed() + { + _logger.LogInformation("OnDialogClosed"); + } - public void OnNavigatedFrom(NavigationContext navigationContext) - { - //throw new NotImplementedException(); - } + public void OnDialogOpened(IDialogParameters? parameters) + { + _logger.LogInformation($"OnDialogOpened:{parameters?.ToString()}"); + IsDialog = true; } } diff --git a/src/Lemon.ModuleNavigation.Sample/ViewModels/ViewBetaViewModel.cs b/src/Lemon.ModuleNavigation.Sample/ViewModels/ViewBetaViewModel.cs index f03ffe3..b0c30e7 100644 --- a/src/Lemon.ModuleNavigation.Sample/ViewModels/ViewBetaViewModel.cs +++ b/src/Lemon.ModuleNavigation.Sample/ViewModels/ViewBetaViewModel.cs @@ -1,67 +1,48 @@ using Lemon.ModuleNavigation.Abstractions; using Lemon.ModuleNavigation.Core; -using Lemon.ModuleNavigation.Dialogs; using Microsoft.Extensions.Logging; using ReactiveUI; using System; using System.Reactive; -namespace Lemon.ModuleNavigation.Sample.ViewModels +namespace Lemon.ModuleNavigation.Sample.ViewModels; + +public class ViewBetaViewModel : BaseNavigationViewModel, IDialogAware { - public class ViewBetaViewModel: SampleViewModelBase, - IDialogAware, - INavigationAware + private readonly ILogger _logger; + public ViewBetaViewModel(ILogger logger) { - private readonly ILogger _logger; - public ViewBetaViewModel(ILogger logger) - { - _logger = logger; - CloseCommand = ReactiveCommand.Create(() => - { - var param = new DialogParameters - { - { "from", nameof(ViewAlphaViewModel) } - }; - RequestClose?.Invoke(new DialogResult(ButtonResult.OK, param)); - }); - } - private bool _isDialog = false; - public bool IsDialog + _logger = logger; + CloseCommand = ReactiveCommand.Create(() => { - get => _isDialog; - set + var param = new DialogParameters { - this.RaiseAndSetIfChanged(ref _isDialog, value); - } - } - public ReactiveCommand CloseCommand { get; } - public string Title => nameof(ViewAlphaViewModel); - public event Action? RequestClose; - - public void OnDialogClosed() - { - _logger.LogInformation("OnDialogClosed"); - } - - public void OnDialogOpened(IDialogParameters? parameters) - { - _logger.LogInformation($"OnDialogOpened:{parameters?.ToString()}"); - IsDialog = true; - } - - public void OnNavigatedTo(NavigationContext navigationContext) + { "from", nameof(ViewAlphaViewModel) } + }; + RequestClose?.Invoke(new DialogResult(ButtonResult.OK, param)); + }); + } + private bool _isDialog = false; + public bool IsDialog + { + get => _isDialog; + set { - + this.RaiseAndSetIfChanged(ref _isDialog, value); } + } + public ReactiveCommand CloseCommand { get; } + public string Title => nameof(ViewAlphaViewModel); + public event Action? RequestClose; - public bool IsNavigationTarget(NavigationContext navigationContext) - { - return true; - } + public void OnDialogClosed() + { + _logger.LogInformation("OnDialogClosed"); + } - public void OnNavigatedFrom(NavigationContext navigationContext) - { - - } + public void OnDialogOpened(IDialogParameters? parameters) + { + _logger.LogInformation($"OnDialogOpened:{parameters?.ToString()}"); + IsDialog = true; } } diff --git a/src/Lemon.ModuleNavigation.Sample/Views/MainView.axaml b/src/Lemon.ModuleNavigation.Sample/Views/MainView.axaml index 20b365a..052df74 100644 --- a/src/Lemon.ModuleNavigation.Sample/Views/MainView.axaml +++ b/src/Lemon.ModuleNavigation.Sample/Views/MainView.axaml @@ -1,15 +1,15 @@ + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> - + @@ -59,9 +59,9 @@ Spacing="5">