diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml index 08bb1584a9..304782dbf9 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml @@ -1,6 +1,30 @@ + + + + + + + + + + + + + + + + ItemsRepeater @@ -12,8 +36,6 @@ Stack - Horizontal UniformGrid - Vertical UniformGrid - Horizontal - WrapLayout - Horizontal - WrapLayout - Veritcal @@ -25,20 +47,8 @@ - - - - - - - - - - - - + diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs index a4003cadc9..9e898c4536 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs @@ -38,6 +38,12 @@ namespace ControlCatalog.Pages AvaloniaXamlLoader.Load(this); } + public void OnSelectTemplateKey(object sender, SelectTemplateEventArgs e) + { + var item = (ItemsRepeaterPageViewModel.Item)e.DataContext; + e.TemplateKey = (item.Index % 2 == 0) ? "even" : "odd"; + } + private void LayoutChanged(object sender, SelectionChangedEventArgs e) { if (_repeater == null) diff --git a/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs b/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs index 73aaeff994..f893a6e28e 100644 --- a/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs @@ -62,13 +62,9 @@ namespace ControlCatalog.ViewModels public class Item : ReactiveObject { private double _height = double.NaN; - private int _index; - - public Item(int index) - { - _index = index; - } + public Item(int index) => Index = index; + public int Index { get; } public string Text { get; set; } public double Height @@ -76,8 +72,6 @@ namespace ControlCatalog.ViewModels get => _height; set => this.RaiseAndSetIfChanged(ref _height, value); } - - public IBrush Background => ((_index % 2) == 0) ? Brushes.Yellow : Brushes.Wheat; } } } diff --git a/src/Avalonia.Controls/Repeater/ElementFactory.cs b/src/Avalonia.Controls/Repeater/ElementFactory.cs new file mode 100644 index 0000000000..1c1b71af88 --- /dev/null +++ b/src/Avalonia.Controls/Repeater/ElementFactory.cs @@ -0,0 +1,29 @@ +using Avalonia.Controls.Templates; + +namespace Avalonia.Controls +{ + public abstract class ElementFactory : IElementFactory + { + bool IDataTemplate.SupportsRecycling => false; + + public IControl Build(object data) + { + return GetElementCore(new ElementFactoryGetArgs { Data = data }); + } + + public IControl GetElement(ElementFactoryGetArgs args) + { + return GetElementCore(args); + } + + public bool Match(object data) => true; + + public void RecycleElement(ElementFactoryRecycleArgs args) + { + RecycleElementCore(args); + } + + protected abstract IControl GetElementCore(ElementFactoryGetArgs args); + protected abstract void RecycleElementCore(ElementFactoryRecycleArgs args); + } +} diff --git a/src/Avalonia.Controls/Repeater/IElementFactory.cs b/src/Avalonia.Controls/Repeater/IElementFactory.cs new file mode 100644 index 0000000000..6a899a6f26 --- /dev/null +++ b/src/Avalonia.Controls/Repeater/IElementFactory.cs @@ -0,0 +1,66 @@ +using Avalonia.Controls.Templates; + +namespace Avalonia.Controls +{ + /// + /// Represents the optional arguments to use when calling an implementation of the + /// 's method. + /// + public class ElementFactoryGetArgs + { + /// + /// Gets or sets the data item for which an appropriate element tree should be realized + /// when calling . + /// + public object Data { get; set; } + + /// + /// Gets or sets the that is expected to be the parent of the + /// realized element from . + /// + public IControl Parent { get; set; } + + /// + /// Gets or sets the index of the item that should be realized. + /// + public int Index { get; set; } + } + + /// + /// Represents the optional arguments to use when calling an implementation of the + /// 's method. + /// + public class ElementFactoryRecycleArgs + { + /// + /// Gets or sets the to recycle when calling + /// . + /// + public IControl Element { get; set; } + + /// + /// Gets or sets the that is expected to be the parent of the + /// realized element from . + /// + public IControl Parent { get; set; } + } + + /// + /// A data template that supports creating and recyling elements for an . + /// + public interface IElementFactory : IDataTemplate + { + /// + /// Gets an . + /// + /// The element args. + public IControl GetElement(ElementFactoryGetArgs args); + + /// + /// Recycles an that was previously retrieved using + /// . + /// + /// The recycle args. + public void RecycleElement(ElementFactoryRecycleArgs args); + } +} diff --git a/src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs b/src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs index 04d859c742..4b784375a9 100644 --- a/src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs +++ b/src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs @@ -7,13 +7,27 @@ using Avalonia.Controls.Templates; namespace Avalonia.Controls { - internal class ItemTemplateWrapper + internal class ItemTemplateWrapper : IElementFactory { private readonly IDataTemplate _dataTemplate; public ItemTemplateWrapper(IDataTemplate dataTemplate) => _dataTemplate = dataTemplate; - public IControl GetElement(IControl parent, object data) + public bool SupportsRecycling => false; + public IControl Build(object param) => GetElement(null, param); + public bool Match(object data) => _dataTemplate.Match(data); + + public IControl GetElement(ElementFactoryGetArgs args) + { + return GetElement(args.Parent, args.Data); + } + + public void RecycleElement(ElementFactoryRecycleArgs args) + { + RecycleElement(args.Parent, args.Element); + } + + private IControl GetElement(IControl parent, object data) { var selectedTemplate = _dataTemplate; var recyclePool = RecyclePool.GetPoolInstance(selectedTemplate); @@ -37,7 +51,7 @@ namespace Avalonia.Controls return element; } - public void RecycleElement(IControl parent, IControl element) + private void RecycleElement(IControl parent, IControl element) { var selectedTemplate = _dataTemplate; var recyclePool = RecyclePool.GetPoolInstance(selectedTemplate); diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 87f4760156..8bc356bdec 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -141,7 +141,7 @@ namespace Avalonia.Controls /// public ItemsSourceView ItemsSourceView { get; private set; } - internal ItemTemplateWrapper ItemTemplateShim { get; set; } + internal IElementFactory ItemTemplateShim { get; set; } internal Point LayoutOrigin { get; set; } internal object LayoutState { get; set; } internal IControl MadeAnchor => _viewportManager.MadeAnchor; @@ -664,7 +664,7 @@ namespace Avalonia.Controls } } - ItemTemplateShim = new ItemTemplateWrapper(newValue); + ItemTemplateShim = newValue as IElementFactory ?? new ItemTemplateWrapper(newValue); InvalidateMeasure(); } diff --git a/src/Avalonia.Controls/Repeater/RecyclePool.cs b/src/Avalonia.Controls/Repeater/RecyclePool.cs index 4e5950bdc5..28f299043c 100644 --- a/src/Avalonia.Controls/Repeater/RecyclePool.cs +++ b/src/Avalonia.Controls/Repeater/RecyclePool.cs @@ -11,10 +11,13 @@ using Avalonia.Controls.Templates; namespace Avalonia.Controls { - internal class RecyclePool + public class RecyclePool { - public static readonly AttachedProperty OriginTemplateProperty = - AvaloniaProperty.RegisterAttached("OriginTemplate", typeof(RecyclePool)); + internal static readonly AttachedProperty OriginTemplateProperty = + AvaloniaProperty.RegisterAttached("OriginTemplate"); + + internal static readonly AttachedProperty ReuseKeyProperty = + AvaloniaProperty.RegisterAttached("ReuseKey", string.Empty); private static ConditionalWeakTable s_pools = new ConditionalWeakTable(); private readonly Dictionary> _elements = new Dictionary>(); @@ -77,6 +80,9 @@ namespace Avalonia.Controls return null; } + internal string GetReuseKey(IControl element) => element.GetValue(ReuseKeyProperty); + internal void SetReuseKey(IControl element, string value) => element.SetValue(ReuseKeyProperty, value); + private IPanel EnsureOwnerIsPanelOrNull(IControl owner) { if (owner is IPanel panel) diff --git a/src/Avalonia.Controls/Repeater/RecyclingElementFactory.cs b/src/Avalonia.Controls/Repeater/RecyclingElementFactory.cs new file mode 100644 index 0000000000..9503239e34 --- /dev/null +++ b/src/Avalonia.Controls/Repeater/RecyclingElementFactory.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls.Templates; + +#nullable enable + +namespace Avalonia.Controls +{ + public class SelectTemplateEventArgs : EventArgs + { + public string? TemplateKey { get; set; } + public object? DataContext { get; internal set; } + public IControl? Owner { get; internal set; } + } + + public class RecyclingElementFactory : ElementFactory + { + private RecyclePool? _recyclePool; + private IDictionary? _templates; + private SelectTemplateEventArgs? _args; + + public RecyclingElementFactory() + { + Templates = new Dictionary(); + } + + public RecyclePool RecyclePool + { + get => _recyclePool ??= new RecyclePool(); + set => _recyclePool = value ?? throw new ArgumentNullException(nameof(value)); + } + + public IDictionary Templates + { + get => _templates ??= new Dictionary(); + set => _templates = value ?? throw new ArgumentNullException(nameof(value)); + } + + public event EventHandler? SelectTemplateKey; + + protected override IControl GetElementCore(ElementFactoryGetArgs args) + { + if (_templates == null || _templates.Count == 0) + { + throw new InvalidOperationException("Templates cannot be empty."); + } + + var templateKey = Templates.Count == 1 ? + Templates.First().Key : + OnSelectTemplateKeyCore(args.Data, args.Parent); + + if (string.IsNullOrEmpty(templateKey)) + { + // Note: We could allow null/whitespace, which would work as long as + // the recycle pool is not shared. in order to make this work in all cases + // currently we validate that a valid template key is provided. + throw new InvalidOperationException("Template key cannot be null or empty."); + } + + // Get an element from the Recycle Pool or create one + var element = RecyclePool.TryGetElement(templateKey, args.Parent); + + if (element is null) + { + // No need to call HasKey if there is only one template. + if (Templates.Count > 1 && !Templates.ContainsKey(templateKey)) + { + var message = $"No templates of key '{templateKey}' were found in the templates collection."; + throw new InvalidOperationException(message); + } + + var dataTemplate = Templates[templateKey]; + element = dataTemplate.Build(args.Data); + + // Associate ReuseKey with element + RecyclePool.SetReuseKey(element, templateKey); + } + + return element; + } + + protected override void RecycleElementCore(ElementFactoryRecycleArgs args) + { + var element = args.Element; + var key = RecyclePool.GetReuseKey(element); + RecyclePool.PutElement(element, key, args.Parent); + } + + protected virtual string OnSelectTemplateKeyCore(object dataContext, IControl owner) + { + if (SelectTemplateKey is object) + { + _args ??= new SelectTemplateEventArgs(); + _args.TemplateKey = null; + _args.DataContext = dataContext; + _args.Owner = owner; + + try + { + SelectTemplateKey(this, _args); + } + finally + { + _args.DataContext = null; + _args.Owner = null; + } + } + + if (string.IsNullOrEmpty(_args?.TemplateKey)) + { + throw new InvalidOperationException( + "Please provide a valid template identifier in the handler for the SelectTemplateKey event."); + } + + return _args!.TemplateKey!; + } + } +} diff --git a/src/Avalonia.Controls/Repeater/ViewManager.cs b/src/Avalonia.Controls/Repeater/ViewManager.cs index eff51804b9..416b1e2824 100644 --- a/src/Avalonia.Controls/Repeater/ViewManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewManager.cs @@ -6,11 +6,9 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; -using System.Linq; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; -using Avalonia.Layout; using Avalonia.Logging; using Avalonia.VisualTree; @@ -26,6 +24,8 @@ namespace Avalonia.Controls private readonly UniqueIdElementPool _resetPool; private IControl _lastFocusedElement; private bool _isDataSourceStableResetPending; + private ElementFactoryGetArgs _elementFactoryGetArgs; + private ElementFactoryRecycleArgs _elementFactoryRecycleArgs; private int _firstRealizedElementIndexHeldByLayout = FirstRealizedElementIndexDefault; private int _lastRealizedElementIndexHeldByLayout = LastRealizedElementIndexDefault; private bool _eventsSubscribed; @@ -134,7 +134,14 @@ namespace Avalonia.Controls if (_owner.ItemTemplateShim != null) { - _owner.ItemTemplateShim.RecycleElement(_owner, element); + var context = _elementFactoryRecycleArgs ??= new ElementFactoryRecycleArgs(); + context.Element = element; + context.Parent = _owner; + + _owner.ItemTemplateShim.RecycleElement(context); + + context.Element = null; + context.Parent = null; } else { @@ -579,7 +586,7 @@ namespace Avalonia.Controls var data = _owner.ItemsSourceView.GetAt(index); var providedElementFactory = _owner.ItemTemplateShim; - ItemTemplateWrapper GetElementFactory() + IElementFactory GetElementFactory() { if (providedElementFactory == null) { @@ -602,7 +609,20 @@ namespace Avalonia.Controls } var elementFactory = GetElementFactory(); - return elementFactory.GetElement(_owner, data); + var args = _elementFactoryGetArgs ??= new ElementFactoryGetArgs(); + + try + { + args.Data = data; + args.Parent = _owner; + args.Index = index; + return elementFactory.GetElement(args); + } + finally + { + args.Data = null; + args.Parent = null; + } } var element = GetElement(); @@ -732,6 +752,7 @@ namespace Avalonia.Controls { _owner.GotFocus += OnFocusChanged; _owner.LostFocus += OnFocusChanged; + _eventsSubscribed = true; } }