Browse Source

Added IRecyclingDataTemplate.

In #4218 we imported `IElementFactory` from WinUI which is broadly analogous to a recycling datatemplate for lists. In Avalonia this implement `IDataTemplate` in order to have a common base class for all types of data templates.

The problem with this is that `IDataTemplate` already had a `SupportsRecycling` property which is incompatible with the way recycling is implemented in `IElementFactory`. Instead, introduce an `IRecyclingDataTemplate` to signal data templates that support recycling.
Steven Kirk 5 years ago
parent
commit
a8b7e87938

+ 0 - 1
src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs

@@ -142,7 +142,6 @@ namespace Avalonia.Controls.Generators
             private readonly IDataTemplate _inner;
             public WrapperTreeDataTemplate(IDataTemplate inner) => _inner = inner;
             public IControl Build(object param) => _inner.Build(param);
-            public bool SupportsRecycling => _inner.SupportsRecycling;
             public bool Match(object data) => _inner.Match(data);
             public InstancedBinding ItemsSelector(object item) => null;
         }

+ 10 - 11
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@@ -86,7 +86,7 @@ namespace Avalonia.Controls.Presenters
 
         private IControl _child;
         private bool _createdChild;
-        private IDataTemplate _dataTemplate;
+        private IRecyclingDataTemplate _recyclingDataTemplate;
         private readonly BorderRenderHelper _borderRenderer = new BorderRenderHelper();
 
         /// <summary>
@@ -281,7 +281,7 @@ namespace Avalonia.Controls.Presenters
         protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
         {
             base.OnAttachedToLogicalTree(e);
-            _dataTemplate = null;
+            _recyclingDataTemplate = null;
             _createdChild = false;
             InvalidateMeasure();
         }
@@ -307,22 +307,21 @@ namespace Avalonia.Controls.Presenters
             {
                 var dataTemplate = this.FindDataTemplate(content, ContentTemplate) ?? FuncDataTemplate.Default;
 
-                // We have content and it isn't a control, so if the new data template is the same
-                // as the old data template, try to recycle the existing child control to display
-                // the new data.
-                if (dataTemplate == _dataTemplate && dataTemplate.SupportsRecycling)
+                if (dataTemplate is IRecyclingDataTemplate rdt)
                 {
-                    newChild = oldChild;
+                    var toRecycle = rdt == _recyclingDataTemplate ? oldChild : null;
+                    newChild = rdt.Build(content, toRecycle);
+                    _recyclingDataTemplate = rdt;
                 }
                 else
                 {
-                    _dataTemplate = dataTemplate;
-                    newChild = _dataTemplate.Build(content);
+                    newChild = dataTemplate.Build(content);
+                    _recyclingDataTemplate = null;
                 }
             }
             else
             {
-                _dataTemplate = null;
+                _recyclingDataTemplate = null;
             }
 
             return newChild;
@@ -422,7 +421,7 @@ namespace Avalonia.Controls.Presenters
                 LogicalChildren.Remove(Child);
                 ((ISetInheritanceParent)Child).SetParent(Child.Parent);
                 Child = null;
-                _dataTemplate = null;
+                _recyclingDataTemplate = null;
             }
 
             InvalidateMeasure();

+ 0 - 2
src/Avalonia.Controls/Repeater/ElementFactory.cs

@@ -4,8 +4,6 @@ namespace Avalonia.Controls
 {
     public abstract class ElementFactory : IElementFactory
     {
-        bool IDataTemplate.SupportsRecycling => false;
-
         public IControl Build(object data)
         {
             return GetElementCore(new ElementFactoryGetArgs { Data = data });

+ 0 - 1
src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs

@@ -13,7 +13,6 @@ namespace Avalonia.Controls
 
         public ItemTemplateWrapper(IDataTemplate dataTemplate) => _dataTemplate = dataTemplate;
 
-        public bool SupportsRecycling => false;
         public IControl Build(object param) => GetElement(null, param);
         public bool Match(object data) => _dataTemplate.Match(data);
 

+ 21 - 8
src/Avalonia.Controls/Templates/FuncDataTemplate.cs

@@ -6,7 +6,7 @@ namespace Avalonia.Controls.Templates
     /// <summary>
     /// Builds a control for a piece of data.
     /// </summary>
-    public class FuncDataTemplate : FuncTemplate<object, IControl>, IDataTemplate
+    public class FuncDataTemplate : FuncTemplate<object, IControl>, IRecyclingDataTemplate
     {
         /// <summary>
         /// The default data template used in the case where no matching data template is found.
@@ -30,10 +30,8 @@ namespace Avalonia.Controls.Templates
                 },
                 true);
 
-        /// <summary>
-        /// The implementation of the <see cref="Match"/> method.
-        /// </summary>
         private readonly Func<object, bool> _match;
+        private readonly bool _supportsRecycling;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="FuncDataTemplate"/> class.
@@ -70,12 +68,9 @@ namespace Avalonia.Controls.Templates
             Contract.Requires<ArgumentNullException>(match != null);
 
             _match = match;
-            SupportsRecycling = supportsRecycling;
+            _supportsRecycling = supportsRecycling;
         }
 
-        /// <inheritdoc/>
-        public bool SupportsRecycling { get; }
-
         /// <summary>
         /// Checks to see if this data template matches the specified data.
         /// </summary>
@@ -88,6 +83,24 @@ namespace Avalonia.Controls.Templates
             return _match(data);
         }
 
