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>
<!-- Pull in shared package versions from the repository root. -->
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Packages.props', '$(MSBuildThisFileDirectory)../'))" />
<!-- PostIt-product-specific versions -->
<ItemGroup>
<PackageVersion Include="Avalonia" Version="12.0.4" />
@ -13,6 +12,7 @@
<PackageVersion Include="Avalonia.Themes.Fluent" Version="12.0.4" />
<PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.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.Core.SplashScreen" Version="1.2.0" />
</ItemGroup>

View file

@ -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<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)
{
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()

View file

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

View file

@ -28,7 +28,8 @@ public class ViewLocator : IDataTemplate
MainPageViewModel => _services.GetRequiredService<MainPage>(),
SettingsPageViewModel => _services.GetRequiredService<SettingsPage>(),
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 @@
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(); }
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 bool _LoginSuccess;
public bool LoginSuccess { get => _isBusy;
private set => this.SetProperty(ref _LoginSuccess, value); }
/// <summary>
/// Optional override used by tests. When set, this factory is called
/// instead of <see cref="Platform.CreateBrowser"/> to obtain the
@ -132,6 +136,7 @@ public partial class LoginPageViewModel : ViewModelBase
/// constructing a fresh one.
/// </summary>
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)
{

View file

@ -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 });
}
}

View file

View file

@ -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<ApplicationUser>()

View file

@ -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())