csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
431 lines
13 KiB
431 lines
13 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.Reactive;
|
|
using System.Reactive.Linq;
|
|
using System.Reactive.Subjects;
|
|
using System.Threading.Tasks;
|
|
using Avalonia.Data;
|
|
using Avalonia.Data.Core;
|
|
using Avalonia.Data.Core.ExpressionNodes.Reflection;
|
|
using Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings;
|
|
using Avalonia.Threading;
|
|
using Avalonia.UnitTests;
|
|
using Microsoft.Reactive.Testing;
|
|
using Xunit;
|
|
|
|
namespace Avalonia.Base.UnitTests.Data.Core
|
|
{
|
|
public class BindingExpressionTests_Property
|
|
{
|
|
[Fact]
|
|
public async Task Should_Get_Simple_Property_Value()
|
|
{
|
|
var data = new { Foo = "foo" };
|
|
var target = BindingExpression.Create(data, o => o.Foo);
|
|
var result = await target.Take(1);
|
|
|
|
Assert.Equal("foo", result);
|
|
|
|
GC.KeepAlive(data);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Should_Get_Simple_Property_Value_Null()
|
|
{
|
|
var data = new { Foo = (string)null };
|
|
var target = BindingExpression.Create(data, o => o.Foo);
|
|
var result = await target.Take(1);
|
|
|
|
Assert.Null(result);
|
|
|
|
GC.KeepAlive(data);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Should_Get_Simple_Property_From_Base_Class()
|
|
{
|
|
var data = new Class3 { Foo = "foo" };
|
|
var target = BindingExpression.Create(data, o => o.Foo);
|
|
var result = await target.Take(1);
|
|
|
|
Assert.Equal("foo", result);
|
|
|
|
GC.KeepAlive(data);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Should_Return_BindingNotification_Error_For_Root_Null()
|
|
{
|
|
var target = BindingExpression.Create(default(Class3), o => o.Foo);
|
|
var result = await target.Take(1);
|
|
|
|
Assert.Equal(
|
|
new BindingNotification(
|
|
new BindingChainException("Binding Source is null.", "Foo", "(source)"),
|
|
BindingErrorType.Error,
|
|
AvaloniaProperty.UnsetValue),
|
|
result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Should_Return_BindingNotification_Error_For_Root_UnsetValue()
|
|
{
|
|
var target = BindingExpression.Create(AvaloniaProperty.UnsetValue, o => (o as Class3).Foo);
|
|
var result = await target.Take(1);
|
|
|
|
Assert.Equal(
|
|
new BindingNotification(
|
|
new BindingChainException("Binding Source is null.", "Foo", "(source)"),
|
|
BindingErrorType.Error,
|
|
AvaloniaProperty.UnsetValue),
|
|
result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Should_Get_Simple_Property_Chain()
|
|
{
|
|
var data = new { Foo = new { Bar = new { Baz = "baz" } } };
|
|
var target = BindingExpression.Create(data, o => o.Foo.Bar.Baz);
|
|
var result = await target.Take(1);
|
|
|
|
Assert.Equal("baz", result);
|
|
|
|
GC.KeepAlive(data);
|
|
}
|
|
|
|
[Fact]
|
|
public void Should_Return_BindingNotification_Error_For_Chain_With_Null_Value()
|
|
{
|
|
var data = new { Foo = default(Class1) };
|
|
var target = BindingExpression.Create(data, o => o.Foo.Foo.Length);
|
|
var result = new List<object>();
|
|
|
|
target.Subscribe(x => result.Add(x));
|
|
|
|
Assert.Equal(
|
|
new[]
|
|
{
|
|
new BindingNotification(
|
|
new BindingChainException("Value is null.", "Foo.Foo.Length", "Foo"),
|
|
BindingErrorType.Error,
|
|
AvaloniaProperty.UnsetValue),
|
|
},
|
|
result);
|
|
|
|
GC.KeepAlive(data);
|
|
}
|
|
|
|
[Fact]
|
|
public void Should_Track_Simple_Property_Value()
|
|
{
|
|
var data = new Class1 { Foo = "foo" };
|
|
var target = BindingExpression.Create(data, o => o.Foo);
|
|
var result = new List<object>();
|
|
|
|
var sub = target.Subscribe(x => result.Add(x));
|
|
data.Foo = "bar";
|
|
|
|
Assert.Equal(new[] { "foo", "bar" }, result);
|
|
|
|
sub.Dispose();
|
|
|
|
// Forces WeakEvent compact
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
Assert.Equal(0, data.PropertyChangedSubscriptionCount);
|
|
|
|
GC.KeepAlive(data);
|
|
}
|
|
|
|
[Fact]
|
|
public void Should_Trigger_PropertyChanged_On_Null_Or_Empty_String()
|
|
{
|
|
var data = new Class1 { Bar = "foo" };
|
|
var target = BindingExpression.Create(data, o => o.Bar);
|
|
var result = new List<object>();
|
|
|
|
var sub = target.Subscribe(x => result.Add(x));
|
|
|
|
Assert.Equal(new[] { "foo" }, result);
|
|
|
|
data.Bar = "bar";
|
|
|
|
Assert.Equal(new[] { "foo" }, result);
|
|
|
|
data.RaisePropertyChanged(string.Empty);
|
|
|
|
Assert.Equal(new[] { "foo", "bar" }, result);
|
|
|
|
data.SetBarWithoutRaising("baz");
|
|
|
|
Assert.Equal(new[] { "foo", "bar" }, result);
|
|
|
|
data.RaisePropertyChanged(null);
|
|
|
|
Assert.Equal(new[] { "foo", "bar", "baz" }, result);
|
|
|
|
sub.Dispose();
|
|
|
|
// Forces WeakEvent compact
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
Assert.Equal(0, data.PropertyChangedSubscriptionCount);
|
|
|
|
GC.KeepAlive(data);
|
|
}
|
|
|
|
[Fact]
|
|
public void Should_Track_End_Of_Property_Chain_Changing()
|
|
{
|
|
var data = new Class1 { Next = new Class2 { Bar = "bar" } };
|
|
var target = BindingExpression.Create(data, o => (o.Next as Class2).Bar);
|
|
var result = new List<object>();
|
|
|
|
var sub = target.Subscribe(x => result.Add(x));
|
|
((Class2)data.Next).Bar = "baz";
|
|
((Class2)data.Next).Bar = null;
|
|
|
|
Assert.Equal(new[] { "bar", "baz", null }, result);
|
|
|
|
sub.Dispose();
|
|
// Forces WeakEvent compact
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
Assert.Equal(0, data.PropertyChangedSubscriptionCount);
|
|
Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount);
|
|
|
|
GC.KeepAlive(data);
|
|
}
|
|
|
|
[Fact]
|
|
public void Should_Track_Property_Chain_Changing()
|
|
{
|
|
var data = new Class1 { Next = new Class2 { Bar = "bar" } };
|
|
var target = BindingExpression.Create(data, o => (o.Next as Class2).Bar);
|
|
var result = new List<object>();
|
|
|
|
var sub = target.Subscribe(x => result.Add(x));
|
|
var old = data.Next;
|
|
data.Next = new Class2 { Bar = "baz" };
|
|
data.Next = new Class2 { Bar = null };
|
|
|
|
Assert.Equal(new[] { "bar", "baz", null }, result);
|
|
|
|
sub.Dispose();
|
|
|
|
// Forces WeakEvent compact
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
Assert.Equal(0, data.PropertyChangedSubscriptionCount);
|
|
Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount);
|
|
Assert.Equal(0, old.PropertyChangedSubscriptionCount);
|
|
|
|
GC.KeepAlive(data);
|
|
}
|
|
|
|
[Fact]
|
|
public void Should_Track_Property_Chain_Breaking_With_Null_Then_Mending()
|
|
{
|
|
var data = new Class1
|
|
{
|
|
Next = new Class2
|
|
{
|
|
Next = new Class2
|
|
{
|
|
Bar = "bar"
|
|
}
|
|
}
|
|
};
|
|
|
|
var target = BindingExpression.Create(data, o => ((o.Next as Class2).Next as Class2).Bar);
|
|
var result = new List<object>();
|
|
|
|
var sub = target.Subscribe(x => result.Add(x));
|
|
var old = data.Next;
|
|
data.Next = new Class2 { Bar = "baz" };
|
|
data.Next = old;
|
|
|
|
Assert.Equal(
|
|
new object[]
|
|
{
|
|
"bar",
|
|
new BindingNotification(
|
|
new BindingChainException("Value is null.", "Next.Next.Bar", "Next"),
|
|
BindingErrorType.Error,
|
|
AvaloniaProperty.UnsetValue),
|
|
"bar"
|
|
},
|
|
result);
|
|
|
|
sub.Dispose();
|
|
|
|
// Forces WeakEvent compact
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
Assert.Equal(0, data.PropertyChangedSubscriptionCount);
|
|
Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount);
|
|
Assert.Equal(0, old.PropertyChangedSubscriptionCount);
|
|
|
|
GC.KeepAlive(data);
|
|
}
|
|
|
|
[Fact]
|
|
public void Should_Track_Property_Chain_Breaking_With_Missing_Member_Then_Mending()
|
|
{
|
|
var data = new Class1 { Next = new Class2 { Bar = "bar" } };
|
|
var target = BindingExpression.Create(data, o => (o.Next as Class2).Bar);
|
|
var result = new List<object>();
|
|
|
|
var sub = target.Subscribe(x => result.Add(x));
|
|
var old = data.Next;
|
|
var breaking = new WithoutBar();
|
|
data.Next = breaking;
|
|
data.Next = new Class2 { Bar = "baz" };
|
|
|
|
Assert.Equal(
|
|
new object[]
|
|
{
|
|
"bar",
|
|
new BindingNotification(
|
|
new BindingChainException(
|
|
$"Could not find a matching property accessor for '{nameof(Class2.Bar)}' on '{typeof(WithoutBar)}'.",
|
|
"Next.Bar",
|
|
"Bar"),
|
|
BindingErrorType.Error),
|
|
"baz",
|
|
},
|
|
result);
|
|
|
|
sub.Dispose();
|
|
|
|
// Forces WeakEvent compact
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
Assert.Equal(0, data.PropertyChangedSubscriptionCount);
|
|
Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount);
|
|
Assert.Equal(0, breaking.PropertyChangedSubscriptionCount);
|
|
Assert.Equal(0, old.PropertyChangedSubscriptionCount);
|
|
|
|
GC.KeepAlive(data);
|
|
}
|
|
|
|
[Fact]
|
|
public void Should_Not_Keep_Source_Alive()
|
|
{
|
|
Func<Tuple<BindingExpression, WeakReference>> run = () =>
|
|
{
|
|
var source = new Class1 { Foo = "foo" };
|
|
var target = BindingExpression.Create(source, o => o.Foo);
|
|
return Tuple.Create(target, new WeakReference(source));
|
|
};
|
|
|
|
var result = run();
|
|
result.Item1.Subscribe(x => { });
|
|
|
|
// Mono trickery
|
|
GC.Collect(2);
|
|
GC.WaitForPendingFinalizers();
|
|
GC.WaitForPendingFinalizers();
|
|
GC.Collect(2);
|
|
|
|
|
|
Assert.Null(result.Item2.Target);
|
|
}
|
|
|
|
[Fact]
|
|
public void Should_Not_Throw_Exception_On_Duplicate_Properties()
|
|
{
|
|
// Repro of https://github.com/AvaloniaUI/Avalonia/issues/4733.
|
|
var source = new MyViewModel();
|
|
var target = BindingExpression.Create(source, x => x.Name);
|
|
var result = new List<object>();
|
|
|
|
target.Subscribe(x => result.Add(x));
|
|
|
|
Assert.Equal(new[] { "NewName" }, result);
|
|
}
|
|
|
|
public class MyViewModelBase { public object Name => "Name"; }
|
|
|
|
public class MyViewModel : MyViewModelBase { public new string Name => "NewName"; }
|
|
|
|
private interface INext
|
|
{
|
|
int PropertyChangedSubscriptionCount { get; }
|
|
}
|
|
|
|
private class Class1 : NotifyingBase
|
|
{
|
|
private string _foo;
|
|
private INext _next;
|
|
|
|
public string Foo
|
|
{
|
|
get { return _foo; }
|
|
set
|
|
{
|
|
_foo = value;
|
|
RaisePropertyChanged(nameof(Foo));
|
|
}
|
|
}
|
|
|
|
private string _bar;
|
|
public string Bar
|
|
{
|
|
get { return _bar; }
|
|
set { _bar = value; }
|
|
}
|
|
|
|
public INext Next
|
|
{
|
|
get { return _next; }
|
|
set
|
|
{
|
|
_next = value;
|
|
RaisePropertyChanged(nameof(Next));
|
|
}
|
|
}
|
|
|
|
public void SetBarWithoutRaising(string value) => _bar = value;
|
|
}
|
|
|
|
private class Class2 : NotifyingBase, INext
|
|
{
|
|
private string _bar;
|
|
private INext _next;
|
|
|
|
public string Bar
|
|
{
|
|
get { return _bar; }
|
|
set
|
|
{
|
|
_bar = value;
|
|
RaisePropertyChanged(nameof(Bar));
|
|
}
|
|
}
|
|
|
|
public INext Next
|
|
{
|
|
get { return _next; }
|
|
set
|
|
{
|
|
_next = value;
|
|
RaisePropertyChanged(nameof(Next));
|
|
}
|
|
}
|
|
}
|
|
|
|
private class Class3 : Class1
|
|
{
|
|
}
|
|
|
|
private class WithoutBar : NotifyingBase, INext
|
|
{
|
|
}
|
|
|
|
private static Recorded<Notification<T>> OnNext<T>(long time, T value)
|
|
{
|
|
return new Recorded<Notification<T>>(time, Notification.CreateOnNext<T>(value));
|
|
}
|
|
}
|
|
}
|
|
|