Skip to content
This repository has been archived by the owner on May 1, 2024. It is now read-only.

Fix 15052 NullReferenceException when returning null from DataTemplateSelector #15074

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7577636
Fix NullReferenceExceptions on null from DataTemplateSelector in Tabb…
bakerhillpins Jan 17, 2022
daaccec
Add DataTemplateSelector tests for null return value.
bakerhillpins Jan 17, 2022
18be4a0
Fix null reference on DataTemplate recycle storage.
bakerhillpins Jan 17, 2022
16c4ea5
Validate BindingContext property set with null Template selected.
bakerhillpins Jan 17, 2022
4fe81b0
Fix CollectionView DataTemplateSelector returns null.
bakerhillpins Jan 18, 2022
2c9325a
iOS, ListView with Recycle, handle DataTemplateSelector return null.
bakerhillpins Jan 18, 2022
8eafd9b
CollectionView DataTemplateSelector returns null for iOS/Tizen/UAP
bakerhillpins Jan 21, 2022
8abeb36
Remove unused DataTemplate code in Flyout.
bakerhillpins Jan 21, 2022
a722a99
Rename only
bakerhillpins Jan 21, 2022
bb6937f
Shell support for DataTemplateSelector returns null
bakerhillpins Jan 21, 2022
686914d
Test for null ItemTemplate and shorten null coalescing statement.
bakerhillpins Jan 21, 2022
295b84d
Move default DataTemplate to single source and update layout to cente…
bakerhillpins Jan 24, 2022
1db1c3e
Use default DataTemplate to create default Page for MultiPage<T> impl…
bakerhillpins Jan 24, 2022
3d72852
Handle null from DataTemplateSelectors.
bakerhillpins Jan 24, 2022
92b0cae
Move default DataTemplate construction into lazy singleton.
bakerhillpins Jan 24, 2022
021ce6b
Adjust method to always return a DataTemplate.
bakerhillpins Jan 24, 2022
8ebc055
Deal with BindableLayout.EmptyViewTemplate selector returning null.
bakerhillpins Jan 25, 2022
74328da
Add support for null from Template Selector
bakerhillpins Jan 26, 2022
f97dfab
Add a simple can return null test.
bakerhillpins Jan 26, 2022
e6cda2a
Merge remote-tracking branch 'origin/5.0.0' into fix-15052
bakerhillpins Apr 28, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 73 additions & 2 deletions Xamarin.Forms.Core.UnitTests/DataTemplateSelectorTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.ObjectModel;
using NUnit.Framework;
using Xamarin.Forms;

Expand Down Expand Up @@ -37,6 +38,22 @@ public TemplateTwo() : base(typeof(EntryCell))
}
}

class TemplateThree : DataTemplate
{
public TemplateThree() : base(typeof(ContentPage))
{

}
}

class NullSelector : DataTemplateSelector
{
protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
{
return null;
}
}

class TestDTS : DataTemplateSelector
{
public TestDTS()
Expand All @@ -51,36 +68,78 @@ protected override DataTemplate OnSelectTemplate(object item, BindableObject con
return templateOne;
if (item is byte)
return new TestDTS();
return templateTwo;
if (item is string)
return templateTwo;

return null;
}

readonly DataTemplate templateOne;
readonly DataTemplate templateTwo;
}

class TestPageDTS : DataTemplateSelector
{
public TestPageDTS()
{
templateThree = new TemplateThree();
}

protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
{
if (item is View)
return templateThree;

return null;
}

readonly DataTemplate templateThree;
}

[Test]
public void Constructor()
{
var dts = new TestDTS();
}

[Test]
public void CanReturnNull()
{
var dts = new NullSelector();
Assert.IsNull(dts.SelectTemplate((short)0, null));
}

[Test]
public void ReturnsCorrectType()
{
var dts = new TestDTS();
Assert.IsInstanceOf<TemplateOne>(dts.SelectTemplate(1d, null));
Assert.IsInstanceOf<TemplateTwo>(dts.SelectTemplate("test", null));
Assert.IsNull(dts.SelectTemplate((short)0, null));
}

