[C] Fix TemplateBinding for non-logicalchildren (#7506)
authorStephane Delcroix <stephane@delcroix.org>
Thu, 19 Sep 2019 10:33:32 +0000 (12:33 +0200)
committerGitHub <noreply@github.com>
Thu, 19 Sep 2019 10:33:32 +0000 (12:33 +0200)
Spans, e.g., are no LogicalChildren of FormattedString, which is not a
LogicalChildren of Label. The newly introduced way of propagating the
TemplatedParent was top-down, using LogicalChildren, was not assigning
the correct TemplatedParent on Spans.

This reverts to a bottom-up lookup, hooking on ParentSet, like it used to
be.

As a side effect, the TemplateBindings are no longer updated on reparenting,
but this was the original behavior, and I can't think of a case where this
would be needed.

The regression to `{TemplateBinding}` was introduced by #4375.

- fixes #7494

Xamarin.Forms.Core.UnitTests/RelativeSourceBindingTests.cs
Xamarin.Forms.Core/Binding.cs
Xamarin.Forms.Core/BindingExpression.cs
Xamarin.Forms.Core/Element.cs
Xamarin.Forms.Core/FormattedString.cs
Xamarin.Forms.Xaml.UnitTests/Issues/Gh7494.xaml [new file with mode: 0644]
Xamarin.Forms.Xaml.UnitTests/Issues/Gh7494.xaml.cs [new file with mode: 0644]

index 976139a9ab71c51472fc7be762d2a635452a63ca..babb21431c5d68f533b052870d0bb5385d1d959d 100644 (file)
@@ -130,40 +130,6 @@ namespace Xamarin.Forms.Core.UnitTests
                        Assert.AreEqual(label1.Text, stack2.StyleId);
                        Assert.AreEqual(label1.BindingContext, "foobar");
                }
-
-               [Test]
-               public void RelativeSourceTemplatedParentBinding()
-               {
-                       var cc1 = new CustomControl
-                       {
-                               CustomText = "RelativeSource Binding 1!",
-                               ControlTemplate = new ControlTemplate(typeof(MyCustomControlTemplate))                          
-                       };
-                       var cc2 = new CustomControl
-                       {
-                               CustomText = "RelativeSource Binding 2!",
-                               ControlTemplate = new ControlTemplate(typeof(MyCustomControlTemplate))
-                       };
-
-                       var realLabel = cc1.LogicalChildren[0].LogicalChildren[0] as Label;                     
-                       Assert.AreEqual(cc1.CustomText, realLabel.Text);
-                       Assert.AreEqual(realLabel.TemplatedParent, cc1);
-
-                       // Test reparenting
-                       int templatedParentChangeCount = 0;
-                       realLabel.PropertyChanged += (sender, e) =>
-                       {
-                               if (e.PropertyName == nameof(Element.TemplatedParent))
-                                       templatedParentChangeCount++;
-                       };
-                       ((StackLayout)realLabel.Parent).Children.Remove(realLabel);
-                       Assert.IsNull(realLabel.TemplatedParent);
-                       Assert.IsNull(realLabel.Text);
-                       ((StackLayout)cc2.LogicalChildren[0]).Children.Add(realLabel);
-                       Assert.AreEqual(realLabel.TemplatedParent, cc2);
-                       Assert.AreEqual(cc2.CustomText, realLabel.Text);
-                       Assert.AreEqual(2, templatedParentChangeCount);
-               }
        }
 
        public class CustomControl : ContentView
index 86f2e8e36b4125dd4ea8a6312f44aa70af5f5046..f1a1170d8988f352fd950d09cf2e1a2d93e0fdc8 100644 (file)
@@ -83,7 +83,7 @@ namespace Xamarin.Forms
                                ThrowIfApplied();
                                _source = value;
                                if ((value as RelativeBindingSource)?.Mode == RelativeBindingSourceMode.TemplatedParent)
-                                       this.AllowChaining = true;
+                                       AllowChaining = true;
                        }
                }
 
@@ -137,17 +137,18 @@ namespace Xamarin.Forms
                        }
                }
 
