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,9 +28,10 @@ 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}" }
};
}
public bool Match(object data) => data is ViewModelBase;
}
}

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

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