[Test]
public void ListViewSupport()
{
var listView = new ListView(ListViewCachingStrategy.RecycleElement);
listView.ItemsSource = new object[] { 0d, "test" };
listView.ItemsSource = new object[] { 0d, "test", (short)0 };

listView.ItemTemplate = new TestDTS();
Assert.IsInstanceOf<ViewCell>(listView.TemplatedItems[0]);
Assert.IsInstanceOf<EntryCell>(listView.TemplatedItems[1]);
Assert.IsInstanceOf<TextCell>(listView.TemplatedItems[2]);
}

[Test]
public void ListViewRecycleDTSupport()
{
var listView = new ListView(ListViewCachingStrategy.RecycleElementAndDataTemplate);
listView.ItemsSource = new object[] { 0d, "test", (short)0 };

listView.ItemTemplate = new TestDTS();
Assert.IsInstanceOf<ViewCell>(listView.TemplatedItems[0]);
Assert.IsInstanceOf<EntryCell>(listView.TemplatedItems[1]);
Assert.IsInstanceOf<TextCell>(listView.TemplatedItems[2]);
}

[Test]
Expand All @@ -89,6 +148,18 @@ public void NestingThrowsException()
var dts = new TestDTS();
Assert.Throws<NotSupportedException>(() => dts.SelectTemplate((byte)0, null));
}

[Test]
public void TabbedPageSupport()
{
var tabbedPage = new TabbedPage();
tabbedPage.ItemsSource = new object[] { new View(), "test" };

tabbedPage.ItemTemplate = new TestPageDTS();
Assert.IsInstanceOf<ContentPage>(tabbedPage.Children[0]);
Assert.IsInstanceOf<Page>(tabbedPage.Children[1]);
Assert.AreEqual(tabbedPage.Children[1].BindingContext, "test");
}
}

[TestFixture]
Expand Down
6 changes: 4 additions & 2 deletions Xamarin.Forms.Core/BindableLayout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -275,9 +275,11 @@ View CreateEmptyView(object emptyView, DataTemplate dataTemplate)
return null;
}

if (dataTemplate != null)
var template = dataTemplate?.SelectDataTemplate(layout.BindingContext, layout);

if (template != null)
{
var view = (View)dataTemplate.CreateContent();
var view = (View)template.CreateContent();
view.BindingContext = layout.BindingContext;
return view;
}
Expand Down
7 changes: 2 additions & 5 deletions Xamarin.Forms.Core/CarouselPage.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using Xamarin.Forms.Internals;
using Xamarin.Forms.Platform;

namespace Xamarin.Forms
Expand All @@ -20,11 +21,7 @@ public CarouselPage()

protected override ContentPage CreateDefault(object item)
{
var page = new ContentPage();
if (item != null)
page.Title = item.ToString();

return page;
return (ContentPage)DataTemplateHelpers.DefaultPageTemplate.CreateContent();
}
}
}
34 changes: 33 additions & 1 deletion Xamarin.Forms.Core/DataTemplateExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.ComponentModel;

namespace Xamarin.Forms.Internals
Expand All @@ -16,7 +17,38 @@ public static DataTemplate SelectDataTemplate(this DataTemplate self, object ite

public static object CreateContent(this DataTemplate self, object item, BindableObject container)
{
return self.SelectDataTemplate(item, container).CreateContent();
return self.SelectDataTemplate(item, container)?.CreateContent();
}
}