-               void ApplyRelativeSourceBinding(
+#pragma warning disable RECS0165 // Asynchronous methods should return a Task instead of void
+               async void ApplyRelativeSourceBinding(
                        BindableObject targetObject, 
                        BindableProperty targetProperty)
+#pragma warning restore RECS0165 // Asynchronous methods should return a Task instead of void
                {
                        if (!(targetObject is Element elem))
                                throw new InvalidOperationException();
                        if (!(Source is RelativeBindingSource relativeSource))
                                return;
 
-                       object resolvedSource = null;                   
-
+                       object resolvedSource;
                        switch (relativeSource.Mode)
                        {
                                case RelativeBindingSourceMode.Self:
@@ -155,8 +156,7 @@ namespace Xamarin.Forms
                                        break;
 
                                case RelativeBindingSourceMode.TemplatedParent:
-                                       _expression.SubscribeToTemplatedParentChanges(elem, targetProperty);
-                                       resolvedSource = elem.TemplatedParent;
+                                       resolvedSource = await TemplateUtilities.FindTemplatedParentAsync(elem);
                                        break;
 
                                case RelativeBindingSourceMode.FindAncestor:
index e1a8093c1a050d0aa63f32e7ccfb448eaa682c1d..5793dc0bccf799a6ccac075753b34530408a64f6 100644 (file)
@@ -16,7 +16,6 @@ namespace Xamarin.Forms
 
                readonly List<BindingExpressionPart> _parts = new List<BindingExpressionPart>();
 
-               bool _trackingTemplatedParent;
                BindableProperty _targetProperty;
                WeakReference<object> _weakSource;
                WeakReference<BindableObject> _weakTarget;
@@ -24,13 +23,8 @@ namespace Xamarin.Forms
 
                internal BindingExpression(BindingBase binding, string path)
                {
-                       if (binding == null)
-                               throw new ArgumentNullException(nameof(binding));
-                       if (path == null)
-                               throw new ArgumentNullException(nameof(path));
-
-                       Binding = binding;
-                       Path = path;
+                       Binding = binding ?? throw new ArgumentNullException(nameof(binding));
+                       Path = path ?? throw new ArgumentNullException(nameof(path));
 
                        ParsePath();
                }
@@ -47,15 +41,13 @@ namespace Xamarin.Forms
                        if (_weakSource == null || _weakTarget == null)
                                return;
 
-                       BindableObject target;
-                       if (!_weakTarget.TryGetTarget(out target))
+                       if (!_weakTarget.TryGetTarget(out BindableObject target))
                        {
                                Unapply();
                                return;
                        }
 
-                       object source;
-                       if (_weakSource.TryGetTarget(out source) && _targetProperty != null)
+                       if (_weakSource.TryGetTarget(out var source) && _targetProperty != null)
                                ApplyCore(source, target, _targetProperty, fromTarget);
                }
 
@@ -66,12 +58,10 @@ namespace Xamarin.Forms
                {
                        _targetProperty = property;
 
-                       BindableObject prevTarget;
-                       if (_weakTarget != null && _weakTarget.TryGetTarget(out prevTarget) && !ReferenceEquals(prevTarget, target))
+                       if (_weakTarget != null && _weakTarget.TryGetTarget(out BindableObject prevTarget) && !ReferenceEquals(prevTarget, target))
                                throw new InvalidOperationException("Binding instances can not be reused");
 
-                       object previousSource;
-                       if (_weakSource != null && _weakSource.TryGetTarget(out previousSource) && !ReferenceEquals(previousSource, sourceObject))
+                       if (_weakSource != null && _weakSource.TryGetTarget(out var previousSource) && !ReferenceEquals(previousSource, sourceObject))
                                throw new InvalidOperationException("Binding instances can not be reused");
 
                        _weakSource = new WeakReference<object>(sourceObject);
@@ -82,29 +72,19 @@ namespace Xamarin.Forms
 
                internal void Unapply()
                {
-                       object sourceObject;
-                       if (_weakSource != null && _weakSource.TryGetTarget(out sourceObject))
+                       if (_weakSource != null && _weakSource.TryGetTarget(out var sourceObject))
                        {
                                for (var i = 0; i < _parts.Count - 1; i++)
                                {
                                        BindingExpressionPart part = _parts[i];
 
                                        if (!part.IsSelf)
-                                       {
                                                part.TryGetValue(sourceObject, out sourceObject);
-                                       }
 
                                        part.Unsubscribe();
                                }
                        }
 
-                       if (_trackingTemplatedParent)
-                       {
-                               BindableObject target = null;
-                               if (_weakTarget?.TryGetTarget(out target) == true && target is Element elem)
-                                       elem.TemplatedParentChanged -= OnTargetTemplatedParentChanged;
-                       }
-
                        _weakSource = null;
                        _weakTarget = null;
 
@@ -124,7 +104,6 @@ namespace Xamarin.Forms
                        bool needsSetter = !needsGetter && ((mode == BindingMode.TwoWay && fromTarget) || mode == BindingMode.OneWayToSource);
 
                        object current = sourceObject;
-                       object previous = null;
                        BindingExpressionPart part = null;
 
                        for (var i = 0; i < _parts.Count; i++)
@@ -154,8 +133,6 @@ namespace Xamarin.Forms
                                if (part.NextPart != null &&   (mode == BindingMode.OneWay || mode == BindingMode.TwoWay)
                                    && current is INotifyPropertyChanged inpc)
                                                part.Subscribe(inpc);
-
-                               previous = current;
                        }
 
                        Debug.Assert(part != null, "There should always be at least the self part in the expression.");
@@ -407,7 +384,6 @@ namespace Xamarin.Forms
                                        }
                                }
 #if !NETSTANDARD1_0
-                               TupleElementNamesAttribute tupleEltNames;
                                if (   property != null
                                        && part.NextPart != null
                                        && property.PropertyType.IsGenericType
@@ -419,7 +395,7 @@ namespace Xamarin.Forms
                                                || property.PropertyType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,>)
                                                || property.PropertyType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,,>)
                                                || property.PropertyType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,,,>))
