diff --git a/SteamAccountManager.AvaloniaUI/App.axaml.cs b/SteamAccountManager.AvaloniaUI/App.axaml.cs index 152fcf1..f2d96de 100644 --- a/SteamAccountManager.AvaloniaUI/App.axaml.cs +++ b/SteamAccountManager.AvaloniaUI/App.axaml.cs @@ -16,7 +16,7 @@ public override void OnFrameworkInitializationCompleted() { if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - desktop.MainWindow = new MainWindow + desktop.MainWindow = new MainWindowView { DataContext = new MainWindowViewModel(), }; diff --git a/SteamAccountManager.AvaloniaUI/AppViewLocator.cs b/SteamAccountManager.AvaloniaUI/AppViewLocator.cs new file mode 100644 index 0000000..d546d58 --- /dev/null +++ b/SteamAccountManager.AvaloniaUI/AppViewLocator.cs @@ -0,0 +1,35 @@ +using ReactiveUI; +using SteamAccountManager.AvaloniaUI.ViewModels; +using SteamAccountManager.AvaloniaUI.Views; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SteamAccountManager.AvaloniaUI +{ + public class AppViewLocator : IViewLocator + { + public IViewFor? ResolveView(T viewModel, string? contract = null) + { + string? viewPath = viewModel?.GetType().FullName? + .Replace("ViewModels", "Views") + .Replace("ViewModel", "View"); + + switch (viewPath) + { + case not null: + return createInstanceFromPath(viewPath); + default: + return null; + } + } + + private IViewFor createInstanceFromPath(string path) + { + Type classType = Type.GetType(path, true) ?? throw new Exception("Couldn't resolve type from path"); + return Activator.CreateInstance(classType) as IViewFor ?? throw new Exception("Type isn't of type IViewFor!"); + } + } +} diff --git a/SteamAccountManager.AvaloniaUI/Common/AdvancedObservableCollection.cs b/SteamAccountManager.AvaloniaUI/Common/AdvancedObservableCollection.cs index 3e6042c..4e74fc2 100644 --- a/SteamAccountManager.AvaloniaUI/Common/AdvancedObservableCollection.cs +++ b/SteamAccountManager.AvaloniaUI/Common/AdvancedObservableCollection.cs @@ -5,7 +5,7 @@ namespace SteamAccountManager.AvaloniaUI.Common { - internal class AdvancedObservableCollection : ObservableCollection + public class AdvancedObservableCollection : ObservableCollection { public AdvancedObservableCollection() : base() { diff --git a/SteamAccountManager.AvaloniaUI/Common/ViewModelStore.cs b/SteamAccountManager.AvaloniaUI/Common/ViewModelStore.cs new file mode 100644 index 0000000..45292f5 --- /dev/null +++ b/SteamAccountManager.AvaloniaUI/Common/ViewModelStore.cs @@ -0,0 +1,33 @@ +using Autofac; +using ReactiveUI; +using SteamAccountManager.Domain.Common.CodeExtensions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SteamAccountManager.AvaloniaUI.Common +{ + internal class ViewModelStore + { + private readonly Dictionary _viewModels = new Dictionary(); + + public IRoutableViewModel Get(IScreen screen) where T : class, IRoutableViewModel + { + var viewModel = _viewModels.GetOrNull(typeof(T)); + if (viewModel is null) + { + viewModel = CreateViewModel(screen); + Register(viewModel); + } + + return viewModel; + } + + public void Register(IRoutableViewModel viewModel) => _viewModels.Add(viewModel.GetType(), viewModel); + + private IRoutableViewModel CreateViewModel(IScreen screen) where T : class, IRoutableViewModel + => Dependencies.Container?.Resolve(new TypedParameter(typeof(IScreen), screen)) ?? throw new Exception("Failed to resolve AccountSwitcherViewModel"); + } +} diff --git a/SteamAccountManager.AvaloniaUI/Dependencies.cs b/SteamAccountManager.AvaloniaUI/Dependencies.cs index c52158e..887f239 100644 --- a/SteamAccountManager.AvaloniaUI/Dependencies.cs +++ b/SteamAccountManager.AvaloniaUI/Dependencies.cs @@ -1,5 +1,7 @@ using Autofac; +using Autofac.Core; using DI; +using ReactiveUI; using SteamAccountManager.Application.Steam.Service; using SteamAccountManager.AvaloniaUI.Mappers; using SteamAccountManager.AvaloniaUI.Notifications; @@ -35,7 +37,15 @@ public static void RegisterAvaloniaModule(this ContainerBuilder builder) public static void RegisterViewModels(this ContainerBuilder builder) { - builder.RegisterType(); + + builder.RegisterViewModel(); + builder.RegisterViewModel(); + } + + private static void RegisterViewModel(this ContainerBuilder builder) where ViewModel : RoutableViewModel + { + builder.RegisterType() + .WithParameter(new TypedParameter(typeof(IScreen), "screen")); } } } diff --git a/SteamAccountManager.AvaloniaUI/Mappers/AccountMapper.cs b/SteamAccountManager.AvaloniaUI/Mappers/AccountMapper.cs index 590d299..e425f22 100644 --- a/SteamAccountManager.AvaloniaUI/Mappers/AccountMapper.cs +++ b/SteamAccountManager.AvaloniaUI/Mappers/AccountMapper.cs @@ -9,7 +9,7 @@ namespace SteamAccountManager.AvaloniaUI.Mappers { - internal class AccountMapper + public class AccountMapper { private AvatarService _avatarService; diff --git a/SteamAccountManager.AvaloniaUI/Models/Account.cs b/SteamAccountManager.AvaloniaUI/Models/Account.cs index 2463e3c..32c8dd3 100644 --- a/SteamAccountManager.AvaloniaUI/Models/Account.cs +++ b/SteamAccountManager.AvaloniaUI/Models/Account.cs @@ -5,7 +5,7 @@ namespace SteamAccountManager.AvaloniaUI.Models { - internal class Account : INotifyPropertyChanged + public class Account : INotifyPropertyChanged { public IBitmap? ProfilePicture { get; set; } public Uri? ProfilePictureUrl { get; set; } diff --git a/SteamAccountManager.AvaloniaUI/Services/AvatarService.cs b/SteamAccountManager.AvaloniaUI/Services/AvatarService.cs index f09ff78..1a18e49 100644 --- a/SteamAccountManager.AvaloniaUI/Services/AvatarService.cs +++ b/SteamAccountManager.AvaloniaUI/Services/AvatarService.cs @@ -8,7 +8,7 @@ namespace SteamAccountManager.AvaloniaUI.Services { - internal class AvatarService + public class AvatarService { private IAssetLoader _assetLoader; private readonly IAvatarService _avatarService; diff --git a/SteamAccountManager.AvaloniaUI/SteamAccountManager.AvaloniaUI.csproj b/SteamAccountManager.AvaloniaUI/SteamAccountManager.AvaloniaUI.csproj index eb81fce..dc00c90 100644 --- a/SteamAccountManager.AvaloniaUI/SteamAccountManager.AvaloniaUI.csproj +++ b/SteamAccountManager.AvaloniaUI/SteamAccountManager.AvaloniaUI.csproj @@ -21,6 +21,12 @@ + + + + + MSBuild:Compile + @@ -42,8 +48,11 @@ - - AccountSwitcher.axaml + + AccountSwitcherView.axaml + + + MainWindowView.axaml diff --git a/SteamAccountManager.AvaloniaUI/SteamAccountSwitcherStyles.xaml b/SteamAccountManager.AvaloniaUI/SteamAccountSwitcherStyles.xaml new file mode 100644 index 0000000..de89a3b --- /dev/null +++ b/SteamAccountManager.AvaloniaUI/SteamAccountSwitcherStyles.xaml @@ -0,0 +1,35 @@ + + + + + + + + + diff --git a/SteamAccountManager.AvaloniaUI/ViewModels/AccountSwitcherViewModel.cs b/SteamAccountManager.AvaloniaUI/ViewModels/AccountSwitcherViewModel.cs index 16520e2..cf13fff 100644 --- a/SteamAccountManager.AvaloniaUI/ViewModels/AccountSwitcherViewModel.cs +++ b/SteamAccountManager.AvaloniaUI/ViewModels/AccountSwitcherViewModel.cs @@ -10,11 +10,13 @@ using System.Threading.Tasks; using System.Windows.Input; using SteamAccountManager.Application.Steam.Observables; +using ReactiveUI; namespace SteamAccountManager.AvaloniaUI.ViewModels { // TODO: looks ridicilous, I should refactor all of this but I don't feel bored enough yet - internal class AccountSwitcherViewModel + // TOOD: switch to reactive commands etc. + public class AccountSwitcherViewModel : RoutableViewModel { private readonly IGetAccountsWithDetailsUseCase _getAccountsUseCase; private readonly ISwitchAccountUseCase _switchAccountUseCase; @@ -28,15 +30,15 @@ internal class AccountSwitcherViewModel public ICommand AddAccountCommand { get; } public Account? SelectedAccount { get; set; } - public AccountSwitcherViewModel ( + IScreen screen, IGetAccountsWithDetailsUseCase getAccountsUseCase, ISwitchAccountUseCase switchAccountUseCase, AccountMapper accountMapper, IAccountStorageObservable accountStorageObserver, ILocalNotificationService notificationService - ) + ) : base(screen) { _getAccountsUseCase = getAccountsUseCase; _switchAccountUseCase = switchAccountUseCase; diff --git a/SteamAccountManager.AvaloniaUI/ViewModels/Commands/QuickCommand.cs b/SteamAccountManager.AvaloniaUI/ViewModels/Commands/QuickCommand.cs index f3b4e8f..0cd4313 100644 --- a/SteamAccountManager.AvaloniaUI/ViewModels/Commands/QuickCommand.cs +++ b/SteamAccountManager.AvaloniaUI/ViewModels/Commands/QuickCommand.cs @@ -3,7 +3,7 @@ namespace SteamAccountManager.AvaloniaUI.ViewModels.Commands { - internal class QuickCommand : ICommand + public class QuickCommand : ICommand { private readonly Action _func; diff --git a/SteamAccountManager.AvaloniaUI/ViewModels/MainWindowViewModel.cs b/SteamAccountManager.AvaloniaUI/ViewModels/MainWindowViewModel.cs index e913d53..f9fe977 100644 --- a/SteamAccountManager.AvaloniaUI/ViewModels/MainWindowViewModel.cs +++ b/SteamAccountManager.AvaloniaUI/ViewModels/MainWindowViewModel.cs @@ -1,8 +1,71 @@ -namespace SteamAccountManager.AvaloniaUI.ViewModels +using ReactiveUI; +using SteamAccountManager.AvaloniaUI.Common; +using SteamAccountManager.AvaloniaUI.ViewModels.Commands; +using System.Reactive; + +namespace SteamAccountManager.AvaloniaUI.ViewModels { - public class MainWindowViewModel + public class NavigationTarget + { + public string Title { get; set; } + public string HintText { get; set; } + public QuickCommand NavigateCommand { get; set; } + + public NavigationTarget(string title, string hintText, QuickCommand navigateCommand) + { + Title = title; + HintText = hintText; + NavigateCommand = navigateCommand; + } + } + + public class MainWindowViewModel : ReactiveObject, IScreen { - public string Greeting => "Welcome to Avalonia!"; + // The Router associated with this Screen. + // Required by the IScreen interface. + public RoutingState Router { get; } = new RoutingState(); + + public AdvancedObservableCollection NavigationTargets { get; } = new(); + + private readonly ViewModelStore _viewModelStore = new ViewModelStore(); + + public MainWindowViewModel() + { + // Manage the routing state. Use the Router.Navigate.Execute + // command to navigate to different view models. + // + // Note, that the Navigate.Execute method accepts an instance + // of a view model, this allows you to pass parameters to + // your view models, or to reuse existing view models. + // + NavigationTargets.Add( + new NavigationTarget + ( + title: "Accounts", + hintText: "Show Accounts", + new QuickCommand(() => NavigateTo(_viewModelStore.Get(this))) + ) + ); + NavigationTargets.Add( + new NavigationTarget + ( + title: "Settings", + hintText: "Show Settings", + new QuickCommand(() => NavigateTo(_viewModelStore.Get(this))) + ) + ); + + NavigateTo(_viewModelStore.Get(this)); + } + + private void NavigateTo(IRoutableViewModel viewModel) + { + if (IsViewAlreadyVisible(viewModel)) + return; + + Router.Navigate.Execute(viewModel); + } + public bool IsViewAlreadyVisible(IRoutableViewModel viewModel) => viewModel == Router.GetCurrentViewModel(); } } diff --git a/SteamAccountManager.AvaloniaUI/ViewModels/SettingsViewModel.cs b/SteamAccountManager.AvaloniaUI/ViewModels/SettingsViewModel.cs new file mode 100644 index 0000000..94a958d --- /dev/null +++ b/SteamAccountManager.AvaloniaUI/ViewModels/SettingsViewModel.cs @@ -0,0 +1,18 @@ +using ReactiveUI; +using SteamAccountManager.AvaloniaUI.ViewModels.Commands; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Input; + +namespace SteamAccountManager.AvaloniaUI.ViewModels +{ + public class SettingsViewModel : RoutableViewModel + { + public SettingsViewModel(IScreen screen) : base(screen) + { + } + } +} diff --git a/SteamAccountManager.AvaloniaUI/ViewModels/ViewModelBase.cs b/SteamAccountManager.AvaloniaUI/ViewModels/ViewModelBase.cs index 9c45939..0cf03ea 100644 --- a/SteamAccountManager.AvaloniaUI/ViewModels/ViewModelBase.cs +++ b/SteamAccountManager.AvaloniaUI/ViewModels/ViewModelBase.cs @@ -1,8 +1,29 @@ using ReactiveUI; +using System; +using System.Windows.Input; namespace SteamAccountManager.AvaloniaUI.ViewModels { - public class ViewModelBase : ReactiveObject + public abstract class ViewModelBase : ReactiveObject { } + + public abstract class RoutableViewModel : ViewModelBase, IRoutableViewModel + { + public ICommand NavigateBackCommand { get; } + + public string? UrlPathSegment => Guid.NewGuid().ToString().Substring(0, 5); + public IScreen HostScreen { get; } + + public RoutableViewModel(IScreen screen) + { + NavigateBackCommand = ReactiveCommand.Create(NavigateBack); + HostScreen = screen; + } + + protected void NavigateBack() + { + HostScreen.Router.NavigateBack.Execute(); + } + } } diff --git a/SteamAccountManager.AvaloniaUI/Views/AccountSwitcher.axaml.cs b/SteamAccountManager.AvaloniaUI/Views/AccountSwitcher.axaml.cs deleted file mode 100644 index 4c81155..0000000 --- a/SteamAccountManager.AvaloniaUI/Views/AccountSwitcher.axaml.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Autofac; -using Avalonia.Controls; -using Avalonia.Markup.Xaml; -using SteamAccountManager.AvaloniaUI.Models; -using SteamAccountManager.AvaloniaUI.ViewModels; - -namespace SteamAccountManager.AvaloniaUI.Views -{ - public partial class AccountSwitcher : UserControl - { - private AccountSwitcherViewModel? _viewModel; - - public AccountSwitcher() - { - InitializeComponent(); - } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - _viewModel = Dependencies.Container?.Resolve() - ?? throw new System.Exception("Failed to resolve AccountSwitcherViewModel"); - DataContext = _viewModel; - } - - private void AccountSelection_SelectionChanged(object sender, SelectionChangedEventArgs e) - { - if ((sender as ListBox)?.SelectedItem is Account selectedAccount) - { - _viewModel?.OnAccountSelected(selectedAccount); - } - } - } -} diff --git a/SteamAccountManager.AvaloniaUI/Views/AccountSwitcher.axaml b/SteamAccountManager.AvaloniaUI/Views/AccountSwitcherView.axaml similarity index 81% rename from SteamAccountManager.AvaloniaUI/Views/AccountSwitcher.axaml rename to SteamAccountManager.AvaloniaUI/Views/AccountSwitcherView.axaml index 08d63d7..33e7628 100644 --- a/SteamAccountManager.AvaloniaUI/Views/AccountSwitcher.axaml +++ b/SteamAccountManager.AvaloniaUI/Views/AccountSwitcherView.axaml @@ -4,7 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="using:SteamAccountManager.AvaloniaUI" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="SteamAccountManager.AvaloniaUI.Views.AccountSwitcher"> + x:Class="SteamAccountManager.AvaloniaUI.Views.AccountSwitcherView"> @@ -13,24 +13,11 @@ - + - - - - - diff --git a/SteamAccountManager.AvaloniaUI/Views/AccountSwitcherView.axaml.cs b/SteamAccountManager.AvaloniaUI/Views/AccountSwitcherView.axaml.cs new file mode 100644 index 0000000..e6cdf18 --- /dev/null +++ b/SteamAccountManager.AvaloniaUI/Views/AccountSwitcherView.axaml.cs @@ -0,0 +1,25 @@ +using Autofac; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; +using SteamAccountManager.AvaloniaUI.Models; +using SteamAccountManager.AvaloniaUI.ViewModels; + +namespace SteamAccountManager.AvaloniaUI.Views +{ + public partial class AccountSwitcherView : ReactiveUserControl + { + public AccountSwitcherView() + { + AvaloniaXamlLoader.Load(this); + } + + private void AccountSelection_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if ((sender as ListBox)?.SelectedItem is Account selectedAccount) + { + ViewModel?.OnAccountSelected(selectedAccount); + } + } + } +} diff --git a/SteamAccountManager.AvaloniaUI/Views/MainWindow.axaml b/SteamAccountManager.AvaloniaUI/Views/MainWindow.axaml deleted file mode 100644 index 055c519..0000000 --- a/SteamAccountManager.AvaloniaUI/Views/MainWindow.axaml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - diff --git a/SteamAccountManager.AvaloniaUI/Views/MainWindowView.axaml b/SteamAccountManager.AvaloniaUI/Views/MainWindowView.axaml new file mode 100644 index 0000000..4eff34c --- /dev/null +++ b/SteamAccountManager.AvaloniaUI/Views/MainWindowView.axaml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + +