The postit://callback re-launch crashed Avalonia inside
DataValidationErrors.SetErrors with 'The calling thread cannot
access this object because a different thread owns it'. Two
Settings instances raced on PropertyChanged: one was the DI
singleton registered by App.OnFrameworkInitializationCompleted,
the other was a freshly-constructed fallback in
LoginPageViewModel() and LoginPage.axaml.cs's DataContext-null
branch. Avalonia's binding sink caught the cross-thread
notification and crashed before the LoginPage could render.
Fix at three layers:
1. Settings: lock the mutation gate so concurrent Load() /
ApplyJson() callers cannot tear reads; override
OnPropertyChanged to marshal every notification onto the
Avalonia UI thread via a new UiDispatcher helper (no more
cross-thread SetErrors). Add BindToServiceProvider /
RequireCurrent so production code paths cannot silently
allocate a second instance.
2. LoginPageViewModel(): resolve the canonical Settings from
the DI container (Settings.RequireCurrent) instead of
new Settings(). The cross-thread crash is now caught loudly
with a clear 'Settings.Current is not bound' error if
something instantiates the VM outside a bound App.
3. HomePage.axaml.cs and LoginPage.axaml.cs: resolve the
next view-model and BlogApiClient through App.Services
instead of constructing them with 'new'. Same instance
tree as the rest of the app; the postit://callback race
disappears by construction.
MainPageViewModel and HomePageViewModel keep their existing
'?? new Settings()' fallback for test friendliness, but the
fallback is now harmless because Settings itself is
thread-safe.
Adds two regression tests in SettingsLoadTests covering
concurrent Load+mutate and concurrent idempotent Load.
/signin only renders the local-account sign-in form on Yavsc.Org;
new accounts live at /Account/Register. Update RegisterUrl in
LoginPageViewModel and the matching test accordingly.