From 49c46f4090d44d250c09edd41e8e11799e8a5a7b Mon Sep 17 00:00:00 2001 From: Jerome Laban Date: Tue, 4 Aug 2020 21:39:57 -0400 Subject: [PATCH] feat: Add support DataTemplate is container host --- .../SamplesApp.Skia/SamplesApp.Skia.csproj | 2 +- .../ItemsControlTests/Given_ItemsControl.cs | 56 ++++++ .../ListViewBaseTests/Given_ListViewBase.cs | 164 ++++++++++++++++-- .../Given_ListViewBase_CustomContainer.cs | 122 +++++++++++++ .../ContentPresenter/ContentPresenter.cs | 2 +- .../Controls/ItemsControl/ItemsControl.cs | 70 +++++--- 6 files changed, 375 insertions(+), 41 deletions(-) create mode 100644 src/Uno.UI.Tests/Windows_UI_XAML_Controls/ListViewBaseTests/Given_ListViewBase_CustomContainer.cs diff --git a/src/SamplesApp/SamplesApp.Skia/SamplesApp.Skia.csproj b/src/SamplesApp/SamplesApp.Skia/SamplesApp.Skia.csproj index 3fd736e9379b..e37317528db9 100644 --- a/src/SamplesApp/SamplesApp.Skia/SamplesApp.Skia.csproj +++ b/src/SamplesApp/SamplesApp.Skia/SamplesApp.Skia.csproj @@ -42,7 +42,7 @@ - + diff --git a/src/Uno.UI.Tests/Windows_UI_XAML_Controls/ItemsControlTests/Given_ItemsControl.cs b/src/Uno.UI.Tests/Windows_UI_XAML_Controls/ItemsControlTests/Given_ItemsControl.cs index 2956c756cb55..97b8c37d87f3 100644 --- a/src/Uno.UI.Tests/Windows_UI_XAML_Controls/ItemsControlTests/Given_ItemsControl.cs +++ b/src/Uno.UI.Tests/Windows_UI_XAML_Controls/ItemsControlTests/Given_ItemsControl.cs @@ -186,6 +186,62 @@ public void When_ObservableVectorChanged() source.Remove(1); Assert.AreEqual(7, count); } + + [TestMethod] + public void When_ContainerStyleSet() + { + var count = 0; + var panel = new StackPanel(); + + var source = new ObservableVector() { 1, 2, 3 }; + + var SUT = new ItemsControl() + { + ItemsPanelRoot = panel, + ItemContainerStyle = BuildBasicContainerStyle(), + InternalItemsPanelRoot = panel, + ItemTemplate = new DataTemplate(() => + { + count++; + return new Border(); + }) + }; + + SUT.ApplyTemplate(); + + Assert.AreEqual(0, count); + + SUT.ItemsSource = source; + Assert.AreEqual(3, count); + + source.Add(4); + Assert.AreEqual(7, count); + + source.Remove(1); + Assert.AreEqual(7, count); + } + + private Style BuildBasicContainerStyle() => + new Style(typeof(Windows.UI.Xaml.Controls.ListViewItem)) + { + Setters = { + new Setter("Template", t => + t.Template = Funcs.Create(() => + new Grid + { + Children = { + new ContentPresenter() + .Apply(p => { + p.SetBinding(ContentPresenter.ContentTemplateProperty, new Binding(){ Path = "ContentTemplate", RelativeSource = RelativeSource.TemplatedParent }); + p.SetBinding(ContentPresenter.ContentProperty, new Binding(){ Path = "Content", RelativeSource = RelativeSource.TemplatedParent }); + }) + } + } + ) + ) + } + }; + } public class MyItemsControl : ItemsControl diff --git a/src/Uno.UI.Tests/Windows_UI_XAML_Controls/ListViewBaseTests/Given_ListViewBase.cs b/src/Uno.UI.Tests/Windows_UI_XAML_Controls/ListViewBaseTests/Given_ListViewBase.cs index 2bd5d8d7a12c..89919a3d834c 100644 --- a/src/Uno.UI.Tests/Windows_UI_XAML_Controls/ListViewBaseTests/Given_ListViewBase.cs +++ b/src/Uno.UI.Tests/Windows_UI_XAML_Controls/ListViewBaseTests/Given_ListViewBase.cs @@ -619,26 +619,156 @@ public void When_ItemClick() Assert.AreEqual(0, selectionChanged[0].RemovedItems.Count); } + [TestMethod] + public void When_ContainerSet_Then_ContentShouldBeSet() + { + var SUT = new ListView() + { + ItemsPanel = new ItemsPanelTemplate(() => new StackPanel()), + ItemContainerStyle = BuildBasicContainerStyle(), + ItemTemplate = new DataTemplate(() => + { + var tb = new TextBlock(); + tb.SetBinding(TextBlock.TextProperty, new Binding()); + return tb; + }), + Template = new ControlTemplate(() => new ItemsPresenter()), + }; + + SUT.ForceLoaded(); + + var source = new[] { + "item 0", + }; + + SUT.ItemsSource = source; + + Assert.AreEqual(-1, SUT.SelectedIndex); + + var si = SUT.ContainerFromItem(source[0]) as SelectorItem; + Assert.IsNotNull(si); + + var tb = si.ContentTemplateRoot as TextBlock; + Assert.AreEqual("item 0", tb?.Text); + } + + [TestMethod] + public void When_IsItsOwnItemContainer_FromSource() + { + var SUT = new ListView() + { + Style = null, + ItemsPanel = new ItemsPanelTemplate(() => new StackPanel()), + ItemContainerStyle = BuildBasicContainerStyle(), + Template = new ControlTemplate(() => new ItemsPresenter()), + SelectionMode = ListViewSelectionMode.Single, + }; + + SUT.ForceLoaded(); + + var source = new[] { + new ListViewItem(){ Content = "item 1" }, + new ListViewItem(){ Content = "item 2" }, + new ListViewItem(){ Content = "item 3" }, + new ListViewItem(){ Content = "item 4" }, + }; + + SUT.ItemsSource = source; + + var si = SUT.ContainerFromItem(source[0]) as ListViewItem; + Assert.IsNotNull(si); + Assert.AreEqual("item 1", si.Content); + } + + [TestMethod] + public void When_NoItemTemplate() + { + var SUT = new ListView() + { + Style = null, + ItemsPanel = new ItemsPanelTemplate(() => new StackPanel()), + ItemContainerStyle = BuildBasicContainerStyle(), + ItemTemplate = null, + ItemTemplateSelector = null, + Template = new ControlTemplate(() => new ItemsPresenter()), + }; + + SUT.ForceLoaded(); + + var source = new[] { + "Item 1" + }; + + SUT.ItemsSource = source; + + var si = SUT.ContainerFromItem(source[0]) as ListViewItem; + Assert.IsNotNull(si); + Assert.AreEqual("Item 1", si.Content); + Assert.IsInstanceOfType(si.ContentTemplateRoot, typeof(ImplicitTextBlock)); + Assert.AreEqual("Item 1", (si.ContentTemplateRoot as TextBlock).Text); + } + + [TestMethod] + public void When_IsItsOwnItemContainer_FromSource_With_DataTemplate() + { + var SUT = new ListView() + { + Style = null, + ItemsPanel = new ItemsPanelTemplate(() => new StackPanel()), + ItemContainerStyle = BuildBasicContainerStyle(), + ItemTemplate = new DataTemplate(() => + { + var tb = new TextBlock(); + tb.SetBinding(TextBlock.TextProperty, new Binding()); + return tb; + }), + Template = new ControlTemplate(() => new ItemsPresenter()), + SelectionMode = ListViewSelectionMode.Single, + }; + + SUT.ForceLoaded(); + + var source = new object[] { + new ListViewItem(){ Content = "item 1" }, + "item 2" + }; + + SUT.ItemsSource = source; + + var si = SUT.ContainerFromItem(source[0]) as ListViewItem; + Assert.IsNotNull(si); + Assert.AreEqual("item 1", si.Content); + Assert.AreSame(si, source[0]); + Assert.IsFalse(si.IsGeneratedContainer); + + var si2 = SUT.ContainerFromItem(source[1]) as ListViewItem; + Assert.IsNotNull(si2); + Assert.AreNotSame(si, source[1]); + Assert.AreEqual("item 2", si2.DataContext); + Assert.AreEqual("item 2", (si2.Content as TextBlock).Text); + Assert.IsTrue(si2.IsGeneratedContainer); + } + private Style BuildBasicContainerStyle() => - new Style(typeof(Windows.UI.Xaml.Controls.ListViewItem)) - { - Setters = { - new Setter("Template", t => - t.Template = Funcs.Create(() => - new Grid - { - Children = { - new ContentPresenter() - .Apply(p => { - p.SetBinding(ContentPresenter.ContentTemplateProperty, new Binding(){ Path = "ContentTemplate", RelativeSource = RelativeSource.TemplatedParent }); - p.SetBinding(ContentPresenter.ContentProperty, new Binding(){ Path = "Content", RelativeSource = RelativeSource.TemplatedParent }); - }) - } + new Style(typeof(Windows.UI.Xaml.Controls.ListViewItem)) + { + Setters = { + new Setter("Template", t => + t.Template = Funcs.Create(() => + new Grid + { + Children = { + new ContentPresenter() + .Apply(p => { + p.SetBinding(ContentPresenter.ContentTemplateProperty, new Binding(){ Path = "ContentTemplate", RelativeSource = RelativeSource.TemplatedParent }); + p.SetBinding(ContentPresenter.ContentProperty, new Binding(){ Path = "Content", RelativeSource = RelativeSource.TemplatedParent }); + }) } - ) + } ) - } - }; + ) + } + }; } public class MyModel diff --git a/src/Uno.UI.Tests/Windows_UI_XAML_Controls/ListViewBaseTests/Given_ListViewBase_CustomContainer.cs b/src/Uno.UI.Tests/Windows_UI_XAML_Controls/ListViewBaseTests/Given_ListViewBase_CustomContainer.cs new file mode 100644 index 000000000000..e8ed6724d329 --- /dev/null +++ b/src/Uno.UI.Tests/Windows_UI_XAML_Controls/ListViewBaseTests/Given_ListViewBase_CustomContainer.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Uno.Extensions; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Data; + +namespace Uno.UI.Tests.ItemsControlTests_CustomContainer +{ + [TestClass] + public class Given_ListViewBase_CustomContainer + { + [TestMethod] + public void When_TemplateRootIsOwnContainer() + { + var count = 0; + var panel = new StackPanel(); + + var SUT = new MyItemsControl() + { + ItemsPanelRoot = panel, + ItemTemplate = new DataTemplate(() => + { + count++; + return new MyCustomItemContainer() { MyValue = 42 }; + }), + Template = new ControlTemplate(() => new ItemsPresenter()), + }; + + SUT.ApplyTemplate(); + + SUT.ItemsSource = new object[] + { + 42 + }; + + var container = SUT.ContainerFromIndex(0) as MyCustomItemContainer; + + Assert.IsNotNull(container); + Assert.IsTrue(container.IsGeneratedContainer); + Assert.IsFalse(container.ContentTemplateRoot is MyCustomItemContainer); + Assert.AreEqual(42, container.Content); + } + + [TestMethod] + public void When_TemplateSelector_RootIsOwnContainer() + { + var count = 0; + var panel = new StackPanel(); + + var itemsPresenter = new MyItemsControl(); + + var itemTemplate = new DataTemplate(() => + { + count++; + return new MyCustomItemContainer() { MyValue = 42 }; + }); + + var SUT = new MyItemsControl() + { + ItemsPanelRoot = panel, + ItemTemplateSelector = new MyDataTemplateSelector(i => itemTemplate), + Template = new ControlTemplate(() => new ItemsPresenter()), + }; + + SUT.ApplyTemplate(); + + SUT.ItemsSource = new object[] + { + 42 + }; + + var container = SUT.ContainerFromIndex(0) as MyCustomItemContainer; + + Assert.IsTrue(container is MyCustomItemContainer); + Assert.IsTrue(container.IsGeneratedContainer); + Assert.IsFalse(container.ContentTemplateRoot is MyCustomItemContainer); + Assert.AreEqual(42, container.Content); + } + } + + public class MyDataTemplateSelector : DataTemplateSelector + { + private Func _selector; + + public MyDataTemplateSelector(Func selector) => _selector = selector; + + protected override DataTemplate SelectTemplateCore(object item) => _selector.Invoke(item); + } + + + public class MyItemsControl : ListView + { + protected override DependencyObject GetContainerForItemOverride() + => new MyCustomItemContainer(); + + protected override bool IsItemItsOwnContainerOverride(object item) + => item is MyCustomItemContainer; + } + + public class MyCustomItemContainer : SelectorItem + { + public int MyValue + { + get { return (int)GetValue(MyValueProperty); } + set { SetValue(MyValueProperty, value); } + } + + // Using a DependencyProperty as the backing store for MyValue. This enables animation, styling, binding, etc... + public static readonly DependencyProperty MyValueProperty = + DependencyProperty.Register("MyValue", typeof(int), typeof(MyCustomItemContainer), new PropertyMetadata(0)); + + + } +} diff --git a/src/Uno.UI/UI/Xaml/Controls/ContentPresenter/ContentPresenter.cs b/src/Uno.UI/UI/Xaml/Controls/ContentPresenter/ContentPresenter.cs index af0daa0e0e76..421f00437221 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ContentPresenter/ContentPresenter.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ContentPresenter/ContentPresenter.cs @@ -602,7 +602,7 @@ protected virtual void OnContentChanged(object oldValue, object newValue) } } - private void TrySetDataContextFromContent(object value) + internal void TrySetDataContextFromContent(object value) { if (value != null) { diff --git a/src/Uno.UI/UI/Xaml/Controls/ItemsControl/ItemsControl.cs b/src/Uno.UI/UI/Xaml/Controls/ItemsControl/ItemsControl.cs index e95cb28081d1..dfde7b0b9a80 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ItemsControl/ItemsControl.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ItemsControl/ItemsControl.cs @@ -15,6 +15,7 @@ using Microsoft.Extensions.Logging; using Uno.UI.Extensions; using System.ComponentModel; +using Windows.Media.Core; #if XAMARIN_ANDROID using View = Android.Views.View; @@ -1011,15 +1012,12 @@ protected virtual DependencyObject GetContainerForItemOverride() /// The item to display. protected virtual void PrepareContainerForItemOverride(DependencyObject element, object item) { - var isOwnContainer = element == item; - - ContentControl containerAsContentControl; - ContentPresenter containerAsContentPresenter; + var isOwnContainer = ReferenceEquals(element, item); var styleFromItemsControl = ItemContainerStyle ?? ItemContainerStyleSelector?.SelectStyle(item, element); //Prepare ContentPresenter - if ((containerAsContentPresenter = element as ContentPresenter) != null) + if (element is ContentPresenter containerAsContentPresenter) { if (styleFromItemsControl != null) { @@ -1030,24 +1028,17 @@ protected virtual void PrepareContainerForItemOverride(DependencyObject element, containerAsContentPresenter.Style = null; } - containerAsContentPresenter.ContentTemplate = ItemTemplate; - containerAsContentPresenter.ContentTemplateSelector = ItemTemplateSelector; - if (!isOwnContainer) { - containerAsContentPresenter.Content = item; + containerAsContentPresenter.TrySetDataContextFromContent(item); } } - - //Prepare ContentControl - if ((containerAsContentControl = element as ContentControl) != null) + else if (element is ContentControl containerAsContentControl) { if (styleFromItemsControl != null) { containerAsContentControl.Style = styleFromItemsControl; } - containerAsContentControl.ContentTemplate = ItemTemplate; - containerAsContentControl.ContentTemplateSelector = ItemTemplateSelector; if (!isOwnContainer) { @@ -1056,11 +1047,6 @@ protected virtual void PrepareContainerForItemOverride(DependencyObject element, // This avoids the inner content to go through a partial content being // the result of the fallback value of the binding set below. containerAsContentControl.DataContext = item; - - if (containerAsContentControl.GetBindingExpression(ContentControl.ContentProperty) == null) - { - containerAsContentControl.SetBinding(ContentControl.ContentProperty, new Binding()); - } } } } @@ -1115,13 +1101,53 @@ internal DependencyObject GetContainerForIndex(int index) { var item = ItemFromIndex(index); - var container = IsItemItsOwnContainerOverride(item) - ? item as DependencyObject - : GetContainerForItemOverride(); + DependencyObject elementTemplateRoot = DataTemplateHelper.ResolveTemplate(ItemTemplate, ItemTemplateSelector, item, null)?.LoadContentCached() as DependencyObject; + + var isItemItsOwnContainer = IsItemItsOwnContainerOverride(item); + var isContainerFromTemplateRoot = !isItemItsOwnContainer + && this is Primitives.Selector + && IsItemItsOwnContainerOverride(elementTemplateRoot); + + DependencyObject container; + + if (isItemItsOwnContainer) + { + container = item as DependencyObject; + + // The item here is left untouched, it's not being applied + // the ItemTemplate/ItemTemplateSelector + } + else if (isContainerFromTemplateRoot) + { + container = elementTemplateRoot; + SetContainerContent(container, item, isGeneratedContainer: true); + } + else + { + container = GetContainerForItemOverride(); + SetContainerContent(container, elementTemplateRoot ?? item); + } container.SetValue(ItemsControlForItemContainerProperty, new WeakReference(this)); return container; + + static void SetContainerContent(DependencyObject container, object content, bool isGeneratedContainer = false) + { + if (container is ContentPresenter containerAsContentPresenter) + { + containerAsContentPresenter.Content = content; + } + else if (container is ContentControl containerAsContentControl) + { + containerAsContentControl.Content = content; + + if (isGeneratedContainer) + { + containerAsContentControl.IsGeneratedContainer = isGeneratedContainer; + } + } + } } public object ItemFromContainer(DependencyObject container)