UI - Added routing (#802)

ColorGradient - Fixed GetColor returning the first stop if position was after the last stop
This commit is contained in:
RobertBeekman authored and GitHub committed 2023-07-02 22:15:38 +02:00
1 parent 79309dcee7
commit 962b13c6ed
77 files changed
+2034 -854

No files matched your search

@@ -0,0 +1,24 @@
using System.Threading;
using System.Threading.Tasks;
using ReactiveUI;
namespace Artemis.UI.Shared.Routing;
/// <summary>
/// For internal use.
/// </summary>
/// <seealso cref="RoutableScreen{TScreen}"/>
/// <seealso cref="RoutableScreen{TScreen, TParam}"/>
internal interface IRoutableScreen : IActivatableViewModel
{
/// <summary>
/// Gets or sets a value indicating whether or not to reuse the child screen instance if the type has not changed.
/// </summary>
/// <remarks>Defaults to <see langword="true"/>.</remarks>
bool RecycleScreen { get; }
object? InternalScreen { get; }
void InternalChangeScreen(object? screen);
Task InternalOnNavigating(NavigationArguments args, CancellationToken cancellationToken);
Task InternalOnClosing(NavigationArguments args);
}
@@ -0,0 +1,79 @@
using System.Threading;
using System.Threading.Tasks;
namespace Artemis.UI.Shared.Routing;
/// <summary>
/// Represents a view model to which routing can take place and which in turn can host another view model.
/// </summary>
/// <typeparam name="TScreen">The type of view model the screen can host.</typeparam>
public abstract class RoutableScreen<TScreen> : ActivatableViewModelBase, IRoutableScreen where TScreen : class
{
private TScreen? _screen;
private bool _recycleScreen = true;
/// <summary>
/// Gets the currently active child screen.
/// </summary>
public TScreen? Screen
{
get => _screen;
private set => RaiseAndSetIfChanged(ref _screen, value);
}
/// <inheritdoc />
public bool RecycleScreen
{
get => _recycleScreen;
protected set => RaiseAndSetIfChanged(ref _recycleScreen, value);
}
/// <summary>
/// Called before navigating to this screen.
/// </summary>
/// <param name="args">Navigation arguments containing information about the navigation action.</param>
public virtual Task BeforeNavigating(NavigationArguments args)
{
return Task.CompletedTask;
}
/// <summary>
/// Called while navigating to this screen.
/// </summary>
/// <param name="args">Navigation arguments containing information about the navigation action.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
public virtual Task OnNavigating(NavigationArguments args, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
/// <summary>
/// Called before navigating away from this screen.
/// </summary>
/// <param name="args">Navigation arguments containing information about the navigation action.</param>
public virtual Task OnClosing(NavigationArguments args)
{
return Task.CompletedTask;
}
#region Overrides of RoutableScreen
object? IRoutableScreen.InternalScreen => Screen;
void IRoutableScreen.InternalChangeScreen(object? screen)
{
Screen = screen as TScreen;
}
async Task IRoutableScreen.InternalOnNavigating(NavigationArguments args, CancellationToken cancellationToken)
{
await OnNavigating(args, cancellationToken);
}
async Task IRoutableScreen.InternalOnClosing(NavigationArguments args)
{
await OnClosing(args);
}
#endregion
}
@@ -0,0 +1,149 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
namespace Artemis.UI.Shared.Routing;
/// <summary>
/// Represents a view model to which routing with parameters can take place and which in turn can host another view
/// model.
/// </summary>
/// <typeparam name="TScreen">The type of view model the screen can host.</typeparam>
/// <typeparam name="TParam">The type of parameters the screen expects. It must have a parameterless constructor.</typeparam>
public abstract class RoutableScreen<TScreen, TParam> : ActivatableViewModelBase, IRoutableScreen where TScreen : class where TParam : new()
{
private bool _recycleScreen = true;
private TScreen? _screen;
/// <summary>
/// Gets the currently active child screen.
/// </summary>
public TScreen? Screen
{
get => _screen;
private set => RaiseAndSetIfChanged(ref _screen, value);
}
/// <summary>
/// Called before navigating to this screen.
/// </summary>
/// <param name="args">Navigation arguments containing information about the navigation action.</param>
public virtual Task BeforeNavigating(NavigationArguments args)
{
return Task.CompletedTask;
}
/// <summary>
/// Called while navigating to this screen.
/// </summary>
/// <param name="parameters">An object containing the parameters of the navigation action.</param>
/// <param name="args">Navigation arguments containing information about the navigation action.</param>
/// <param name="cancellationToken">
/// A cancellation token that can be used by other objects or threads to receive notice of
/// cancellation.
/// </param>
public virtual Task OnNavigating(TParam parameters, NavigationArguments args, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
/// <summary>
/// Called before navigating away from this screen.
/// </summary>
/// <param name="args">Navigation arguments containing information about the navigation action.</param>
public virtual Task OnClosing(NavigationArguments args)
{
return Task.CompletedTask;
}
/// <inheritdoc />
public bool RecycleScreen
{
get => _recycleScreen;
protected set => RaiseAndSetIfChanged(ref _recycleScreen, value);
}
#region Overrides of RoutableScreen
object? IRoutableScreen.InternalScreen => Screen;
void IRoutableScreen.InternalChangeScreen(object? screen)
{
Screen = screen as TScreen;
}
async Task IRoutableScreen.InternalOnNavigating(NavigationArguments args, CancellationToken cancellationToken)
{
Func<object[], TParam> activator = GetParameterActivator();
if (args.SegmentParameters.Length != _parameterPropertyCount)
throw new ArtemisRoutingException($"Did not retrieve the required amount of parameters, expects {_parameterPropertyCount}, got {args.SegmentParameters.Length}.");
TParam parameters = activator(args.SegmentParameters);
await OnNavigating(parameters, args, cancellationToken);
}
async Task IRoutableScreen.InternalOnClosing(NavigationArguments args)
{
await OnClosing(args);
}
#endregion
#region Parameter generation
// ReSharper disable once StaticMemberInGenericType - That's intentional, each kind of TParam should have its own property count
private static int _parameterPropertyCount;
private static Func<object[], TParam>? _parameterActivator;
private static Func<object[], TParam> GetParameterActivator()
{
if (_parameterActivator != null)
return _parameterActivator;
// Generates a lambda that creates a new instance of TParam
// - Each property of TParam with a public setter must be set using the source object[]
// - Use the index of each property as the index on the source array
// - Cast the object of the source array to the correct type for that property
Type parameterType = typeof(TParam);
ParameterExpression sourceExpression = Expression.Parameter(typeof(object[]), "source");
ParameterExpression parameterExpression = Expression.Parameter(parameterType, "parameters");
List<BinaryExpression> propertyAssignments = parameterType.GetProperties()
.Where(p => p.CanWrite)
.Select((property, index) =>
{
UnaryExpression sourceValueExpression = Expression.Convert(
Expression.ArrayIndex(sourceExpression, Expression.Constant(index)),
property.PropertyType
);
BinaryExpression propertyAssignment = Expression.Assign(
Expression.Property(parameterExpression, property),
sourceValueExpression
);
return propertyAssignment;
})
.ToList();
Expression<Func<object[], TParam>> lambda = Expression.Lambda<Func<object[], TParam>>(
Expression.Block(
new[] {parameterExpression},
Expression.Assign(parameterExpression, Expression.New(parameterType)),
Expression.Block(propertyAssignments),
parameterExpression
),
sourceExpression
);
_parameterActivator = lambda.Compile();
_parameterPropertyCount = propertyAssignments.Count;
return _parameterActivator;
}
#endregion
}
@@ -0,0 +1,18 @@
using System;
namespace Artemis.UI.Shared.Routing.ParameterParsers;
internal class GuidParameterParser : IRouteParameterParser
{
/// <inheritdoc />
public bool IsMatch(RouteSegment segment, string source)
{
return Guid.TryParse(source, out _);
}
/// <inheritdoc />
public object GetValue(RouteSegment segment, string source)
{
return Guid.Parse(source);
}
}
@@ -0,0 +1,7 @@
namespace Artemis.UI.Shared.Routing.ParameterParsers;
public interface IRouteParameterParser
{
bool IsMatch(RouteSegment segment, string source);
object GetValue(RouteSegment segment, string source);
}
@@ -0,0 +1,16 @@
namespace Artemis.UI.Shared.Routing.ParameterParsers;
internal class StringParameterParser : IRouteParameterParser
{
/// <inheritdoc />
public bool IsMatch(RouteSegment segment, string source)
{
return true;
}
/// <inheritdoc />
public object GetValue(RouteSegment segment, string source)
{
return source;
}
}
@@ -0,0 +1,22 @@
using System.Collections.Generic;
using System.Linq;
namespace Artemis.UI.Shared.Routing;
public class Route
{
public Route(string path)
{
Path = path;
Segments = path.Split('/').Select(s => new RouteSegment(s)).ToList();
}
public string Path { get; }
public List<RouteSegment> Segments { get; }
/// <inheritdoc />
public override string ToString()
{
return Path;
}
}
@@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Artemis.UI.Shared.Routing;
public class RouteRegistration<TViewModel> : IRouterRegistration where TViewModel : ViewModelBase
{
public RouteRegistration(string path)
{
Route = new Route(path);
}
/// <inheritdoc />
public override string ToString()
{
return $"{nameof(Route)}: {Route}, {nameof(ViewModel)}: {ViewModel}";
}
public Route Route { get; }
/// <inheritdoc />
public Type ViewModel => typeof(TViewModel);
/// <inheritdoc />
public List<IRouterRegistration> Children { get; set; } = new();
}
public interface IRouterRegistration
{
Route Route { get; }
Type ViewModel { get; }
List<IRouterRegistration> Children { get; set; }
}
@@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using DryIoc;
namespace Artemis.UI.Shared.Routing;
internal class RouteResolution
{
private RouteResolution(string path)
{
Path = path;
}
public string Path { get; }
public bool Success { get; private set; }
public Type? ViewModel { get; private set; }
public object[]? Parameters { get; private set; }
public RouteResolution? Child { get; private set; }
public static RouteResolution Resolve(IRouterRegistration registration, string path)
{
List<string> segments = path.Split('/').ToList();
if (registration.Route.Segments.Count > segments.Count)
return AsFailure(path);
// Ensure self is a match
List<object> parameters = new();
int currentSegment = 0;
foreach (RouteSegment routeSegment in registration.Route.Segments)
{
string segment = segments[currentSegment];
if (!routeSegment.IsMatch(segment))
return AsFailure(path);
object? parameter = routeSegment.GetParameter(segment);
if (parameter != null)
parameters.Add(parameter);
currentSegment++;
}
if (currentSegment == segments.Count)
return AsSuccess(registration.ViewModel, path, parameters.ToArray());
// If segments remain, a child should match it
string childPath = string.Join('/', segments.Skip(currentSegment));
foreach (IRouterRegistration routerRegistration in registration.Children)
{
RouteResolution result = Resolve(routerRegistration, childPath);
if (result.Success)
return AsSuccess(registration.ViewModel, path, parameters.ToArray(), result);
}
return AsFailure(path);
}
public static RouteResolution AsFailure(string path)
{
return new RouteResolution(path);
}
public static RouteResolution AsSuccess(Type viewModel, string path, object[] parameters, RouteResolution? child = null)
{
if (child != null && !child.Success)
throw new ArtemisRoutingException("Cannot create a success route resolution with a failed child");
return new RouteResolution(path)
{
Success = true,
ViewModel = viewModel,
Parameters = parameters,
Child = child
};
}
public object GetViewModel(IContainer container)
{
if (ViewModel == null)
throw new ArtemisRoutingException("Cannot get a view model of a non-success route resolution");
object? viewModel = container.Resolve(ViewModel);
if (viewModel == null)
throw new ArtemisRoutingException($"Could not resolve view model of type {ViewModel}");
return viewModel;
}
public T GetViewModel<T>(IContainer container)
{
if (ViewModel == null)
throw new ArtemisRoutingException("Cannot get a view model of a non-success route resolution");
object? viewModel = container.Resolve(ViewModel);
if (viewModel == null)
throw new ArtemisRoutingException($"Could not resolve view model of type {ViewModel}");
if (viewModel is not T typedViewModel)
throw new ArtemisRoutingException($"View model of type {ViewModel} is does mot implement {typeof(T)}.");
return typedViewModel;
}
public object[] GetAllParameters()
{
List<object> result = new();
if (Parameters != null)
result.AddRange(Parameters);
object[]? childParameters = Child?.GetAllParameters();
if (childParameters != null)
result.AddRange(childParameters);
return result.ToArray();
}
}
@@ -0,0 +1,60 @@
using System;
using System.Text.RegularExpressions;
using Artemis.UI.Shared.Routing.ParameterParsers;
namespace Artemis.UI.Shared.Routing;
public partial class RouteSegment
{
private readonly IRouteParameterParser? _parameterParser;
public RouteSegment(string segment)
{
Segment = segment;
Match match = ParameterRegex().Match(segment);
if (match.Success)
{
Parameter = match.Groups[1].Value;
ParameterType = match.Groups[2].Value;
_parameterParser = GetParameterParser(ParameterType);
}
}
public string Segment { get; }
public string? Parameter { get; }
public string? ParameterType { get; }
public bool IsMatch(string value)
{
if (_parameterParser == null)
return Segment.Equals(value, StringComparison.InvariantCultureIgnoreCase);
return _parameterParser.IsMatch(this, value);
}
public object? GetParameter(string value)
{
if (_parameterParser == null)
return null;
return _parameterParser.GetValue(this, value);
}
/// <inheritdoc />
public override string ToString()
{
return $"{Segment} (param: {Parameter ?? "none"}, type: {ParameterType ?? "N/A"})";
}
private IRouteParameterParser GetParameterParser(string parameterType)
{
if (parameterType == "guid")
return new GuidParameterParser();
// Default to a string parser which just returns the segment as is
return new StringParameterParser();
}
[GeneratedRegex(@"\{(\w+):(\w+)\}")]
private static partial Regex ParameterRegex();
}
@@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Artemis.UI.Shared.Routing;
/// <summary>
/// Represents a router that can be used to navigate to different screens.
/// </summary>
public interface IRouter
{
/// <summary>
/// Gets an observable containing the current path.
/// </summary>
IObservable<string?> CurrentPath { get; }
/// <summary>
/// Gets a list of router registrations, you can use this to register new routes.
/// </summary>
List<IRouterRegistration> Routes { get; }
/// <summary>
/// Asynchronously navigates to the provided path.
/// </summary>
/// <remarks>Navigating cancels any currently processing navigations.</remarks>
/// <param name="path">The path to navigate to.</param>
/// <param name="options">Optional navigation options used to control navigation behaviour.</param>
/// <returns>A task representing the operation</returns>
Task Navigate(string path, RouterNavigationOptions? options = null);
/// <summary>
/// Asynchronously navigates back to the previous active route.
/// </summary>
/// <returns>A task containing a boolean value which indicates whether there was a previous path to go back to.</returns>
Task<bool> GoBack();
/// <summary>
/// Asynchronously navigates forward to the previous active route.
/// </summary>
/// <returns>A task containing a boolean value which indicates whether there was a forward path to go back to.</returns>
Task<bool> GoForward();
/// <summary>
/// Clears the navigation history.
/// </summary>
void ClearHistory();
/// <summary>
/// Sets the root screen from which navigation takes place.
/// </summary>
/// <param name="root">The root screen to set.</param>
/// <typeparam name="TScreen">The type of the root screen. It must be a class.</typeparam>
void SetRoot<TScreen>(RoutableScreen<TScreen> root) where TScreen : class;
/// <summary>
/// Sets the root screen from which navigation takes place.
/// </summary>
/// <param name="root">The root screen to set.</param>
/// <typeparam name="TScreen">The type of the root screen. It must be a class.</typeparam>
/// <typeparam name="TParam">The type of the parameters for the root screen. It must have a parameterless constructor.</typeparam>
void SetRoot<TScreen, TParam>(RoutableScreen<TScreen, TParam> root) where TScreen : class where TParam : new();
}
@@ -0,0 +1,121 @@
using System;
using System.Reactive.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Threading;
using DryIoc;
using ReactiveUI;
using Serilog;
namespace Artemis.UI.Shared.Routing;
internal class Navigation
{
private readonly IContainer _container;
private readonly ILogger _logger;
private readonly IRoutableScreen _root;
private readonly RouteResolution _resolution;
private readonly RouterNavigationOptions _options;
private CancellationTokenSource _cts;
public Navigation(IContainer container, ILogger logger, IRoutableScreen root, RouteResolution resolution, RouterNavigationOptions options)
{
_container = container;
_logger = logger;
_root = root;
_resolution = resolution;
_options = options;
_cts = new CancellationTokenSource();
}
public bool Cancelled => _cts.IsCancellationRequested;
public bool Completed { get; private set; }
public async Task Navigate(NavigationArguments args)
{
_logger.Information("Navigating to {Path}", _resolution.Path);
_cts = new CancellationTokenSource();
await NavigateResolution(_resolution, args, _root);
if (!Cancelled)
_logger.Information("Navigated to {Path}", _resolution.Path);
}
public void Cancel()
{
if (Cancelled || Completed)
return;
_logger.Information("Cancelled navigation to {Path}", _resolution.Path);
_cts.Cancel();
}
private async Task NavigateResolution(RouteResolution resolution, NavigationArguments args, IRoutableScreen host)
{
if (Cancelled)
return;
// Reuse the screen if its type has not changed, if a new one must be created, don't do so on the UI thread
object screen;
if (_options.RecycleScreens && host.RecycleScreen && host.InternalScreen != null && host.InternalScreen.GetType() == resolution.ViewModel)
screen = host.InternalScreen;
else
screen = await Task.Run(() => resolution.GetViewModel(_container));
// If resolution has a child, ensure the screen can host it
if (resolution.Child != null && screen is not IRoutableScreen)
throw new ArtemisRoutingException($"Route resolved with a child but view model of type {resolution.ViewModel} is does mot implement {nameof(IRoutableScreen)}.");
// Only change the screen if it wasn't reused
if (!ReferenceEquals(host.InternalScreen, screen))
host.InternalChangeScreen(screen);
if (CancelIfRequested(args, "ChangeScreen", screen))
return;
// If the screen implements some form of Navigable, activate it
args.SegmentParameters = resolution.Parameters ?? Array.Empty<object>();
if (screen is IRoutableScreen routableScreen)
{
try
{
await routableScreen.InternalOnNavigating(args, _cts.Token);
}
catch (Exception e)
{
Cancel();
_logger.Error(e, "Failed to navigate to {Path}", resolution.Path);
}
if (CancelIfRequested(args, "OnNavigating", screen))
return;
}
if (resolution.Child != null && screen is IRoutableScreen childScreen)
await NavigateResolution(resolution.Child, args, childScreen);
Completed = true;
}
public bool PathEquals(string path, bool allowPartialMatch)
{
if (allowPartialMatch)
return _resolution.Path.StartsWith(path, StringComparison.InvariantCultureIgnoreCase);
return string.Equals(_resolution.Path, path, StringComparison.InvariantCultureIgnoreCase);
}
private bool CancelIfRequested(NavigationArguments args, string stage, object screen)
{
if (Cancelled)
return true;
if (!args.Cancelled)
return false;
_logger.Debug("Navigation to {Path} during {Stage} by {Screen}", args.Path, stage, screen.GetType().Name);
Cancel();
return true;
}
}
@@ -0,0 +1,49 @@
using System;
using System.Threading.Tasks;
namespace Artemis.UI.Shared.Routing;
/// <summary>
/// Represents an object that contains information about the current navigation action.
/// </summary>
public class NavigationArguments
{
internal NavigationArguments(IRouter router, string path, object[] routeParameters)
{
Router = router;
Path = path;
RouteParameters = routeParameters;
SegmentParameters = Array.Empty<object>();
}
/// <summary>
/// Gets the router in which the navigation is taking place.
/// </summary>
public IRouter Router { get; }
/// <summary>
/// Gets the path of the route that is being navigated to.
/// </summary>
public string Path { get; }
/// <summary>
/// GEts an array of all parameters provided to this route.
/// </summary>
public object[] RouteParameters { get; }
/// <summary>
/// Gets an array of parameters provided to this screen's segment of the route.
/// </summary>
public object[] SegmentParameters { get; internal set; }
internal bool Cancelled { get; private set; }
/// <summary>
/// Cancels further processing of the current navigation.
/// </summary>
/// <remarks>It not necessary to cancel the navigation in order to navigate to another route, the current navigation will be cancelled by the router.</remarks>
public void Cancel()
{
Cancelled = true;
}
}
@@ -0,0 +1,167 @@
using System;
using System.Collections.Generic;
using System.Reactive.Subjects;
using System.Threading.Tasks;
using Artemis.Core;
using Avalonia.Threading;
using Serilog;
namespace Artemis.UI.Shared.Routing;
internal class Router : CorePropertyChanged, IRouter
{
private readonly Stack<string> _backStack = new();
private readonly BehaviorSubject<string?> _currentRouteSubject;
private readonly Stack<string> _forwardStack = new();
private readonly Func<IRoutableScreen, RouteResolution, RouterNavigationOptions, Navigation> _getNavigation;
private readonly ILogger _logger;
private Navigation? _currentNavigation;
private IRoutableScreen? _root;
public Router(ILogger logger, Func<IRoutableScreen, RouteResolution, RouterNavigationOptions, Navigation> getNavigation)
{
_logger = logger;
_getNavigation = getNavigation;
_currentRouteSubject = new BehaviorSubject<string?>(null);
}
private RouteResolution Resolve(string path)
{
foreach (IRouterRegistration routerRegistration in Routes)
{
RouteResolution result = RouteResolution.Resolve(routerRegistration, path);
if (result.Success)
return result;
}
return RouteResolution.AsFailure(path);
}
private async Task<bool> RequestClose(object screen, NavigationArguments args)
{
if (screen is not IRoutableScreen routableScreen)
return true;
await routableScreen.InternalOnClosing(args);
if (args.Cancelled)
{
_logger.Debug("Navigation to {Path} cancelled during RequestClose by {Screen}", args.Path, screen.GetType().Name);
return false;
}
if (routableScreen.InternalScreen == null)
return true;
return await RequestClose(routableScreen.InternalScreen, args);
}
private bool PathEquals(string path, bool allowPartialMatch)
{
if (allowPartialMatch)
return _currentRouteSubject.Value != null && _currentRouteSubject.Value.StartsWith(path, StringComparison.InvariantCultureIgnoreCase);
return string.Equals(_currentRouteSubject.Value, path, StringComparison.InvariantCultureIgnoreCase);
}
/// <inheritdoc />
public IObservable<string?> CurrentPath => _currentRouteSubject;
/// <inheritdoc />
public List<IRouterRegistration> Routes { get; } = new();
/// <inheritdoc />
public async Task Navigate(string path, RouterNavigationOptions? options = null)
{
options ??= new RouterNavigationOptions();
// Routing takes place on the UI thread with processing heavy tasks offloaded by the router itself
await Dispatcher.UIThread.InvokeAsync(() => InternalNavigate(path, options));
}
private async Task InternalNavigate(string path, RouterNavigationOptions options)
{
if (_root == null)
throw new ArtemisRoutingException("Cannot navigate without a root having been set");
if (PathEquals(path, options.IgnoreOnPartialMatch) || (_currentNavigation != null && _currentNavigation.PathEquals(path, options.IgnoreOnPartialMatch)))
return;
string? previousPath = _currentRouteSubject.Value;
RouteResolution resolution = Resolve(path);
if (!resolution.Success)
{
_logger.Warning("Failed to resolve path {Path}", path);
return;
}
NavigationArguments args = new(this, resolution.Path, resolution.GetAllParameters());
if (!await RequestClose(_root, args))
return;
Navigation navigation = _getNavigation(_root, resolution, options);
_currentNavigation?.Cancel();
_currentNavigation = navigation;
// Execute the navigation
await navigation.Navigate(args);
// If it was cancelled before completion, don't add it to history or update the current path
if (navigation.Cancelled)
return;
if (options.AddToHistory && previousPath != null)
{
_backStack.Push(previousPath);
_forwardStack.Clear();
}
_currentRouteSubject.OnNext(path);
}
/// <inheritdoc />
public async Task<bool> GoBack()
{
if (!_backStack.TryPop(out string? path))
return false;
string? previousPath = _currentRouteSubject.Value;
await Navigate(path, new RouterNavigationOptions {AddToHistory = false});
if (previousPath != null)
_forwardStack.Push(previousPath);
return true;
}
/// <inheritdoc />
public async Task<bool> GoForward()
{
if (!_forwardStack.TryPop(out string? path))
return false;
string? previousPath = _currentRouteSubject.Value;
await Navigate(path, new RouterNavigationOptions {AddToHistory = false});
if (previousPath != null)
_backStack.Push(previousPath);
return true;
}
/// <inheritdoc />
public void ClearHistory()
{
_backStack.Clear();
_forwardStack.Clear();
}
/// <inheritdoc />
public void SetRoot<TScreen>(RoutableScreen<TScreen> root) where TScreen : class
{
_root = root;
}
/// <inheritdoc />
public void SetRoot<TScreen, TParam>(RoutableScreen<TScreen, TParam> root) where TScreen : class where TParam : new()
{
_root = root;
}
}
@@ -0,0 +1,23 @@
namespace Artemis.UI.Shared.Routing;
/// <summary>
/// Represents navigation options used to control navigation behaviour.
/// </summary>
public class RouterNavigationOptions
{
/// <summary>
/// Gets or sets a boolean indicating whether or not to add the navigation to the history.
/// </summary>
public bool AddToHistory { get; set; } = true;
/// <summary>
/// Gets or sets a boolean indicating whether or not to recycle already active screens.
/// </summary>
public bool RecycleScreens { get; set; } = true;
/// <summary>
/// Gets or sets a boolean indicating whether route changes should be ignored if they are a partial match.
/// </summary>
/// <example>If set to true, a route change from <c>page/subpage1/subpage2</c> to <c>page/subpage1</c> will be ignored.</example>
public bool IgnoreOnPartialMatch { get; set; } = false;
}