Start Debugging

Миграция с Xamarin.Forms 5.0 на .NET MAUI 11: полный чек-лист

Сквозная миграция с Xamarin.Forms 5.0 на .NET MAUI 11 GA на net11.0: переписывание csproj, преобразование пользовательских рендереров в хендлеры, подключение AppShell, удаление DependencyService, отказ от MessagingCenter, ресурсы Resizetizer и подводные камни, которые бьют по реальной продакшен-кодовой базе.

Xamarin.Forms вышел из поддержки 2024-05-01, и с тех пор Microsoft не выпустила ни одного исправления. .NET MAUI 11 — это LTS-точка приземления для каждого приложения на Xamarin.Forms 5.0, живущего в долг по времени, и с релиза MAUI 11.0 GA в ноябре 2025 года платформа наконец-то получила инфраструктуру рендеринга, производительность RecyclerView и поддержку iOS 18 / Android 15, которых не хватало ранним выпускам MAUI. Сфокусированная миграция одним разработчиком среднего приложения, от десяти до двадцати экранов с горсткой пользовательских рендереров, занимает от одной до трёх недель. Сложные части — это не XAML и не система сборки; это пользовательские рендереры, DependencyService и любой код, напрямую обращавшийся к платформенным проектам. Этот пост фиксирует Xamarin.Forms 5.0.0.2622 как источник и .NET MAUI 11.0.0 на net11.0 как цель, с net11.0-android35.0, net11.0-ios18.0 и net11.0-maccatalyst18.0 как активными TFM.

Откат нетривиален: как только .csproj переключится на SDK-стиль с Microsoft.NET.Sdk и UseMaui=true, вы не вернётесь к Xamarin.Forms-проекту-обёртке без восстановления исходного csproj и packages.config из системы контроля версий. Относитесь к миграции как к поездке в один конец и формируйте ветки соответственно.

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

Что ломается

ОбластьИзменениеСерьёзность
Структура проектаОбщий проект плюс три head-проекта сжимаются в один csproj SDK-стилявысокая
Пространство Xamarin.FormsЗаменено на Microsoft.Maui.Controls, Microsoft.Maui.Graphics и др.высокая
Пользовательские рендерерыExportRenderer и IVisualElementRenderer удалены. Используйте хендлерывысокая
DependencyServiceУдалён. Используйте Microsoft.Extensions.DependencyInjectionвысокая
MessagingCenterПомечен устаревшим и запланирован к удалению. Используйте IMessenger из CommunityToolkit.Mvvmвысокая
Application.PropertiesУдалён. Используйте Microsoft.Maui.Storage.Preferencesсредняя
ListViewОбёртка над CollectionView. Лучше мигрировать, см. руководство по переходу с ListView на CollectionViewсредняя
MasterDetailPageПереименован в FlyoutPage. XAML нужно обновитьнизкая
FrameМягко устарел. Используйте Border плюс StrokeShapeнизкая
OpenGLViewПолностью удалённизкая
MainActivity AndroidДелится на MainActivity.cs плюс MainApplication.cs, оба partialсредняя
AppDelegate iOSЗаменяет FormsApplicationDelegate на MauiUIApplicationDelegateсредняя
App.xaml.csApplication.MainPage работает в MAUI 11, но Shell-first — новый дефолтнизкая
ТаргетингMonoAndroid12.0, Xamarin.iOS10 ушли. Только net11.0-android35.0высокая

