Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CollectionView with EmptyView and EmptyViewTemplate for Data template selector throws an exception #25418

Merged
merged 16 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public EmptyViewGallery()
new EmptyViewTemplateGallery(), Navigation),
GalleryBuilder.NavButton("EmptyView (Swap EmptyView)", () =>
new EmptyViewSwapGallery(), Navigation),
GalleryBuilder.NavButton("EmptyView (Data Template Selector)", () =>
new EmptyViewWithDataTemplateSelector(), Navigation),
GalleryBuilder.NavButton("EmptyView (load simulation)", () =>
new EmptyViewLoadSimulateGallery(), Navigation),
GalleryBuilder.NavButton("EmptyView RTL", () =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Maui.Controls.Sample.Pages.CollectionViewGalleries.EmptyViewGalleries.EmptyViewWithDataTemplateSelector">
<ContentPage.Resources>
<DataTemplate x:Key="AdvancedTemplate">
<StackLayout>
<Label Text="No results matched your filter."
Margin="10,25,10,10"
FontAttributes="Bold"
FontSize="18"
HorizontalOptions="Fill"
HorizontalTextAlignment="Center"/>
<Label Text="Try a broader filter?"
FontAttributes="Italic"
FontSize="12"
HorizontalOptions="Fill"
HorizontalTextAlignment="Center"/>
</StackLayout>
</DataTemplate>

<DataTemplate x:Key="BasicTemplate">
<Label Text="No items to display."
Margin="10,25,10,10"
FontAttributes="Bold"
FontSize="18"
HorizontalOptions="Fill"
HorizontalTextAlignment="Center"/>
</DataTemplate>
</ContentPage.Resources>
<Grid Margin="20" RowDefinitions="Auto, Auto, *">
<StackLayout Grid.Row="0">
<Label Text="1. The test passes if you are able to filter below items by search term." />
<Label Text="2. The test passes if the filtering 'Xamarin'(no matched results), the string 'No items to display.' will be displayed." />
<Label Text="3. The test passes if the filtering operation yields no data(except Xamarin), the strings 'No results matched your filter.' and 'Try a broader filter?' will be displayed." />
</StackLayout>
<SearchBar Grid.Row="1"
x:Name="searchBar"
AutomationId="SearchBar"
SearchCommand="{Binding FilterCommand}"
SearchCommandParameter="{Binding Source={x:Reference searchBar}, Path=Text}"
Placeholder="Filter" />
<CollectionView Grid.Row="2"
x:Name="collectionView"
AutomationId="Success"
ItemsSource="{Binding Monkeys}"
EmptyView="{Binding Source={x:Reference searchBar}, Path=Text}">
<CollectionView.ItemTemplate>
<DataTemplate>
<Grid Padding="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label Grid.Column="1"
Text="{Binding Name}"
FontAttributes="Bold" />
<Label Grid.Row="1"
Grid.Column="1"
Text="{Binding Location}"
FontAttributes="Italic"
VerticalOptions="End" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</Grid>
</ContentPage>
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#nullable enable
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Xaml;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Windows.Input;

namespace Maui.Controls.Sample.Pages.CollectionViewGalleries.EmptyViewGalleries
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class EmptyViewWithDataTemplateSelector : ContentPage
{
public EmptyViewWithDataTemplateSelector()
{
InitializeComponent();
var emptyViewTemplateSelector = new SearchTermDataTemplateSelector
{
DefaultTemplate = (DataTemplate)Resources["AdvancedTemplate"],
OtherTemplate = (DataTemplate)Resources["BasicTemplate"]
};
collectionView.EmptyViewTemplate = emptyViewTemplateSelector;
BindingContext = new EmptyViewWithDataTemplateSelectorViewModel();
}

public class Monkey
{
public string? Name { get; set; }
public string? Location { get; set; }
public string? Details { get; set; }
}

public class SearchTermDataTemplateSelector : DataTemplateSelector
{
public DataTemplate? DefaultTemplate { get; set; }
public DataTemplate? OtherTemplate { get; set; }

protected override DataTemplate? OnSelectTemplate(object item, BindableObject container)
{
string query = (string)item;
return query.Equals("xamarin", StringComparison.OrdinalIgnoreCase) ? OtherTemplate : DefaultTemplate;
}
}

internal class EmptyViewWithDataTemplateSelectorViewModel
{
public ObservableCollection<Monkey> Monkeys { get; } = new();
public ICommand FilterCommand => new Command<string>(FilterItems);

public EmptyViewWithDataTemplateSelectorViewModel()
{
// Directly populate the ObservableCollection
Monkeys.Add(new Monkey
{
Name = "Baboon",
Location = "Africa & Asia",
Details = "Baboons are African and Arabian Old World monkeys belonging to the genus Papio, part of the subfamily Cercopithecinae."
});
}

private void FilterItems(string filter)
{
var filteredItems = Monkeys.Where(monkey => monkey.Name?.Contains(filter, StringComparison.OrdinalIgnoreCase) ?? false).ToList();
Monkeys.Clear();
foreach (var monkey in filteredItems)
{
Monkeys.Add(monkey);
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -567,7 +567,7 @@ protected virtual void HandleFormsElementMeasureInvalidated(VisualElement formsE
internal void UpdateView(object view, DataTemplate viewTemplate, ref UIView uiView, ref VisualElement formsElement)
{
// Is view set on the ItemsView?
if (view is null && viewTemplate is null)
if (view is null && (viewTemplate is null || viewTemplate is DataTemplateSelector))
Copy link
Contributor

@jsuarezruiz jsuarezruiz Nov 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the PR description:

"The exception occurred because the RealizeView method in the ItemsViewController file was attempting to create and render a view when both EmptyView and EmptyViewTemplate were defined in the CollectionView, with a DataTemplateSelector for the EmptyViewTemplate. The original code did not account for the scenario where the viewTemplate was a DataTemplateSelector."

In the case of using a DataTemplateSelector, here

viewTemplate = viewTemplate.SelectDataTemplate(view, itemsView);

should select the correct template, and then get the content to create the handler and the PlatformView.

Is the SelectTemplate method returning null?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jsuarezruiz ,
In this case, the SelectTemplate method itself isn't returning null. Instead, the exception occurs immediately afterward, when the selected viewTemplate is used in CreateContent. This happens because, on the initial render, there's no template available to select, resulting in viewTemplate being null. This leads to an exception when CreateContent is called with an empty or unassigned DataTemplateSelector.

Let me know if further clarification is needed!

{
if (formsElement != null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ protected virtual CGRect DetermineEmptyViewFrame()
internal void UpdateView(object view, DataTemplate viewTemplate, ref UIView uiView, ref VisualElement formsElement)
{
// Is view set on the ItemsView?
if (view == null && viewTemplate is null)
if (view is null && (viewTemplate is null || viewTemplate is DataTemplateSelector))
{
if (formsElement != null)
{
Expand Down
76 changes: 76 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue25224.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Maui.Controls.Sample.Issues.Issue25224"
xmlns:local="clr-namespace:Maui.Controls.Sample.Issues"
x:Name="ThisMainPage"
Title="Main Page">

<ContentPage.Resources>
<DataTemplate x:Key="AdvancedTemplate">
<StackLayout>
<Label Text="No results matched your filter."
Margin="10,25,10,10"
FontAttributes="Bold"
FontSize="18"
HorizontalOptions="Fill"
HorizontalTextAlignment="Center"/>
<Label Text="Try a broader filter?"
FontAttributes="Italic"
FontSize="12"
HorizontalOptions="Fill"
HorizontalTextAlignment="Center"/>
</StackLayout>
</DataTemplate>

<DataTemplate x:Key="BasicTemplate">
<Label Text="No items to display."
Margin="10,25,10,10"
FontAttributes="Bold"
FontSize="18"
HorizontalOptions="Fill"
HorizontalTextAlignment="Center"/>
</DataTemplate>
</ContentPage.Resources>
<Grid Margin="20" RowDefinitions="Auto, Auto, *">
<StackLayout Grid.Row="0">
<Label Text="1. The test passes if you are able to filter below items by search term." />
<Label Text="2. The test passes if the filtering 'Xamarin'(no matched results), the string 'No items to display.' will be displayed." />
<Label Text="3. The test passes if the filtering operation yields no data(except Xamarin), the strings 'No results matched your filter.' and 'Try a broader filter?' will be displayed." />
</StackLayout>
<SearchBar Grid.Row="1"
x:Name="searchBar"
AutomationId="SearchBar"
SearchCommand="{Binding FilterCommand}"
SearchCommandParameter="{Binding Source={x:Reference searchBar}, Path=Text}"
Placeholder="Filter" />
<CollectionView Grid.Row="2"
x:Name="collectionView"
AutomationId="Success"
ItemsSource="{Binding Monkeys}"
EmptyView="{Binding Source={x:Reference searchBar}, Path=Text}">
<CollectionView.ItemTemplate>
<DataTemplate>
<Grid Padding="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label Grid.Column="1"
Text="{Binding Name}"
FontAttributes="Bold" />
<Label Grid.Row="1"
Grid.Column="1"
Text="{Binding Location}"
FontAttributes="Italic"
VerticalOptions="End" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</Grid>
</ContentPage>
64 changes: 64 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue25224.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#nullable enable
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;

namespace Maui.Controls.Sample.Issues
{
[XamlCompilation(XamlCompilationOptions.Compile)]
[Issue(IssueTracker.Github, 25224, "CollectionView - EmptyView with EmptyViewTemplate for Data template selector page throws an exception", PlatformAffected.iOS)]
public partial class Issue25224: ContentPage
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The build is failing:

D:\a\_work\1\s\src\Controls\samples\Controls.Sample\Pages\Controls\CollectionViewGalleries\EmptyViewGalleries\EmptyViewWithDataTemplateSelector.xaml.cs(58,19): error CS8618: Non-nullable property 'Details' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. [D:\a\_work\1\s\src\Controls\samples\Controls.Sample\Maui.Controls.Sample.csproj::TargetFramework=net9.0-windows10.0.19041.0]
D:\a\_work\1\s\src\Controls\samples\Controls.Sample\Pages\Controls\CollectionViewGalleries\EmptyViewGalleries\EmptyViewWithDataTemplateSelector.xaml.cs(30,24): error CS8618: Non-nullable property 'DefaultTemplate' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. [D:\a\_work\1\s\src\Controls\samples\Controls.Sample\Maui.Controls.Sample.csproj::TargetFramework=net9.0-windows10.0.19041.0]
D:\a\_work\1\s\src\Controls\samples\Controls.Sample\Pages\Controls\CollectionViewGalleries\EmptyViewGalleries\EmptyViewWithDataTemplateSelector.xaml.cs(31,24): error CS8618: Non-nullable property 'OtherTemplate' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. [D:\a\_work\1\s\src\Controls\samples\Controls.Sample\Maui.Controls.Sample.csproj::TargetFramework=net9.0-windows10.0.19041.0]
D:\a\_work\1\s\src\Controls\samples\Controls.Sample\Pages\Controls\CollectionViewGalleries\EmptyViewGalleries\EmptyViewWithDataTemplateSelector.xaml.cs(93,45): error CS8612: Nullability of reference types in type of 'event PropertyChangedEventHandler EmptyViewWithDataTemplateSelectorViewModel.PropertyChanged' doesn't match implicitly implemented member 'event PropertyChangedEventHandler? INotifyPropertyChanged.PropertyChanged'. [D:\a\_work\1\s\src\Controls\samples\Controls.Sample\Maui.Controls.Sample.csproj::TargetFramework=net9.0-windows10.0.20348.0]
D:\a\_work\1\s\src\Controls\samples\Controls.Sample\Pages\Controls\CollectionViewGalleries\EmptyViewGalleries\EmptyViewWithDataTemplateSelector.xaml.cs(95,78): error CS8625: Cannot convert null literal to non-nullable reference type. [D:\a\_work\1\s\src\Controls\samples\Controls.Sample\Maui.Controls.Sample.csproj::TargetFramework=net9.0-windows10.0.20348.0]
D:\a\_work\1\s\src\Controls\samples\Controls.Sample\Pages\Controls\CollectionViewGalleries\EmptyViewGalleries\EmptyViewWithDataTemplateSelector.xaml.cs(46,11): error CS8618: Non-nullable property 'Monkeys' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. [D:\a\_work\1\s\src\Controls\samples\Controls.Sample\Maui.Controls.Sample.csproj::TargetFramework=net9.0-windows10.0.20348.0]
D:\a\_work\1\s\src\Controls\samples\Controls.Sample\Pages\Controls\CollectionViewGalleries\EmptyViewGalleries\EmptyViewWithDataTemplateSelector.xaml.cs(46,11): error CS8618: Non-nullable event 'PropertyChanged' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the event as nullable. [D:\a\_work\1\s\src\Controls\samples\Controls.Sample\Maui.Controls.Sample.csproj::TargetFramework=net9.0-windows10.0.20348.0]
D:\a\_work\1\s\src\Controls\samples\Controls.Sample\Pages\Controls\CollectionViewGalleries\EmptyViewGalleries\EmptyViewWithDataTemplateSelector.xaml.cs(54,19): error CS8618: Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. [D:\a\_work\1\s\src\Controls\samples\Controls.Sample\Maui.Controls.Sample.csproj::TargetFramework=net9.0-windows10.0.20348.0]
D:\a\_work\1\s\src\Controls\samples\Controls.Sample\Pages\Controls\CollectionViewGalleries\EmptyViewGalleries\EmptyViewWithDataTemplateSelector.xaml.cs(56,19): error CS8618: Non-nullable property 'Location' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. [D:\a\_work\1\s\src\Controls\samples\Controls.Sample\Maui.Controls.Sample.csproj::TargetFramework=net9.0-windows10.0.20348.0]
D:\a\_work\1\s\src\Controls\samples\Controls.Sample\Pages\Controls\CollectionViewGalleries\EmptyViewGalleries\EmptyViewWithDataTemplateSelector.xaml.cs(58,19): error CS8618: Non-nullable property 'Details' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. [D:\a\_work\1\s\src\Controls\samples\Controls.Sample\Maui.Controls.Sample.csproj::TargetFramework=net9.0-windows10.0.20348.0]
D:\a\_work\1\s\src\Controls\samples\Controls.Sample\Pages\Controls\CollectionViewGalleries\EmptyViewGalleries\EmptyViewWithDataTemplateSelector.xaml.cs(30,24): error CS8618: Non-nullable property 'DefaultTemplate' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. [D:\a\_work\1\s\src\Controls\samples\Controls.Sample\Maui.Controls.Sample.csproj::TargetFramework=net9.0-windows10.0.20348.0]
D:\a\_work\1\s\src\Controls\samples\Controls.Sample\Pages\Controls\CollectionViewGalleries\EmptyViewGalleries\EmptyViewWithDataTemplateSelector.xaml.cs(31,24): error CS8618: Non-nullable property 'OtherTemplate' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. [D:\a\_work\1\s\src\Controls\samples\Controls.Sample\Maui.Controls.Sample.csproj::TargetFramework=net9.0-windows10.0.20348.0]
    6126 Warning(s)
    63 Error(s)

Could you take a look?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jsuarezruiz , I’ve resolved the build issues mentioned above. Let me know if there’s anything else needed!

{
public Issue25224()
{
InitializeComponent();
var emptyViewTemplateSelector = new SearchTermDataTemplateSelector
{
DefaultTemplate = (DataTemplate)Resources["AdvancedTemplate"],
OtherTemplate = (DataTemplate)Resources["BasicTemplate"]
};
collectionView.EmptyViewTemplate = emptyViewTemplateSelector;
BindingContext = new Issue25224ViewModel();
}

public class SearchTermDataTemplateSelector : DataTemplateSelector
{
public DataTemplate? DefaultTemplate { get; set; }
public DataTemplate? OtherTemplate { get; set; }

protected override DataTemplate? OnSelectTemplate(object item, BindableObject container)
{
string query = (string)item;
return query.Equals("xamarin", StringComparison.OrdinalIgnoreCase) ? OtherTemplate : DefaultTemplate;
}
}

internal class Issue25224ViewModel
{
public ObservableCollection<Monkey> Monkeys { get; } = new();
public ICommand FilterCommand => new Command<string>(FilterItems);

public Issue25224ViewModel()
{
// Directly populate the ObservableCollection
Monkeys.Add(new Monkey
{
Name = "Baboon",
Location = "Africa & Asia",
Details = "Baboons are African and Arabian Old World monkeys belonging to the genus Papio, part of the subfamily Cercopithecinae."
});
}

private void FilterItems(string filter)
{
var filteredItems = Monkeys.Where(monkey => monkey.Name?.Contains(filter, StringComparison.OrdinalIgnoreCase) ?? false).ToList();
Monkeys.Clear();
foreach (var monkey in filteredItems)
{
Monkeys.Add(monkey);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Issues
{
internal class Issue25224 : _IssuesUITest
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to include the sample from the test here https://github.com/dotnet/maui/tree/main/src/Controls/samples/Controls.Sample/Pages/Controls/CollectionViewGalleries/EmptyViewGalleries
In this case, we will have samples to validate all the EmptyView scenarios (using a Label, View, Template, TemplateSelector).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jsuarezruiz ,
Thanks! I’ve added the tests to EmptyViewGalleries for all EmptyView scenarios as requested. Let me know if there’s anything else needed!

{
public Issue25224(TestDevice device) : base(device) { }

public override string Issue => "CollectionView - EmptyView with EmptyViewTemplate for Data template selector page throws an exception";

[Test]
[Category(UITestCategories.CollectionView)]
public void CollectionViewEmptyViewDefaultTemplateShouldNotCrashOnDisplay()
{
App.WaitForElement("SearchBar");
App.EnterText("SearchBar", "test");
App.PressEnter();
// On UI test, pressing Enter twice performs filtering and shows the empty view.
// This code is necessary due to the app's behavior on UI test, which differs from simple samples.
App.PressEnter();
App.WaitForElement("Success");
}

[Test]
[Category(UITestCategories.CollectionView)]
public void CollectionViewEmptyViewOtherTemplateShouldNotCrashOnDisplay()
{
App.WaitForElement("SearchBar");
App.EnterText("SearchBar", "xamarin");
App.PressEnter();
App.WaitForElement("Success");
}
}
}
Loading