Browse Source

Recycle DataTemplates.

If a `ContentPresenter`s content is assigned a value which matches the
current `DataTemplate` then just change the
`ContentPresenter.DataContext` and the existing item will update itself.
Steven Kirk 9 years ago
parent
commit
35c4835ae4

+ 2 - 2
src/Avalonia.Base/AvaloniaObject.cs

@@ -26,7 +26,7 @@ namespace Avalonia
         /// <summary>
         /// The parent object that inherited values are inherited from.
         /// </summary>
-        private AvaloniaObject _inheritanceParent;
+        private IAvaloniaObject _inheritanceParent;
 
         /// <summary>
         /// The set values/bindings on this object.
@@ -120,7 +120,7 @@ namespace Avalonia
         /// <value>
         /// The inheritance parent.
         /// </value>
-        protected AvaloniaObject InheritanceParent
+        protected IAvaloniaObject InheritanceParent
         {
             get
             {

+ 1 - 0
src/Avalonia.Controls/Avalonia.Controls.csproj

@@ -58,6 +58,7 @@
     <Compile Include="INameScope.cs" />
     <Compile Include="IPseudoClasses.cs" />
     <Compile Include="DropDownItem.cs" />
+    <Compile Include="ISetInheritanceParent.cs" />
     <Compile Include="ItemVirtualizationMode.cs" />
     <Compile Include="IVirtualizingPanel.cs" />
     <Compile Include="LayoutTransformControl.cs" />

+ 10 - 1
src/Avalonia.Controls/Control.cs

@@ -33,7 +33,7 @@ namespace Avalonia.Controls
     /// - Implements <see cref="IStyleable"/> to allow styling to work on the control.
     /// - Implements <see cref="ILogical"/> to form part of a logical tree.
     /// </remarks>
-    public class Control : InputElement, IControl, INamed, ISetLogicalParent, ISupportInitialize
+    public class Control : InputElement, IControl, INamed, ISetInheritanceParent, ISetLogicalParent, ISupportInitialize
     {
         /// <summary>
         /// Defines the <see cref="DataContext"/> property.
@@ -455,6 +455,15 @@ namespace Avalonia.Controls
             }
         }
 
+        /// <summary>
+        /// Sets the control's inheritance parent.
+        /// </summary>
+        /// <param name="parent">The parent.</param>
+        void ISetInheritanceParent.SetParent(IAvaloniaObject parent)
+        {
+            InheritanceParent = parent;
+        }
+
         /// <summary>
         /// Adds a pseudo-class to be set when a property is true.
         /// </summary>

+ 22 - 0
src/Avalonia.Controls/ISetInheritanceParent.cs

@@ -0,0 +1,22 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Defines an interface through which a <see cref="Control"/>'s inheritance parent can be set.
+    /// </summary>
+    /// <remarks>
+    /// You should not usually need to use this interface - it is for advanced scenarios only.
+    /// Additionally, <see cref="ISetLogicalParent"/> also sets the inheritance parent; this
+    /// interface is only needed where the logical and inheritance parents differ.
+    /// </remarks>
+    public interface ISetInheritanceParent
+    {
+        /// <summary>
+        /// Sets the control's inheritance parent.
+        /// </summary>
+        /// <param name="parent">The parent.</param>
+        void SetParent(IAvaloniaObject parent);
+    }
+}

+ 55 - 15
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@@ -80,6 +80,7 @@ namespace Avalonia.Controls.Presenters
 
         private IControl _child;
         private bool _createdChild;
+        private IDataTemplate _dataTemplate;
 
         /// <summary>
         /// Initializes static members of the <see cref="ContentPresenter"/> class.
@@ -200,6 +201,13 @@ namespace Avalonia.Controls.Presenters
             }
         }
 
+        /// <inheritdoc/>
+        protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+        {
+            base.OnAttachedToVisualTree(e);
+            _dataTemplate = null;
+        }
+
         /// <summary>
         /// Updates the <see cref="Child"/> control based on the control's <see cref="Content"/>.
         /// </summary>
