New binding mechanism: Expression tracking

This commit is contained in:
2023-08-15 13:17:42 +02:00
parent 335433562a
commit b792639635
35 changed files with 971 additions and 311 deletions

View File

@@ -0,0 +1,259 @@
using TerminalUI.Controls;
using TerminalUI.Extensions;
using TerminalUI.Tests.Models;
namespace TerminalUI.Tests;
public class BindingTests
{
private static string TestMapperMethodStatic(string s) => s.ToUpper();
private string TestMapperMethod(string s) => s.ToUpper();
[Fact]
public void StaticCaptureMethodBinding_ByDefault_ShouldSetValue()
{
var testViewModel = new TestViewModel
{
Text = "test"
};
var grid = new Grid<TestViewModel>
{
DataContext = testViewModel
};
var txtb1 = grid.CreateChild<TextBlock<TestViewModel>>();
txtb1.Bind(
txtb1,
vm => TestMapperMethodStatic(vm.Text),
t => t.Text,
v => v);
Assert.Equal("TEST", txtb1.Text);
}
[Fact]
public void StaticCaptureMethodBinding_AfterUpdate_ShouldSetValue()
{
var testViewModel = new TestViewModel
{
Text = "test"
};
var grid = new Grid<TestViewModel>
{
DataContext = testViewModel
};
var txtb1 = grid.CreateChild<TextBlock<TestViewModel>>();
txtb1.Bind(
txtb1,
vm => TestMapperMethodStatic(vm.Text),
t => t.Text,
v => v);
testViewModel.Text = "Updated";
Assert.Equal("UPDATED", txtb1.Text);
}
[Fact]
public void CaptureMethodBinding_ByDefault_ShouldSetValue()
{
var testViewModel = new TestViewModel
{
Text = "test"
};
var grid = new Grid<TestViewModel>
{
DataContext = testViewModel
};
var txtb1 = grid.CreateChild<TextBlock<TestViewModel>>();
txtb1.Bind(
txtb1,
vm => TestMapperMethod(vm.Text),
t => t.Text,
v => v);
Assert.Equal("TEST", txtb1.Text);
}
[Fact]
public void CaptureMethodBinding_AfterUpdate_ShouldSetValue()
{
var testViewModel = new TestViewModel
{
Text = "test"
};
var grid = new Grid<TestViewModel>
{
DataContext = testViewModel
};
var txtb1 = grid.CreateChild<TextBlock<TestViewModel>>();
txtb1.Bind(
txtb1,
vm => TestMapperMethod(vm.Text),
t => t.Text,
v => v);
testViewModel.Text = "Updated";
Assert.Equal("UPDATED", txtb1.Text);
}
[Fact]
public void FallbackValue_BindingFails_ShouldBeApplied()
{
var grid = new Grid<TestViewModel>();
var txtb1 = grid.CreateChild<TextBlock<TestViewModel>>();
txtb1.Bind(
txtb1,
vm => vm.Items,
t => t.Text,
v => v.ToString(),
fallbackValue: "Fallback");
Assert.Equal("Fallback", txtb1.Text);
}
[Fact]
public void ConditionalExpression_WhenTrue_ShouldResultInTrueValue()
{
var grid = new Grid<TestViewModel>();
var txtb1 = grid.CreateChild<TextBlock<TestViewModel>>();
txtb1.Bind(
txtb1,
vm => vm == null ? "null" : "not null",
t => t.Text,
v => v);
Assert.Equal("null", txtb1.Text);
}
[Fact]
public void ConditionalExpression_WhenFalse_ShouldResultInFalseValue()
{
var testViewModel = new TestViewModel();
var grid = new Grid<TestViewModel>
{
DataContext = testViewModel
};
var txtb1 = grid.CreateChild<TextBlock<TestViewModel>>();
txtb1.Bind(
txtb1,
vm => vm == null ? "null" : "not null",
t => t.Text,
v => v);
Assert.Equal("not null", txtb1.Text);
}
[Fact]
public void ConditionalExpressionWithNestedValues_WhenTrue_ShouldResultInTrueNestedValue()
{
var testViewModel = new TestViewModel();
var grid = new Grid<TestViewModel>
{
DataContext = testViewModel
};
var txtb1 = grid.CreateChild<TextBlock<TestViewModel>>();
txtb1.Bind(
txtb1,
vm => vm != null ? vm.Items.Count : -1,
t => t.Text,
v => v.ToString());
Assert.Equal("1", txtb1.Text);
}
[Fact]
public void ConditionalExpressionWithNestedValues_WhenFalse_ShouldResultInFalseNestedValue()
{
var testViewModel = new TestViewModel();
var grid = new Grid<TestViewModel>
{
DataContext = testViewModel
};
var txtb1 = grid.CreateChild<TextBlock<TestViewModel>>();
txtb1.Bind(
txtb1,
vm => vm == null ? -1 : vm.Items.Count,
t => t.Text,
v => v.ToString());
Assert.Equal("1", txtb1.Text);
}
[Fact]
public void MixedBindingWithMethodCall_WhenRootChanged_ShouldUpdate()
{
var testViewModel = new TestViewModel();
var grid = new Grid<TestViewModel>
{
DataContext = testViewModel
};
var txtb1 = grid.CreateChild<TextBlock<TestViewModel>>();
txtb1.Bind(
txtb1,
vm => vm.Items[0].GetNestedItem(0).OtherItems.Count,
t => t.Text,
v => v.ToString());
testViewModel.Items = new()
{
TestNestedCollectionItem.Create(4,
TestNestedCollectionItem.Create(
6,
TestNestedCollectionItem.Create(4)
),
TestNestedCollectionItem.Create(1),
TestNestedCollectionItem.Create(2),
TestNestedCollectionItem.Create(3)
),
TestNestedCollectionItem.Create(1),
TestNestedCollectionItem.Create(2),
};
Assert.Equal("6", txtb1.Text);
}
[Fact]
public void NestedPropertyBinding_WhenRootChanged_ShouldUpdateText()
{
var testViewModel = new TestViewModel();
var grid = new Grid<TestViewModel>
{
DataContext = testViewModel
};
var txtb1 = grid.CreateChild<TextBlock<TestViewModel>>();
txtb1.Bind(
txtb1,
vm => vm.Items.Count,
t => t.Text,
v => v.ToString());
testViewModel.Items = new List<TestNestedCollectionItem>
{
TestNestedCollectionItem.Create(4,
TestNestedCollectionItem.Create(
6,
TestNestedCollectionItem.Create(4)
),
TestNestedCollectionItem.Create(1),
TestNestedCollectionItem.Create(2),
TestNestedCollectionItem.Create(3)
),
TestNestedCollectionItem.Create(1),
TestNestedCollectionItem.Create(2),
};
Assert.Equal("3", txtb1.Text);
}
}

