1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

Router - Require routable VMs to implement new RoutableScreen

This commit is contained in:
Robert 2023-09-02 15:52:17 +02:00
parent 318ec99ad4
commit c132edeb51
43 changed files with 284 additions and 287 deletions

View File

@ -0,0 +1,13 @@
namespace Artemis.UI.Shared.Routing;
/// <summary>
/// For internal use.
/// </summary>
/// <seealso cref="RoutableHostScreen{TScreen}" />
/// <seealso cref="RoutableHostScreen{TScreen,TParam}" />
internal interface IRoutableHostScreen : IRoutableScreen
{
bool RecycleScreen { get; }
IRoutableScreen? InternalScreen { get; }
void InternalChangeScreen(IRoutableScreen? screen);
}

View File

@ -0,0 +1,14 @@
using System.Threading;
using System.Threading.Tasks;
using ReactiveUI;
namespace Artemis.UI.Shared.Routing;
/// <summary>
/// For internal use.
/// </summary>
internal interface IRoutableScreen : IActivatableViewModel
{
Task InternalOnNavigating(NavigationArguments args, CancellationToken cancellationToken);
Task InternalOnClosing(NavigationArguments args);
}

View File

@ -0,0 +1,42 @@
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 RoutableHostScreen<TScreen> : RoutableScreen, IRoutableHostScreen where TScreen : RoutableScreen
{
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);
}
/// <inheritdoc />
public bool RecycleScreen
{
get => _recycleScreen;
protected set => RaiseAndSetIfChanged(ref _recycleScreen, value);
}
IRoutableScreen? IRoutableHostScreen.InternalScreen => Screen;
void IRoutableHostScreen.InternalChangeScreen(IRoutableScreen? screen)
{
if (screen == null)
{
Screen = null;
return;
}
if (screen is not TScreen typedScreen)
throw new ArtemisRoutingException($"Screen cannot be hosted, {screen.GetType().Name} is not assignable to {typeof(TScreen).Name}.");
Screen = typedScreen;
}
}

View File

@ -0,0 +1,44 @@
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 RoutableHostScreen<TScreen, TParam> : RoutableScreen<TParam>, IRoutableHostScreen where TScreen : RoutableScreen 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);
}
/// <inheritdoc />
public bool RecycleScreen
{
get => _recycleScreen;
protected set => RaiseAndSetIfChanged(ref _recycleScreen, value);
}
IRoutableScreen? IRoutableHostScreen.InternalScreen => Screen;
void IRoutableHostScreen.InternalChangeScreen(IRoutableScreen? screen)
{
if (screen == null)
{
Screen = null;
return;
}
if (screen is not TScreen typedScreen)
throw new ArtemisRoutingException($"Screen cannot be hosted, {screen.GetType().Name} is not assignable to {typeof(TScreen).Name}.");
Screen = typedScreen;
}
}

View File

@ -1,24 +1,55 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ReactiveUI;
namespace Artemis.UI.Shared.Routing; namespace Artemis.UI.Shared.Routing;
/// <summary> /// <summary>
/// For internal use. /// Represents a view model to which routing can take place.
/// </summary> /// </summary>
/// <seealso cref="RoutableScreen{TScreen}"/> public abstract class RoutableScreen : ActivatableViewModelBase, IRoutableScreen
/// <seealso cref="RoutableScreen{TScreen, TParam}"/>
internal interface IRoutableScreen : IActivatableViewModel
{ {
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not to reuse the child screen instance if the type has not changed. /// Called before navigating to this screen.
/// </summary> /// </summary>
/// <remarks>Defaults to <see langword="true"/>.</remarks> /// <param name="args">Navigation arguments containing information about the navigation action.</param>
bool RecycleScreen { get; } public virtual Task BeforeNavigating(NavigationArguments args)
{
return Task.CompletedTask;
}
object? InternalScreen { get; } /// <summary>
void InternalChangeScreen(object? screen); /// Called while navigating to this screen.
Task InternalOnNavigating(NavigationArguments args, CancellationToken cancellationToken); /// </summary>
Task InternalOnClosing(NavigationArguments args); /// <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
async Task IRoutableScreen.InternalOnNavigating(NavigationArguments args, CancellationToken cancellationToken)
{
await OnNavigating(args, cancellationToken);
}
async Task IRoutableScreen.InternalOnClosing(NavigationArguments args)
{
await OnClosing(args);
}
#endregion
} }

View File

