Browse Source

Added ItemsControl.MemberSelector

And use it to select the TabItem.Content property for the Deck in a
TabControl.
Steven Kirk 10 years ago
parent
commit
338a1c0d91

+ 3 - 1
src/Perspex.Controls/Generators/IItemContainerGenerator.cs

@@ -25,10 +25,12 @@ namespace Perspex.Controls.Generators
         /// The index of the first item of the data in the containing collection.
         /// </param>
         /// <param name="items">The items.</param>
+        /// <param name="selector">An optional member selector.</param>
         /// <returns>The created controls.</returns>
         IList<IControl> CreateContainers(
             int startingIndex,
-            IEnumerable items);
+            IEnumerable items,
+            IMemberSelector selector);
 
         /// <summary>
         /// Removes a set of created containers from the index and returns the removed controls.

+ 5 - 2
src/Perspex.Controls/Generators/ItemContainerGenerator.cs

@@ -45,10 +45,12 @@ namespace Perspex.Controls.Generators
         /// The index of the first item of the data in the containing collection.
         /// </param>
         /// <param name="items">The items.</param>
+        /// <param name="selector">An optional member selector.</param>
         /// <returns>The created container controls.</returns>
         public IList<IControl> CreateContainers(
             int startingIndex,
-            IEnumerable items)
+            IEnumerable items,
+            IMemberSelector selector)
         {
             Contract.Requires<ArgumentNullException>(items != null);
 
@@ -57,7 +59,8 @@ namespace Perspex.Controls.Generators
 
             foreach (var item in items)
             {
-                IControl container = CreateContainer(item);
+                var i = selector != null ? selector.Select(item) : item;
+                var container = CreateContainer(i);
                 result.Add(container);
             }
 

+ 0 - 1
src/Perspex.Controls/Generators/ItemContainerGenerator`1.cs

@@ -37,7 +37,6 @@ namespace Perspex.Controls.Generators
             {
                 var result = new T();
                 result.Content = Owner.MaterializeDataTemplate(item);
-                result.DataContext = item;
                 return result;
             }
         }

+ 8 - 3
src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs

@@ -46,8 +46,12 @@ namespace Perspex.Controls.Generators
         /// The index of the first item of the data in the containing collection.
         /// </param>
         /// <param name="items">The items.</param>
+        /// <param name="selector">An optional member selector.</param>
         /// <returns>The created container controls.</returns>
-        public IList<IControl> CreateContainers(int startingIndex, IEnumerable items)
+        public IList<IControl> CreateContainers(
+            int startingIndex, 
+            IEnumerable items,
+            IMemberSelector selector)
         {
             Contract.Requires<ArgumentNullException>(items != null);
 
@@ -56,8 +60,9 @@ namespace Perspex.Controls.Generators
 
             foreach (var item in items)
             {
-                var container = CreateContainer(item);
-                _containers.Add(item, container);
+                var i = selector != null ? selector.Select(item) : item;
+                var container = CreateContainer(i);
+                _containers.Add(i, container);
                 result.Add(container);
             }
 

+ 17 - 2
src/Perspex.Controls/ItemsControl.cs

@@ -33,13 +33,19 @@ namespace Perspex.Controls
         /// Defines the <see cref="Items"/> property.
         /// </summary>
         public static readonly PerspexProperty<IEnumerable> ItemsProperty =
-            PerspexProperty.Register<ItemsControl, IEnumerable>("Items");
+            PerspexProperty.Register<ItemsControl, IEnumerable>(nameof(Items));
 
         /// <summary>
         /// Defines the <see cref="ItemsPanel"/> property.
         /// </summary>
         public static readonly PerspexProperty<ITemplate<IPanel>> ItemsPanelProperty =
-            PerspexProperty.Register<ItemsControl, ITemplate<IPanel>>("ItemsPanel", defaultValue: DefaultPanel);
+            PerspexProperty.Register<ItemsControl, ITemplate<IPanel>>(nameof(ItemsPanel), DefaultPanel);
+
+        /// <summary>
+        /// Defines the <see cref="MemberSelector"/> property.
+        /// </summary>
+        public static readonly PerspexProperty<IMemberSelector> MemberSelectorProperty =
+            PerspexProperty.Register<ItemsControl, IMemberSelector>(nameof(MemberSelector));
 
         private IItemContainerGenerator _itemContainerGenerator;
 
@@ -94,6 +100,15 @@ namespace Perspex.Controls
             set { SetValue(ItemsPanelProperty, value); }
         }
 
+        /// <summary>
+        /// Selects a member from <see cref="Items"/> to use as the list item.
+        /// </summary>
+        public IMemberSelector MemberSelector
+        {
+            get { return GetValue(MemberSelectorProperty); }
+            set { SetValue(MemberSelectorProperty, value); }
+        }
+
         /// <summary>
         /// Gets the items presenter control.
         /// </summary>

+ 2 - 0
src/Perspex.Controls/Perspex.Controls.csproj

@@ -71,6 +71,7 @@
     <Compile Include="Canvas.cs" />
     <Compile Include="Templates\ControlTemplate`2.cs" />
     <Compile Include="Templates\DataTemplate`1.cs" />
+    <Compile Include="Templates\FuncMemberSelector.cs" />
     <Compile Include="Templates\IControlTemplate.cs" />
     <Compile Include="Templates\FuncTemplate`2.cs" />
     <Compile Include="Templates\FuncTemplate`1.cs" />
@@ -122,6 +123,7 @@
     <Compile Include="Primitives\Thumb.cs" />
     <Compile Include="RequestBringIntoViewEventArgs.cs" />
     <Compile Include="Shapes\Ellipse.cs" />
+    <Compile Include="Templates\IMemberSelector.cs" />
     <Compile Include="Templates\ITemplate`2.cs" />
     <Compile Include="Templates\ITemplate`1.cs" />
     <Compile Include="Templates\ITreeDataTemplate.cs" />

+ 16 - 1
src/Perspex.Controls/Presenters/DeckPresenter.cs

@@ -30,6 +30,12 @@ namespace Perspex.Controls.Presenters
         /// </summary>
         public static readonly PerspexProperty<ITemplate<IPanel>> ItemsPanelProperty =
             ItemsControl.ItemsPanelProperty.AddOwner<DeckPresenter>();
+        
+        /// <summary>
+        /// Defines the <see cref="MemberSelector"/> property.
+        /// </summary>
+        public static readonly PerspexProperty<IMemberSelector> MemberSelectorProperty =
+            ItemsControl.MemberSelectorProperty.AddOwner<DeckPresenter>();
 
         /// <summary>
         /// Defines the <see cref="SelectedIndex"/> property.
@@ -101,6 +107,15 @@ namespace Perspex.Controls.Presenters
             set { SetValue(ItemsPanelProperty, value); }
         }
 
