From 57bd04c2136cd929985632079db67b914ff1789e Mon Sep 17 00:00:00 2001 From: John Lambert Date: Tue, 14 Jul 2026 06:45:21 -0400 Subject: [PATCH 1/3] Fix blank browse columns for part-ref child nodes in XmlVc NodeChildrenDisplayCommand always passed a null caller to ProcessChildren, even when it originated from a "part" node in XmlViewsUtils.GetDisplayCommandForColumn1. Child fragments that resolve attributes (e.g. "ws" for multilingual strings) from the caller got nothing and rendered blank, matching XmlVc's own "part" handling which does pass a non-null caller through ProcessChildren. Carry the part-ref node down as the caller so both paths agree. Co-Authored-By: Claude Sonnet 5 --- Src/Common/Controls/XMLViews/XmlVc.cs | 40 +++++++++++++++++-- Src/Common/Controls/XMLViews/XmlViewsUtils.cs | 14 ++++--- 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/Src/Common/Controls/XMLViews/XmlVc.cs b/Src/Common/Controls/XMLViews/XmlVc.cs index 885f15157e..7e354b04b1 100644 --- a/Src/Common/Controls/XMLViews/XmlVc.cs +++ b/Src/Common/Controls/XMLViews/XmlVc.cs @@ -5074,6 +5074,12 @@ public override int GetHashCode() /// public class NodeChildrenDisplayCommand : NodeDisplayCommand { + // Optional "part ref" node that called the part whose children we display. When non-null it is + // passed down as the caller so that child fragments (e.g. multilingual strings) can resolve + // attributes such as "ws" from it. This mirrors XmlVc's behavior where the "part" case is the + // one thing that calls ProcessChildren with a non-null caller. + readonly XmlNode m_caller; + /// ------------------------------------------------------------------------------------ /// /// Initializes a new instance of the class. @@ -5085,14 +5091,39 @@ public NodeChildrenDisplayCommand(XmlNode node) { } + /// ------------------------------------------------------------------------------------ + /// + /// Initializes a new instance of the class that + /// carries a caller (part ref) node down to the children being displayed. + /// + /// The node whose children are displayed. + /// The calling part ref node (may be null). + /// ------------------------------------------------------------------------------------ + public NodeChildrenDisplayCommand(XmlNode node, XmlNode caller) + : base(node) + { + m_caller = caller; + } + + /// + /// The calling part ref node, or null if none was supplied. + /// + public XmlNode Caller + { + get { return m_caller; } + } + internal override void PerformDisplay(XmlVc vc, int fragId, int hvo, IVwEnv vwenv) { - vc.ProcessChildren(Node, vwenv, hvo); + if (m_caller == null) + vc.ProcessChildren(Node, vwenv, hvo); + else + vc.ProcessChildren(Node, vwenv, hvo, m_caller); } internal override void DetermineNeededFields(XmlVc vc, int fragId, NeededPropertyInfo info) { - DetermineNeededFieldsForChildren(vc, Node, null, info); + DetermineNeededFieldsForChildren(vc, Node, m_caller, info); } // Make it work sensibly as a hash key @@ -5108,7 +5139,8 @@ internal override void DetermineNeededFields(XmlVc vc, int fragId, NeededPropert /// ------------------------------------------------------------------------------------ public override bool Equals(object obj) { - return base.Equals(obj) && obj is NodeChildrenDisplayCommand; + var other = obj as NodeChildrenDisplayCommand; + return other != null && base.Equals(obj) && other.m_caller == m_caller; } /// ------------------------------------------------------------------------------------ @@ -5122,7 +5154,7 @@ public override bool Equals(object obj) /// ------------------------------------------------------------------------------------ public override int GetHashCode() { - return base.GetHashCode(); + return base.GetHashCode() ^ (m_caller == null ? 0 : m_caller.GetHashCode()); } diff --git a/Src/Common/Controls/XMLViews/XmlViewsUtils.cs b/Src/Common/Controls/XMLViews/XmlViewsUtils.cs index 1ffcd8a631..4d90f3a02e 100644 --- a/Src/Common/Controls/XMLViews/XmlViewsUtils.cs +++ b/Src/Common/Controls/XMLViews/XmlViewsUtils.cs @@ -1134,19 +1134,23 @@ static NodeDisplayCommand GetDisplayCommandForColumn1(IManyOnePathSortItem bvi, collectOuterStructParts.Add(node); return GetDisplayCommandForColumn1(bvi, mainChild, cache, mdc, sda, layouts, depth, out hvo, collectOuterStructParts); } - // Review JohnT: In XmlVc, "part" is the one thing that calls ProcessChildren with non-null caller. - // this should make some difference here, but I can't figure what yet, or come up with a test that fails. - // We may need a "caller" argument to pass this down so it can be used in GetNodeForRelatedObject. + // In XmlVc, "part" is the one thing that calls ProcessChildren with a non-null caller (the + // part ref node). The off-screen cell-render path must do the same, otherwise children that + // read attributes such as "ws" from the caller (e.g. multilingual strings configured via the + // part ref) resolve nothing and render blank. So we capture the part ref node and carry it as + // the caller into the NodeChildrenDisplayCommand. (See LT- for the "Grammatical Info." blank + // column bug in the Avalonia browse table.) case "part": { string layoutName = XmlUtils.GetOptionalAttributeValue(node, "ref"); if (layoutName != null) { // It's actually a part ref, in a layout, not a part looked up by one! - // Get the node it refers to, and make a command to process its children. + // Get the node it refers to, and make a command to process its children, passing this + // part ref node down as the caller so child fragments can resolve attributes from it. XmlNode part = XmlVc.GetNodeForPart(hvo, layoutName, false, sda, layouts); if (part != null) - return new NodeChildrenDisplayCommand(part); // display this object using the children of the part referenced. + return new NodeChildrenDisplayCommand(part, node); // display this object using the children of the part referenced. else return new NodeDisplayCommand(node); // no matching part, do default. } From 53768137d2c011a87a985389befdab1d9e3718a3 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Tue, 14 Jul 2026 12:37:38 -0400 Subject: [PATCH 2/3] Add regression test for part-ref caller propagation Addresses the "no test added" gap noted in the PR description: pins that a column-spec node carries itself down as the resulting NodeChildrenDisplayCommand's Caller, so child fragments that read attributes (e.g. "ws") from the caller resolve correctly instead of rendering blank. Co-Authored-By: Claude Sonnet 5 --- .../XMLViewsTests/TestManyOneBrowse.cs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/Src/Common/Controls/XMLViews/XMLViewsTests/TestManyOneBrowse.cs b/Src/Common/Controls/XMLViews/XMLViewsTests/TestManyOneBrowse.cs index 935d349cdb..b7a4fb7bd6 100644 --- a/Src/Common/Controls/XMLViews/XMLViewsTests/TestManyOneBrowse.cs +++ b/Src/Common/Controls/XMLViews/XMLViewsTests/TestManyOneBrowse.cs @@ -449,5 +449,38 @@ public void DisplayDoubleSeqPathObject() m_mdc, m_sda, m_layouts, out useHvo, collectStructNodes); Assert.That(useHvo, Is.EqualTo(9)); // the third sense, in which context we display the 4th SD } + + /// + /// Regression test for the part-ref caller-propagation fix in GetDisplayCommandForColumn1's "part" + /// case: in XmlVc, "part" is the one thing that calls ProcessChildren with a non-null caller, so a + /// column-spec node must carry ITSELF down as the resulting + /// NodeChildrenDisplayCommand's Caller (not null), so that child fragments reading attributes such + /// as "ws" from the caller (as multilingual strings configured via the part ref do) can resolve + /// them. Before the fix, the caller was always dropped, which is what caused the "Grammatical Info." + /// blank-column bug in the Avalonia browse table's off-screen cell rendering. + /// + [Test] + public void GetDisplayCommandForColumn_PartRefNode_CarriesItselfAsCaller() + { + ArrayList list = new ArrayList(); + XmlViewsUtils.CollectBrowseItems(1, m_columnList[0], list, null, m_mdc, m_sda, m_layouts); + IManyOnePathSortItem bvi = list[0] as IManyOnePathSortItem; + + // A hand-built node, exactly the shape XmlVc embeds inside a layout when it + // refers to another part by name (here reusing the existing LexEntry-Jt-Msas part from + // TestParts.xml via the same class+layoutName naming convention GetNodeForPart resolves). + var doc = new XmlDocument(); + XmlElement partRefNode = doc.CreateElement("part"); + partRefNode.SetAttribute("ref", "Msas"); + + int hvo; + NodeDisplayCommand cmd = XmlViewsUtils.GetDisplayCommandForColumn(bvi, partRefNode, null, + m_mdc, m_sda, m_layouts, out hvo, null); + + var childrenCmd = cmd as NodeChildrenDisplayCommand; + Assert.That(childrenCmd, Is.Not.Null, "a resolvable part ref must yield a NodeChildrenDisplayCommand"); + Assert.That(childrenCmd.Caller, Is.SameAs(partRefNode), + "the part ref node itself must be carried down as the caller so child fragments can resolve attributes from it"); + } } } From f4b3f76bd36ec74c364f7a9c3653684f90173428 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Tue, 14 Jul 2026 12:45:07 -0400 Subject: [PATCH 3/3] Trim test comments to one line Co-Authored-By: Claude Sonnet 5 --- .../XMLViews/XMLViewsTests/TestManyOneBrowse.cs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/Src/Common/Controls/XMLViews/XMLViewsTests/TestManyOneBrowse.cs b/Src/Common/Controls/XMLViews/XMLViewsTests/TestManyOneBrowse.cs index b7a4fb7bd6..536bd1b32b 100644 --- a/Src/Common/Controls/XMLViews/XMLViewsTests/TestManyOneBrowse.cs +++ b/Src/Common/Controls/XMLViews/XMLViewsTests/TestManyOneBrowse.cs @@ -450,15 +450,7 @@ public void DisplayDoubleSeqPathObject() Assert.That(useHvo, Is.EqualTo(9)); // the third sense, in which context we display the 4th SD } - /// - /// Regression test for the part-ref caller-propagation fix in GetDisplayCommandForColumn1's "part" - /// case: in XmlVc, "part" is the one thing that calls ProcessChildren with a non-null caller, so a - /// column-spec node must carry ITSELF down as the resulting - /// NodeChildrenDisplayCommand's Caller (not null), so that child fragments reading attributes such - /// as "ws" from the caller (as multilingual strings configured via the part ref do) can resolve - /// them. Before the fix, the caller was always dropped, which is what caused the "Grammatical Info." - /// blank-column bug in the Avalonia browse table's off-screen cell rendering. - /// + /// A part-ref node must carry itself as the resulting Caller, so child fragments (e.g. multilingual strings) can resolve attributes like "ws" from it. [Test] public void GetDisplayCommandForColumn_PartRefNode_CarriesItselfAsCaller() { @@ -466,9 +458,7 @@ public void GetDisplayCommandForColumn_PartRefNode_CarriesItselfAsCaller() XmlViewsUtils.CollectBrowseItems(1, m_columnList[0], list, null, m_mdc, m_sda, m_layouts); IManyOnePathSortItem bvi = list[0] as IManyOnePathSortItem; - // A hand-built node, exactly the shape XmlVc embeds inside a layout when it - // refers to another part by name (here reusing the existing LexEntry-Jt-Msas part from - // TestParts.xml via the same class+layoutName naming convention GetNodeForPart resolves). + // Hand-built node, the shape XmlVc embeds when a part ref names another part. var doc = new XmlDocument(); XmlElement partRefNode = doc.CreateElement("part"); partRefNode.SetAttribute("ref", "Msas");