DataTemplate и сложные объекты в ListView

Последнее обновление: 23.01.2023

В прошлой теме был рассмотрен вывод в ListView простейших данных - массива строк. На самом деле в простейшем варианте ListView получает для каждого объекта списка стоковое представление и выводит его в отдельной ячейке. Однако, как правило, данные, которыми манипулируют приложения, представляют более сложные по структуре объекты, которые могут содержать множество свойств. И для организации отображения каждого объекта класс ListView определяет свойство ItemTemplate или шаблон данных. Этому свойству передаестя объект DataTemplate, который устанавливает, как объект будет отображаться в списке.

Рассмотрим, как выполнять привязку к списку объектов на примере ListView. Для создания объектов определим класс User:

public class User
{
    public string Name { get; set; } = "";
    public int Age { get; set; }
}

Теперь определим следующий класс страницы:

namespace HelloApp;

class StartPage : ContentPage
{
    public List<User> Users { get; set; }
    public StartPage()
    {
        Label header = new Label { Text = "Список пользователей", FontSize = 18 };
        // определяем данные
        Users = new List<User>
        {
            new User {Name="Tom", Age=38 },
            new User {Name = "Bob", Age= 42},
            new User {Name="Sam", Age = 28},
            new User {Name = "Alice", Age = 33}
        };
        ListView usersListView = new ListView();
        // определяем источник данных
        usersListView.ItemsSource = Users;
        // определяем шаблон данных
        usersListView.ItemTemplate = new DataTemplate(() =>
        {
            // привязка к свойству Name
            Label nameLabel = new Label { FontSize = 16 };
            nameLabel.SetBinding(Label.TextProperty, "Name");

            // привязка к свойству Age
            Label ageLabel = new Label { FontSize = 14 };
            ageLabel.SetBinding(Label.TextProperty, "Age");

            // создаем объект ViewCell.
            return new ViewCell
            {
                View = new StackLayout
                {
                    Padding = new Thickness(0, 5),
                    Orientation = StackOrientation.Vertical,
                    Children = { nameLabel, ageLabel }
                }
            };
        });
        Content = new StackLayout { Children = { header, usersListView }, Padding=7};
    }
}

С помощью свойства ItemsSource в качестве источника данных устанавливаем список объектов User.

Свойство ItemTemplate определяет, как эти данные будут отображаться. ItemTemplate представляет объект DataTemplate. Его конструктор принимает делегат Func<object>, который устанавливает, как данные будут располагаться и как будет выглядеть ячейка. Содержимое DataTemplate представляет одну из ячеек, то есть класс Cell. В данном случае это тип ViewCell, поэтому делегат возвращает объект ViewCell, который определяет структуру строки в ListView.

Внутри ViewCell внутренние элементы упавляются контейнером StackLayout, а отдельные элементы Label внутри StackLayout привязаны к отдельным свойствам объекта User.

определение шаблона данных в ListView с помощью DataTemplate и ViewCell в .NET MAUI и C#

DataTemplate в Xaml

Определение DataTemplate в коде XAML:

<?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="HelloApp.MainPage"
             xmlns:local="clr-namespace:HelloApp">
    <ContentPage.Resources>
        <ResourceDictionary>
            <x:Array x:Key="users" Type="{x:Type local:User}">
                <local:User Name="Tom" Age="38" />
                <local:User Name="Bob" Age="42" />
                <local:User Name="Sam" Age="28" />
                <local:User Name="Alice" Age="33" />
            </x:Array>
        </ResourceDictionary>
    </ContentPage.Resources>
    <StackLayout Padding="7">
        <Label Text="Список пользователей" FontSize="18" />
        <!--<Label Text="{Binding Source={Reference usersList}, Path=SelectedItem}" />-->
        <ListView ItemsSource="{StaticResource users}">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ViewCell>
                        <ViewCell.View>
                            <StackLayout>
                                <Label Text="{Binding Name}" FontSize="16" />
                                <Label Text="{Binding Age}" FontSize="14" />
                            </StackLayout>
                        </ViewCell.View>
                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </StackLayout>
</ContentPage>

Здесь практически тоже самое. Для простоты набор данных определен в виде ресурса users непосредственно в XAML, хотя также можно было бы задать список объектов User в коде C#. Для определения визуального шаблона выводимых данных применяется объект DataTemplate, в котором через элемент ViewCell устанавливается формат отображения - в виде вертикального стека элементов Label, каждый из которых привязан к одному из свойств объекта User:

<DataTemplate>
   <ViewCell>
      <ViewCell.View>
        <StackLayout>
           <Label Text="{Binding Name}" FontSize="16" />
           <Label Text="{Binding Age}" FontSize="14" />
        </StackLayout>
      </ViewCell.View>
   </ViewCell>
</DataTemplate>

Получение выделенного элемента

В этом случае может возникнуть вопрос, а как же обрабатывать выбор подобных сложных данных? В случае выше каждый элемент в ListView будет представлять объект User, поэтому свойство SelectedItem будет представлять выбранный объект User. Обращаясь к свойствам этого элемента, мы можем привязать элементы интерфейса к отдельным свойствам выделенного объекта. Например, в примере выше для XAML изменим код StackLayout:

