Skip to content

Commit 6fc0ad5

Browse files
authored
Merge pull request #22733 from unoplatform/dev/xygu/20260224/dc-propagation-3
fix: data-context propagation around non-FE DO
2 parents 960c9b1 + 7c2d15a commit 6fc0ad5

File tree

14 files changed

+269
-23
lines changed

14 files changed

+269
-23
lines changed

src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Given_UIElement.DataContext.cs

Lines changed: 167 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1-
using System.Threading.Tasks;
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading.Tasks;
25
using Microsoft.UI;
36
using Microsoft.UI.Xaml;
47
using Microsoft.UI.Xaml.Controls;
8+
using Microsoft.UI.Xaml.Data;
9+
using Microsoft.UI.Xaml.Documents;
510
using Microsoft.UI.Xaml.Media;
611
using Uno.UI.Extensions;
712
using Uno.UI.RuntimeTests.Helpers;
@@ -65,4 +70,165 @@ public async Task When_SeveredSubtree_ContainsDataContextSource()
6570
Assert.AreEqual(DC, nested3.DataContext, "3. when reattached, DC (nested3) should still be inherited&unaffected");
6671
Assert.AreEqual(DC, nested4.DataContext, "3. when reattached, DC (nested4) should still be inherited&unaffected");
6772
}
73+
74+
#if HAS_UNO // there is no DC on non-FE DO for winui, not directly*
75+
[TestMethod]
76+
[RunsOnUIThread]
77+
[PlatformCondition(ConditionMode.Include, RuntimeTestPlatforms.Skia)]
78+
public async Task SingleParentNonFE_Direct_DataContext_Propagation_Works()
79+
{
80+
var dc = new { Data = "Context" };
81+
82+
var run = new Run();
83+
run.SetBinding(Run.TextProperty, new Binding { Path = new(nameof(dc.Data)) });
84+
var tblock = new TextBlock();
85+
tblock.Inlines.Add(run);
86+
tblock.DataContext = dc;
87+
88+
await UITestHelper.Load(tblock, x => x.IsLoaded);
89+
90+
Assert.AreEqual(dc, run.DataContext);
91+
Assert.AreEqual(dc.Data, run.Text);
92+
}
93+
94+
[TestMethod]
95+
[RunsOnUIThread]
96+
[PlatformCondition(ConditionMode.Include, RuntimeTestPlatforms.Skia)]
97+
public async Task MultiParentNonFE_Direct_DataContext_Propagation_WorksOnlyOnce1()
98+
{
99+
var brush = new SolidColorBrush(Colors.SkyBlue);
100+
101+
#if DEBUG
102+
using var disp = brush.RegisterDisposablePropertyChangedCallback(
103+
SolidColorBrush.DataContextProperty,
104+
(s, e) => { /* breakpoint here to investigate */ });
105+
#endif
106+
107+
// variant: assignment order: foreground > dc
108+
109+
var setup0 = new Control();
110+
setup0.Foreground = brush;
111+
setup0.DataContext = new { Data = "Context 0" };
112+
Assert.IsNotNull(brush.DataContext, "0. until it is attached to multiple \"parent\", dc propagate should work");
113+
114+
var setup1 = new Control();
115+
setup1.Foreground = brush;
116+
setup1.DataContext = new { Data = "Context 1" };
117+
Assert.IsNull(brush.DataContext, "1. once it is attached to multiple \"parent\", dc should no longer propagate");
118+
119+
setup0.Foreground = null;
120+
setup1.Foreground = null;
121+
var setup2 = new Control();
122+
setup2.Foreground = brush;
123+
setup2.DataContext = new { Data = "Context 2" };
124+
Assert.IsNull(brush.DataContext, "2. once it has been attached to multiple \"parent\", dc shouldn't propagate anymore even if we only have a single parent now");
125+
}
126+
127+
[TestMethod]
128+
[RunsOnUIThread]
129+
[PlatformCondition(ConditionMode.Include, RuntimeTestPlatforms.Skia)]
130+
public async Task MultiParentNonFE_Direct_DataContext_Propagation_WorksOnlyOnce2()
131+
{
132+
var brush = new SolidColorBrush(Colors.SkyBlue);
133+
134+
#if DEBUG
135+
using var disp = brush.RegisterDisposablePropertyChangedCallback(
136+
SolidColorBrush.DataContextProperty,
137+
(s, e) => { /* breakpoint here to investigate */ });
138+
#endif
139+
140+
// variant: assignment order: dc > foreground
141+
142+
var setup0 = new Control();
143+
setup0.DataContext = new { Data = "Context 0" };
144+
setup0.Foreground = brush;
145+
Assert.IsNotNull(brush.DataContext, "0. until it is attached to multiple \"parent\", dc propagate should work");
146+
147+
var setup1 = new Control();
148+
setup1.DataContext = new { Data = "Context 1" };
149+
setup1.Foreground = brush;
150+
Assert.IsNull(brush.DataContext, "1. once it is attached to multiple \"parent\", dc should no longer propagate");
151+
152+
setup0.Foreground = null;
153+
setup1.Foreground = null;
154+
var setup2 = new Control();
155+
setup2.DataContext = new { Data = "Context 2" };
156+
setup2.Foreground = brush;
157+
Assert.IsNull(brush.DataContext, "2. once it has been attached to multiple \"parent\", dc shouldn't propagate anymore even if we only have a single parent now");
158+
}
159+
160+
[TestMethod]
161+
[RunsOnUIThread]
162+
[PlatformCondition(ConditionMode.Include, RuntimeTestPlatforms.Skia)]
163+
public async Task MultiParentNonFE_Inherited_DataContext_Propagation_WorksOnlyOnce()
164+
{
165+
// all permutations just in case
166+
var variants = """
167+
A. child.fg > host.dc > host.child
168+
B. child.fg > host.child > host.dc
169+
C. host.dc > child.fg > host.child
170+
D. host.dc > host.child > child.fg
171+
E. host.child > host.dc > child.fg
172+
F. host.child > child.fg > host.dc
173+
""".Split('\n', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
174+
.Where(x => !x.StartsWith("//"))
175+
.Select(x => new
176+
{
177+
Label = x[0..1],
178+
Instructions = x[3..].Split(" > "),
179+
})
180+
.ToArray();
181+
var instructionMap = new Dictionary<string, Action<Border, Control, object, Brush>>
182+
{
183+
["child.fg"] = (host, child, dc, brush) => child.Foreground = brush,
184+
["host.dc"] = (host, child, dc, brush) => host.DataContext = dc,
185+
["host.child"] = (host, child, dc, brush) => host.Child = child,
186+
};
187+
188+
foreach (var variant in variants)
189+
{
190+
var brush = new SolidColorBrush(Colors.SkyBlue);
191+
#if DEBUG
192+
using var disp = brush.RegisterDisposablePropertyChangedCallback(
193+
SolidColorBrush.DataContextProperty,
194+
(s, e) => { /* breakpoint here to investigate */ });
195+
#endif
196+
197+
var setup0 = new
198+
{
199+
Host = new Border(),
200+
Child = new Control(),
201+
DC = new { Data = $"Context {variant.Label}0" },
202+
};
203+
instructionMap[variant.Instructions[0]](setup0.Host, setup0.Child, setup0.DC, brush);
204+
instructionMap[variant.Instructions[1]](setup0.Host, setup0.Child, setup0.DC, brush);
205+
instructionMap[variant.Instructions[2]](setup0.Host, setup0.Child, setup0.DC, brush);
206+
Assert.IsNotNull(brush.DataContext, $"{variant.Label}0. until it is attached to multiple \"parent\", dc propagate should work");
207+
208+
var setup1 = new
209+
{
210+
Host = new Border(),
211+
Child = new Control(),
212+
DC = new { Data = $"Context {variant.Label}1" },
213+
};
214+
instructionMap[variant.Instructions[0]](setup1.Host, setup1.Child, setup1.DC, brush);
215+
instructionMap[variant.Instructions[1]](setup1.Host, setup1.Child, setup1.DC, brush);
216+
instructionMap[variant.Instructions[2]](setup1.Host, setup1.Child, setup1.DC, brush);
217+
Assert.IsNull(brush.DataContext, $"{variant.Label}1. once it is attached to multiple \"parent\", dc should no longer propagate");
218+
219+
setup0.Child.Foreground = null;
220+
setup1.Child.Foreground = null;
221+
var setup2 = new
222+
{
223+
Host = new Border(),
224+
Child = new Control(),
225+
DC = new { Data = $"Context {variant.Label}2" },
226+
};
227+
instructionMap[variant.Instructions[0]](setup2.Host, setup2.Child, setup2.DC, brush);
228+
instructionMap[variant.Instructions[1]](setup2.Host, setup2.Child, setup2.DC, brush);
229+
instructionMap[variant.Instructions[2]](setup2.Host, setup2.Child, setup2.DC, brush);
230+
Assert.IsNull(brush.DataContext, $"{variant.Label}2. once it has been attached to multiple \"parent\", dc shouldn't propagate anymore even if we only have a single parent now");
231+
}
232+
}
233+
#endif
68234
}

