From ee2c8452ac5378af342f8590514938b79453fad3 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Fri, 26 Jun 2026 01:45:21 +0100 Subject: [PATCH] Navigation and DI --- src/PostIt/Directory.Packages.props | 2 +- src/PostIt/PostIt/App.axaml.cs | 70 ++++++++++--------- src/PostIt/PostIt/PostIt.csproj | 1 + src/PostIt/PostIt/ViewLocator.cs | 5 +- .../PostIt/ViewModels/HomePageViewModel.cs | 19 ++++- .../PostIt/ViewModels/LoginPageViewModel.cs | 9 ++- src/PostIt/PostIt/Views/HomePage.axaml.cs | 23 +++--- src/PostIt/PostIt/Views/InitialPage.cs | 0 src/Yavsc.Org/Extensions/HostingExtensions.cs | 7 +- src/Yavsc.Web/Program.cs | 7 +- 10 files changed, 87 insertions(+), 56 deletions(-) create mode 100644 src/PostIt/PostIt/Views/InitialPage.cs diff --git a/src/PostIt/Directory.Packages.props b/src/PostIt/Directory.Packages.props index 4855cad3..12ed35df 100644 --- a/src/PostIt/Directory.Packages.props +++ b/src/PostIt/Directory.Packages.props @@ -1,7 +1,6 @@ - @@ -13,6 +12,7 @@ + diff --git a/src/PostIt/PostIt/App.axaml.cs b/src/PostIt/PostIt/App.axaml.cs index 5454d478..9457c8b0 100644 --- a/src/PostIt/PostIt/App.axaml.cs +++ b/src/PostIt/PostIt/App.axaml.cs @@ -1,8 +1,11 @@ using System; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Avalonia; +using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; +using Microsoft.Extensions.DependencyInjection; using PostIt.Services; using PostIt.ViewModels; using PostIt.Views; @@ -22,49 +25,52 @@ public partial class App : Application public override void OnFrameworkInitializationCompleted() { - // Single-instance hand-off: if we were launched with a - // custom-scheme URL on the command line, we are a 2nd - // instance whose job is to forward the OAuth2 callback - // URL to the running PostIt process and exit. The first - // instance is parked inside CustomSchemeBrowser.InvokeAsync - // waiting on the named pipe for exactly this message. - if (TryHandOffCustomSchemeUrl()) - { - return; - } + if (TryHandOffCustomSchemeUrl()) return; + var settings = new Settings(); - // Synchronous: Settings.Load is intentionally non-async so we - // don't deadlock the Avalonia UI thread. .Wait() on an async - // method would block here forever on the await inside the - // file read. settings.Load(); var tokenStore = new TokenStore(System.IO.Path.Combine( System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData), "PostIt", "tokens.json")); - var client = new BlogApiClient(new YavscApiClient(settings, tokenStore)); + + var api = new YavscApiClient(settings, tokenStore); + var client = new BlogApiClient(api); + + // Configure DI + var services = new ServiceCollection(); + + // Vues + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + // ViewModels + services.AddSingleton(settings); + services.AddSingleton(api); + services.AddSingleton(client); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + var provider = services.BuildServiceProvider(); + + // Injecter le ViewLocator avec le provider + DataTemplates.Clear(); + DataTemplates.Add(new ViewLocator(provider)); + + // Page de départ + var homeVm = provider.GetRequiredService(); + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - desktop.MainWindow = new MainWindow - { - DataContext = new MainPageViewModel(client, settings) - }; + desktop.MainWindow = new MainWindow { DataContext = homeVm }; } - else if (ApplicationLifetime is IActivityApplicationLifetime singleViewFactoryApplicationLifetime) + else if (ApplicationLifetime is ISingleViewApplicationLifetime singleView) { - singleViewFactoryApplicationLifetime.MainViewFactory = () => - { - return new MainPage { DataContext = new MainPageViewModel(client, settings) }; - }; + singleView.MainView = new MainWindow { DataContext = homeVm }; } - else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform) - { - singleViewPlatform.MainView = new MainPage - { - DataContext = new MainPageViewModel(client, settings) - }; - } - } private bool TryHandOffCustomSchemeUrl() diff --git a/src/PostIt/PostIt/PostIt.csproj b/src/PostIt/PostIt/PostIt.csproj index 13645f88..50563e36 100644 --- a/src/PostIt/PostIt/PostIt.csproj +++ b/src/PostIt/PostIt/PostIt.csproj @@ -19,6 +19,7 @@ + diff --git a/src/PostIt/PostIt/ViewLocator.cs b/src/PostIt/PostIt/ViewLocator.cs index c6b66dfa..9a05aa84 100644 --- a/src/PostIt/PostIt/ViewLocator.cs +++ b/src/PostIt/PostIt/ViewLocator.cs @@ -28,9 +28,10 @@ public class ViewLocator : IDataTemplate MainPageViewModel => _services.GetRequiredService(), SettingsPageViewModel => _services.GetRequiredService(), LoginPageViewModel => _services.GetRequiredService(), - _ => new TextBlock { Text = $"No view for {data.GetType().Name}" } + HomePageViewModel => _services.GetRequiredService(), + _ => new TextBlock { Text = $"No view for {data.GetType().Name}" } }; } public bool Match(object data) => data is ViewModelBase; -} \ No newline at end of file +} diff --git a/src/PostIt/PostIt/ViewModels/HomePageViewModel.cs b/src/PostIt/PostIt/ViewModels/HomePageViewModel.cs index d56711af..e8e76a30 100644 --- a/src/PostIt/PostIt/ViewModels/HomePageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/HomePageViewModel.cs @@ -1,15 +1,28 @@ - -namespace PostIt.ViewModels; +using PostIt; +using PostIt.Services; +using PostIt.ViewModels; public class HomePageViewModel : ViewModelBase { + public YavscApiClient Api { get; } + public Settings Settings { get; } + private string _welcomeText = "Welcome to PostIt!"; public string WelcomeText { get => _welcomeText; set => SetProperty(ref _welcomeText, value); } + public override bool CanNavigateNext { get => true; protected set => throw new System.NotImplementedException(); } public override bool CanNavigatePrevious { get => false; protected set => throw new System.NotImplementedException(); } -} \ No newline at end of file + public HomePageViewModel(YavscApiClient api, Settings settings) + { + Api = api; + Settings = settings; + } + + // Constructeur sans arg pour le designer Avalonia + public HomePageViewModel() : this(null!, null!) { } +} diff --git a/src/PostIt/PostIt/ViewModels/LoginPageViewModel.cs b/src/PostIt/PostIt/ViewModels/LoginPageViewModel.cs index 84c0ba30..cf8c27a7 100644 --- a/src/PostIt/PostIt/ViewModels/LoginPageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/LoginPageViewModel.cs @@ -111,6 +111,10 @@ public partial class LoginPageViewModel : ViewModelBase private set => this.SetProperty(ref _isBusy, value); } + private bool _LoginSuccess; + public bool LoginSuccess { get => _isBusy; + private set => this.SetProperty(ref _LoginSuccess, value); } + /// /// Optional override used by tests. When set, this factory is called /// instead of to obtain the @@ -132,6 +136,7 @@ public partial class LoginPageViewModel : ViewModelBase /// constructing a fresh one. /// public YavscApiClient? ApiClientOverride { get; set; } + public Action LoginSucceeded { get; internal set; } private YavscApiClient? _api; @@ -171,7 +176,7 @@ public partial class LoginPageViewModel : ViewModelBase try { IsBusy = true; - + LoginSuccess = false; if (SettingsLoadOverride is not null) await SettingsLoadOverride().ConfigureAwait(false); else @@ -222,6 +227,8 @@ public partial class LoginPageViewModel : ViewModelBase IsBusy = false; AccessToken = _api.CurrentAccessToken; StatusMessage = "Interactive token acquired."; + LoginSuccess = true; + LoginSucceeded?.Invoke(); } catch (Exception ex) { diff --git a/src/PostIt/PostIt/Views/HomePage.axaml.cs b/src/PostIt/PostIt/Views/HomePage.axaml.cs index 8b32847d..f2750e82 100644 --- a/src/PostIt/PostIt/Views/HomePage.axaml.cs +++ b/src/PostIt/PostIt/Views/HomePage.axaml.cs @@ -1,8 +1,8 @@ -using Avalonia; using Avalonia.Controls; using Avalonia.Interactivity; -using System.Threading.Tasks; +using PostIt.Services; +using PostIt.ViewModels; namespace PostIt.Views; @@ -13,11 +13,18 @@ public partial class HomePage : ContentPage InitializeComponent(); } - private async void OnLoginClick(object? sender, RoutedEventArgs e) + private void OnLoginClick(object? sender, RoutedEventArgs e) { - if (Navigation is not null) - - await Navigation.PushModalAsync(new LoginPage()); + var vm = (HomePageViewModel)DataContext!; + var loginVm = new LoginPageViewModel(vm.Settings, apiClient: vm.Api); + loginVm.LoginSucceeded += () => + { + var client = new BlogApiClient(vm.Api); + Navigation?.PushAsync(new MainPage + { + DataContext = new MainPageViewModel(client, vm.Settings) + }); + }; + Navigation?.PushAsync(new LoginPage { DataContext = loginVm }); } - -} \ No newline at end of file +} diff --git a/src/PostIt/PostIt/Views/InitialPage.cs b/src/PostIt/PostIt/Views/InitialPage.cs new file mode 100644 index 00000000..e69de29b diff --git a/src/Yavsc.Org/Extensions/HostingExtensions.cs b/src/Yavsc.Org/Extensions/HostingExtensions.cs index 7cca271e..b62a6567 100644 --- a/src/Yavsc.Org/Extensions/HostingExtensions.cs +++ b/src/Yavsc.Org/Extensions/HostingExtensions.cs @@ -19,20 +19,15 @@ using Microsoft.AspNetCore.Localization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.OpenSsl; using Microsoft.IdentityModel.Tokens; using Org.BouncyCastle.Security; -using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Localization; -using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; using Newtonsoft.Json; using Yavsc.Helpers; @@ -44,7 +39,6 @@ using Yavsc.Services; using Yavsc.Services.Kyc; using Yavsc.Settings; using Yavsc.ViewModels.Auth; -using static IdentityServer8.IdentityServerConstants; using IdentityServer8.Models; using IdentityServer8.EntityFramework.Mappers; @@ -302,6 +296,7 @@ public static class HostingExtensions // see https://IdentityServer8.readthedocs.io/en/latest/topics/resources.html options.EmitStaticAudienceClaim = true; + options.UserInteraction.LoginUrl = "/signin"; }) .AddAspNetIdentity() diff --git a/src/Yavsc.Web/Program.cs b/src/Yavsc.Web/Program.cs index 5f8065f4..4bb15ca9 100644 --- a/src/Yavsc.Web/Program.cs +++ b/src/Yavsc.Web/Program.cs @@ -1,10 +1,10 @@ /* - Copyright (c) 2024 HigginsSoft, Alexander Higgins - https://github.com/alexhiggins732/ + Copyright (c) 2024 HigginsSoft, Alexander Higgins - https://github.com/alexhiggins732/ Copyright (c) 2018, Brock Allen & Dominick Baier. All rights reserved. - Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. - Source code and license this software can be found + Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + Source code and license this software can be found The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. @@ -50,6 +50,7 @@ builder.Services options.ClaimActions.MapUniqueJsonKey("preferred_username", "preferred_username"); options.ClaimActions.MapUniqueJsonKey("gender", "gender"); options.SaveTokens = true; + }); using (var app = builder.Build())