Browse Source

Merge pull request #4218 from AvaloniaUI/feature/itemsrepeater-elementfactory

Ported ItemsRepeater element factories from WinUI
Steven Kirk 5 years ago
parent
commit
f84254c04b

+ 26 - 16
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml

@@ -1,6 +1,30 @@
 <UserControl xmlns="https://github.com/avaloniaui"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              x:Class="ControlCatalog.Pages.ItemsRepeaterPage">
+  <UserControl.Resources>
+    <RecyclePool x:Key="RecyclePool" />
+    <DataTemplate x:Key="odd">
+      <TextBlock Background="Yellow"
+                 Foreground="Black"
+                 Height="{Binding Height}"
+                 Text="{Binding Text}"/>
+    </DataTemplate>
+    <DataTemplate x:Key="even">
+      <TextBlock Background="Wheat"
+                 Foreground="Black"
+                 Height="{Binding Height}"
+                 Text="{Binding Text}"/>
+    </DataTemplate>
+    <RecyclingElementFactory x:Key="elementFactory"
+                             RecyclePool="{StaticResource RecyclePool}"
+                             SelectTemplateKey="OnSelectTemplateKey">
+      <RecyclingElementFactory.Templates>
+        <StaticResource x:Key="odd" ResourceKey="odd" />
+        <StaticResource x:Key="even" ResourceKey="even" />
+      </RecyclingElementFactory.Templates>
+    </RecyclingElementFactory>
+  </UserControl.Resources>
+  
   <DockPanel>
     <StackPanel DockPanel.Dock="Top" Spacing="4" Margin="0 0 0 16">
       <TextBlock Classes="h1">ItemsRepeater</TextBlock>
@@ -12,8 +36,6 @@
         <ComboBoxItem>Stack - Horizontal</ComboBoxItem>
         <ComboBoxItem>UniformGrid - Vertical</ComboBoxItem>
         <ComboBoxItem>UniformGrid - Horizontal</ComboBoxItem>
-        <ComboBoxItem>WrapLayout - Horizontal</ComboBoxItem>
-        <ComboBoxItem>WrapLayout - Veritcal</ComboBoxItem>
       </ComboBox>
       <Button Command="{Binding AddItem}">Add Item</Button>
       <Button Command="{Binding RandomizeHeights}">Randomize Heights</Button>
@@ -25,20 +47,8 @@
       <ScrollViewer Name="scroller"
                     HorizontalScrollBarVisibility="Auto"
                     VerticalScrollBarVisibility="Auto">
-        <ItemsRepeater Name="repeater" Background="Transparent" Items="{Binding Items}">
-          <ItemsRepeater.Styles>
-            <Style Selector="Border:pointerover">
-              <Setter Property="Width" Value="200" />
-            </Style>
-          </ItemsRepeater.Styles>
-          <ItemsRepeater.ItemTemplate>
-            <DataTemplate>
-              <Border Background="Purple" BorderThickness="2" BorderBrush="Green" MinHeight="100" MinWidth="100">
-                <TextBlock Focusable="True" Height="{Binding Height}" Text="{Binding Text}"  Foreground="White" />
-              </Border>
-            </DataTemplate>
-          </ItemsRepeater.ItemTemplate>
-        </ItemsRepeater>
+        <ItemsRepeater Name="repeater" Background="Transparent" Items="{Binding Items}"
+                       ItemTemplate="{StaticResource elementFactory}"/>
       </ScrollViewer>
     </Border>
   </DockPanel>

+ 6 - 0
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)

+ 2 - 8
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;
         }
     }
 }

+ 29 - 0
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);
+    }
+}

+ 66 - 0
src/Avalonia.Controls/Repeater/IElementFactory.cs

