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.
pull/4271/head
Steven Kirk 6 years ago
parent
commit
a8b7e87938
  1. 1
      src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs
  2. 21
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  3. 2
      src/Avalonia.Controls/Repeater/ElementFactory.cs
  4. 1
      src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs
  5. 29
      src/Avalonia.Controls/Templates/FuncDataTemplate.cs
  6. 6
      src/Avalonia.Controls/Templates/FuncTemplate`2.cs
  7. 12
      src/Avalonia.Controls/Templates/IDataTemplate.cs
  8. 25
      src/Avalonia.Controls/Templates/IRecyclingDataTemplate.cs
  9. 2
      src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs
  10. 11
      src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs
  11. 2
      src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs
  12. 2
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

1
src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs

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

21
src/Avalonia.Controls/Presenters/ContentPresenter.cs

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

2
src/Avalonia.Controls/Repeater/ElementFactory.cs

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

1
src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs

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

29
src/Avalonia.Controls/Templates/FuncDataTemplate.cs

@ -6,7 +6,7 @@ namespace Avalonia.Controls.Templates
/// <summary> /// <summary>
/// Builds a control for a piece of data. /// Builds a control for a piece of data.
/// </summary> /// </summary>
public class FuncDataTemplate : FuncTemplate<object, IControl>, IDataTemplate public class FuncDataTemplate : FuncTemplate<object, IControl>, IRecyclingDataTemplate
{ {
/// <summary> /// <summary>
/// The default data template used in the case where no matching data template is found. /// The default data template used in the case where no matching data template is found.
@ -30,10 +30,8 @@ namespace Avalonia.Controls.Templates
}, },
true); true);
/// <summary>
/// The implementation of the <see cref="Match"/> method.
/// </summary>
private readonly Func<object, bool> _match; private readonly Func<object, bool> _match;
private readonly bool _supportsRecycling;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="FuncDataTemplate"/> class. /// Initializes a new instance of the <see cref="FuncDataTemplate"/> class.
@ -70,12 +68,9 @@ namespace Avalonia.Controls.Templates
Contract.Requires<ArgumentNullException>(match != null); Contract.Requires<ArgumentNullException>(match != null);
_match = match; _match = match;
SupportsRecycling = supportsRecycling; _supportsRecycling = supportsRecycling;
} }
/// <inheritdoc/>
public bool SupportsRecycling { get; }
/// <summary> /// <summary>
/// Checks to see if this data template matches the specified data. /// Checks to see if this data template matches the specified data.
/// </summary> /// </summary>
@ -88,6 +83,24 @@ namespace Avalonia.Controls.Templates
return _match(data); 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> /// <summary>
/// Determines of an object is of the specified type. /// Determines of an object is of the specified type.
/// </summary> /// </summary>

6
src/Avalonia.Controls/Templates/FuncTemplate`2.cs

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

12
src/Avalonia.Controls/Templates/IDataTemplate.cs

@ -1,3 +1,7 @@
using System;
#nullable enable
namespace Avalonia.Controls.Templates namespace Avalonia.Controls.Templates
{ {
/// <summary> /// <summary>
@ -5,12 +9,6 @@ namespace Avalonia.Controls.Templates
/// </summary> /// </summary>
public interface IDataTemplate : ITemplate<object, IControl> 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> /// <summary>
/// Checks to see if this data template matches the specified data. /// Checks to see if this data template matches the specified data.
/// </summary> /// </summary>
@ -20,4 +18,4 @@ namespace Avalonia.Controls.Templates
/// </returns> /// </returns>
bool Match(object data); bool Match(object data);
} }
} }

25
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);
}
}

2
src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs

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

11
src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs

@ -5,7 +5,7 @@ using Avalonia.Metadata;
namespace Avalonia.Markup.Xaml.Templates namespace Avalonia.Markup.Xaml.Templates
{ {
public class DataTemplate : IDataTemplate public class DataTemplate : IRecyclingDataTemplate
{ {
public Type DataType { get; set; } public Type DataType { get; set; }
@ -14,8 +14,6 @@ namespace Avalonia.Markup.Xaml.Templates
[TemplateContent] [TemplateContent]
public object Content { get; set; } public object Content { get; set; }
public bool SupportsRecycling { get; set; } = true;
public bool Match(object data) public bool Match(object data)
{ {
if (DataType == null) 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;
}
} }
} }

2
src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs

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

2
tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

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

Loading…
Cancel
Save