Browse Source

Merge pull request #8922 from timunie/feature/DisplayMemberBinding

Add DisplayMemberBinding to ItemsControl and derived items
Max Katz 2 years ago
parent
commit
ee487bf4cf

+ 1 - 0
samples/ControlCatalog/Pages/ListBoxPage.xaml

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

+ 1 - 7
samples/ControlCatalog/Pages/TabControlPage.xaml

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

+ 31 - 5
samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs

@@ -4,6 +4,7 @@ using System.Linq;
 using System.Reactive;
 using Avalonia.Controls;
 using Avalonia.Controls.Selection;
+using ControlCatalog.Pages;
 using MiniMvvm;
 
 namespace ControlCatalog.ViewModels
@@ -20,9 +21,9 @@ namespace ControlCatalog.ViewModels
 
         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);
 
             _selectionMode = this.WhenAnyValue(
@@ -58,8 +59,8 @@ namespace ControlCatalog.ViewModels
             });
         }
 
-        public ObservableCollection<string> Items { get; }
-        public SelectionModel<string> Selection { get; }
+        public ObservableCollection<ItemModel> Items { get; }
+        public SelectionModel<ItemModel> Selection { get; }
         public IObservable<SelectionMode> SelectionMode => _selectionMode;
 
         public bool Multiple
@@ -96,6 +97,31 @@ namespace ControlCatalog.ViewModels
         public MiniCommand RemoveItemCommand { 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 - 0
src/Avalonia.Controls/Generators/IItemContainerGenerator.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using Avalonia.Controls.Templates;
+using Avalonia.Data;
 using Avalonia.Styling;
 
 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.
         /// </summary>
         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>
         /// Gets the ContainerType, or null if its an untyped ContainerGenerator.

+ 12 - 1
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.
         /// </summary>
         public IDataTemplate? ItemTemplate { get; set; }
+        
+        /// <inheritdoc />
+        public IBinding? DisplayMemberBinding { get; set; }
 
         /// <summary>
         /// Gets the owner control.
@@ -189,7 +192,15 @@ namespace Avalonia.Controls.Generators
             if (result == null)
             {
                 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)
                 {

+ 10 - 2
src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs

@@ -53,8 +53,16 @@ namespace Avalonia.Controls.Generators
                     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))
                 {
                     container.DataContext = item;

+ 7 - 0
src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs

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

+ 9 - 1
src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs

@@ -76,7 +76,15 @@ namespace Avalonia.Controls.Generators
                     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);
 

+ 19 - 0
src/Avalonia.Controls/ItemsControl.cs

@@ -11,6 +11,7 @@ using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
 using Avalonia.Controls.Utils;
+using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.LogicalTree;
 using Avalonia.Metadata;
@@ -61,6 +62,23 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<IDataTemplate?> ItemTemplateProperty =
             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 int _itemCount;
         private IItemContainerGenerator? _itemContainerGenerator;
@@ -97,6 +115,7 @@ namespace Avalonia.Controls
 
                     _itemContainerGenerator.ItemContainerTheme = ItemContainerTheme;
                     _itemContainerGenerator.ItemTemplate = ItemTemplate;
+                    _itemContainerGenerator.DisplayMemberBinding = DisplayMemberBinding;
                     _itemContainerGenerator.Materialized += (_, e) => OnContainersMaterialized(e);
                     _itemContainerGenerator.Dematerialized += (_, e) => OnContainersDematerialized(e);
                     _itemContainerGenerator.Recycled += (_, e) => OnContainersRecycled(e);

+ 17 - 0
src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs

@@ -5,6 +5,7 @@ using Avalonia.Collections;
 using Avalonia.Controls.Generators;
 using Avalonia.Controls.Templates;
 using Avalonia.Controls.Utils;
+using Avalonia.Data;
 using Avalonia.LogicalTree;
 using Avalonia.Styling;
 
@@ -33,6 +34,12 @@ namespace Avalonia.Controls.Presenters
         public static readonly StyledProperty<IDataTemplate?> ItemTemplateProperty =
             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 IDisposable? _itemsSubscription;
         private bool _createdPanel;
@@ -120,6 +127,15 @@ namespace Avalonia.Controls.Presenters
             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>
         /// Gets the panel used to display the items.
         /// </summary>
@@ -177,6 +193,7 @@ namespace Avalonia.Controls.Presenters
             {
                 result = new ItemContainerGenerator(this);
                 result.ItemTemplate = ItemTemplate;
+                result.DisplayMemberBinding = DisplayMemberBinding;
             }
 
             result.Materialized += ContainerActionHandler;

+ 17 - 0
src/Avalonia.Controls/TabControl.cs

@@ -12,6 +12,7 @@ using Avalonia.LogicalTree;
 using Avalonia.VisualTree;
 using Avalonia.Automation;
 using Avalonia.Controls.Metadata;
+using Avalonia.Data;
 
 namespace Avalonia.Controls
 {
@@ -57,6 +58,12 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<IDataTemplate?> SelectedContentTemplateProperty =
             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>
         /// The default value for the <see cref="ItemsControl.ItemsPanel"/> property.
         /// </summary>
@@ -134,6 +141,16 @@ namespace Avalonia.Controls
             get { return GetValue(SelectedContentTemplateProperty); }
             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; }
 

+ 20 - 0
tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs

@@ -4,6 +4,7 @@ using System.Linq;
 using Avalonia.Collections;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Templates;
+using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.LogicalTree;
 using Avalonia.Styling;
@@ -736,6 +737,25 @@ namespace Avalonia.Controls.UnitTests
             root.Child = null;
             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
         {