[EditorBrowsable(EditorBrowsableState.Never)]
public static class DataTemplateHelpers
{
public readonly static DataTemplate DefaultContentTemplate =
new Lazy<DataTemplate>(
() => new DataTemplate(CreateCoreContent)).Value;

public readonly static DataTemplate DefaultPageTemplate =
new Lazy<DataTemplate>(
() => new DataTemplate(() =>
{
var page = new ContentPage() { Content = CreateCoreContent() };
page.SetBinding(Page.TitleProperty, ".");

return page;
})).Value;

static View CreateCoreContent()
{
var label = new Label();
label.SetBinding(Label.TextProperty, ".");

return new StackLayout()
{
HorizontalOptions = LayoutOptions.Center,
VerticalOptions = LayoutOptions.Center,
Children = { label }
};
}
}
}
2 changes: 1 addition & 1 deletion Xamarin.Forms.Core/DataTemplateSelector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public DataTemplate SelectTemplate(object item, BindableObject container)
throw new NotSupportedException(
"DataTemplateSelector.OnSelectTemplate must not return another DataTemplateSelector");

if (recycle)
if (recycle && dataTemplate != null)
{
if (!dataTemplate.CanRecycle)
throw new NotSupportedException(
Expand Down
10 changes: 9 additions & 1 deletion Xamarin.Forms.Core/Shell/BaseShellItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,15 @@ BindableObject NonImplicitParent
}
}

internal static DataTemplate CreateDefaultFlyoutItemCell(string textBinding, string iconBinding)
internal static DataTemplate MenuItemDefaultDataTemplate =
new Lazy<DataTemplate>(
() => CreateDefaultFlyoutItemCell("Text", "Icon") ).Value;

internal static DataTemplate ItemDefaultDataTemplate =
new Lazy<DataTemplate>(
() => CreateDefaultFlyoutItemCell("Title", "FlyoutIcon")).Value;

