Skip to content

Commit e1c6d57

Browse files
fix(grid): register assigned definition collections with their shared size group (#21848)
* test(grid): reproduce shared size groups ignoring assigned definitions Definitions supplied through the ColumnDefinitions or RowDefinitions setter are already in the collection when the grid claims it, so they never pass through the collection-changed handler that joins them to the parent tree. They never register with their shared size group, and the definitions they replace never unregister from it. * fix(grid): join assigned definition collections to the parent tree DefinitionList.SetParent assigned each definition's Parent but never called OnEnterParentTree, which only ran from the collection-changed handler. Assigning Parent is not sufficient: OnEnterParentTree also sets InheritanceParent, and a definition cannot read the inherited PrivateSharedSizeScope that registers it with its group until that link exists. Definitions supplied through the ColumnDefinitions setter - an object initializer, a shared resource, or ColumnDefinitions="Auto,*" - were therefore silently absent from their shared size group. Enter and exit the parent tree from SetParent, and release the outgoing collection when Grid swaps one in. Without that release the replaced definitions stay registered with the group; nothing resets their measured minimum any more, so they pin it at whatever they last contributed. * test(grid): cover the definition ownership contract Removing a definition leaves it holding its old Parent and its property inheritance link, so it still reads the grid's shared size scope and can re-register itself into a scope it has left. Also covers moving a definition between grids, reassigning the same collection, and row definitions, which the assignment fix reached but nothing exercised. * refactor(grid): centralise definition parent-tree transitions Definition ownership was implemented twice, and the two paths disagreed: SetParent exited a definition and cleared its Parent, while removing one from the collection called OnExitParentTree but left Parent set. Detach was incomplete either way, since OnEnterParentTree establishes InheritanceParent but OnExitParentTree never cleared it - so a removed definition kept reading the grid's inherited PrivateSharedSizeScope, and the grid kept it alive as an inheritance child. Route every owner change through one transition that exits the old tree, assigns Parent, and enters the new one, and clear InheritanceParent on exit so detach mirrors attach. --------- Co-authored-by: Julien Lebosquain <julien@lebosquain.net>
1 parent 1e0d314 commit e1c6d57

4 files changed

Lines changed: 292 additions & 12 deletions

File tree

src/Avalonia.Controls/DefinitionBase.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ internal void OnExitParentTree()
6060
}
6161

6262
Parent?.InvalidateMeasure();
63+
64+
// while this link survives the definition still reads the grid's inherited
65+
// PrivateSharedSizeScope, and the grid holds it as an inheritance child.
66+
InheritanceParent = null;
6367
}
6468

6569
/// <summary>

src/Avalonia.Controls/DefinitionList.cs

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,53 @@ internal Grid? Parent
2525

2626
private void SetParent(Grid? value)
2727
{
28+
if (_parent == value)
29+
{
30+
return;
31+
}
32+
2833
_parent = value;
2934

35+
// definitions already present when the grid claims the collection never pass through
36+
// OnCollectionChanged, so they have to change trees here.
3037
var idx = 0;
3138

3239
foreach (T definition in this)
3340
{
34-
definition.Parent = value;
41+
SetDefinitionParent(definition, value);
3542
definition.Index = idx++;
3643
}
3744
}
3845

46+
/// <summary>
47+
/// Moves a definition from its current parent tree to <paramref name="parent"/>. Every route
48+
/// that changes a definition's owner goes through here.
49+
/// </summary>
50+
/// <remarks>
51+
/// Ownership is more than the Parent pointer: entering a tree also establishes the property
52+
/// inheritance link a definition needs to see its shared size scope, and leaving one releases
53+
/// that link and its shared size registration.
54+
/// </remarks>
55+
private static void SetDefinitionParent(DefinitionBase definition, Grid? parent)
56+
{
57+
if (definition.Parent == parent)
58+
{
59+
return;
60+
}
61+
62+
if (definition.Parent is not null)
63+
{
64+
definition.OnExitParentTree();
65+
}
66+
67+
definition.Parent = parent;
68+
69+
if (parent is not null)
70+
{
71+
definition.OnEnterParentTree();
72+
}
73+
}
74+
3975
internal void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
4076
{
4177
var idx = 0;
@@ -62,17 +98,7 @@ private void UpdateDefinitionParent(IList? items, bool wasRemoved)
6298

6399
for (var i = 0; i < count; i++)
64100
{
65-
var definition = (DefinitionBase) items[i]!;
66-
67-
if (wasRemoved)
68-
{
69-
definition.OnExitParentTree();
70-
}
71-
else
72-
{
73-
definition.Parent = Parent;
74-
definition.OnEnterParentTree();
75-
}
101+
SetDefinitionParent((DefinitionBase)items[i]!, wasRemoved ? null : Parent);
76102
}
77103
}
78104
}

