Repository navigation
fix(#327): ChannelsPanePrototype's 81 bindings were all on reflection — three declared templates, and a drawer label bound to a member IChannel never declared - #394
Conversation
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>
|
/agentic_review |
PR Summary by QodoCompile Channels pane bindings and expose channel scale errors
AI Description
Diagram
High-Level Assessment
Files changed (3)
|
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 4b13ea8 |
|
Qodo-clean, CI green — ready for review. Two Qodo rounds on head 4b13ea8, both graded Balanced with 0 findings. Each was confirmed to have fired after its trigger, and after each I waited 3+ minutes and re-checked: 0 Qodo threads. CI run 35394779286 on that same SHA is green on all heads, including Android and iOS. Not merging. |
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit 4b13ea8 |
|
Shepherd: Qodo-clean, CI green, ready for review. CI run 35394779286 is green on 4b13ea8, which is the current head (it checked out Merge 4b13ea8 into 642caef, today's main). I fired an independent round at 21:23:10Z on that unchanged head. It ran rather than being refused: the summary updated at 21:25:22Z, anchored on 4b13ea8, with 0 bugs, 0 rule violations and 0 requirement gaps. After a 200 s settle I re-checked both surfaces: still 0, and 0 qodo threads, with the author filter validated as 3 on control #196. The standing grade is Balanced, from the builder's round on this head. The new render shows no grade of its own. I also checked by hand: the host route and DataContext for each scope (MainWindow's tab hands DaqifiViewModel and OnLoaded swaps in ChannelsPaneViewModel), and the tile, palette and swatch template types against the VM (ObservableCollection x3, IBrush[] Palette, ObservableCollection device names). All 5 undeclared ControlTemplates carry no {Binding}. Parsing the XAML gives 81 bindings (72 markup + 9 element), matching the body. I pushed nothing and edited nothing in the body. |
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit 4b13ea8 |
|
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
ChannelsPanePrototype.axaml(the desktop Channels tab) declared nox:DataType, so all 81 of its bindings resolved by reflection: 72 attribute bindings plus the 9<Binding>children of its 4MultiBindings, counted by parsing the XAML (not grepping it). Rename or delete a member onChannelsPaneViewModel,ChannelTileViewModel,IChannelorIStreamingDeviceand a tile would render blank, or a drawer control would stop responding, while every head still built green.The fix
x:CompileBindings="True"ChannelsPaneViewModelChannelTileTemplate(keyed, inUserControl.Resources)ChannelTileViewModelIsLoggingActive,ToggleChannelCommand,OpenSettingsCommand) via$parent[UserControl].DataContextChannelsPaneViewModelItemTemplateConnectedDeviceNamesstringItemTemplateIBrushSelectedChannel.ChannelColorBrush,SetColorCommand) via$parent[UserControl]ChannelsPaneViewModelSwatchButton'sControlTemplate(Background="{Binding}")IBrushWhy each declaration is there:
DataTemplateinfers nothing. With the declaration removed, the build fails with 14AVLN2000s onXamlPseudoType. All three lists that use the template (AnalogInputs,DigitalInputs,DigitalOutputs) areObservableCollection<ChannelTileViewModel>.ControlTemplatecompile without their declarations today, because the bindings inside them have no member path (<Binding/>and{Binding}). They are declared anyway because both would otherwise be typed wrongly.Paletteis an array, and an array infers nothing. AControlTemplateis not a new scope, so it would type-check against the root'sChannelsPaneViewModel, but at runtime its DataContext is the palette entry. The type isIBrush, notBrush.ChannelsPaneViewModelhasusing Brush = Avalonia.Media.IBrush;, soPaletteis really anIBrush[]holdingImmutableSolidColorBrushes. My first draft declaredBrush(the abstract Avalonia class). That compiled and was wrong, and I caught it only by reading the view-model's usings.IChannelgainsstring ScaleExpressionError { get; }. Without it, the drawer's error label (SelectedChannel.ScaleExpressionError, whereSelectedChannelis typedIChannel) isAVLN2000. This is not a runtime bug fix. Onmainthe reflection binding resolves against the concreteAbstractChannel, which has always had the member, so the label worked. The interface simply didn't declare it. The hunk is byte-identical to the one in #392 (ChannelsMobileView), which needs the same member for the same reason. So the two PRs should merge in either order without a conflict in that file.AbstractChannelis the onlyIChannelimplementer in the repo.What DataContext each scope holds at runtime (read, not observed)
The one host is
MainWindow.axaml's Channels tab:<ContentControl Content="{Binding}">whoseContentTemplateholds<prototype:ChannelsPanePrototype/>. So the pane first inherits the window'sDaqifiViewModel. Its code-behind then swaps in its own view-model inOnLoaded(if (DataContext is not ChannelsPaneViewModel) DataContext = new ChannelsPaneViewModel();) and nulls it inOnUnloaded.HeadlessBenchbuilds aChannelsPaneViewModeldirectly and never this view.ChannelsPaneViewModelis the only type these bindings are written for.The one runtime difference:
SelectedDevice.PwmFrequencyHzbeforeOnLoaded(re-checked by a shepherd, 2026-09-18). Between the pane attaching andOnLoadedfiring, the pane holds the window'sDaqifiViewModelon the first visit andnullon later visits (#397 measured this on the Devices pane, which has the same host and the same swap). Onmain, the reflection bindingSelectedDevice.PwmFrequencyHzresolved againstDaqifiViewModel.SelectedDeviceduring that window. It is the only root path whose name also exists onDaqifiViewModel.DaqifiViewModel.IsLoggingActiveis an explicit interface implementation, so reflection never saw it. On this branch the compiled binding rejects the mismatched type and the box stays blank. This is not a regression, becausemainwas showing the wrong data there:DaqifiViewModel.SelectedDeviceis the Devices pane's selection. This pane'sSelectedDeviceis a different property.OnLoadedcreates a freshChannelsPaneViewModelwhoseSelectedDeviceisnulland whoseIsSettingsOpenisfalse. It is set only byOpenSettings, which runs when the user clicks a tile's cog, to the device that owns that channel (FindOwningDevice), and it is cleared again byCloseSettings. So once the swap happens, bothmainand this branch show a blank box inside a closed drawer. The value never comes back afterOnLoaded.null, somainis blank there too. It also needs a device to be selected in Devices at that moment. The drawer is visible in that frame only because a failedIsSettingsOpenbinding leavesIsVisibleat its defaulttrue, on bothmainand this branch. That is Devices pane paints one or two frames with the settings drawer forced open on every tab visit (pre-OnLoaded DataContext) #397's defect, and this PR neither causes nor fixes it.UpdateSourceTrigger=LostFocus, and nothing can take focus within that frame.This conclusion is read from the code plus #397's frame measurement. I did not capture a frame of the Channels pane myself.
Verification
One-build positive control on
main. I put a typo in the root scope (HasConnectedDeviceQ), a typo in the tile template (TileBackgroundQ) and a bogus property (BogusPropertyQon the rootGrid) in the same build. The result was exactly 1 error: the propertyAVLN2000at (336,10). The compiler read the file and said nothing about either typo, which is the defect this PR closes.Branch, one typo per scope, all 11 in one build from a fresh edit. Result: 11
AVLN2000s, each naming the right type:ChannelsPaneViewModelvia the tile's$parenthop (151), at the root (350), and via the palette's$parenthop (638)ChannelTileViewModel(153)IChannelthrough the tile'sChannel.(177), through aMultiBindingchild (650), and throughSelectedChannel.(787)IBrushin the swatchControlTemplate(299) and in the palette template (629)stringin the chip template (404)IStreamingDevicethroughSelectedDevice.(768)Swatch
ControlTemplateruntime-type control (shepherd, 2026-09-18, one build from a fresh edit). At runtime the template'sBordergets its DataContext from the swatchButton. TheButtonsets no DataContext and no Content, so that DataContext is the palette item: anImmutableSolidColorBrushfromPalette, which is anIBrush[]. The type that must be checked is thereforeIBrush. In one build, next to{Binding}I addedTag="{Binding OpacityQ}"(a typo) andOpacity="{Binding Opacity}"(a member ofIBrushbut not ofChannelsPaneViewModel). The result was exactly 1AVLN2000:'OpacityQ' on type 'Avalonia.Media.IBrush'at (300,29).Opacitywas accepted. Then, from another fresh edit, I removed the template'sx:DataType="IBrush". The result was 2 errors, both namingChannelsPaneViewModel, one forOpacityQand one for the validOpacity. So without the declaration the template type-checks against the wrong type, and the declaration is what makes it correct. I reverted the edit afterwards, and the clean build exited 0.Declaration-necessity control. I dropped all three inner declarations in one build. The result was 14
AVLN2000onXamlPseudoType, all in the tile template. The twoIBrushdeclarations are not needed to compile, as stated above.Test:
ChannelsPanePrototypeBindingTests, 28 lines, using only existingBindingFactshelpers (AssertRootDeclares×2,AssertNoEscapeHatch). I ran two mutations against it:x:CompileBindingsline fails the CompileBindings row (1/3 red).x:CompileBindings = 'False'on the palette template fails the escape-hatch row (1/3 red).Build gate is green: desktop, iOS restore, and iOS Release
iossimulator-arm64(the iOS leg was incremental).dotnet testpasses 996/996.Rendering:
tools/parity-audit/run.sh --check-baseline, run alone (load avg ~1.5, no other capture running), gave 39/39 in one run. The denominator isgit show origin/main:tools/parity-audit/baselines/macos-arm64.sha256 | wc -l= 39.desktop-3-channelsanddesktop-min-3-channelsare byte-identical tomain's baseline. Those screens show the no-device state, so they exercise the root scope and the empty-state gate. The tiles, the settings drawer, the palette and the swatch were not rendered; the compiler controls above cover them.Warnings: delta 0, taken from CI's own logs at both ends:
main642caef, run 35377977069, against this head 4b13ea8, run 35394779286. These are all-project totals per job from cold CI builds:The distinct (file, line, code) warning sets are identical for every job. CI tests went 993 → 996.
Coordination
IChannelhunk; see above.ChannelScalingExpressionTests.The_scaling_error_label_shows_the_reason_on_both_headshas a comment saying neither view declares anx:DataType, and 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 rewrites it to say the desktop view still doesn't. I left that file alone to avoid a conflict with 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. Whichever PR lands second should correct the comment. The test itself still passes, because itsAssertBindsliteral is unchanged.Diffstat: +55 / −6 across 3 files (+28 test, +20/−6 view, +7 interface).
🤖 Generated with Claude Code