Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Issue 17744 fix - DataContext Ancestor Inherited Value Change #17755

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion src/Avalonia.Base/PropertyStore/ValueStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -656,7 +656,32 @@ public void OnAncestorInheritedValueChanged<T>(

// 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<T>.Default.Equals(tInst, newValue))
{
return;
}
}

using var notifying = PropertyNotifying.Start(Owner, property);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls\Avalonia.Controls.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj" />
<ProjectReference Include="..\..\src\tools\Avalonia.Generators\Avalonia.Generators.csproj" />
<ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj" />
</ItemGroup>
<ItemGroup>
Expand All @@ -36,6 +37,14 @@
<Link>PlatformFactAttribute.cs</Link>
</Compile>
</ItemGroup>
<ItemGroup>
<Compile Update="Xaml\CompiledBindingsTests\TestChildControl.axaml.cs">
<DependentUpon>TestChildControl.axaml</DependentUpon>
</Compile>
<Compile Update="Xaml\CompiledBindingsTests\TestWindow.axaml.cs">
<DependentUpon>TestWindow.axaml</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<PackageReference Update="xunit.runner.console" Version="2.9.2">
<PrivateAssets>all</PrivateAssets>
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:datalocal='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml.CompiledBindingsTests'
DataContext='{Binding Source={x:Static datalocal:TestingViewModelLocator.ModelLocator}, Path=TestingViewModelInstance}'
>
<Grid DataContext='{Binding .}'>
<DockPanel DataContext='{Binding .}'>
<Grid DataContext='{Binding .}'>
<ListBox x:Name='testingListBox'
DataContext='{Binding .}'
ItemsSource='{Binding ItemsList}'
/>
</Grid>
</DockPanel>
</Grid>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.NotNull(window);
window.Show();

var listBox = window.Get<ListBox>("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<TestingViewModel>(testWindow.DataContext);
var rootControl = testWindow.TestRootControlInstance;
Assert.NotNull(rootControl);
Assert.IsType<TestingViewModel>(rootControl.DataContext);
var childControl = rootControl.TestChildControlInstance;
Assert.NotNull(childControl);
Assert.IsType<TestingViewModel>(childControl.DataContext);
var testListBox = childControl.TestListBox;
Assert.NotNull(testListBox);
Assert.IsType<TestingViewModel>(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<string> ItemsList { get; private set; } = new();

public TestingViewModel()
{
for (var i = 0; i < 20; i++)
{
ItemsList.Add(i.ToString(CultureInfo.InvariantCulture));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml.CompiledBindingsTests"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Avalonia.Markup.Xaml.UnitTests.Xaml.CompiledBindingsTests.TestChildControl"
x:CompileBindings="True"
x:DataType="local:TestingViewModel"


>
<Grid>
<DockPanel>
<ListBox
DataContext="{Binding Path=.}"
ItemsSource="{Binding Path=ItemsList}"
x:Name="TestListBox"
/>
</DockPanel>
</Grid>
</UserControl>
Original file line number Diff line number Diff line change
@@ -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<ListBox>("TestListBox");
}

public ListBox TestListBox { get; private set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml.CompiledBindingsTests"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Avalonia.Markup.Xaml.UnitTests.Xaml.CompiledBindingsTests.TestRootControl"
x:CompileBindings="True"
x:DataType="local:TestingViewModel"
>
<Grid>
<DockPanel>
<Grid>
<local:TestChildControl x:Name="TestChildControlInstance"/>
</Grid>
</DockPanel>
</Grid>
</UserControl>
Original file line number Diff line number Diff line change
@@ -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<TestChildControl>("TestChildControlInstance");
}

public TestChildControl TestChildControlInstance { get; private set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml.CompiledBindingsTests"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Avalonia.Markup.Xaml.UnitTests.Xaml.CompiledBindingsTests.TestWindow"
x:CompileBindings="True"
x:DataType="local:TestingViewModel"
DataContext="{Binding (local:TestingViewModelLocator).TestingViewModelInstance, Source={StaticResource ViewModelLocator}}"

>
<Grid>
<DockPanel>
<local:TestRootControl x:Name="TestRootControlInstance"/>
</DockPanel>
</Grid>
</Window>
Original file line number Diff line number Diff line change
@@ -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<TestRootControl>("TestRootControlInstance");
}

public TestRootControl TestRootControlInstance { get; private set; }
}
20 changes: 20 additions & 0 deletions tests/Avalonia.UnitTests/UnitTestApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IToolTipService>())?.Dispose();

scope.Dispose();
Dispatcher.ResetForUnitTests();
SynchronizationContext.SetSynchronizationContext(oldContext);
});
}

public override void RegisterServices()
{
AvaloniaLocator.CurrentMutable
Expand Down
Loading