@ -4,39 +4,15 @@ using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Platform;
namespace Artemis.UI.Shared.Routing; namespace Artemis.UI.Shared.Routing;
/// <summary> /// <summary>
/// Represents a view model to which routing with parameters can take place and which in turn can host another view /// Represents a view model to which routing with parameters can take place.
/// model.
/// </summary> /// </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> /// <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() public abstract class RoutableScreen<TParam> : RoutableScreen, IRoutableScreen 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> /// <summary>
/// Called while navigating to this screen. /// Called while navigating to this screen.
/// </summary> /// </summary>
@ -50,40 +26,7 @@ public abstract class RoutableScreen<TScreen, TParam> : ActivatableViewModelBase
{ {
return Task.CompletedTask; 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)
{
if (screen == null)
{
Screen = null;
return;
}
if (screen is not TScreen typedScreen)
throw new ArtemisRoutingException($"Provided screen is not assignable to {typeof(TScreen).FullName}");
Screen = typedScreen;
}
async Task IRoutableScreen.InternalOnNavigating(NavigationArguments args, CancellationToken cancellationToken) async Task IRoutableScreen.InternalOnNavigating(NavigationArguments args, CancellationToken cancellationToken)
{ {
Func<object[], TParam> activator = GetParameterActivator(); Func<object[], TParam> activator = GetParameterActivator();
@ -92,6 +35,7 @@ public abstract class RoutableScreen<TScreen, TParam> : ActivatableViewModelBase
throw new ArtemisRoutingException($"Did not retrieve the required amount of parameters, expects {_parameterPropertyCount}, got {args.SegmentParameters.Length}."); throw new ArtemisRoutingException($"Did not retrieve the required amount of parameters, expects {_parameterPropertyCount}, got {args.SegmentParameters.Length}.");
TParam parameters = activator(args.SegmentParameters); TParam parameters = activator(args.SegmentParameters);
await OnNavigating(args, cancellationToken);
await OnNavigating(parameters, args, cancellationToken); await OnNavigating(parameters, args, cancellationToken);
} }
@ -100,8 +44,6 @@ public abstract class RoutableScreen<TScreen, TParam> : ActivatableViewModelBase
await OnClosing(args); await OnClosing(args);
} }
#endregion
#region Parameter generation #region Parameter generation
// ReSharper disable once StaticMemberInGenericType - That's intentional, each kind of TParam should have its own property count // ReSharper disable once StaticMemberInGenericType - That's intentional, each kind of TParam should have its own property count

View File

@ -1,87 +0,0 @@
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)
{
if (screen == null)
{
Screen = null;
return;
}
if (screen is not TScreen typedScreen)
throw new ArtemisRoutingException($"Screen cannot be hosted, {screen.GetType().Name} is not assignable to {typeof(TScreen).Name}.");
Screen = typedScreen;
}
async Task IRoutableScreen.InternalOnNavigating(NavigationArguments args, CancellationToken cancellationToken)
{
await OnNavigating(args, cancellationToken);
}
async Task IRoutableScreen.InternalOnClosing(NavigationArguments args)
{
await OnClosing(args);
}
#endregion
}

View File

@ -7,7 +7,7 @@ namespace Artemis.UI.Shared.Routing;
/// Represents a registration for a route and its associated view model. /// Represents a registration for a route and its associated view model.
/// </summary> /// </summary>
/// <typeparam name="TViewModel">The type of the view model associated with the route.</typeparam> /// <typeparam name="TViewModel">The type of the view model associated with the route.</typeparam>
public class RouteRegistration<TViewModel> : IRouterRegistration where TViewModel : ViewModelBase public class RouteRegistration<TViewModel> : IRouterRegistration where TViewModel : RoutableScreen
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="RouteRegistration{TViewModel}" /> class. /// Initializes a new instance of the <see cref="RouteRegistration{TViewModel}" /> class.

View File