@@ -215,32 +223,64 @@ namespace Avalonia.Controls.Presenters
         {
             var old = Child;
             var content = Content;
-            var result = this.MaterializeDataTemplate(content, ContentTemplate);
+            var result = content as IControl;
 
-            if (old != null)
+            if (result == null)
             {
-                VisualChildren.Remove(old);
+                DataContext = content;
+
+                if (content != null)
+                {
+                    if (old != null && _dataTemplate?.Match(content) == true)
+                    {
+                        result = old;
+                    }
+                    else
+                    {
+                        _dataTemplate = this.FindDataTemplate(content, ContentTemplate) ?? FuncDataTemplate.Default;
+                        result = _dataTemplate.Build(content);
+
+                        var controlResult = result as Control;
+
+                        if (controlResult != null)
+                        {
+                            NameScope.SetNameScope(controlResult, new NameScope());
+                        }
+                    }
+                }
+                else
+                {
+                    _dataTemplate = null;
+                }
+            }
+            else
+            {
+                _dataTemplate = null;
             }
 
-            if (result != null)
+            if (result != old)
             {
-                if (!(content is IControl))
+                if (old != null)
                 {
-                    result.DataContext = content;
+                    VisualChildren.Remove(old);
                 }
 
-                Child = result;
+                if (result != null)
+                {
+                    Child = result;
 
-                if (result.Parent == null)
+                    if (result.Parent == null)
+                    {
+                        ((ISetLogicalParent)result).SetParent((ILogical)this.TemplatedParent ?? this);
+                    }
+
+                    ((ISetInheritanceParent)result).SetParent(this);
+                    VisualChildren.Add(result);
+                }
+                else
                 {
-                    ((ISetLogicalParent)result).SetParent((ILogical)this.TemplatedParent ?? this);
+                    Child = null;
                 }
-
-                VisualChildren.Add(result);
-            }
-            else
-            {
-                Child = null;
             }
 
             _createdChild = true;

+ 0 - 48
src/Avalonia.Controls/Templates/DataTemplateExtensions.cs

@@ -11,54 +11,6 @@ namespace Avalonia.Controls.Templates
     /// </summary>
     public static class DataTemplateExtensions
     {
-        /// <summary>
-        /// Materializes a piece of data based on a data template.
-        /// </summary>
-        /// <param name="control">The control materializing the data template.</param>
-        /// <param name="data">The data.</param>
-        /// <param name="primary">
-        /// An optional primary template that can will be tried before the
-        /// <see cref="IControl.DataTemplates"/> in the tree are searched.
-        /// </param>
-        /// <returns>The data materialized as a control.</returns>
-        public static IControl MaterializeDataTemplate(
-            this IControl control,
-            object data,
-            IDataTemplate primary = null)
-        {
-            if (data == null)
-            {
-                return null;
-            }
-            else
-            {
-                var asControl = data as IControl;
-
-                if (asControl != null)
-                {
-                    return asControl;
-                }
-                else
-                {
-                    IDataTemplate template = control.FindDataTemplate(data, primary);
-                    IControl result;
-
-                    if (template != null)
-                    {
-                        result = template.Build(data);
-                    }
-                    else
-                    {
-                        result = FuncDataTemplate.Default.Build(data);
-                    }
-
-                    NameScope.SetNameScope((Control)result, new NameScope());
-
-                    return result;
-                }
-            }
-        }
-
         /// <summary>
         /// Find a data template that matches a piece of data.
         /// </summary>

+ 18 - 2
src/Avalonia.Controls/Templates/FuncDataTemplate.cs

@@ -2,6 +2,7 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using System.Reactive.Linq;
 using System.Reflection;
 
 namespace Avalonia.Controls.Templates
@@ -12,10 +13,25 @@ namespace Avalonia.Controls.Templates
     public class FuncDataTemplate : FuncTemplate<object, IControl>, IDataTemplate
     {
         /// <summary>
-        /// The default data template used in the case where not matching data template is found.
+        /// The default data template used in the case where no matching data template is found.
         /// </summary>
         public static readonly FuncDataTemplate Default =
-           new FuncDataTemplate(typeof(object), o => (o != null) ? new TextBlock { Text = o.ToString() } : null);
+            new FuncDataTemplate<object>(
+                data =>
+                {
+                    if (data != null)
+                    {
+                        var result = new TextBlock();
+                        result.Bind(
+                            TextBlock.TextProperty,
+                            result.GetObservable(Control.DataContextProperty).Select(x => x?.ToString()));
+                        return result;
+                    }
+                    else
+                    {
+                        return null;
+                    }
+                });
 
         /// <summary>
         /// The implementation of the <see cref="Match"/> method.

+ 1 - 6
src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs

@@ -28,11 +28,6 @@ namespace Avalonia.Markup.Xaml.Templates
             }
         }
 
-        public IControl Build(object data)
-        {
-            var visualTreeForItem = Content.Load();
-            visualTreeForItem.DataContext = data;
-            return visualTreeForItem;
-        }
+        public IControl Build(object data) => Content.Load();
     }
 }

+ 1 - 1
tests/Avalonia.Controls.UnitTests/ControlTests.cs

@@ -262,7 +262,7 @@ namespace Avalonia.Controls.UnitTests
 
         private class TestControl : Control
         {
-            public new AvaloniaObject InheritanceParent => base.InheritanceParent;
+            public new IAvaloniaObject InheritanceParent => base.InheritanceParent;
         }
     }
 }

+ 24 - 0
tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs

@@ -9,6 +9,7 @@ using Avalonia.Controls.Templates;
 using Avalonia.UnitTests;
 using Avalonia.VisualTree;
 using Xunit;
+using System;
 
 namespace Avalonia.Controls.UnitTests.Presenters
 {
@@ -152,6 +153,29 @@ namespace Avalonia.Controls.UnitTests.Presenters
             Assert.Equal("foo", target.DataContext);
         }
 
+        [Fact]
+        public void Tries_To_Recycle_DataTemplate()
+        {
+            var target = new ContentPresenter
+            {
+                DataTemplates = new DataTemplates
+                {
+                    new FuncDataTemplate<string>(_ => new Border()),
+                },
+                Content = "foo",
+            };
+
+            target.UpdateChild();
+            var control = target.Child;
+
+            Assert.IsType<Border>(control);
+
+            target.Content = "bar";
+            target.UpdateChild();
+
+            Assert.Same(control, target.Child);
+        }
+
         private class TestContentControl : ContentControl
         {
             public IControl Child { get; set; }