+        /// <summary>
+        /// Selects a member from <see cref="Items"/> to use as the list item.
+        /// </summary>
+        public IMemberSelector MemberSelector
+        {
+            get { return GetValue(MemberSelectorProperty); }
+            set { SetValue(MemberSelectorProperty, value); }
+        }
+
         /// <summary>
         /// Gets or sets the index of the selected page.
         /// </summary>
@@ -181,7 +196,7 @@ namespace Perspex.Controls.Presenters
             if (toIndex != -1)
             {
                 var item = Items.Cast<object>().ElementAt(toIndex);
-                to = generator.CreateContainers(toIndex, new[] { item }).FirstOrDefault();
+                to = generator.CreateContainers(toIndex, new[] { item }, MemberSelector).FirstOrDefault();
 
                 if (to != null)
                 {

+ 17 - 3
src/Perspex.Controls/Presenters/ItemsPresenter.cs

@@ -28,6 +28,12 @@ namespace Perspex.Controls.Presenters
         public static readonly PerspexProperty<ITemplate<IPanel>> ItemsPanelProperty =
             ItemsControl.ItemsPanelProperty.AddOwner<ItemsPresenter>();
 
+        /// <summary>
+        /// Defines the <see cref="MemberSelector"/> property.
+        /// </summary>
+        public static readonly PerspexProperty<IMemberSelector> MemberSelectorProperty =
+            ItemsControl.MemberSelectorProperty.AddOwner<ItemsPresenter>();
+
         private bool _createdPanel;
 
         private IItemContainerGenerator _generator;
@@ -96,6 +102,15 @@ namespace Perspex.Controls.Presenters
             set { SetValue(ItemsPanelProperty, value); }
         }
 
+        /// <summary>
+        /// Selects a member from <see cref="Items"/> to use as the list item.
+        /// </summary>
+        public IMemberSelector MemberSelector
+        {
+            get { return GetValue(MemberSelectorProperty); }
+            set { SetValue(MemberSelectorProperty, value); }
+        }
+
         /// <summary>
         /// Gets the panel used to display the items.
         /// </summary>
@@ -171,8 +186,7 @@ namespace Perspex.Controls.Presenters
         {
             if (items != null)
             {
-                Panel.Children.AddRange(
-                    ItemContainerGenerator.CreateContainers(0, Items));
+                Panel.Children.AddRange(ItemContainerGenerator.CreateContainers(0, Items, MemberSelector));
 
                 INotifyCollectionChanged incc = items as INotifyCollectionChanged;
 
@@ -229,7 +243,7 @@ namespace Perspex.Controls.Presenters
                 {
                     case NotifyCollectionChangedAction.Add:
                         Panel.Children.AddRange(
-                            generator.CreateContainers(e.NewStartingIndex, e.NewItems));
+                            generator.CreateContainers(e.NewStartingIndex, e.NewItems, MemberSelector));
                         break;
 
                     case NotifyCollectionChangedAction.Remove:

+ 35 - 0
src/Perspex.Controls/Templates/FuncMemberSelector.cs

@@ -0,0 +1,35 @@
+// Copyright (c) The Perspex Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+
+namespace Perspex.Controls.Templates
+{
+    /// <summary>
+    /// Selects a member of an object using a <see cref="Func{TObject, TMember}"/>.
+    /// </summary>
+    public class FuncMemberSelector<TObject, TMember> : IMemberSelector
+    {
+        private Func<TObject, TMember> _selector;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="FuncMemberSelector{TObject, TMember}"/>
+        /// class.
+        /// </summary>
+        /// <param name="selector">The selector.</param>
+        public FuncMemberSelector(Func<TObject, TMember> selector)
+        {
+            this._selector = selector;
+        }
+
+        /// <summary>
+        /// Selects a member of an object.
+        /// </summary>
+        /// <param name="o">The obeject.</param>
+        /// <returns>The selected member.</returns>
+        public object Select(object o)
+        {
+            return (o is TObject) ? _selector((TObject)o) : o;
+        }
+    }
+}

+ 18 - 0
src/Perspex.Controls/Templates/IMemberSelector.cs

@@ -0,0 +1,18 @@
+// Copyright (c) The Perspex Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+namespace Perspex.Controls.Templates
+{
+    /// <summary>
+    /// Selects a member of an object.
+    /// </summary>
+    public interface IMemberSelector
+    {
+        /// <summary>
+        /// Selects a member of an object.
+        /// </summary>
+        /// <param name="o">The obeject.</param>
+        /// <returns>The selected member.</returns>
+        object Select(object o);
+    }
+}

+ 1 - 0
src/Perspex.Themes.Default/DeckStyle.cs

@@ -42,6 +42,7 @@ namespace Perspex.Themes.Default
             return new DeckPresenter
             {
                 Name = "itemsPresenter",
+                MemberSelector = control.MemberSelector,
                 [~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty],
                 [~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty],
                 [~DeckPresenter.SelectedIndexProperty] = control[~SelectingItemsControl.SelectedIndexProperty],

+ 1 - 0
src/Perspex.Themes.Default/DropDownStyle.cs

@@ -128,6 +128,7 @@ namespace Perspex.Themes.Default
                                 Padding = new Thickness(4),
                                 Child = new ItemsPresenter
                                 {
+                                    MemberSelector = control.MemberSelector,
                                     [~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty],
                                 }
                             },

+ 1 - 0
src/Perspex.Themes.Default/ItemsControlStyle.cs

@@ -42,6 +42,7 @@ namespace Perspex.Themes.Default
             return new ItemsPresenter
             {
                 Name = "itemsPresenter",
+                MemberSelector = control.MemberSelector,
                 [~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty],
                 [~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty],
             };

+ 1 - 0
src/Perspex.Themes.Default/ListBoxStyle.cs

@@ -53,6 +53,7 @@ namespace Perspex.Themes.Default
                     Content = new ItemsPresenter
                     {
                         Name = "itemsPresenter",
+                        MemberSelector = control.MemberSelector,
                         [~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty],
                         [~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty],
                     }

+ 3 - 6
src/Perspex.Themes.Default/TabControlStyle.cs

@@ -59,12 +59,9 @@ namespace Perspex.Themes.Default
                     new Deck
                     {
                         Name = "deck",
-                        DataTemplates = new DataTemplates
-                        {
-                            new DataTemplate<TabItem>(x => (Control)control.MaterializeDataTemplate(x.Content)),
-                        },
-                        [!ItemsControl.ItemsProperty] = control[!ItemsControl.ItemsProperty],
-                        [!SelectingItemsControl.SelectedItemProperty] = control[!SelectingItemsControl.SelectedItemProperty],
+                        MemberSelector = new FuncMemberSelector<TabItem, object>(x => x.Content),
+                        [!Deck.ItemsProperty] = control[!ItemsControl.ItemsProperty],
+                        [!Deck.SelectedItemProperty] = control[!SelectingItemsControl.SelectedItemProperty],
                         [~Deck.TransitionProperty] = control[~TabControl.TransitionProperty],
                         [Grid.RowProperty] = 1,
                     }

+ 1 - 0
src/Perspex.Themes.Default/TreeViewStyle.cs

@@ -54,6 +54,7 @@ namespace Perspex.Themes.Default
                     Content = new ItemsPresenter
                     {
                         Name = "itemsPresenter",
+                        MemberSelector = control.MemberSelector,
                         [~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty],
                         [~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty],
                     }

+ 24 - 0
tests/Perspex.Controls.UnitTests/ContentControlTests.cs

@@ -262,6 +262,30 @@ namespace Perspex.Controls.UnitTests
             Assert.Equal("Bar", ((TextBlock)target.Presenter.Child).Text);
         }
 
+        [Fact]
+        public void DataContext_Should_Be_Set_For_Templated_Data()
+        {
+            var target = new ContentControl();
+
+            target.Template = GetTemplate();
+            target.Content = "Foo";
+            target.ApplyTemplate();
+
+            Assert.Equal("Foo", target.Presenter.Child.DataContext);
+        }
+
+        [Fact]
+        public void DataContext_Should_Not_Be_Set_For_Control_Data()
+        {
+            var target = new ContentControl();
+
+            target.Template = GetTemplate();
+            target.Content = new TextBlock();
+            target.ApplyTemplate();
+
+            Assert.Null(target.Presenter.Child.DataContext);
+        }
+
         private ControlTemplate GetTemplate()
         {
             return new ControlTemplate<ContentControl>(parent =>

+ 22 - 0
tests/Perspex.Controls.UnitTests/ItemsControlTests.cs

@@ -6,6 +6,7 @@ using System.Linq;
 using Perspex.Collections;
 using Perspex.Controls.Presenters;
 using Perspex.Controls.Templates;
+using Perspex.LogicalTree;
 using Perspex.Styling;
 using Perspex.VisualTree;
 using Xunit;
@@ -321,6 +322,26 @@ namespace Perspex.Controls.UnitTests
                 dataContexts);
         }
 
+        [Fact]
+        public void MemberSelector_Should_Select_Member()
+        {
+            var target = new ItemsControl
+            {
+                Template = GetTemplate(),
+                Items = new[] { new Item("Foo"), new Item("Bar") },
+                MemberSelector = new FuncMemberSelector<Item, string>(x => x.Value),
+            };
+
+            target.ApplyTemplate();
+
+            var text = target.Presenter.Panel.Children
+                .Cast<TextBlock>()
+                .Select(x => x.Text)
+                .ToList();
+
+            Assert.Equal(new[] { "Foo", "Bar" }, text);
+        }
+
         private class Item
         {
             public Item(string value)
@@ -341,6 +362,7 @@ namespace Perspex.Controls.UnitTests
                     Child = new ItemsPresenter
                     {
                         Name = "itemsPresenter",
+                        MemberSelector = parent.MemberSelector,
                         [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty],
                     }
                 };

+ 51 - 2
tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs

@@ -101,12 +101,12 @@ namespace Perspex.Controls.UnitTests.Presenters
 
             target.ApplyTemplate();
 
-            var text = target.Panel.Children.OfType<TextBlock>().Select(x => x.Text).ToList();
+            var text = target.Panel.Children.Cast<TextBlock>().Select(x => x.Text).ToList();
             Assert.Equal(new[] { "foo", "bar" }, text);
 
             items.RemoveAt(1);
 
-            text = target.Panel.Children.OfType<TextBlock>().Select(x => x.Text).ToList();
+            text = target.Panel.Children.Cast<TextBlock>().Select(x => x.Text).ToList();
             Assert.Equal(new[] { "foo", "bar" }, text);
         }
 
@@ -174,6 +174,55 @@ namespace Perspex.Controls.UnitTests.Presenters
             Assert.Equal(target.Panel, child);
         }
 
