diff --git a/src/PostIt/Directory.Packages.props b/src/PostIt/Directory.Packages.props index 386d8924e..580a8e521 100644 --- a/src/PostIt/Directory.Packages.props +++ b/src/PostIt/Directory.Packages.props @@ -4,7 +4,7 @@ true - 12.1.2 + 12.1.3 @@ -13,15 +13,15 @@ - + - + - + @@ -36,4 +36,4 @@ - \ No newline at end of file + diff --git a/src/PostIt/PostIt.Desktop/Program.cs b/src/PostIt/PostIt.Desktop/Program.cs index a2b8cd66f..1c5aa0c27 100644 --- a/src/PostIt/PostIt.Desktop/Program.cs +++ b/src/PostIt/PostIt.Desktop/Program.cs @@ -1,6 +1,7 @@ using System; using Avalonia; using PostIt.Services; +using PostIt.CLIOptions; namespace PostIt.Desktop; diff --git a/src/PostIt/PostIt.Tests/SessionStatusBannerTests.cs b/src/PostIt/PostIt.Tests/SessionStatusBannerTests.cs index cc08304d5..3516e93d6 100644 --- a/src/PostIt/PostIt.Tests/SessionStatusBannerTests.cs +++ b/src/PostIt/PostIt.Tests/SessionStatusBannerTests.cs @@ -101,7 +101,7 @@ public class SessionStatusBannerTests { MainWindow window = new MainWindow(); window.Show(); - var banner = window.MainView.SessionBanner; + var banner = new SessionStatusBanner(); var status = new SessionStatusViewModel(); banner.DataContext = status; return banner; diff --git a/src/PostIt/PostIt/App.axaml.cs b/src/PostIt/PostIt/App.axaml.cs index 4769fedf3..5c986d1dd 100644 --- a/src/PostIt/PostIt/App.axaml.cs +++ b/src/PostIt/PostIt/App.axaml.cs @@ -14,15 +14,17 @@ using PostIt.Helpers; using System.Globalization; using Live.Avalonia; using System.Diagnostics; +using System.Linq; using ReactiveUI; using System.Reactive; namespace PostIt; -public partial class App : Application, ILiveView +public partial class App : Application, ILiveView, IVMPusherApp { + public static bool LiveMode { get; private set; } private int _bootStarted; - public string? CliConfigFileSpecification {  get; private set; } + public static string? CliConfigFileSpecification { get; private set; } /// /// DI container the platform entry points hand to ViewModels so @@ -49,38 +51,57 @@ public partial class App : Application, ILiveView public override void OnFrameworkInitializationCompleted() { - -#if DEBUG - if (Debugger.IsAttached && !IsProduction()) - { - // Debugging requires pdb loading etc, so we disable live reloading - // during a test run with an attached debugger. - var window = new Window(); - window.Content = CreateView(window); - window.Show(); - } - else -#endif - { if (TryHandOffCustomSchemeUrl()) return; this.ServiceProvider = new ServiceCollection().BuildPostItServices(); var settings = ServiceProvider.GetRequiredService(); - if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + if (!LiveMode) { - InitializeClassicDesktopLifetime(settings, desktop); + // Debugging requires pdb loading etc, so we disable live reloading + // during a test run with an attached debugger. + + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + InitializeClassicDesktopLifetime(settings, desktop); + } + else if (ApplicationLifetime is IActivityApplicationLifetime singleViewFactoryApplicationLifetime) + { + InitializeActivityLifetime(settings, singleViewFactoryApplicationLifetime); + } + else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform) + { + InitializeSingleViewLifetime(settings, singleViewPlatform); + } } - else if (ApplicationLifetime is IActivityApplicationLifetime singleViewFactoryApplicationLifetime) + else { - InitializeActivityLifetime(settings, singleViewFactoryApplicationLifetime); - } - else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform) - { - InitializeSingleViewLifetime(settings, singleViewPlatform); + // Here, we create a new LiveViewHost, located in the 'Live.Avalonia' + // namespace, and pass an ILiveView implementation to it. The ILiveView + // implementation should have a parameterless constructor! Next, we + // start listening for any changes in the source files. And then, we + // show the LiveViewHost window. Simple enough, huh? + var window = new LiveViewHost(this, Console.WriteLine); + var view = ServiceProvider!.GetRequiredService(); + + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + InitializeClassicDesktopLifetime(window, view, settings, desktop); + } + else if (ApplicationLifetime is IActivityApplicationLifetime singleViewFactoryApplicationLifetime) + { + InitializeActivityLifetime(settings, singleViewFactoryApplicationLifetime); + } + else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform) + { + InitializeSingleViewLifetime(settings, singleViewPlatform); + } + + window.StartWatchingSourceFilesForHotReloading(); + Console.WriteLine("LiveViewHost started for hot reloading."); + window.Show(); } - } #if DEBUG // Here we subscribe to ReactiveUI default exception handler to avoid app @@ -89,6 +110,7 @@ public partial class App : Application, ILiveView // // In case if you are using another MV* framework, please refer to its // documentation explaining global exception handling. + Console.WriteLine("Debug mode: ReactiveUI default exception handler is set."); RxApp.DefaultExceptionHandler = Observer.Create(Console.WriteLine); #endif @@ -98,12 +120,50 @@ public partial class App : Application, ILiveView private bool IsProduction() { #if DEBUG - return false; + return false; #else return true; #endif } + public async Task PushPageAsync(ViewModelBase viewModel) + { + var template = DataTemplates.FirstOrDefault(t => t.Match(viewModel)); + if (template is null) + { + throw new InvalidOperationException($"No IDataTemplate found for {viewModel.GetType().Name}."); + } + + Control? view = template.Build(viewModel) ; + if (view is null) + { + throw new InvalidOperationException( + $"Template for {viewModel.GetType().Name} returned ."); + } + + var page = view as Page; + if (page is null) + { + // NavigationPage expects Page instances. Wrap any fallback control + // (e.g. ViewLocator error TextBlock) into a ContentPage so it can render. + page = new ContentPage { Content = view }; + } + + page.DataContext = viewModel; + + MainView? mainView = (Current as App).View; + + // Avoid stacking the same singleton page twice (e.g. SettingsPage). + var stack = mainView.NavRoot.NavigationStack; + if (stack.Count > 0 && ReferenceEquals(stack[stack.Count - 1], page)) + { + return page; + } + + await mainView.NavRoot.PushAsync(page); + return page; + } + public void InitializeSingleViewLifetime(Settings settings, ISingleViewApplicationLifetime singleViewPlatform) { singleViewPlatform.MainView = View = ServiceProvider!.GetRequiredService(); @@ -125,37 +185,34 @@ public partial class App : Application, ILiveView public void InitializeClassicDesktopLifetime(Settings settings, IClassicDesktopStyleApplicationLifetime desktop) { var window = ServiceProvider!.GetRequiredService(); - desktop.MainWindow = window; - this.ConfigureRootView(window.MainView); + var mainView = (MainView)CreateView(window); + InitializeClassicDesktopLifetime(window, mainView, settings, desktop); + } + public void InitializeClassicDesktopLifetime(Window window, MainView mainView, Settings settings, IClassicDesktopStyleApplicationLifetime desktop) + { + this.ConfigureRootView(mainView); ApplyStettings(settings); + desktop.MainWindow = window; + window.Content = mainView; + window.Show(); } public void ConfigureRootView(MainView rootView) -{ - // Déclencher le Boot une seule fois lors du chargement du contrôle à l'écran. - rootView.AttachedToVisualTree += async (_, _) => await BootOnceAsync(); - - var sessionStatus = ServiceProvider!.GetRequiredService(); - sessionStatus.LogoutCompleted += () => { - // Remplacer Window.NavRoot par rootView.NavRoot - rootView.NavRoot.PopToRootAsync(); - }; + rootView.AttachedToVisualTree += async (_, _) => await BootOnceAsync(); - rootView.SessionBanner.DataContext = sessionStatus; - this.View = rootView; -} - - private async Task BootOnceAsync() - { - if (Interlocked.Exchange(ref _bootStarted, 1) == 1) + var sessionStatus = ServiceProvider!.GetRequiredService(); + sessionStatus.LogoutCompleted += () => { - return; - } + // Remplacer Window.NavRoot par rootView.NavRoot + rootView.NavRoot.PopToRootAsync(); + }; + if (rootView.DataContext==null) + rootView.DataContext = ServiceProvider!.GetRequiredService(); - var api = ServiceProvider!.GetRequiredService(); - await BootAsync(this.ServiceProvider!, api); + rootView.SessionBanner.DataContext = sessionStatus; + this.View = rootView; } /// @@ -177,6 +234,18 @@ public partial class App : Application, ILiveView settings.DarkMode ? ThemeVariant.Dark : ThemeVariant.Light; } + + private async Task BootOnceAsync() + { + if (Interlocked.Exchange(ref _bootStarted, 1) == 1) + { + return; + } + + var api = ServiceProvider!.GetRequiredService(); + await BootAsync(this.ServiceProvider!, api); + } + /// /// Run once after the main window is shown: try to refresh the /// cached OIDC tokens silently; on success, push MainPage on top @@ -192,9 +261,9 @@ public partial class App : Application, ILiveView var refreshed = await api.TrySilentLoginAsync().ConfigureAwait(true); var sessionStatus = provider.GetRequiredService(); sessionStatus.Refresh(); - var homePage = provider.GetRequiredService(); + var homePageVm = provider.GetRequiredService(); var app = (App)Current!; - await app.PushPageAsync(homePage); + await app.PushPageAsync(homePageVm); } /// @@ -250,20 +319,25 @@ public partial class App : Application, ILiveView await View!.NavRoot.PopAsync(); } - internal void UseConfigFileWhenLoading(string configFile) + internal static void UseConfigFileWhenLoading(string configFile) { - this.CliConfigFileSpecification = configFile; + CliConfigFileSpecification = configFile; + } + + internal static void EnableLiveMode() + { + LiveMode = true; } public object CreateView(Window window) { - if (this.ServiceProvider==null) + if (this.ServiceProvider == null) { var serviceCollection = new ServiceCollection(); this.ServiceProvider = serviceCollection.BuildPostItServices(); } if (window.DataContext == null) - window.DataContext = ServiceProvider!.GetRequiredService(); + window.DataContext = ServiceProvider!.GetRequiredService(); // The AppView class will inherit the DataContext // of the window. The AppView class can be a diff --git a/src/PostIt/PostIt/CLIOptions/CLIOptions.cs b/src/PostIt/PostIt/CLIOptions/CLIOptions.cs index c0bea85ff..643102b38 100644 --- a/src/PostIt/PostIt/CLIOptions/CLIOptions.cs +++ b/src/PostIt/PostIt/CLIOptions/CLIOptions.cs @@ -4,15 +4,21 @@ using Mono.Options; using PostIt; using PostIt.ViewModels; +namespace PostIt.CLIOptions; + public static class CLIOptionHandler { public static void HandleArgs(string[] args) { bool showHelp = false; string configFile = null; + bool liveMode = false; var options = new OptionSet { { "c|configuration=", "Configuration file to load", (string v) => configFile = v }, + { "l|live", "Enable live mode", v => liveMode = true }, + { "v|version", "Show the application version", v => Console.WriteLine("PostIt version " + + typeof(App).Assembly.GetName().Version?.ToString()) }, { "h|help", "Show this message and exit", v => showHelp = true } }; @@ -26,8 +32,11 @@ public static class CLIOptionHandler } if (configFile != null) { - var app = App.Current as App; - app.UseConfigFileWhenLoading(configFile); + App.UseConfigFileWhenLoading(configFile); + } + if (liveMode) + { + App.EnableLiveMode(); } } } diff --git a/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs b/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs index 55dad555b..778f2c4ed 100644 --- a/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs +++ b/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs @@ -39,9 +39,12 @@ public static class ServiceCollectionHelpers var reverseGeocoding = new NominatimReverseGeocodingService(); // Vues - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + + // Models services.AddSingleton(); // SettingsPage is a singleton: there must be one and only one diff --git a/src/PostIt/PostIt/Helpers/ViewModelBaseHelpers.cs b/src/PostIt/PostIt/Helpers/ViewModelBaseHelpers.cs deleted file mode 100644 index f30e021d9..000000000 --- a/src/PostIt/PostIt/Helpers/ViewModelBaseHelpers.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using Avalonia.Controls; -using PostIt.ViewModels; - -namespace PostIt.Helpers; - -public static class ViewModelBaseHelpers -{ - public static async Task PushPageAsync(this App app, ViewModelBase vm) - { - var window = app.View; - if (window is null) - { - throw new InvalidOperationException("MainWindow is not initialized yet."); - } - - var template = app.DataTemplates.FirstOrDefault(t => t.Match(vm)); - if (template is null) - { - throw new InvalidOperationException($"No IDataTemplate found for {vm.GetType().Name}."); - } - - var view = template.Build(vm); - if (view is null) - { - throw new InvalidOperationException( - $"Template for {vm.GetType().Name} returned ."); - } - - var page = view as Page; - if (page is null) - { - // NavigationPage expects Page instances. Wrap any fallback control - // (e.g. ViewLocator error TextBlock) into a ContentPage so it can render. - page = new ContentPage { Content = view }; - } - - page.DataContext = vm; - - // Avoid stacking the same singleton page twice (e.g. SettingsPage). - var stack = window.NavRoot.NavigationStack; - if (stack.Count > 0 && ReferenceEquals(stack[stack.Count - 1], page)) - { - return page; - } - - await window.NavRoot.PushAsync(page); - - return page; - } -} diff --git a/src/PostIt/PostIt/IVMPusherApp.cs b/src/PostIt/PostIt/IVMPusherApp.cs new file mode 100644 index 000000000..3fc0f474f --- /dev/null +++ b/src/PostIt/PostIt/IVMPusherApp.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using PostIt.ViewModels; +using Avalonia.Controls; + +namespace PostIt; + +internal interface IVMPusherApp +{ + Task PushPageAsync(ViewModelBase viewModel); +} diff --git a/src/PostIt/PostIt/Lang/Resources.Designer.cs b/src/PostIt/PostIt/Lang/Resources.Designer.cs index 5537f5924..7d600fd2a 100644 --- a/src/PostIt/PostIt/Lang/Resources.Designer.cs +++ b/src/PostIt/PostIt/Lang/Resources.Designer.cs @@ -61,7 +61,7 @@ namespace Yavsc.Lang { } /// - /// Looks up a localized string similar to Bienvenue dans PostIt!. + /// Looks up a localized string similar to Bienvenue dans PostIt !!!. /// public static string GreetingText { get { diff --git a/src/PostIt/PostIt/Lang/Resources.resx b/src/PostIt/PostIt/Lang/Resources.resx index ae18ff220..574138ef6 100644 --- a/src/PostIt/PostIt/Lang/Resources.resx +++ b/src/PostIt/PostIt/Lang/Resources.resx @@ -1,5 +1,5 @@ - Bienvenue dans PostIt! + Bienvenue dans PostIt !!! diff --git a/src/PostIt/PostIt/ViewModels/Layout/HomePageViewModel.cs b/src/PostIt/PostIt/ViewModels/Layout/HomePageViewModel.cs index 19ed05833..b76f31f27 100644 --- a/src/PostIt/PostIt/ViewModels/Layout/HomePageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/Layout/HomePageViewModel.cs @@ -3,7 +3,6 @@ using System.Threading.Tasks; using Avalonia; using CommunityToolkit.Mvvm.Input; using Microsoft.Extensions.DependencyInjection; -using PostIt.Helpers; using PostIt.Services; using PostIt.ViewModels.Chat; using Yavsc.Api.Client; diff --git a/src/PostIt/PostIt/ViewModels/Settings/Settings.cs b/src/PostIt/PostIt/ViewModels/Settings/Settings.cs index e92bf9385..c034437b8 100644 --- a/src/PostIt/PostIt/ViewModels/Settings/Settings.cs +++ b/src/PostIt/PostIt/ViewModels/Settings/Settings.cs @@ -271,10 +271,9 @@ public partial class Settings : ViewModelBase } var app = App.Current as App; - if (app!=null) - if (app.CliConfigFileSpecification!=null) + if (App.CliConfigFileSpecification!=null) { - SettingsFileFullName = app.CliConfigFileSpecification; + SettingsFileFullName = App.CliConfigFileSpecification; } if (SettingsFileFullName is not null) diff --git a/src/PostIt/PostIt/Views/Chat/ChatPage..axaml b/src/PostIt/PostIt/Views/Chat/ChatPage.axaml similarity index 69% rename from src/PostIt/PostIt/Views/Chat/ChatPage..axaml rename to src/PostIt/PostIt/Views/Chat/ChatPage.axaml index d6b77f2d7..0ac7e818c 100644 --- a/src/PostIt/PostIt/Views/Chat/ChatPage..axaml +++ b/src/PostIt/PostIt/Views/Chat/ChatPage.axaml @@ -14,20 +14,6 @@ - - - - -