src/Avalonia.Controls/Grid.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,12 @@ public ColumnDefinitions ColumnDefinitions
198198
set
199199
{
200200
if (_extData == null) { _extData = new ExtendedData(); }
201+
// otherwise the outgoing definitions stay registered with their shared size
202+
// group and keep contributing to its minimum.
203+
if (_extData.ColumnDefinitions is { } oldDefinitions && !ReferenceEquals(oldDefinitions, value))
204+
{
205+
oldDefinitions.Parent = null;
206+
}
201207
_extData.ColumnDefinitions = value;
202208
_extData.ColumnDefinitions.Parent = this;
203209
InvalidateMeasure();
@@ -220,6 +226,12 @@ public RowDefinitions RowDefinitions
220226
set
221227
{
222228
if (_extData == null) { _extData = new ExtendedData(); }
229+
// otherwise the outgoing definitions stay registered with their shared size
230+
// group and keep contributing to its minimum.
231+
if (_extData.RowDefinitions is { } oldDefinitions && !ReferenceEquals(oldDefinitions, value))
232+
{
233+
oldDefinitions.Parent = null;
234+
}
223235
_extData.RowDefinitions = value;
224236
_extData.RowDefinitions.Parent = this;
225237
InvalidateMeasure();

tests/Avalonia.Controls.UnitTests/GridTests.cs

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1309,6 +1309,244 @@ void ExecuteSharedSizeLayoutPass()
13091309
Assert.Equal(10, plainGrid.ColumnDefinitions[0].ActualWidth);
13101310
}
13111311

1312+
[Fact]
1313+
public void Shared_Size_Group_Is_Registered_For_Definitions_Assigned_As_A_Collection()
1314+
{
1315+
// Definitions supplied through the ColumnDefinitions setter - an object initializer, a
1316+
// shared resource, or ColumnDefinitions="Auto,*" - are already in the collection when the
1317+
// grid claims it, so they never pass through the collection-changed handler that joins
1318+
// them to the parent tree.
1319+
var grids = new[]
1320+
{
1321+
new Grid
1322+
{
1323+
ColumnDefinitions = new ColumnDefinitions
1324+
{
1325+
new ColumnDefinition { Width = GridLength.Auto, SharedSizeGroup = "A" },
1326+
new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) },
1327+
},
1328+
},
1329+
new Grid
1330+
{
1331+
ColumnDefinitions = new ColumnDefinitions
1332+
{
1333+
new ColumnDefinition { Width = GridLength.Auto, SharedSizeGroup = "A" },
1334+
new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) },
1335+
},
1336+
},
1337+
};
1338+
grids[0].Children.Add(new Border { Width = 50, Height = 10 });
1339+
1340+
var scope = new StackPanel
1341+
{
1342+
[Grid.IsSharedSizeScopeProperty] = true,
1343+
Children = { grids[0], grids[1] },
1344+
};
1345+
var root = new TestRoot(scope);
1346+
1347+
root.ExecuteInitialLayoutPass();
1348+
// Shared groups validate after layout and apply any resulting invalidation on the next pass.
1349+
root.LayoutManager.ExecuteLayoutPass();
1350+
1351+
Assert.Equal(50, grids[0].ColumnDefinitions[0].ActualWidth);
1352+
Assert.Equal(50, grids[1].ColumnDefinitions[0].ActualWidth);
1353+
}
1354+
1355+
[Fact]
1356+
public void Replacing_Definition_Collection_Releases_Its_Shared_Size_Group()
1357+
{
1358+
// The outgoing definitions are no longer reachable from the grid, so nothing resets their
1359+
// measured minimum. Left registered, they keep the group pinned at whatever size they
1360+
// last contributed.
1361+
var grid = new Grid();
1362+
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto, SharedSizeGroup = "A" });
1363+
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
1364+
grid.Children.Add(new Border { Width = 50, Height = 10 });
1365+
1366+
var other = new Grid();
1367+
other.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto, SharedSizeGroup = "A" });
1368+
other.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
1369+
1370+
var scope = new StackPanel
1371+
{
1372+
[Grid.IsSharedSizeScopeProperty] = true,
1373+
Children = { grid, other },
1374+
};
1375+
var root = new TestRoot(scope);
1376+
1377+
root.ExecuteInitialLayoutPass();
1378+
// Shared groups validate after layout and apply any resulting invalidation on the next pass.
1379+
root.LayoutManager.ExecuteLayoutPass();
1380+
Assert.Equal(50, other.ColumnDefinitions[0].ActualWidth);
1381+
1382+
grid.ColumnDefinitions = new ColumnDefinitions
1383+
{
1384+
new ColumnDefinition { Width = GridLength.Auto },
1385+
new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) },
1386+
};
1387+
root.LayoutManager.ExecuteLayoutPass();
1388+
root.LayoutManager.ExecuteLayoutPass();
1389+
1390+
Assert.Equal(0, other.ColumnDefinitions[0].ActualWidth);
1391+
}
1392+
1393+
[Fact]
1394+
public void Removing_Definition_Detaches_It_From_The_Grid()
1395+
{
1396+
var shared = new ColumnDefinition { Width = GridLength.Auto, SharedSizeGroup = "A" };
1397+
var grid = new Grid();
1398+
grid.ColumnDefinitions.Add(shared);
1399+
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
1400+
grid.Children.Add(new Border { Width = 50, Height = 10 });
1401+
1402+
var other = new Grid();
1403+
other.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto, SharedSizeGroup = "A" });
1404+
other.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
1405+
1406+
var scope = new StackPanel
1407+
{
1408+
[Grid.IsSharedSizeScopeProperty] = true,
1409+
Children = { grid, other },
1410+
};
1411+
var root = new TestRoot(scope);
1412+
1413+
root.ExecuteInitialLayoutPass();
1414+
root.LayoutManager.ExecuteLayoutPass();
1415+
Assert.Equal(50, other.ColumnDefinitions[0].ActualWidth);
1416+
1417+
grid.ColumnDefinitions.Remove(shared);
1418+
root.LayoutManager.ExecuteLayoutPass();
1419+
root.LayoutManager.ExecuteLayoutPass();
1420+
Assert.Equal(0, other.ColumnDefinitions[0].ActualWidth);
1421+
Assert.Null(shared.Parent);
1422+
1423+
// A definition that has left the grid must no longer see the grid's scope.
1424+
shared.SharedSizeGroup = null;
1425+
shared.SharedSizeGroup = "A";
1426+
root.LayoutManager.ExecuteLayoutPass();
1427+
root.LayoutManager.ExecuteLayoutPass();
1428+
Assert.Equal(0, other.ColumnDefinitions[0].ActualWidth);
1429+
}
1430+
1431+
[Fact]
1432+
public void Moving_Definition_Between_Grids_Moves_Its_Shared_Size_Registration()
1433+
{
1434+
var shared = new ColumnDefinition { Width = GridLength.Auto, SharedSizeGroup = "A" };
1435+
var source = new Grid();
1436+
source.ColumnDefinitions.Add(shared);
1437+
source.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
1438+
source.Children.Add(new Border { Width = 50, Height = 10 });
1439+
1440+
var sourcePartner = new Grid();
1441+
sourcePartner.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto, SharedSizeGroup = "A" });
1442+
sourcePartner.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
1443+
1444+
var target = new Grid();
1445+
target.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
1446+
1447+
var targetPartner = new Grid();
1448+
targetPartner.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto, SharedSizeGroup = "A" });
1449+
targetPartner.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
1450+
targetPartner.Children.Add(new Border { Width = 20, Height = 10 });
1451+
1452+
var sourceScope = new StackPanel
1453+
{
1454+
[Grid.IsSharedSizeScopeProperty] = true,
1455+
Children = { source, sourcePartner },
1456+
};
1457+
var targetScope = new StackPanel
1458+
{
1459+
[Grid.IsSharedSizeScopeProperty] = true,
1460+
Children = { target, targetPartner },
1461+
};
1462+
var root = new TestRoot(new StackPanel { Children = { sourceScope, targetScope } });
1463+
1464+
root.ExecuteInitialLayoutPass();
1465+
root.LayoutManager.ExecuteLayoutPass();
1466+
Assert.Equal(50, sourcePartner.ColumnDefinitions[0].ActualWidth);
1467+
Assert.Equal(20, targetPartner.ColumnDefinitions[0].ActualWidth);
1468+
1469+
source.ColumnDefinitions.Remove(shared);
1470+
target.ColumnDefinitions.Insert(0, shared);
1471+
root.LayoutManager.ExecuteLayoutPass();
1472+
root.LayoutManager.ExecuteLayoutPass();
1473+
1474+
Assert.Same(target, shared.Parent);
1475+
Assert.Equal(0, sourcePartner.ColumnDefinitions[0].ActualWidth);
1476+
Assert.Equal(20, target.ColumnDefinitions[0].ActualWidth);
1477+
}
1478+
1479+
[Fact]
1480+
public void Reassigning_The_Same_Definition_Collection_Is_Inert()
1481+
{
1482+
var grid = new Grid();
1483+
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto, SharedSizeGroup = "A" });
1484+
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
1485+
grid.Children.Add(new Border { Width = 50, Height = 10 });
1486+
1487+
var other = new Grid();
1488+
other.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto, SharedSizeGroup = "A" });
1489+
other.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
1490+
1491+
var scope = new StackPanel
1492+
{
1493+
[Grid.IsSharedSizeScopeProperty] = true,
1494+
Children = { grid, other },
1495+
};
1496+
var root = new TestRoot(scope);
1497+
1498+
root.ExecuteInitialLayoutPass();
1499+
root.LayoutManager.ExecuteLayoutPass();
1500+
Assert.Equal(50, other.ColumnDefinitions[0].ActualWidth);
1501+
1502+
var definitions = grid.ColumnDefinitions;
1503+
grid.ColumnDefinitions = definitions;
1504+
root.LayoutManager.ExecuteLayoutPass();
1505+
root.LayoutManager.ExecuteLayoutPass();
1506+
1507+
Assert.Same(definitions, grid.ColumnDefinitions);
1508+
Assert.Equal(50, other.ColumnDefinitions[0].ActualWidth);
1509+
}
1510+
1511+
[Fact]
1512+
public void Shared_Size_Group_Is_Registered_For_Row_Definitions_Assigned_As_A_Collection()
1513+
{
1514+
var grids = new[]
1515+
{
1516+
new Grid
1517+
{
1518+
RowDefinitions = new RowDefinitions
1519+
{
1520+
new RowDefinition { Height = GridLength.Auto, SharedSizeGroup = "A" },
1521+
new RowDefinition { Height = new GridLength(1, GridUnitType.Star) },
1522+
},
1523+
},
1524+
new Grid
1525+
{
1526+
RowDefinitions = new RowDefinitions
1527+
{
1528+
new RowDefinition { Height = GridLength.Auto, SharedSizeGroup = "A" },
1529+
new RowDefinition { Height = new GridLength(1, GridUnitType.Star) },
1530+
},
1531+
},
1532+
};
1533+
grids[0].Children.Add(new Border { Width = 10, Height = 50 });
1534+
1535+
var scope = new StackPanel
1536+
{
1537+
Orientation = Layout.Orientation.Horizontal,
1538+
[Grid.IsSharedSizeScopeProperty] = true,
1539+
Children = { grids[0], grids[1] },
1540+
};
1541+
var root = new TestRoot(scope);
1542+
1543+
root.ExecuteInitialLayoutPass();
1544+
root.LayoutManager.ExecuteLayoutPass();
1545+
1546+
Assert.Equal(50, grids[0].RowDefinitions[0].ActualHeight);
1547+
Assert.Equal(50, grids[1].RowDefinitions[0].ActualHeight);
1548+
}
1549+
13121550
[Fact]
13131551
public void Collection_Changes_Are_Tracked()
13141552
{

0 commit comments

Comments
 (0)