@ -74,19 +74,12 @@ internal class RouteResolution
}; };
} }
public object GetViewModel(IContainer container) public RoutableScreen GetViewModel(IContainer container)
{ {
if (ViewModel == null) return GetViewModel<RoutableScreen>(container);
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) public T GetViewModel<T>(IContainer container) where T : RoutableScreen
{ {
if (ViewModel == null) if (ViewModel == null)
throw new ArtemisRoutingException("Cannot get a view model of a non-success route resolution"); throw new ArtemisRoutingException("Cannot get a view model of a non-success route resolution");

View File

@ -50,7 +50,7 @@ public interface IRouter
/// </summary> /// </summary>
/// <param name="root">The root screen to set.</param> /// <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="TScreen">The type of the root screen. It must be a class.</typeparam>
void SetRoot<TScreen>(RoutableScreen<TScreen> root) where TScreen : class; void SetRoot<TScreen>(RoutableHostScreen<TScreen> root) where TScreen : RoutableScreen;
/// <summary> /// <summary>
/// Sets the root screen from which navigation takes place. /// Sets the root screen from which navigation takes place.
@ -58,7 +58,7 @@ public interface IRouter
/// <param name="root">The root screen to set.</param> /// <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="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> /// <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(); void SetRoot<TScreen, TParam>(RoutableHostScreen<TScreen, TParam> root) where TScreen : RoutableScreen where TParam : new();
/// <summary> /// <summary>
/// Clears the route used by the previous window, so that it is not restored when the main window opens. /// Clears the route used by the previous window, so that it is not restored when the main window opens.

View File

@ -14,12 +14,12 @@ internal class Navigation
private readonly IContainer _container; private readonly IContainer _container;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IRoutableScreen _root; private readonly IRoutableHostScreen _root;
private readonly RouteResolution _resolution; private readonly RouteResolution _resolution;
private readonly RouterNavigationOptions _options; private readonly RouterNavigationOptions _options;
private CancellationTokenSource _cts; private CancellationTokenSource _cts;
public Navigation(IContainer container, ILogger logger, IRoutableScreen root, RouteResolution resolution, RouterNavigationOptions options) public Navigation(IContainer container, ILogger logger, IRoutableHostScreen root, RouteResolution resolution, RouterNavigationOptions options)
{ {
_container = container; _container = container;
_logger = logger; _logger = logger;
@ -54,21 +54,21 @@ internal class Navigation
_cts.Cancel(); _cts.Cancel();
} }
private async Task NavigateResolution(RouteResolution resolution, NavigationArguments args, IRoutableScreen host) private async Task NavigateResolution(RouteResolution resolution, NavigationArguments args, IRoutableHostScreen host)
{ {
if (Cancelled) if (Cancelled)
return; 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 // 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; IRoutableScreen screen;
if (_options.RecycleScreens && host.RecycleScreen && host.InternalScreen != null && host.InternalScreen.GetType() == resolution.ViewModel) if (_options.RecycleScreens && host.RecycleScreen && host.InternalScreen != null && host.InternalScreen.GetType() == resolution.ViewModel)
screen = host.InternalScreen; screen = host.InternalScreen;
else else
screen = await Task.Run(() => resolution.GetViewModel(_container)); screen = await Task.Run(() => resolution.GetViewModel(_container));
// If resolution has a child, ensure the screen can host it // If resolution has a child, ensure the screen can host it
if (resolution.Child != null && screen is not IRoutableScreen) if (resolution.Child != null && screen is not IRoutableHostScreen)
throw new ArtemisRoutingException($"Route resolved with a child but view model of type {resolution.ViewModel} is does mot implement {nameof(IRoutableScreen)}."); throw new ArtemisRoutingException($"Route resolved with a child but view model of type {resolution.ViewModel} is does mot implement {nameof(IRoutableHostScreen)}.");
// Only change the screen if it wasn't reused // Only change the screen if it wasn't reused
if (!ReferenceEquals(host.InternalScreen, screen)) if (!ReferenceEquals(host.InternalScreen, screen))
@ -87,27 +87,24 @@ internal class Navigation
if (CancelIfRequested(args, "ChangeScreen", screen)) if (CancelIfRequested(args, "ChangeScreen", screen))
return; return;
// If the screen implements some form of Navigable, activate it // Navigate on the screen
args.SegmentParameters = resolution.Parameters ?? Array.Empty<object>(); args.SegmentParameters = resolution.Parameters ?? Array.Empty<object>();
if (screen is IRoutableScreen routableScreen) try
{ {
try await screen.InternalOnNavigating(args, _cts.Token);
{ }
await routableScreen.InternalOnNavigating(args, _cts.Token); catch (Exception e)
} {
catch (Exception e) Cancel();
{ if (e is not TaskCanceledException)
Cancel(); _logger.Error(e, "Failed to navigate to {Path}", resolution.Path);
if (e is not TaskCanceledException)
_logger.Error(e, "Failed to navigate to {Path}", resolution.Path);
}
if (CancelIfRequested(args, "OnNavigating", screen))
return;
} }
if (screen is IRoutableScreen childScreen) if (CancelIfRequested(args, "OnNavigating", screen))
return;
if (screen is IRoutableHostScreen childScreen)
{ {
// Navigate the child too // Navigate the child too
if (resolution.Child != null) if (resolution.Child != null)

View File

@ -14,15 +14,15 @@ internal class Router : CorePropertyChanged, IRouter, IDisposable
private readonly Stack<string> _backStack = new(); private readonly Stack<string> _backStack = new();
private readonly BehaviorSubject<string?> _currentRouteSubject; private readonly BehaviorSubject<string?> _currentRouteSubject;
private readonly Stack<string> _forwardStack = new(); private readonly Stack<string> _forwardStack = new();
private readonly Func<IRoutableScreen, RouteResolution, RouterNavigationOptions, Navigation> _getNavigation; private readonly Func<IRoutableHostScreen, RouteResolution, RouterNavigationOptions, Navigation> _getNavigation;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IMainWindowService _mainWindowService; private readonly IMainWindowService _mainWindowService;
private Navigation? _currentNavigation; private Navigation? _currentNavigation;
private IRoutableScreen? _root; private IRoutableHostScreen? _root;
private string? _previousWindowRoute; private string? _previousWindowRoute;
public Router(ILogger logger, IMainWindowService mainWindowService, Func<IRoutableScreen, RouteResolution, RouterNavigationOptions, Navigation> getNavigation) public Router(ILogger logger, IMainWindowService mainWindowService, Func<IRoutableHostScreen, RouteResolution, RouterNavigationOptions, Navigation> getNavigation)
{ {
_logger = logger; _logger = logger;
_mainWindowService = mainWindowService; _mainWindowService = mainWindowService;
@ -45,21 +45,17 @@ internal class Router : CorePropertyChanged, IRouter, IDisposable
return RouteResolution.AsFailure(path); return RouteResolution.AsFailure(path);
} }
private async Task<bool> RequestClose(object screen, NavigationArguments args) private async Task<bool> RequestClose(IRoutableScreen screen, NavigationArguments args)
{ {
if (screen is not IRoutableScreen routableScreen) // Drill down to child screens first
return true; if (screen is IRoutableHostScreen hostScreen && hostScreen.InternalScreen != null && !await RequestClose(hostScreen.InternalScreen, args))
await routableScreen.InternalOnClosing(args);
if (args.Cancelled)
{
_logger.Debug("Navigation to {Path} cancelled during RequestClose by {Screen}", args.Path, screen.GetType().Name);
return false; return false;
}
if (routableScreen.InternalScreen == null) await screen.InternalOnClosing(args);
if (!args.Cancelled)
return true; return true;
return await RequestClose(routableScreen.InternalScreen, args); _logger.Debug("Navigation to {Path} cancelled during RequestClose by {Screen}", args.Path, screen.GetType().Name);
return false;
} }
private bool PathEquals(string path, bool allowPartialMatch) private bool PathEquals(string path, bool allowPartialMatch)
@ -161,13 +157,13 @@ internal class Router : CorePropertyChanged, IRouter, IDisposable
} }
/// <inheritdoc /> /// <inheritdoc />
public void SetRoot<TScreen>(RoutableScreen<TScreen> root) where TScreen : class public void SetRoot<TScreen>(RoutableHostScreen<TScreen> root) where TScreen : RoutableScreen
{ {
_root = root; _root = root;
} }
/// <inheritdoc /> /// <inheritdoc />
public void SetRoot<TScreen, TParam>(RoutableScreen<TScreen, TParam> root) where TScreen : class where TParam : new() public void SetRoot<TScreen, TParam>(RoutableHostScreen<TScreen, TParam> root) where TScreen : RoutableScreen where TParam : new()
{ {
_root = root; _root = root;
} }

View File

@ -35,8 +35,9 @@ public static class Routes
{ {
Children = new List<IRouterRegistration>() Children = new List<IRouterRegistration>()
{ {
new RouteRegistration<LibraryInstalledViewModel>("installed"), new RouteRegistration<InstalledTabViewModel>("installed"),
new RouteRegistration<LibrarySubmissionsViewModel>("submissions"), new RouteRegistration<SubmissionsTabViewModel>("submissions"),
new RouteRegistration<SubmissionsDetailViewModel>("submissions/{entryId:guid}"),
} }
} }
} }

View File

@ -1,12 +1,13 @@
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.UI.Screens.StartupWizard; using Artemis.UI.Screens.StartupWizard;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
using Avalonia.Threading; using Avalonia.Threading;
namespace Artemis.UI.Screens.Home; namespace Artemis.UI.Screens.Home;
public class HomeViewModel : ViewModelBase, IMainScreenViewModel public class HomeViewModel : RoutableScreen, IMainScreenViewModel
{ {
public HomeViewModel(ISettingsService settingsService, IWindowService windowService) public HomeViewModel(ISettingsService settingsService, IWindowService windowService)
{ {

View File

@ -17,14 +17,13 @@ using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services.MainWindow; using Artemis.UI.Shared.Services.MainWindow;
using Artemis.UI.Shared.Services.ProfileEditor; using Artemis.UI.Shared.Services.ProfileEditor;
using Avalonia.Threading;
using DynamicData; using DynamicData;
using DynamicData.Binding; using DynamicData.Binding;
using ReactiveUI; using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor; namespace Artemis.UI.Screens.ProfileEditor;
public class ProfileEditorViewModel : RoutableScreen<object, ProfileEditorViewModelParameters>, IMainScreenViewModel public class ProfileEditorViewModel : RoutableScreen<ProfileEditorViewModelParameters>, IMainScreenViewModel
{ {
private readonly IProfileEditorService _profileEditorService; private readonly IProfileEditorService _profileEditorService;
private readonly IProfileService _profileService; private readonly IProfileService _profileService;

View File

@ -1,8 +1,9 @@
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
namespace Artemis.UI.Screens.Root; namespace Artemis.UI.Screens.Root;
public class BlankViewModel : ViewModelBase, IMainScreenViewModel public class BlankViewModel : RoutableScreen, IMainScreenViewModel
{ {
/// <inheritdoc /> /// <inheritdoc />
public ViewModelBase? TitleBarViewModel => null; public ViewModelBase? TitleBarViewModel => null;

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using Artemis.UI.Shared.Routing;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using Avalonia.Threading; using Avalonia.Threading;
using ReactiveUI; using ReactiveUI;
@ -14,7 +15,7 @@ public partial class RootView : ReactiveUserControl<RootViewModel>
this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen).Subscribe(Navigate).DisposeWith(d)); this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen).Subscribe(Navigate).DisposeWith(d));
} }
private void Navigate(IMainScreenViewModel viewModel) private void Navigate(RoutableScreen viewModel)
{ {
try try
{ {

View File

@ -18,7 +18,7 @@ using ReactiveUI;
namespace Artemis.UI.Screens.Root; namespace Artemis.UI.Screens.Root;
public class RootViewModel : RoutableScreen<IMainScreenViewModel>, IMainWindowProvider public class RootViewModel : RoutableHostScreen<RoutableScreen>, IMainWindowProvider
{ {
private readonly ICoreService _coreService; private readonly ICoreService _coreService;
private readonly IDebugService _debugService; private readonly IDebugService _debugService;
@ -100,12 +100,10 @@ public class RootViewModel : RoutableScreen<IMainScreenViewModel>, IMainWindowPr
_router.GoForward(); _router.GoForward();
} }
private void UpdateTitleBarViewModel(IMainScreenViewModel? viewModel) private void UpdateTitleBarViewModel(RoutableScreen? viewModel)
{ {
if (viewModel?.TitleBarViewModel != null) IMainScreenViewModel? mainScreenViewModel = viewModel as IMainScreenViewModel;
TitleBarViewModel = viewModel.TitleBarViewModel; TitleBarViewModel = mainScreenViewModel?.TitleBarViewModel ?? _defaultTitleBarViewModel;
else
TitleBarViewModel = _defaultTitleBarViewModel;
} }
private void CurrentMainWindowOnClosing(object? sender, EventArgs e) private void CurrentMainWindowOnClosing(object? sender, EventArgs e)

View File

@ -11,7 +11,7 @@ using ReactiveUI;
namespace Artemis.UI.Screens.Settings; namespace Artemis.UI.Screens.Settings;
public class SettingsViewModel : RoutableScreen<ActivatableViewModelBase>, IMainScreenViewModel public class SettingsViewModel : RoutableHostScreen<RoutableScreen>, IMainScreenViewModel
{ {
private readonly IRouter _router; private readonly IRouter _router;
private RouteViewModel? _selectedTab; private RouteViewModel? _selectedTab;

View File

@ -1,9 +1,9 @@
using Artemis.Core; using Artemis.Core;
using Artemis.UI.Shared; using Artemis.UI.Shared.Routing;
namespace Artemis.UI.Screens.Settings; namespace Artemis.UI.Screens.Settings;
public class AboutTabViewModel : ActivatableViewModelBase public class AboutTabViewModel : RoutableScreen
{ {
public AboutTabViewModel() public AboutTabViewModel()
{ {

View File

@ -8,7 +8,7 @@ using Artemis.Core;
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.UI.DryIoc.Factories; using Artemis.UI.DryIoc.Factories;
using Artemis.UI.Screens.Device; using Artemis.UI.Screens.Device;
using Artemis.UI.Shared; using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
using Avalonia.Threading; using Avalonia.Threading;
using DynamicData; using DynamicData;
@ -16,7 +16,7 @@ using ReactiveUI;
namespace Artemis.UI.Screens.Settings; namespace Artemis.UI.Screens.Settings;
public class DevicesTabViewModel : ActivatableViewModelBase public class DevicesTabViewModel : RoutableScreen
{ {
private readonly IDeviceVmFactory _deviceVmFactory; private readonly IDeviceVmFactory _deviceVmFactory;
private readonly IRgbService _rgbService; private readonly IRgbService _rgbService;

View File

@ -13,8 +13,8 @@ using Artemis.Core.Services;
using Artemis.UI.Screens.StartupWizard; using Artemis.UI.Screens.StartupWizard;
using Artemis.UI.Services.Interfaces; using Artemis.UI.Services.Interfaces;
using Artemis.UI.Services.Updating; using Artemis.UI.Services.Updating;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Providers; using Artemis.UI.Shared.Providers;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Builders; using Artemis.UI.Shared.Services.Builders;
using Avalonia.Threading; using Avalonia.Threading;
@ -26,7 +26,7 @@ using Serilog.Events;
namespace Artemis.UI.Screens.Settings; namespace Artemis.UI.Screens.Settings;
public class GeneralTabViewModel : ActivatableViewModelBase public class GeneralTabViewModel : RoutableScreen
{ {
private readonly IAutoRunProvider? _autoRunProvider; private readonly IAutoRunProvider? _autoRunProvider;
private readonly IDebugService _debugService; private readonly IDebugService _debugService;

View File

@ -9,18 +9,17 @@ using Artemis.Core;
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.UI.DryIoc.Factories; using Artemis.UI.DryIoc.Factories;
using Artemis.UI.Screens.Plugins; using Artemis.UI.Screens.Plugins;
using Artemis.UI.Shared; using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Builders; using Artemis.UI.Shared.Services.Builders;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using Avalonia.Threading;
using DynamicData; using DynamicData;
using DynamicData.Binding; using DynamicData.Binding;
using ReactiveUI; using ReactiveUI;
namespace Artemis.UI.Screens.Settings; namespace Artemis.UI.Screens.Settings;
public class PluginsTabViewModel : ActivatableViewModelBase public class PluginsTabViewModel : RoutableScreen
{ {
private readonly INotificationService _notificationService; private readonly INotificationService _notificationService;
private readonly IPluginManagementService _pluginManagementService; private readonly IPluginManagementService _pluginManagementService;

View File

@ -22,7 +22,7 @@ using StrawberryShake;
namespace Artemis.UI.Screens.Settings; namespace Artemis.UI.Screens.Settings;
public class ReleasesTabViewModel : RoutableScreen<ReleaseDetailsViewModel> public class ReleasesTabViewModel : RoutableHostScreen<ReleaseDetailsViewModel>
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IUpdateService _updateService; private readonly IUpdateService _updateService;

View File

@ -18,7 +18,7 @@ using StrawberryShake;
namespace Artemis.UI.Screens.Settings.Updating; namespace Artemis.UI.Screens.Settings.Updating;
public class ReleaseDetailsViewModel : RoutableScreen<ViewModelBase, ReleaseDetailsViewModelParameters> public class ReleaseDetailsViewModel : RoutableScreen<ReleaseDetailsViewModelParameters>
{ {
private readonly ObservableAsPropertyHelper<long> _fileSize; private readonly ObservableAsPropertyHelper<long> _fileSize;
private readonly ILogger _logger; private readonly ILogger _logger;

View File

@ -10,6 +10,7 @@ using Artemis.Core.Services;
using Artemis.UI.DryIoc.Factories; using Artemis.UI.DryIoc.Factories;
using Artemis.UI.Extensions; using Artemis.UI.Extensions;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
using Avalonia; using Avalonia;
using ReactiveUI; using ReactiveUI;
@ -17,7 +18,7 @@ using SkiaSharp;
namespace Artemis.UI.Screens.SurfaceEditor; namespace Artemis.UI.Screens.SurfaceEditor;
public class SurfaceEditorViewModel : ActivatableViewModelBase, IMainScreenViewModel public class SurfaceEditorViewModel : RoutableScreen, IMainScreenViewModel
{ {
private readonly IDeviceService _deviceService; private readonly IDeviceService _deviceService;
private readonly IDeviceVmFactory _deviceVmFactory; private readonly IDeviceVmFactory _deviceVmFactory;

View File

@ -18,7 +18,7 @@ using StrawberryShake;
namespace Artemis.UI.Screens.Workshop.Entries; namespace Artemis.UI.Screens.Workshop.Entries;
public abstract class EntryListBaseViewModel : RoutableScreen<ActivatableViewModelBase, WorkshopListParameters>, IWorkshopViewModel public abstract class EntryListBaseViewModel : RoutableScreen<WorkshopListParameters>
{ {
private readonly INotificationService _notificationService; private readonly INotificationService _notificationService;
private readonly IWorkshopClient _workshopClient; private readonly IWorkshopClient _workshopClient;

View File

@ -14,7 +14,7 @@ using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Home; namespace Artemis.UI.Screens.Workshop.Home;
public class WorkshopHomeViewModel : ActivatableViewModelBase, IWorkshopViewModel public class WorkshopHomeViewModel : RoutableScreen
{ {
private readonly IWindowService _windowService; private readonly IWindowService _windowService;
private readonly IWorkshopService _workshopService; private readonly IWorkshopService _workshopService;

View File

@ -10,7 +10,7 @@ using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Home; namespace Artemis.UI.Screens.Workshop.Home;
public class WorkshopOfflineViewModel : RoutableScreen<ActivatableViewModelBase, WorkshopOfflineParameters>, IWorkshopViewModel public class WorkshopOfflineViewModel : RoutableScreen<WorkshopOfflineParameters>
{ {
private readonly IRouter _router; private readonly IRouter _router;
private readonly IWorkshopService _workshopService; private readonly IWorkshopService _workshopService;

View File

@ -9,7 +9,7 @@ using StrawberryShake;
namespace Artemis.UI.Screens.Workshop.Layout; namespace Artemis.UI.Screens.Workshop.Layout;
public class LayoutDetailsViewModel : RoutableScreen<ActivatableViewModelBase, WorkshopDetailParameters>, IWorkshopViewModel public class LayoutDetailsViewModel : RoutableScreen<WorkshopDetailParameters>
{ {
private readonly IWorkshopClient _client; private readonly IWorkshopClient _client;
private IGetEntryById_Entry? _entry; private IGetEntryById_Entry? _entry;

View File

@ -3,6 +3,6 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Library.Tabs.LibraryInstalledView"> x:Class="Artemis.UI.Screens.Workshop.Library.Tabs.InstalledTabView">
Installed entries management here 🫡 Installed entries management here 🫡
</UserControl> </UserControl>

View File

@ -5,9 +5,9 @@ using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Library.Tabs; namespace Artemis.UI.Screens.Workshop.Library.Tabs;
public partial class LibraryInstalledView : ReactiveUserControl<LibraryInstalledViewModel> public partial class InstalledTabView : ReactiveUserControl<InstalledTabViewModel>
{ {
public LibraryInstalledView() public InstalledTabView()
{ {
InitializeComponent(); InitializeComponent();
} }

View File

@ -1,8 +1,9 @@
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
namespace Artemis.UI.Screens.Workshop.Library.Tabs; namespace Artemis.UI.Screens.Workshop.Library.Tabs;
public class LibraryInstalledViewModel : ActivatableViewModelBase public class InstalledTabViewModel : RoutableScreen
{ {
} }

View File

@ -0,0 +1,8 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Library.Tabs.SubmissionsDetailView">
Welcome to Avalonia!
</UserControl>

View File

@ -0,0 +1,14 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Library.Tabs;
public partial class SubmissionsDetailView : ReactiveUserControl<SubmissionsDetailViewModel>
{
public SubmissionsDetailView()
{
InitializeComponent();
}
}

View File

@ -0,0 +1,16 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Artemis.UI.Screens.Workshop.Parameters;
using Artemis.UI.Shared.Routing;
namespace Artemis.UI.Screens.Workshop.Library.Tabs;
public class SubmissionsDetailViewModel : RoutableScreen<WorkshopDetailParameters>
{
public override Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
{
Console.WriteLine(parameters.EntryId);
return Task.CompletedTask;
}
}

View File

@ -8,8 +8,8 @@
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia" xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
xmlns:converters="clr-namespace:Artemis.UI.Converters" xmlns:converters="clr-namespace:Artemis.UI.Converters"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="650" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="650"
x:Class="Artemis.UI.Screens.Workshop.Library.Tabs.LibrarySubmissionsView" x:Class="Artemis.UI.Screens.Workshop.Library.Tabs.SubmissionsTabView"
x:DataType="tabs:LibrarySubmissionsViewModel"> x:DataType="tabs:SubmissionsTabViewModel">
<UserControl.Resources> <UserControl.Resources>
<converters:EntryIconUriConverter x:Key="EntryIconUriConverter" /> <converters:EntryIconUriConverter x:Key="EntryIconUriConverter" />
<converters:DateTimeConverter x:Key="DateTimeConverter" /> <converters:DateTimeConverter x:Key="DateTimeConverter" />
@ -54,7 +54,7 @@
Margin="0 0 0 5" Margin="0 0 0 5"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch" HorizontalContentAlignment="Stretch"
Command="{Binding $parent[tabs:LibrarySubmissionsView].DataContext.NavigateToEntry}" Command="{Binding $parent[tabs:SubmissionsTabView].DataContext.NavigateToEntry}"
CommandParameter="{CompiledBinding}"> CommandParameter="{CompiledBinding}">
<Grid ColumnDefinitions="Auto,*,Auto"> <Grid ColumnDefinitions="Auto,*,Auto">
<!-- Icon --> <!-- Icon -->

View File

@ -5,9 +5,9 @@ using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Library.Tabs; namespace Artemis.UI.Screens.Workshop.Library.Tabs;
public partial class LibrarySubmissionsView : ReactiveUserControl<LibrarySubmissionsViewModel> public partial class SubmissionsTabView : ReactiveUserControl<SubmissionsTabViewModel>
{ {
public LibrarySubmissionsView() public SubmissionsTabView()
{ {
InitializeComponent(); InitializeComponent();
} }

View File

@ -6,7 +6,6 @@ using System.Threading.Tasks;
using Artemis.UI.Extensions; using Artemis.UI.Extensions;
using Artemis.UI.Screens.Workshop.CurrentUser; using Artemis.UI.Screens.Workshop.CurrentUser;
using Artemis.UI.Screens.Workshop.SubmissionWizard; using Artemis.UI.Screens.Workshop.SubmissionWizard;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
using Artemis.WebClient.Workshop; using Artemis.WebClient.Workshop;
@ -17,7 +16,7 @@ using StrawberryShake;
namespace Artemis.UI.Screens.Workshop.Library.Tabs; namespace Artemis.UI.Screens.Workshop.Library.Tabs;
public class LibrarySubmissionsViewModel : ActivatableViewModelBase, IWorkshopViewModel public class SubmissionsTabViewModel : RoutableScreen
{ {
private readonly IWorkshopClient _client; private readonly IWorkshopClient _client;
private readonly SourceCache<IGetSubmittedEntries_SubmittedEntries, Guid> _entries; private readonly SourceCache<IGetSubmittedEntries_SubmittedEntries, Guid> _entries;
@ -25,7 +24,7 @@ public class LibrarySubmissionsViewModel : ActivatableViewModelBase, IWorkshopVi
private bool _isLoading = true; private bool _isLoading = true;
private bool _workshopReachable; private bool _workshopReachable;
public LibrarySubmissionsViewModel(IWorkshopClient client, IAuthenticationService authenticationService, IWindowService windowService, IWorkshopService workshopService, IRouter router) public SubmissionsTabViewModel(IWorkshopClient client, IAuthenticationService authenticationService, IWindowService windowService, IWorkshopService workshopService, IRouter router)
{ {
_client = client; _client = client;
_windowService = windowService; _windowService = windowService;

View File

@ -12,7 +12,7 @@ using System;
namespace Artemis.UI.Screens.Workshop.Library; namespace Artemis.UI.Screens.Workshop.Library;
public class WorkshopLibraryViewModel : RoutableScreen<ActivatableViewModelBase>, IWorkshopViewModel public class WorkshopLibraryViewModel : RoutableHostScreen<RoutableScreen>
{ {
private RouteViewModel? _selectedTab; private RouteViewModel? _selectedTab;

View File

@ -18,7 +18,7 @@ using StrawberryShake;
namespace Artemis.UI.Screens.Workshop.Profile; namespace Artemis.UI.Screens.Workshop.Profile;
public class ProfileDetailsViewModel : RoutableScreen<ActivatableViewModelBase, WorkshopDetailParameters>, IWorkshopViewModel public class ProfileDetailsViewModel : RoutableScreen<WorkshopDetailParameters>
{ {
private readonly IWorkshopClient _client; private readonly IWorkshopClient _client;
private readonly ProfileEntryDownloadHandler _downloadHandler; private readonly ProfileEntryDownloadHandler _downloadHandler;

View File

@ -19,7 +19,6 @@ public class SearchViewModel : ViewModelBase
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IRouter _router; private readonly IRouter _router;
private readonly IWorkshopClient _workshopClient; private readonly IWorkshopClient _workshopClient;
private EntryType? _entryType;
private bool _isLoading; private bool _isLoading;
private SearchResultViewModel? _selectedEntry; private SearchResultViewModel? _selectedEntry;
@ -44,12 +43,6 @@ public class SearchViewModel : ViewModelBase
set => RaiseAndSetIfChanged(ref _selectedEntry, value); set => RaiseAndSetIfChanged(ref _selectedEntry, value);
} }
public EntryType? EntryType
{
get => _entryType;
set => RaiseAndSetIfChanged(ref _entryType, value);
}
public bool IsLoading public bool IsLoading
{ {
get => _isLoading; get => _isLoading;
@ -76,7 +69,7 @@ public class SearchViewModel : ViewModelBase
return new List<object>(); return new List<object>();
IsLoading = true; IsLoading = true;
IOperationResult<ISearchEntriesResult> results = await _workshopClient.SearchEntries.ExecuteAsync(input, EntryType, cancellationToken); IOperationResult<ISearchEntriesResult> results = await _workshopClient.SearchEntries.ExecuteAsync(input, null, cancellationToken);
return results.Data?.SearchEntries.Select(e => new SearchResultViewModel(e) as object) ?? new List<object>(); return results.Data?.SearchEntries.Select(e => new SearchResultViewModel(e) as object) ?? new List<object>();
} }
catch (Exception e) catch (Exception e)

View File

@ -1,38 +1,18 @@
using System; using Artemis.UI.Screens.Workshop.Home;
using System.Threading;
using System.Threading.Tasks;
using Artemis.UI.Screens.Workshop.Home;
using Artemis.UI.Screens.Workshop.Search; using Artemis.UI.Screens.Workshop.Search;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Routing;
using Artemis.WebClient.Workshop;
namespace Artemis.UI.Screens.Workshop; namespace Artemis.UI.Screens.Workshop;
public class WorkshopViewModel : RoutableScreen<IWorkshopViewModel>, IMainScreenViewModel public class WorkshopViewModel : RoutableHostScreen<RoutableScreen>, IMainScreenViewModel
{ {
private readonly SearchViewModel _searchViewModel;
public WorkshopViewModel(SearchViewModel searchViewModel, WorkshopHomeViewModel homeViewModel) public WorkshopViewModel(SearchViewModel searchViewModel, WorkshopHomeViewModel homeViewModel)
{ {
_searchViewModel = searchViewModel;
TitleBarViewModel = searchViewModel; TitleBarViewModel = searchViewModel;
HomeViewModel = homeViewModel; HomeViewModel = homeViewModel;
} }
public ViewModelBase TitleBarViewModel { get; } public ViewModelBase TitleBarViewModel { get; }
public WorkshopHomeViewModel HomeViewModel { get; } public WorkshopHomeViewModel HomeViewModel { get; }
/// <inheritdoc />
public override Task OnNavigating(NavigationArguments args, CancellationToken cancellationToken)
{
_searchViewModel.EntryType = Screen?.EntryType;
return Task.CompletedTask;
}
}
public interface IWorkshopViewModel
{
public EntryType? EntryType { get; }
} }