Start Debugging
2026-05-07 Обновлено 2026-05-07 mauixamarinxamarin-formscollectionview Edit on GitHub

Миграция высокопроизводительного Xamarin.Forms ListView на MAUI CollectionView

Пошаговая миграция с Xamarin.Forms 5.0 ListView на .NET MAUI 11 CollectionView для приложений, в которых уже выжимали максимум производительности из ListView. Рассмотрены переиспользование ячеек, виртуализация, группировка, pull-to-refresh, контекстные действия, выделение, ItemsLayout, EmptyView и подводные камни, которые встречаются в реальных приложениях.

Кратко: замените ListView на CollectionView, обёрнутый в RefreshView, если вам нужен был pull-to-refresh, замените ContextActions на SwipeView, удалите HasUnevenRows, RowHeight и CachingStrategy, поскольку виртуализация с переиспользованием ячеек теперь единственный режим в MAUI, и перепишите выделение с ItemTapped / ItemSelected на SelectionMode плюс SelectionChangedCommand. Если ваш старый ListView был настроен с CachingStrategy="RecycleElement", HasUnevenRows="True" и DataTemplateSelector, переход в основном механический. Протестировано на .NET MAUI 11.0 GA в net11.0-android35.0, net11.0-ios18.0 и net11.0-maccatalyst18.0. Рассчитывайте на один-три дня сосредоточенной работы для приложения с пятью-десятью списками, дольше, если вы активно используете группировку в стиле TableView или собственные рендеры ViewCell.

Поддержка Xamarin.Forms закончилась 1 мая 2024 года, а MAUI 11 GA вышел в ноябре 2025 года с третьим поколением исправлений CollectionView, устраняющих дёрганья на Android. Если вы откладывали миграцию, потому что производительность вашего приложения была вручную настроена под особенности ListView, такие как RecycleElementAndDataTemplate, обновляться стоит сейчас: CollectionView в MAUI 11 наконец сравнялся со старым ListView из Xamarin.Forms на Android или превзошёл его, если выключить ItemSizingStrategy.MeasureAllItems. Это руководство для такой аудитории.

Зачем мигрировать сейчас

Что ломается

ОбластьXamarin.Forms ListViewMAUI CollectionViewСерьёзность
Стратегия кешированияCachingStrategy="RetainElement" или RecycleElementВсегда переиспользует. Свойство отсутствует.средняя
Высота строкиHasUnevenRows, RowHeightItemSizingStrategy="MeasureAllItems" или MeasureFirstItemсредняя
Pull to refreshIsPullToRefreshEnabled, RefreshCommand на ListViewОборачивайте в RefreshViewвысокая
Тип ячейкиViewCell, TextCell, ImageCellОбычный DataTemplate с любым корнем макетавысокая
Обработка нажатийItemTapped, ItemSelectedSelectionMode, SelectionChanged, SelectionChangedCommandвысокая
Свайп-действияContextActionsSwipeViewвысокая
РазделителиSeparatorVisibility, SeparatorColorНет. Добавьте BoxView в шаблон.низкая
Заголовки / нижние колонтитулыHeader, Footer (объект или шаблон)Те же имена, но только как шаблоны / представлениянизкая
ГруппировкаIsGroupingEnabled, GroupHeaderTemplateIsGrouped, GroupHeaderTemplate, GroupFooterTemplateсредняя
Прокрутка к элементуScrollTo(item, ScrollToPosition)ScrollTo(item, position, animate)низкая
Пустое состояниеРучная соседняя меткаEmptyView, EmptyViewTemplateнизкая

Два изменения, которые причиняют боль, это pull-to-refresh и выделение. Всё остальное это просто переименование или удаление.

Предполётный чеклист

  1. Установлен .NET 11 SDK (dotnet --version сообщает 11.0.x). MAUI 11 требует .NET 11 SDK. В более ранних SDK нет манифеста workload.
  2. Установлен MAUI workload: dotnet workload install maui. Если вы обновлялись поверх более старой установки, сначала выполните dotnet workload repair.
  3. Доступны платформы Android SDK 35, iOS 18 и Mac Catalyst 18. Visual Studio 2022 17.13 или Visual Studio Code с расширением .NET MAUI 1.6+.
  4. Ветка git, которую можно выбросить. Держите проект на Xamarin.Forms собирающимся в main, пока сборка MAUI не станет зелёной на реальном устройстве.
  5. Список каждого экземпляра ListView в проекте. Сгруппируйте их по функциональным областям; мигрируйте по одной функции и выпускайте за feature flag, если ваш цикл релизов ежемесячный.
  6. Запустите .NET Upgrade Assistant на проекте один раз, чтобы он обработал изменения csproj, пространств имён и MauiProgram. Upgrade Assistant не переписывает XAML с ListView за вас, чем и занимается остальная часть этого поста.

