Nesting Vertical and Horizontal RadListViews
Environment
Product Version | 6.0.0 |
Product | ListView for MAUI |
Description
This how-to article describes how you can use two RadListView
controls together without running into problems with nesting multiple ScrollView
elements together.
The example code in this article is going to present a Kanban-like display where the top-level RadListView
scrolls horizontally, while the inner RadListView
scrolls vertically. As a bonus feature, the example also demonstrates how you can programmatically access that nested ListView and programmatically scroll its items from the top-level page.
Solution
The key takeaway in this approach is that the inner/nested ListView inside the parent's ItemTemplate
has a different scrolling direction. This avoids two ScrollViews using the same scrolling direction, which leads to problems by breaking UI virtualization and significant performance and responsiveness issues.
-
Ensure you have the
telerik
namespace in your XAML file:xmlns:telerik="http://schemas.telerik.com/2022/xaml/maui" ```
- Create the needed business objects. For this example, you'll be using a hierarchical object structure, where a parent
KanbanItem
type holds a collection ofKanbanSubItem
children:
using System.Collections.ObjectModel; using Telerik.Maui.Controls.Compatibility.DataControls; namespace HorizontalAndVerticalDemo; public class KanbanSubItem { public string Title { get; set; } public DateTime Deadline { get; set; } } public class KanbanItem { public string WeekName { get; set; } public ObservableCollection<KanbanSubItem> SubItems { get; set; } public RadListView ListViewHost {get; set; } public void ScrollToSubItem(KanbanSubItem item) { if(ListViewHost == null || item == null) return; ListViewHost?.ScrollItemIntoView(item); } } ```
- Create the needed business objects. For this example, you'll be using a hierarchical object structure, where a parent
-
Create a view model class. For this example, you'll create a
MainViewModel
type, and generate sample data items in the constructor.using System.Collections.ObjectModel; namespace HorizontalAndVerticalDemo; public class MainViewModel { public MainViewModel() { var data = new ObservableCollection<KanbanItem>(); for (int j = 1; j < 5; j++) { var item = new KanbanItem(); item.WeekName = $"Week {j}"; item.SubItems = new ObservableCollection<KanbanSubItem>(); for (int i = 1; i < 31; i++) { item.SubItems.Add(new KanbanSubItem {Title = $"Job {i}", Deadline = DateTime.Now.AddDays(i * j)}); } data.Add(item); } Items = data; } public ObservableCollection<KanbanItem> Items { get; set; } } ```
- On the XAML page, you'll define two
RadListView
controls with the desired design. The important considerations are: - The parent
RadListView
uses aListViewLinearLayout
withOrientation="Horizontal"
. - The
RadListView
in theItemTemplate
uses aListViewLinearLayout
withOrientation="Vertical"
. - A
Button
to demonstrate programmatic scrolling.
<?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" xmlns:telerik="http://schemas.telerik.com/2022/xaml/maui" xmlns:local="clr-namespace:HorizontalAndVerticalDemo" x:Class="HorizontalAndVerticalDemo.MainPage"> <Grid> <telerik:RadListView x:Name="listView" ItemsSource="{Binding Items}"> <telerik:RadListView.ItemTemplate> <DataTemplate x:DataType="local:KanbanItem"> <telerik:ListViewTemplateCell> <Grid RowDefinitions="Auto, *" WidthRequest="200" RowSpacing="10" Padding="10"> <Label Text="{Binding WeekName}" Grid.Row="0" /> <telerik:RadListView x:Name="InnerListView" ItemsSource="{Binding SubItems}" BindingContextChanged="OnListViewBindingContextChanged" Grid.Row="1"> <telerik:RadListView.ItemTemplate> <DataTemplate x:DataType="local:KanbanSubItem"> <telerik:ListViewTemplateCell> <VerticalStackLayout Padding="10" Spacing="5"> <Label Text="{Binding Title}" FontAttributes="Bold" /> <Label Text="{Binding Deadline, StringFormat='{0:g}'}" /> </VerticalStackLayout> </telerik:ListViewTemplateCell> </DataTemplate> </telerik:RadListView.ItemTemplate> <telerik:RadListView.LayoutDefinition> <telerik:ListViewLinearLayout VerticalItemSpacing="5" Orientation="Vertical" /> </telerik:RadListView.LayoutDefinition> </telerik:RadListView> </Grid> </telerik:ListViewTemplateCell> </DataTemplate> </telerik:RadListView.ItemTemplate> <telerik:RadListView.LayoutDefinition> <telerik:ListViewLinearLayout HorizontalItemSpacing="20" Orientation="Horizontal" /> </telerik:RadListView.LayoutDefinition> </telerik:RadListView> <Button Text="Scroll Week 3 to SubItem #25" Clicked="Button_OnClicked" HorizontalOptions="End" VerticalOptions="Center" Margin="10" /> </Grid> </ContentPage> ```
Notice that you're subscribing to the inner ListView's
BindingContextChanged
event. This is used to set theKanbanItem
'sListViewHost
property, which is what enables programmatical scrolling of the inner list. In a real-world project, you will want to set the with aWeakReference
to avoid memory leaks, or a better approach is to wrap theDataTemplate
contents into a custom control where you have more control over the lifecycle. - On the XAML page, you'll define two
-
Finally, in the view's code behind, you'll:
- Set the view's
BindingContext
to a new instance ofMainViewModel
. - Define the
OnListViewBindingContextChanged
event handler to get a reference to the innerRadListView
. - Define the
Button_OnClicked
event handler to demonstrate programmatic scrolling of a subitem into view.
using Telerik.Maui.Controls.Compatibility.DataControls; namespace HorizontalAndVerticalDemo; public partial class MainPage : ContentPage { public MainPage() { InitializeComponent(); BindingContext = new MainViewModel(); } private void OnListViewBindingContextChanged(object sender, EventArgs e) { if (sender is RadListView { BindingContext: KanbanItem item } lv) { // Dynamically gets a reference to the view element for programmatic scrolling when the concrete UI is created. // The BindingContextChanged event fires when the RadListView's UI virtualization mechanism recycles the container during initialization and any scrolling. item.ListViewHost = lv; } } private void Button_OnClicked(object sender, EventArgs e) { try { if (BindingContext is not MainViewModel mainViewModel) return; // To demonstrate programmatic scrolling of any column's inner list, we'll scroll the 3rd column's vertical list to item 25 var week3 = mainViewModel.Items[2]; var subItem25 = week3.SubItems[24]; // Scroll! week3.ScrollToSubItem(subItem25); } catch (Exception exception) { Console.WriteLine(exception); } } } ```
Screenshots
Here are three screenshots of the design at runtime.
- At launch, we see the parent horizontal items (which appear as columns) and each's inner vertical items.
- Here are some callouts to help mentally visualize the layout.
- Finally, here's what happens after clicking the button that will scroll the 3rd horizontal item's inner list to the 25th item.
- Set the view's