Automate view initialization

This commit is contained in:
tyrrrz 2026-04-02 14:35:17 +03:00
parent 7ee2763d4b
commit f6166764e9
11 changed files with 62 additions and 66 deletions

View file

@ -12,7 +12,6 @@ using DiscordChatExporter.Gui.Utils.Extensions;
using DiscordChatExporter.Gui.ViewModels; using DiscordChatExporter.Gui.ViewModels;
using DiscordChatExporter.Gui.ViewModels.Components; using DiscordChatExporter.Gui.ViewModels.Components;
using DiscordChatExporter.Gui.ViewModels.Dialogs; using DiscordChatExporter.Gui.ViewModels.Dialogs;
using DiscordChatExporter.Gui.Views;
using Material.Styles.Themes; using Material.Styles.Themes;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -20,11 +19,8 @@ namespace DiscordChatExporter.Gui;
public class App : Application, IDisposable public class App : Application, IDisposable
{ {
private readonly DisposableCollector _eventRoot = new();
private readonly ServiceProvider _services; private readonly ServiceProvider _services;
private readonly SettingsService _settingsService; private readonly DisposableCollector _eventRoot = new();
private readonly MainViewModel _mainViewModel;
private bool _isDisposed; private bool _isDisposed;
@ -53,35 +49,30 @@ public class App : Application, IDisposable
services.AddTransient<SettingsViewModel>(); services.AddTransient<SettingsViewModel>();
_services = services.BuildServiceProvider(true); _services = services.BuildServiceProvider(true);
_settingsService = _services.GetRequiredService<SettingsService>();
_mainViewModel = _services.GetRequiredService<ViewModelManager>().CreateMainViewModel();
// Re-initialize the theme when the user changes it // Re-initialize the theme when the user changes it
_eventRoot.Add( _eventRoot.Add(
_settingsService.WatchProperty( _services
o => o.Theme, .GetRequiredService<SettingsService>()
() => .WatchProperty(
{ o => o.Theme,
RequestedThemeVariant = _settingsService.Theme switch () =>
{ {
ThemeVariant.Light => Avalonia.Styling.ThemeVariant.Light, RequestedThemeVariant = _services
ThemeVariant.Dark => Avalonia.Styling.ThemeVariant.Dark, .GetRequiredService<SettingsService>()
_ => Avalonia.Styling.ThemeVariant.Default, .Theme switch
}; {
ThemeVariant.Light => Avalonia.Styling.ThemeVariant.Light,
ThemeVariant.Dark => Avalonia.Styling.ThemeVariant.Dark,
_ => Avalonia.Styling.ThemeVariant.Default,
};
InitializeTheme(); InitializeTheme();
} }
) )
); );
} }
public override void Initialize()
{
base.Initialize();
AvaloniaXamlLoader.Load(this);
}
private void InitializeTheme() private void InitializeTheme()
{ {
var actualTheme = RequestedThemeVariant?.Key switch var actualTheme = RequestedThemeVariant?.Key switch
@ -97,25 +88,28 @@ public class App : Application, IDisposable
: Theme.Create(Theme.Dark, Color.Parse("#E8E8E8"), Color.Parse("#F9A825")); : Theme.Create(Theme.Dark, Color.Parse("#E8E8E8"), Color.Parse("#F9A825"));
} }
public override void Initialize()
{
base.Initialize();
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted() public override void OnFrameworkInitializationCompleted()
{ {
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{ {
desktop.MainWindow = new MainView { DataContext = _mainViewModel }; desktop.MainWindow = _services
.GetRequiredService<ViewManager>()
void OnExit(object? sender, ControlledApplicationLifetimeExitEventArgs args) .TryBindWindow(
{ _services.GetRequiredService<ViewModelManager>().CreateMainViewModel()
if (sender is IControlledApplicationLifetime lifetime) );
lifetime.Exit -= OnExit;
Dispose();
}
// Although `App.Dispose()` is invoked from `Program.Main(...)`, on some platforms // Although `App.Dispose()` is invoked from `Program.Main(...)`, on some platforms
// it may be called too late in the shutdown lifecycle. Attach an exit // it may be called too late in the shutdown lifecycle. Attach an exit
// handler to ensure timely disposal as a safeguard. // handler to ensure timely disposal as a safeguard.
// https://github.com/Tyrrrz/YoutubeDownloader/issues/795 // https://github.com/Tyrrrz/YoutubeDownloader/issues/795
desktop.Exit += OnExit; desktop.Exit += (_, _) => Dispose();
} }
base.OnFrameworkInitializationCompleted(); base.OnFrameworkInitializationCompleted();
@ -124,7 +118,7 @@ public class App : Application, IDisposable
InitializeTheme(); InitializeTheme();
// Load settings // Load settings
_settingsService.Load(); _services.GetRequiredService<SettingsService>().Load();
} }
private void Application_OnActualThemeVariantChanged(object? sender, EventArgs args) => private void Application_OnActualThemeVariantChanged(object? sender, EventArgs args) =>

View file

@ -30,8 +30,23 @@ public partial class ViewManager
view.DataContext ??= viewModel; view.DataContext ??= viewModel;
if (view.IsInitialized)
{
_ = viewModel.InitializeAsync();
}
else
{
view.Initialized += async (_, _) => await viewModel.InitializeAsync();
}
return view; return view;
} }
public UserControl<T>? TryBindUserControl<T>(T viewModel)
where T : ViewModelBase => TryBindView(viewModel) as UserControl<T>;
public Window<T>? TryBindWindow<T>(T viewModel)
where T : ViewModelBase => TryBindView(viewModel) as Window<T>;
} }
public partial class ViewManager : IDataTemplate public partial class ViewManager : IDataTemplate

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
namespace DiscordChatExporter.Gui.Framework; namespace DiscordChatExporter.Gui.Framework;
@ -9,6 +10,8 @@ public abstract class ViewModelBase : ObservableObject, IDisposable
protected void OnAllPropertiesChanged() => OnPropertyChanged(string.Empty); protected void OnAllPropertiesChanged() => OnPropertyChanged(string.Empty);
public virtual Task InitializeAsync() => Task.CompletedTask;
protected virtual void Dispose(bool disposing) { } protected virtual void Dispose(bool disposing) { }
public void Dispose() public void Dispose()

