for the refact, not the result ... Live still is KO
Some checks failed
Dotnet build and test / build (push) Failing after 13m0s

This commit is contained in:
Paul Schneider 2026-10-01 19:27:38 +01:00
commit 251cc9cd48
Signed by: notazof
GPG key ID: AD4B9B545FBD7D12
15 changed files with 170 additions and 142 deletions

View file

@ -4,7 +4,7 @@
<!-- https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management -->
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<AvaloniaVersionBase>12.1.2</AvaloniaVersionBase>
<AvaloniaVersionBase>12.1.3</AvaloniaVersionBase>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Avalonia" Version="$(AvaloniaVersionBase)" />
@ -13,15 +13,15 @@
<PackageVersion Include="Avalonia.Desktop" Version="$(AvaloniaVersionBase)" />
<PackageVersion Include="Avalonia.Browser" Version="$(AvaloniaVersionBase)" />
<PackageVersion Include="Avalonia.Android" Version="$(AvaloniaVersionBase)" />
<PackageVersion Include="Avalonia.Headless" Version="12.1.2" />
<PackageVersion Include="Avalonia.Headless" Version="12.1.3" />
<PackageVersion Include="Avalonia.Headless.Xunit" Version="$(AvaloniaVersionBase)" />
<PackageVersion Include="Avalonia.Fonts.Inter" Version="$(AvaloniaVersionBase)" />
<PackageVersion Include="LibVLCSharp.Avalonia" Version="3.10.1" />
<PackageVersion Include="Live.Avalonia" Version="1.4.1" />
<PackageVersion Include="Live.Avalonia" Version="1.4.2" />
<PackageVersion Include="Mapsui" Version="5.1.0" />
<PackageVersion Include="Mapsui.Avalonia12" Version="5.1.0" />
<PackageVersion Include="MarkdownViewer.Core" Version="1.0.5" />
<PackageVersion Include="Material.Avalonia" Version="3.20.0" />
<PackageVersion Include="Material.Avalonia" Version="3.21.0" />
<PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.3" />
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.2" />
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.12" />
@ -36,4 +36,4 @@
<PackageVersion Include="Xamarin.UITest" Version="4.4.2" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.12" />
</ItemGroup>
</Project>
</Project>

View file

@ -1,6 +1,7 @@
using System;
using Avalonia;
using PostIt.Services;
using PostIt.CLIOptions;
namespace PostIt.Desktop;

View file

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

View file

@ -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; }
/// <summary>
/// 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<Settings>();
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<MainView>();
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<Exception>(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<Page> 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 <null>.");
}
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<MainView>();
@ -125,37 +185,34 @@ public partial class App : Application, ILiveView
public void InitializeClassicDesktopLifetime(Settings settings, IClassicDesktopStyleApplicationLifetime desktop)
{
var window = ServiceProvider!.GetRequiredService<MainWindow>();
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<SessionStatusViewModel>();
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<SessionStatusViewModel>();
sessionStatus.LogoutCompleted += () =>
{
return;
}
// Remplacer Window.NavRoot par rootView.NavRoot
rootView.NavRoot.PopToRootAsync();
};
if (rootView.DataContext==null)
rootView.DataContext = ServiceProvider!.GetRequiredService<HomePageViewModel>();
var api = ServiceProvider!.GetRequiredService<YavscApiClient>();
await BootAsync(this.ServiceProvider!, api);
rootView.SessionBanner.DataContext = sessionStatus;
this.View = rootView;
}
/// <summary>
@ -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<YavscApiClient>();
await BootAsync(this.ServiceProvider!, api);
}
/// <summary>
/// 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<SessionStatusViewModel>();
sessionStatus.Refresh();
var homePage = provider.GetRequiredService<HomePageViewModel>();
var homePageVm = provider.GetRequiredService<HomePageViewModel>();
var app = (App)Current!;
await app.PushPageAsync(homePage);
await app.PushPageAsync(homePageVm);
}
/// <summary>
@ -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<HomePageViewModel>();
window.DataContext = ServiceProvider!.GetRequiredService<HomePageViewModel>();
// The AppView class will inherit the DataContext
// of the window. The AppView class can be a

View file

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

View file

