using System; using System.Collections.Generic; using System.Reactive.Subjects; using System.Threading.Tasks; using Artemis.Core; using Artemis.UI.Shared.Services.MainWindow; using Avalonia.Threading; using Serilog; namespace Artemis.UI.Shared.Routing; internal class Router : CorePropertyChanged, IRouter, IDisposable { private readonly Stack _backStack = new(); private readonly BehaviorSubject _currentRouteSubject; private readonly Stack _forwardStack = new(); private readonly Func _getNavigation; private readonly ILogger _logger; private readonly IMainWindowService _mainWindowService; private Navigation? _currentNavigation; private IRoutableScreen? _root; private string? _previousWindowRoute; public Router(ILogger logger, IMainWindowService mainWindowService, Func getNavigation) { _logger = logger; _mainWindowService = mainWindowService; _getNavigation = getNavigation; _currentRouteSubject = new BehaviorSubject(null); mainWindowService.MainWindowOpened += MainWindowServiceOnMainWindowOpened; mainWindowService.MainWindowClosed += MainWindowServiceOnMainWindowClosed; } 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 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); } /// public IObservable CurrentPath => _currentRouteSubject; /// public List Routes { get; } = new(); /// 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); } /// public async Task 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; } /// public async Task 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; } /// public void ClearHistory() { _backStack.Clear(); _forwardStack.Clear(); } /// public void SetRoot(RoutableScreen root) where TScreen : class { _root = root; } /// public void SetRoot(RoutableScreen root) where TScreen : class where TParam : new() { _root = root; } /// public void ClearPreviousWindowRoute() { _previousWindowRoute = null; } public void Dispose() { _currentRouteSubject.Dispose(); _mainWindowService.MainWindowOpened -= MainWindowServiceOnMainWindowOpened; _mainWindowService.MainWindowClosed -= MainWindowServiceOnMainWindowClosed; } private void MainWindowServiceOnMainWindowOpened(object? sender, EventArgs e) { if (_previousWindowRoute != null && _currentRouteSubject.Value == "blank") Dispatcher.UIThread.InvokeAsync(async () => await Navigate(_previousWindowRoute, new RouterNavigationOptions {AddToHistory = false, EnableLogging = false})); } private void MainWindowServiceOnMainWindowClosed(object? sender, EventArgs e) { _previousWindowRoute = _currentRouteSubject.Value; Dispatcher.UIThread.InvokeAsync(async () => await Navigate("blank", new RouterNavigationOptions {AddToHistory = false, EnableLogging = false})); } }