src/Uno.UI/UI/Xaml/DependencyObjectStore.Binder.cs

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,17 @@ public partial class DependencyObjectStore
3838
{
3939
private readonly object _gate = new object();
4040

41-
private HashtableEx? _childrenBindableMap;
41+
private HashtableEx? _childrenBindableMap; // maps DependencyProperty to _childrenBindable[index]
4242
private List<object?>? _childrenBindable;
43+
private object? _associatedParent; // see note in AssociateParent(object)
4344

4445
private HashtableEx ChildrenBindableMap => _childrenBindableMap ??= new HashtableEx(DependencyPropertyComparer.Default);
4546
private List<object?> ChildrenBindable => _childrenBindable ??= new List<object?>();
4647

4748
private bool _isApplyingDataContextBindings;
4849
private bool _bindingsSuspended;
4950
private readonly DependencyProperty _dataContextProperty;
51+
private bool _inheritanceContextEnabled = true;
5052

5153
#if ENABLE_LEGACY_DO_TP_SUPPORT
5254
private ManagedWeakReference? _templatedParentWeakRef;
@@ -145,7 +147,16 @@ private void ApplyChildrenBindable(object? inheritedValue)
145147
}
146148

147149
private void SetInheritedDataContext(object? dataContext)
148-
=> SetValue(_dataContextProperty!, dataContext, DependencyPropertyValuePrecedences.Inheritance, _properties.DataContextPropertyDetails);
150+
{
151+
if (_inheritanceContextEnabled)
152+
{
153+
SetValue(_dataContextProperty!, dataContext, DependencyPropertyValuePrecedences.Inheritance, _properties.DataContextPropertyDetails);
154+
}
155+
else
156+
{
157+
ClearValue(_dataContextProperty!, DependencyPropertyValuePrecedences.Inheritance);
158+
}
159+
}
149160

