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.
487 lines
17 KiB
487 lines
17 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Data;
|
|
using Avalonia.Data.Converters;
|
|
using Avalonia.Data.Core;
|
|
using Avalonia.Input;
|
|
using Avalonia.Logging;
|
|
using Avalonia.Markup.Xaml.MarkupExtensions;
|
|
using Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings;
|
|
using Avalonia.Reactive;
|
|
using Avalonia.UnitTests;
|
|
using Xunit;
|
|
|
|
#nullable enable
|
|
|
|
namespace Avalonia.Markup.UnitTests.Data
|
|
{
|
|
public class BindingTests_Logging : ScopedTestBase
|
|
{
|
|
public class DataContext : ScopedTestBase
|
|
{
|
|
[Fact]
|
|
public void Should_Not_Log_Missing_Member_On_Null_DataContext()
|
|
{
|
|
var target = new Decorator { };
|
|
var root = new TestRoot(target);
|
|
var binding = new Binding("Foo");
|
|
|
|
using (AssertNoLog())
|
|
{
|
|
target.Bind(Control.TagProperty, binding);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Should_Log_Missing_Member_On_DataContext()
|
|
{
|
|
var target = new Decorator { DataContext = new TestClass("foo") };
|
|
var root = new TestRoot(target);
|
|
var binding = new Binding("Foo.Bar");
|
|
|
|
using (AssertLog(
|
|
target,
|
|
binding.Path,
|
|
"Could not find a matching property accessor for 'Bar' on 'System.String'.",
|
|
"Bar"))
|
|
{
|
|
target.Bind(Control.TagProperty, binding);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Should_Log_Null_In_Binding_Chain()
|
|
{
|
|
var target = new Decorator { DataContext = new TestClass() };
|
|
var root = new TestRoot(target);
|
|
var binding = new Binding("Foo.Length");
|
|
|
|
using (AssertLog(target, binding.Path, "Value is null.", "Foo"))
|
|
{
|
|
target.Bind(Control.TagProperty, binding);
|
|
}
|
|
}
|
|
}
|
|
|
|
public class Source : ScopedTestBase
|
|
{
|
|
[Fact]
|
|
public void Should_Log_Null_Source()
|
|
{
|
|
var target = new Decorator { };
|
|
var root = new TestRoot(target);
|
|
var binding = new Binding("Foo") { Source = null };
|
|
|
|
using (AssertLog(target, binding.Path, "Binding Source is null.", "(source)"))
|
|
{
|
|
target.Bind(Control.TagProperty, binding);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Should_Log_Null_Source_For_Unrooted_Control()
|
|
{
|
|
var target = new Decorator { };
|
|
var binding = new Binding("Foo") { Source = null };
|
|
|
|
using (AssertLog(target, binding.Path, "Binding Source is null.", "(source)"))
|
|
{
|
|
target.Bind(Control.TagProperty, binding);
|
|
}
|
|
}
|
|
}
|
|
|
|
public class LogicalAncestor : ScopedTestBase
|
|
{
|
|
[Fact]
|
|
public void Should_Log_Ancestor_Not_Found()
|
|
{
|
|
var target = new Decorator { };
|
|
var root = new TestRoot(target);
|
|
var binding = new Binding("$parent[TextBlock]") { TypeResolver = ResolveType };
|
|
|
|
using (AssertLog(target, binding.Path, "Ancestor not found.", "$parent[TextBlock]"))
|
|
{
|
|
target.Bind(Control.TagProperty, binding);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Should_Not_Log_Ancestor_Not_Found_For_Unrooted_Control()
|
|
{
|
|
var target = new Decorator { };
|
|
var binding = new Binding("$parent[TextBlock]") { TypeResolver = ResolveType };
|
|
|
|
using (AssertNoLog())
|
|
{
|
|
target.Bind(Control.TagProperty, binding);
|
|
}
|
|
}
|
|
}
|
|
|
|
public class VisualAncestor : ScopedTestBase
|
|
{
|
|
[Fact]
|
|
public void Should_Log_Ancestor_Not_Found()
|
|
{
|
|
var target = new Decorator { };
|
|
var root = new TestRoot(target);
|
|
var binding = new Binding
|
|
{
|
|
RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor)
|
|
{
|
|
AncestorType = typeof(TextBlock),
|
|
}
|
|
};
|
|
|
|
using (AssertLog(target, "$visualParent[TextBlock]", "Ancestor not found.", "$visualParent[TextBlock]"))
|
|
{
|
|
target.Bind(Control.TagProperty, binding);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Should_Log_Ancestor_Property_Not_Found()
|
|
{
|
|
var target = new Decorator { };
|
|
var root = new TestRoot(target);
|
|
var binding = new Binding("Foo")
|
|
{
|
|
RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor)
|
|
{
|
|
AncestorType = typeof(TestRoot),
|
|
}
|
|
};
|
|
|
|
using (AssertLog(
|
|
target,
|
|
"$visualParent[TestRoot].Foo",
|
|
"Could not find a matching property accessor for 'Foo' on 'Avalonia.UnitTests.TestRoot'.",
|
|
"Foo"))
|
|
{
|
|
target.Bind(Control.TagProperty, binding);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Should_Not_Log_Ancestor_Not_Found_For_Unrooted_Control()
|
|
{
|
|
var target = new Decorator { };
|
|
var binding = new Binding
|
|
{
|
|
RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor)
|
|
{
|
|
AncestorType = typeof(Window),
|
|
}
|
|
};
|
|
|
|
using (AssertNoLog())
|
|
{
|
|
target.Bind(Control.TagProperty, binding);
|
|
}
|
|
}
|
|
}
|
|
|
|
public class NamedElement : ScopedTestBase
|
|
{
|
|
[Fact]
|
|
public void Should_Log_NameScope_Not_Found()
|
|
{
|
|
var target = new Decorator { };
|
|
var root = new TestRoot(target);
|
|
var binding = new Binding("#source") { TypeResolver = ResolveType };
|
|
|
|
using (AssertLog(target, binding.Path, "NameScope not found.", "#source"))
|
|
{
|
|
target.Bind(Control.TagProperty, binding);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Should_Not_Log_Element_Property_Null_For_Unrooted_Control()
|
|
{
|
|
var ns = new NameScope();
|
|
var source = new Canvas { Name = "source" };
|
|
var target = new Decorator { };
|
|
var binding = new Binding("#source.DataContext.Foo") { TypeResolver = ResolveType, NameScope = new(ns) };
|
|
var container = new StackPanel
|
|
{
|
|
[NameScope.NameScopeProperty] = ns,
|
|
Children = { source, target }
|
|
};
|
|
|
|
ns.Register(source.Name, source);
|
|
|
|
using (AssertNoLog())
|
|
{
|
|
target.Bind(Control.TagProperty, binding);
|
|
}
|
|
|
|
// Sanity check to that the binding works when rooted: make sure that we're not just testing a broken
|
|
// binding!
|
|
using (AssertNoLog())
|
|
{
|
|
var root = new TestRoot(container);
|
|
root.DataContext = new { Foo = "foo" };
|
|
Assert.Equal("foo", target.Tag);
|
|
}
|
|
}
|
|
}
|
|
|
|
public class Converter : ScopedTestBase
|
|
{
|
|
[Fact]
|
|
public void Should_Log_Error_For_Unconvertible_Type()
|
|
{
|
|
var target = new Decorator { DataContext = new { Foo = new System.Version() } };
|
|
var root = new TestRoot(target);
|
|
var binding = new Binding("Foo");
|
|
|
|
using (AssertLog(
|
|
target,
|
|
binding.Path,
|
|
"Could not convert '0.0' (System.Version) to 'Avalonia.Thickness'.",
|
|
property: Control.MarginProperty))
|
|
{
|
|
target.Bind(Control.MarginProperty, binding);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Should_Log_Error_For_Unconvertible_Type_With_Converter()
|
|
{
|
|
var target = new Decorator { DataContext = new { Foo = new System.Version() } };
|
|
var root = new TestRoot(target);
|
|
var binding = new Binding("Foo")
|
|
{
|
|
Converter = new ThrowingConverter(),
|
|
};
|
|
|
|
using (AssertLog(
|
|
target,
|
|
binding.Path,
|
|
"Could not convert '0.0' (System.Version) to 'Avalonia.Thickness' " +
|
|
"using 'Avalonia.Markup.UnitTests.Data.BindingTests_Logging+ThrowingConverter': " +
|
|
"The method or operation is not implemented.",
|
|
property: Control.MarginProperty))
|
|
{
|
|
target.Bind(Control.MarginProperty, binding);
|
|
}
|
|
}
|
|
}
|
|
|
|
public class Fallback : ScopedTestBase
|
|
{
|
|
[Theory]
|
|
[InlineData(true)]
|
|
[InlineData(false)]
|
|
public void Should_Log_Invalid_FallbackValue(bool rooted)
|
|
{
|
|
var target = new Decorator { };
|
|
var binding = new Binding("foo") { FallbackValue = "bar" };
|
|
|
|
if (rooted)
|
|
new TestRoot(target);
|
|
|
|
// An invalid fallback value is invalid whether the control is rooted or not.
|
|
using (AssertLog(
|
|
target,
|
|
binding.Path,
|
|
"Could not convert FallbackValue 'bar' to 'System.Double'.",
|
|
level: LogEventLevel.Error,
|
|
property: Visual.OpacityProperty))
|
|
{
|
|
target.Bind(Visual.OpacityProperty, binding);
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(true)]
|
|
[InlineData(false)]
|
|
public void Should_Log_Invalid_TargetNullValue(bool rooted)
|
|
{
|
|
var target = new Decorator { DataContext = new { Bar = (string?) null } };
|
|
var binding = new Binding("Bar") { TargetNullValue = "foo" };
|
|
|
|
if (rooted)
|
|
new TestRoot(target);
|
|
|
|
// An invalid target null value is invalid whether the control is rooted or not.
|
|
using (AssertLog(
|
|
target,
|
|
binding.Path,
|
|
"Could not convert TargetNullValue 'foo' to 'System.Double'.",
|
|
level: LogEventLevel.Error,
|
|
property: Visual.OpacityProperty))
|
|
{
|
|
target.Bind(Visual.OpacityProperty, binding);
|
|
}
|
|
}
|
|
}
|
|
|
|
public class NonControlDataContext : ScopedTestBase
|
|
{
|
|
[Fact]
|
|
public void Should_Not_Log_Missing_Member_On_Null_DataContext()
|
|
{
|
|
var target = new TestRoot();
|
|
var binding = new Binding("Foo") { DefaultAnchor = new(target) };
|
|
|
|
target.KeyBindings.Add(new KeyBinding
|
|
{
|
|
Gesture = new KeyGesture(Key.A),
|
|
[!KeyBinding.CommandProperty] = binding
|
|
});
|
|
|
|
using (AssertNoLog())
|
|
{
|
|
target.Bind(Control.TagProperty, binding);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Should_Log_Missing_Member_On_DataContext()
|
|
{
|
|
var target = new TestRoot();
|
|
var binding = new Binding("Foo") { DefaultAnchor = new(target) };
|
|
|
|
target.KeyBindings.Add(new KeyBinding
|
|
{
|
|
Gesture = new KeyGesture(Key.A),
|
|
[!KeyBinding.CommandProperty] = binding
|
|
});
|
|
|
|
target.DataContext = new object();
|
|
|
|
using (AssertLog(
|
|
target,
|
|
binding.Path,
|
|
"Could not find a matching property accessor for 'Foo' on 'System.Object'.",
|
|
"Foo"))
|
|
{
|
|
target.Bind(Control.TagProperty, binding);
|
|
}
|
|
}
|
|
}
|
|
|
|
public class CompiledBinding : ScopedTestBase
|
|
{
|
|
[Fact]
|
|
public void Should_Log_For_Invalid_DataContext_Type()
|
|
{
|
|
var target = new TestRoot { DataContext = 48 };
|
|
var stringLengthProperty = new ClrPropertyInfo(
|
|
"Length",
|
|
x => ((string)x).Length,
|
|
null,
|
|
typeof(int));
|
|
var bindingPath = new CompiledBindingPathBuilder()
|
|
.Property(stringLengthProperty, PropertyInfoAccessorFactory.CreateInpcPropertyAccessor)
|
|
.Build();
|
|
var binding = new CompiledBindingExtension(bindingPath);
|
|
|
|
using (AssertLog(
|
|
target,
|
|
bindingPath.ToString(),
|
|
"Unable to cast object of type 'System.Int32' to type 'System.String'.",
|
|
"Length"))
|
|
{
|
|
target.Bind(Control.TagProperty, binding);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static IDisposable AssertLog(
|
|
AvaloniaObject target,
|
|
string expression,
|
|
string message,
|
|
string? errorPoint = null,
|
|
LogEventLevel level = LogEventLevel.Warning,
|
|
AvaloniaProperty? property = null)
|
|
{
|
|
var logs = new List<LogMessage>();
|
|
var sink = TestLogSink.Start((l, a, s, m, p) =>
|
|
{
|
|
if (l >= level)
|
|
logs.Add(new(l, a, s, m, p));
|
|
});
|
|
|
|
return Disposable.Create(() =>
|
|
{
|
|
sink.Dispose();
|
|
Assert.Equal(1, logs.Count);
|
|
|
|
var l = logs[0];
|
|
var messageTemplate = errorPoint is not null ?
|
|
"An error occurred binding {Property} to {Expression} at {ExpressionErrorPoint}: {Message}" :
|
|
"An error occurred binding {Property} to {Expression}: {Message}";
|
|
|
|
Assert.Equal(level, l.level);
|
|
Assert.Equal(LogArea.Binding, l.area);
|
|
Assert.Equal(target, l.source);
|
|
Assert.Equal(messageTemplate, l.messageTemplate);
|
|
Assert.Equal(property ?? Control.TagProperty, l.propertyValues[0]);
|
|
Assert.Equal(expression, l.propertyValues[1]);
|
|
|
|
if (errorPoint is not null)
|
|
{
|
|
Assert.Equal(errorPoint, l.propertyValues[2]);
|
|
Assert.Equal(message, l.propertyValues[3]);
|
|
}
|
|
else
|
|
{
|
|
Assert.Equal(message, l.propertyValues[2]);
|
|
}
|
|
});
|
|
}
|
|
|
|
private static IDisposable AssertNoLog()
|
|
{
|
|
var count = 0;
|
|
var sink = TestLogSink.Start((l, a, s, m, p) =>
|
|
{
|
|
if (l >= LogEventLevel.Warning)
|
|
++count;
|
|
});
|
|
|
|
return Disposable.Create(() =>
|
|
{
|
|
sink.Dispose();
|
|
Assert.Equal(0, count);
|
|
});
|
|
}
|
|
|
|
private static Type ResolveType(string? ns, string typeName)
|
|
{
|
|
return typeName switch
|
|
{
|
|
"TextBlock" => typeof(TextBlock),
|
|
"TestRoot" => typeof(TestRoot),
|
|
_ => throw new InvalidOperationException($"Could not resolve type {typeName}.")
|
|
};
|
|
}
|
|
|
|
private class TestClass
|
|
{
|
|
public TestClass(string? foo = null) => Foo = foo;
|
|
public string? Foo { get; set; }
|
|
}
|
|
|
|
private class ThrowingConverter : IValueConverter
|
|
{
|
|
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
}
|
|
|
|
private record LogMessage(LogEventLevel level, string area, object? source, string messageTemplate, params object?[] propertyValues);
|
|
}
|
|
}
|
|
|