Browse Source

Merge pull request #8922 from timunie/feature/DisplayMemberBinding

Add DisplayMemberBinding to ItemsControl and derived items
pull/9544/head
Max Katz 3 years ago
committed by GitHub
parent
commit
ee487bf4cf
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      samples/ControlCatalog/Pages/ListBoxPage.xaml
  2. 8
      samples/ControlCatalog/Pages/TabControlPage.xaml
  3. 36
      samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs
  4. 6
      src/Avalonia.Controls/Generators/IItemContainerGenerator.cs
  5. 13
      src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
  6. 12
      src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs
  7. 7
      src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs
  8. 10
      src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs
  9. 19
      src/Avalonia.Controls/ItemsControl.cs
  10. 17
      src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
  11. 17
      src/Avalonia.Controls/TabControl.cs
  12. 20
      tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs

1
samples/ControlCatalog/Pages/ListBoxPage.xaml

@ -32,6 +32,7 @@
</StackPanel> </StackPanel>
<ListBox Items="{Binding Items}" <ListBox Items="{Binding Items}"
Selection="{Binding Selection}" Selection="{Binding Selection}"
DisplayMemberBinding="{Binding (viewModels:ItemModel).ID, StringFormat='{}Item {0:N0}'}"
AutoScrollToSelectedItem="{Binding AutoScrollToSelectedItem}" AutoScrollToSelectedItem="{Binding AutoScrollToSelectedItem}"
SelectionMode="{Binding SelectionMode^}" SelectionMode="{Binding SelectionMode^}"
WrapSelection="{Binding WrapSelection}"/> WrapSelection="{Binding WrapSelection}"/>

8
samples/ControlCatalog/Pages/TabControlPage.xaml

@ -53,14 +53,8 @@
<TabControl <TabControl
Items="{Binding Tabs}" Items="{Binding Tabs}"
Margin="0 16" Margin="0 16"
HeaderDisplayMemberBinding="{Binding Header, x:DataType=viewModels:TabControlPageViewModelItem}"
TabStripPlacement="{Binding TabPlacement}"> TabStripPlacement="{Binding TabPlacement}">
<TabControl.ItemTemplate>
<DataTemplate x:DataType="viewModels:TabControlPageViewModelItem">
<TextBlock
Text="{Binding Header}">
</TextBlock>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate> <TabControl.ContentTemplate>
<DataTemplate x:DataType="viewModels:TabControlPageViewModelItem"> <DataTemplate x:DataType="viewModels:TabControlPageViewModelItem">
<StackPanel Orientation="Vertical" Spacing="8"> <StackPanel Orientation="Vertical" Spacing="8">

36
samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs

@ -4,6 +4,7 @@ using System.Linq;
using System.Reactive; using System.Reactive;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Selection; using Avalonia.Controls.Selection;
using ControlCatalog.Pages;
using MiniMvvm; using MiniMvvm;
namespace ControlCatalog.ViewModels namespace ControlCatalog.ViewModels
@ -20,9 +21,9 @@ namespace ControlCatalog.ViewModels
public ListBoxPageViewModel() public ListBoxPageViewModel()
{ {
Items = new ObservableCollection<string>(Enumerable.Range(1, 10000).Select(i => GenerateItem())); Items = new ObservableCollection<ItemModel>(Enumerable.Range(1, 10000).Select(i => GenerateItem()));
Selection = new SelectionModel<string>(); Selection = new SelectionModel<ItemModel>();
Selection.Select(1); Selection.Select(1);
_selectionMode = this.WhenAnyValue( _selectionMode = this.WhenAnyValue(
@ -58,8 +59,8 @@ namespace ControlCatalog.ViewModels
}); });
} }
public ObservableCollection<string> Items { get; } public ObservableCollection<ItemModel> Items { get; }
public SelectionModel<string> Selection { get; } public SelectionModel<ItemModel> Selection { get; }
public IObservable<SelectionMode> SelectionMode => _selectionMode; public IObservable<SelectionMode> SelectionMode => _selectionMode;
public bool Multiple public bool Multiple
@ -96,6 +97,31 @@ namespace ControlCatalog.ViewModels
public MiniCommand RemoveItemCommand { get; } public MiniCommand RemoveItemCommand { get; }
public MiniCommand SelectRandomItemCommand { get; } public MiniCommand SelectRandomItemCommand { get; }
private string GenerateItem() => $"Item {_counter++.ToString()}"; private ItemModel GenerateItem() => new ItemModel(_counter ++);
}
/// <summary>
/// An Item model for the <see cref="ListBoxPage"/>
/// </summary>
public class ItemModel
{
/// <summary>
/// Creates a new ItemModel with the given ID
/// </summary>
/// <param name="id">The ID to display</param>
public ItemModel(int id)
{
ID = id;
}
/// <summary>
/// The ID of this Item
/// </summary>
public int ID { get; }
public override string ToString()
{
return $"Item {ID}";
}
} }
} }

