diff --git a/src/Perspex.Controls/Generators/IItemContainerGenerator.cs b/src/Perspex.Controls/Generators/IItemContainerGenerator.cs index 6f3f71d6eb..7d098755c2 100644 --- a/src/Perspex.Controls/Generators/IItemContainerGenerator.cs +++ b/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. /// /// The items. + /// An optional member selector. /// The created controls. IList CreateContainers( int startingIndex, - IEnumerable items); + IEnumerable items, + IMemberSelector selector); /// /// Removes a set of created containers from the index and returns the removed controls. diff --git a/src/Perspex.Controls/Generators/ItemContainerGenerator.cs b/src/Perspex.Controls/Generators/ItemContainerGenerator.cs index 51f70fadb6..f2b2ff131f 100644 --- a/src/Perspex.Controls/Generators/ItemContainerGenerator.cs +++ b/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. /// /// The items. + /// An optional member selector. /// The created container controls. public IList CreateContainers( int startingIndex, - IEnumerable items) + IEnumerable items, + IMemberSelector selector) { Contract.Requires(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); } diff --git a/src/Perspex.Controls/Generators/ItemContainerGenerator`1.cs b/src/Perspex.Controls/Generators/ItemContainerGenerator`1.cs index 1f6cbeb6ac..561e3f61c5 100644 --- a/src/Perspex.Controls/Generators/ItemContainerGenerator`1.cs +++ b/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; } } diff --git a/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs b/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs index c778f3750d..1a0fdb1a5f 100644 --- a/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs +++ b/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. /// /// The items. + /// An optional member selector. /// The created container controls. - public IList CreateContainers(int startingIndex, IEnumerable items) + public IList CreateContainers( + int startingIndex, + IEnumerable items, + IMemberSelector selector) { Contract.Requires(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); } diff --git a/src/Perspex.Controls/ItemsControl.cs b/src/Perspex.Controls/ItemsControl.cs index 83f86eba78..2939347e67 100644 --- a/src/Perspex.Controls/ItemsControl.cs +++ b/src/Perspex.Controls/ItemsControl.cs @@ -33,13 +33,19 @@ namespace Perspex.Controls /// Defines the property. /// public static readonly PerspexProperty ItemsProperty = - PerspexProperty.Register("Items"); + PerspexProperty.Register(nameof(Items)); /// /// Defines the property. /// public static readonly PerspexProperty> ItemsPanelProperty = - PerspexProperty.Register>("ItemsPanel", defaultValue: DefaultPanel); + PerspexProperty.Register>(nameof(ItemsPanel), DefaultPanel); + + /// + /// Defines the property. + /// + public static readonly PerspexProperty MemberSelectorProperty = + PerspexProperty.Register(nameof(MemberSelector)); private IItemContainerGenerator _itemContainerGenerator; @@ -94,6 +100,15 @@ namespace Perspex.Controls set { SetValue(ItemsPanelProperty, value); } } + /// + /// Selects a member from to use as the list item. + /// + public IMemberSelector MemberSelector + { + get { return GetValue(MemberSelectorProperty); } + set { SetValue(MemberSelectorProperty, value); } + } + /// /// Gets the items presenter control. /// diff --git a/src/Perspex.Controls/Perspex.Controls.csproj b/src/Perspex.Controls/Perspex.Controls.csproj index 85972615f8..6bcd55b76c 100644 --- a/src/Perspex.Controls/Perspex.Controls.csproj +++ b/src/Perspex.Controls/Perspex.Controls.csproj @@ -71,6 +71,7 @@ + @@ -122,6 +123,7 @@ + diff --git a/src/Perspex.Controls/Presenters/DeckPresenter.cs b/src/Perspex.Controls/Presenters/DeckPresenter.cs index 9b27aeee7b..526f37e7b9 100644 --- a/src/Perspex.Controls/Presenters/DeckPresenter.cs +++ b/src/Perspex.Controls/Presenters/DeckPresenter.cs @@ -30,6 +30,12 @@ namespace Perspex.Controls.Presenters /// public static readonly PerspexProperty> ItemsPanelProperty = ItemsControl.ItemsPanelProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly PerspexProperty MemberSelectorProperty = + ItemsControl.MemberSelectorProperty.AddOwner(); /// /// Defines the property. @@ -101,6 +107,15 @@ namespace Perspex.Controls.Presenters set { SetValue(ItemsPanelProperty, value); } } + /// + /// Selects a member from to use as the list item. + /// + public IMemberSelector MemberSelector + { + get { return GetValue(MemberSelectorProperty); } + set { SetValue(MemberSelectorProperty, value); } + } + /// /// Gets or sets the index of the selected page. /// @@ -181,7 +196,7 @@ namespace Perspex.Controls.Presenters if (toIndex != -1) { var item = Items.Cast().ElementAt(toIndex); - to = generator.CreateContainers(toIndex, new[] { item }).FirstOrDefault(); + to = generator.CreateContainers(toIndex, new[] { item }, MemberSelector).FirstOrDefault(); if (to != null) { diff --git a/src/Perspex.Controls/Presenters/ItemsPresenter.cs b/src/Perspex.Controls/Presenters/ItemsPresenter.cs index 8d43a0a949..15f73f6822 100644 --- a/src/Perspex.Controls/Presenters/ItemsPresenter.cs +++ b/src/Perspex.Controls/Presenters/ItemsPresenter.cs @@ -28,6 +28,12 @@ namespace Perspex.Controls.Presenters public static readonly PerspexProperty> ItemsPanelProperty = ItemsControl.ItemsPanelProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly PerspexProperty MemberSelectorProperty = + ItemsControl.MemberSelectorProperty.AddOwner(); + private bool _createdPanel; private IItemContainerGenerator _generator; @@ -96,6 +102,15 @@ namespace Perspex.Controls.Presenters set { SetValue(ItemsPanelProperty, value); } } + /// + /// Selects a member from to use as the list item. + /// + public IMemberSelector MemberSelector + { + get { return GetValue(MemberSelectorProperty); } + set { SetValue(MemberSelectorProperty, value); } + } + /// /// Gets the panel used to display the items. /// @@ -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: diff --git a/src/Perspex.Controls/Templates/FuncMemberSelector.cs b/src/Perspex.Controls/Templates/FuncMemberSelector.cs new file mode 100644 index 0000000000..0a0824eb1a --- /dev/null +++ b/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 +{ + /// + /// Selects a member of an object using a . + /// + public class FuncMemberSelector : IMemberSelector + { + private Func _selector; + + /// + /// Initializes a new instance of the + /// class. + /// + /// The selector. + public FuncMemberSelector(Func selector) + { + this._selector = selector; + } + + /// + /// Selects a member of an object. + /// + /// The obeject. + /// The selected member. + public object Select(object o) + { + return (o is TObject) ? _selector((TObject)o) : o; + } + } +} \ No newline at end of file diff --git a/src/Perspex.Controls/Templates/IMemberSelector.cs b/src/Perspex.Controls/Templates/IMemberSelector.cs new file mode 100644 index 0000000000..7488f83820 --- /dev/null +++ b/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 +{ + /// + /// Selects a member of an object. + /// + public interface IMemberSelector + { + /// + /// Selects a member of an object. + /// + /// The obeject. + /// The selected member. + object Select(object o); + } +} \ No newline at end of file diff --git a/src/Perspex.Themes.Default/DeckStyle.cs b/src/Perspex.Themes.Default/DeckStyle.cs index 7b5a53dbe2..73868b2e7b 100644 --- a/src/Perspex.Themes.Default/DeckStyle.cs +++ b/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], diff --git a/src/Perspex.Themes.Default/DropDownStyle.cs b/src/Perspex.Themes.Default/DropDownStyle.cs index b5ed07cea3..5705031b04 100644 --- a/src/Perspex.Themes.Default/DropDownStyle.cs +++ b/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], } }, diff --git a/src/Perspex.Themes.Default/ItemsControlStyle.cs b/src/Perspex.Themes.Default/ItemsControlStyle.cs index f64a7cbc0c..7c66107010 100644 --- a/src/Perspex.Themes.Default/ItemsControlStyle.cs +++ b/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], }; diff --git a/src/Perspex.Themes.Default/ListBoxStyle.cs b/src/Perspex.Themes.Default/ListBoxStyle.cs index 25ce960740..06edb9f7bf 100644 --- a/src/Perspex.Themes.Default/ListBoxStyle.cs +++ b/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], } diff --git a/src/Perspex.Themes.Default/TabControlStyle.cs b/src/Perspex.Themes.Default/TabControlStyle.cs index 139052f29b..ab04a1fddb 100644 --- a/src/Perspex.Themes.Default/TabControlStyle.cs +++ b/src/Perspex.Themes.Default/TabControlStyle.cs @@ -59,12 +59,9 @@ namespace Perspex.Themes.Default new Deck { Name = "deck", - DataTemplates = new DataTemplates - { - new DataTemplate(x => (Control)control.MaterializeDataTemplate(x.Content)), - }, - [!ItemsControl.ItemsProperty] = control[!ItemsControl.ItemsProperty], - [!SelectingItemsControl.SelectedItemProperty] = control[!SelectingItemsControl.SelectedItemProperty], + MemberSelector = new FuncMemberSelector(x => x.Content), + [!Deck.ItemsProperty] = control[!ItemsControl.ItemsProperty], + [!Deck.SelectedItemProperty] = control[!SelectingItemsControl.SelectedItemProperty], [~Deck.TransitionProperty] = control[~TabControl.TransitionProperty], [Grid.RowProperty] = 1, } diff --git a/src/Perspex.Themes.Default/TreeViewStyle.cs b/src/Perspex.Themes.Default/TreeViewStyle.cs index 27108b96a3..b85cb82143 100644 --- a/src/Perspex.Themes.Default/TreeViewStyle.cs +++ b/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], } diff --git a/tests/Perspex.Controls.UnitTests/ContentControlTests.cs b/tests/Perspex.Controls.UnitTests/ContentControlTests.cs index a946e91d5b..35c91b746c 100644 --- a/tests/Perspex.Controls.UnitTests/ContentControlTests.cs +++ b/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(parent => diff --git a/tests/Perspex.Controls.UnitTests/ItemsControlTests.cs b/tests/Perspex.Controls.UnitTests/ItemsControlTests.cs index 04559bd5bb..da7d4b953f 100644 --- a/tests/Perspex.Controls.UnitTests/ItemsControlTests.cs +++ b/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(x => x.Value), + }; + + target.ApplyTemplate(); + + var text = target.Presenter.Panel.Children + .Cast() + .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], } }; diff --git a/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs b/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs index 819842166c..50378047dc 100644 --- a/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs +++ b/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs @@ -101,12 +101,12 @@ namespace Perspex.Controls.UnitTests.Presenters target.ApplyTemplate(); - var text = target.Panel.Children.OfType().Select(x => x.Text).ToList(); + var text = target.Panel.Children.Cast().Select(x => x.Text).ToList(); Assert.Equal(new[] { "foo", "bar" }, text); items.RemoveAt(1); - text = target.Panel.Children.OfType().Select(x => x.Text).ToList(); + text = target.Panel.Children.Cast().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(x => x.Value), + }; + + target.ApplyTemplate(); + + var text = target.Panel.Children + .Cast() + .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(x => x.Value), + }; + + target.ApplyTemplate(); + + var dataContexts = target.Panel.Children + .Cast() + .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 { } diff --git a/tests/Perspex.Controls.UnitTests/TabControlTests.cs b/tests/Perspex.Controls.UnitTests/TabControlTests.cs index 6ce1a120a2..0c1b80fa1a 100644 --- a/tests/Perspex.Controls.UnitTests/TabControlTests.cs +++ b/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(CreateTabControlTemplate), + DataContext = "Base", + DataTemplates = new DataTemplates + { + new DataTemplate(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(CreateDeckTemplate), - DataTemplates = new DataTemplates - { - new DataTemplate(x => (Control)parent.MaterializeDataTemplate(x.Content)), - }, + MemberSelector = new FuncMemberSelector(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; } + } } }