diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs index 789383b860a..ca16870dce6 100644 --- a/src/Avalonia.Base/PropertyStore/ValueStore.cs +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -656,7 +656,32 @@ public void OnAncestorInheritedValueChanged( // If the inherited value is set locally, propagation stops here. if (_effectiveValues.ContainsKey(property)) - return; + { + // BUT, we also need to double-check whether the affected property is the Data-Context AND whether the Data-Context is bound. + // That is, a bound data-context needs re-evaluating even if it's bound locally + // Because a change in the data-context changes what it means for the data-context to be bound + if (property != StyledElement.DataContextProperty) + { + return; + } + var bindingExpression = BindingOperations.GetBindingExpressionBase(Owner, property) as BindingExpression; + if (bindingExpression is null) + { + return; + } + // Both of these checks returning false means the affected property is the Data-Context, and it is bound. + // So, continue, because the data-context changing from an inherited context changes the meaning + // of what the property should be after binding. + + // But now we need to handle cases where the DataContext is intending to change (that is, the path is not `.` ) + // easiest way is to just get the value of the binding expression and check whether it isn't the same as the newValue + + var bindingValue = bindingExpression.GetValue(); + if (bindingValue is T tInst && !EqualityComparer.Default.Equals(tInst, newValue)) + { + return; + } + } using var notifying = PropertyNotifying.Start(Owner, property); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj index bd63ddd4968..341c5f1189d 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj @@ -18,6 +18,7 @@ + @@ -36,6 +37,14 @@ PlatformFactAttribute.cs + + + TestChildControl.axaml + + + TestWindow.axaml + + all diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/CompiledBindingsTests/BindingTests_InheritedDataContext.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/CompiledBindingsTests/BindingTests_InheritedDataContext.cs new file mode 100644 index 00000000000..9b2d247b879 --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/CompiledBindingsTests/BindingTests_InheritedDataContext.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Xaml.CompiledBindingsTests +{ + // Unit Tests for Github Issue 17755 + public class BindingTests_InheritedDataContext : XamlTestBase + { + [Fact] + public void Binding_Inherited_Data_Context_Properly_Notifies_Children() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + + var xaml = @" + + + + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + Assert.NotNull(window); + window.Show(); + + var listBox = window.Get("testingListBox"); + Assert.NotNull(listBox); + Assert.NotNull(listBox.DataContext); + Assert.NotNull(listBox.ItemsSource); + } + } + + [Fact] + public void Compiled_Binding_Inherited_Data_Context_Properly_Notifies_Children() + { + UnitTestApplication testApp = new(TestServices.StyledWindow); + testApp.Resources.Add("ViewModelLocator", new TestingViewModelLocator()); + using (testApp.StartInstance()) + { + TestWindow testWindow = new(); + testWindow.Show(); + Assert.IsType(testWindow.DataContext); + var rootControl = testWindow.TestRootControlInstance; + Assert.NotNull(rootControl); + Assert.IsType(rootControl.DataContext); + var childControl = rootControl.TestChildControlInstance; + Assert.NotNull(childControl); + Assert.IsType(childControl.DataContext); + var testListBox = childControl.TestListBox; + Assert.NotNull(testListBox); + Assert.IsType(testListBox.DataContext); + Assert.NotNull(testListBox.ItemsSource); + } + } + } + + // to facilitate Binding Source as a Static Resource + public class TestingViewModelLocator + { + public TestingViewModel TestingViewModelInstance { get; set; } = new(); + + public static TestingViewModelLocator ModelLocator { get; set; } = new(); + } + + public class TestingViewModel : INotifyPropertyChanged + { + public event PropertyChangedEventHandler PropertyChanged; + + public AvaloniaList ItemsList { get; private set; } = new(); + + public TestingViewModel() + { + for (var i = 0; i < 20; i++) + { + ItemsList.Add(i.ToString(CultureInfo.InvariantCulture)); + } + } + } +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/CompiledBindingsTests/TestChildControl.axaml b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/CompiledBindingsTests/TestChildControl.axaml new file mode 100644 index 00000000000..15115278125 --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/CompiledBindingsTests/TestChildControl.axaml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/CompiledBindingsTests/TestChildControl.axaml.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/CompiledBindingsTests/TestChildControl.axaml.cs new file mode 100644 index 00000000000..891efcbccac --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/CompiledBindingsTests/TestChildControl.axaml.cs @@ -0,0 +1,22 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Avalonia.Markup.Xaml.UnitTests.Xaml.CompiledBindingsTests; + +public partial class TestChildControl : UserControl +{ + public TestChildControl() + { + InitializeComponent(); + } + + internal void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + + TestListBox = this.Get("TestListBox"); + } + + public ListBox TestListBox { get; private set; } +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/CompiledBindingsTests/TestRootControl.axaml b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/CompiledBindingsTests/TestRootControl.axaml new file mode 100644 index 00000000000..e9a5cd63d2b --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/CompiledBindingsTests/TestRootControl.axaml @@ -0,0 +1,18 @@ + + + + + + + + + diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/CompiledBindingsTests/TestRootControl.axaml.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/CompiledBindingsTests/TestRootControl.axaml.cs new file mode 100644 index 00000000000..70ac909c5f1 --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/CompiledBindingsTests/TestRootControl.axaml.cs @@ -0,0 +1,22 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Avalonia.Markup.Xaml.UnitTests.Xaml.CompiledBindingsTests; + +public partial class TestRootControl : UserControl +{ + public TestRootControl() + { + InitializeComponent(); + } + + internal void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + + TestChildControlInstance = this.Get("TestChildControlInstance"); + } + + public TestChildControl TestChildControlInstance { get; private set; } +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/CompiledBindingsTests/TestWindow.axaml b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/CompiledBindingsTests/TestWindow.axaml new file mode 100644 index 00000000000..1f33d4a97f0 --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/CompiledBindingsTests/TestWindow.axaml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/CompiledBindingsTests/TestWindow.axaml.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/CompiledBindingsTests/TestWindow.axaml.cs new file mode 100644 index 00000000000..85ae1216661 --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/CompiledBindingsTests/TestWindow.axaml.cs @@ -0,0 +1,22 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Avalonia.Markup.Xaml.UnitTests.Xaml.CompiledBindingsTests; + +public partial class TestWindow : Window +{ + public TestWindow() + { + InitializeComponent(); + } + + internal void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + + TestRootControlInstance = this.Get("TestRootControlInstance"); + } + + public TestRootControl TestRootControlInstance { get; private set; } +} diff --git a/tests/Avalonia.UnitTests/UnitTestApplication.cs b/tests/Avalonia.UnitTests/UnitTestApplication.cs index 453c6c05d1d..6f90bcf6899 100644 --- a/tests/Avalonia.UnitTests/UnitTestApplication.cs +++ b/tests/Avalonia.UnitTests/UnitTestApplication.cs @@ -58,6 +58,26 @@ public static IDisposable Start(TestServices services = null) }); } + public IDisposable StartInstance() + { + var scope = AvaloniaLocator.EnterScope(); + var oldContext = SynchronizationContext.Current; + Dispatcher.ResetForUnitTests(); + return Disposable.Create(() => + { + if (Dispatcher.UIThread.CheckAccess()) + { + Dispatcher.UIThread.RunJobs(); + } + + ((ToolTipService)AvaloniaLocator.Current.GetService())?.Dispose(); + + scope.Dispose(); + Dispatcher.ResetForUnitTests(); + SynchronizationContext.SetSynchronizationContext(oldContext); + }); + } + public override void RegisterServices() { AvaloniaLocator.CurrentMutable