150161
/// <summary>
151162
/// Apply load-time binding updates. Processes the x:Bind markup for the current FrameworkElement, applies load-time ElementName bindings, and updates ResourceBindings.
@@ -574,11 +585,31 @@ private void SetChildrenBindableValue(DependencyPropertyDetails propertyDetails,
574585
{
575586
if (IsCandidateChild(value))
576587
{
577-
ChildrenBindable[GetOrCreateChildBindablePropertyIndex(propertyDetails.Property)] = value;
588+
var index = GetOrCreateChildBindablePropertyIndex(propertyDetails.Property);
589+
UpdateChildBindable(index, value);
578590
}
579591
else if (TryGetChildBindablePropertyIndex(propertyDetails.Property, out var index))
580592
{
581-
ChildrenBindable[index] = null;
593+
// clear the old value if the new value is not a candidate child, to avoid keeping stale reference.
594+
UpdateChildBindable(index, null);
595+
}
596+
597+
void UpdateChildBindable(int index, object? newValue)
598+
{
599+
var oldValue = ChildrenBindable[index];
600+
if (ReferenceEquals(oldValue, newValue)) return;
601+
602+
if (oldValue is IMultiParentShareableDependencyObject &&
603+
oldValue is IDependencyObjectStoreProvider { Store: { _inheritanceContextEnabled: true } oldStore })
604+
{
605+
oldStore.UnassociateParent(ActualInstance);
606+
}
607+
ChildrenBindable[index] = newValue;
608+
if (newValue is IMultiParentShareableDependencyObject &&
609+
newValue is IDependencyObjectStoreProvider { Store: { _inheritanceContextEnabled: true } newStore })
610+
{
611+
newStore.AssociateParent(ActualInstance);
612+
}
582613
}
583614
}
584615

@@ -615,6 +646,44 @@ private bool TryGetChildBindablePropertyIndex(DependencyProperty property, out i
615646
return false;
616647
}
617648

649+
private void AssociateParent(object? parent)
650+
{
651+
if (parent == null) return;
652+
if (!_inheritanceContextEnabled) return;
653+
654+
// the code below is used to mimic the behavior of: CMultiParentShareableDependencyObject::GetMentor
655+
// we are omitting the exception cases with:
656+
// If we have multiple parents, then InheritanceContext is turned off permanently except in the cases
657+
// of ResourceDictionary and ContentControl. It is ok to have multiple parents as long as
658+
// the first parent is a ResourceDictionary, or if the first parent is a ContentControl and there are at most
659+
// two parents.
660+
// todo: if we are to implement that in the future, we should promote `object? _associatedParent` into a `List/HashSet<object?>? _associatedParents`
661+
662+
if (_associatedParent == null)
663+
{
664+
_associatedParent = parent;
665+
}
666+
else
667+
{
668+
// if there are multiple parents (would be if we count the previous one `_associatedParent` and the current one `parent`),
669+
// it means that the current instance is shared across multiple owners/parents,
670+
// which means that it should no longer participate in any dc propagation.
671+
_inheritanceContextEnabled = false;
672+
673+
ClearInheritedDataContext();
674+
_associatedParent = null;
675+
}
676+
}
677+
private void UnassociateParent(object? parent)
678+
{
679+
if (parent == null) return;
680+
if (!_inheritanceContextEnabled) return;
681+
682+
if (ReferenceEquals(_associatedParent, parent))
683+
{
684+
_associatedParent = null;
685+
}
686+
}
618687

619688
public BindingExpression GetBindingExpression(DependencyProperty dependencyProperty)
620689
=> _properties.GetBindingExpression(dependencyProperty);

src/Uno.UI/UI/Xaml/DependencyObjectStore.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,11 @@ private void InnerSetValue(DependencyProperty property, object? value, Dependenc
532532
TryClearBinding(value, propertyDetails);
533533
}
534534

535+
if (!_inheritanceContextEnabled && precedence == DependencyPropertyValuePrecedences.Inheritance)
536+
{
537+
value = DependencyProperty.UnsetValue;
538+
}
539+
535540
var previousValue = GetValue(propertyDetails);
536541
var previousPrecedence = GetCurrentHighestValuePrecedence(propertyDetails);
537542

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace Microsoft.UI.Xaml;
2+
3+
internal interface IMultiParentShareableDependencyObject;
4+
5+
/* This interface is used to mark DependencyObjects that can be shared across multiple "parents".
6+
* While they CAN participate in data-context inheritance/propagation,
7+
* this only works until they are shared by more than one parent.
8+
* And, once multiple parents are detected, the DC inheritance/propagation is forever disabled on this object.
9+
*/

src/Uno.UI/UI/Xaml/Media/Animation/Transition.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ namespace Microsoft.UI.Xaml.Media.Animation
1515
/// Transition : Based on WinRT Transition
1616
/// (https://msdn.microsoft.com/en-us/library/windows/apps/windows.ui.xaml.media.animation.transition.Aspx)
1717
/// </summary>
18-
public partial class Transition : DependencyObject
18+
public partial class Transition : DependencyObject, IMultiParentShareableDependencyObject
1919
{
2020
private Transform _elementTransform;
2121

src/Uno.UI/UI/Xaml/Media/Brush.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
namespace Microsoft.UI.Xaml.Media
1515
{
1616
[TypeConverter(typeof(BrushConverter))]
17-
public partial class Brush : DependencyObject
17+
public partial class Brush : DependencyObject, IMultiParentShareableDependencyObject
1818
{
1919
private WeakEventHelper.WeakEventCollection? _invalidateRenderHandlers;
2020

src/Uno.UI/UI/Xaml/Media/GeneralTransform.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
namespace Microsoft.UI.Xaml.Media
77
{
8-
public partial class GeneralTransform : DependencyObject
8+
public partial class GeneralTransform : DependencyObject, IMultiParentShareableDependencyObject
99
{
1010
protected GeneralTransform() { }
1111

src/Uno.UI/UI/Xaml/Media/ImageSource.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
namespace Microsoft.UI.Xaml.Media
1919
{
2020
[TypeConverter(typeof(ImageSourceConverter))]
21-
public partial class ImageSource : DependencyObject, IDisposable
21+
public partial class ImageSource : DependencyObject, IMultiParentShareableDependencyObject, IDisposable
2222
{
2323
private protected ImageData _imageData = ImageData.Empty;
2424

src/Uno.UI/UI/Xaml/Media/Projection.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace Microsoft.UI.Xaml.Media;
88
/// <summary>
99
/// Provides a base class for projections, which describe how to transform an object in 3-D space using perspective transforms.
1010
/// </summary>
11-
public partial class Projection : DependencyObject
11+
public partial class Projection : DependencyObject, IMultiParentShareableDependencyObject
1212
{
1313
private WeakReference<UIElement> _owner;
1414

src/Uno.UI/UI/Xaml/Media/Shadow.cs

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
1-
namespace Microsoft.UI.Xaml.Media
2-
{
3-
/// <summary>
4-
/// The base class for shadow effects that can be applied to a XAML element.
5-
/// </summary>
6-
public partial class Shadow : DependencyObject
7-
{
8-
}
9-
}
1+
namespace Microsoft.UI.Xaml.Media;
2+
3+
/// <summary>
4+
/// The base class for shadow effects that can be applied to a XAML element.
5+
/// </summary>
6+
public partial class Shadow : DependencyObject, IMultiParentShareableDependencyObject;

0 commit comments

Comments
 (0)