Skip to content

Commit 1967a66

Browse files
committed
LT-22414: Rebuild Morph Type slice after SwapValues to prevent disappearing field
Problem: - When replacing the Morph Type via autocomplete (e.g., typing "suffix" over "root" and pressing Enter) the Morph Type field could disappear from the Entry Pane. Root cause: - SwapValues set DoNotRefresh = true while making model changes that fire PropChanged. - With m_postponePropChanged enabled (LT-22018/LT-21980), PropChanged defers refreshes via BeginInvoke instead of calling RefreshList synchronously, so RefreshListNeeded was never set during the DoNotRefresh window. - When DoNotRefresh was cleared no synchronous rebuild occurred; cached slice indices were stale and the Morph Type slice could be lost. Fix: - In MorphTypeAtomicLauncher.cs ensure the data tree is marked for refresh (set RefreshListNeeded = true) before releasing the DoNotRefresh guard so the setter triggers a synchronous rebuild. - Locate the rebuilt Morph Type slice by type (search for MorphTypeAtomicReferenceSlice) instead of relying on a saved index that can become stale. - Addresses regression reported in LT-22414; related to changes for LT-22018/LT-21980. Compiles cleanly.
1 parent a46204c commit 1967a66

File tree

2 files changed

+185
-10
lines changed

2 files changed