+        [Fact]
+        public void MemberSelector_Should_Select_Member()
+        {
+            var target = new ItemsPresenter
+            {
+                Items = new[] { new Item("Foo"), new Item("Bar") },
+                MemberSelector = new FuncMemberSelector<Item, string>(x => x.Value),
+            };
+
+            target.ApplyTemplate();
+
+            var text = target.Panel.Children
+                .Cast<TextBlock>()
+                .Select(x => x.Text)
+                .ToList();
+
+            Assert.Equal(new[] { "Foo", "Bar" }, text);
+        }
+
+        [Fact]
+        public void MemberSelector_Should_Set_DataContext()
+        {
+            var items = new[] { new Item("Foo"), new Item("Bar") };
+            var target = new ItemsPresenter
+            {
+                Items = items,
+                MemberSelector = new FuncMemberSelector<Item, string>(x => x.Value),
+            };
+
+            target.ApplyTemplate();
+
+            var dataContexts = target.Panel.Children
+                .Cast<TextBlock>()
+                .Select(x => x.DataContext)
+                .ToList();
+
+            Assert.Equal(new[] { "Foo", "Bar" }, dataContexts);
+        }
+
+        private class Item
+        {
+            public Item(string value)
+            {
+                Value = value;
+            }
+
+            public string Value { get; }
+        }
+
         private class TestItem : ContentControl
         {
         }

