From e0655dbb315c05833bc19ac81efd921bfa020e14 Mon Sep 17 00:00:00 2001 From: Jaap-Jan de Wit | DodoTech Date: Wed, 5 Aug 2026 12:00:20 +0200 Subject: [PATCH] Look the host list up by name, rather than off a field that is never assigned MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nightly 0.0.0-alpha.0.133 died before its first frame. The long press I added attached itself in HostsScreen's constructor through the field the Avalonia name generator declares for `x:Name` — and that field is assigned by the generated InitializeComponent, which no view in this repository calls. Every one of them loads its XAML directly. So the field compiles, resolves in the editor, and is null at run time; PhoneShell builds this control on the way up, so the NullReferenceException took the launch rather than the hosts screen. PhoneShell and TerminalScreen both use FindControl, and PhoneShell carries a saying exactly this and naming exactly this consequence. I read neither and wrote the field. So the rule is in docs/platform-flags.md now as well. A comment on the control that already got it right is not where somebody writing a new one is looking, which is the whole of why two correct examples and one warning were not enough. And phase 8 opens with "it launches at all". Nothing on this head is covered by a test — no test project, no headless surface — so a view that throws while being built takes the launch with it and no gate anywhere says so. Thirty seconds, and it would have caught this one before it was published. --- docs/manual-checks.md | 13 +++++++++++++ docs/platform-flags.md | 16 ++++++++++++++++ .../Views/HostsScreen.axaml.cs | 18 ++++++++++++++++-- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/docs/manual-checks.md b/docs/manual-checks.md index e295a63..ca81618 100644 --- a/docs/manual-checks.md +++ b/docs/manual-checks.md @@ -904,6 +904,19 @@ the rectangles are not. So what follows is the rectangles. Run on a 360dp-wide device or emulator, unlocked, on HOSTS. +### 8.0 It launches at all · **thirty seconds, and it has caught a shipped crash** + +Install the APK and open it. + +**Pass:** the opening screen appears. + +**Failure means:** nothing on this head is covered by a test — see the paragraph above — so a view that +throws while it is being built takes the launch with it and no build gate anywhere says so. The one that +shipped was a generated `x:Name` field used from a constructor, which is null in every view in this +repository; see `docs/platform-flags.md`. Get the stack with `adb logcat -d -b crash`, which names the view. + +Every check below assumes this one passed, and none of them can be reached if it did not. + ### 8.1 The + is reachable and is not covering anything Look at the bottom right of the host list with no host selected. diff --git a/docs/platform-flags.md b/docs/platform-flags.md index 7a57569..9528158 100644 --- a/docs/platform-flags.md +++ b/docs/platform-flags.md @@ -366,6 +366,22 @@ like packaging. **Linux ships AppImage and Flatpak first**, specifically so the WebKit runtime is bundled rather than assumed present on the user's machine. +**A field generated for `x:Name` is null in every view in this repository, and using one is a crash rather +than a mistake you can see.** The Avalonia name generator declares a field per `x:Name` and assigns it +inside the `InitializeComponent` it also generates. **No view here calls that method** — every one of them +loads its XAML directly with `AvaloniaXamlLoader.Load(this)`, on both heads. So the field exists, compiles, +resolves in the editor, and is null at run time. + +What that costs depends on where it is touched. In a constructor it is a `NullReferenceException` while the +control is being built, and a control being built by `PhoneShell` on the way up takes the whole launch with +it: the application dies before the first frame, with a stack that names the view rather than the name. +Nightly `0.0.0-alpha.0.133` shipped exactly that, from one line in `HostsScreen`'s constructor. + +`this.FindControl("Name")!` is the idiom, and it is what `PhoneShell`, `TerminalScreen` and `HostsScreen` +all use. Two of those three carried a `` warning about it before the third did it anyway, which is +why it is written here as well: a comment on the control that already got it right is not where somebody +writing a new one is looking. + **Opening the system browser depends on the platform handler.** `SystemBrowserLauncher` uses `UseShellExecute`, which delegates to `ShellExecute` on Windows, `open` on macOS and `xdg-open` on Linux. *Unverified off Windows:* `xdg-open` comes from `xdg-utils`, which is not guaranteed on a diff --git a/src/DodoSSH.Client.Android/Views/HostsScreen.axaml.cs b/src/DodoSSH.Client.Android/Views/HostsScreen.axaml.cs index 8b592d6..c069162 100644 --- a/src/DodoSSH.Client.Android/Views/HostsScreen.axaml.cs +++ b/src/DodoSSH.Client.Android/Views/HostsScreen.axaml.cs @@ -34,17 +34,31 @@ internal sealed partial class HostsScreen : UserControl private bool held; /// + /// /// The long press is attached here rather than in the markup so that it sits beside the property that /// makes it fire at all. is set rather than assumed: /// it is the whole of the gesture, and a default that changed would take it away silently — every tap /// would go on working and nothing would ever open the bar again. + /// + /// + /// ◆ FindControl rather than the field the name generator declares for x:Name, + /// which is the same rule PhoneShell and TerminalScreen already carry, and which this + /// file learned the hard way. That field is + /// assigned by the generated InitializeComponent, and no view on this head calls it — they load + /// their XAML directly, as above. So the field compiles, is null at run time, and the first thing + /// touching it throws. Here that was a in this constructor, which + /// is a crash before the first frame: the phone shell builds this control on the way up, so the + /// application died on launch rather than on the hosts screen. + /// /// public HostsScreen() { AvaloniaXamlLoader.Load(this); - InputElement.SetIsHoldingEnabled(Rows, true); - Rows.Holding += OnRowHeld; + var rows = this.FindControl("Rows")!; + + InputElement.SetIsHoldingEnabled(rows, true); + rows.Holding += OnRowHeld; } ///