View file

@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -96,11 +95,12 @@ public partial class DashboardViewModel : ViewModelBase
public ObservableCollection<ChannelConnection> SelectedChannels { get; } = []; public ObservableCollection<ChannelConnection> SelectedChannels { get; } = [];
[RelayCommand] public override Task InitializeAsync()
private void Initialize()
{ {
if (!string.IsNullOrWhiteSpace(_settingsService.LastToken)) if (!string.IsNullOrWhiteSpace(_settingsService.LastToken))
Token = _settingsService.LastToken; Token = _settingsService.LastToken;
return Task.CompletedTask;
} }
[RelayCommand] [RelayCommand]

View file

@ -102,8 +102,7 @@ public partial class ExportSetupViewModel(
? MessageFilter.Parse(MessageFilterValue) ? MessageFilter.Parse(MessageFilterValue)
: MessageFilter.Null; : MessageFilter.Null;
[RelayCommand] public override Task InitializeAsync()
private void Initialize()
{ {
// Persist preferences // Persist preferences
SelectedFormat = settingsService.LastExportFormat; SelectedFormat = settingsService.LastExportFormat;
@ -126,6 +125,8 @@ public partial class ExportSetupViewModel(
|| ShouldReuseAssets || ShouldReuseAssets
|| !string.IsNullOrWhiteSpace(AssetsDirPath) || !string.IsNullOrWhiteSpace(AssetsDirPath)
|| IsReverseMessageOrder; || IsReverseMessageOrder;
return Task.CompletedTask;
} }
[RelayCommand] [RelayCommand]

View file

@ -2,7 +2,6 @@
using System.Diagnostics; using System.Diagnostics;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia; using Avalonia;
using CommunityToolkit.Mvvm.Input;
using DiscordChatExporter.Gui.Framework; using DiscordChatExporter.Gui.Framework;
using DiscordChatExporter.Gui.Localization; using DiscordChatExporter.Gui.Localization;
using DiscordChatExporter.Gui.Services; using DiscordChatExporter.Gui.Services;
@ -100,8 +99,7 @@ public partial class MainViewModel(
} }
} }
[RelayCommand] public override async Task InitializeAsync()
private async Task InitializeAsync()
{ {
await ShowUkraineSupportMessageAsync(); await ShowUkraineSupportMessageAsync();
await ShowDevelopmentBuildMessageAsync(); await ShowDevelopmentBuildMessageAsync();

View file

@ -12,11 +12,8 @@ public partial class DashboardView : UserControl<DashboardViewModel>
{ {
public DashboardView() => InitializeComponent(); public DashboardView() => InitializeComponent();
private void UserControl_OnLoaded(object? sender, RoutedEventArgs args) private void UserControl_OnLoaded(object? sender, RoutedEventArgs args) =>
{
DataContext.InitializeCommand.Execute(null);
TokenValueTextBox.Focus(); TokenValueTextBox.Focus();
}
private void AvailableGuildsListBox_OnSelectionChanged( private void AvailableGuildsListBox_OnSelectionChanged(
object? sender, object? sender,

View file

@ -10,8 +10,7 @@
xmlns:utils="clr-namespace:DiscordChatExporter.Gui.Utils" xmlns:utils="clr-namespace:DiscordChatExporter.Gui.Utils"
x:Name="UserControl" x:Name="UserControl"
Width="380" Width="380"
x:DataType="dialogs:ExportSetupViewModel" x:DataType="dialogs:ExportSetupViewModel">
Loaded="UserControl_OnLoaded">
<Grid RowDefinitions="Auto,*,Auto"> <Grid RowDefinitions="Auto,*,Auto">
<!-- Guild/channel info --> <!-- Guild/channel info -->
<Grid <Grid

View file

@ -1,4 +1,3 @@
using Avalonia.Interactivity;
using DiscordChatExporter.Gui.Framework; using DiscordChatExporter.Gui.Framework;
using DiscordChatExporter.Gui.ViewModels.Dialogs; using DiscordChatExporter.Gui.ViewModels.Dialogs;
@ -7,7 +6,4 @@ namespace DiscordChatExporter.Gui.Views.Dialogs;
public partial class ExportSetupView : UserControl<ExportSetupViewModel> public partial class ExportSetupView : UserControl<ExportSetupViewModel>
{ {
public ExportSetupView() => InitializeComponent(); public ExportSetupView() => InitializeComponent();
private void UserControl_OnLoaded(object? sender, RoutedEventArgs args) =>
DataContext.InitializeCommand.Execute(null);
} }

View file

@ -14,10 +14,7 @@
Icon="/favicon.ico" Icon="/favicon.ico"
RenderOptions.BitmapInterpolationMode="HighQuality" RenderOptions.BitmapInterpolationMode="HighQuality"
WindowStartupLocation="CenterScreen"> WindowStartupLocation="CenterScreen">
<dialogHostAvalonia:DialogHost <dialogHostAvalonia:DialogHost x:Name="DialogHost" CloseOnClickAway="False">
x:Name="DialogHost"
CloseOnClickAway="False"
Loaded="DialogHost_OnLoaded">
<materialStyles:SnackbarHost HostName="Root" SnackbarMaxCounts="3"> <materialStyles:SnackbarHost HostName="Root" SnackbarMaxCounts="3">
<ContentControl Content="{Binding Dashboard}" /> <ContentControl Content="{Binding Dashboard}" />
</materialStyles:SnackbarHost> </materialStyles:SnackbarHost>

View file

@ -1,5 +1,4 @@
using Avalonia.Interactivity; using DiscordChatExporter.Gui.Framework;
using DiscordChatExporter.Gui.Framework;
using DiscordChatExporter.Gui.ViewModels; using DiscordChatExporter.Gui.ViewModels;
namespace DiscordChatExporter.Gui.Views; namespace DiscordChatExporter.Gui.Views;
@ -7,7 +6,4 @@ namespace DiscordChatExporter.Gui.Views;
public partial class MainView : Window<MainViewModel> public partial class MainView : Window<MainViewModel>
{ {
public MainView() => InitializeComponent(); public MainView() => InitializeComponent();
private void DialogHost_OnLoaded(object? sender, RoutedEventArgs args) =>
DataContext.InitializeCommand.Execute(null);
} }