+ 54 - 6
tests/Perspex.Controls.UnitTests/TabControlTests.cs

@@ -130,6 +130,46 @@ namespace Perspex.Controls.UnitTests
             Assert.Same(target.SelectedTab, target.SelectedItem);
         }
 
+        [Fact]
+        public void DataContexts_Should_Be_Correctly_Set()
+        {
+            var items = new object[]
+            {
+                "Foo",
+                new Item("Bar"),
+                new TextBlock { Text = "Baz" },
+                new TabItem { Content = "Qux" },
+            };
+
+            var target = new TabControl
+            {
+                Template = new ControlTemplate<TabControl>(CreateTabControlTemplate),
+                DataContext = "Base",
+                DataTemplates = new DataTemplates
+                {
+                    new DataTemplate<Item>(x => new Button { Content = x })
+                },
+                Items = items,
+            };
+
+            target.ApplyTemplate();
+
+            var dataContext = ((TextBlock)target.GetLogicalChildren().Single()).DataContext;
+            Assert.Equal(items[0], dataContext);
+
+            target.SelectedIndex = 1;
+            dataContext = ((Button)target.GetLogicalChildren().Single()).DataContext;
+            Assert.Equal(items[1], dataContext);
+
+            target.SelectedIndex = 2;
+            dataContext = ((TextBlock)target.GetLogicalChildren().Single()).DataContext;
+            Assert.Equal("Base", dataContext);
+
+            target.SelectedIndex = 3;
+            dataContext = ((TextBlock)target.GetLogicalChildren().Single()).DataContext;
+            Assert.Equal("Qux", dataContext);
+        }
+
         private Control CreateTabControlTemplate(TabControl parent)
         {
             return new StackPanel
@@ -147,10 +187,7 @@ namespace Perspex.Controls.UnitTests
                     {
                         Name = "deck",
                         Template = new ControlTemplate<Deck>(CreateDeckTemplate),
-                        DataTemplates = new DataTemplates
-                        {
-                            new DataTemplate<TabItem>(x => (Control)parent.MaterializeDataTemplate(x.Content)),
-                        },
+                        MemberSelector = new FuncMemberSelector<TabItem, object>(x => x.Content),
                         [!ItemsControl.ItemsProperty] = parent[!ItemsControl.ItemsProperty],
                         [!SelectingItemsControl.SelectedItemProperty] = parent[!SelectingItemsControl.SelectedItemProperty],
                     }
@@ -172,11 +209,22 @@ namespace Perspex.Controls.UnitTests
             return new DeckPresenter
             {
                 Name = "itemsPresenter",
-                [!ItemsPresenter.ItemsProperty] = control[!ItemsControl.ItemsProperty],
-                [!ItemsPresenter.ItemsPanelProperty] = control[!ItemsControl.ItemsPanelProperty],
+                [!DeckPresenter.ItemsProperty] = control[!ItemsControl.ItemsProperty],
+                [!DeckPresenter.ItemsPanelProperty] = control[!ItemsControl.ItemsPanelProperty],
+                [!DeckPresenter.MemberSelectorProperty] = control[!ItemsControl.MemberSelectorProperty],
                 [!DeckPresenter.SelectedIndexProperty] = control[!SelectingItemsControl.SelectedIndexProperty],
                 [~DeckPresenter.TransitionProperty] = control[~Deck.TransitionProperty],
             };
         }
+
+        private class Item
+        {
+            public Item(string value)
+            {
+                Value = value;
+            }
+
+            public string Value { get; }
+        }
     }
 }