-                                       && (tupleEltNames = property.GetCustomAttribute(typeof(TupleElementNamesAttribute)) as TupleElementNamesAttribute) != null)
+                                       && property.GetCustomAttribute(typeof(TupleElementNamesAttribute)) is TupleElementNamesAttribute tupleEltNames)
                                {
                                        //modify the nextPart to access the tuple item via the ITuple indexer
                                        var nextPart = part.NextPart;
@@ -435,7 +411,7 @@ namespace Xamarin.Forms
                        }
 
                }
-               static Type[] DecimalTypes = new[] { typeof(float), typeof(decimal), typeof(double) };
+               static readonly Type[] DecimalTypes = { typeof(float), typeof(decimal), typeof(double) };
 
                internal static bool TryConvert(ref object value, BindableProperty targetProperty, Type convertTo, bool toTarget)
                {
@@ -471,22 +447,6 @@ namespace Xamarin.Forms
                        }
                }
 
-               internal void SubscribeToTemplatedParentChanges(Element target, BindableProperty targetProperty)
-               {
-                       _targetProperty = targetProperty;
-                       target.TemplatedParentChanged += OnTargetTemplatedParentChanged;
-                       _trackingTemplatedParent = true;
-               }
-
-               void OnTargetTemplatedParentChanged(object sender, EventArgs e)
-               {
-                       if (!(sender is Element elem) ||
-                               !(this.Binding is Binding binding))
-                               return;
-                       binding.Unapply();
-                       binding.Apply(null, elem, _targetProperty);
-               }
-
                // SubscribeToAncestryChanges, ClearAncestryChangeSubscriptions, FindAncestryIndex, and
                // OnElementParentSet are used with RelativeSource ancestor-type bindings, to detect when
                // there has been an ancestry change requiring re-applying the binding, and to minimize
