Navigation and DI

This commit is contained in:
Paul Schneider 2026-06-26 01:45:21 +01:00
commit ee2c8452ac
10 changed files with 87 additions and 56 deletions

View file

@ -1,7 +1,6 @@
<Project> <Project>
<!-- Pull in shared package versions from the repository root. --> <!-- Pull in shared package versions from the repository root. -->
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Packages.props', '$(MSBuildThisFileDirectory)../'))" /> <Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Packages.props', '$(MSBuildThisFileDirectory)../'))" />
<!-- PostIt-product-specific versions --> <!-- PostIt-product-specific versions -->
<ItemGroup> <ItemGroup>
<PackageVersion Include="Avalonia" Version="12.0.4" /> <PackageVersion Include="Avalonia" Version="12.0.4" />
@ -13,6 +12,7 @@
<PackageVersion Include="Avalonia.Themes.Fluent" Version="12.0.4" /> <PackageVersion Include="Avalonia.Themes.Fluent" Version="12.0.4" />
<PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.2" /> <PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.2" />
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.2" /> <PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.2" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.9" />
<PackageVersion Include="Xamarin.AndroidX.Browser" Version="1.8.0" /> <PackageVersion Include="Xamarin.AndroidX.Browser" Version="1.8.0" />
<PackageVersion Include="Xamarin.AndroidX.Core.SplashScreen" Version="1.2.0" /> <PackageVersion Include="Xamarin.AndroidX.Core.SplashScreen" Version="1.2.0" />
</ItemGroup> </ItemGroup>

View file

@ -1,8 +1,11 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Avalonia; using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Microsoft.Extensions.DependencyInjection;
using PostIt.Services; using PostIt.Services;
using PostIt.ViewModels; using PostIt.ViewModels;
using PostIt.Views; using PostIt.Views;
@ -22,49 +25,52 @@ public partial class App : Application
public override void OnFrameworkInitializationCompleted() public override void OnFrameworkInitializationCompleted()
{ {
// Single-instance hand-off: if we were launched with a if (TryHandOffCustomSchemeUrl()) return;
// 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;
}
var settings = new Settings(); 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(); settings.Load();
var tokenStore = new TokenStore(System.IO.Path.Combine( var tokenStore = new TokenStore(System.IO.Path.Combine(
System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData), System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData),
"PostIt", "tokens.json")); "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<MainPage>();
services.AddTransient<LoginPage>();
services.AddTransient<SettingsPage>();
services.AddTransient<HomePage>();
// ViewModels
services.AddSingleton(settings);
services.AddSingleton(api);
services.AddSingleton(client);
services.AddTransient<MainPageViewModel>();
services.AddTransient<SettingsPageViewModel>();
services.AddTransient<LoginPageViewModel>();
services.AddTransient<HomePageViewModel>();
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<HomePageViewModel>();
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{ {
desktop.MainWindow = new MainWindow desktop.MainWindow = new MainWindow { DataContext = homeVm };
{
DataContext = new MainPageViewModel(client, settings)
};
} }
else if (ApplicationLifetime is IActivityApplicationLifetime singleViewFactoryApplicationLifetime) else if (ApplicationLifetime is ISingleViewApplicationLifetime singleView)
{ {
singleViewFactoryApplicationLifetime.MainViewFactory = () => singleView.MainView = new MainWindow { DataContext = homeVm };
{
return new MainPage { DataContext = new MainPageViewModel(client, settings) };
};
} }
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
{
singleViewPlatform.MainView = new MainPage
{
DataContext = new MainPageViewModel(client, settings)
};
}
} }
private bool TryHandOffCustomSchemeUrl() private bool TryHandOffCustomSchemeUrl()

View file

@ -19,6 +19,7 @@
</PackageReference> </PackageReference>
<PackageReference Include="CommunityToolkit.Mvvm" /> <PackageReference Include="CommunityToolkit.Mvvm" />
<PackageReference Include="IdentityModel.OidcClient" /> <PackageReference Include="IdentityModel.OidcClient" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<ProjectReference Include="../../Yavsc.Abstract/Yavsc.Abstract.csproj" /> <ProjectReference Include="../../Yavsc.Abstract/Yavsc.Abstract.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -28,7 +28,8 @@ public class ViewLocator : IDataTemplate
MainPageViewModel => _services.GetRequiredService<MainPage>(), MainPageViewModel => _services.GetRequiredService<MainPage>(),
SettingsPageViewModel => _services.GetRequiredService<SettingsPage>(), SettingsPageViewModel => _services.GetRequiredService<SettingsPage>(),
LoginPageViewModel => _services.GetRequiredService<LoginPage>(), LoginPageViewModel => _services.GetRequiredService<LoginPage>(),
_ => new TextBlock { Text = $"No view for {data.GetType().Name}" } HomePageViewModel => _services.GetRequiredService<HomePage>(),
_ => new TextBlock { Text = $"No view for {data.GetType().Name}" }
}; };
} }

View file

