diff --git a/Toolkit.Foundation.Avalonia/Navigation/ContentControlHandler.cs b/Toolkit.Foundation.Avalonia/Navigation/ContentControlHandler.cs new file mode 100644 index 0000000..7c27100 --- /dev/null +++ b/Toolkit.Foundation.Avalonia/Navigation/ContentControlHandler.cs @@ -0,0 +1,19 @@ +using Avalonia.Controls; +using Avalonia.Controls.Primitives; + +namespace Toolkit.Foundation.Avalonia +{ + public class ContentControlHandler : NavigationRouteHandler + { + public override void Receive(NavigationRouteRequest message) + { + if (message.Template is TemplatedControl control) + { + control.DataContext = message.Data; + message.Target.Content = control; + } + + message.Reply(true); + } + } +} diff --git a/Toolkit.Foundation.Avalonia/Navigation/ContentDialogHandler.cs b/Toolkit.Foundation.Avalonia/Navigation/ContentDialogHandler.cs new file mode 100644 index 0000000..c86e99d --- /dev/null +++ b/Toolkit.Foundation.Avalonia/Navigation/ContentDialogHandler.cs @@ -0,0 +1,54 @@ +using FluentAvalonia.UI.Controls; +using Kromek.Framework.Core.Extensions; +using Toolkit.Foundation.Avalonia; + +namespace Kromek.Framework.Avalonia +{ + public class ContentDialogHandler : NavigationRouteHandler + { + public override async void Receive(NavigationRouteRequest message) + { + if (message.Template is ContentDialog contentDialog) + { + async void HandleButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args) + { + ContentDialogButtonClickDeferral defferal = args.GetDeferral(); + + if (sender.DataContext is INavigationConfirmationAsync confirmationAsync) + { + if (!await confirmationAsync.CanConfirmAsync()) + { + args.Cancel = true; + } + } + + if (sender.DataContext is INavigationConfirmation confirmation) + { + if (!confirmation.CanConfirm()) + { + args.Cancel = true; + } + } + + if (!args.Cancel) + { + contentDialog.SecondaryButtonClick -= HandleButtonClick; + contentDialog.PrimaryButtonClick -= HandleButtonClick; + contentDialog.CloseButtonClick -= HandleButtonClick; + } + + defferal.Complete(); + } + + contentDialog.SecondaryButtonClick += HandleButtonClick; + contentDialog.PrimaryButtonClick += HandleButtonClick; + contentDialog.CloseButtonClick += HandleButtonClick; + + contentDialog.DataContext = message.Data; + await contentDialog.ShowAsync(); + + message.Reply(true); + } + } + } +} diff --git a/Toolkit.Foundation.Avalonia/Navigation/FrameHandler.cs b/Toolkit.Foundation.Avalonia/Navigation/FrameHandler.cs new file mode 100644 index 0000000..3df1bcb --- /dev/null +++ b/Toolkit.Foundation.Avalonia/Navigation/FrameHandler.cs @@ -0,0 +1,35 @@ +using Avalonia.Controls.Primitives; +using FluentAvalonia.UI.Controls; +using FluentAvalonia.UI.Navigation; +using Toolkit.Foundation.Avalonia; + +namespace Kromek.Framework.Avalonia +{ + public class FrameHandler : NavigationRouteHandler + { + public override async void Receive(NavigationRouteRequest message) + { + message.Target.NavigationPageFactory = new NavigationPageFactory(); + + TaskCompletionSource completionSource = new(); + if (message.Template is TemplatedControl content) + { + void HandleNavigated(object sender, NavigationEventArgs args) + { + message.Target.Navigated -= HandleNavigated; + if (message.Target.Content is TemplatedControl control) + { + control.DataContext = message.Data; + completionSource.SetResult(true); + } + } + + message.Target.Navigated += HandleNavigated; + message.Target.NavigateFromObject(content); + } + + bool result = await completionSource.Task; + message.Reply(result); + } + } +} diff --git a/Toolkit.Foundation.Avalonia/Navigation/NavigationPageFactory.cs b/Toolkit.Foundation.Avalonia/Navigation/NavigationPageFactory.cs new file mode 100644 index 0000000..58b4d1b --- /dev/null +++ b/Toolkit.Foundation.Avalonia/Navigation/NavigationPageFactory.cs @@ -0,0 +1,18 @@ +using FluentAvalonia.UI.Controls; +using Avalonia.Controls; + +namespace Toolkit.Foundation.Avalonia +{ + internal class NavigationPageFactory : INavigationPageFactory + { + public IControl? GetPage(Type srcType) + { + return default; + } + + public IControl GetPageFromObject(object target) + { + return (IControl)target; + } + } +} diff --git a/Toolkit.Foundation.Avalonia/Navigation/NavigationRoute.cs b/Toolkit.Foundation.Avalonia/Navigation/NavigationRoute.cs new file mode 100644 index 0000000..689671f --- /dev/null +++ b/Toolkit.Foundation.Avalonia/Navigation/NavigationRoute.cs @@ -0,0 +1,38 @@ +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; + +namespace Toolkit.Foundation.Avalonia +{ + public class NavigationRoute : INavigationRoute + { + private readonly INavigationRouteDescriptorCollection routes; + + public NavigationRoute(INavigationRouteDescriptorCollection routes) + { + this.routes = routes; + } + + public void Add(string name, object route) + { + if (route is TemplatedControl control) + { + void HandleUnloaded(object? sender, RoutedEventArgs args) + { + if (routes.FirstOrDefault(x => x.Route == sender) is INavigationRouteDescriptor descriptor) + { + routes.Remove(descriptor); + } + } + + control.Unloaded += HandleUnloaded; + } + + if (routes.FirstOrDefault(x => x.Name == name) is INavigationRouteDescriptor descriptor) + { + routes.Remove(descriptor); + } + + routes.Add(new NavigationRouteDescriptor(name, route)); + } + } +} diff --git a/Toolkit.Foundation.Avalonia/Navigation/NavigationRouteHandler.cs b/Toolkit.Foundation.Avalonia/Navigation/NavigationRouteHandler.cs new file mode 100644 index 0000000..d7a40f6 --- /dev/null +++ b/Toolkit.Foundation.Avalonia/Navigation/NavigationRouteHandler.cs @@ -0,0 +1,10 @@ +using Avalonia.Controls.Primitives; +using CommunityToolkit.Mvvm.Messaging; + +namespace Toolkit.Foundation.Avalonia +{ + public abstract class NavigationRouteHandler : IRecipient> where TTarget : TemplatedControl + { + public abstract void Receive(NavigationRouteRequest message); + } +} diff --git a/Toolkit.Foundation.Avalonia/Navigation/NavigationRouteRequest.cs b/Toolkit.Foundation.Avalonia/Navigation/NavigationRouteRequest.cs new file mode 100644 index 0000000..9fa8dae --- /dev/null +++ b/Toolkit.Foundation.Avalonia/Navigation/NavigationRouteRequest.cs @@ -0,0 +1,24 @@ +using Avalonia.Controls.Primitives; +using CommunityToolkit.Mvvm.Messaging.Messages; + +namespace Toolkit.Foundation.Avalonia +{ + public class NavigationRouteRequest : AsyncRequestMessage where TTarget : TemplatedControl + { + public NavigationRouteRequest(TTarget target, object? data, object? template, IDictionary? parameters = null) + { + Target = target; + Data = data; + Template = template; + Parameters = parameters; + } + + public TTarget Target { get; } + + public object? Data { get; } + + public object? Template { get; } + + public IDictionary? Parameters { get; } + } +} diff --git a/Toolkit.Foundation.Avalonia/Navigation/NavigationRouter.cs b/Toolkit.Foundation.Avalonia/Navigation/NavigationRouter.cs new file mode 100644 index 0000000..5614f5b --- /dev/null +++ b/Toolkit.Foundation.Avalonia/Navigation/NavigationRouter.cs @@ -0,0 +1,137 @@ +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using CommunityToolkit.Mvvm.Messaging; +using FluentAvalonia.UI.Controls; + +namespace Toolkit.Foundation.Avalonia +{ + public class NavigationRouter : INavigationRouter + { + private readonly INavigationRouteDescriptorCollection descriptors; + private readonly IMessenger messenger; + private readonly INamedDataTemplateFactory namedDataTemplateFactory; + private readonly INamedTemplateFactory namedTemplateFactory; + private readonly ITemplateDescriptorProvider templateDescriptorProvider; + private readonly ITemplateFactory templateFactory; + private readonly ITypedDataTemplateFactory typedDataTemplateFactory; + + public NavigationRouter(ITemplateDescriptorProvider templateDescriptorProvider, + ITemplateFactory templateFactory, + INamedTemplateFactory namedTemplateFactory, + INamedDataTemplateFactory namedDataTemplateFactory, + ITypedDataTemplateFactory typedDataTemplateFactory, + IMessenger messenger, + INavigationRouteDescriptorCollection descriptors) + { + this.templateDescriptorProvider = templateDescriptorProvider; + this.templateFactory = templateFactory; + this.namedTemplateFactory = namedTemplateFactory; + this.namedDataTemplateFactory = namedDataTemplateFactory; + this.typedDataTemplateFactory = typedDataTemplateFactory; + this.messenger = messenger; + this.descriptors = descriptors; + } + + public Task InitializeAsync() + { + messenger.Register(this, (sender, args) => OnNavigate(args)); + messenger.Register(this, (sender, args) => OnNavigateBack(args)); + + return Task.CompletedTask; + } + + private async void OnNavigate(Navigate args) + { + object? data = null; + object? template = null; + + Dictionary keyedParameters = new(); + List parameters = new(); + + foreach (object? parameter in args.Parameters) + { + if (parameter is not null) + { + if (parameter is KeyValuePair keyed) + { + keyedParameters.Add(keyed.Key, keyed.Value); + } + else + { + parameters.Add(parameter); + } + } + } + + if (args.Name is { Length: > 0 } name) + { + data = namedDataTemplateFactory.Create(name, parameters.ToArray()); + template = namedTemplateFactory.Create(name); + } + + if (args.Type is Type type) + { + data = typedDataTemplateFactory.Create(type, parameters.ToArray()); + template = templateFactory.Create(data); + } + + if (template is not null) + { + bool navigated = false; + if (template is ContentDialog contentDialog) + { + navigated = await messenger.Send(new NavigationRouteRequest(contentDialog, data, template, keyedParameters)); + } + else + { + if (descriptors.FirstOrDefault(x => args.Route is string { } name && name == x.Name) is NavigationRouteDescriptor descriptor) + { + switch (descriptor.Route) + { + case Frame frame: + navigated = await messenger.Send(new NavigationRouteRequest(frame, data, template, keyedParameters)); + break; + case ContentControl contentControl: + navigated = await messenger.Send(new NavigationRouteRequest(contentControl, data, template, keyedParameters)); + break; + } + } + } + + if (navigated) + { + messenger.Send((Navigated)Navigated.Create((dynamic?)template, (dynamic?)data, keyedParameters)); + } + } + else + { + if (descriptors.FirstOrDefault(x => args.Route is string { } name && name == x.Name) is NavigationRouteDescriptor descriptor) + { + if (descriptor.Route is ContentControl contentControl) + { + contentControl.Content = null; + } + } + } + } + + private void OnNavigateBack(NavigateBack args) + { + if (descriptors.FirstOrDefault(x => args.Route is string { } name && name == x.Name) is NavigationRouteDescriptor descriptor) + { + if (descriptor.Route is ContentControl { Content: TemplatedControl content }) + { + if (content.DataContext is IDisposable disposable) + { + disposable.Dispose(); + } + } + + if (descriptor.Route is Frame frame) + { + frame.GoBack(); + } + } + } + } +} diff --git a/Toolkit.Foundation.Avalonia/Toolkit.Foundation.Avalonia.csproj b/Toolkit.Foundation.Avalonia/Toolkit.Foundation.Avalonia.csproj new file mode 100644 index 0000000..3dde651 --- /dev/null +++ b/Toolkit.Foundation.Avalonia/Toolkit.Foundation.Avalonia.csproj @@ -0,0 +1,18 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + + diff --git a/Toolkit.Foundation/Navigation/IEventParameter.cs b/Toolkit.Foundation/Navigation/IEventParameter.cs new file mode 100644 index 0000000..226c0b6 --- /dev/null +++ b/Toolkit.Foundation/Navigation/IEventParameter.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; + +namespace Toolkit.Foundation +{ + public interface IEventParameter + { + List GetValues(EventArgs args); + } +} \ No newline at end of file diff --git a/Toolkit.Foundation/Navigation/INavigationConfirmation.cs b/Toolkit.Foundation/Navigation/INavigationConfirmation.cs new file mode 100644 index 0000000..864cb3e --- /dev/null +++ b/Toolkit.Foundation/Navigation/INavigationConfirmation.cs @@ -0,0 +1,7 @@ +namespace Kromek.Framework.Core.Extensions +{ + public interface INavigationConfirmation + { + bool CanConfirm(); + } +} \ No newline at end of file diff --git a/Toolkit.Foundation/Navigation/INavigationConfirmationAsync.cs b/Toolkit.Foundation/Navigation/INavigationConfirmationAsync.cs new file mode 100644 index 0000000..4a9c6d3 --- /dev/null +++ b/Toolkit.Foundation/Navigation/INavigationConfirmationAsync.cs @@ -0,0 +1,8 @@ + +namespace Kromek.Framework.Core.Extensions +{ + public interface INavigationConfirmationAsync + { + Task CanConfirmAsync(); + } +} \ No newline at end of file diff --git a/Toolkit.Foundation/Navigation/INavigationRoute.cs b/Toolkit.Foundation/Navigation/INavigationRoute.cs new file mode 100644 index 0000000..2085a4e --- /dev/null +++ b/Toolkit.Foundation/Navigation/INavigationRoute.cs @@ -0,0 +1,7 @@ +namespace Toolkit.Foundation +{ + public interface INavigationRoute + { + void Add(string name, object route); + } +} \ No newline at end of file diff --git a/Toolkit.Foundation/Navigation/INavigationRouteDescriptor.cs b/Toolkit.Foundation/Navigation/INavigationRouteDescriptor.cs new file mode 100644 index 0000000..8288166 --- /dev/null +++ b/Toolkit.Foundation/Navigation/INavigationRouteDescriptor.cs @@ -0,0 +1,9 @@ +namespace Toolkit.Foundation +{ + public interface INavigationRouteDescriptor + { + object Route { get; } + + string? Name { get; } + } +} \ No newline at end of file diff --git a/Toolkit.Foundation/Navigation/INavigationRouteDescriptorCollection.cs b/Toolkit.Foundation/Navigation/INavigationRouteDescriptorCollection.cs new file mode 100644 index 0000000..818e746 --- /dev/null +++ b/Toolkit.Foundation/Navigation/INavigationRouteDescriptorCollection.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace Toolkit.Foundation +{ + public interface INavigationRouteDescriptorCollection : IList + { + } +} \ No newline at end of file diff --git a/Toolkit.Foundation/Navigation/INavigationRouter.cs b/Toolkit.Foundation/Navigation/INavigationRouter.cs new file mode 100644 index 0000000..e11f7ca --- /dev/null +++ b/Toolkit.Foundation/Navigation/INavigationRouter.cs @@ -0,0 +1,6 @@ +namespace Toolkit.Foundation +{ + public interface INavigationRouter : IInitializer + { + } +} \ No newline at end of file diff --git a/Toolkit.Foundation/Navigation/IParameter.cs b/Toolkit.Foundation/Navigation/IParameter.cs new file mode 100644 index 0000000..aea666d --- /dev/null +++ b/Toolkit.Foundation/Navigation/IParameter.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Toolkit.Foundation +{ + public interface IParameter + { + string? Key { get; } + + KeyValuePair? GetValue(object target); + } +} \ No newline at end of file diff --git a/Toolkit.Foundation/Navigation/Navigate.cs b/Toolkit.Foundation/Navigation/Navigate.cs new file mode 100644 index 0000000..e5278bf --- /dev/null +++ b/Toolkit.Foundation/Navigation/Navigate.cs @@ -0,0 +1,29 @@ +using System; + +namespace Toolkit.Foundation +{ + public record Navigate + { + public Navigate(string name, params object?[] parameters) + { + Name = name; + Parameters = parameters; + } + + public Navigate(Type type, params object?[] parameters) + { + Type = type; + Parameters = parameters; + } + + public Type? Type { get; } + + public object? Route { get; init; } + + public string? Name { get; } + + public string? FriendlyName { get; init; } + + public object?[] Parameters { get; } + } +} \ No newline at end of file diff --git a/Toolkit.Foundation/Navigation/NavigateBack.cs b/Toolkit.Foundation/Navigation/NavigateBack.cs new file mode 100644 index 0000000..dfb3ec6 --- /dev/null +++ b/Toolkit.Foundation/Navigation/NavigateBack.cs @@ -0,0 +1,4 @@ +namespace Toolkit.Foundation +{ + public record NavigateBack(object Route); +} \ No newline at end of file diff --git a/Toolkit.Foundation/Navigation/NavigateHandler.cs b/Toolkit.Foundation/Navigation/NavigateHandler.cs new file mode 100644 index 0000000..71899a0 --- /dev/null +++ b/Toolkit.Foundation/Navigation/NavigateHandler.cs @@ -0,0 +1,19 @@ +using CommunityToolkit.Mvvm.Messaging; + +namespace Toolkit.Foundation +{ + public class NavigateHandler : IRecipient + { + private readonly IMessenger messenger; + + public NavigateHandler(IMessenger messenger) + { + this.messenger = messenger; + } + + public void Receive(Navigate request) + { + messenger.Send(request); + } + } +} \ No newline at end of file diff --git a/Toolkit.Foundation/Navigation/Navigated.cs b/Toolkit.Foundation/Navigation/Navigated.cs new file mode 100644 index 0000000..c2485f0 --- /dev/null +++ b/Toolkit.Foundation/Navigation/Navigated.cs @@ -0,0 +1,30 @@ +namespace Toolkit.Foundation +{ + public class Navigated where TContent : class where TDataContext : class + { + public Navigated() + { + } + + public Navigated(TContent content, TDataContext dataContext, IDictionary? parameters = null) + { + Content = content; + DataContext = dataContext; + Parameters = parameters; + } + + public TContent? Content { get; } + + public TDataContext? DataContext { get; } + + public IDictionary? Parameters { get; } + } + + public class Navigated + { + public static Navigated Create(TTemplate content, TDataTemplate dataContext, IDictionary? parameters = null) where TTemplate : class where TDataTemplate : class + { + return new Navigated(content, dataContext, parameters); + } + } +} \ No newline at end of file diff --git a/Toolkit.Foundation/Navigation/NavigationRouteDescriptor.cs b/Toolkit.Foundation/Navigation/NavigationRouteDescriptor.cs new file mode 100644 index 0000000..0bc64a7 --- /dev/null +++ b/Toolkit.Foundation/Navigation/NavigationRouteDescriptor.cs @@ -0,0 +1,15 @@ +namespace Toolkit.Foundation +{ + public record NavigationRouteDescriptor : INavigationRouteDescriptor + { + public NavigationRouteDescriptor(string name, object route) + { + Name = name; + Route = route; + } + + public string Name { get; } + + public object Route { get; } + } +} \ No newline at end of file diff --git a/Toolkit.Foundation/Navigation/NavigationRouteDescriptorCollection.cs b/Toolkit.Foundation/Navigation/NavigationRouteDescriptorCollection.cs new file mode 100644 index 0000000..01eda79 --- /dev/null +++ b/Toolkit.Foundation/Navigation/NavigationRouteDescriptorCollection.cs @@ -0,0 +1,9 @@ +namespace Toolkit.Foundation +{ + public class NavigationRouteDescriptorCollection : List, INavigationRouteDescriptorCollection + { + public NavigationRouteDescriptorCollection(IEnumerable collection) : base(collection) + { + } + } +} \ No newline at end of file diff --git a/Toolkit.sln b/Toolkit.sln index dc93ed3..3fba99a 100644 --- a/Toolkit.sln +++ b/Toolkit.sln @@ -3,7 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.4.33110.190 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Toolkit.Foundation", "Toolkit.Foundation\Toolkit.Foundation.csproj", "{A3332A95-DF10-4A3F-A500-18513BF1EE2E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Toolkit.Foundation", "Toolkit.Foundation\Toolkit.Foundation.csproj", "{A3332A95-DF10-4A3F-A500-18513BF1EE2E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Toolkit.Foundation.Avalonia", "Toolkit.Foundation.Avalonia\Toolkit.Foundation.Avalonia.csproj", "{31217E1F-8F2D-468C-A9DC-839EE9B6F9D4}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -15,6 +17,10 @@ Global {A3332A95-DF10-4A3F-A500-18513BF1EE2E}.Debug|Any CPU.Build.0 = Debug|Any CPU {A3332A95-DF10-4A3F-A500-18513BF1EE2E}.Release|Any CPU.ActiveCfg = Release|Any CPU {A3332A95-DF10-4A3F-A500-18513BF1EE2E}.Release|Any CPU.Build.0 = Release|Any CPU + {31217E1F-8F2D-468C-A9DC-839EE9B6F9D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {31217E1F-8F2D-468C-A9DC-839EE9B6F9D4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {31217E1F-8F2D-468C-A9DC-839EE9B6F9D4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {31217E1F-8F2D-468C-A9DC-839EE9B6F9D4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE