Repository navigation
fix(#327): ChannelsMobileView's 76 bindings were all on reflection — two declared templates, three cast hops, and a drawer label bound to a member IChannel never had - #392
Conversation
…one declared root, one declared resource template, and a drawer label bound to a member IChannel never had Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… infers nothing Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
PR Summary by QodoCompile-check ChannelsMobileView bindings and fix IChannel contract
AI Description
Diagram
High-Level Assessment
Files changed (4)
|
Code Review by Qodo🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)
Great, no issues found!Qodo reviewed your code and found no material issues that require reviewTip of the day💡 Did you know, you can type 'qodo, fix this' on a finding and the fix lands right on your PR |
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit 01764b9 |
|
Qodo-clean, CI green — ready for review. Head |
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit 01764b9 |
|
Qodo-clean, CI green, ready for review (shepherd). CI's green covers head Review. I fired a third The "latent bug" question: no user-visible bug existed on Hand review of the rest.
Body corrections (no commit):
The attached image is the branch render. |
|
Merge build (shepherd, throwaway local branch, nothing pushed): |
Root declares ChannelsPaneViewModel with x:CompileBindings; the resource-dictionary tile template declares ChannelTileViewModel (infers nothing otherwise); the palette ItemTemplate and SwatchButton's ControlTemplate declare IBrush (Palette is IBrush[] via the VM's Brush alias). IChannel gains ScaleExpressionError, identical to #392's hunk, so the drawer's error label type-checks through SelectedChannel. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Sweep (13-way merge build): all 13 PRs are clean together on Covered: #387 Per PR: every head matches its ready note. Every PR is
Merge build: I merged
Test count reconciles exactly: 993 on Cross-PR hazards checked:
No merge was performed and nothing was pushed to any PR branch. |

