From f44628957487cbe3bf7be4590813097b369d7a10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20S=CC=8Colte=CC=81s?= Date: Thu, 9 Jan 2025 13:11:07 +0100 Subject: [PATCH 01/27] Use DynamicResource for SortIcon setter --- src/Avalonia.Controls.TreeDataGrid/Themes/Generic.axaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.TreeDataGrid/Themes/Generic.axaml b/src/Avalonia.Controls.TreeDataGrid/Themes/Generic.axaml index ecbd716b..662fc59f 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Themes/Generic.axaml +++ b/src/Avalonia.Controls.TreeDataGrid/Themes/Generic.axaml @@ -146,12 +146,12 @@ From 4bc9549454a49dc33e66d4fd5cb9561850d282e4 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 29 Jan 2025 14:15:25 +0000 Subject: [PATCH 02/27] When using Auto sized columns dont cause layout cycles due to columns having NaN as actual size. --- .../Models/TreeDataGrid/ColumnBase`1.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Avalonia.Controls.TreeDataGrid/Models/TreeDataGrid/ColumnBase`1.cs b/src/Avalonia.Controls.TreeDataGrid/Models/TreeDataGrid/ColumnBase`1.cs index 5b21ff2c..6ceb198c 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Models/TreeDataGrid/ColumnBase`1.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Models/TreeDataGrid/ColumnBase`1.cs @@ -134,6 +134,12 @@ bool IUpdateColumnLayout.CommitActualWidth() var oldWidth = ActualWidth; ActualWidth = width; _starWidthWasConstrained = false; + + if (double.IsNaN(oldWidth) && double.IsNaN(ActualWidth)) + { + return false; + } + return !MathUtilities.AreClose(oldWidth, ActualWidth); } From a731b8ab472a3ca9419c825e39c8cd7c16f4914e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 29 Jan 2025 15:04:05 +0000 Subject: [PATCH 03/27] add a comment to explain the fix and why its important. --- .../Models/TreeDataGrid/ColumnBase`1.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Avalonia.Controls.TreeDataGrid/Models/TreeDataGrid/ColumnBase`1.cs b/src/Avalonia.Controls.TreeDataGrid/Models/TreeDataGrid/ColumnBase`1.cs index 6ceb198c..4d2e9a94 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Models/TreeDataGrid/ColumnBase`1.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Models/TreeDataGrid/ColumnBase`1.cs @@ -135,6 +135,12 @@ bool IUpdateColumnLayout.CommitActualWidth() ActualWidth = width; _starWidthWasConstrained = false; + // MathUtilites.AreClose will return true for this condition. + // If the user has auto columns that are not yet realized, then the + // _autoWidth will remain NaN. + // This will lead to an endless layout cycle causing the whole UI + // to have degraded performance, until all columns have an actual value + // set for _autoWidth. if (double.IsNaN(oldWidth) && double.IsNaN(ActualWidth)) { return false; From 59288c9754e0a033cb2e4841c4435fae92dc841b Mon Sep 17 00:00:00 2001 From: Maxwell Katz Date: Fri, 7 Mar 2025 12:11:57 +0900 Subject: [PATCH 04/27] Update readme.md --- readme.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/readme.md b/readme.md index ea2f9445..6f8ed38e 100644 --- a/readme.md +++ b/readme.md @@ -20,10 +20,6 @@ An example of `TreeDataGrid` displaying flat data: ## Current Status -The control is currently in *early beta*. As such there will be bugs, missing features and lacking docs, but the control should be generally usable and performant. - -**Note**: - We accept issues and pull requests but we answer and review only pull requests and issues that are created by our customers. It's a quite big project and servicing all issues and pull requests will require more time than we have. But feel free to open issues and pull requests because they may be useful for us! ## Getting Started From 2313902c7e9b4fa46834262bac41d4e3832196e4 Mon Sep 17 00:00:00 2001 From: Al12rs <26797547+Al12rs@users.noreply.github.com> Date: Fri, 28 Mar 2025 11:55:17 +0100 Subject: [PATCH 05/27] Create tests for `TypedBinding` using binding.Write for updates --- .../Bindings/TypedBindingTests.cs | 453 ++++++++++++++++++ 1 file changed, 453 insertions(+) create mode 100644 tests/Avalonia.Controls.TreeDataGrid.Tests/Bindings/TypedBindingTests.cs diff --git a/tests/Avalonia.Controls.TreeDataGrid.Tests/Bindings/TypedBindingTests.cs b/tests/Avalonia.Controls.TreeDataGrid.Tests/Bindings/TypedBindingTests.cs new file mode 100644 index 00000000..416c424a --- /dev/null +++ b/tests/Avalonia.Controls.TreeDataGrid.Tests/Bindings/TypedBindingTests.cs @@ -0,0 +1,453 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using Avalonia.Data; +using Avalonia.Experimental.Data; +using Xunit; + +namespace Avalonia.Controls.TreeDataGridTests.Bindings; + +public class TypedBindingTests +{ + [Fact] + public void OneWay_Binding_Single_Link_Should_Get_Value() + { + // Arrange + var source = new TestViewModel { Name = "Test" }; + var target = new TestTarget(); + + // Create a binding with a single link (source.Name) + var binding = TypedBinding.OneWay(vm => vm.Name); + var expression = binding.Instance(source); + + // Act + target.Bind(TestTarget.TextProperty, expression); + + // Assert + Assert.Equal("Test", target.Text); + } + + [Fact] + public void OneWay_Binding_Single_Link_Should_Listen_To_Changes() + { + // Arrange + var source = new TestViewModel { Name = "Test" }; + var target = new TestTarget(); + + // Create a binding with a single link (source.Name) + var binding = TypedBinding.OneWay(vm => vm.Name); + var expression = binding.Instance(source); + + // Act + target.Bind(TestTarget.TextProperty, expression); + source.Name = "Updated"; + + // Assert + Assert.Equal("Updated", target.Text); + } + + [Fact] + public void TwoWay_Binding_Single_Link_Should_Listen_And_Write() + { + // Arrange + var source = new TestViewModel { Name = "Test" }; + var target = new TestTarget(); + + // Create a binding with a single link (source.Name) + var binding = TypedBinding.TwoWay(vm => vm.Name); + var expression = binding.Instance(source, BindingMode.TwoWay); + + // Act - set up binding + target.Bind(TestTarget.TextProperty, expression); + + // Act - change source + source.Name = "Updated"; + + // Assert + Assert.Equal("Updated", target.Text); + + // Act - change target + target.Text = "UpdatedFromTarget"; + binding.Write!.Invoke(source, target.Text); + + // Assert + Assert.Equal("UpdatedFromTarget", source.Name); + } + + [Fact] + public void OneWay_Binding_Two_Links_Should_Get_Value() + { + // Arrange + var child = new TestViewModel { Name = "Child" }; + var source = new TestViewModel { Name = "Parent", Child = child }; + var target = new TestTarget(); + + // Create a binding with two links (source.Child.Name) + var binding = TypedBinding.OneWay(vm => vm.Child!.Name); + var expression = binding.Instance(source); + + // Act + target.Bind(TestTarget.TextProperty, expression); + + // Assert + Assert.Equal("Child", target.Text); + } + + [Fact] + public void OneWay_Binding_Two_Links_Should_Listen_To_Changes() + { + // Arrange + var child = new TestViewModel { Name = "Child" }; + var source = new TestViewModel { Name = "Parent", Child = child }; + var target = new TestTarget(); + + // Create a binding with two links (source.Child.Name) + var binding = TypedBinding.OneWay(vm => vm.Child!.Name); + var expression = binding.Instance(source); + + // Act - initialize binding + target.Bind(TestTarget.TextProperty, expression); + + // Act - update leaf property + child.Name = "UpdatedChild"; + + // Assert + Assert.Equal("UpdatedChild", target.Text); + + // Act - update intermediate property + var newChild = new TestViewModel { Name = "NewChild" }; + source.Child = newChild; + + // Assert + Assert.Equal("NewChild", target.Text); + } + + [Fact] + public void TwoWay_Binding_Two_Links_Should_Listen_And_Write() + { + // Arrange + var child = new TestViewModel { Name = "Child" }; + var source = new TestViewModel { Name = "Parent", Child = child }; + var target = new TestTarget(); + + // Create a binding with two links (source.Child.Name) + var binding = TypedBinding.TwoWay(vm => vm.Child!.Name); + var expression = binding.Instance(source, BindingMode.TwoWay); + + // Act - initialize binding + target.Bind(TestTarget.TextProperty, expression); + + // Act - update from source + child.Name = "UpdatedChild"; + + // Assert + Assert.Equal("UpdatedChild", target.Text); + + // Act - update from target + target.Text = "UpdatedFromTarget"; + binding.Write!.Invoke(source, target.Text); + + // Assert + Assert.Equal("UpdatedFromTarget", child.Name); + + // Act - change intermediate link + var newChild = new TestViewModel { Name = "NewChild" }; + source.Child = newChild; + + // Assert + Assert.Equal("NewChild", target.Text); + + // Act - update from target after changing intermediate + target.Text = "FinalUpdate"; + binding.Write!.Invoke(source, target.Text); + + // Assert + Assert.Equal("FinalUpdate", newChild.Name); + } + + [Fact] + public void OneWay_Binding_Three_Links_Should_Get_Value() + { + // Arrange + var grandChild = new TestViewModel { Name = "GrandChild" }; + var child = new TestViewModel { Name = "Child", Child = grandChild }; + var source = new TestViewModel { Name = "Parent", Child = child }; + var target = new TestTarget(); + + // Create a binding with three links (source.Child.Child.Name) + var binding = TypedBinding.OneWay(vm => vm.Child!.Child!.Name); + var expression = binding.Instance(source); + + // Act + target.Bind(TestTarget.TextProperty, expression); + + // Assert + Assert.Equal("GrandChild", target.Text); + } + + [Fact] + public void OneWay_Binding_Three_Links_Should_Listen_To_Changes() + { + // Arrange + var grandChild = new TestViewModel { Name = "GrandChild" }; + var child = new TestViewModel { Name = "Child", Child = grandChild }; + var source = new TestViewModel { Name = "Parent", Child = child }; + var target = new TestTarget(); + + // Create a binding with three links (source.Child.Child.Name) + var binding = TypedBinding.OneWay(vm => vm.Child!.Child!.Name); + var expression = binding.Instance(source); + + // Act - initialize binding + target.Bind(TestTarget.TextProperty, expression); + + // Act - update leaf property + grandChild.Name = "UpdatedGrandChild"; + + // Assert + Assert.Equal("UpdatedGrandChild", target.Text); + + // Act - update middle property + var newGrandChild = new TestViewModel { Name = "NewGrandChild" }; + child.Child = newGrandChild; + + // Assert + Assert.Equal("NewGrandChild", target.Text); + + // Act - update root property + var newChildWithGrandChild = new TestViewModel { + Name = "NewChild", + Child = new TestViewModel { Name = "NewestGrandChild" } + }; + source.Child = newChildWithGrandChild; + + // Assert + Assert.Equal("NewestGrandChild", target.Text); + } + + [Fact] + public void TwoWay_Binding_Three_Links_Should_Listen_And_Write() + { + // Arrange + var grandChild = new TestViewModel { Name = "GrandChild" }; + var child = new TestViewModel { Name = "Child", Child = grandChild }; + var source = new TestViewModel { Name = "Parent", Child = child }; + var target = new TestTarget(); + + // Create a binding with three links (source.Child.Child.Name) + var binding = TypedBinding.TwoWay(vm => vm.Child!.Child!.Name); + var expression = binding.Instance(source, BindingMode.TwoWay); + + // Act - initialize binding + target.Bind(TestTarget.TextProperty, expression); + + // Act - set from target + target.Text = "UpdatedFromTarget"; + binding.Write!.Invoke(source, target.Text); + + // Assert + Assert.Equal("UpdatedFromTarget", grandChild.Name); + + // Act - change intermediate node and update from target + var newGrandChild = new TestViewModel { Name = "NewGrandChild" }; + child.Child = newGrandChild; + + // Assert initial update + Assert.Equal("NewGrandChild", target.Text); + + // Act - update again from target + target.Text = "AfterIntermediateChange"; + binding.Write!.Invoke(source, target.Text); + + // Assert + Assert.Equal("AfterIntermediateChange", newGrandChild.Name); + } + + [Fact] + public void OneWay_Binding_Four_Links_Should_Get_Value() + { + // Arrange + var greatGrandChild = new TestViewModel { Name = "GreatGrandChild" }; + var grandChild = new TestViewModel { Name = "GrandChild", Child = greatGrandChild }; + var child = new TestViewModel { Name = "Child", Child = grandChild }; + var source = new TestViewModel { Name = "Parent", Child = child }; + var target = new TestTarget(); + + // Create a binding with four links (source.Child.Child.Child.Name) + var binding = TypedBinding.OneWay(vm => vm.Child!.Child!.Child!.Name); + var expression = binding.Instance(source); + + // Act + target.Bind(TestTarget.TextProperty, expression); + + // Assert + Assert.Equal("GreatGrandChild", target.Text); + } + + [Fact] + public void OneWay_Binding_Four_Links_Should_Listen_To_Changes() + { + // Arrange + var greatGrandChild = new TestViewModel { Name = "GreatGrandChild" }; + var grandChild = new TestViewModel { Name = "GrandChild", Child = greatGrandChild }; + var child = new TestViewModel { Name = "Child", Child = grandChild }; + var source = new TestViewModel { Name = "Parent", Child = child }; + var target = new TestTarget(); + + // Create a binding with four links (source.Child.Child.Child.Name) + var binding = TypedBinding.OneWay(vm => vm.Child!.Child!.Child!.Name); + var expression = binding.Instance(source); + + // Act - initialize binding + target.Bind(TestTarget.TextProperty, expression); + + // Act - update leaf node + greatGrandChild.Name = "UpdatedGreatGrandChild"; + + // Assert + Assert.Equal("UpdatedGreatGrandChild", target.Text); + + // Act - update middle node + var newGreatGrandChild = new TestViewModel { Name = "NewGreatGrandChild" }; + grandChild.Child = newGreatGrandChild; + + // Assert + Assert.Equal("NewGreatGrandChild", target.Text); + + // Act - update higher node + var newGrandChildWithChild = new TestViewModel + { + Name = "NewGrandChild", + Child = new TestViewModel { Name = "BrandNewGreatGrandChild" } + }; + child.Child = newGrandChildWithChild; + + // Assert + Assert.Equal("BrandNewGreatGrandChild", target.Text); + } + + [Fact] + public void TwoWay_Binding_Four_Links_Should_Listen_And_Write() + { + // Arrange + var greatGrandChild = new TestViewModel { Name = "GreatGrandChild" }; + var grandChild = new TestViewModel { Name = "GrandChild", Child = greatGrandChild }; + var child = new TestViewModel { Name = "Child", Child = grandChild }; + var source = new TestViewModel { Name = "Parent", Child = child }; + var target = new TestTarget(); + + // Create a binding with four links (source.Child.Child.Child.Name) + var binding = TypedBinding.TwoWay(vm => vm.Child!.Child!.Child!.Name); + var expression = binding.Instance(source, BindingMode.TwoWay); + + // Act - initialize binding + target.Bind(TestTarget.TextProperty, expression); + + // Act - set from target + target.Text = "UpdatedFromTarget"; + binding.Write!.Invoke(source, target.Text); + + // Assert + Assert.Equal("UpdatedFromTarget", greatGrandChild.Name); + + // Act - change leaf node + var newGreatGrandChild = new TestViewModel { Name = "NewGreatGrandChild" }; + grandChild.Child = newGreatGrandChild; + + // Assert + Assert.Equal("NewGreatGrandChild", target.Text); + + // Act - update from target + target.Text = "AfterLeafChange"; + binding.Write!.Invoke(source, target.Text); + + // Assert + Assert.Equal("AfterLeafChange", newGreatGrandChild.Name); + + // Act - update middle node + var newGrandChildWithChild = new TestViewModel + { + Name = "NewGrandChild", + Child = new TestViewModel { Name = "BrandNewGreatGrandChild" } + }; + child.Child = newGrandChildWithChild; + + // Assert + Assert.Equal("BrandNewGreatGrandChild", target.Text); + + // Act - update from target + target.Text = "AfterMiddleChange"; + binding.Write!.Invoke(source, target.Text); + + // Assert + Assert.Equal("AfterMiddleChange", newGrandChildWithChild.Child.Name); + } + + [Fact] + public void Binding_Should_Handle_Null_Intermediate_Values() + { + // Arrange + var source = new TestViewModel { Name = "Parent", Child = null }; + var target = new TestTarget(); + + // Create a binding that will encounter a null (source.Child.Name) + var binding = TypedBinding.OneWay(vm => vm.Child!.Name); + var expression = binding.Instance(source); + + // Act - initialize binding + target.Bind(TestTarget.TextProperty, expression); + + // Assert - binding should have error state due to null reference + Assert.Null(target.Text); // Default value since binding path fails + + // Act - fix the null reference + var newChild = new TestViewModel { Name = "NewChild" }; + source.Child = newChild; + + // Assert - binding should work now + Assert.Equal("NewChild", target.Text); + } + + // Test view model implementing property change notifications + private class TestViewModel : INotifyPropertyChanged + { + private string _name = string.Empty; + private TestViewModel ?_child = null; + + public string Name + { + get => _name; + set => SetProperty(ref _name, value); + } + + public TestViewModel ?Child + { + get => _child; + set => SetProperty(ref _child, value); + } + + public event PropertyChangedEventHandler? PropertyChanged; + + protected void SetProperty(ref T field, T value, [CallerMemberName] string? propertyName = null) + { + if (!EqualityComparer.Default.Equals(field, value)) + { + field = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } + } + + // Simple target class with an AvaloniaProperty + private class TestTarget : AvaloniaObject + { + public static readonly StyledProperty TextProperty = + AvaloniaProperty.Register(nameof(Text)); + + public string Text + { + get => GetValue(TextProperty); + set => SetValue(TextProperty, value); + } + } +} \ No newline at end of file From 9aaa29511d5fd5b57b3ed7025e09bdf7e466aed8 Mon Sep 17 00:00:00 2001 From: Al12rs <26797547+Al12rs@users.noreply.github.com> Date: Fri, 28 Mar 2025 17:14:20 +0100 Subject: [PATCH 06/27] Add more TypedBinding tests to cover more failure cases --- .../Bindings/TypedBindingTests.cs | 540 ++++++++++++++---- 1 file changed, 437 insertions(+), 103 deletions(-) diff --git a/tests/Avalonia.Controls.TreeDataGrid.Tests/Bindings/TypedBindingTests.cs b/tests/Avalonia.Controls.TreeDataGrid.Tests/Bindings/TypedBindingTests.cs index 416c424a..a4f9fd2a 100644 --- a/tests/Avalonia.Controls.TreeDataGrid.Tests/Bindings/TypedBindingTests.cs +++ b/tests/Avalonia.Controls.TreeDataGrid.Tests/Bindings/TypedBindingTests.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.ComponentModel; using System.Runtime.CompilerServices; @@ -15,65 +16,65 @@ public void OneWay_Binding_Single_Link_Should_Get_Value() // Arrange var source = new TestViewModel { Name = "Test" }; var target = new TestTarget(); - + // Create a binding with a single link (source.Name) var binding = TypedBinding.OneWay(vm => vm.Name); var expression = binding.Instance(source); - + // Act target.Bind(TestTarget.TextProperty, expression); - + // Assert Assert.Equal("Test", target.Text); } - + [Fact] public void OneWay_Binding_Single_Link_Should_Listen_To_Changes() { // Arrange var source = new TestViewModel { Name = "Test" }; var target = new TestTarget(); - + // Create a binding with a single link (source.Name) var binding = TypedBinding.OneWay(vm => vm.Name); var expression = binding.Instance(source); - + // Act target.Bind(TestTarget.TextProperty, expression); source.Name = "Updated"; - + // Assert Assert.Equal("Updated", target.Text); } - + [Fact] public void TwoWay_Binding_Single_Link_Should_Listen_And_Write() { // Arrange var source = new TestViewModel { Name = "Test" }; var target = new TestTarget(); - + // Create a binding with a single link (source.Name) var binding = TypedBinding.TwoWay(vm => vm.Name); var expression = binding.Instance(source, BindingMode.TwoWay); - + // Act - set up binding target.Bind(TestTarget.TextProperty, expression); - + // Act - change source source.Name = "Updated"; - + // Assert Assert.Equal("Updated", target.Text); - + // Act - change target target.Text = "UpdatedFromTarget"; binding.Write!.Invoke(source, target.Text); - + // Assert Assert.Equal("UpdatedFromTarget", source.Name); } - + [Fact] public void OneWay_Binding_Two_Links_Should_Get_Value() { @@ -81,18 +82,18 @@ public void OneWay_Binding_Two_Links_Should_Get_Value() var child = new TestViewModel { Name = "Child" }; var source = new TestViewModel { Name = "Parent", Child = child }; var target = new TestTarget(); - + // Create a binding with two links (source.Child.Name) var binding = TypedBinding.OneWay(vm => vm.Child!.Name); var expression = binding.Instance(source); - + // Act target.Bind(TestTarget.TextProperty, expression); - + // Assert Assert.Equal("Child", target.Text); } - + [Fact] public void OneWay_Binding_Two_Links_Should_Listen_To_Changes() { @@ -100,28 +101,28 @@ public void OneWay_Binding_Two_Links_Should_Listen_To_Changes() var child = new TestViewModel { Name = "Child" }; var source = new TestViewModel { Name = "Parent", Child = child }; var target = new TestTarget(); - + // Create a binding with two links (source.Child.Name) var binding = TypedBinding.OneWay(vm => vm.Child!.Name); var expression = binding.Instance(source); - + // Act - initialize binding target.Bind(TestTarget.TextProperty, expression); - + // Act - update leaf property child.Name = "UpdatedChild"; - + // Assert Assert.Equal("UpdatedChild", target.Text); - + // Act - update intermediate property var newChild = new TestViewModel { Name = "NewChild" }; source.Child = newChild; - + // Assert Assert.Equal("NewChild", target.Text); } - + [Fact] public void TwoWay_Binding_Two_Links_Should_Listen_And_Write() { @@ -129,42 +130,42 @@ public void TwoWay_Binding_Two_Links_Should_Listen_And_Write() var child = new TestViewModel { Name = "Child" }; var source = new TestViewModel { Name = "Parent", Child = child }; var target = new TestTarget(); - + // Create a binding with two links (source.Child.Name) var binding = TypedBinding.TwoWay(vm => vm.Child!.Name); var expression = binding.Instance(source, BindingMode.TwoWay); - + // Act - initialize binding target.Bind(TestTarget.TextProperty, expression); - + // Act - update from source child.Name = "UpdatedChild"; - + // Assert Assert.Equal("UpdatedChild", target.Text); - + // Act - update from target target.Text = "UpdatedFromTarget"; binding.Write!.Invoke(source, target.Text); - + // Assert Assert.Equal("UpdatedFromTarget", child.Name); - + // Act - change intermediate link var newChild = new TestViewModel { Name = "NewChild" }; source.Child = newChild; - + // Assert Assert.Equal("NewChild", target.Text); - + // Act - update from target after changing intermediate target.Text = "FinalUpdate"; binding.Write!.Invoke(source, target.Text); - + // Assert Assert.Equal("FinalUpdate", newChild.Name); } - + [Fact] public void OneWay_Binding_Three_Links_Should_Get_Value() { @@ -173,18 +174,18 @@ public void OneWay_Binding_Three_Links_Should_Get_Value() var child = new TestViewModel { Name = "Child", Child = grandChild }; var source = new TestViewModel { Name = "Parent", Child = child }; var target = new TestTarget(); - + // Create a binding with three links (source.Child.Child.Name) var binding = TypedBinding.OneWay(vm => vm.Child!.Child!.Name); var expression = binding.Instance(source); - + // Act target.Bind(TestTarget.TextProperty, expression); - + // Assert Assert.Equal("GrandChild", target.Text); } - + [Fact] public void OneWay_Binding_Three_Links_Should_Listen_To_Changes() { @@ -193,38 +194,39 @@ public void OneWay_Binding_Three_Links_Should_Listen_To_Changes() var child = new TestViewModel { Name = "Child", Child = grandChild }; var source = new TestViewModel { Name = "Parent", Child = child }; var target = new TestTarget(); - + // Create a binding with three links (source.Child.Child.Name) var binding = TypedBinding.OneWay(vm => vm.Child!.Child!.Name); var expression = binding.Instance(source); - + // Act - initialize binding target.Bind(TestTarget.TextProperty, expression); - + // Act - update leaf property grandChild.Name = "UpdatedGrandChild"; - + // Assert Assert.Equal("UpdatedGrandChild", target.Text); - + // Act - update middle property var newGrandChild = new TestViewModel { Name = "NewGrandChild" }; child.Child = newGrandChild; - + // Assert Assert.Equal("NewGrandChild", target.Text); - + // Act - update root property - var newChildWithGrandChild = new TestViewModel { - Name = "NewChild", - Child = new TestViewModel { Name = "NewestGrandChild" } + var newChildWithGrandChild = new TestViewModel + { + Name = "NewChild", + Child = new TestViewModel { Name = "NewestGrandChild" } }; source.Child = newChildWithGrandChild; - + // Assert Assert.Equal("NewestGrandChild", target.Text); } - + [Fact] public void TwoWay_Binding_Three_Links_Should_Listen_And_Write() { @@ -233,36 +235,37 @@ public void TwoWay_Binding_Three_Links_Should_Listen_And_Write() var child = new TestViewModel { Name = "Child", Child = grandChild }; var source = new TestViewModel { Name = "Parent", Child = child }; var target = new TestTarget(); - + // Create a binding with three links (source.Child.Child.Name) var binding = TypedBinding.TwoWay(vm => vm.Child!.Child!.Name); var expression = binding.Instance(source, BindingMode.TwoWay); - + // Act - initialize binding target.Bind(TestTarget.TextProperty, expression); - + // Act - set from target target.Text = "UpdatedFromTarget"; binding.Write!.Invoke(source, target.Text); - + + // Assert Assert.Equal("UpdatedFromTarget", grandChild.Name); - + // Act - change intermediate node and update from target var newGrandChild = new TestViewModel { Name = "NewGrandChild" }; child.Child = newGrandChild; - + // Assert initial update Assert.Equal("NewGrandChild", target.Text); - + // Act - update again from target target.Text = "AfterIntermediateChange"; binding.Write!.Invoke(source, target.Text); - + // Assert Assert.Equal("AfterIntermediateChange", newGrandChild.Name); } - + [Fact] public void OneWay_Binding_Four_Links_Should_Get_Value() { @@ -272,18 +275,18 @@ public void OneWay_Binding_Four_Links_Should_Get_Value() var child = new TestViewModel { Name = "Child", Child = grandChild }; var source = new TestViewModel { Name = "Parent", Child = child }; var target = new TestTarget(); - + // Create a binding with four links (source.Child.Child.Child.Name) var binding = TypedBinding.OneWay(vm => vm.Child!.Child!.Child!.Name); var expression = binding.Instance(source); - + // Act target.Bind(TestTarget.TextProperty, expression); - + // Assert Assert.Equal("GreatGrandChild", target.Text); } - + [Fact] public void OneWay_Binding_Four_Links_Should_Listen_To_Changes() { @@ -293,27 +296,27 @@ public void OneWay_Binding_Four_Links_Should_Listen_To_Changes() var child = new TestViewModel { Name = "Child", Child = grandChild }; var source = new TestViewModel { Name = "Parent", Child = child }; var target = new TestTarget(); - + // Create a binding with four links (source.Child.Child.Child.Name) var binding = TypedBinding.OneWay(vm => vm.Child!.Child!.Child!.Name); var expression = binding.Instance(source); - + // Act - initialize binding target.Bind(TestTarget.TextProperty, expression); - + // Act - update leaf node greatGrandChild.Name = "UpdatedGreatGrandChild"; - + // Assert Assert.Equal("UpdatedGreatGrandChild", target.Text); - + // Act - update middle node var newGreatGrandChild = new TestViewModel { Name = "NewGreatGrandChild" }; grandChild.Child = newGreatGrandChild; - + // Assert Assert.Equal("NewGreatGrandChild", target.Text); - + // Act - update higher node var newGrandChildWithChild = new TestViewModel { @@ -321,11 +324,11 @@ public void OneWay_Binding_Four_Links_Should_Listen_To_Changes() Child = new TestViewModel { Name = "BrandNewGreatGrandChild" } }; child.Child = newGrandChildWithChild; - + // Assert Assert.Equal("BrandNewGreatGrandChild", target.Text); } - + [Fact] public void TwoWay_Binding_Four_Links_Should_Listen_And_Write() { @@ -335,35 +338,35 @@ public void TwoWay_Binding_Four_Links_Should_Listen_And_Write() var child = new TestViewModel { Name = "Child", Child = grandChild }; var source = new TestViewModel { Name = "Parent", Child = child }; var target = new TestTarget(); - + // Create a binding with four links (source.Child.Child.Child.Name) var binding = TypedBinding.TwoWay(vm => vm.Child!.Child!.Child!.Name); var expression = binding.Instance(source, BindingMode.TwoWay); - + // Act - initialize binding target.Bind(TestTarget.TextProperty, expression); - + // Act - set from target target.Text = "UpdatedFromTarget"; binding.Write!.Invoke(source, target.Text); - + // Assert Assert.Equal("UpdatedFromTarget", greatGrandChild.Name); - + // Act - change leaf node var newGreatGrandChild = new TestViewModel { Name = "NewGreatGrandChild" }; grandChild.Child = newGreatGrandChild; - + // Assert Assert.Equal("NewGreatGrandChild", target.Text); - + // Act - update from target target.Text = "AfterLeafChange"; binding.Write!.Invoke(source, target.Text); - + // Assert Assert.Equal("AfterLeafChange", newGreatGrandChild.Name); - + // Act - update middle node var newGrandChildWithChild = new TestViewModel { @@ -371,63 +374,394 @@ public void TwoWay_Binding_Four_Links_Should_Listen_And_Write() Child = new TestViewModel { Name = "BrandNewGreatGrandChild" } }; child.Child = newGrandChildWithChild; - + // Assert Assert.Equal("BrandNewGreatGrandChild", target.Text); - + // Act - update from target target.Text = "AfterMiddleChange"; binding.Write!.Invoke(source, target.Text); - + // Assert Assert.Equal("AfterMiddleChange", newGrandChildWithChild.Child.Name); } - - [Fact] + + [Fact] public void Binding_Should_Handle_Null_Intermediate_Values() { // Arrange var source = new TestViewModel { Name = "Parent", Child = null }; var target = new TestTarget(); - + // Create a binding that will encounter a null (source.Child.Name) var binding = TypedBinding.OneWay(vm => vm.Child!.Name); var expression = binding.Instance(source); - + // Act - initialize binding target.Bind(TestTarget.TextProperty, expression); - + // Assert - binding should have error state due to null reference Assert.Null(target.Text); // Default value since binding path fails - + // Act - fix the null reference var newChild = new TestViewModel { Name = "NewChild" }; source.Child = newChild; - + // Assert - binding should work now Assert.Equal("NewChild", target.Text); } - + + [Fact] + public void TwoWay_Binding_Single_Link_Should_Update_Via_OnNext() + { + // Arrange + var source = new TestViewModel { Name = "Test" }; + var target = new TestTarget(); + + // Create a binding with a single link (source.Name) + var binding = TypedBinding.TwoWay(vm => vm.Name); + var expression = binding.Instance(source, BindingMode.TwoWay); + + // Act - set up binding + target.Bind(TestTarget.TextProperty, expression); + + // Act - update via OnNext + expression.OnNext("UpdatedViaOnNext"); + + // Assert + Assert.Equal("UpdatedViaOnNext", source.Name); + Assert.Equal("UpdatedViaOnNext", target.Text); + } + + [Fact] + public void TwoWay_Binding_Two_Links_Should_Update_Via_OnNext() + { + // Arrange + var child = new TestViewModel { Name = "Child" }; + var source = new TestViewModel { Name = "Parent", Child = child }; + var target = new TestTarget(); + + // Create a binding with two links (source.Child.Name) + var binding = TypedBinding.TwoWay(vm => vm.Child!.Name); + var expression = binding.Instance(source, BindingMode.TwoWay); + + // Act - initialize binding + target.Bind(TestTarget.TextProperty, expression); + + // Act - update via OnNext + expression.OnNext("UpdatedViaOnNext"); + + // Assert + Assert.Equal("UpdatedViaOnNext", child.Name); + Assert.Equal("UpdatedViaOnNext", target.Text); + + // Act - change intermediate link + var newChild = new TestViewModel { Name = "NewChild" }; + source.Child = newChild; + + // Assert + Assert.Equal("NewChild", target.Text); + + // Act - update via OnNext after changing intermediate + expression.OnNext("AfterIntermediateChange"); + + // Assert + Assert.Equal("AfterIntermediateChange", newChild.Name); + } + + [Fact] + public void TwoWay_Binding_Three_Links_Should_Update_Via_OnNext() + { + // Arrange + var grandChild = new TestViewModel { Name = "GrandChild" }; + var child = new TestViewModel { Name = "Child", Child = grandChild }; + var source = new TestViewModel { Name = "Parent", Child = child }; + var target = new TestTarget(); + + // Create a binding with three links (source.Child.Child.Name) + var binding = TypedBinding.TwoWay(vm => vm.Child!.Child!.Name); + var expression = binding.Instance(source, BindingMode.TwoWay); + + // Act - initialize binding + target.Bind(TestTarget.TextProperty, expression); + + // Act - update via OnNext + expression.OnNext("UpdatedViaOnNext"); + + // Assert + Assert.Equal("UpdatedViaOnNext", grandChild.Name); + + // Act - change intermediate node + var newGrandChild = new TestViewModel { Name = "NewGrandChild" }; + child.Child = newGrandChild; + + // Assert initial update + Assert.Equal("NewGrandChild", target.Text); + + // Act - update via OnNext after changing intermediate + expression.OnNext("AfterIntermediateChange"); + + // Assert + Assert.Equal("AfterIntermediateChange", newGrandChild.Name); + } + + [Fact] + public void TwoWay_Binding_Should_Propagate_IsExpanded_Toggle() + { + // Arrange + var node = new TestExpandableNode { IsExpanded = false }; + var target = new TestExpandableTarget(); + + // Create a binding for IsExpanded property (like in FileTreeNodeModel) + var binding = TypedBinding.TwoWay(vm => vm.IsExpanded); + var expression = binding.Instance(node, BindingMode.TwoWay); + + // Act - initialize binding + target.Bind(TestExpandableTarget.IsExpandedProperty, expression, binding, node); + + // Assert initial state + Assert.False(target.IsExpanded); + Assert.False(node.IsExpanded); + Assert.Equal(0, node.ExpandCallCount); + Assert.Equal(0, node.CollapseCallCount); + + // Act - simulate ToggleExpandedCommand by directly toggling the property + node.IsExpanded = true; + + // Assert expanded state propagated to target + Assert.True(target.IsExpanded); + Assert.True(node.IsExpanded); + Assert.Equal(1, node.ExpandCallCount); + Assert.Equal(0, node.CollapseCallCount); + + // Act - toggle back to collapsed + node.IsExpanded = false; + + // Assert collapsed state propagated to target + Assert.False(target.IsExpanded); + Assert.False(node.IsExpanded); + Assert.Equal(1, node.ExpandCallCount); + Assert.Equal(1, node.CollapseCallCount); + + // Act - toggle via target property, which should invoke Write + node.IsExpanded = true; + + // Assert expanded state propagated to target + Assert.True(target.IsExpanded); + Assert.True(node.IsExpanded); + Assert.Equal(2, node.ExpandCallCount); + Assert.Equal(1, node.CollapseCallCount); + } + + [Fact] + public void TwoWay_Binding_Should_Work_With_Multiple_Binding_Expressions() + { + // Arrange + var node = new TestExpandableNode { IsExpanded = false }; + var target1 = new TestExpandableTarget(); + var target2 = new TestExpandableTarget(); + + // Create a binding for IsExpanded property + var binding = TypedBinding.TwoWay(vm => vm.IsExpanded); + + // Create two separate binding expressions from the same binding + var expression1 = binding.Instance(node, BindingMode.TwoWay); + var expression2 = binding.Instance(node, BindingMode.TwoWay); + + // Initialize bindings + target1.Bind(TestExpandableTarget.IsExpandedProperty, expression1, binding, node); + target2.Bind(TestExpandableTarget.IsExpandedProperty, expression2, binding, node); + + // Act - change from model + node.IsExpanded = true; + + // Assert both targets updated + Assert.True(target1.IsExpanded); + Assert.True(target2.IsExpanded); + + // Act - change from one target + target1.IsExpanded = false; + + // Assert all synchronized + Assert.False(node.IsExpanded); + Assert.False(target2.IsExpanded); + } + + [Fact] +public void TwoWay_Binding_Should_Work_With_Multiple_Binding_Expressions_And_Long_Chain() +{ + // Create a deep chain of 5 TestViewModels + var level5 = new TestViewModel { Name = "Level5" }; + var level4 = new TestViewModel { Name = "Level4", Child = level5 }; + var level3 = new TestViewModel { Name = "Level3", Child = level4 }; + var level2 = new TestViewModel { Name = "Level2", Child = level3 }; + var level1 = new TestViewModel { Name = "Level1", Child = level2 }; + var root = new TestViewModel { Name = "Root", Child = level1 }; + + // Create simple targets with a Text property + var target1 = new TestTarget(); + var target2 = new TestTarget(); + + // Create a binding for the deep Name property (5 levels deep) + var binding = TypedBinding.TwoWay(vm => vm.Child!.Child!.Child!.Child!.Child!.Name); + + // Create two separate binding expressions from the same binding + var expression1 = binding.Instance(root, BindingMode.TwoWay); + var expression2 = binding.Instance(root, BindingMode.TwoWay); + + // Initialize bindings + target1.Bind(TestTarget.TextProperty, expression1); + target2.Bind(TestTarget.TextProperty, expression2); + + // Assert initial binding + Assert.Equal("Level5", target1.Text); + Assert.Equal("Level5", target2.Text); + + // Act - change leaf node property + level5.Name = "Updated Level5"; + + // Assert both targets updated + Assert.Equal("Updated Level5", target1.Text); + Assert.Equal("Updated Level5", target2.Text); + + // Act - replace a middle node in the chain + var newLevel4 = new TestViewModel { + Name = "New Level4", + Child = new TestViewModel { Name = "New Level5" } + }; + level3.Child = newLevel4; + + // Assert both targets updated to the new path + Assert.Equal("New Level5", target1.Text); + Assert.Equal("New Level5", target2.Text); + + // Act - change leaf node property on the old path + level5.Name = "Changed from Old Path"; + + // Assert both targets still point to the new path + Assert.Equal("New Level5", target1.Text); + Assert.Equal("New Level5", target2.Text); + + // Act - update from target after chain replacement + target1.Text = "Changed from Target1"; + binding.Write!.Invoke(root, target1.Text); + + // Assert the new leaf node and other target got updated + Assert.Equal("Changed from Target1", newLevel4.Child!.Name); + Assert.Equal("Changed from Target1", target2.Text); + + // Act - replace another node higher in the chain + var newLevel2 = new TestViewModel { + Name = "New Level2", + Child = new TestViewModel { + Name = "Newest Level3", + Child = new TestViewModel { + Name = "Newest Level4", + Child = new TestViewModel { + Name = "Newest Level5" + } + } + } + }; + level1.Child = newLevel2; + + // Assert both targets updated to the new deeply nested chain + Assert.Equal("Newest Level5", target1.Text); + Assert.Equal("Newest Level5", target2.Text); + + // Act - update from target after major chain replacement + target2.Text = "Final value"; + binding.Write!.Invoke(root, target2.Text); + + // Assert the new deep leaf node and other target got updated + Assert.Equal("Final value", newLevel2.Child!.Child!.Child!.Name); + Assert.Equal("Final value", target1.Text); +} + + + // Test model that tracks expand/collapse operations + private class TestExpandableNode : INotifyPropertyChanged + { + private bool _isExpanded; + + public int ExpandCallCount { get; private set; } + public int CollapseCallCount { get; private set; } + + public bool IsExpanded + { + get => _isExpanded; + set + { + if (_isExpanded != value) + { + _isExpanded = value; + + // Simulate expand/collapse logic + if (value) + ExpandCallCount++; + else + CollapseCallCount++; + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsExpanded))); + } + } + } + + public event PropertyChangedEventHandler? PropertyChanged; + } + + // Simple target for IsExpanded property + private class TestExpandableTarget : AvaloniaObject + { + public static readonly StyledProperty IsExpandedProperty = + AvaloniaProperty.Register(nameof(IsExpanded)); + + private TypedBinding? _typedBinding; + private TestExpandableNode? _sourceNode; + + public void Bind( + StyledProperty property, + IObservable> source, + TypedBinding typedBinding, + TestExpandableNode sourceNode + ) + { + this.Bind(property, source, BindingPriority.Style); + _sourceNode = sourceNode; + _typedBinding = typedBinding; + } + + public bool IsExpanded + { + get => GetValue(IsExpandedProperty); + set + { + SetValue(IsExpandedProperty, value); + _typedBinding?.Write?.Invoke(_sourceNode!, value); + } + } + } + // Test view model implementing property change notifications private class TestViewModel : INotifyPropertyChanged { private string _name = string.Empty; - private TestViewModel ?_child = null; - + private TestViewModel? _child = null; + public string Name { get => _name; set => SetProperty(ref _name, value); } - - public TestViewModel ?Child + + public TestViewModel? Child { get => _child; set => SetProperty(ref _child, value); } - + public event PropertyChangedEventHandler? PropertyChanged; - + protected void SetProperty(ref T field, T value, [CallerMemberName] string? propertyName = null) { if (!EqualityComparer.Default.Equals(field, value)) @@ -437,13 +771,13 @@ protected void SetProperty(ref T field, T value, [CallerMemberName] string? p } } } - + // Simple target class with an AvaloniaProperty private class TestTarget : AvaloniaObject { public static readonly StyledProperty TextProperty = AvaloniaProperty.Register(nameof(Text)); - + public string Text { get => GetValue(TextProperty); From 0bd4fb8c2e241a34398b1e176a1d450812dddd81 Mon Sep 17 00:00:00 2001 From: Al12rs <26797547+Al12rs@users.noreply.github.com> Date: Sat, 29 Mar 2025 13:45:08 +0100 Subject: [PATCH 07/27] Add XML documentation for Build method in ExpressionChainVisitor detailing tha the last expression member isn't returned as a link --- .../Data/Core/Parsers/ExpressionChainVisitor.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Avalonia.Controls.TreeDataGrid/Experimental/Data/Core/Parsers/ExpressionChainVisitor.cs b/src/Avalonia.Controls.TreeDataGrid/Experimental/Data/Core/Parsers/ExpressionChainVisitor.cs index bd89b6ef..f11abfab 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Experimental/Data/Core/Parsers/ExpressionChainVisitor.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Experimental/Data/Core/Parsers/ExpressionChainVisitor.cs @@ -17,6 +17,19 @@ public ExpressionChainVisitor(LambdaExpression expression) _rootExpression = expression; } + /// + /// Builds an array of delegates which return the intermediate objects in the expression chain. + /// + /// For example, if the expression is x => x.Foo.Bar.Baz then the links will be: + /// + /// - x => x + /// - x => x.Foo + /// - x => x.Foo.Bar + /// + /// There is no delegate for the final property of the expression x => x.Foo.Bar.Baz. + /// + /// + public static Func[] Build(Expression> expression) { var visitor = new ExpressionChainVisitor(expression); From 500fa8159ccf245f302e76bc7780edd507e5e73d Mon Sep 17 00:00:00 2001 From: Al12rs <26797547+Al12rs@users.noreply.github.com> Date: Sat, 29 Mar 2025 13:48:31 +0100 Subject: [PATCH 08/27] Fix incorrect link index in TypedBinding to set property value correctly --- .../Experimental/Data/TypedBinding`1.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.TreeDataGrid/Experimental/Data/TypedBinding`1.cs b/src/Avalonia.Controls.TreeDataGrid/Experimental/Data/TypedBinding`1.cs index b66f4b0d..b2c6727c 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Experimental/Data/TypedBinding`1.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Experimental/Data/TypedBinding`1.cs @@ -64,7 +64,8 @@ public static TypedBinding TwoWay(Expression> e (o, v) => property.SetValue(o, v) : (root, v) => { - var o = links[^2](root); + // The last link points the object containing the property to set + var o = links[^1](root); property.SetValue(o, v); }; From 86440f49ccc96105c30cf3d07225ee9fd1165e3a Mon Sep 17 00:00:00 2001 From: Al12rs <26797547+Al12rs@users.noreply.github.com> Date: Sat, 29 Mar 2025 13:55:50 +0100 Subject: [PATCH 09/27] Fix chain property changed handling in TypedBindingExpression --- .../Data/Core/TypedBindingExpression`2.cs | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls.TreeDataGrid/Experimental/Data/Core/TypedBindingExpression`2.cs b/src/Avalonia.Controls.TreeDataGrid/Experimental/Data/Core/TypedBindingExpression`2.cs index b19341af..ca3721d6 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Experimental/Data/Core/TypedBindingExpression`2.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Experimental/Data/Core/TypedBindingExpression`2.cs @@ -308,15 +308,25 @@ private int ChainIndexOf(object o) private void ChainPropertyChanged(object? sender) { - if (sender is null) + if (sender is null || _chain is null) return; - var index = ChainIndexOf(sender); + var senderIndex = ChainIndexOf(sender); - if (index != -1) + if (senderIndex == -1) { - StopListeningToChain(index); - ListenToChain(index); + // If the sender is not in the chain, we should stop listening to it. + UnsubscribeToChanges(sender); + return; + } + + // The member that changed is the next one in the chain. + var changedMemberIndex = senderIndex + 1; + + if (changedMemberIndex < _chain.Length) + { + StopListeningToChain(from: changedMemberIndex); + ListenToChain(from: changedMemberIndex); } PublishValue(); From dfd34d4c96f9b4238da5d9f34d07fb539381b721 Mon Sep 17 00:00:00 2001 From: Al12rs <26797547+Al12rs@users.noreply.github.com> Date: Sat, 29 Mar 2025 14:02:47 +0100 Subject: [PATCH 10/27] Clarify TypedBindingExpression ChainPropertyChanged behaviour with comments --- .../Experimental/Data/Core/TypedBindingExpression`2.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls.TreeDataGrid/Experimental/Data/Core/TypedBindingExpression`2.cs b/src/Avalonia.Controls.TreeDataGrid/Experimental/Data/Core/TypedBindingExpression`2.cs index ca3721d6..b1314989 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Experimental/Data/Core/TypedBindingExpression`2.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Experimental/Data/Core/TypedBindingExpression`2.cs @@ -323,6 +323,7 @@ private void ChainPropertyChanged(object? sender) // The member that changed is the next one in the chain. var changedMemberIndex = senderIndex + 1; + // Update subscriptions on the changed member and the links after it. if (changedMemberIndex < _chain.Length) { StopListeningToChain(from: changedMemberIndex); From a1a78cece5bb597c75d6b1841aa94b222169dd1a Mon Sep 17 00:00:00 2001 From: Al12rs <26797547+Al12rs@users.noreply.github.com> Date: Sat, 29 Mar 2025 15:02:37 +0100 Subject: [PATCH 11/27] Replace Xunit [Fact] attributes with AvaloniaFact in TypedBindingTests --- .../Bindings/TypedBindingTests.cs | 222 +++++++++--------- 1 file changed, 114 insertions(+), 108 deletions(-) diff --git a/tests/Avalonia.Controls.TreeDataGrid.Tests/Bindings/TypedBindingTests.cs b/tests/Avalonia.Controls.TreeDataGrid.Tests/Bindings/TypedBindingTests.cs index a4f9fd2a..0e0f1110 100644 --- a/tests/Avalonia.Controls.TreeDataGrid.Tests/Bindings/TypedBindingTests.cs +++ b/tests/Avalonia.Controls.TreeDataGrid.Tests/Bindings/TypedBindingTests.cs @@ -4,13 +4,14 @@ using System.Runtime.CompilerServices; using Avalonia.Data; using Avalonia.Experimental.Data; +using Avalonia.Headless.XUnit; using Xunit; namespace Avalonia.Controls.TreeDataGridTests.Bindings; public class TypedBindingTests { - [Fact] + [AvaloniaFact] public void OneWay_Binding_Single_Link_Should_Get_Value() { // Arrange @@ -28,7 +29,7 @@ public void OneWay_Binding_Single_Link_Should_Get_Value() Assert.Equal("Test", target.Text); } - [Fact] + [AvaloniaFact] public void OneWay_Binding_Single_Link_Should_Listen_To_Changes() { // Arrange @@ -47,7 +48,7 @@ public void OneWay_Binding_Single_Link_Should_Listen_To_Changes() Assert.Equal("Updated", target.Text); } - [Fact] + [AvaloniaFact] public void TwoWay_Binding_Single_Link_Should_Listen_And_Write() { // Arrange @@ -75,7 +76,7 @@ public void TwoWay_Binding_Single_Link_Should_Listen_And_Write() Assert.Equal("UpdatedFromTarget", source.Name); } - [Fact] + [AvaloniaFact] public void OneWay_Binding_Two_Links_Should_Get_Value() { // Arrange @@ -94,7 +95,7 @@ public void OneWay_Binding_Two_Links_Should_Get_Value() Assert.Equal("Child", target.Text); } - [Fact] + [AvaloniaFact] public void OneWay_Binding_Two_Links_Should_Listen_To_Changes() { // Arrange @@ -123,7 +124,7 @@ public void OneWay_Binding_Two_Links_Should_Listen_To_Changes() Assert.Equal("NewChild", target.Text); } - [Fact] + [AvaloniaFact] public void TwoWay_Binding_Two_Links_Should_Listen_And_Write() { // Arrange @@ -166,7 +167,7 @@ public void TwoWay_Binding_Two_Links_Should_Listen_And_Write() Assert.Equal("FinalUpdate", newChild.Name); } - [Fact] + [AvaloniaFact] public void OneWay_Binding_Three_Links_Should_Get_Value() { // Arrange @@ -186,7 +187,7 @@ public void OneWay_Binding_Three_Links_Should_Get_Value() Assert.Equal("GrandChild", target.Text); } - [Fact] + [AvaloniaFact] public void OneWay_Binding_Three_Links_Should_Listen_To_Changes() { // Arrange @@ -227,7 +228,7 @@ public void OneWay_Binding_Three_Links_Should_Listen_To_Changes() Assert.Equal("NewestGrandChild", target.Text); } - [Fact] + [AvaloniaFact] public void TwoWay_Binding_Three_Links_Should_Listen_And_Write() { // Arrange @@ -266,7 +267,7 @@ public void TwoWay_Binding_Three_Links_Should_Listen_And_Write() Assert.Equal("AfterIntermediateChange", newGrandChild.Name); } - [Fact] + [AvaloniaFact] public void OneWay_Binding_Four_Links_Should_Get_Value() { // Arrange @@ -287,7 +288,7 @@ public void OneWay_Binding_Four_Links_Should_Get_Value() Assert.Equal("GreatGrandChild", target.Text); } - [Fact] + [AvaloniaFact] public void OneWay_Binding_Four_Links_Should_Listen_To_Changes() { // Arrange @@ -329,7 +330,7 @@ public void OneWay_Binding_Four_Links_Should_Listen_To_Changes() Assert.Equal("BrandNewGreatGrandChild", target.Text); } - [Fact] + [AvaloniaFact] public void TwoWay_Binding_Four_Links_Should_Listen_And_Write() { // Arrange @@ -386,7 +387,7 @@ public void TwoWay_Binding_Four_Links_Should_Listen_And_Write() Assert.Equal("AfterMiddleChange", newGrandChildWithChild.Child.Name); } - [Fact] + [AvaloniaFact] public void Binding_Should_Handle_Null_Intermediate_Values() { // Arrange @@ -411,7 +412,7 @@ public void Binding_Should_Handle_Null_Intermediate_Values() Assert.Equal("NewChild", target.Text); } - [Fact] + [AvaloniaFact] public void TwoWay_Binding_Single_Link_Should_Update_Via_OnNext() { // Arrange @@ -433,7 +434,7 @@ public void TwoWay_Binding_Single_Link_Should_Update_Via_OnNext() Assert.Equal("UpdatedViaOnNext", target.Text); } - [Fact] + [AvaloniaFact] public void TwoWay_Binding_Two_Links_Should_Update_Via_OnNext() { // Arrange @@ -469,7 +470,7 @@ public void TwoWay_Binding_Two_Links_Should_Update_Via_OnNext() Assert.Equal("AfterIntermediateChange", newChild.Name); } - [Fact] + [AvaloniaFact] public void TwoWay_Binding_Three_Links_Should_Update_Via_OnNext() { // Arrange @@ -505,7 +506,7 @@ public void TwoWay_Binding_Three_Links_Should_Update_Via_OnNext() Assert.Equal("AfterIntermediateChange", newGrandChild.Name); } - [Fact] + [AvaloniaFact] public void TwoWay_Binding_Should_Propagate_IsExpanded_Toggle() { // Arrange @@ -553,7 +554,7 @@ public void TwoWay_Binding_Should_Propagate_IsExpanded_Toggle() Assert.Equal(1, node.CollapseCallCount); } - [Fact] + [AvaloniaFact] public void TwoWay_Binding_Should_Work_With_Multiple_Binding_Expressions() { // Arrange @@ -587,96 +588,101 @@ public void TwoWay_Binding_Should_Work_With_Multiple_Binding_Expressions() Assert.False(target2.IsExpanded); } - [Fact] -public void TwoWay_Binding_Should_Work_With_Multiple_Binding_Expressions_And_Long_Chain() -{ - // Create a deep chain of 5 TestViewModels - var level5 = new TestViewModel { Name = "Level5" }; - var level4 = new TestViewModel { Name = "Level4", Child = level5 }; - var level3 = new TestViewModel { Name = "Level3", Child = level4 }; - var level2 = new TestViewModel { Name = "Level2", Child = level3 }; - var level1 = new TestViewModel { Name = "Level1", Child = level2 }; - var root = new TestViewModel { Name = "Root", Child = level1 }; - - // Create simple targets with a Text property - var target1 = new TestTarget(); - var target2 = new TestTarget(); - - // Create a binding for the deep Name property (5 levels deep) - var binding = TypedBinding.TwoWay(vm => vm.Child!.Child!.Child!.Child!.Child!.Name); - - // Create two separate binding expressions from the same binding - var expression1 = binding.Instance(root, BindingMode.TwoWay); - var expression2 = binding.Instance(root, BindingMode.TwoWay); - - // Initialize bindings - target1.Bind(TestTarget.TextProperty, expression1); - target2.Bind(TestTarget.TextProperty, expression2); - - // Assert initial binding - Assert.Equal("Level5", target1.Text); - Assert.Equal("Level5", target2.Text); - - // Act - change leaf node property - level5.Name = "Updated Level5"; - - // Assert both targets updated - Assert.Equal("Updated Level5", target1.Text); - Assert.Equal("Updated Level5", target2.Text); - - // Act - replace a middle node in the chain - var newLevel4 = new TestViewModel { - Name = "New Level4", - Child = new TestViewModel { Name = "New Level5" } - }; - level3.Child = newLevel4; - - // Assert both targets updated to the new path - Assert.Equal("New Level5", target1.Text); - Assert.Equal("New Level5", target2.Text); - - // Act - change leaf node property on the old path - level5.Name = "Changed from Old Path"; - - // Assert both targets still point to the new path - Assert.Equal("New Level5", target1.Text); - Assert.Equal("New Level5", target2.Text); - - // Act - update from target after chain replacement - target1.Text = "Changed from Target1"; - binding.Write!.Invoke(root, target1.Text); - - // Assert the new leaf node and other target got updated - Assert.Equal("Changed from Target1", newLevel4.Child!.Name); - Assert.Equal("Changed from Target1", target2.Text); - - // Act - replace another node higher in the chain - var newLevel2 = new TestViewModel { - Name = "New Level2", - Child = new TestViewModel { - Name = "Newest Level3", - Child = new TestViewModel { - Name = "Newest Level4", - Child = new TestViewModel { - Name = "Newest Level5" - } - } - } - }; - level1.Child = newLevel2; - - // Assert both targets updated to the new deeply nested chain - Assert.Equal("Newest Level5", target1.Text); - Assert.Equal("Newest Level5", target2.Text); - - // Act - update from target after major chain replacement - target2.Text = "Final value"; - binding.Write!.Invoke(root, target2.Text); - - // Assert the new deep leaf node and other target got updated - Assert.Equal("Final value", newLevel2.Child!.Child!.Child!.Name); - Assert.Equal("Final value", target1.Text); -} + [AvaloniaFact] + public void TwoWay_Binding_Should_Work_With_Multiple_Binding_Expressions_And_Long_Chain() + { + // Create a deep chain of 5 TestViewModels + var level5 = new TestViewModel { Name = "Level5" }; + var level4 = new TestViewModel { Name = "Level4", Child = level5 }; + var level3 = new TestViewModel { Name = "Level3", Child = level4 }; + var level2 = new TestViewModel { Name = "Level2", Child = level3 }; + var level1 = new TestViewModel { Name = "Level1", Child = level2 }; + var root = new TestViewModel { Name = "Root", Child = level1 }; + + // Create simple targets with a Text property + var target1 = new TestTarget(); + var target2 = new TestTarget(); + + // Create a binding for the deep Name property (5 levels deep) + var binding = TypedBinding.TwoWay(vm => vm.Child!.Child!.Child!.Child!.Child!.Name); + + // Create two separate binding expressions from the same binding + var expression1 = binding.Instance(root, BindingMode.TwoWay); + var expression2 = binding.Instance(root, BindingMode.TwoWay); + + // Initialize bindings + target1.Bind(TestTarget.TextProperty, expression1); + target2.Bind(TestTarget.TextProperty, expression2); + + // Assert initial binding + Assert.Equal("Level5", target1.Text); + Assert.Equal("Level5", target2.Text); + + // Act - change leaf node property + level5.Name = "Updated Level5"; + + // Assert both targets updated + Assert.Equal("Updated Level5", target1.Text); + Assert.Equal("Updated Level5", target2.Text); + + // Act - replace a middle node in the chain + var newLevel4 = new TestViewModel + { + Name = "New Level4", + Child = new TestViewModel { Name = "New Level5" } + }; + level3.Child = newLevel4; + + // Assert both targets updated to the new path + Assert.Equal("New Level5", target1.Text); + Assert.Equal("New Level5", target2.Text); + + // Act - change leaf node property on the old path + level5.Name = "Changed from Old Path"; + + // Assert both targets still point to the new path + Assert.Equal("New Level5", target1.Text); + Assert.Equal("New Level5", target2.Text); + + // Act - update from target after chain replacement + target1.Text = "Changed from Target1"; + binding.Write!.Invoke(root, target1.Text); + + // Assert the new leaf node and other target got updated + Assert.Equal("Changed from Target1", newLevel4.Child!.Name); + Assert.Equal("Changed from Target1", target2.Text); + + // Act - replace another node higher in the chain + var newLevel2 = new TestViewModel + { + Name = "New Level2", + Child = new TestViewModel + { + Name = "Newest Level3", + Child = new TestViewModel + { + Name = "Newest Level4", + Child = new TestViewModel + { + Name = "Newest Level5" + } + } + } + }; + level1.Child = newLevel2; + + // Assert both targets updated to the new deeply nested chain + Assert.Equal("Newest Level5", target1.Text); + Assert.Equal("Newest Level5", target2.Text); + + // Act - update from target after major chain replacement + target2.Text = "Final value"; + binding.Write!.Invoke(root, target2.Text); + + // Assert the new deep leaf node and other target got updated + Assert.Equal("Final value", newLevel2.Child!.Child!.Child!.Name); + Assert.Equal("Final value", target1.Text); + } // Test model that tracks expand/collapse operations From 57309ec8a8c3ab4aba00a8756745ef75b80192cc Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 1 Apr 2025 10:42:20 +0100 Subject: [PATCH 12/27] fix memory leak caused by event handler subscriptions. EffectiveViewPortChanged needs to be unsubscribed when detaching from the visual tree, as it registers us with the LayoutManager. This will prevent us being collected when we are removed from the visual tree. --- .../Primitives/TreeDataGridPresenterBase.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridPresenterBase.cs b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridPresenterBase.cs index 11e88c52..34452e66 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridPresenterBase.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridPresenterBase.cs @@ -53,7 +53,6 @@ public TreeDataGridPresenterBase() _recycleElement = RecycleElement; _recycleElementOnItemRemoved = RecycleElementOnItemRemoved; _updateElementIndex = UpdateElementIndex; - EffectiveViewportChanged += OnEffectiveViewportChanged; } public TreeDataGridElementFactory? ElementFactory @@ -415,12 +414,14 @@ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); _scrollViewer = this.FindAncestorOfType(); + EffectiveViewportChanged += OnEffectiveViewportChanged; } protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { base.OnDetachedFromVisualTree(e); _scrollViewer = null; + EffectiveViewportChanged -= OnEffectiveViewportChanged; } protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) From 885ee7972a4d8cff8abff0539ba7dc0c0f76c124 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 1 Apr 2025 10:43:21 +0100 Subject: [PATCH 13/27] Fix memory leaks caused by holding reference to the ColumnList Because the columns list is part of the TDG models and held by the ViewModels. we need to unsubscribe INCC events when the TDGPresenter is removed from the visual tree. --- .../Primitives/TreeDataGridPresenterBase.cs | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridPresenterBase.cs b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridPresenterBase.cs index 34452e66..2ee736ac 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridPresenterBase.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridPresenterBase.cs @@ -41,6 +41,7 @@ public abstract class TreeDataGridPresenterBase : Border private bool _isInLayout; private bool _isWaitingForViewportUpdate; private IReadOnlyList? _items; + private bool _isSubscribedToItemChanges; private RealizedStackElements? _measureElements; private RealizedStackElements? _realizedElements; private ScrollViewer? _scrollViewer; @@ -68,14 +69,12 @@ public IReadOnlyList? Items { if (_items != value) { - if (_items is INotifyCollectionChanged oldIncc) - oldIncc.CollectionChanged -= OnItemsCollectionChanged; + UnsubscribeFromItemChanges(); var oldValue = _items; _items = value; - if (_items is INotifyCollectionChanged newIncc) - newIncc.CollectionChanged += OnItemsCollectionChanged; + SubscribeToItemChanges(); RaisePropertyChanged( ItemsProperty, @@ -415,6 +414,8 @@ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) base.OnAttachedToVisualTree(e); _scrollViewer = this.FindAncestorOfType(); EffectiveViewportChanged += OnEffectiveViewportChanged; + + SubscribeToItemChanges(); } protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) @@ -422,6 +423,8 @@ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e base.OnDetachedFromVisualTree(e); _scrollViewer = null; EffectiveViewportChanged -= OnEffectiveViewportChanged; + + UnsubscribeFromItemChanges(); } protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) @@ -461,6 +464,24 @@ protected virtual void UnrealizeElementOnItemRemoved(Control element) UnrealizeElement(element); } + private void SubscribeToItemChanges() + { + if (!_isSubscribedToItemChanges && _items is INotifyCollectionChanged newIncc) + { + newIncc.CollectionChanged += OnItemsCollectionChanged; + _isSubscribedToItemChanges = true; + } + } + + private void UnsubscribeFromItemChanges() + { + if (_isSubscribedToItemChanges && _items is INotifyCollectionChanged oldIncc) + { + oldIncc.CollectionChanged -= OnItemsCollectionChanged; + _isSubscribedToItemChanges = false; + } + } + private void RealizeElements( IReadOnlyList items, Size availableSize, From 29a93fc137726921c3b9cb6859df0ed711b4a066 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 2 Apr 2025 09:54:00 +0100 Subject: [PATCH 14/27] add comment explaining EffectiveViewPortChanged event. --- .../Primitives/TreeDataGridPresenterBase.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridPresenterBase.cs b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridPresenterBase.cs index 2ee736ac..b95dd152 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridPresenterBase.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridPresenterBase.cs @@ -413,6 +413,9 @@ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); _scrollViewer = this.FindAncestorOfType(); + + // Subscribing to this event adds a reference to 'this' in the layout manager. + // so this must be unsubscribed to avoid memory leaks. EffectiveViewportChanged += OnEffectiveViewportChanged; SubscribeToItemChanges(); @@ -422,6 +425,7 @@ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e { base.OnDetachedFromVisualTree(e); _scrollViewer = null; + EffectiveViewportChanged -= OnEffectiveViewportChanged; UnsubscribeFromItemChanges(); From 44982af2f890527aca58195139d656bede3ee8fe Mon Sep 17 00:00:00 2001 From: ITDancer13 Date: Tue, 22 Apr 2025 21:30:47 +0200 Subject: [PATCH 15/27] This change enables support for custom event handlers in scenarios where the TreeDataGrid is empty, by permitting TreeDataGridRow to be null. --- src/Avalonia.Controls.TreeDataGrid/TreeDataGrid.cs | 13 ++++++++----- .../TreeDataGridRowDragEventArgs.cs | 4 ++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls.TreeDataGrid/TreeDataGrid.cs b/src/Avalonia.Controls.TreeDataGrid/TreeDataGrid.cs index 1483405a..1a8728a6 100644 --- a/src/Avalonia.Controls.TreeDataGrid/TreeDataGrid.cs +++ b/src/Avalonia.Controls.TreeDataGrid/TreeDataGrid.cs @@ -607,7 +607,7 @@ private void AutoScroll(bool direction) [MemberNotNullWhen(true, nameof(_source))] private bool CalculateAutoDragDrop( - TreeDataGridRow targetRow, + TreeDataGridRow? targetRow, DragEventArgs e, [NotNullWhen(true)] out DragInfo? data, out TreeDataGridRowDropPosition position) @@ -616,6 +616,7 @@ private bool CalculateAutoDragDrop( e.Data.Get(DragInfo.DataFormat) is not DragInfo di || _source is null || _source.IsSorted || + targetRow is null || di.Source != _source) { data = null; @@ -647,7 +648,6 @@ private void OnDragOver(DragEventArgs e) if (!TryGetRow(e.Source as Control, out var row)) { e.DragEffects = DragDropEffects.None; - return; } if (!CalculateAutoDragDrop(row, e, out _, out var adorner)) @@ -663,7 +663,10 @@ private void OnDragOver(DragEventArgs e) adorner = ev.Position; } - ShowDragAdorner(row, adorner); + if (row != null) + { + ShowDragAdorner(row, adorner); + } if (Scroll is ScrollViewer scroller) { @@ -687,8 +690,7 @@ private void OnDrop(DragEventArgs e) { StopDrag(); - if (!TryGetRow(e.Source as Control, out var row)) - return; + TryGetRow(e.Source as Control, out var row); var autoDrop = CalculateAutoDragDrop(row, e, out var data, out var position); var route = BuildEventRoute(RowDropEvent); @@ -707,6 +709,7 @@ private void OnDrop(DragEventArgs e) if (autoDrop && _source is not null && + row is not null && position != TreeDataGridRowDropPosition.None) { var targetIndex = _source.Rows.RowIndexToModelIndex(row.RowIndex); diff --git a/src/Avalonia.Controls.TreeDataGrid/TreeDataGridRowDragEventArgs.cs b/src/Avalonia.Controls.TreeDataGrid/TreeDataGridRowDragEventArgs.cs index 7fc5447a..35abc1dd 100644 --- a/src/Avalonia.Controls.TreeDataGrid/TreeDataGridRowDragEventArgs.cs +++ b/src/Avalonia.Controls.TreeDataGrid/TreeDataGridRowDragEventArgs.cs @@ -24,7 +24,7 @@ public class TreeDataGridRowDragEventArgs : RoutedEventArgs /// The event being raised. /// The row that is being dragged over. /// The inner drag event args. - public TreeDataGridRowDragEventArgs(RoutedEvent routedEvent, TreeDataGridRow row, DragEventArgs inner) + public TreeDataGridRowDragEventArgs(RoutedEvent routedEvent, TreeDataGridRow? row, DragEventArgs inner) : base(routedEvent) { TargetRow = row; @@ -39,7 +39,7 @@ public TreeDataGridRowDragEventArgs(RoutedEvent routedEvent, TreeDataGridRow row /// /// Gets the row being dragged over. /// - public TreeDataGridRow TargetRow { get; } + public TreeDataGridRow? TargetRow { get; } /// /// Gets or sets a value indicating the how the data should be dropped into From 15541f60593e99409494d3b42e1a611abb69310b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 30 Apr 2025 21:56:03 -0700 Subject: [PATCH 16/27] Fix wikipedia page --- samples/TreeDataGridDemo/ViewModels/WikipediaPageViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/TreeDataGridDemo/ViewModels/WikipediaPageViewModel.cs b/samples/TreeDataGridDemo/ViewModels/WikipediaPageViewModel.cs index f895169b..1b1dc7a6 100644 --- a/samples/TreeDataGridDemo/ViewModels/WikipediaPageViewModel.cs +++ b/samples/TreeDataGridDemo/ViewModels/WikipediaPageViewModel.cs @@ -45,7 +45,7 @@ private async Task LoadContent() var client = new HttpClient(); var d = DateTimeOffset.Now.Day; var m = DateTimeOffset.Now.Month; - var uri = $"https://api.wikimedia.org/feed/v1/wikipedia/en/onthisday/all/{m}/{d}"; + var uri = $"https://api.wikimedia.org/feed/v1/wikipedia/en/onthisday/all/{m:00}/{d:00}"; var s = await client.GetStringAsync(uri); var data = JsonSerializer.Deserialize(s, new JsonSerializerOptions { From e7daf1b9bd1008d66fff5358c2daf40f4bcb779b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 30 Apr 2025 21:56:24 -0700 Subject: [PATCH 17/27] Add AvaloniaSamplesVersion --- Directory.Build.props | 1 + samples/TreeDataGridDemo/TreeDataGridDemo.csproj | 12 ++++++------ .../Avalonia.Controls.TreeDataGrid.Tests.csproj | 4 ++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index b52a8875..a771f171 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -13,5 +13,6 @@ 11.0.0 + 11.2.8 diff --git a/samples/TreeDataGridDemo/TreeDataGridDemo.csproj b/samples/TreeDataGridDemo/TreeDataGridDemo.csproj index 055ae9ac..73c8c44a 100644 --- a/samples/TreeDataGridDemo/TreeDataGridDemo.csproj +++ b/samples/TreeDataGridDemo/TreeDataGridDemo.csproj @@ -12,12 +12,12 @@ - - - - - - + + + + + + diff --git a/tests/Avalonia.Controls.TreeDataGrid.Tests/Avalonia.Controls.TreeDataGrid.Tests.csproj b/tests/Avalonia.Controls.TreeDataGrid.Tests/Avalonia.Controls.TreeDataGrid.Tests.csproj index cc5443b9..e4140c75 100644 --- a/tests/Avalonia.Controls.TreeDataGrid.Tests/Avalonia.Controls.TreeDataGrid.Tests.csproj +++ b/tests/Avalonia.Controls.TreeDataGrid.Tests/Avalonia.Controls.TreeDataGrid.Tests.csproj @@ -5,8 +5,8 @@ Avalonia.Controls.TreeDataGridTests - - + + all From 528a3f7c69ed20f8959cea08e825bdc05330e2fe Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 30 Apr 2025 22:06:15 -0700 Subject: [PATCH 18/27] Allow pen secondary button drag --- .../Primitives/TreeDataGridRow.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridRow.cs b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridRow.cs index 7f345e48..f248cbd3 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridRow.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridRow.cs @@ -152,9 +152,17 @@ protected override void OnPointerMoved(PointerEventArgs e) { base.OnPointerMoved(e); - var delta = e.GetPosition(this) - _mouseDownPosition; + var currentPoint = e.GetCurrentPoint(this); + var delta = currentPoint.Position - _mouseDownPosition; - if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || + var inputSupportsDrag = currentPoint.Pointer.Type switch + { + PointerType.Mouse => currentPoint.Properties.IsLeftButtonPressed, + PointerType.Pen => currentPoint.Properties.IsRightButtonPressed, + _ => false + }; + + if (!inputSupportsDrag || e.Handled || Math.Abs(delta.X) < DragDistance && Math.Abs(delta.Y) < DragDistance || _mouseDownPosition == s_InvalidPoint) From 32f742b4e75bf3257b9ef90a756189d66cc8dbec Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 30 Apr 2025 22:06:31 -0700 Subject: [PATCH 19/27] Allow pen secondary button selection on press --- .../Selection/TreeDataGridCellSelectionModel.cs | 9 ++++++++- .../Selection/TreeDataGridRowSelectionModel.cs | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs index 19d709ef..9882a02d 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs @@ -140,12 +140,19 @@ void ITreeDataGridSelectionInteraction.OnPointerPressed(TreeDataGrid sender, Poi // Select a cell on pointer pressed if: // // - It's a mouse click, not touch: we don't want to select on touch scroll gesture start + // - It's a pen secondary button press, we don't want to select on primary button scroll gesture start // - The cell isn't already selected: we don't want to deselect an existing multiple selection // if the user is trying to drag multiple cells // // Otherwise select on pointer release. + var inputSupportsSelection = e.Pointer.Type switch + { + PointerType.Mouse => true, + PointerType.Pen => e.GetCurrentPoint(null).Properties.IsRightButtonPressed, + _ => false + }; if (!e.Handled && - e.Pointer.Type == PointerType.Mouse && + inputSupportsSelection && e.Source is Control source && sender.TryGetCell(source, out var cell) && !IsSelected(cell.ColumnIndex, cell.RowIndex)) diff --git a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridRowSelectionModel.cs b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridRowSelectionModel.cs index ec112459..095549f4 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridRowSelectionModel.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridRowSelectionModel.cs @@ -318,12 +318,19 @@ void ITreeDataGridSelectionInteraction.OnPointerPressed(TreeDataGrid sender, Poi // Select a row on pointer pressed if: // // - It's a mouse click, not touch: we don't want to select on touch scroll gesture start + // - It's a pen secondary button press, we don't want to select on primary button scroll gesture start // - The row isn't already selected: we don't want to deselect an existing multiple selection // if the user is trying to drag multiple rows // // Otherwise select on pointer release. + var inputSupportsSelection = e.Pointer.Type switch + { + PointerType.Mouse => true, + PointerType.Pen => e.GetCurrentPoint(null).Properties.IsRightButtonPressed, + _ => false + }; if (!e.Handled && - e.Pointer.Type == PointerType.Mouse && + inputSupportsSelection && e.Source is Control source && sender.TryGetRow(source, out var row) && _source.Rows.RowIndexToModelIndex(row.RowIndex) is { } modelIndex && From 5118be694ac7dd38480f724c186454f80c863f38 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 30 Apr 2025 22:09:15 -0700 Subject: [PATCH 20/27] Rename field --- .../Primitives/TreeDataGridRow.cs | 4 ++-- .../Selection/TreeDataGridCellSelectionModel.cs | 4 ++-- .../Selection/TreeDataGridRowSelectionModel.cs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridRow.cs b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridRow.cs index f248cbd3..c9ad891a 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridRow.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridRow.cs @@ -155,14 +155,14 @@ protected override void OnPointerMoved(PointerEventArgs e) var currentPoint = e.GetCurrentPoint(this); var delta = currentPoint.Position - _mouseDownPosition; - var inputSupportsDrag = currentPoint.Pointer.Type switch + var pointerSupportsDrag = currentPoint.Pointer.Type switch { PointerType.Mouse => currentPoint.Properties.IsLeftButtonPressed, PointerType.Pen => currentPoint.Properties.IsRightButtonPressed, _ => false }; - if (!inputSupportsDrag || + if (!pointerSupportsDrag || e.Handled || Math.Abs(delta.X) < DragDistance && Math.Abs(delta.Y) < DragDistance || _mouseDownPosition == s_InvalidPoint) diff --git a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs index 9882a02d..4f887dbd 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs @@ -145,14 +145,14 @@ void ITreeDataGridSelectionInteraction.OnPointerPressed(TreeDataGrid sender, Poi // if the user is trying to drag multiple cells // // Otherwise select on pointer release. - var inputSupportsSelection = e.Pointer.Type switch + var pointerSupportSelectionOnPress = e.Pointer.Type switch { PointerType.Mouse => true, PointerType.Pen => e.GetCurrentPoint(null).Properties.IsRightButtonPressed, _ => false }; if (!e.Handled && - inputSupportsSelection && + pointerSupportSelectionOnPress && e.Source is Control source && sender.TryGetCell(source, out var cell) && !IsSelected(cell.ColumnIndex, cell.RowIndex)) diff --git a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridRowSelectionModel.cs b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridRowSelectionModel.cs index 095549f4..bd4da5d7 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridRowSelectionModel.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridRowSelectionModel.cs @@ -323,14 +323,14 @@ void ITreeDataGridSelectionInteraction.OnPointerPressed(TreeDataGrid sender, Poi // if the user is trying to drag multiple rows // // Otherwise select on pointer release. - var inputSupportsSelection = e.Pointer.Type switch + var pointerSupportSelectionOnPress = e.Pointer.Type switch { PointerType.Mouse => true, PointerType.Pen => e.GetCurrentPoint(null).Properties.IsRightButtonPressed, _ => false }; if (!e.Handled && - inputSupportsSelection && + pointerSupportSelectionOnPress && e.Source is Control source && sender.TryGetRow(source, out var row) && _source.Rows.RowIndexToModelIndex(row.RowIndex) is { } modelIndex && From e14f3b94b4ee38cd1ba16eff40189457995252ee Mon Sep 17 00:00:00 2001 From: Maxwell Katz Date: Thu, 1 May 2025 20:49:49 -0700 Subject: [PATCH 21/27] Update Avalonia.Controls.TreeDataGrid.Tests.csproj --- .../Avalonia.Controls.TreeDataGrid.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.Controls.TreeDataGrid.Tests/Avalonia.Controls.TreeDataGrid.Tests.csproj b/tests/Avalonia.Controls.TreeDataGrid.Tests/Avalonia.Controls.TreeDataGrid.Tests.csproj index e4140c75..397a2017 100644 --- a/tests/Avalonia.Controls.TreeDataGrid.Tests/Avalonia.Controls.TreeDataGrid.Tests.csproj +++ b/tests/Avalonia.Controls.TreeDataGrid.Tests/Avalonia.Controls.TreeDataGrid.Tests.csproj @@ -1,6 +1,6 @@ - net6.0 + net8.0 False Avalonia.Controls.TreeDataGridTests From 1df7cce253fa7d0aa1d97e591d06e1d87d27e524 Mon Sep 17 00:00:00 2001 From: Maxwell Katz Date: Thu, 1 May 2025 20:50:04 -0700 Subject: [PATCH 22/27] Update TreeDataGridDemo.csproj --- samples/TreeDataGridDemo/TreeDataGridDemo.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/TreeDataGridDemo/TreeDataGridDemo.csproj b/samples/TreeDataGridDemo/TreeDataGridDemo.csproj index 73c8c44a..44fa1c27 100644 --- a/samples/TreeDataGridDemo/TreeDataGridDemo.csproj +++ b/samples/TreeDataGridDemo/TreeDataGridDemo.csproj @@ -1,6 +1,6 @@  - net6.0 + net8.0 False WinExe From c8030342957abc8727a36b530594ba8da4edc2b5 Mon Sep 17 00:00:00 2001 From: Maxwell Katz Date: Thu, 1 May 2025 20:51:34 -0700 Subject: [PATCH 23/27] Update MainWindow.axaml.cs --- samples/TreeDataGridDemo/MainWindow.axaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/TreeDataGridDemo/MainWindow.axaml.cs b/samples/TreeDataGridDemo/MainWindow.axaml.cs index 883cac22..4a1bcdd0 100644 --- a/samples/TreeDataGridDemo/MainWindow.axaml.cs +++ b/samples/TreeDataGridDemo/MainWindow.axaml.cs @@ -107,7 +107,7 @@ private void DragDrop_RowDragStarted(object? sender, TreeDataGridRowDragStartedE private void DragDrop_RowDragOver(object? sender, TreeDataGridRowDragEventArgs e) { if (e.Position == TreeDataGridRowDropPosition.Inside && - e.TargetRow.Model is DragDropItem i && + e.TargetRow?.Model is DragDropItem i && !i.AllowDrop) e.Inner.DragEffects = DragDropEffects.None; } From 8b74adf21ba757e716c31be5fbea65d38f678382 Mon Sep 17 00:00:00 2001 From: Maxwell Katz Date: Thu, 1 May 2025 21:15:29 -0700 Subject: [PATCH 24/27] Update azure-pipelines.yml --- azure-pipelines.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 2c9ec504..e19a47f2 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -7,6 +7,12 @@ pool: vmImage: ubuntu-latest steps: + +- task: UseDotNet@2 + displayName: 'Use .NET SDK' + inputs: + version: 8.0.x + - task: CmdLine@2 displayName: 'Install Nuke' inputs: From fcc961ee654d610ce16d5a20241c834d919ce38f Mon Sep 17 00:00:00 2001 From: Maxwell Katz Date: Thu, 1 May 2025 21:15:34 -0700 Subject: [PATCH 25/27] Update build-and-test.yml --- .github/workflows/build-and-test.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 86347d20..65472559 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -7,12 +7,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Setup .NET Core SDK ${{ matrix.dotnet-version }} - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: | - 6.0.x 8.0.x - name: Install dependencies run: dotnet restore From cc4b76ced13b644edbe9e36aed51332bff9645f9 Mon Sep 17 00:00:00 2001 From: Maxwell Katz Date: Thu, 1 May 2025 21:18:35 -0700 Subject: [PATCH 26/27] Update _build.csproj --- nukebuild/_build.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index d387b765..be23117c 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net8.0 false False From 835d49505d2eabaac1074c478c19c5b9c7cffcc8 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 1 May 2025 21:33:22 -0700 Subject: [PATCH 27/27] Update nukebuild --- .nuke/build.schema.json | 153 ++++++++++++++++++----------------- global.json | 6 -- nukebuild/Build.cs | 63 +++++---------- nukebuild/BuildParameters.cs | 4 +- nukebuild/_build.csproj | 6 +- 5 files changed, 106 insertions(+), 126 deletions(-) delete mode 100644 global.json diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index ee72d94a..76be051b 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -1,15 +1,53 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "title": "Build Schema", - "$ref": "#/definitions/build", "definitions": { - "build": { - "type": "object", + "Host": { + "type": "string", + "enum": [ + "AppVeyor", + "AzurePipelines", + "Bamboo", + "Bitbucket", + "Bitrise", + "GitHubActions", + "GitLab", + "Jenkins", + "Rider", + "SpaceAutomation", + "TeamCity", + "Terminal", + "TravisCI", + "VisualStudio", + "VSCode" + ] + }, + "ExecutableTarget": { + "type": "string", + "enum": [ + "CiAzureLinux", + "CiAzureOSX", + "CiAzureWindows", + "Clean", + "Compile", + "CreateNugetPackages", + "Package", + "RunCoreLibsTests", + "RunTests", + "ZipFiles" + ] + }, + "Verbosity": { + "type": "string", + "description": "", + "enum": [ + "Verbose", + "Normal", + "Minimal", + "Quiet" + ] + }, + "NukeBuild": { "properties": { - "Configuration": { - "type": "string", - "description": "configuration" - }, "Continue": { "type": "boolean", "description": "Indicates to continue a previously failed build attempt" @@ -19,24 +57,8 @@ "description": "Shows the help text for this build assembly" }, "Host": { - "type": "string", "description": "Host for execution. Default is 'automatic'", - "enum": [ - "AppVeyor", - "AzurePipelines", - "Bamboo", - "Bitrise", - "GitHubActions", - "GitLab", - "Jenkins", - "Rider", - "SpaceAutomation", - "TeamCity", - "Terminal", - "TravisCI", - "VisualStudio", - "VSCode" - ] + "$ref": "#/definitions/Host" }, "NoLogo": { "type": "boolean", @@ -57,22 +79,6 @@ "type": "string" } }, - "PublishFramework": { - "type": "string", - "description": "publish-framework" - }, - "PublishProject": { - "type": "string", - "description": "publish-project" - }, - "PublishRuntime": { - "type": "string", - "description": "publish-runtime" - }, - "PublishSelfContained": { - "type": "boolean", - "description": "publish-self-contained" - }, "Root": { "type": "string", "description": "Root directory during build execution" @@ -81,51 +87,50 @@ "type": "array", "description": "List of targets to be skipped. Empty list skips all dependencies", "items": { - "type": "string", - "enum": [ - "Clean", - "Compile", - "Pack", - "Publish", - "Restore", - "Test" - ] + "$ref": "#/definitions/ExecutableTarget" } }, - "Solution": { - "type": "string", - "description": "Path to a solution file that is automatically loaded" - }, "Target": { "type": "array", "description": "List of targets to be invoked. Default is '{default_target}'", "items": { - "type": "string", - "enum": [ - "Clean", - "Compile", - "Pack", - "Publish", - "Restore", - "Test" - ] + "$ref": "#/definitions/ExecutableTarget" } }, "Verbosity": { - "type": "string", "description": "Logging verbosity during build execution. Default is 'Normal'", - "enum": [ - "Minimal", - "Normal", - "Quiet", - "Verbose" - ] + "$ref": "#/definitions/Verbosity" + } + } + } + }, + "allOf": [ + { + "properties": { + "Configuration": { + "type": "string", + "description": "configuration" }, - "VersionSuffix": { + "ForceNugetVersion": { + "type": "string", + "description": "force-nuget-version" + }, + "SkipPreviewer": { + "type": "boolean", + "description": "skip-previewer" + }, + "SkipTests": { + "type": "boolean", + "description": "skip-tests" + }, + "Solution": { "type": "string", - "description": "version-suffix" + "description": "Path to a solution file that is automatically loaded. Default is Avalonia.Controls.TreeDataGrid.sln" } } + }, + { + "$ref": "#/definitions/NukeBuild" } - } -} \ No newline at end of file + ] +} diff --git a/global.json b/global.json deleted file mode 100644 index d3492186..00000000 --- a/global.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "sdk": { - "version": "8.0.101", - "rollForward": "latestFeature" - } -} diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 3427f354..a48300e8 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -6,12 +6,12 @@ using System.Linq; using System.Runtime.InteropServices; using Nuke.Common; +using Nuke.Common.IO; using Nuke.Common.ProjectModel; using Nuke.Common.Tooling; using Nuke.Common.Tools.DotNet; using Nuke.Common.Tools.MSBuild; using static Nuke.Common.EnvironmentInfo; -using static Nuke.Common.IO.FileSystemTasks; using static Nuke.Common.Tools.MSBuild.MSBuildTasks; using static Nuke.Common.Tools.DotNet.DotNetTasks; using static Nuke.Common.Tools.VSWhere.VSWhereTasks; @@ -82,8 +82,7 @@ IReadOnlyCollection MsBuildCommon( return MSBuild(c => c .SetProjectFile(projectFile) // This is required for VS2019 image on Azure Pipelines - .When(Parameters.IsRunningOnWindows && - Parameters.IsRunningOnAzure, _ => _ + .When(_ => Parameters.IsRunningOnWindows && Parameters.IsRunningOnAzure, _ => _ .AddProperty("JavaSdkDirectory", GetVariable("JAVA_HOME_11_X64"))) .AddProperty("PackageVersion", Parameters.Version) .AddProperty("iOSRoslynPathHackRequired", true) @@ -95,13 +94,13 @@ IReadOnlyCollection MsBuildCommon( Target Clean => _ => _.Executes(() => { - Parameters.BuildDirs.ForEach(DeleteDirectory); - Parameters.BuildDirs.ForEach(EnsureCleanDirectory); - EnsureCleanDirectory(Parameters.ArtifactsDir); - EnsureCleanDirectory(Parameters.NugetIntermediateRoot); - EnsureCleanDirectory(Parameters.NugetRoot); - EnsureCleanDirectory(Parameters.ZipRoot); - EnsureCleanDirectory(Parameters.TestResultsRoot); + Parameters.BuildDirs.ForEach(p => p.DeleteDirectory()); + Parameters.BuildDirs.ForEach(p => p.CreateOrCleanDirectory()); + Parameters.ArtifactsDir.CreateOrCleanDirectory(); + Parameters.NugetIntermediateRoot.CreateOrCleanDirectory(); + Parameters.NugetRoot.CreateOrCleanDirectory(); + Parameters.ZipRoot.CreateOrCleanDirectory(); + Parameters.TestResultsRoot.CreateOrCleanDirectory(); }); Target Compile => _ => _ @@ -110,7 +109,7 @@ IReadOnlyCollection MsBuildCommon( { if (Parameters.IsRunningOnWindows) MsBuildCommon(Parameters.MSBuildSolution, c => c - .SetProcessArgumentConfigurator(a => a.Add("/r")) + .SetProcessAdditionalArguments("/r") .AddTargets("Build") ); @@ -122,41 +121,23 @@ IReadOnlyCollection MsBuildCommon( ); }); - void RunCoreTest(string projectName) - { - Information($"Running tests from {projectName}"); - var project = Solution.GetProject(projectName).NotNull("project != null"); - - foreach (var fw in project.GetTargetFrameworks()) - { - if (fw.StartsWith("net4") - && RuntimeInformation.IsOSPlatform(OSPlatform.Linux) - && Environment.GetEnvironmentVariable("FORCE_LINUX_TESTS") != "1") - { - Information($"Skipping {projectName} ({fw}) tests on Linux - https://github.com/mono/mono/issues/13969"); - continue; - } - - Information($"Running for {projectName} ({fw}) ..."); - - DotNetTest(c => c - .SetProjectFile(project) - .SetConfiguration(Parameters.Configuration) - .SetFramework(fw) - .EnableNoBuild() - .EnableNoRestore() - .When(Parameters.PublishTestResults, _ => _ - .SetLoggers("trx") - .SetResultsDirectory(Parameters.TestResultsRoot))); - } - } - Target RunCoreLibsTests => _ => _ .OnlyWhenStatic(() => !Parameters.SkipTests) .DependsOn(Compile) .Executes(() => { - RunCoreTest("Avalonia.Controls.TreeDataGrid.Tests"); + foreach (var testProject in (RootDirectory / "tests").GlobFiles("**/*.csproj")) + { + Information($"Running tests from {testProject}"); + DotNetTest(c => c + .SetProjectFile(testProject) + .SetConfiguration(Parameters.Configuration) + .EnableNoBuild() + .EnableNoRestore() + .When(_ => Parameters.PublishTestResults, _ => _ + .SetLoggers("trx") + .SetResultsDirectory(Parameters.TestResultsRoot))); + } }); Target ZipFiles => _ => _ diff --git a/nukebuild/BuildParameters.cs b/nukebuild/BuildParameters.cs index 43a25e78..d68286e5 100644 --- a/nukebuild/BuildParameters.cs +++ b/nukebuild/BuildParameters.cs @@ -56,7 +56,7 @@ public class BuildParameters public AbsolutePath BinRoot { get; } public AbsolutePath TestResultsRoot { get; } public string DirSuffix { get; } - public List BuildDirs { get; } + public List BuildDirs { get; } public string FileZipSuffix { get; } public AbsolutePath ZipCoreArtifacts { get; } public AbsolutePath ZipNuGetArtifacts { get; } @@ -125,7 +125,7 @@ public BuildParameters(Build b) ZipRoot = ArtifactsDir / "zip"; BinRoot = ArtifactsDir / "bin"; TestResultsRoot = ArtifactsDir / "test-results"; - BuildDirs = GlobDirectories(RootDirectory, "**bin").Concat(GlobDirectories(RootDirectory, "**obj")).ToList(); + BuildDirs = RootDirectory.GlobDirectories("**bin").Concat(RootDirectory.GlobDirectories("**obj")).ToList(); DirSuffix = Configuration; FileZipSuffix = Version + ".zip"; ZipCoreArtifacts = ZipRoot / ("Avalonia-" + FileZipSuffix); diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index be23117c..497faaa7 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -7,12 +7,12 @@ False CS0649;CS0169 + 1 - - - + +