6
src/Avalonia.Controls/Generators/IItemContainerGenerator.cs

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Styling; using Avalonia.Styling;
namespace Avalonia.Controls.Generators namespace Avalonia.Controls.Generators
@ -24,6 +25,11 @@ namespace Avalonia.Controls.Generators
/// Gets or sets the data template used to display the items in the control. /// Gets or sets the data template used to display the items in the control.
/// </summary> /// </summary>
IDataTemplate? ItemTemplate { get; set; } IDataTemplate? ItemTemplate { get; set; }
/// <summary>
/// Gets or sets the binding to use to bind to the member of an item used for displaying
/// </summary>
IBinding? DisplayMemberBinding { get; set; }
/// <summary> /// <summary>
/// Gets the ContainerType, or null if its an untyped ContainerGenerator. /// Gets the ContainerType, or null if its an untyped ContainerGenerator.

13
src/Avalonia.Controls/Generators/ItemContainerGenerator.cs

@ -45,6 +45,9 @@ namespace Avalonia.Controls.Generators
/// Gets or sets the data template used to display the items in the control. /// Gets or sets the data template used to display the items in the control.
/// </summary> /// </summary>
public IDataTemplate? ItemTemplate { get; set; } public IDataTemplate? ItemTemplate { get; set; }
/// <inheritdoc />
public IBinding? DisplayMemberBinding { get; set; }
/// <summary> /// <summary>
/// Gets the owner control. /// Gets the owner control.
@ -189,7 +192,15 @@ namespace Avalonia.Controls.Generators
if (result == null) if (result == null)
{ {
result = new ContentPresenter(); result = new ContentPresenter();
result.SetValue(ContentPresenter.ContentProperty, item, BindingPriority.Style); if (DisplayMemberBinding is not null)
{
result.SetValue(StyledElement.DataContextProperty, item, BindingPriority.Style);
result.Bind(ContentPresenter.ContentProperty, DisplayMemberBinding, BindingPriority.Style);
}
else
{
result.SetValue(ContentPresenter.ContentProperty, item, BindingPriority.Style);
}
if (ItemTemplate != null) if (ItemTemplate != null)
{ {

12
src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs

@ -53,8 +53,16 @@ namespace Avalonia.Controls.Generators
container.SetValue(ContentTemplateProperty, ItemTemplate, BindingPriority.Style); container.SetValue(ContentTemplateProperty, ItemTemplate, BindingPriority.Style);
} }
container.SetValue(ContentProperty, item, BindingPriority.Style); if (DisplayMemberBinding is not null)
{
container.SetValue(StyledElement.DataContextProperty, item, BindingPriority.Style);
container.Bind(ContentProperty, DisplayMemberBinding, BindingPriority.Style);
}
else
{
container.SetValue(ContentProperty, item, BindingPriority.Style);
}
if (!(item is IControl)) if (!(item is IControl))
{ {
container.DataContext = item; container.DataContext = item;

7
src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Reactive; using Avalonia.Reactive;
using Avalonia.VisualTree; using Avalonia.VisualTree;
@ -33,6 +34,12 @@ namespace Avalonia.Controls.Generators
TabControl.ItemTemplateProperty)); TabControl.ItemTemplateProperty));
} }
if (Owner.HeaderDisplayMemberBinding is not null)
{
tabItem.Bind(HeaderedContentControl.HeaderProperty, Owner.HeaderDisplayMemberBinding,
BindingPriority.Style);
}
if (tabItem.Header == null) if (tabItem.Header == null)
{ {
if (item is IHeadered headered) if (item is IHeadered headered)

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

@ -76,7 +76,15 @@ namespace Avalonia.Controls.Generators
result.SetValue(Control.ThemeProperty, ItemContainerTheme, BindingPriority.Style); result.SetValue(Control.ThemeProperty, ItemContainerTheme, BindingPriority.Style);
} }
result.SetValue(ContentProperty, template.Build(item), BindingPriority.Style); if (DisplayMemberBinding is not null)
{
result.SetValue(StyledElement.DataContextProperty, item, BindingPriority.Style);
result.Bind(ContentProperty, DisplayMemberBinding, BindingPriority.Style);
}
else
{
result.SetValue(ContentProperty, template.Build(item), BindingPriority.Style);
}
var itemsSelector = template.ItemsSelector(item); var itemsSelector = template.ItemsSelector(item);