static DataTemplate CreateDefaultFlyoutItemCell(string textBinding, string iconBinding)
{
return new DataTemplate(() =>
{
Expand Down
28 changes: 11 additions & 17 deletions Xamarin.Forms.Core/Shell/Shell.cs
Original file line number Diff line number Diff line change
Expand Up @@ -255,35 +255,29 @@ internal static BindableObject GetBindableObjectWithFlyoutItemTemplate(BindableO

DataTemplate IShellController.GetFlyoutItemDataTemplate(BindableObject bo)
{
BindableProperty bp = null;
string textBinding;
string iconBinding;
BindableProperty bp;
DataTemplate defaultTemplate;

var bindableObjectWithTemplate = GetBindableObjectWithFlyoutItemTemplate(bo);

if (bo is IMenuItemController)
{
bp = MenuItemTemplateProperty;
textBinding = "Text";
iconBinding = "Icon";
defaultTemplate = BaseShellItem.MenuItemDefaultDataTemplate;
}
else
{
bp = ItemTemplateProperty;
textBinding = "Title";
iconBinding = "FlyoutIcon";
}

if (bindableObjectWithTemplate.IsSet(bp))
{
return (DataTemplate)bindableObjectWithTemplate.GetValue(bp);
defaultTemplate = BaseShellItem.ItemDefaultDataTemplate;
}

if (IsSet(bp))
{
return (DataTemplate)GetValue(bp);
}
DataTemplate dataTemplate = (DataTemplate)(bindableObjectWithTemplate.IsSet(bp) ?
bindableObjectWithTemplate.GetValue(bp) :
IsSet(bp) ?
GetValue(bp) :
null);

return BaseShellItem.CreateDefaultFlyoutItemCell(textBinding, iconBinding);
return dataTemplate?.SelectDataTemplate( bo, this ) ?? defaultTemplate;
}

event EventHandler IShellController.StructureChanged
Expand Down
4 changes: 2 additions & 2 deletions Xamarin.Forms.Core/Shell/ShellContent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ public DataTemplate ContentTemplate

Page IShellContentController.GetOrCreateContent()
{
var template = ContentTemplate;
var content = Content;
var template = ContentTemplate?.SelectDataTemplate(content, this);

Page result = null;
if (template == null)
Expand All @@ -64,7 +64,7 @@ Page IShellContentController.GetOrCreateContent()
}
else
{
result = ContentCache ?? (Page)template.CreateContent(content, this);
result = ContentCache ?? (Page)template.CreateContent();
ContentCache = result;
}

Expand Down
6 changes: 4 additions & 2 deletions Xamarin.Forms.Core/Shell/ShellTemplatedViewManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,11 @@ public static void OnViewTemplateChanged(
Shell shell)
{
View newContentView = currentViewData as View;
if (newViewTemplate != null)

DataTemplate template = newViewTemplate?.SelectDataTemplate(newViewTemplate, shell);
if (template != null)
{
newContentView = (View)newViewTemplate.CreateContent(newViewTemplate, shell);
newContentView = (View)template.CreateContent();
}

SetView(ref localViewRef,
Expand Down
7 changes: 2 additions & 5 deletions Xamarin.Forms.Core/TabbedPage.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using Xamarin.Forms.Internals;
using Xamarin.Forms.Platform;

namespace Xamarin.Forms
Expand Down Expand Up @@ -49,11 +50,7 @@ public Color SelectedTabColor

protected override Page CreateDefault(object item)
{
var page = new Page();
if (item != null)
page.Title = item.ToString();

return page;
return (Page)DataTemplateHelpers.DefaultPageTemplate.CreateContent();
}

public TabbedPage()
Expand Down
12 changes: 8 additions & 4 deletions Xamarin.Forms.Core/TemplatedItemsList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -532,12 +532,13 @@ public int IndexOf(TItem item)
[EditorBrowsable(EditorBrowsableState.Never)]
public DataTemplate SelectDataTemplate(object item)
{
return ItemTemplate.SelectDataTemplate(item, _itemsView);
return ItemTemplate?.SelectDataTemplate(item, _itemsView);
}

public TItem ActivateContent(int index, object item)
{
TItem content = ItemTemplate != null ? (TItem)ItemTemplate.CreateContent(item, _itemsView) : _itemsView.CreateDefault(item);
TItem content = (TItem)ItemTemplate?.CreateContent(item, _itemsView) ??
_itemsView.CreateDefault(item);

content = UpdateContent(content, index, item);

Expand Down Expand Up @@ -740,9 +741,12 @@ TemplatedItemsList<TView, TItem> InsertGrouped(object item, int index)

groupProxy.BindingContext = item;

if (GroupHeaderTemplate != null)
var groupHeaderTemplate =
GroupHeaderTemplate?.SelectDataTemplate(groupProxy.ItemsSource, _itemsView);

if (groupHeaderTemplate != null)
{
groupProxy.HeaderContent = (TItem)GroupHeaderTemplate.CreateContent(groupProxy.ItemsSource, _itemsView);
groupProxy.HeaderContent = (TItem)groupHeaderTemplate.CreateContent();
groupProxy.HeaderContent.BindingContext = groupProxy.ItemsSource;
//groupProxy.HeaderContent.BindingContext = groupProxy;
//groupProxy.HeaderContent.SetBinding (BindingContextProperty, "ItemsSource");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
namespace Xamarin.Forms.Platform.Android
{
public class TemplatedItemViewHolder : SelectableViewHolder
{
readonly ItemContentView _itemContentView;
{
readonly ItemContentView _itemContentView;
readonly DataTemplate _template;
DataTemplate _selectedTemplate;

Expand Down Expand Up @@ -48,11 +48,12 @@ public void Recycle(ItemsView itemsView)
public void Bind(object itemBindingContext, ItemsView itemsView,
Action<Size> reportMeasure = null, Size? size = null)
{
var template = _template.SelectDataTemplate(itemBindingContext, itemsView);
var template = _template?.SelectDataTemplate(itemBindingContext, itemsView) ??
DataTemplateHelpers.DefaultContentTemplate;

var templateChanging = template != _selectedTemplate;

if (templateChanging)
if (templateChanging)
{
// Clean up any content we're still holding on to
_itemContentView.Recycle();
Expand Down
Loading