refs #327 — not merging, for review.
What was wrong
All 76 bindings in
ChannelsMobileView.axaml(the mobile Channels tab) resolved by reflection. If a member onChannelsPaneViewModel,ChannelTileViewModelorIChannelwere renamed or removed, the tile, the settings card or its error label would just go blank, and desktop and iOS would still build green.Turning compile checking on found one binding that only worked because reflection doesn't check types. The scaling-error label binds
SelectedChannel.ScaleExpressionError(#311).SelectedChannelis typedIChannel, and that member was declared only onAbstractChannel. It worked at runtime because every channel is anAbstractChannel. Against the declared type, the compiler rejects it (AVLN2000 … 'ScaleExpressionError' on type 'IChannel').This was not a user-visible bug on
main, and this PR does not fix one. Reflection resolves the path on the runtime object, and every concreteIChannelin the repo (AnalogChannel,DigitalChannel, and the four test doubles) derives fromAbstractChannel, which declares the member.AbstractChannelis the only direct implementer ofIChannel. A shepherd rendered the view with two channels on bothmain642caefand this branch, opened the drawer through the tile's own gear button, and entered a 40-deep parenthesised expression. Both builds showedTOO MANY NESTED PARENTHESES (LIMIT 8)in the label, and the two PNGs are byte-identical. The interface change is a contract correction that compile checking needs. It is not a behaviour fix.The fix
The runtime DataContext for each scope was traced, not guessed.
MobileMainView.OnChannelsis the only place that constructs the view:new ChannelsMobileView { DataContext = new ChannelsPaneViewModel() }. Nothing else in the repo creates it, and the capture harness gets to it through that same route.x:DataType="vm:ChannelsPaneViewModel"+x:CompileBindings="True"ChannelsPaneViewModel.CompileBindingsis needed becauseAvaloniaUseCompiledBindingsByDefaultisfalse(#326)ChannelTileTemplate(resource)x:DataType="vm:ChannelTileViewModel"AnalogInputs,DigitalInputs,DigitalOutputs) areObservableCollection<ChannelTileViewModel>$parent[ItemsControl]hops((vm:ChannelsPaneViewModel)DataContext)castItemsControlthe tile will be placed in, so it typesDataContextasSystem.Object(measured: 3 × AVLN2000). The traversal itself is unchanged:$parentwalks the logical tree, and the nearestItemsControlis still the section list whose DataContext is the pane VMItemTemplatestringfromConnectedDeviceNames(measured)ItemTemplatex:DataType="IBrush"ItemsSourceinfers no type.PaletteisIBrush[](declaredBrush[]under the file'susing Brush = Avalonia.Media.IBrushalias, and holdingImmutableSolidColorBrushvalues at runtime), and a member path in that template isAVLN2000 … on type 'XamlPseudoType'(measured). Its two bindings are path-less{Binding}, so they compiled without the declaration, but only because they had nothing to check.IBrushis the element type exactly. Its$parent[ItemsControl].DataContexthop resolves against the root scope without a castIChannelgainsstring ScaleExpressionError { get; }.AbstractChannelis the only implementer, and its[ObservableProperty]already generates a public get/set that satisfies the getter-only member, so runtime behaviour is unchanged. The alternative was a((AbstractChannel)SelectedChannel)cast in the binding. It would also compile, but it ties the view to the base class, and the desktop drawer (ChannelsPanePrototype.axaml:807, still on reflection) binds the same path and would need the same cast when it is converted. Declaring the member on the interface fixes both. The test that pins the label binding text on both views (ChannelScalingExpressionTests.The_scaling_error_label_shows_the_reason_on_both_heads) still passes unchanged; only its comment was updated.No escape hatches: no
x:CompileBindings="False"subtree and no{ReflectionBinding}in any spelling. A test guards this.Verification
Positive control, one build per side (fresh edits each time, exit codes read, no re-runs trusted):
AVLN2000, each naming the right type:ChannelsPaneViewModel(rootDeviceNameZZ),ChannelTileViewModel(TypeLabelZZ),ChannelsPaneViewModel(tile cast hopIsLoggingActiveZZ),System.String(device-name template),Avalonia.Media.IBrush(palette template),ChannelsPaneViewModel(palette$parenthop),IChannel(SelectedChannel.ScaleExpressionErrorZZ).main: one build with a bogus propertyBogusPropertyZZ="1"and a binding typo{Binding TypeLabelZZ}on the same element gave exactly 1 error, the property (AVLN2000 … BogusPropertyZZ on type TextBlock), and nothing for the typo. This shows the compiler read the file and skipped the binding, rather than that no build ran.Gate: desktop build, iOS restore, and iOS Release
iossimulator-arm64build all exit 0.dotnet test: 996 passed, 0 failed.Capture baseline:
mobile-portrait-2-channelsandmobile-landscape-2-channelsare captured screens../run.sh --check-baseline, single run:baseline OK, 39/39 screens (the manifest onorigin/mainhas 39 lines). Limitation: both channels captures show the no-device empty state, so the tile template, the palette and the settings card compile but no capture renders them. The empty-state gate (!HasConnectedDevice) and the root scope are what the pixels confirm. (Shepherd, later: this limitation is now covered by a separate render. A temporary HeadlessBench probe, since reverted, connected aScriptedDevicewith two analog inputs throughConnectionManager.Connect. It built the view the wayMobileMainView.OnChannelsdoes and drove it through the tile controls. Both tiles rendered. The gear's cast hop opened the drawer. The toggle's cast hop flippedIsActivefrom false to true. All 8 palette swatches rendered through theIBrushtemplate, and the error label showed the specific reason. The PNG is attached in a comment.)Warnings (from CI logs, both ends): delta 0. Read from CI’s own logs at both ends,
main642caef(run 35377977069, this branch’s merge base) against this branch01764b9(run 35388501986). Printed all-project totals are identical on every head that prints one: iOS 170/170, Android 175/175, Desktop-on-macOS 169/169. The de-duplicated warning sets extracted from all four build jobs (Library+Desktop, iOS, Android, macOS) are identical, with nothing added or removed.Test:
ChannelsMobileViewBindingTestsuses only the existingBindingFactshelpers:AssertRootDeclares×2 andAssertNoEscapeHatch. The compiler already enforces the tile template's declaration: drop it and the build fails (XamlPseudoType on the first member path). Dropping the palette's declaration would still build, because its bindings have no path. That is the case the declaration covers, and it is not tested (no existing helper states it, and #387 ownsBindingFacts.cs).Scope
One view (#327 is a checklist).
BindingFacts.csis untouched (#387 edits it). Diff: 4 files, +68/−13 (--numstat: view +30/−11, test +28/−0,IChannel+7/−0, scaling-test comment +3/−2).🤖 Generated with Claude Code