19
src/Avalonia.Controls/ItemsControl.cs

@ -11,6 +11,7 @@ using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Controls.Utils; using Avalonia.Controls.Utils;
using Avalonia.Data;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Metadata; using Avalonia.Metadata;
@ -61,6 +62,23 @@ namespace Avalonia.Controls
public static readonly StyledProperty<IDataTemplate?> ItemTemplateProperty = public static readonly StyledProperty<IDataTemplate?> ItemTemplateProperty =
AvaloniaProperty.Register<ItemsControl, IDataTemplate?>(nameof(ItemTemplate)); AvaloniaProperty.Register<ItemsControl, IDataTemplate?>(nameof(ItemTemplate));
/// <summary>
/// Defines the <see cref="DisplayMemberBinding" /> property
/// </summary>
public static readonly StyledProperty<IBinding?> DisplayMemberBindingProperty =
AvaloniaProperty.Register<ItemsControl, IBinding?>(nameof(DisplayMemberBinding));
/// <summary>
/// Gets or sets the <see cref="IBinding"/> to use for binding to the display member of each item.
/// </summary>
[AssignBinding]
public IBinding? DisplayMemberBinding
{
get { return GetValue(DisplayMemberBindingProperty); }
set { SetValue(DisplayMemberBindingProperty, value); }
}
private IEnumerable? _items = new AvaloniaList<object>(); private IEnumerable? _items = new AvaloniaList<object>();
private int _itemCount; private int _itemCount;
private IItemContainerGenerator? _itemContainerGenerator; private IItemContainerGenerator? _itemContainerGenerator;
@ -97,6 +115,7 @@ namespace Avalonia.Controls
_itemContainerGenerator.ItemContainerTheme = ItemContainerTheme; _itemContainerGenerator.ItemContainerTheme = ItemContainerTheme;
_itemContainerGenerator.ItemTemplate = ItemTemplate; _itemContainerGenerator.ItemTemplate = ItemTemplate;
_itemContainerGenerator.DisplayMemberBinding = DisplayMemberBinding;
_itemContainerGenerator.Materialized += (_, e) => OnContainersMaterialized(e); _itemContainerGenerator.Materialized += (_, e) => OnContainersMaterialized(e);
_itemContainerGenerator.Dematerialized += (_, e) => OnContainersDematerialized(e); _itemContainerGenerator.Dematerialized += (_, e) => OnContainersDematerialized(e);
_itemContainerGenerator.Recycled += (_, e) => OnContainersRecycled(e); _itemContainerGenerator.Recycled += (_, e) => OnContainersRecycled(e);

17
src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs

