123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490 |
- using System;
- using System.Collections.Generic;
- using System.Globalization;
- using Avalonia.Controls;
- using Avalonia.Controls.Converters;
- using Avalonia.Data;
- using Avalonia.Data.Converters;
- using Avalonia.Data.Core;
- using Avalonia.Data.Core.Plugins;
- using Avalonia.Input;
- using Avalonia.Logging;
- using Avalonia.LogicalTree;
- 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);
- }
- }
|