@ -39,9 +39,12 @@ public static class ServiceCollectionHelpers
var reverseGeocoding = new NominatimReverseGeocodingService();
// Vues
services.AddSingleton<MainView>();
services.AddSingleton<BlogsPage>();
services.AddSingleton<MainWindow>();
services.AddTransient<MainWindow>();
services.AddTransient<MainView>();
services.AddTransient<BlogsPage>();
// Models
services.AddSingleton<ChatViewModel>();
// SettingsPage is a singleton: there must be one and only one

View file

@ -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<Page> 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 <null>.");
}
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;
}
}

View file

@ -0,0 +1,10 @@
using System.Threading.Tasks;
using PostIt.ViewModels;
using Avalonia.Controls;
namespace PostIt;
internal interface IVMPusherApp
{
Task<Page> PushPageAsync(ViewModelBase viewModel);
}

View file

@ -61,7 +61,7 @@ namespace Yavsc.Lang {
}
/// <summary>
/// Looks up a localized string similar to Bienvenue dans PostIt!.
/// Looks up a localized string similar to Bienvenue dans PostIt !!!.
/// </summary>
public static string GreetingText {
get {

View file

@ -1,5 +1,5 @@
<root>
<data name="GreetingText">
<value>Bienvenue dans PostIt!</value>
<value>Bienvenue dans PostIt !!!</value>
</data>
</root>

View file

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

View file

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

View file

@ -14,20 +14,6 @@
<Grid RowDefinitions="Auto, *, Auto" Margin="10">
<!-- SECTION 1 : BARRE DE CONNEXION & PSEUDO -->
<Border Grid.Row="0" Background="#F5F5F5" CornerRadius="5" Padding="10" Margin="0,0,0,10">
<StackPanel Orientation="Horizontal" Spacing="10">
<TextBox Text="{Binding ServerUrl}" Watermark="URL du Hub SignalR" Width="250" VerticalAlignment="Center"/>
<Button Content="Connexion" Command="{Binding ConnectCommand}" IsEnabled="{Binding !IsConnected}"/>
<Separator Background="Gray" Width="1" Margin="5,0"/>
<TextBox Text="{Binding NickName}" Watermark="Nouveau Pseudo" Width="150" VerticalAlignment="Center"/>
<Button Content="Changer Nick" Command="{Binding ChangeNickCommand}"/>
</StackPanel>
</Border>
<!-- SECTION 2 : ESPACE CENTRAL DU CHAT -->
<Grid Grid.Row="1" ColumnDefinitions="*, 200" Margin="0,0,0,10">
<!-- Journal des flux de messages & notifications -->
@ -37,7 +23,7 @@
<Button Content="Rejoindre Salon (Join)" Command="{Binding JoinRoomCommand}"/>
</StackPanel>
<ListBox Grid.Row="1" ItemsSource="{Binding Messages}" Background="#FAFAFA" BorderBrush="#E0E0E0" BorderThickness="1">
<ListBox Grid.Row="1" ItemsSource="{Binding Messages}" BorderBrush="#E0E0E0" BorderThickness="1">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" TextWrapping="Wrap"/>
@ -47,7 +33,7 @@
</Grid>
<!-- Liste des utilisateurs présents (MàJ via l'événement 'joint') -->
<Border Grid.Column="1" Margin="10,0,0,0" BorderBrush="#E0E0E0" BorderThickness="1" Background="#FFF">
<Border Grid.Column="1" Margin="10,0,0,0" BorderBrush="#E0E0E0" BorderThickness="1" >
<Grid RowDefinitions="Auto, *">
<TextBlock Grid.Row="0" Text="Membres Présents" FontWeight="Bold" Margin="5" HorizontalAlignment="Center"/>
<ListBox Grid.Row="1" ItemsSource="{Binding ConnectedUsers}" BorderThickness="0"/>

View file

@ -16,7 +16,7 @@
FontWeight="SemiBold"
HorizontalAlignment="Center"/>
<Button Content="Chat"
<Button Content="Les Salons de discussion"
Command="{Binding OpenChat}"
HorizontalAlignment="Center"
IsEnabled="{Binding SessionStatus.IsLoggedIn}"/>

View file

@ -8,5 +8,5 @@
x:Class="PostIt.Views.MainWindow"
Icon="/Assets/avalonia-logo.ico"
Title="PostIt">
<views:MainView x:Name="MainView" />
<views:MainView />
</Window>