+185
-10
lines changed
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
// Copyright (c) 2026 SIL International
2+
// This software is licensed under the LGPL, version 2.1 or later
3+
// (http://www.gnu.org/licenses/lgpl-2.1.html)
4+
5+
using System.Linq;
6+
using System.Windows.Forms;
7+
using NUnit.Framework;
8+
using SIL.FieldWorks.Common.FwUtils;
9+
using SIL.LCModel;
10+
using SIL.LCModel.Core.Text;
11+
using XCore;
12+
13+
namespace SIL.FieldWorks.Common.Framework.DetailControls
14+
{
15+
/// <summary>
16+
/// Regression tests for the DoNotRefresh + m_postponePropChanged interaction (LT-22414).
17+
///
18+
/// Root cause: when <c>m_postponePropChanged=true</c> (default since LT-22018),
19+
/// <see cref="DataTree.PropChanged"/> defers refresh via <c>BeginInvoke</c>.
20+
/// This means <see cref="DataTree.RefreshListNeeded"/> is never set during a
21+
/// <c>DoNotRefresh</c> window, so releasing <c>DoNotRefresh</c> does not trigger
22+
/// a synchronous refresh. Code that brackets LCModel changes with DoNotRefresh
23+
/// (like <c>MorphTypeAtomicLauncher.SwapValues</c>) must explicitly set
24+
/// <c>RefreshListNeeded=true</c> before releasing <c>DoNotRefresh</c>.
25+
/// </summary>
26+
[TestFixture]
27+
public class MorphTypeSwapRefreshTests : MemoryOnlyBackendProviderRestoredForEachTestTestBase
28+
{
29+
private Inventory m_parts;
30+
private Inventory m_layouts;
31+
private ILexEntry m_entry;
32+
private Mediator m_mediator;
33+
private PropertyTable m_propertyTable;
34+
private DataTree m_dtree;
35+
private Form m_parent;
36+
37+
#region Fixture Setup and Teardown
38+
39+
public override void FixtureSetup()
40+
{
41+
base.FixtureSetup();
42+
m_layouts = DataTreeTests.GenerateLayouts();
43+
m_parts = DataTreeTests.GenerateParts();
44+
}
45+
46+
#endregion
47+
48+
#region Test Setup and Teardown
49+
50+
public override void TestSetup()
51+
{
52+
base.TestSetup();
53+
54+
m_entry = Cache.ServiceLocator.GetInstance<ILexEntryFactory>().Create();
55+
m_entry.CitationForm.VernacularDefaultWritingSystem =
56+
TsStringUtils.MakeString("test", Cache.DefaultVernWs);
57+
m_entry.Bibliography.SetAnalysisDefaultWritingSystem("bib content");
58+
m_entry.Bibliography.SetVernacularDefaultWritingSystem("bib content");
59+
60+
m_dtree = new DataTree();
61+
m_mediator = new Mediator();
62+
m_propertyTable = new PropertyTable(m_mediator);
63+
m_dtree.Init(m_mediator, m_propertyTable, null);
64+
m_parent = new Form();
65+
m_parent.Controls.Add(m_dtree);
66+
}
67+
68+
public override void TestTearDown()
69+
{
70+
if (m_parent != null)
71+
{
72+
m_parent.Close();
73+
m_parent.Dispose();
74+
}
75+
if (m_propertyTable != null)
76+
{
77+
m_propertyTable.Dispose();
78+
m_propertyTable = null;
79+
}
80+
if (m_mediator != null)
81+
{
82+
m_mediator.Dispose();
83+
m_mediator = null;
84+
}
85+
86+
base.TestTearDown();
87+
}
88+
89+
#endregion
90+
91+
/// <summary>
92+
/// LT-22414 regression: after clearing bibliography data inside a
93+
/// DoNotRefresh window, the ifdata bibliography slice should disappear.
94+
///
95+
/// With m_postponePropChanged=true (default since LT-22018), PropChanged
96+
/// defers via BeginInvoke and never sets RefreshListNeeded during the
97+
/// DoNotRefresh window. Callers (like SwapValues) must explicitly set
98+
/// RefreshListNeeded=true before releasing DoNotRefresh.
99+
///
100+
/// RED phase: comment out RefreshListNeeded=true → test FAILS (stale slices).
101+
/// GREEN phase: RefreshListNeeded=true present → test PASSES.
102+
/// </summary>
103+
[Test]
104+
public void DoNotRefresh_SlicesMustReflectChanges_AfterRelease_LT22414()
105+
{
106+
// Arrange: show entry with CfAndBib layout (CitationForm + Bibliography ifdata)
107+
m_dtree.Initialize(Cache, false, m_layouts, m_parts);
108+
m_dtree.ShowObject(m_entry, "CfAndBib", null, m_entry, false);
109+
110+
// Verify initial state: both slices visible (bib has data)
111+
Assert.That(m_dtree.Controls.Count, Is.EqualTo(2),
112+
"Setup: should have CitationForm + Bibliography");
113+
114+
// Act: simulate the DoNotRefresh pattern from SwapValues
115+
m_dtree.DoNotRefresh = true;
116+
117+
// Make changes that should affect visible slices:
118+
// clearing bibliography data should cause the ifdata slice to disappear on refresh
119+
m_entry.Bibliography.SetVernacularDefaultWritingSystem("");
120+
m_entry.Bibliography.SetAnalysisDefaultWritingSystem("");
121+
122+
// LT-22414 FIX: callers must explicitly set RefreshListNeeded=true.
123+
// Without this line, the test FAILS (proving the bug exists).
124+
// >>> COMMENT OUT THIS LINE TO SEE THE BUG <<<
125+
m_dtree.RefreshListNeeded = true;
126+
127+
m_dtree.DoNotRefresh = false;
128+
129+
// Assert: after refresh, bibliography slice should be gone (no data → ifdata hides it)
130+
Assert.That(m_dtree.Controls.Count, Is.EqualTo(1),
131+
"LT-22414: After DoNotRefresh=false, slices should reflect data changes. " +
132+
"Bibliography has no data so ifdata should hide it. " +
133+
"If this fails with count=2, no refresh occurred.");
134+
Assert.That((m_dtree.Controls[0] as Slice).Label, Is.EqualTo("CitationForm"));
135+
}
136+
137+
/// <summary>
138+
/// Complementary test: verify that WITHOUT RefreshListNeeded=true,
139+
/// releasing DoNotRefresh does NOT trigger a refresh (the bug behavior).
140+
/// This documents the root cause of LT-22414.
141+
/// </summary>
142+
[Test]
143+
public void DoNotRefresh_WithoutRefreshListNeeded_DoesNotRefresh_LT22414_BugDemo()
144+
{
145+
// Arrange: show entry with CfAndBib layout
146+
m_dtree.Initialize(Cache, false, m_layouts, m_parts);
147+
m_dtree.ShowObject(m_entry, "CfAndBib", null, m_entry, false);
148+
Assert.That(m_dtree.Controls.Count, Is.EqualTo(2));
149+
150+
var originalSlice = m_dtree.Slices[0];
151+
152+
// Act: DoNotRefresh without setting RefreshListNeeded
153+
m_dtree.DoNotRefresh = true;
154+
m_entry.Bibliography.SetVernacularDefaultWritingSystem("");
155+
m_entry.Bibliography.SetAnalysisDefaultWritingSystem("");
156+
// Intentionally NOT setting RefreshListNeeded (simulates buggy SwapValues)
157+
m_dtree.DoNotRefresh = false;
158+
159+
// Assert: slices are STALE — bibliography still visible despite no data
160+
Assert.That(m_dtree.Controls.Count, Is.EqualTo(2),
161+
"Without RefreshListNeeded, DoNotRefresh=false does not trigger refresh; " +
162+
"slices remain stale (bibliography still visible despite no data).");
163+
}
164+
}
165+
}