index ca812b92b3622aa7f03a8a8fbc45fdfa8d716624..b96b0a5fe12a746f6538ce54d686af19251fb69c 100644 (file)
@@ -12,15 +12,8 @@ namespace Xamarin.Forms
        {
                public static readonly BindableProperty MenuProperty = BindableProperty.CreateAttached(nameof(Menu), typeof(Menu), typeof(Element), null);
 
-               public static Menu GetMenu(BindableObject bindable)
-               {
-                       return (Menu)bindable.GetValue(MenuProperty);
-               }
-
-               public static void SetMenu(BindableObject bindable, Menu menu)
-               {
-                       bindable.SetValue(MenuProperty, menu);
-               }
+               public static Menu GetMenu(BindableObject bindable) => (Menu)bindable.GetValue(MenuProperty);
+               public static void SetMenu(BindableObject bindable, Menu menu) => bindable.SetValue(MenuProperty, menu);
 
                internal static readonly ReadOnlyCollection<Element> EmptyChildren = new ReadOnlyCollection<Element>(new Element[0]);
 
@@ -59,8 +52,8 @@ namespace Xamarin.Forms
 
                public string ClassId
                {
-                       get { return (string)GetValue(ClassIdProperty); }
-                       set { SetValue(ClassIdProperty, value); }
+                       get => (string)GetValue(ClassIdProperty);
+                       set => SetValue(ClassIdProperty, value);
                }
 
                public IList<Effect> Effects
@@ -164,10 +157,7 @@ namespace Xamarin.Forms
                [EditorBrowsable(EditorBrowsableState.Never)]
                public Element RealParent { get; private set; }
 
-               Dictionary<BindableProperty, string> DynamicResources
-               {
-                       get { return _dynamicResources ?? (_dynamicResources = new Dictionary<BindableProperty, string>()); }
-               }
+               Dictionary<BindableProperty, string> DynamicResources => _dynamicResources ?? (_dynamicResources = new Dictionary<BindableProperty, string>());
 
                void IElement.AddResourcesChangedListener(Action<object, ResourcesChangedEventArgs> onchanged)
                {
@@ -213,38 +203,6 @@ namespace Xamarin.Forms
                                OnParentSet();
 
                                OnPropertyChanged();
-
-                               RefreshTemplatedParent();
-                       }
-               }
-
-               internal event EventHandler TemplatedParentChanged;
-
-               BindableObject _templatedParent;
-               public BindableObject TemplatedParent
-               {
-                       get => _templatedParent;
-                       private set
-                       {
-                               _templatedParent = value;
-                               TemplatedParentChanged?.Invoke(this, null);
-                               OnPropertyChanged();
-                       }
-               }
-
-               void RefreshTemplatedParent()
-               {
-                       var templatedParent = this.IsTemplateRoot
-                               ? this.Parent
-                               : this.Parent?.TemplatedParent;
-                       if (ReferenceEquals(templatedParent, this.TemplatedParent))
-                               return;
-
-                       this.TemplatedParent = templatedParent;                 
-                       foreach (var element in this.LogicalChildren)
-                       {
-                               if (!element.IsTemplateRoot)
-                                       element.RefreshTemplatedParent();
                        }
                }
 
index 801ac21fedcad81a108afcc0615dd4963e0fed45..657b0239eac97019db357fcf365eb96398714fa9 100644 (file)
@@ -13,10 +13,7 @@ namespace Xamarin.Forms
                readonly SpanCollection _spans = new SpanCollection();
                internal event NotifyCollectionChangedEventHandler SpansCollectionChanged;
 
-               public FormattedString()
-               {
-                       _spans.CollectionChanged += OnCollectionChanged;
-               }
+               public FormattedString() => _spans.CollectionChanged += OnCollectionChanged;
 
                protected override void OnBindingContextChanged()
                {
@@ -25,25 +22,13 @@ namespace Xamarin.Forms
                                SetInheritedBindingContext(Spans[i], BindingContext);                   
                }
 
-               public IList<Span> Spans
-               {
-                       get { return _spans; }
-               }
-                               
-               public static explicit operator string(FormattedString formatted)
-               {
-                       return formatted.ToString();
-               }
+               public IList<Span> Spans => _spans;
 
-               public static implicit operator FormattedString(string text)
-               {
-                       return new FormattedString { Spans = { new Span { Text = text } } };
-               }
+               public static explicit operator string(FormattedString formatted) => formatted.ToString();
 
-               public override string ToString()
-               {
-                       return string.Concat(Spans.Select(span => span.Text));
-               }
+               public static implicit operator FormattedString(string text) => new FormattedString { Spans = { new Span { Text = text } } };
+
+               public override string ToString() => string.Concat(Spans.Select(span => span.Text));
 
                void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
                {
@@ -81,37 +66,18 @@ namespace Xamarin.Forms
                        SpansCollectionChanged?.Invoke(sender, e);
                }
 
-               void OnItemPropertyChanged(object sender, PropertyChangedEventArgs e)
-               {
-                       OnPropertyChanged(nameof(Spans));
-               }
+               void OnItemPropertyChanged(object sender, PropertyChangedEventArgs e) => OnPropertyChanged(nameof(Spans));
 
