Browse Source

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

Ported ItemsRepeater element factories from WinUI
pull/4281/head
Steven Kirk 6 years ago
committed by GitHub
parent
commit
f84254c04b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 42
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
  2. 6
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs
  3. 10
      samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs
  4. 29
      src/Avalonia.Controls/Repeater/ElementFactory.cs
  5. 66
      src/Avalonia.Controls/Repeater/IElementFactory.cs
  6. 20
      src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs
  7. 4
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  8. 12
      src/Avalonia.Controls/Repeater/RecyclePool.cs
  9. 119
      src/Avalonia.Controls/Repeater/RecyclingElementFactory.cs
  10. 31
      src/Avalonia.Controls/Repeater/ViewManager.cs

42
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
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)

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

20
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);

4
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();
}

12
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
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!;
}
}
}

31
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;
}
}

Loading…
Cancel
Save