Src/Common/Controls/DetailControls/MorphTypeAtomicLauncher.cs

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -380,11 +380,10 @@ private bool CheckForStemDataLoss(IMoStemAllomorph stem, List<IMoMorphSynAnalysi
380380
return false;
381381
}
382382

383-
private void SwapValues(ILexEntry entry, IMoForm origForm, IMoForm newForm, IMoMorphType type,
383+
internal void SwapValues(ILexEntry entry, IMoForm origForm, IMoForm newForm, IMoMorphType type,
384384
List<IMoMorphSynAnalysis> rgmsaOld)
385385
{
386386
DataTree dtree = Slice.ContainingDataTree;
387-
int idx = Slice.IndexInContainer;
388387
dtree.DoNotRefresh = true; // don't let the datatree repeatedly redraw itself...
389388
entry.ReplaceMoForm(origForm, newForm);
390389
newForm.MorphTypeRA = type;
@@ -402,16 +401,27 @@ private void SwapValues(ILexEntry entry, IMoForm origForm, IMoForm newForm, IMoM
402401
}
403402
// now fix the record list, since it may be showing MoForm dependent columns (e.g. MorphType, Homograph, etc...)
404403
dtree.FixRecordList();
404+
// When PropChanged is deferred via BeginInvoke (m_postponePropChanged),
405+
// RefreshListNeeded is never set during the DoNotRefresh window. Ensure it
406+
// is set so that the DoNotRefresh=false setter triggers a synchronous refresh
407+
// and the new slices (including Morph Type) are properly rebuilt. (LT-22414)
408+
dtree.RefreshListNeeded = true;
405409
dtree.DoNotRefresh = false;
406-
Slice sliceT = dtree.Slices[idx];
407-
if (sliceT != null && sliceT is MorphTypeAtomicReferenceSlice)
410+
// After the refresh, find the new MorphType slice by type rather than
411+
// relying on the pre-swap index which may be stale.
412+
for (int i = 0; i < dtree.Slices.Count; i++)
408413
{
409-
// When the new slice is created, the launch button is placed in the middle of
410-
// the slice rather than at the end. This fiddling with the slice width seems
411-
// to fix that. Then setting the index restores focus to the new slice.
412-
sliceT.Width += 1;
413-
sliceT.Width -= 1;
414-
dtree.GotoNextSliceAfterIndex(idx - 1);
414+
Slice sliceT = dtree.Slices[i];
415+
if (sliceT is MorphTypeAtomicReferenceSlice)
416+
{
417+
// When the new slice is created, the launch button is placed in the middle of
418+
// the slice rather than at the end. This fiddling with the slice width seems
419+
// to fix that. Then setting the index restores focus to the new slice.
420+
sliceT.Width += 1;
421+
sliceT.Width -= 1;
422+
dtree.GotoNextSliceAfterIndex(i - 1);
423+
break;
424+
}
415425
}
416426
}
417427

0 commit comments

Comments
 (0)