View File

@@ -0,0 +1 @@
global using Xunit;

View File

@@ -0,0 +1,10 @@
namespace TerminalUI.Tests.Models;
public class TestCollectionItem
{
public List<TestItem>? Items1 { get; set; } = new()
{
new TestItem() {Name = "Name1"},
new TestItem() {Name = "Name2"},
};
}

View File

@@ -0,0 +1,6 @@
namespace TerminalUI.Tests.Models;
public class TestItem
{
public string? Name { get; set; }
}

View File

@@ -0,0 +1,22 @@
namespace TerminalUI.Tests.Models;
public class TestNestedCollectionItem
{
public List<TestNestedCollectionItem> Items { get; set; } = new();
public List<TestCollectionItem> OtherItems { get; set; } = new();
public TestNestedCollectionItem GetNestedItem(int index) => Items[index];
public TestCollectionItem GetSimpleItem(int index) => OtherItems[index];
public static TestNestedCollectionItem Create(int otherItemCount, params TestNestedCollectionItem[] items)
{
var collection = new TestNestedCollectionItem()
{
Items = items.ToList()
};
for (var i = 0; i < otherItemCount; i++)
collection.OtherItems.Add(new TestCollectionItem());
return collection;
}
}

View File

@@ -0,0 +1,32 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using PropertyChanged.SourceGenerator;
namespace TerminalUI.Tests.Models;
public sealed partial class TestViewModel : INotifyPropertyChanged
{
[Notify] private List<TestNestedCollectionItem> _items;
[Notify] private string _text;
public TestViewModel()
{
_text = "Initial text";
_items = new List<TestNestedCollectionItem>
{
TestNestedCollectionItem.Create(
3,
TestNestedCollectionItem.Create(
1,
TestNestedCollectionItem.Create(2)
),
new()
)
};
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

View File

@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="PropertyChanged.SourceGenerator" Version="1.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.2.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\TerminalUI\TerminalUI.csproj" />
</ItemGroup>
</Project>