<StackLayout Padding="7">
    <Label Text="{Binding Source={Reference usersListView}, Path=SelectedItem.Name, StringFormat='Selected: {0}'}"
            FontSize="18"/>
    <ListView x:Name="usersListView" ItemsSource="{StaticResource users}">
        <ListView.ItemTemplate>
            <DataTemplate>
                <ViewCell>
                    <ViewCell.View>
                        <StackLayout>
                            <Label Text="{Binding Name}" FontSize="16" />
                            <Label Text="{Binding Age}" FontSize="14" />
                        </StackLayout>
                    </ViewCell.View>
                </ViewCell>
            </DataTemplate>
          </ListView.ItemTemplate>
    </ListView>
</StackLayout>

Здесь текст метки привязан к свойству Name выделенного элемента:

<Label Text="{Binding Source={Reference usersListView}, Path=SelectedItem.Name, StringFormat='Selected: {0}'}"

Причем при привязке будет идти форматирование.

получение выделенного элементв в ListView из DataTemplate в .NET MAUI и C#

Если бы мы просто указали бы "SelectedItem", которая метка пыталась получить строковое представление объекта и отобразить его

<Label Text="{Binding Source={Reference usersListView}, Path=SelectedItem}"

Аналогичный пример установки привязки в коде C#:

namespace HelloApp;

class StartPage : ContentPage
{
    public List<User> Users { get; set; } = new();
    public StartPage()
    {
        // определяем данные
        Users = new List<User>
        {
            new User {Name="Tom", Age=38 },
            new User {Name = "Bob", Age= 42},
            new User {Name="Sam", Age = 28},
            new User {Name = "Alice", Age = 33}
        };
        ListView usersListView = new ListView();
        // определяем источник данных
        usersListView.ItemsSource = Users;

        // определяем шаблон данных
        usersListView.ItemTemplate = new DataTemplate(() =>
        {
            // привязка к свойству Name
            Label nameLabel = new Label { FontSize = 16 };
            nameLabel.SetBinding(Label.TextProperty, "Name");

            // привязка к свойству Age
            Label ageLabel = new Label { FontSize = 14 };
            ageLabel.SetBinding(Label.TextProperty, "Age");

            // создаем объект ViewCell.
            return new ViewCell
            {
                View = new StackLayout
                {
                    Padding = new Thickness(0, 5),
                    Orientation = StackOrientation.Vertical,
                    Children = { nameLabel, ageLabel }
                }
            };
        });
        Label header = new Label { FontSize = 18 };
        // определяем привязку к выбранному элементу
        Binding userBinding = new Binding { StringFormat = "Selected: {0}", Path="SelectedItem.Name", Source = usersListView };
        header.SetBinding(Label.TextProperty, userBinding);
        Content = new StackLayout { Children = { header, usersListView }, Padding=7};
    }
}

Обратка выбора элемента

При нажатии или выделении элемента в списке в обработчиках событий ItemTapped и ItemSelected нажатый/выделенный объект будет представлять тип объектов списка, то есть в случаях выше типа User. Например, определим в C# следующую страницу:

using Microsoft.Maui.Controls.Internals;

namespace HelloApp;

class StartPage : ContentPage
{
    public List<User> Users { get; set; } = new();
    Label selectedItemHeader = new Label { FontSize = 18 };
    Label tappedItemHeader = new Label { FontSize = 18 };
    public StartPage()
    {
        // определяем данные
        Users = new List<User>
        {
            new User {Name="Tom", Age=38 },
            new User {Name = "Bob", Age= 42},
            new User {Name="Sam", Age = 28},
            new User {Name = "Alice", Age = 33}
        };
        ListView usersListView = new ListView();
        // определяем источник данных
        usersListView.ItemsSource = Users;

        // определяем шаблон данных
        usersListView.ItemTemplate = new DataTemplate(() =>
        {
            // привязка к свойству Name
            Label nameLabel = new Label { FontSize = 16 };
            nameLabel.SetBinding(Label.TextProperty, "Name");

            // привязка к свойству Age
            Label ageLabel = new Label { FontSize = 14 };
            ageLabel.SetBinding(Label.TextProperty, "Age");

            // создаем объект ViewCell.
            return new ViewCell
            {
                View = new StackLayout
                {
                    Padding = new Thickness(0, 5),
                    Orientation = StackOrientation.Vertical,
                    Children = { nameLabel, ageLabel }
                }
            };
        });
        usersListView.ItemSelected += UsersListView_ItemSelected;
        usersListView.ItemTapped += UsersListView_ItemTapped;
        Content = new StackLayout { Children = { selectedItemHeader, tappedItemHeader, usersListView }, Padding=7};
    }

    private void UsersListView_ItemTapped(object sender, ItemTappedEventArgs e)
    {
        var tappedUser = e.Item as User;
        if (tappedUser != null) 
            tappedItemHeader.Text = $"Нажато: {tappedUser.Name}";
    }

    private void UsersListView_ItemSelected(object sender, SelectedItemChangedEventArgs e)
    {
        var selectedUser = e.SelectedItem as User;
        if (selectedUser != null)
            selectedItemHeader.Text = $"Выбрано: {selectedUser.Name}";
    }
}

В обработчиках событий мы можем привести объект, для которого сработало событие, к его изначальному типу:

// событие выделения объекта
var selectedUser = e.SelectedItem as User;
// событие нажатия объекта
var tappedUser = e.Item as User;

Для целей тестирования обработчиков события здесь определены две отдельные метки, которые выводят данные нажатого элемента:

Обработка выбора элемента в ListView в .NET MAUI и C#
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850