refactor: page factory and less mvvm-breaking

This commit is contained in:
estrogen elf
2025-06-23 09:37:30 -05:00
parent c78f75a332
commit 471559d987
12 changed files with 118 additions and 90 deletions

View File

@@ -13,17 +13,25 @@ using Needlework.Net.Views.MainWindow;
using System; using System;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks;
namespace Needlework.Net; namespace Needlework.Net;
public partial class App : Application, IEnableLogger public partial class App : Application, IEnableLogger
{ {
private readonly IServiceProvider _serviceProvider; private readonly IDataTemplate _viewLocator;
private readonly IBlobCache _blobCache;
private readonly PageFactory _pageFactory;
private readonly MainWindowViewModel _mainWindowViewModel;
public App(IServiceProvider serviceProvider) public App(IServiceProvider serviceProvider)
{ {
_serviceProvider = serviceProvider; _viewLocator = serviceProvider.GetRequiredService<IDataTemplate>();
_blobCache = serviceProvider.GetRequiredService<IBlobCache>();
_pageFactory = serviceProvider.GetRequiredService<PageFactory>();
_mainWindowViewModel = serviceProvider.GetRequiredService<MainWindowViewModel>();
this.Log() this.Log()
.Debug("NeedleworkDotNet version: {Version}", AppInfo.Version); .Debug("NeedleworkDotNet version: {Version}", AppInfo.Version);
@@ -43,30 +51,20 @@ public partial class App : Application, IEnableLogger
public override void Initialize() public override void Initialize()
{ {
DataTemplates.Add(_serviceProvider.GetRequiredService<IDataTemplate>()); DataTemplates.Add(_viewLocator);
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
} }
public override void OnFrameworkInitializationCompleted() public override void OnFrameworkInitializationCompleted()
{ {
foreach (var page in _serviceProvider.GetServices<PageBase>())
{
Task.Run(page.InitializeAsync);
}
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{ {
desktop.MainWindow = new MainWindowView() desktop.MainWindow = new MainWindowView(_mainWindowViewModel, _pageFactory);
{
DataContext = _serviceProvider.GetRequiredService<MainWindowViewModel>()
};
MainWindow = desktop.MainWindow; MainWindow = desktop.MainWindow;
desktop.ShutdownRequested += (_, _) => desktop.ShutdownRequested += (_, _) =>
{ {
var blobCache = _serviceProvider.GetRequiredService<IBlobCache>(); _blobCache.Flush().Wait();
blobCache.Flush().Wait(); _blobCache.Dispose();
blobCache.Dispose();
}; };
} }

View File

@@ -137,6 +137,8 @@ class Program
builder.AddSingleton<PageBase, SchemasViewModel>(); builder.AddSingleton<PageBase, SchemasViewModel>();
builder.AddSingleton<PageBase, AboutViewModel>(); builder.AddSingleton<PageBase, AboutViewModel>();
builder.AddSingleton<PageBase, SettingsViewModel>(); builder.AddSingleton<PageBase, SettingsViewModel>();
builder.AddSingleton<PageFactory>();
} }
private static void Program_UnhandledException(object sender, UnhandledExceptionEventArgs e) private static void Program_UnhandledException(object sender, UnhandledExceptionEventArgs e)

View File

@@ -1,16 +1,12 @@
using Avalonia; using Avalonia.Threading;
using Avalonia.Media;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using FluentAvalonia.UI.Controls;
using Needlework.Net.Constants; using Needlework.Net.Constants;
using Needlework.Net.Extensions; using Needlework.Net.Extensions;
using Needlework.Net.Helpers; using Needlework.Net.Helpers;
using Needlework.Net.Messages; using Needlework.Net.Messages;
using Needlework.Net.Services; using Needlework.Net.Services;
using Needlework.Net.ViewModels.Pages;
using Needlework.Net.Views.MainWindow; using Needlework.Net.Views.MainWindow;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -34,21 +30,13 @@ public partial class MainWindowViewModel
private readonly SchemaPaneService _schemaPaneService; private readonly SchemaPaneService _schemaPaneService;
public MainWindowViewModel(IEnumerable<PageBase> pages, DialogService dialogService, DocumentService documentService, NotificationService notificationService, SchemaPaneService schemaPaneService) public MainWindowViewModel(DialogService dialogService, DocumentService documentService, NotificationService notificationService, SchemaPaneService schemaPaneService)
{ {
_dialogService = dialogService; _dialogService = dialogService;
_documentService = documentService; _documentService = documentService;
_notificationService = notificationService; _notificationService = notificationService;
_schemaPaneService = schemaPaneService; _schemaPaneService = schemaPaneService;
NavigationViewItems = pages
.OrderBy(p => p.Index)
.ThenBy(p => p.DisplayName)
.Select(ToNavigationViewItem)
.ToList();
SelectedNavigationViewItem = NavigationViewItems.First();
CurrentPage = (PageBase)SelectedNavigationViewItem.Tag!;
_notificationService.Notifications.Subscribe(async notification => _notificationService.Notifications.Subscribe(async notification =>
{ {
var vm = new NotificationViewModel(notification); var vm = new NotificationViewModel(notification);
@@ -92,29 +80,13 @@ public partial class MainWindowViewModel
[ObservableProperty] [ObservableProperty]
private ObservableCollection<NotificationViewModel> _notifications = []; private ObservableCollection<NotificationViewModel> _notifications = [];
[ObservableProperty]
private NavigationViewItem _selectedNavigationViewItem;
[ObservableProperty]
private PageBase _currentPage;
[ObservableProperty] [ObservableProperty]
private SchemaSearchDetailsViewModel? _selectedSchemaSearchDetails; private SchemaSearchDetailsViewModel? _selectedSchemaSearchDetails;
public List<NavigationViewItem> NavigationViewItems { get; private set; } = [];
public string AppName => AppInfo.Name; public string AppName => AppInfo.Name;
public string Title => $"{AppInfo.Name} {AppInfo.Version}"; public string Title => $"{AppInfo.Name} {AppInfo.Version}";
partial void OnSelectedNavigationViewItemChanged(NavigationViewItem value)
{
if (value.Tag is PageBase page)
{
CurrentPage = page;
}
}
partial void OnSelectedSchemaSearchDetailsChanged(SchemaSearchDetailsViewModel? value) partial void OnSelectedSchemaSearchDetailsChanged(SchemaSearchDetailsViewModel? value)
{ {
if (value == null) return; if (value == null) return;
@@ -155,25 +127,6 @@ public partial class MainWindowViewModel
} }
} }
private NavigationViewItem ToNavigationViewItem(PageBase page) => new()
{
Content = page.DisplayName,
Tag = page,
IconSource = new ImageIconSource
{
Source = new Projektanker.Icons.Avalonia.IconImage()
{
Value = page.Icon,
Brush = new SolidColorBrush(Application.Current!.ActualThemeVariant.Key switch
{
"Light" => Colors.Black,
"Dark" => Colors.White,
_ => Colors.Gray
})
}
}
};
public async Task<IEnumerable<object>> PopulateAsync(string? searchText, CancellationToken cancellationToken) public async Task<IEnumerable<object>> PopulateAsync(string? searchText, CancellationToken cancellationToken)
{ {
if (searchText == null) return []; if (searchText == null) return [];

View File

@@ -12,7 +12,7 @@ public partial class ConsoleViewModel : PageBase
{ {
private readonly DocumentService _documentService; private readonly DocumentService _documentService;
public ConsoleViewModel(DocumentService documentService, NotificationService notificationService) : base("Console", "fa-solid fa-terminal", -200) public ConsoleViewModel(DocumentService documentService, NotificationService notificationService) : base("Console", "fa-solid fa-terminal")
{ {
_request = new(notificationService, Endpoints.Tab.LCU); _request = new(notificationService, Endpoints.Tab.LCU);
_documentService = documentService; _documentService = documentService;

View File

@@ -22,7 +22,7 @@ public partial class EndpointsViewModel : PageBase
private readonly NotificationService _notificationService; private readonly NotificationService _notificationService;
public EndpointsViewModel(DocumentService documentService, NotificationService notificationService) : base("Endpoints", "fa-solid fa-rectangle-list", -500) public EndpointsViewModel(DocumentService documentService, NotificationService notificationService) : base("Endpoints", "fa-solid fa-rectangle-list")
{ {
_documentService = documentService; _documentService = documentService;
_notificationService = notificationService; _notificationService = notificationService;

View File

@@ -21,7 +21,7 @@ public partial class HomeViewModel : PageBase, IEnableLogger
private readonly IDisposable _carouselNextDisposable; private readonly IDisposable _carouselNextDisposable;
public HomeViewModel(HextechDocsService hextechDocsService) : base("Home", "fa-solid fa-house", int.MinValue) public HomeViewModel(HextechDocsService hextechDocsService) : base("Home", "fa-solid fa-house")
{ {
_hextechDocsService = hextechDocsService; _hextechDocsService = hextechDocsService;

View File

@@ -4,11 +4,11 @@ using System.Threading.Tasks;
namespace Needlework.Net.ViewModels.Pages; namespace Needlework.Net.ViewModels.Pages;
public abstract partial class PageBase(string displayName, string icon, int index = 0) : ObservableValidator public abstract partial class PageBase(string displayName, string icon) : ObservableValidator
{ {
[ObservableProperty] private string _displayName = displayName; public string DisplayName { get; } = displayName;
[ObservableProperty] private string _icon = icon;
[ObservableProperty] private int _index = index; public string Icon { get; } = icon;
public abstract Task InitializeAsync(); public abstract Task InitializeAsync();
} }

View File

@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Needlework.Net.ViewModels.Pages
{
public class PageFactory
{
private readonly IEnumerable<PageBase> _pages;
public PageFactory(IEnumerable<PageBase> pages)
{
_pages = pages;
}
public PageBase GetPage<T>() where T : PageBase
{
var page = _pages.Where(page => typeof(T) == page.GetType())
.FirstOrDefault() ?? throw new NotSupportedException(typeof(T).FullName);
Task.Run(page.InitializeAsync);
return page;
}
}
}

View File

@@ -22,7 +22,7 @@ namespace Needlework.Net.ViewModels.Pages.Schemas
private List<SchemaSearchDetailsViewModel> _schemas = []; private List<SchemaSearchDetailsViewModel> _schemas = [];
public SchemasViewModel(DocumentService documentService, SchemaPaneService schemaPaneService) : base("Schemas", "fa-solid fa-file-lines", -100) public SchemasViewModel(DocumentService documentService, SchemaPaneService schemaPaneService) : base("Schemas", "fa-solid fa-file-lines")
{ {
_documentService = documentService; _documentService = documentService;
_schemaPaneService = schemaPaneService; _schemaPaneService = schemaPaneService;

View File

@@ -35,7 +35,7 @@ public partial class WebSocketViewModel : PageBase, IEnableLogger
private readonly object _tokenLock = new(); private readonly object _tokenLock = new();
public WebSocketViewModel(IFlurlClientCache clients, NotificationService notificationService) : base("Event Viewer", "fa-solid fa-plug", -100) public WebSocketViewModel(IFlurlClientCache clients, NotificationService notificationService) : base("Event Viewer", "fa-solid fa-plug")
{ {
_githubUserContentClient = clients.Get(FlurlClientKeys.GithubUserContentClient); _githubUserContentClient = clients.Get(FlurlClientKeys.GithubUserContentClient);
_notificationService = notificationService; _notificationService = notificationService;

View File

@@ -66,13 +66,12 @@
OpenPaneLength="350" OpenPaneLength="350"
PanePlacement="Right"> PanePlacement="Right">
<ui:NavigationView AlwaysShowHeader="False" <ui:NavigationView AlwaysShowHeader="False"
PaneDisplayMode="Left" PaneDisplayMode="Left"
IsSettingsVisible="False" IsSettingsVisible="False"
IsPaneOpen="False" IsPaneOpen="False"
OpenPaneLength="200" OpenPaneLength="200"
Grid.Row="1" Grid.Row="1"
MenuItemsSource="{Binding NavigationViewItems}" Name="NavigationView">
SelectedItem="{Binding SelectedNavigationViewItem}">
<ui:NavigationView.PaneFooter> <ui:NavigationView.PaneFooter>
<StackPanel Orientation="Vertical"> <StackPanel Orientation="Vertical">
<StackPanel.Styles> <StackPanel.Styles>
@@ -105,7 +104,7 @@
</StackPanel> </StackPanel>
</ui:NavigationView.PaneFooter> </ui:NavigationView.PaneFooter>
<Grid> <Grid>
<TransitioningContentControl Content="{Binding CurrentPage}"/> <TransitioningContentControl Name="CurrentPageContentControl"/>
<ItemsControl ItemsSource="{Binding Notifications}" <ItemsControl ItemsSource="{Binding Notifications}"
VerticalAlignment="Bottom"> VerticalAlignment="Bottom">
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>

View File

@@ -4,7 +4,18 @@ using Avalonia.Interactivity;
using Avalonia.Media; using Avalonia.Media;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using FluentAvalonia.UI.Windowing; using FluentAvalonia.UI.Windowing;
using Needlework.Net.ViewModels.MainWindow;
using Needlework.Net.ViewModels.Pages;
using Needlework.Net.ViewModels.Pages.About;
using Needlework.Net.ViewModels.Pages.Console;
using Needlework.Net.ViewModels.Pages.Endpoints;
using Needlework.Net.ViewModels.Pages.Home;
using Needlework.Net.ViewModels.Pages.Schemas;
using Needlework.Net.ViewModels.Pages.Settings;
using Needlework.Net.ViewModels.Pages.WebSocket;
using System; using System;
using System.Collections.Generic;
using System.Linq;
namespace Needlework.Net.Views.MainWindow; namespace Needlework.Net.Views.MainWindow;
@@ -13,34 +24,74 @@ public partial class MainWindowView : AppWindow
public MainWindowView() public MainWindowView()
{ {
InitializeComponent(); InitializeComponent();
}
public MainWindowView(MainWindowViewModel mainWindowViewModel, PageFactory pageFactory)
{
InitializeComponent();
DataContext = mainWindowViewModel;
TitleBar.ExtendsContentIntoTitleBar = true; TitleBar.ExtendsContentIntoTitleBar = true;
TitleBar.TitleBarHitTestType = TitleBarHitTestType.Complex; TitleBar.TitleBarHitTestType = TitleBarHitTestType.Complex;
TransparencyLevelHint = [WindowTransparencyLevel.Mica, WindowTransparencyLevel.None]; TransparencyLevelHint = [WindowTransparencyLevel.Mica, WindowTransparencyLevel.None];
Background = IsWindows11 ? null : Background; Background = IsWindows11 ? null : Background;
NavigationView.MenuItems = [.. new List<PageBase>()
{
pageFactory.GetPage<HomeViewModel>(),
pageFactory.GetPage<EndpointsViewModel>(),
pageFactory.GetPage<ConsoleViewModel>(),
pageFactory.GetPage<WebSocketViewModel>(),
pageFactory.GetPage<SchemasViewModel>(),
pageFactory.GetPage<SettingsViewModel>(),
pageFactory.GetPage<AboutViewModel>(),
}
.Select(ToNavigationViewItem)];
NavigationView.GetObservable(NavigationView.SelectedItemProperty)
.Subscribe(value =>
{
if (value is NavigationViewItem item)
{
CurrentPageContentControl.Content = item.Tag;
}
});
NavigationView.SelectedItem = NavigationView.MenuItems.Cast<NavigationViewItem>()
.First();
SchemaAutoCompleteBox.MinimumPopulateDelay = TimeSpan.FromSeconds(1); SchemaAutoCompleteBox.MinimumPopulateDelay = TimeSpan.FromSeconds(1);
SchemaAutoCompleteBox.MinimumPrefixLength = 3; SchemaAutoCompleteBox.MinimumPrefixLength = 3;
App.Current!.TryGetResource("TextFillColorPrimaryBrush", ActualThemeVariant, out var brush);
CloseCommandBarButton.IconSource = new ImageIconSource CloseCommandBarButton.IconSource = new ImageIconSource
{ {
Source = new Projektanker.Icons.Avalonia.IconImage() Source = new Projektanker.Icons.Avalonia.IconImage()
{ {
Value = "fa-solid fa-file-circle-xmark", Value = "fa-solid fa-file-circle-xmark",
Brush = new SolidColorBrush(Application.Current!.ActualThemeVariant.Key switch Brush = (SolidColorBrush)brush!
}
};
}
private NavigationViewItem ToNavigationViewItem(PageBase page)
{
App.Current!.TryGetResource("TextFillColorPrimaryBrush", ActualThemeVariant, out var brush);
return new NavigationViewItem()
{
Content = page.DisplayName,
Tag = page,
IconSource = new ImageIconSource
{
Source = new Projektanker.Icons.Avalonia.IconImage()
{ {
"Light" => Colors.Black, Value = page.Icon,
"Dark" => Colors.White, Brush = (SolidColorBrush)brush!
_ => Colors.Gray }
})
} }
}; };
} }
protected override void OnLoaded(RoutedEventArgs e) protected override void OnLoaded(RoutedEventArgs e)
{ {
base.OnLoaded(e);
if (VisualRoot is AppWindow aw) if (VisualRoot is AppWindow aw)
{ {
TitleBarHost.ColumnDefinitions[3].Width = new GridLength(aw.TitleBar.RightInset, GridUnitType.Pixel); TitleBarHost.ColumnDefinitions[3].Width = new GridLength(aw.TitleBar.RightInset, GridUnitType.Pixel);