@ -1,15 +1,28 @@
using PostIt;
namespace PostIt.ViewModels; using PostIt.Services;
using PostIt.ViewModels;
public class HomePageViewModel : ViewModelBase public class HomePageViewModel : ViewModelBase
{ {
public YavscApiClient Api { get; }
public Settings Settings { get; }
private string _welcomeText = "Welcome to PostIt!"; private string _welcomeText = "Welcome to PostIt!";
public string WelcomeText public string WelcomeText
{ {
get => _welcomeText; get => _welcomeText;
set => SetProperty(ref _welcomeText, value); set => SetProperty(ref _welcomeText, value);
} }
public override bool CanNavigateNext { get => true; protected set => throw new System.NotImplementedException(); } public override bool CanNavigateNext { get => true; protected set => throw new System.NotImplementedException(); }
public override bool CanNavigatePrevious { get => false; protected set => throw new System.NotImplementedException(); } public override bool CanNavigatePrevious { get => false; protected set => throw new System.NotImplementedException(); }
public HomePageViewModel(YavscApiClient api, Settings settings)
{
Api = api;
Settings = settings;
}
// Constructeur sans arg pour le designer Avalonia
public HomePageViewModel() : this(null!, null!) { }
} }

View file

@ -111,6 +111,10 @@ public partial class LoginPageViewModel : ViewModelBase
private set => this.SetProperty(ref _isBusy, value); private set => this.SetProperty(ref _isBusy, value);
} }
private bool _LoginSuccess;
public bool LoginSuccess { get => _isBusy;
private set => this.SetProperty(ref _LoginSuccess, value); }
/// <summary> /// <summary>
/// Optional override used by tests. When set, this factory is called /// Optional override used by tests. When set, this factory is called
/// instead of <see cref="Platform.CreateBrowser"/> to obtain the /// instead of <see cref="Platform.CreateBrowser"/> to obtain the
@ -132,6 +136,7 @@ public partial class LoginPageViewModel : ViewModelBase
/// constructing a fresh one. /// constructing a fresh one.
/// </summary> /// </summary>
public YavscApiClient? ApiClientOverride { get; set; } public YavscApiClient? ApiClientOverride { get; set; }
public Action LoginSucceeded { get; internal set; }
private YavscApiClient? _api; private YavscApiClient? _api;
@ -171,7 +176,7 @@ public partial class LoginPageViewModel : ViewModelBase
try try
{ {
IsBusy = true; IsBusy = true;
LoginSuccess = false;
if (SettingsLoadOverride is not null) if (SettingsLoadOverride is not null)
await SettingsLoadOverride().ConfigureAwait(false); await SettingsLoadOverride().ConfigureAwait(false);
else else
@ -222,6 +227,8 @@ public partial class LoginPageViewModel : ViewModelBase
IsBusy = false; IsBusy = false;
AccessToken = _api.CurrentAccessToken; AccessToken = _api.CurrentAccessToken;
StatusMessage = "Interactive token acquired."; StatusMessage = "Interactive token acquired.";
LoginSuccess = true;
LoginSucceeded?.Invoke();
} }
catch (Exception ex) catch (Exception ex)
{ {

View file

@ -1,8 +1,8 @@
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using System.Threading.Tasks; using PostIt.Services;
using PostIt.ViewModels;
namespace PostIt.Views; namespace PostIt.Views;
@ -13,11 +13,18 @@ public partial class HomePage : ContentPage
InitializeComponent(); InitializeComponent();
} }
private async void OnLoginClick(object? sender, RoutedEventArgs e) private void OnLoginClick(object? sender, RoutedEventArgs e)
{ {
if (Navigation is not null) var vm = (HomePageViewModel)DataContext!;
var loginVm = new LoginPageViewModel(vm.Settings, apiClient: vm.Api);
await Navigation.PushModalAsync(new LoginPage()); loginVm.LoginSucceeded += () =>
{
var client = new BlogApiClient(vm.Api);
Navigation?.PushAsync(new MainPage
{
DataContext = new MainPageViewModel(client, vm.Settings)
});
};
Navigation?.PushAsync(new LoginPage { DataContext = loginVm });
} }
} }

View file

View file

@ -19,20 +19,15 @@ using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.OpenSsl; using Org.BouncyCastle.OpenSsl;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using Org.BouncyCastle.Security; using Org.BouncyCastle.Security;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
using Newtonsoft.Json; using Newtonsoft.Json;
using Yavsc.Helpers; using Yavsc.Helpers;
@ -44,7 +39,6 @@ using Yavsc.Services;
using Yavsc.Services.Kyc; using Yavsc.Services.Kyc;
using Yavsc.Settings; using Yavsc.Settings;
using Yavsc.ViewModels.Auth; using Yavsc.ViewModels.Auth;
using static IdentityServer8.IdentityServerConstants;
using IdentityServer8.Models; using IdentityServer8.Models;
using IdentityServer8.EntityFramework.Mappers; using IdentityServer8.EntityFramework.Mappers;
@ -302,6 +296,7 @@ public static class HostingExtensions
// see https://IdentityServer8.readthedocs.io/en/latest/topics/resources.html // see https://IdentityServer8.readthedocs.io/en/latest/topics/resources.html
options.EmitStaticAudienceClaim = true; options.EmitStaticAudienceClaim = true;
options.UserInteraction.LoginUrl = "/signin";
}) })
.AddAspNetIdentity<ApplicationUser>() .AddAspNetIdentity<ApplicationUser>()

View file

@ -50,6 +50,7 @@ builder.Services
options.ClaimActions.MapUniqueJsonKey("preferred_username", "preferred_username"); options.ClaimActions.MapUniqueJsonKey("preferred_username", "preferred_username");
options.ClaimActions.MapUniqueJsonKey("gender", "gender"); options.ClaimActions.MapUniqueJsonKey("gender", "gender");
options.SaveTokens = true; options.SaveTokens = true;
}); });
using (var app = builder.Build()) using (var app = builder.Build())