Шаги миграции

1. Замените ListView на CollectionView

Простейший случай это плоский список с собственным DataTemplate:

<!-- Before. Xamarin.Forms 5.0 -->
<ListView ItemsSource="{Binding Articles}"
          CachingStrategy="RecycleElementAndDataTemplate"
          HasUnevenRows="True"
          SeparatorVisibility="None"
          ItemTapped="OnArticleTapped">
    <ListView.ItemTemplate>
        <DataTemplate>
            <ViewCell>
                <Grid Padding="12" RowDefinitions="Auto,Auto">
                    <Label Text="{Binding Title}" FontAttributes="Bold" />
                    <Label Grid.Row="1" Text="{Binding Excerpt}" />
                </Grid>
            </ViewCell>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>
<!-- After. .NET MAUI 11.0 -->
<CollectionView ItemsSource="{Binding Articles}"
                SelectionMode="Single"
                SelectionChangedCommand="{Binding OpenArticleCommand}"
                SelectionChangedCommandParameter="{Binding Source={RelativeSource Self}, Path=SelectedItem}">
    <CollectionView.ItemTemplate>
        <DataTemplate x:DataType="vm:ArticleVm">
            <Grid Padding="12" RowDefinitions="Auto,Auto">
                <Label Text="{Binding Title}" FontAttributes="Bold" />
                <Label Grid.Row="1" Text="{Binding Excerpt}" />
            </Grid>
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>

Что изменилось:

Проверка: соберите для Android (dotnet build -t:Run -f net11.0-android35.0) и прокрутите список из 1000 элементов на устройстве среднего класса. Время кадра должно быть меньше 16 мс в режиме Profile GPU Rendering в Android Studio. Если нет, проверьте, что x:DataType указан и что ни одна привязка внутри шаблона не использует Source={x:Reference ...}.

2. Оборачивание в RefreshView для pull-to-refresh

IsPullToRefreshEnabled, IsRefreshing и RefreshCommand больше не находятся на списке. Они живут в RefreshView, который оборачивает любое прокручиваемое содержимое.

<!-- After. .NET MAUI 11.0 -->
<RefreshView IsRefreshing="{Binding IsRefreshing}"
             Command="{Binding RefreshCommand}">
    <CollectionView ItemsSource="{Binding Articles}"
                    SelectionMode="Single">
        <CollectionView.ItemTemplate>
            <DataTemplate x:DataType="vm:ArticleVm">
                <!-- as before -->
            </DataTemplate>
        </CollectionView.ItemTemplate>
    </CollectionView>
</RefreshView>

RefreshView срабатывает только тогда, когда обёрнутый прокручиваемый контрол находится на нулевом смещении, поэтому поведение совпадает со старым ListView. Никакого IsPullToRefreshEnabled нет; если нужно отключить, установите IsEnabled="False" на RefreshView.

Проверка: потяните вниз по списку, находясь на нулевом смещении. Должен появиться платформенный спиннер, а RefreshCommand должен срабатывать ровно один раз за жест.

3. Перепишите выделение

Событие ItemTapped в Xamarin.Forms срабатывает на каждое нажатие, даже если SelectedItem не изменился. CollectionView чётко разделяет это: нажатие это выделение, а выделение наблюдается через SelectionChanged или SelectionChangedCommand. Если вы полагались на нажатие для перехода к одному и тому же элементу два раза подряд, придётся отказываться от состояния выделения, читая и очищая SelectedItem после каждого перехода:

// MainViewModel.cs, .NET MAUI 11.0, C# 14
[RelayCommand]
private async Task OpenArticleAsync(ArticleVm? selected)
{
    if (selected is null) return;
    await Shell.Current.GoToAsync(nameof(ArticleDetailPage),
        new Dictionary<string, object> { ["article"] = selected });
    SelectedArticle = null; // re-arm so the same row can fire next time
}

SelectionMode="None" плюс TapGestureRecognizer внутри шаблона это ближайший эквивалент старой семантики ItemTapped. Используйте его только при наличии причины. Шаблон на основе выделения это то, что ожидают платформы, и именно он бесплатно даёт вам кольца фокуса доступности на Windows.

Проверка: нажмите на элемент, вернитесь назад, нажмите на тот же элемент. Оба нажатия должны открывать страницу деталей.

4. Замените ContextActions на SwipeView

<!-- Before. Xamarin.Forms 5.0 -->
<ViewCell>
    <ViewCell.ContextActions>
        <MenuItem Text="Delete" IsDestructive="True"
                  Command="{Binding DeleteCommand}" />
    </ViewCell.ContextActions>
    <Grid>...</Grid>
