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; } ///