Get started with the .NET MAUI CollectionView—the more flexible and performant alternative to ListView that will enable your bravest scenarios of presenting lists of data.
When you think of the most common things we see in the apps we use in our day-to-day life, what do you think about? If you thought about lists, you guessed right and may also check yourself for possessing psychic abilities since you have just read my mind! The possibilities of lists on a screen are as limitless as the ones of the human brain. Be it your device’s contacts, chats, emails, endless to-do lists. 😅
And to let you even more into my world, in this blog post I am going to share one of my to-dos—introducing you to the newest, more flexible and performant component of presenting lists of data—the .NET MAUI CollectionView. 🌟
Like many other times, I have thought of an example that will be built step by step in the scope of this blog post and will naturally unveil most of the key features that the CollectionView component from Progress Telerik UI for .NET MAUI offers. Based on the commonly used lists that were mentioned at the very beginning, I suggest we recreate the start of most human beings’ usual work morning.
Scenario—a hot cup of coffee and the emails list that complements it.
I promised that we would take things slow. Assuming you already have a fresh .NET MAUI app and the Telerik.UI.for.Maui NuGet, bringing the CollectionView component will only take a single line of code (XAML or C#).
XAML:
<telerik:RadCollectionView x:Name="collectionView" />
C#:
RadCollectionView collectionView = new RadCollectionView();
And just like that, you are already a collector.
If we were to see your collection now, we would see a blank space. No worries, that is to change in the blink of an eye (almost). But for such scenarios, it is nice to know that you can benefit from Collection View’s EmptyContentTemplate to display something meaningful to the end user. It is also nice to benefit from an awesome existing example from the online documentation of the component.
Though a life with an empty inbox might be nice, it is not realistic, so let’s sneak a peek at the future collection of our email data:
The CollectionView component in Telerik UI for .NET MAUI is specifically crafted to simplify data binding, and (needless to say) it supports UI virtualization which enables faster data visualization and effortless display of items while scrolling, regardless of the dataset size.
So many words, so little code so far. Let’s prepare the data model of our emails.
public class Email : ViewModelBase
{
private string sender;
private string subject;
private DateTime received;
private string message;
private bool isUnread;
public string Sender
{
get => this.sender;
set => this.UpdateValue(ref this.sender, value);
}
public string Subject
{
get => this.subject;
set => this.UpdateValue(ref this.subject, value);
}
public DateTime Received
{
get => this.received;
set => this.UpdateValue(ref this.received, value);
}
public string Message
{
get => this.message;
set => this.UpdateValue(ref this.message, value);
}
public bool IsUnread
{
get => this.isUnread;
set => this.UpdateValue(ref this.isUnread, value);
}
}
And now create a View Model that we will pour some messages into:
public class InboxViewModel : ExampleViewModel
{
public InboxViewModel()
{
this.Messages = new ObservableCollection<Email>
{
new Email { Sender = "Internal Communications", Subject = "All Hands Confirmation", Received = DateTime.ParseExact("2024-05-15-11:47", "yyyy-MM-dd-HH:mm", CultureInfo.InvariantCulture), Message = "Thank you for registering for the All Hands meeting. If you can’t attend live, you may join the webinar by following the link in this email.", IsUnread = true },
new Email { Sender = "Jane Poe", Subject = "Social Hour", Received = DateTime.ParseExact("2024-05-15-12:11", "yyyy-MM-dd-HH:mm", CultureInfo.InvariantCulture), Message = "Hi all, if you are in the office, please join us for a social hour after the All Hands in the cafeteria. Food and drinks will be provided.", IsUnread = true },
new Email { Sender = "Fig Nelson", Subject = "Buttons Customization Example", Received = DateTime.ParseExact("2024-05-15-10:13", "yyyy-MM-dd-HH:mm", CultureInfo.InvariantCulture), Message = "Showcase the customization capabilities of all supported types of buttons present in the suite.", IsUnread = true },
new Email { Sender = "Norman Gordon", Subject = "CollectionView Configuration Example", Received = DateTime.ParseExact("2024-05-02-15:26", "yyyy-MM-dd-HH:mm", CultureInfo.InvariantCulture), Message = "Please find attached the proposals for the configuration screens for the RadCollectionView." },
new Email { Sender = "Hilary Ouse", Subject = "Chat (Conversational UI) scenarios and design", Received = DateTime.ParseExact("2023-09-25-09:15", "yyyy-MM-dd-HH:mm", CultureInfo.InvariantCulture), Message = "We have Travel Assistance and Chat Room demos. Texts, images and all other assets are can be downloaded from Figma." },
new Email { Sender = "Spruce Springclean", Subject = "CryptoTracker App", Received = DateTime.ParseExact("2024-05-15-07:02", "yyyy-MM-dd-HH:mm", CultureInfo.InvariantCulture), Message = "Hi, there are some design improvements which can be found in the XD artboard for the app. Please, review and implement them.", IsUnread = true },
new Email { Sender = "Phillip Anthropy", Subject = ".NET MAUI AI Prompt Documentation", Received = DateTime.ParseExact("2024-05-15-17:51", "yyyy-MM-dd-HH:mm", CultureInfo.InvariantCulture), Message = "Could you please update me on the status of the .NET MAUI AI Prompt documentation?", IsUnread = true },
new Email { Sender = "Azure DevOps", Subject = "PR build succeeded", Received = DateTime.ParseExact("2024-05-14-10:13", "yyyy-MM-dd-HH:mm", CultureInfo.InvariantCulture), Message = "We sent you this notification due to a default subscription. BUILD #MAUI CI SUCCEEDED." },
new Email { Sender = "Azure DevOps", Subject = "Product Backlog Item was assigned to you", Received = DateTime.ParseExact("2024-05-14-10:13", "yyyy-MM-dd-HH:mm", CultureInfo.InvariantCulture), Message = "Product Backlog Item—Change the CryptoTracker’s header so that it is consistent with iOS was assigned to you." },
new Email { Sender = "workday-donotreply", Subject = "Request Time Off—Successfully Completed", Received = DateTime.ParseExact("2024-05-14-15:04", "yyyy-MM-dd-HH:mm", CultureInfo.InvariantCulture), Message = "Your time off request has been successfully completed. Click here to view the notification details." },
new Email { Sender = "workday-donotreply", Subject = "Feedback Requested", Received = DateTime.ParseExact("2024-05-14-09:00", "yyyy-MM-dd-HH:mm", CultureInfo.InvariantCulture), Message = "You have been requested to provide feedback. Your insights and perspective are greatly appreciated.", IsUnread = true },
new Email { Sender = "Information Security Team", Subject = "You’ve completed your training", Received = DateTime.ParseExact("2024-05-14-08:46", "yyyy-MM-dd-HH:mm", CultureInfo.InvariantCulture), Message = "Thank you for completing your assigned training." },
new Email { Sender = "IT HelpDesk", Subject = "Your password is about to expire", Received = DateTime.ParseExact("2024-05-14-10:56", "yyyy-MM-dd-HH:mm", CultureInfo.InvariantCulture), Message = "Your Domain password is expiring. If you don’t update it within the given period, you will receive a prompt to change it when the old one has expired.", IsUnread = true },
new Email { Sender = "IT HelpDesk", Subject = "Planned System Upgrade", Received = DateTime.ParseExact("2024-05-14-11:02", "yyyy-MM-dd-HH:mm", CultureInfo.InvariantCulture), Message = "We would like to inform you that IT will be performing a planned system upgrade of Azure DevOps Server. During the maintenance period, all Azure DevOps Server services will be unavailable." },
new Email { Sender = "John Doe", Subject = "DevTools Monthly Series", Received = DateTime.ParseExact("2024-04-25-10:13", "yyyy-MM-dd-HH:mm", CultureInfo.InvariantCulture), Message = "Hi all, our next session for this month will be hosted next Wednesday. See you there! If you are not able to make it, do not worry—the session will be recorded." },
new Email { Sender = "Shannon Riley", Subject = "Monthly Support KPI Report", Received = DateTime.ParseExact("2024-04-25-18:24", "yyyy-MM-dd-HH:mm", CultureInfo.InvariantCulture), Message = "Hi team, here is the monthly support KPI report. Click on the KPI name to view the detailed data regarding each KPI." },
new Email { Sender = "Karren Koe", Subject = "[Design] MAUI—Control Samples—Calendar", Received = DateTime.ParseExact("2023-09-24-16:13", "yyyy-MM-dd-HH:mm", CultureInfo.InvariantCulture), Message = "Hello, I am sending over some proposals for Calendar First Look demo design. I’ll be more than happy to discuss them with you and make a decision." },
new Email { Sender = "Karren Koe", Subject = "[Design] Buttons Desktop Hover States", Received = DateTime.ParseExact("2024-05-14-17:10", "yyyy-MM-dd-HH:mm", CultureInfo.InvariantCulture), Message = "Please find attached the updates regarding the newly added hover state for the new Buttons." },
new Email { Sender = "Karren Koe", Subject = "ControlsSamples App", Received = DateTime.ParseExact("2024-04-25-17:37", "yyyy-MM-dd-HH:mm", CultureInfo.InvariantCulture), Message = "Hi guys, I am happy to announce the new ControlsSamples App design! If you have any questions or concerns, please don’t hesitate to contact me." },
new Email { Sender = "Figma", Subject = "4 new comments in MAUI Designs", Received = DateTime.ParseExact("2024-04-25-08:35", "yyyy-MM-dd-HH:mm", CultureInfo.InvariantCulture), Message = "There are 4 new comments from Karren. Click on the link to view the comments in the file." },
};
}
public ObservableCollection<Email> Messages { get; set; }
}
One essential thing left—to populate our CollectionView with these Messages using the ItemsSource property:
<telerik:RadCollectionView x:Name="collectionView" ItemsSource="{Binding Messages}" />
The above surely will populate the collection view with items, but they will not get the appearance from the sneak peek screenshot I provided earlier automatically. Follow me to the next section where we will define an ItemTemplate corresponding to the desired visual appearance.
We want to have a fancier appearance describing our emails and that is why we will define a custom ItemTemplate:
<telerik:RadCollectionView.ItemTemplate>
<DataTemplate>
<Grid RowDefinitions="Auto, Auto, Auto"
ColumnDefinitions="20, *, Auto"
Padding="8, 8, 12, 8">
<Label IsVisible="{Binding IsUnread}"
FontFamily="TelerikFontExamples"
FontSize="16"
Text=""
TextColor="#0285F0"
Margin="2, 0" />
<telerik:RadHighlightLabel Grid.Column="1"
FontSize="16"
UnformattedText="{Binding Sender}"
HighlightText="{Binding Source={x:Reference searchEntry}, Path=Text}"
HighlightTextColor="#FF6600"
TextColor="Black"
FontAttributes="{Binding IsUnread, Converter={StaticResource IsEmailUnreadToFontAttributesConverter}}"
HorizontalOptions="Start"
VerticalOptions="Center" />
<Label Grid.Column="2"
Text="{Binding Received, Converter={StaticResource FormattedDateTimeConverter}}"
TextColor="{Binding IsUnread, Converter={StaticResource IsEmailUnreadToPlaceholderTextColorConverter}}"
FontAttributes="{Binding IsUnread, Converter={StaticResource IsEmailUnreadToFontAttributesConverter}}"
FontSize="12"
HorizontalOptions="End"
VerticalOptions="Center" />
<telerik:RadHighlightLabel Grid.Column="1"
Grid.Row="1"
Grid.ColumnSpan="2"
FontSize="14"
UnformattedText="{Binding Subject}"
HighlightText="{Binding Source={x:Reference searchEntry}, Path=Text}"
HighlightTextColor="#FF6600"
TextColor="{Binding IsUnread, Converter={StaticResource IsEmailUnreadToTextColorConverter}}"
FontAttributes="{Binding IsUnread, Converter={StaticResource IsEmailUnreadToFontAttributesConverter}}" />
<telerik:RadHighlightLabel Grid.Column="1"
Grid.Row="2"
Grid.ColumnSpan="2"
FontSize="14"
MaxLines="1"
LineBreakMode="TailTruncation"
UnformattedText="{Binding Message}"
HighlightText="{Binding Source={x:Reference searchEntry}, Path=Text}"
HighlightTextColor="#FF6600"
Opacity="0.6" />
</Grid>
</DataTemplate>
</telerik:RadCollectionView.ItemTemplate>
I am sparing you the Converters used in this template, since they are easy-peasy and self-explanatory and, mostly, to not flood you with code. Instead, I can show you our progress:
Seems close to the original, yet some essential things are missing. If you look more carefully at the data, you will notice that the groups are missing, and—more importantly—the order is messed up. And currently, I can guarantee that selecting an unread email will not result in marking it as read. 🤭
I like things ordered, and when talking about emails, it is somewhat expected for them to be sorted by date from newest to oldest, of course. And if possible, it would be nice of them to appear grouped so that the emails from today and yesterday are separated from the rest. How do we do that? Let me show you.
this.collectionView.GroupDescriptors.Add(new Telerik.Maui.Controls.Data.DelegateGroupDescriptor
{
KeyLookup = new EmailDateKeyLookup(),
SortOrder = Telerik.Maui.Controls.Data.SortOrder.Descending
});
I chose to use the DelegateGroupDescriptor since it allows grouping the data by a custom key. We could have just grouped it by date—the Received
property using a PropertyGroupDescriptor—but our scenario is different.
The DelegateGroupDescriptor
requires a KeyLookup
, which in our case looks like this:
internal class EmailDateKeyLookup : Telerik.Maui.Controls.Data.IKeyLookup
{
private static DateTime Today = DateTime.ParseExact("2024-05-15-00:00", "yyyy-MM-dd-HH:mm", System.Globalization.CultureInfo.InvariantCulture).Date;
private static DateTime Yesterday = Today.AddDays(-1);
private static DateTime Older = Yesterday.AddDays(-1);
public object GetKey(object arg)
{
var email = arg as Email;
var receivedDate = email.Received.Date;
if (receivedDate == Today)
{
return Today;
}
if (receivedDate == Yesterday)
{
return Yesterday;
}
return Older;
}
}
Now, let’s sort the emails by the date they are received. This time, we are going to use a PropertySortDescriptor.
<telerik:RadCollectionView.SortDescriptors>
<telerik:PropertySortDescriptor PropertyName="Received" SortOrder="Descending" />
</telerik:RadCollectionView.SortDescriptors>
And there is one more thing I did for the group view’s header to display the custom strings we want—I have overridden the GroupHeaderTemplate
:
<telerik:RadCollectionView.GroupHeaderTemplate>
<DataTemplate>
<Label TextColor="#000000"
Text="{Binding Key, Converter={StaticResource DateTimeToDateCategoryConverter}}"
FontAttributes="Bold"
FontSize="14"
VerticalTextAlignment="Center" />
</DataTemplate>
</telerik:RadCollectionView.GroupHeaderTemplate>
Phew, that is better! Yet still a little incomplete because what is a list of emails without a way to filter them? We should add a search.
Straight to business—adding an instance of RadEntry
:
<Grid RowDefinitions="Auto, *"
RowSpacing="20">
<telerik:RadEntry x:Name="searchEntry"
Placeholder="Search emails..."
ReserveSpaceForErrorView="False" />
<telerik:RadCollectionView x:Name="collectionView"
ItemsSource="{Binding Messages}" ... />
</Grid>
After that adding a DelegateFilterDescriptor
:
private Telerik.Maui.Controls.Data.DelegateFilterDescriptor emailFilterDescriptor;
And instantiating it in the constructor of the code-behind of our sample:
this.emailFilterDescriptor = new Telerik.Maui.Controls.Data.DelegateFilterDescriptor();
We will need to prepare our custom filter as well and I suggest we make it in a way that it enables us to search by an email’s sender, subject and—of course—content:
internal class SearchFilter : Telerik.Maui.Controls.Data.IFilter
{
private string searchText;
public SearchFilter(string text)
{
this.searchText = text;
}
public bool PassesFilter(object item)
{
if (item is Email email
&& (email.Sender.Contains(this.searchText, StringComparison.InvariantCultureIgnoreCase)
|| email.Sender.Contains(this.searchText, StringComparison.InvariantCultureIgnoreCase)
|| email.Message.Contains(this.searchText, StringComparison.InvariantCultureIgnoreCase)))
{
return true;
}
else
{
return false;
}
}
}
We are now ready to add the filter to our CollectionView when the text of the search entry changes:
this.searchEntry.TextChanged += (s, e) =>
{
this.emailFilterDescriptor.Filter = new SearchFilter(e.NewTextValue);
if (!this.collectionView.FilterDescriptors.Contains(this.emailFilterDescriptor))
{
this.collectionView.FilterDescriptors.Add(this.emailFilterDescriptor);
}
};
What do you say if we switch the platform before checking the result? Ladies and gentlemen, please welcome the filtered WinUI list to our stage:
I love it! 💟
Isn’t it time to check if we can “read” an unread email?
What is a collection without the possibility to pick an item from it? A total must for a list. The .NET MAUI CollectionView provides three selection modes allowing you to manipulate the selection type, all controlled by the SelectionMode property. Our example does not need anything more than its default value—Single. For other scenarios, None or Multiple might be more suitable. 🤷
Now, back to the unread emails. Our goal is clear—marking an unread email as read once we “go away” from it and select another one.
this.collectionView.SelectionChanged += (s, e) =>
{
if (e.RemovedItems.Count() > 0 && e.RemovedItems.First() is Email email && email.IsUnread)
{
email.IsUnread = false;
}
};
From what you have seen and read until this section, the .NET MAUI CollectionView is super flexible and can easily be tailored to meet your needs.
Regarding visual appearance, there are several more things that have not been mentioned so far and they refer to the items and groups.
In our example, we took advantage of the ItemTemplate but there is also the ItemViewStyle, which can be used to further tune in the item’s colors, offsets, visual states, etc. What if your scenario requires applying different styles to each item depending on a specific condition? No worries—ItemViewStyleSelector will come in handy.
Just like items, we used a custom GroupHeaderTemplate to adjust the content of the group view to our liking. To further customize this group view, we might also use the GroupViewStyle—it exposes the ExpandCollapseIndicatorStyle property that enables styling the default chevron Label used to expand/collapse a group.
Scenarios requiring conditional styling of collection view groups are also covered—the GroupViewStyleSelector is the way to go for such.
Time to come clean and confess that I used the ExpandCollapseIndicatorStyle to override the padding of it to be the same for all platforms just for the sake of our example:
<telerik:RadCollectionView.GroupViewStyle>
<Style TargetType="telerik:RadCollectionViewGroupView">
<Setter Property="ExpandCollapseIndicatorStyle">
<Style TargetType="Label">
<Setter Property="Padding" Value="8, 0" />
</Style>
</Setter>
</Style>
</telerik:RadCollectionView.GroupViewStyle>
Did we miss something? Yes, we have not checked our emails on mobile!
There is a lot more to explore and even more is coming in the next releases, so stay tuned. Catch up with the full features list of the .NET MAUI CollectionView in its online documentation.
It is no coincidence that the .NET MAUI CollectionView is one of the brightest stars of the latest Telerik UI for .NET MAUI release, but there are a lot more in the Progress galaxy, so be sure to get the bits now and explore at your own pace.
If by some chance you are a new explorer you just need to sign up for our free 30-day trial, which gives you access to the components as well as our legendary technical support at zero cost.
Once you plug the .NET MAUI CollectionView in your MAUI app, don’t forget to let us know what impression it made. Be sure to share your opinion and ideas either in the comment section below or by visiting the Telerik UI for .NET MAUI Feedback Portal.
A huge thanks from me to you for reading this blog post, and happy exploring—you know you lead the way! ✨
Viktoria is a Senior Front-end Developer with 5+ years of experience designing and building modern UI for desktop and mobile platforms on Microsoft/Telerik technology stacks. Her spare time is dedicated to friends and family and discovering new pieces of the world. Find her on Twitter and LinkedIn.