</ViewCell>
<!-- After. .NET MAUI 11.0 -->
<SwipeView>
    <SwipeView.RightItems>
        <SwipeItems Mode="Execute">
            <SwipeItem Text="Delete"
                       BackgroundColor="Crimson"
                       Command="{Binding DeleteCommand}" />
        </SwipeItems>
    </SwipeView.RightItems>
    <Grid>...</Grid>
</SwipeView>

SwipeView это полноценный свайп влево и вправо с настраиваемыми фонами. IsDestructive превращается в обычный BackgroundColor. Mode="Execute" соответствует старому поведению с закрытием по нажатию; Mode="Reveal" оставляет меню открытым. Длительное нажатие на iOS для показа меню исчезло; если оно нужно, используйте MenuFlyout в MAUI 11 (новинка этого релиза).

Проверка: свайпните влево на iOS и Android. Нажмите delete. Элемент удаляется. Свайпните по следующей строке, чтобы убедиться, что одновременно открыт только один свайп.

5. Переключите группировку

<!-- After. .NET MAUI 11.0 -->
<CollectionView ItemsSource="{Binding ArticlesByDay}"
                IsGrouped="True">
    <CollectionView.GroupHeaderTemplate>
        <DataTemplate x:DataType="vm:ArticleDayVm">
            <Label Text="{Binding Day, StringFormat='{0:D}'}"
                   FontAttributes="Bold"
                   Padding="12,8" />
        </DataTemplate>
    </CollectionView.GroupHeaderTemplate>
    <CollectionView.ItemTemplate>
        <DataTemplate x:DataType="vm:ArticleVm">
            <!-- as before -->
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>

Меняются две вещи. Флаг называется IsGrouped, а не IsGroupingEnabled. И источник должен быть коллекцией коллекций, где каждый внешний элемент сам является перечислимым. Самый чистый шаблон это класс Grouping<TKey, TItem>, наследуемый от List<TItem> и предоставляющий Key. Это тот же шаблон, который использовал Xamarin.Forms, и он переносится без изменений.

Проверка: прокрутите сгруппированный список из 30 дней. Заголовки групп должны прилипать к верху своей группы на iOS по умолчанию (GroupHeadersStick="True" на нативном iOS; поведение MAUI по умолчанию повторяет это).

6. Выберите ItemsLayout

По умолчанию используется вертикальный LinearItemsLayout, что соответствует ListView. Переключение на сетку с одной линией:

<CollectionView ItemsSource="{Binding Photos}">
    <CollectionView.ItemsLayout>
        <GridItemsLayout Orientation="Vertical"
                         Span="3"
                         HorizontalItemSpacing="4"
                         VerticalItemSpacing="4" />
    </CollectionView.ItemsLayout>
    <CollectionView.ItemTemplate>
        <DataTemplate x:DataType="vm:PhotoVm">
            <Image Source="{Binding ThumbnailUrl}" Aspect="AspectFill" />
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>

Span="3" это фиксированное число столбцов. Встроенной адаптивной сетки нет; для неё размеры ячеек подгоняются под доступную ширину через behavior или повторное связывание при изменениях размера.

Проверка: поверните устройство. Сетка должна перерисоваться без потери позиции прокрутки.

7. Добавьте EmptyView

<CollectionView ItemsSource="{Binding Articles}">
    <CollectionView.EmptyView>
        <VerticalStackLayout HorizontalOptions="Center"
                             VerticalOptions="Center" Spacing="8">
            <Label Text="No articles yet" FontSize="18" />
            <Button Text="Refresh" Command="{Binding RefreshCommand}" />
        </VerticalStackLayout>
    </CollectionView.EmptyView>
</CollectionView>

Для нескольких пустых состояний (нет данных против ошибки) привяжите EmptyView к свойству и используйте EmptyViewTemplate с DataTemplateSelector.

Проверка: очистите источник. Появляется пустое представление. Заполните источник снова. Пустое представление исчезает без кадра наложения.

Чеклист проверки

После миграции каждого списка:

План отката

На практике это односторонняя миграция. Как только проект переведён на целевые фреймворки net11.0-* и MauiProgram.cs, возврат к Xamarin.Forms означает восстановление старой формы csproj, Xamarin.Forms.Forms.Init и платформенных точек входа приложения. Ничто из описанного в этом посте про CollectionView само по себе не обратимо отдельно от обновления проекта. Планируйте отгрузить рабочую сборку MAUI до удаления ветки Xamarin.Forms.

Подводные камни, на которые мы наткнулись

Связанное

Источники

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Назад