From 4e642623abafd7a32072977e5f123a55872b0699 Mon Sep 17 00:00:00 2001 From: Stephane Delcroix Date: Thu, 25 Apr 2024 15:23:30 +0200 Subject: [PATCH 1/3] [X] do not apply Bindings if DataType doesnt match Make sure the behavior of bindings is consistent accross implementations (compiled/not compiled) fixes a bunch of issues --- src/Controls/src/Core/Binding.cs | 11 +++- .../src/Core/IXamlDataTypeProvider.cs | 5 ++ .../Xaml/MarkupExtensions/BindingExtension.cs | 11 +++- src/Controls/src/Xaml/XamlServiceProvider.cs | 56 +++++++++++++++++++ .../Xaml.UnitTests/BindingsCompiler.xaml.cs | 14 +++-- 5 files changed, 90 insertions(+), 7 deletions(-) create mode 100644 src/Controls/src/Core/IXamlDataTypeProvider.cs diff --git a/src/Controls/src/Core/Binding.cs b/src/Controls/src/Core/Binding.cs index e6eb96bb16ff..057807ab7492 100644 --- a/src/Controls/src/Core/Binding.cs +++ b/src/Controls/src/Core/Binding.cs @@ -105,6 +105,8 @@ public string UpdateSourceEventName } } + internal Type DataType { get; set; } + internal override void Apply(bool fromTarget) { base.Apply(fromTarget); @@ -120,7 +122,13 @@ internal override void Apply(object context, BindableObject bindObj, BindablePro object src = _source; var isApplied = IsApplied; - base.Apply(src ?? context, bindObj, targetProperty, fromBindingContextChanged, specificity); + var bindingContext = src ?? Context ?? context; + if (DataType != null && bindingContext != null && !DataType.IsAssignableFrom(bindingContext.GetType())) + { + bindingContext = null; + } + + base.Apply(bindingContext, bindObj, targetProperty, fromBindingContextChanged, specificity); if (src != null && isApplied && fromBindingContextChanged) return; @@ -131,7 +139,6 @@ internal override void Apply(object context, BindableObject bindObj, BindablePro } else { - object bindingContext = src ?? Context ?? context; if (_expression == null) _expression = new BindingExpression(this, SelfPath); _expression.Apply(bindingContext, bindObj, targetProperty, specificity); diff --git a/src/Controls/src/Core/IXamlDataTypeProvider.cs b/src/Controls/src/Core/IXamlDataTypeProvider.cs new file mode 100644 index 000000000000..0ccf28c7c934 --- /dev/null +++ b/src/Controls/src/Core/IXamlDataTypeProvider.cs @@ -0,0 +1,5 @@ +namespace Microsoft.Maui.Controls.Xaml; +interface IXamlDataTypeProvider +{ + string BindingDataType { get; } +} diff --git a/src/Controls/src/Xaml/MarkupExtensions/BindingExtension.cs b/src/Controls/src/Xaml/MarkupExtensions/BindingExtension.cs index 2ef129a53e9e..e5188f9939c3 100644 --- a/src/Controls/src/Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Controls/src/Xaml/MarkupExtensions/BindingExtension.cs @@ -1,11 +1,11 @@ using System; using System.ComponentModel; +using System.Linq.Expressions; using Microsoft.Maui.Controls.Internals; namespace Microsoft.Maui.Controls.Xaml { [ContentProperty(nameof(Path))] - [AcceptEmptyServiceProvider] public sealed class BindingExtension : IMarkupExtension { public string Path { get; set; } = Binding.SelfPath; @@ -21,12 +21,21 @@ public sealed class BindingExtension : IMarkupExtension BindingBase IMarkupExtension.ProvideValue(IServiceProvider serviceProvider) { + Type bindingXDataType = null; + if ((serviceProvider.GetService(typeof(IXamlTypeResolver)) is IXamlTypeResolver typeResolver) + && (serviceProvider.GetService(typeof(IXamlDataTypeProvider)) is IXamlDataTypeProvider dataTypeProvider) + && dataTypeProvider.BindingDataType != null) + { + typeResolver.TryResolve(dataTypeProvider.BindingDataType, out bindingXDataType); + } + if (TypedBinding == null) return new Binding(Path, Mode, Converter, ConverterParameter, StringFormat, Source) { UpdateSourceEventName = UpdateSourceEventName, FallbackValue = FallbackValue, TargetNullValue = TargetNullValue, + DataType = bindingXDataType, }; TypedBinding.Mode = Mode; diff --git a/src/Controls/src/Xaml/XamlServiceProvider.cs b/src/Controls/src/Xaml/XamlServiceProvider.cs index aa2e0cb42629..1c2b24f96dc4 100644 --- a/src/Controls/src/Xaml/XamlServiceProvider.cs +++ b/src/Controls/src/Xaml/XamlServiceProvider.cs @@ -27,6 +27,9 @@ internal XamlServiceProvider(INode node, HydrationContext context) IXmlLineInfoProvider = new XmlLineInfoProvider(xmlLineInfo); IValueConverterProvider = new ValueConverterProvider(); + + if (node is IElementNode elementNode) + Add(typeof(IXamlDataTypeProvider), new XamlDataTypeProvider(elementNode)); } public XamlServiceProvider() => IValueConverterProvider = new ValueConverterProvider(); @@ -262,4 +265,57 @@ public string LookupNamespace(string prefix) public string LookupPrefix(string namespaceName) => throw new NotImplementedException(); public void Add(string prefix, string ns) => namespaces.Add(prefix, ns); } + + class XamlDataTypeProvider : IXamlDataTypeProvider + { + public XamlDataTypeProvider(IElementNode node) + { + static IElementNode GetParent(IElementNode node) + { + return node switch + { + { Parent: ListNode { Parent: IElementNode parentNode } } => parentNode, + { Parent: IElementNode parentNode } => parentNode, + _ => null, + }; + } + + static bool IsBindingContextBinding(IElementNode node) + { + if ( ApplyPropertiesVisitor.TryGetPropertyName(node, node.Parent, out XmlName name) + && name.NamespaceURI == "" + && name.LocalName == nameof(BindableObject.BindingContext)) + return true; + return false; + } + + INode dataTypeNode = null; + IElementNode n = node as IElementNode; + + // Special handling for BindingContext={Binding ...} + // The order of checks is: + // - x:DataType on the binding itself + // - SKIP looking for x:DataType on the parent + // - continue looking for x:DataType on the parent's parent... + IElementNode skipNode = null; + if (IsBindingContextBinding(node)) + { + skipNode = GetParent(node); + } + + while (n != null) + { + if (n != skipNode && n.Properties.TryGetValue(XmlName.xDataType, out dataTypeNode)) + { + break; + } + + n = GetParent(n); + } + if (dataTypeNode is ValueNode valueNode) + BindingDataType = valueNode.Value as string; + + } + public string BindingDataType { get; } + } } \ No newline at end of file diff --git a/src/Controls/tests/Xaml.UnitTests/BindingsCompiler.xaml.cs b/src/Controls/tests/Xaml.UnitTests/BindingsCompiler.xaml.cs index d04d1bd78c64..79f3155db6b4 100644 --- a/src/Controls/tests/Xaml.UnitTests/BindingsCompiler.xaml.cs +++ b/src/Controls/tests/Xaml.UnitTests/BindingsCompiler.xaml.cs @@ -28,10 +28,9 @@ public class Tests { [SetUp] public void Setup() => DispatcherProvider.SetCurrent(new DispatcherProviderStub()); [TearDown] public void TearDown() => DispatcherProvider.SetCurrent(null); - - [TestCase(false)] - [TestCase(true)] - public void Test(bool useCompiledXaml) + + [Test] + public void TestCompiledBindings([Values(false, true)]bool useCompiledXaml) { if (useCompiledXaml) MockCompiler.Compile(typeof(BindingsCompiler)); @@ -113,6 +112,13 @@ public void Test(bool useCompiledXaml) layout.BindingContext = new object(); Assert.AreEqual(null, layout.label0.Text); } + + [Test] + public void BindingsNotAppliedWithWrongContext([Values(false, true)]bool useCompiledXaml) + { + var page = new BindingsCompiler(useCompiledXaml) { BindingContext = new {Text="Foo"} }; + Assert.AreEqual(null, page.label0.Text); + } } } From a494445e81b679ff6826fccd4ca7ab16211bb26b Mon Sep 17 00:00:00 2001 From: Stephane Delcroix Date: Thu, 16 May 2024 15:15:58 +0200 Subject: [PATCH 2/3] fixes --- .../Xaml/MarkupExtensions/BindingExtension.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Controls/src/Xaml/MarkupExtensions/BindingExtension.cs b/src/Controls/src/Xaml/MarkupExtensions/BindingExtension.cs index e5188f9939c3..74eee25ba432 100644 --- a/src/Controls/src/Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Controls/src/Xaml/MarkupExtensions/BindingExtension.cs @@ -21,15 +21,14 @@ public sealed class BindingExtension : IMarkupExtension BindingBase IMarkupExtension.ProvideValue(IServiceProvider serviceProvider) { - Type bindingXDataType = null; - if ((serviceProvider.GetService(typeof(IXamlTypeResolver)) is IXamlTypeResolver typeResolver) - && (serviceProvider.GetService(typeof(IXamlDataTypeProvider)) is IXamlDataTypeProvider dataTypeProvider) - && dataTypeProvider.BindingDataType != null) - { - typeResolver.TryResolve(dataTypeProvider.BindingDataType, out bindingXDataType); - } - - if (TypedBinding == null) + if (TypedBinding is null) { + Type bindingXDataType = null; + if ((serviceProvider.GetService(typeof(IXamlTypeResolver)) is IXamlTypeResolver typeResolver) + && (serviceProvider.GetService(typeof(IXamlDataTypeProvider)) is IXamlDataTypeProvider dataTypeProvider) + && dataTypeProvider.BindingDataType != null) + { + typeResolver.TryResolve(dataTypeProvider.BindingDataType, out bindingXDataType); + } return new Binding(Path, Mode, Converter, ConverterParameter, StringFormat, Source) { UpdateSourceEventName = UpdateSourceEventName, @@ -37,6 +36,7 @@ BindingBase IMarkupExtension.ProvideValue(IServiceProvider serviceP TargetNullValue = TargetNullValue, DataType = bindingXDataType, }; + } TypedBinding.Mode = Mode; TypedBinding.Converter = Converter; From 3619304618c07675b6080b545bb32dda0baedc48 Mon Sep 17 00:00:00 2001 From: Stephane Delcroix Date: Tue, 16 Jul 2024 09:47:10 +0200 Subject: [PATCH 3/3] send binding diagnostic --- src/Controls/src/Core/Binding.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Controls/src/Core/Binding.cs b/src/Controls/src/Core/Binding.cs index 057807ab7492..5cebcda0b928 100644 --- a/src/Controls/src/Core/Binding.cs +++ b/src/Controls/src/Core/Binding.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using System.Globalization; using System.Reflection; +using Microsoft.Maui.Controls.Xaml.Diagnostics; namespace Microsoft.Maui.Controls { @@ -125,6 +126,7 @@ internal override void Apply(object context, BindableObject bindObj, BindablePro var bindingContext = src ?? Context ?? context; if (DataType != null && bindingContext != null && !DataType.IsAssignableFrom(bindingContext.GetType())) { + BindingDiagnostics.SendBindingFailure(this, "Binding", "Mismatch between the specified x:DataType and the current binding context"); bindingContext = null; }