From 38eb3922c492eeb403122c6083346f780841dd15 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 28 Jun 2026 01:05:02 +0100 Subject: [PATCH 001/404] postit: drop ConfigureAwait(false) after interactive login MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LoginAsync uses ConfigureAwait(false) on the await of LoginInteractiveCoreAsync. Since the surrounding code is already executing on the UI thread (it was reached via a RelayCommand that the UI dispatcher dispatched), the ConfigureAwait drops the SynchronizationContext, and the subsequent setters — IsBusy, AccessToken, StatusMessage, LoginSuccess, and the LoginSucceeded?.Invoke() — run on a thread-pool worker. The downstream effects are all UI-bound: PropertyChanged events fire, the BindingEngine republishes them as AvaloniaObject.SetValue calls, and SetValue calls Dispatcher.VerifyAccess. VerifyAccess throws because the AvaloniaObject was created on the UI thread (owned by it) and the SetValue is being attempted from a thread-pool worker. Avalonia 11.12 throws SynchronousException through DispatcherOperation.InvokeCore instead of dispatching back, so the X11 message loop crashes the process with System.InvalidOperationException: 'The calling thread cannot access this object because a different thread owns it.' Reproduced with the freshly installed postit_1.0.0-1_amd64.deb package on a Debian 13 host — the .NET runtime loaded the app, Avalonia started the X11 message loop, the operator clicked 'Se connecter', the OIDC flow reached the post-login phase, and the post-await setter chain crashed the process. Drop ConfigureAwait(false) so the await captures the UI thread SynchronizationContext and the setters resume on the UI thread. The inner LoginInteractiveCoreAsync still uses ConfigureAwait(false) for its own await, which is fine — the inner method does not touch observables, only mutates Platform.CreateBrowser and awaits the OIDC roundtrip, so it can run anywhere. --- src/PostIt/PostIt/ViewModels/LoginPageViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PostIt/PostIt/ViewModels/LoginPageViewModel.cs b/src/PostIt/PostIt/ViewModels/LoginPageViewModel.cs index 5c847bebd..7b3dcb134 100644 --- a/src/PostIt/PostIt/ViewModels/LoginPageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/LoginPageViewModel.cs @@ -262,7 +262,7 @@ public partial class LoginPageViewModel : ViewModelBase // keeps the text detail (URLs, error messages). Same // underlying flow, two views. var progress = new Progress(p => Phase = p); - await LoginInteractiveCoreAsync(_api, progress).ConfigureAwait(false); + await LoginInteractiveCoreAsync(_api, progress); IsBusy = false; AccessToken = _api.CurrentAccessToken; From bdf7676875bca1e6efa88a7d100b50a07b09c98b Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 28 Jun 2026 01:13:08 +0100 Subject: [PATCH 002/404] postit: change MainPage base class from NavigationPage to ContentPage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HomePage.OnLoginClick does Navigation.PushAsync(new MainPage { ... }). NavigationPage.PushAsync only accepts a Page (or Page subclass), not a MultiPage. MainPage was declared as public partial class MainPage : NavigationPage in MainPage.axaml.cs and the root of MainPage.axaml was which means 'new MainPage()' produced a MultiPage, not a Page. PushAsync against a MultiPage argument does not route through the standard Page push path; the visible result is that the login succeeds, LoginSucceeded fires, but the UI stays on LoginPage — the user is left looking at the post-login state without any navigation. The XAML content of MainPage (a StackPanel with the post CRUD UI, a ListBox, a TextEditor) does not need the multi-page container semantics — it's a single screen. Switch both the code-behind base class and the XAML root element to ContentPage so that MainPage is what PushAsync expects. --- src/PostIt/PostIt/Views/MainPage.axaml | 4 ++-- src/PostIt/PostIt/Views/MainPage.axaml.cs | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/PostIt/PostIt/Views/MainPage.axaml b/src/PostIt/PostIt/Views/MainPage.axaml index b4b65be96..e6a0c59f8 100644 --- a/src/PostIt/PostIt/Views/MainPage.axaml +++ b/src/PostIt/PostIt/Views/MainPage.axaml @@ -1,4 +1,4 @@ - - + diff --git a/src/PostIt/PostIt/Views/MainPage.axaml.cs b/src/PostIt/PostIt/Views/MainPage.axaml.cs index f84098cd4..769234e34 100644 --- a/src/PostIt/PostIt/Views/MainPage.axaml.cs +++ b/src/PostIt/PostIt/Views/MainPage.axaml.cs @@ -1,14 +1,13 @@ - using Avalonia; using Avalonia.Controls; namespace PostIt.Views; -public partial class MainPage : NavigationPage +public partial class MainPage : ContentPage { public MainPage() { InitializeComponent(); } -} \ No newline at end of file +} From bbde168340894c3f42732c52951dff6155cc8372 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 28 Jun 2026 01:43:39 +0100 Subject: [PATCH 003/404] petit refacto --- ...{MainViewModel.cs => MainPageViewModel.cs} | 33 ++++++++++++++----- src/PostIt/PostIt/Views/LoginPage.axaml | 15 ++------- 2 files changed, 26 insertions(+), 22 deletions(-) rename src/PostIt/PostIt/ViewModels/{MainViewModel.cs => MainPageViewModel.cs} (95%) diff --git a/src/PostIt/PostIt/ViewModels/MainViewModel.cs b/src/PostIt/PostIt/ViewModels/MainPageViewModel.cs similarity index 95% rename from src/PostIt/PostIt/ViewModels/MainViewModel.cs rename to src/PostIt/PostIt/ViewModels/MainPageViewModel.cs index e34d753c3..69742832d 100644 --- a/src/PostIt/PostIt/ViewModels/MainViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/MainPageViewModel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.ObjectModel; +using System.ComponentModel; using System.Linq; using System.Threading.Tasks; using Avalonia.Styling; @@ -50,17 +51,20 @@ public partial class MainPageViewModel : ViewModelBase /// App.axaml.cs so the same client (and its token store) /// is shared with the login flow. /// - public BlogApiClient BlogClient { get; } + public BlogApiClient? BlogClient { get; } public override bool CanNavigateNext { get => throw new NotImplementedException(); protected set => throw new NotImplementedException(); } public override bool CanNavigatePrevious { get => throw new NotImplementedException(); protected set => throw new NotImplementedException(); } - /// - /// Test-friendly constructor: caller supplies a pre-built - /// . Production code uses the - /// (Settings, BlogApiClient) overload below. - /// - public MainPageViewModel(BlogApiClient blogClient, Settings? settings = null) + + public MainPageViewModel() + { + Init(null); + SettingsModel = new SettingsPageViewModel(); + BlogClient = null; + } + + private void Init(Settings? settings) { SearchText = string.Empty; Posts = new ObservableCollection(); @@ -71,10 +75,21 @@ public partial class MainPageViewModel : ViewModelBase Settings = settings ?? new Settings(); Title = "PostIt"; CurrentViewModel = this; - SettingsModel = new SettingsPageViewModel(); - BlogClient = blogClient ?? throw new ArgumentNullException(nameof(blogClient)); } + /// + /// Test-friendly constructor: caller supplies a pre-built + /// . Production code uses the + /// (Settings, BlogApiClient) overload below. + /// + public MainPageViewModel(BlogApiClient blogClient, Settings? settings = null) + { + SettingsModel = new SettingsPageViewModel(); + BlogClient = blogClient ?? throw new ArgumentNullException(nameof(blogClient));; + + Init(settings); + } + partial void OnSearchTextChanged(string value) => ApplyFilter(); partial void OnSelectedPostChanged(BlogPost? value) => UpdateCommandStates(); diff --git a/src/PostIt/PostIt/Views/LoginPage.axaml b/src/PostIt/PostIt/Views/LoginPage.axaml index df8d86e3c..7969c98a7 100644 --- a/src/PostIt/PostIt/Views/LoginPage.axaml +++ b/src/PostIt/PostIt/Views/LoginPage.axaml @@ -5,10 +5,10 @@ x:DataType="vm:LoginPageViewModel" Header="Login"> - + - @@ -27,18 +27,7 @@ FontSize="24" HorizontalAlignment="Center"/> - - - - - - - -