+        /// <summary>
+        /// Creates or recycles a control to display the specified data.
+        /// </summary>
+        /// <param name="data">The data to display.</param>
+        /// <param name="existing">An optional control to recycle.</param>
+        /// <returns>
+        /// The <paramref name="existing"/> control if supplied and applicable to
+        /// <paramref name="data"/>, otherwise a new control.
+        /// </returns>
+        /// <remarks>
+        /// The caller should ensure that any control passed to <paramref name="existing"/>
+        /// originated from the same data template.
+        /// </remarks>
+        public IControl Build(object data, IControl existing)
+        {
+            return _supportsRecycling && existing is object ? existing : Build(data);
+        }
+
         /// <summary>
         /// Determines of an object is of the specified type.
         /// </summary>

+ 3 - 3
src/Avalonia.Controls/Templates/FuncTemplate`2.cs

@@ -1,5 +1,7 @@
 using System;
 
+#nullable enable
+
 namespace Avalonia.Controls.Templates
 {
     /// <summary>
@@ -18,9 +20,7 @@ namespace Avalonia.Controls.Templates
         /// <param name="func">The function used to create the control.</param>
         public FuncTemplate(Func<TParam, INameScope, TControl> func)
         {
-            Contract.Requires<ArgumentNullException>(func != null);
-
-            _func = func;
+            _func = func ?? throw new ArgumentNullException(nameof(func));
         }
 
         /// <summary>

+ 5 - 7
src/Avalonia.Controls/Templates/IDataTemplate.cs

@@ -1,3 +1,7 @@
+using System;
+
+#nullable enable
+
 namespace Avalonia.Controls.Templates
 {
     /// <summary>
@@ -5,12 +9,6 @@ namespace Avalonia.Controls.Templates
     /// </summary>
     public interface IDataTemplate : ITemplate<object, IControl>
     {
-        /// <summary>
-        /// Gets a value indicating whether the data template supports recycling of the generated
-        /// control.
-        /// </summary>
-        bool SupportsRecycling { get; }
-
         /// <summary>
         /// Checks to see if this data template matches the specified data.
         /// </summary>
@@ -20,4 +18,4 @@ namespace Avalonia.Controls.Templates
         /// </returns>
         bool Match(object data);
     }
-}
+}

+ 25 - 0
src/Avalonia.Controls/Templates/IRecyclingDataTemplate.cs

@@ -0,0 +1,25 @@
+#nullable enable
+
+namespace Avalonia.Controls.Templates
+{
+    /// <summary>
+    /// An <see cref="IDataTemplate"/> that supports recycling existing elements.
+    /// </summary>
+    public interface IRecyclingDataTemplate : IDataTemplate
+    {
+        /// <summary>
+        /// Creates or recycles a control to display the specified data.
+        /// </summary>
+        /// <param name="data">The data to display.</param>
+        /// <param name="existing">An optional control to recycle.</param>
+        /// <returns>
+        /// The <paramref name="existing"/> control if supplied and applicable to
+        /// <paramref name="data"/>, otherwise a new control.
+        /// </returns>
+        /// <remarks>
+        /// The caller should ensure that any control passed to <paramref name="existing"/>
+        /// originated from the same data template.
+        /// </remarks>
+        IControl Build(object data, IControl? existing);
+    }
+}

+ 0 - 2
src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs

@@ -7,8 +7,6 @@ namespace Avalonia.Diagnostics
 {
     internal class ViewLocator : IDataTemplate
     {
-        public bool SupportsRecycling => false;
-
         public IControl Build(object data)
         {
             var name = data.GetType().FullName.Replace("ViewModel", "View");

+ 7 - 4
src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs

@@ -5,7 +5,7 @@ using Avalonia.Metadata;
 
 namespace Avalonia.Markup.Xaml.Templates
 {
-    public class DataTemplate : IDataTemplate
+    public class DataTemplate : IRecyclingDataTemplate
     {
         public Type DataType { get; set; }
 
@@ -14,8 +14,6 @@ namespace Avalonia.Markup.Xaml.Templates
         [TemplateContent]
         public object Content { get; set; }
 
-        public bool SupportsRecycling { get; set; } = true;
-
         public bool Match(object data)
         {
             if (DataType == null)
@@ -28,6 +26,11 @@ namespace Avalonia.Markup.Xaml.Templates
             }
         }
 
-        public IControl Build(object data) => TemplateContent.Load(Content).Control;
+        public IControl Build(object data) => Build(data, null);
+
+        public IControl Build(object data, IControl existing)
+        {
+            return existing ?? TemplateContent.Load(Content).Control;
+        }
     }
 }

+ 0 - 2
src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs

@@ -18,8 +18,6 @@ namespace Avalonia.Markup.Xaml.Templates
         [AssignBinding]
         public Binding ItemsSource { get; set; }
 
-        public bool SupportsRecycling { get; set; } = true;
-
         public bool Match(object data)
         {
             if (DataType == null)

+ 0 - 2
tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

@@ -1273,8 +1273,6 @@ namespace Avalonia.Controls.UnitTests
                 return new TextBlock { Text = node.Value };
             }
 
-            public bool SupportsRecycling => false;
-
             public InstancedBinding ItemsSelector(object item)
             {
                 var obs = ExpressionObserver.Create(item, o => (o as Node).Children);