@@ -0,0 +1,66 @@
+using Avalonia.Controls.Templates;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Represents the optional arguments to use when calling an implementation of the
+    /// <see cref="IElementFactory"/>'s <see cref="IElementFactory.GetElement"/> method.
+    /// </summary>
+    public class ElementFactoryGetArgs
+    {
+        /// <summary>
+        /// Gets or sets the data item for which an appropriate element tree should be realized
+        /// when calling <see cref="IElementFactory.GetElement"/>.
+        /// </summary>
+        public object Data { get; set; }
+
+        /// <summary>
+        /// Gets or sets the <see cref="IControl"/> that is expected to be the parent of the
+        /// realized element from <see cref="IElementFactory.GetElement"/>.
+        /// </summary>
+        public IControl Parent { get; set; }
+
+        /// <summary>
+        /// Gets or sets the index of the item that should be realized.
+        /// </summary>
+        public int Index { get; set; }
+    }
+
+    /// <summary>
+    /// Represents the optional arguments to use when calling an implementation of the
+    /// <see cref="IElementFactory"/>'s <see cref="IElementFactory.GetElement"/> method.
+    /// </summary>
+    public class ElementFactoryRecycleArgs
+    {
+        /// <summary>
+        /// Gets or sets the <see cref="IControl"/> to recycle when calling 
+        /// <see cref="IElementFactory.RecycleElement"/>.
+        /// </summary>
+        public IControl Element { get; set; }
+
+        /// <summary>
+        /// Gets or sets the <see cref="IControl"/> that is expected to be the parent of the
+        /// realized element from <see cref="IElementFactory.GetElement"/>.
+        /// </summary>
+        public IControl Parent { get; set; }
+    }
+
+    /// <summary>
+    /// A data template that supports creating and recyling elements for an <see cref="ItemsRepeater"/>.
+    /// </summary>
+    public interface IElementFactory : IDataTemplate
+    {
+        /// <summary>
+        /// Gets an <see cref="IControl"/>.
+        /// </summary>
+        /// <param name="args">The element args.</param>
+        public IControl GetElement(ElementFactoryGetArgs args);
+
+        /// <summary>
+        /// Recycles an <see cref="IControl"/> that was previously retrieved using
+        /// <see cref="GetElement"/>.
+        /// </summary>
+        /// <param name="args">The recycle args.</param>
+        public void RecycleElement(ElementFactoryRecycleArgs args);
+    }
+}

+ 17 - 3
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);

+ 2 - 2
src/Avalonia.Controls/Repeater/ItemsRepeater.cs

@@ -141,7 +141,7 @@ namespace Avalonia.Controls
         /// </summary>
         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();
         }

+ 9 - 3
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<IDataTemplate> OriginTemplateProperty =
-            AvaloniaProperty.RegisterAttached<Control, IDataTemplate>("OriginTemplate", typeof(RecyclePool));
+        internal static readonly AttachedProperty<IDataTemplate> OriginTemplateProperty =
+            AvaloniaProperty.RegisterAttached<RecyclePool, Control, IDataTemplate>("OriginTemplate");
+
+        internal static readonly AttachedProperty<string> ReuseKeyProperty =
+            AvaloniaProperty.RegisterAttached<RecyclePool, Control, string>("ReuseKey", string.Empty);
 
         private static ConditionalWeakTable<IDataTemplate, RecyclePool> s_pools = new ConditionalWeakTable<IDataTemplate, RecyclePool>();
         private readonly Dictionary<string, List<ElementInfo>> _elements = new Dictionary<string, List<ElementInfo>>();
@@ -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)

+ 119 - 0
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<string, IDataTemplate>? _templates;
+        private SelectTemplateEventArgs? _args;
+
+        public RecyclingElementFactory()
+        {
+            Templates = new Dictionary<string, IDataTemplate>();
+        }
+
+        public RecyclePool RecyclePool 
+        {
+            get => _recyclePool ??= new RecyclePool();
+            set => _recyclePool = value ?? throw new ArgumentNullException(nameof(value));
+        }
+
+        public IDictionary<string, IDataTemplate> Templates 
+        {
+            get => _templates ??= new Dictionary<string, IDataTemplate>();
+            set => _templates = value ?? throw new ArgumentNullException(nameof(value));
+        }
+
+        public event EventHandler<SelectTemplateEventArgs>? 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!;
+        }
+    }
+}

+ 26 - 5
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;
             }
         }