-               void OnItemPropertyChanging(object sender, PropertyChangingEventArgs e)
-               {
-                       OnPropertyChanging(nameof(Spans));
-               }
+               void OnItemPropertyChanging(object sender, PropertyChangingEventArgs e) => OnPropertyChanging(nameof(Spans));
 
                class SpanCollection : ObservableCollection<Span>
                {
-                       protected override void InsertItem(int index, Span item)
-                       {
-                               if (item == null)
-                                       throw new ArgumentNullException("item");
-
-                               base.InsertItem(index, item);
-                       }
-
-                       protected override void SetItem(int index, Span item)
-                       {
-                               if (item == null)
-                                       throw new ArgumentNullException("item");
-
-                               base.SetItem(index, item);
-                       }
+                       protected override void InsertItem(int index, Span item) => base.InsertItem(index, item ?? throw new ArgumentNullException(nameof(item)));
+                       protected override void SetItem(int index, Span item) => base.SetItem(index, item ?? throw new ArgumentNullException(nameof(item)));
 
                        protected override void ClearItems()
                        {
-                               List<Span> removed = new List<Span>(this);
+                               var removed = new List<Span>(this);
                                base.ClearItems();
                                base.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed));
                        }
diff --git a/Xamarin.Forms.Xaml.UnitTests/Issues/Gh7494.xaml b/Xamarin.Forms.Xaml.UnitTests/Issues/Gh7494.xaml
new file mode 100644 (file)
index 0000000..f50587e
--- /dev/null
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ContentPage
+        xmlns="http://xamarin.com/schemas/2014/forms"
+        xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+        xmlns:local="using:Xamarin.Forms.Xaml.UnitTests"
+        x:Class="Xamarin.Forms.Xaml.UnitTests.Gh7494">
+    <local:Gh7494Content Title="Foo">
+        <local:Gh7494Content.ControlTemplate>
+            <ControlTemplate>
+                <StackLayout>
+                    <Label>
+                        <Label.FormattedText>
+                            <FormattedString>
+                                <Span Text="{TemplateBinding Title}" />
+                            </FormattedString>
+                        </Label.FormattedText>
+                    </Label>
+                    <ContentPresenter/>
+                </StackLayout>
+            </ControlTemplate>
+        </local:Gh7494Content.ControlTemplate>            
+        <Label Text="Content"/>
+    </local:Gh7494Content>
+</ContentPage>
diff --git a/Xamarin.Forms.Xaml.UnitTests/Issues/Gh7494.xaml.cs b/Xamarin.Forms.Xaml.UnitTests/Issues/Gh7494.xaml.cs
new file mode 100644 (file)
index 0000000..46ca4d5
--- /dev/null
@@ -0,0 +1,49 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+using System;
+using System.Collections.Generic;
+using NUnit.Framework;
+using Xamarin.Forms;
+using Xamarin.Forms.Core.UnitTests;
+
+namespace Xamarin.Forms.Xaml.UnitTests
+{
+       public partial class Gh7494 : ContentPage
+       {
+               public Gh7494()
+               {
+                       InitializeComponent();
+               }
+               public Gh7494(bool useCompiledXaml)
+               {
+                       //this stub will be replaced at compile time
+               }
+
+               [TestFixture]
+               class Tests
+               {
+                       [SetUp] public void Setup() => Device.PlatformServices = new MockPlatformServices();
+                       [TearDown] public void TearDown() => Device.PlatformServices = null;
+
+                       [Test]
+                       public void TemplateBindingInSpans([Values(false, true)]bool useCompiledXaml)
+                       {
+                               var layout = new Gh7494(useCompiledXaml);
+                               var view = layout.Content as Gh7494Content;
+                               var templatedLabel = ((StackLayout)view.Children[0]).Children[0] as Label;
+
+                               Assert.That(templatedLabel.FormattedText.Spans[0].Text, Is.EqualTo(view.Title));
+                       }
+               }
+       }
+
+       public class Gh7494Content : ContentView
+       {
+               public static readonly BindableProperty TitleProperty = BindableProperty.Create(nameof(Title), typeof(string), typeof(Gh7494Content), default(string));
+
+               public string Title {
+                       get => (string)GetValue(TitleProperty);
+                       set => SetValue(TitleProperty, value);
+               }
+       }
+}