Upstream upgrade assistant поставляется как dotnet tool и покрывает заметную часть переписывания пространств имён и конвертации файла проекта. Он не справляется с пользовательскими рендерерами и регистрациями DependencyService, а именно там и уходит реальное время.

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

  1. Установите SDK .NET 11 и workloads MAUI. Проверьте через dotnet workload list. Вам нужны maui, maui-android, maui-ios и maui-maccatalyst, все версии 11.0.x.

    # .NET 11.0
    dotnet workload install maui
    dotnet workload list
  2. Зафиксируйте SDK в global.json, чтобы ветка миграции не поползла вперёд посреди PR:

    // global.json, repo root
    {
      "sdk": {
        "version": "11.0.100",
        "rollForward": "latestFeature"
      }
    }
  3. Пометьте текущую сборку Xamarin.Forms в системе контроля версий. git tag pre-maui-migration достаточно. Если миграция уйдёт не туда на второй неделе, вы хотите чистую точку восстановления.

  4. Сделайте снимок платформенных ресурсов. Пройдитесь по папкам Drawable*, Assets.xcassets, storyboard-ам splash и записям Info.plist / AndroidManifest.xml. Resizetizer перестраивает всё это дерево, и вам нужна опись «до» на случай, если какой-то ресурс потеряется.

  5. Соберите инвентарь регистраций DependencyService. grep -rn "DependencyService\\|Dependency(typeof" . вернёт исчерпывающий список. Каждый превратится в вызов services.AddSingleton или services.AddTransient.

  6. Соберите инвентарь пользовательских рендереров. Грепните ExportRenderer и прочитайте каждый. Часть можно удалить как есть (дефолты MAUI лучше дефолтов Xamarin.Forms), часть станет хендлерами, часть — Behaviors.

  7. Запустите dotnet test на дереве Xamarin.Forms и сохраните зелёную базу. Миграция, добавляющая три тестовые регрессии, диагностируется куда проще, чем миграция, приземляющаяся на кодовую базу с пятью flaky-тестами.

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

  1. Сначала прогоните upgrade assistant по общему проекту. Он переписывает csproj в SDK-стиль, обновляет самые очевидные пространства имён (Xamarin.FormsMicrosoft.Maui.Controls) и помечает места рендереров и DependencyService, с которыми не справился.

    # .NET 11
    dotnet tool install -g upgrade-assistant
    upgrade-assistant upgrade ./src/MyApp/MyApp.csproj --target-tfm net11.0

    Проверка: dotnet restore успешно отрабатывает на новом csproj, а git status показывает ожидаемые переписывания. Пока не запускайте на head-проектах; вы их сейчас удалите.

  2. Сожмите head-проекты в один csproj SDK-стиля. Xamarin.Forms поставляется как один общий проект плюс MyApp.Android, MyApp.iOS и опционально MyApp.UWP head-проекты. MAUI поставляется одним csproj с мульти-таргетингом. Перенесите Android-специфичный код в Platforms/Android/, iOS-специфичный — в Platforms/iOS/ и удалите head-csproj-ы. Заголовок нового csproj выглядит так:

    <!-- src/MyApp/MyApp.csproj, .NET MAUI 11, net11.0 -->
    <Project Sdk="Microsoft.NET.Sdk">
      <PropertyGroup>
        <TargetFrameworks>net11.0-android35.0;net11.0-ios18.0;net11.0-maccatalyst18.0</TargetFrameworks>
        <TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net11.0-windows10.0.19041.0</TargetFrameworks>
        <OutputType>Exe</OutputType>
        <UseMaui>true</UseMaui>
        <SingleProject>true</SingleProject>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <RootNamespace>MyApp</RootNamespace>
        <ApplicationId>net.mycompany.myapp</ApplicationId>
        <ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
        <ApplicationVersion>1</ApplicationVersion>
      </PropertyGroup>
    </Project>

    Проверка: dotnet build -t:Restore успешен, и проект загружается в Visual Studio 2026 17.14 или Rider 2026.1 без предупреждений «unsupported project type».

  3. Перепишите App.xaml.cs под использование MauiProgram.CreateMauiApp. Xamarin.Forms стартовал в MainActivity.OnCreate через LoadApplication(new App()). MAUI передаёт это MauiAppBuilder. Точка входа становится такой:

    // MauiProgram.cs, .NET MAUI 11, C# 14
    using Microsoft.Extensions.Logging;
    using Microsoft.Maui.Hosting;
    
    namespace MyApp;
    
    public static class MauiProgram
    {
        public static MauiApp CreateMauiApp()
        {
            var builder = MauiApp.CreateBuilder();
            builder
                .UseMauiApp<App>()
                .ConfigureFonts(fonts =>
                {
                    fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                    fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
                });
    
            builder.Services.AddSingleton<IAuthService, AuthService>();
            builder.Services.AddTransient<MainPageViewModel>();
    
            return builder.Build();
        }
    }

    Проверка: dotnet build -f net11.0-android35.0 выходит с 0, IDE разрешает MauiProgram из любого конструктора страницы.

  4. Конвертируйте каждую регистрацию DependencyService в IServiceCollection. Это механика, но обязательная; DependencyService.Get<T>() в MAUI 11 исчез. Замена:

    // Before, Xamarin.Forms 5.0
    DependencyService.Register<IAuthService, AuthService>();
    var auth = DependencyService.Get<IAuthService>();
    
    // After, .NET MAUI 11
    // Registration moves to MauiProgram.CreateMauiApp (step 3).
    // Resolution moves to the constructor or to IPlatformApplication.Current.Services.
    public partial class MainPage : ContentPage
    {
        public MainPage(IAuthService auth) // injected
        {
            InitializeComponent();
        }
    }

    Там, где внедрение через конструктор непрактично (статический хелпер, хендлер AppShell), IPlatformApplication.Current!.Services.GetRequiredService<IAuthService>() — это аварийный выход.

    Проверка: grep -rn "DependencyService" src/ возвращает ноль совпадений. CI падает, если это не так.

  5. Замените пользовательские рендереры на хендлеры. Это самый трудоёмкий шаг. Пользовательский рендерер Xamarin.Forms, переопределявший OnElementPropertyChanged, превращается в MAUI-хендлер со словарём Mapper. Пример — рендерер, убирающий подчёркивание из Android-Entry:

    // .NET MAUI 11, C# 14
    // Platforms/Android/EntryHandlerCustomization.cs
    using Microsoft.Maui.Handlers;
    
    public static class EntryHandlerCustomization
    {
        public static void Apply()
        {
            EntryHandler.Mapper.AppendToMapping("NoUnderline", (handler, entry) =>
            {
                handler.PlatformView.BackgroundTintList =
                    Android.Content.Res.ColorStateList.ValueOf(
                        Android.Graphics.Color.Transparent);
            });
        }
    }

    Зарегистрируйте кастомизацию в MauiProgram.CreateMauiApp через builder.ConfigureMauiHandlers(...) или вызовите Apply() из partial void в MauiProgram, чтобы он запускался только на Android. Тот же шаблон сохраните для iOS в Platforms/iOS/.

    Проверка: каждая страница, зависевшая от старого рендерера, корректно рисуется в dotnet build -t:Run -f net11.0-android35.0. Визуальный smoke-тест, а не только успешная сборка.

  6. Откажитесь от MessagingCenter в пользу messenger из CommunityToolkit. MessagingCenter помечен устаревшим в MAUI 11 и запланирован к удалению в MAUI 12. Возьмите CommunityToolkit.Mvvm 8.4.0 или выше:

    # .NET MAUI 11
    dotnet add package CommunityToolkit.Mvvm --version 8.4.0

    Смена шаблона:

    // Before, Xamarin.Forms 5.0
    MessagingCenter.Subscribe<LoginViewModel>(this, "LoggedIn", _ => RefreshUi());
    MessagingCenter.Send(this, "LoggedIn");
    
    // After, .NET MAUI 11 with CommunityToolkit.Mvvm 8.4.0
    public sealed record LoggedInMessage;
    WeakReferenceMessenger.Default.Register<LoggedInMessage>(this, (r, m) => RefreshUi());
    WeakReferenceMessenger.Default.Send(new LoggedInMessage());

    WeakReferenceMessenger устраняет баги жизненного цикла, из-за которых MessagingCenter тёк в долгоживущих shell-приложениях.

  7. Переведите ресурсы на Resizetizer. Удалите Resources/drawable-*, Assets.xcassets и продублированные splash-экраны. Положите один appicon.svg и один splash.svg в Resources/AppIcon/ и Resources/Splash/. csproj уже знает о них через элементы MauiIcon и MauiSplashScreen, которые сгенерировал upgrade assistant. Ресайз на этапе сборки заменяет всю лестницу плотностей.

    <!-- src/MyApp/MyApp.csproj fragment -->
    <ItemGroup>
      <MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4" />
      <MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#512BD4" BaseSize="128,128" />
      <MauiImage Include="Resources\Images\*" />
      <MauiFont Include="Resources\Fonts\*" />
    </ItemGroup>

    Проверка: dotnet build -f net11.0-android35.0 производит bin/Debug/net11.0-android35.0/Resources/drawable-xxhdpi/appicon.png без вашего ручного вмешательства.

  8. Обновите Android MainActivity и MainApplication. В Xamarin.Forms был один MainActivity, наследовавший FormsAppCompatActivity. MAUI делит это на MainActivity плюс MainApplication:

    // Platforms/Android/MainActivity.cs, .NET MAUI 11
    using Android.App;
    using Android.Content.PM;
    using Android.OS;
    
    namespace MyApp;
    
    [Activity(Theme = "@style/Maui.SplashTheme",
              MainLauncher = true,
              ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation |
                                     ConfigChanges.UiMode | ConfigChanges.ScreenLayout |
                                     ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
    public class MainActivity : MauiAppCompatActivity { }
    
    // Platforms/Android/MainApplication.cs, .NET MAUI 11
    [Application]
    public class MainApplication : MauiApplication
    {
        public MainApplication(IntPtr handle, Android.Runtime.JniHandleOwnership ownership)
            : base(handle, ownership) { }
    
        protected override MauiApp CreateMauiApp() => MyApp.MauiProgram.CreateMauiApp();
    }

    На iOS эквивалент — один AppDelegate, наследующий MauiUIApplicationDelegate. Шаблон тот же: переопределите CreateMauiApp и вызовите общий MauiProgram.

  9. Конвертируйте XAML-пространства имён. Каждый xmlns="http://xamarin.com/schemas/2014/forms" становится xmlns="http://schemas.microsoft.com/dotnet/2021/maui". Upgrade assistant обрабатывает большую часть, но файлы со смешанными пространствами имён (библиотеки пользовательских контролов, подтягивавшие xamarin.toolkit) требуют ручного обхода. Frame ещё один релиз работает, но выдаёт предупреждение сборки. Запланируйте замену на Border плюс StrokeShape="RoundRectangle 12" и BackgroundColor. MasterDetailPage нужно переименовать в FlyoutPage и в XAML, и в code-behind, включая любые x:TypeArguments.

  10. Проведите аудит перехода с Application.Properties на Preferences. Любой код, писавший Application.Current.Properties["key"] = value, должен перейти на Preferences.Set("key", value) из Microsoft.Maui.Storage. Форма похожа, но бэкенд хранилища отличается, поэтому при первом запуске может понадобиться разовая копия. Сделайте копирование идемпотентным с флагом "migrated_to_preferences", чтобы оно не запускалось повторно.

  11. Зафиксируйте каждую зависимость NuGet на MAUI-совместимой версии. После апгрейда запустите dotnet list package --vulnerable и dotnet list package --outdated. Типичные подозреваемые: Xamarin.Essentials (исчез, влит в MAUI), Xamarin.Forms.Maps (заменён на Microsoft.Maui.Controls.Maps), Xamarin.Forms.Visual.Material (заменён стилями Material 3 из MAUI, см. статью про Material 3 в MAUI 10).

Проверка

После шагов выше:

План отката

После переписывания csproj автоматизированного отката нет. Реалистичный план:

  1. Сохраните git-тег pre-maui-migration из предполётного чек-листа.
  2. Держите миграцию в отдельной ветке до тех пор, пока проверка не станет зелёной и на Android, и на iOS.
  3. Если придётся откатывать уже после слияния в main, безопасный путь — git revert коммита слияния, затем чистое восстановление дерева Xamarin.Forms и редеплой. Никакого in-place «даунгрейда» csproj SDK-стиля обратно в legacy-схему общего проекта не существует.

Если ваше окно релиза не терпит миграции в один конец, выкатите MAUI-сборку как приложение с параллельным ID (net.mycompany.myapp.maui) в магазинах на один цикл, добейтесь crash-free-доли выше 99.5 % на продакшен-трафике и затем переключите bundle ID принудительным обновлением.

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

Связанное

Источники

Comments

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

< Назад