Миграция с 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 из системы контроля версий. Относитесь к миграции как к поездке в один конец и формируйте ветки соответственно.
Зачем мигрировать сейчас
- Xamarin.Forms не поддерживается. Нет патчей под подъём target SDK Android 15, который Google Play начал требовать с 2025-08-31, а Apple теперь отклоняет в App Store Connect сборки, слинкованные со старым SDK iOS 17.
- MAUI 11 по умолчанию запускается на CoreCLR для Android и iOS, а не на Mono. Холодный старт на Pixel 8 падает примерно с 1.6 s до 0.9 s на типовом Shell-приложении, а аллокации в установившемся режиме снижаются, потому что GC — генерационный, а не
SGen. Переключение описано в нашей статье про CoreCLR по умолчанию в MAUI. - Архитектура хендлеров заменяет пользовательские рендереры на более узкий API и избавляет от навесных конструкций
EffectплюсPlatformEffect, которые накопились в Xamarin.Forms за годы. - Resizetizer встроен. Конвейер
Resources/Images/*.svgгенерирует платформенные PNG на этапе сборки, так что вы наконец удаляете зоопарк изdrawable-xhdpi,drawable-xxhdpi,Assets.xcassetsиLaunchScreen.storyboard.
Что ломается
| Область | Изменение | Серьёзность |
|---|---|---|
| Структура проекта | Общий проект плюс три 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.cs | Application.MainPage работает в MAUI 11, но Shell-first — новый дефолт | низкая |
| Таргетинг | MonoAndroid12.0, Xamarin.iOS10 ушли. Только net11.0-android35.0 | высокая |
Upstream upgrade assistant поставляется как dotnet tool и покрывает заметную часть переписывания пространств имён и конвертации файла проекта. Он не справляется с пользовательскими рендерерами и регистрациями DependencyService, а именно там и уходит реальное время.
Предполётный чек-лист
-
Установите 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 -
Зафиксируйте SDK в
global.json, чтобы ветка миграции не поползла вперёд посреди PR:// global.json, repo root { "sdk": { "version": "11.0.100", "rollForward": "latestFeature" } } -
Пометьте текущую сборку Xamarin.Forms в системе контроля версий.
git tag pre-maui-migrationдостаточно. Если миграция уйдёт не туда на второй неделе, вы хотите чистую точку восстановления. -
Сделайте снимок платформенных ресурсов. Пройдитесь по папкам
Drawable*,Assets.xcassets, storyboard-ам splash и записям Info.plist / AndroidManifest.xml. Resizetizer перестраивает всё это дерево, и вам нужна опись «до» на случай, если какой-то ресурс потеряется. -
Соберите инвентарь регистраций
DependencyService.grep -rn "DependencyService\\|Dependency(typeof" .вернёт исчерпывающий список. Каждый превратится в вызовservices.AddSingletonилиservices.AddTransient. -
Соберите инвентарь пользовательских рендереров. Грепните
ExportRendererи прочитайте каждый. Часть можно удалить как есть (дефолты MAUI лучше дефолтов Xamarin.Forms), часть станет хендлерами, часть —Behaviors. -
Запустите
dotnet testна дереве Xamarin.Forms и сохраните зелёную базу. Миграция, добавляющая три тестовые регрессии, диагностируется куда проще, чем миграция, приземляющаяся на кодовую базу с пятью flaky-тестами.
Шаги миграции
-
Сначала прогоните upgrade assistant по общему проекту. Он переписывает csproj в SDK-стиль, обновляет самые очевидные пространства имён (
Xamarin.Forms→Microsoft.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-проектах; вы их сейчас удалите. -
Сожмите head-проекты в один csproj SDK-стиля. Xamarin.Forms поставляется как один общий проект плюс
MyApp.Android,MyApp.iOSи опциональноMyApp.UWPhead-проекты. 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». -
Перепишите
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из любого конструктора страницы. -
Конвертируйте каждую регистрацию
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 падает, если это не так. -
Замените пользовательские рендереры на хендлеры. Это самый трудоёмкий шаг. Пользовательский рендерер 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-тест, а не только успешная сборка. -
Откажитесь от
MessagingCenterв пользу messenger из CommunityToolkit.MessagingCenterпомечен устаревшим в MAUI 11 и запланирован к удалению в MAUI 12. ВозьмитеCommunityToolkit.Mvvm8.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-приложениях. -
Переведите ресурсы на 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без вашего ручного вмешательства. -
Обновите 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. -
Конвертируйте 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. -
Проведите аудит перехода с
Application.PropertiesнаPreferences. Любой код, писавшийApplication.Current.Properties["key"] = value, должен перейти наPreferences.Set("key", value)изMicrosoft.Maui.Storage. Форма похожа, но бэкенд хранилища отличается, поэтому при первом запуске может понадобиться разовая копия. Сделайте копирование идемпотентным с флагом"migrated_to_preferences", чтобы оно не запускалось повторно. -
Зафиксируйте каждую зависимость 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).
Проверка
После шагов выше:
dotnet restoreвыходит с 0 на чистом клоне миграционной ветки.dotnet build -f net11.0-android35.0и-f net11.0-ios18.0оба выходят с 0.- Проект юнит-тестов, перенацеленный на
net11.0, запускаетdotnet testв чистый зелёный. dotnet build -t:Run -f net11.0-android35.0запускает приложение в эмуляторе и доходит до первой страницы без необработанного исключения.- Ручной smoke-тест на реальном устройстве каждой страницы, содержавшей пользовательский рендерер в дереве Xamarin.Forms.
- Сравните время холодного старта до и после, замеряя секундомером от касания иконки лаунчера до первого кадра. CoreCLR плюс AOT должны увести среднее приложение под секунду на среднестатистическом Android-устройстве 2024 года. Если просели — перепроверьте шаг 5; чаще всего виноват хендлер, делающий layout-работу синхронно в UI-потоке.
План отката
После переписывания csproj автоматизированного отката нет. Реалистичный план:
- Сохраните git-тег
pre-maui-migrationиз предполётного чек-листа. - Держите миграцию в отдельной ветке до тех пор, пока проверка не станет зелёной и на Android, и на iOS.
- Если придётся откатывать уже после слияния в main, безопасный путь —
git revertкоммита слияния, затем чистое восстановление дерева Xamarin.Forms и редеплой. Никакого in-place «даунгрейда» csproj SDK-стиля обратно в legacy-схему общего проекта не существует.
Если ваше окно релиза не терпит миграции в один конец, выкатите MAUI-сборку как приложение с параллельным ID (net.mycompany.myapp.maui) в магазинах на один цикл, добейтесь crash-free-доли выше 99.5 % на продакшен-трафике и затем переключите bundle ID принудительным обновлением.
Подводные камни, на которые мы наступили
- Android resource shrinking уносит ваши шрифты.
Resources/Fonts/OpenSans-Regular.ttfпосле ресайз-переименования оказывается вResources/font/opensans_regular.ttf. Resource shrinker из R8 радостно удаляет шрифты, которые из XAML выглядят неиспользуемыми. Исправляется явным добавлением<MauiAsset Include="Resources/Fonts/**/*.ttf" />и отключением shrinking-а до следующего релиза:<AndroidLinkResources>false</AndroidLinkResources>только в Debug. UIRequiredDeviceCapabilitiesв iOSInfo.plistтребуетarm64. Переход с Mono на CoreCLR поставляет только ARM64-бинарники. ЕслиInfo.plistвсё ещё перечисляетarmv7, App Store Connect отклонит загрузку.- Markup-расширение
OnPlatformведёт себя иначе дляDefault. В Xamarin.Forms неуказанныйDefaultпадал в платформенное значение. В MAUI 11Defaultдолжен быть задан явно, когда используется как markup-расширение. Добавьте значениеDefaultили переключитесь на элементную форму<OnPlatform>. FrameвнутриGridсхлопывается до нулевой высоты. ЗаменаBorderне наследует дефолтHorizontalOptions="Fill"отFrame. Будьте явны:HorizontalOptions="Fill" VerticalOptions="Fill".Microsoft.Maui.Controls.Compatibilityне бесплатен. Он существует и позволяет держать живыми один-два упрямых рендерера, пока вы дотягиваете миграцию, но каждая ссылка наCompatibilityсохраняет legacy-цепочку рендереров в сборке и съедает часть выигрыша по холодному старту от CoreCLR. Используйте как мостик, не как конечную точку.
Связанное
- Миграция высокопроизводительного ListView из Xamarin.Forms на CollectionView в MAUI
- Миграция с .NET 8 на .NET 11: полный чек-лист
- Миграция с .NET Framework 4.8 на .NET 11 в 2026 году
- MAUI vs Avalonia vs Uno в 2026 году
- Flutter vs React Native vs MAUI для нового мобильного проекта в 2026 году
Источники
- .NET MAUI upgrade from Xamarin.Forms на MS Learn.
- Документация хендлеров .NET MAUI по замене рендереров.
- Release notes .NET MAUI 11.0 на GitHub.
Microsoft.Maui.Storage.Preferencesв качестве заменыApplication.Properties.- Гайд по messenger из CommunityToolkit.Mvvm в качестве замены
MessagingCenter.
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.