From 4e8402e766d50e8af9c5a074454203f67dac3af2 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 28 Jun 2026 01:05:02 +0100 Subject: [PATCH 001/217] 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 5c847beb..7b3dcb13 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 fd6d6ab2d86a806c91f2fcbfdbf8fd1ed892ce38 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 28 Jun 2026 01:13:08 +0100 Subject: [PATCH 002/217] 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 b4b65be9..e6a0c59f 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 f84098cd..769234e3 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 2f7df74000c79382e91c8d8db374806cec6c18c9 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 28 Jun 2026 01:43:39 +0100 Subject: [PATCH 003/217] 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 e34d753c..69742832 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 df8d86e3..7969c98a 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"/> - - - - - - - -