From a0d53b0c9db2341a5a2f6ad4962b067247477a22 Mon Sep 17 00:00:00 2001 From: Jaap-Jan de Wit | DodoTech Date: Sat, 1 Aug 2026 22:35:04 +0200 Subject: [PATCH] Give the phone its screens back by not stealing a data context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signing in on the phone reached an unlocked shell showing PREFERENCES over the middle of the screen, with a bottom bar whose four buttons did nothing. The buttons were fine. Every one of them changed the shell's screen exactly as asked, and nothing moved, because an opaque panel was sitting on top of the whole page area and never came down. PendingScreen set DataContext = this in its constructor, so that Heading and Detail could be written as plain bindings inside its own XAML. That is not a private arrangement: a binding the parent writes ON one of these — IsVisible="{Binding IsPreferencesShowing}" in PhoneShell — resolves against this control's data context, which was no longer the shell. The binding looked for a shell property on a PendingScreen, found nothing, and left IsVisible at its default. Its default is true. So the panel that says "this screen is not built yet" was permanently visible, last in the Panel and therefore on top of the host list and the keychain both — and the terminal underneath them. The two properties are read with $parent now and the control inherits its context like every other screen. There is no x:DataType on it any more either, so a plain binding here is a compile error rather than a silently missing screen. The desktop head has the other half of this lesson written down already: MainWindow gives the terminal's IsVisible a FallbackValue precisely because an unresolved visibility binding does not hide anything, it shows everything. That note was about the previewer. This is what it looks like at runtime. Verified by reproducing the mechanism rather than by reasoning about it: a control that owns its data context ignores a parent's IsVisible binding and stays visible; one that inherits obeys it. The head builds in Debug and Release. Nothing has been run on a device, as ever — see docs/android-port.md. Co-Authored-By: Claude Opus 5 (1M context) --- .../Views/PendingScreen.axaml | 9 ++++--- .../Views/PendingScreen.axaml.cs | 27 ++++++++++++------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/DodoSSH.Client.Android/Views/PendingScreen.axaml b/src/DodoSSH.Client.Android/Views/PendingScreen.axaml index 8c6904e..c4b79c8 100644 --- a/src/DodoSSH.Client.Android/Views/PendingScreen.axaml +++ b/src/DodoSSH.Client.Android/Views/PendingScreen.axaml @@ -2,7 +2,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:views="using:DodoSSH.Client.Android.Views" x:Class="DodoSSH.Client.Android.Views.PendingScreen" - x:DataType="views:PendingScreen" Background="{StaticResource Canvas}"> @@ -24,11 +27,11 @@ HorizontalAlignment="Center" VerticalAlignment="Center" /> - - diff --git a/src/DodoSSH.Client.Android/Views/PendingScreen.axaml.cs b/src/DodoSSH.Client.Android/Views/PendingScreen.axaml.cs index 7fe36ce..afb7644 100644 --- a/src/DodoSSH.Client.Android/Views/PendingScreen.axaml.cs +++ b/src/DodoSSH.Client.Android/Views/PendingScreen.axaml.cs @@ -8,8 +8,25 @@ namespace DodoSSH.Client.Android.Views; /// A state this head has not built, saying so in its own words. /// /// +/// /// Styled properties rather than a view model: there is no state behind this control, and giving it one /// would make it look like a screen that might one day have data. +/// +/// +/// It must not set its own DataContext, and the two properties are read with $parent for exactly +/// that reason. This control once did DataContext = this so the properties could be bound as +/// {Binding Heading}, and it cost the phone its navigation. A binding written by the parent +/// on one of these — IsVisible="{Binding IsPreferencesShowing}" in PhoneShell — resolves against +/// this control's own data context, so with one set here the binding looked for a shell property on a +/// PendingScreen, found nothing, and left IsVisible at its default of true. The result was an opaque +/// PREFERENCES panel permanently on top of the host list, over a bottom bar whose buttons all worked +/// perfectly and changed nothing anybody could see. +/// +/// +/// The desktop head carries the other half of this lesson in MainWindow.axaml, where the terminal's +/// visibility is given a FallbackValue for the same reason: an unresolved IsVisible binding +/// does not hide anything, it shows everything. +/// /// internal sealed partial class PendingScreen : UserControl { @@ -19,15 +36,7 @@ internal sealed partial class PendingScreen : UserControl public static readonly StyledProperty DetailProperty = AvaloniaProperty.Register(nameof(Detail), string.Empty); - public PendingScreen() - { - AvaloniaXamlLoader.Load(this); - - // Its own data context, so the two properties can be bound in XAML like anything else. Safe here - // and only here: this control deliberately shows nothing from the shell, so there is no inherited - // context worth keeping. - DataContext = this; - } + public PendingScreen() => AvaloniaXamlLoader.Load(this); public string Heading {