@ -5,6 +5,7 @@ using Avalonia.Collections;
using Avalonia.Controls.Generators; using Avalonia.Controls.Generators;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Controls.Utils; using Avalonia.Controls.Utils;
using Avalonia.Data;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Styling; using Avalonia.Styling;
@ -33,6 +34,12 @@ namespace Avalonia.Controls.Presenters
public static readonly StyledProperty<IDataTemplate?> ItemTemplateProperty = public static readonly StyledProperty<IDataTemplate?> ItemTemplateProperty =
ItemsControl.ItemTemplateProperty.AddOwner<ItemsPresenterBase>(); ItemsControl.ItemTemplateProperty.AddOwner<ItemsPresenterBase>();
/// <summary>
/// Defines the <see cref="DisplayMemberBinding" /> property
/// </summary>
public static readonly StyledProperty<IBinding?> DisplayMemberBindingProperty =
ItemsControl.DisplayMemberBindingProperty.AddOwner<ItemsPresenterBase>();
private IEnumerable? _items; private IEnumerable? _items;
private IDisposable? _itemsSubscription; private IDisposable? _itemsSubscription;
private bool _createdPanel; private bool _createdPanel;
@ -120,6 +127,15 @@ namespace Avalonia.Controls.Presenters
set { SetValue(ItemTemplateProperty, value); } set { SetValue(ItemTemplateProperty, value); }
} }
/// <summary>
/// Gets or sets the <see cref="IBinding"/> to use for binding to the display member of each item.
/// </summary>
public IBinding? DisplayMemberBinding
{
get { return GetValue(DisplayMemberBindingProperty); }
set { SetValue(DisplayMemberBindingProperty, value); }
}
/// <summary> /// <summary>
/// Gets the panel used to display the items. /// Gets the panel used to display the items.
/// </summary> /// </summary>
@ -177,6 +193,7 @@ namespace Avalonia.Controls.Presenters
{ {
result = new ItemContainerGenerator(this); result = new ItemContainerGenerator(this);
result.ItemTemplate = ItemTemplate; result.ItemTemplate = ItemTemplate;
result.DisplayMemberBinding = DisplayMemberBinding;
} }
result.Materialized += ContainerActionHandler; result.Materialized += ContainerActionHandler;

17
src/Avalonia.Controls/TabControl.cs

@ -12,6 +12,7 @@ using Avalonia.LogicalTree;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using Avalonia.Automation; using Avalonia.Automation;
using Avalonia.Controls.Metadata; using Avalonia.Controls.Metadata;
using Avalonia.Data;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -57,6 +58,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<IDataTemplate?> SelectedContentTemplateProperty = public static readonly StyledProperty<IDataTemplate?> SelectedContentTemplateProperty =
AvaloniaProperty.Register<TabControl, IDataTemplate?>(nameof(SelectedContentTemplate)); AvaloniaProperty.Register<TabControl, IDataTemplate?>(nameof(SelectedContentTemplate));
/// <summary>
/// Defines the <see cref="HeaderDisplayMemberBinding" /> property
/// </summary>
public static readonly StyledProperty<IBinding?> HeaderDisplayMemberBindingProperty =
AvaloniaProperty.Register<HeaderedItemsControl, IBinding?>(nameof(HeaderDisplayMemberBinding));
/// <summary> /// <summary>
/// The default value for the <see cref="ItemsControl.ItemsPanel"/> property. /// The default value for the <see cref="ItemsControl.ItemsPanel"/> property.
/// </summary> /// </summary>
@ -134,6 +141,16 @@ namespace Avalonia.Controls
get { return GetValue(SelectedContentTemplateProperty); } get { return GetValue(SelectedContentTemplateProperty); }
internal set { SetValue(SelectedContentTemplateProperty, value); } internal set { SetValue(SelectedContentTemplateProperty, value); }
} }
/// <summary>
/// Gets or sets the <see cref="IBinding"/> to use for binding to the display member of each tab-items header.
/// </summary>
[AssignBinding]
public IBinding? HeaderDisplayMemberBinding
{
get { return GetValue(HeaderDisplayMemberBindingProperty); }
set { SetValue(HeaderDisplayMemberBindingProperty, value); }
}
internal ItemsPresenter? ItemsPresenterPart { get; private set; } internal ItemsPresenter? ItemsPresenterPart { get; private set; }

20
tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs

@ -4,6 +4,7 @@ using System.Linq;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Styling; using Avalonia.Styling;
@ -736,6 +737,25 @@ namespace Avalonia.Controls.UnitTests
root.Child = null; root.Child = null;
root.Child = target; root.Child = target;
} }
[Fact]
public void Should_Use_DisplayMemberBinding()
{
var target = new ItemsControl
{
Template = GetTemplate(),
DisplayMemberBinding = new Binding("Length")
};
target.Items = new[] { "Foo" };
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
var container = (ContentPresenter)target.Presenter.Panel.Children[0];
container.UpdateChild();
Assert.Equal(container.Child!.GetValue(TextBlock.TextProperty), "3");
}
private class Item private class Item
{ {

Loading…
Cancel
Save