Введение: почему миграция на .NET MAUI неизбежна
1 мая 2024 года Microsoft официально прекратила поддержку всех SDK Xamarin, включая Xamarin.Forms. Проще говоря, ваши Xamarin-приложения больше не получают обновлений безопасности, исправлений багов и совместимости с новыми версиями ОС. Для команд, которые всё ещё сидят на Xamarin.Forms, миграция на .NET MAUI — это не вопрос «если», а вопрос «когда».
Я сам прошёл через миграцию нескольких коммерческих проектов, и честно скажу — это не так страшно, как кажется на первый взгляд. Да, работы немало, но при грамотном подходе всё вполне управляемо.
.NET MAUI (Multi-platform App UI) — эволюционный наследник Xamarin.Forms. Он предлагает единую модель проекта, встроенную поддержку внедрения зависимостей, новую архитектуру обработчиков (Handlers) вместо рендереров, улучшенную производительность и поддержку .NET 10. В этом руководстве мы подробно разберём каждый этап — от подготовки и планирования до финального тестирования и деплоя.
Итак, поехали.
1. Оценка текущего проекта и планирование миграции
1.1 Аудит зависимостей
Прежде чем бросаться переписывать код, проведите тщательный аудит проекта. Начните с инвентаризации всех NuGet-пакетов и сторонних библиотек:
// Используйте CLI для анализа зависимостей
dotnet list package --outdated
dotnet list package --vulnerable
Составьте таблицу совместимости для каждого пакета. Это скучная, но очень важная работа — пропустив этот шаг, вы рискуете застрять на середине миграции:
- Полностью совместимые — пакеты, уже поддерживающие .NET MAUI (например, CommunityToolkit.Mvvm, Newtonsoft.Json)
- Требуют обновления — пакеты с .NET MAUI-версиями (например, Syncfusion, Telerik)
- Требуют замены — устаревшие пакеты без MAUI-поддержки, для которых нужно найти альтернативы
- Собственные рендереры — кастомные рендереры, которые придётся переписать в обработчики
1.2 Подготовка Xamarin.Forms проекта
Для максимально гладкой миграции выполните подготовительные шаги ещё в рамках Xamarin.Forms:
- Обновите Xamarin.Forms до версии 5.0 или выше
- Переведите проект на .NET Standard 2.0 или выше
- Замените устаревшие API (MessagingCenter → WeakReferenceMessenger из CommunityToolkit)
- Удалите неиспользуемые зависимости
- Убедитесь, что все тесты проходят
Серьёзно, не пренебрегайте этим этапом. Чем чище будет ваш Xamarin-проект перед миграцией, тем проще будет весь процесс.
1.3 Выбор стратегии миграции
Тут есть три основных пути:
- Автоматическая миграция — с помощью .NET Upgrade Assistant или GitHub Copilot Agent в Visual Studio 2026
- Ручная пошаговая миграция — преобразование каждого проекта вручную с полным контролем
- Создание нового проекта — создание .NET MAUI проекта с нуля и постепенный перенос кода
Для большинства проектов среднего размера я бы рекомендовал комбинированный подход: используйте Upgrade Assistant для первоначального преобразования, а затем вручную доработайте результат. Это экономит кучу времени на рутине, но при этом оставляет вам полный контроль над важными моментами.
2. Преобразование структуры проекта
2.1 Переход на SDK-стиль проекта
Первый и самый фундаментальный шаг — переход с классического формата .csproj на SDK-стиль. Вот как это выглядит на практике:
Было (Xamarin.Forms):
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0"
DefaultTargets="Build"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2578" />
<PackageReference Include="Xamarin.Essentials" Version="1.8.1" />
</ItemGroup>
</Project>
Стало (.NET MAUI):
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net10.0-android;net10.0-ios;net10.0-maccatalyst</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">
$(TargetFrameworks);net10.0-windows10.0.19041.0
</TargetFrameworks>
<OutputType>Exe</OutputType>
<RootNamespace>MyApp</RootNamespace>
<UseMaui>true</UseMaui>
<SingleProject>true</SingleProject>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
Заметьте, насколько проще стал файл проекта. Это одна из тех вещей, которые сразу чувствуются после миграции.
2.2 Объединение платформенных проектов
В Xamarin.Forms типичное решение включало отдельные проекты для каждой платформы (MyApp.Android, MyApp.iOS). В .NET MAUI всё живёт в одном проекте, а платформенно-специфичный код лежит в папке Platforms:
MyApp/
├── Platforms/
│ ├── Android/
│ │ ├── AndroidManifest.xml
│ │ ├── MainActivity.cs
│ │ └── MainApplication.cs
│ ├── iOS/
│ │ ├── AppDelegate.cs
│ │ ├── Info.plist
│ │ └── Program.cs
│ ├── MacCatalyst/
│ │ └── ...
│ └── Windows/
│ └── ...
├── Resources/
│ ├── Images/
│ ├── Fonts/
│ └── Raw/
├── Views/
├── ViewModels/
├── Services/
├── MauiProgram.cs
└── App.xaml
Перенесите платформенно-специфичный код из отдельных проектов в соответствующие папки внутри Platforms. Файлы с условной компиляцией также можно размещать в общих папках с использованием суффиксов:
Services/
├── DeviceService.cs // Общий интерфейс
├── DeviceService.Android.cs // Android-реализация
└── DeviceService.iOS.cs // iOS-реализация
Мне такой подход нравится больше, чем размещение всего в Platforms — код логически группируется по функциональности, а не по платформе.
3. Обновление пространств имён и XAML
3.1 Замена пространств имён C#
Основное изменение — замена всех пространств имён Xamarin на соответствующие в .NET MAUI. Вот полная таблица замен:
// Основные замены
Xamarin.Forms → Microsoft.Maui.Controls
Xamarin.Forms.Xaml → Microsoft.Maui.Controls.Xaml
Xamarin.Essentials → Microsoft.Maui.Devices (и подпространства)
Xamarin.Forms.Maps → Microsoft.Maui.Controls.Maps
Xamarin.Forms.DualScreen → Microsoft.Maui.Controls.Foldable
Xamarin.Forms.PlatformConfiguration → Microsoft.Maui.Controls.PlatformConfiguration
Для автоматической замены отлично подходят регулярные выражения. Можно сделать это из командной строки:
# Массовая замена пространств имён с помощью sed
find . -name "*.cs" -exec sed -i '' \
's/using Xamarin.Forms/using Microsoft.Maui.Controls/g' {} +
find . -name "*.cs" -exec sed -i '' \
's/using Xamarin.Essentials/using Microsoft.Maui.Devices/g' {} +
3.2 Обновление XAML-файлов
Во всех XAML-файлах нужно заменить пространство имён по умолчанию:
Было:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
Стало:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
Не забудьте также проверить, что все локальные пространства имён (xmlns:local, xmlns:viewmodels) указывают на обновлённые namespace вашего проекта.
4. Миграция точки входа приложения
4.1 Замена App.xaml.cs
В Xamarin.Forms точкой входа был класс App, наследующий Application. В .NET MAUI появляется новый файл MauiProgram.cs — это теперь основная точка конфигурации:
// MauiProgram.cs — новая точка входа в .NET MAUI
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");
});
// Регистрация сервисов (замена DependencyService)
builder.Services.AddSingleton<INavigationService, NavigationService>();
builder.Services.AddSingleton<IApiService, ApiService>();
builder.Services.AddTransient<MainViewModel>();
builder.Services.AddTransient<MainPage>();
#if DEBUG
builder.Logging.AddDebug();
#endif
return builder.Build();
}
}
Если вы работали с ASP.NET Core, то паттерн покажется знакомым. По сути, Microsoft унифицировала подход к конфигурации приложений — и это большой плюс.
4.2 Обновление App.xaml.cs
Класс App становится значительно проще, так как конфигурация перенесена в MauiProgram:
// Было (Xamarin.Forms)
public class App : Xamarin.Forms.Application
{
public App()
{
InitializeComponent();
DependencyService.Register<IApiService, ApiService>();
MainPage = new NavigationPage(new MainPage());
}
}
// Стало (.NET MAUI)
public class App : Microsoft.Maui.Controls.Application
{
public App()
{
InitializeComponent();
MainPage = new AppShell();
}
}
5. Замена DependencyService на встроенное внедрение зависимостей
5.1 Удаление атрибутов DependencyService
Это, пожалуй, одно из самых приятных изменений. В Xamarin.Forms для регистрации платформенных сервисов использовался (скажем прямо, довольно неуклюжий) атрибут [assembly: Dependency]. В .NET MAUI он полностью заменяется нормальным DI-контейнером на основе Microsoft.Extensions.DependencyInjection.
Было (Xamarin.Forms):
// Интерфейс
public interface IDeviceOrientationService
{
DeviceOrientation GetOrientation();
}
// Android-реализация
[assembly: Dependency(typeof(DeviceOrientationService))]
namespace MyApp.Droid.Services
{
public class DeviceOrientationService : IDeviceOrientationService
{
public DeviceOrientation GetOrientation()
{
// Платформенная логика
var windowManager = Android.App.Application.Context
.GetSystemService(Context.WindowService) as IWindowManager;
// ...
}
}
}
// Использование
var orientation = DependencyService.Get<IDeviceOrientationService>()
.GetOrientation();
Стало (.NET MAUI):
// Интерфейс (тот же)
public interface IDeviceOrientationService
{
DeviceOrientation GetOrientation();
}
// Платформенная реализация с partial class
namespace MyApp.Services
{
public partial class DeviceOrientationService : IDeviceOrientationService
{
public partial DeviceOrientation GetOrientation();
}
}
// Platforms/Android/Services/DeviceOrientationService.cs
namespace MyApp.Services
{
public partial class DeviceOrientationService
{
public partial DeviceOrientation GetOrientation()
{
var windowManager = Android.App.Application.Context
.GetSystemService(Context.WindowService) as IWindowManager;
// ...
}
}
}
// Регистрация в MauiProgram.cs
builder.Services.AddSingleton<IDeviceOrientationService, DeviceOrientationService>();
// Использование через конструкторное внедрение
public class MainViewModel
{
private readonly IDeviceOrientationService _orientationService;
public MainViewModel(IDeviceOrientationService orientationService)
{
_orientationService = orientationService;
}
}
Конструкторное внедрение — это просто небо и земля по сравнению с DependencyService.Get. Код становится тестируемым, явным и предсказуемым.
5.2 Время жизни сервисов
При регистрации сервисов важно правильно выбрать время жизни:
AddSingleton<T>()— один экземпляр на всё время работы приложения (настройки, кэши, HTTP-клиенты)AddTransient<T>()— новый экземпляр при каждом запросе (для ViewModel, страниц)AddScoped<T>()— один экземпляр в рамках определённой области
На практике AddScoped в мобильных приложениях используется редко — в основном хватает Singleton и Transient.
6. Миграция кастомных рендереров на обработчики (Handlers)
6.1 Понимание архитектуры обработчиков
Обработчики — это, пожалуй, самое важное архитектурное изменение в .NET MAUI. В отличие от рендереров, обработчики:
- Не создают дополнительный контейнерный элемент (привет, производительность)
- Используют паттерн маппинга свойств (PropertyMapper) вместо OnElementPropertyChanged
- Обеспечивают более слабую связность между кроссплатформенным и нативным кодом
- Позволяют легко модифицировать существующие контролы без создания подклассов
6.2 Пример миграции кастомного Entry
Давайте разберём конкретный пример — миграцию кастомного Entry без рамки.
Было (Xamarin.Forms Custom Renderer):
// Кастомный контрол
public class BorderlessEntry : Entry { }
// Android Renderer
[assembly: ExportRenderer(typeof(BorderlessEntry), typeof(BorderlessEntryRenderer))]
namespace MyApp.Droid.Renderers
{
public class BorderlessEntryRenderer : EntryRenderer
{
public BorderlessEntryRenderer(Context context) : base(context) { }
protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
{
base.OnElementChanged(e);
if (Control != null)
{
Control.Background = null;
Control.SetBackgroundColor(Android.Graphics.Color.Transparent);
Control.SetPadding(0, 0, 0, 0);
}
}
protected override void OnElementPropertyChanged(object sender,
PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == Entry.TextProperty.PropertyName)
{
// Обработка изменения свойства
}
}
}
}
Стало (.NET MAUI Handler):
// Кастомный контрол с интерфейсом
public interface IBorderlessEntry : IEntry { }
public class BorderlessEntry : Entry, IBorderlessEntry { }
// Handler
public partial class BorderlessEntryHandler : EntryHandler
{
public BorderlessEntryHandler() : base(PropertyMapper) { }
// PropertyMapper заменяет OnElementPropertyChanged
public static new IPropertyMapper<IEntry, BorderlessEntryHandler> PropertyMapper =
new PropertyMapper<IEntry, BorderlessEntryHandler>(EntryHandler.Mapper)
{
[nameof(IEntry.Background)] = MapBackground,
};
// Метод маппинга для Android
public static void MapBackground(BorderlessEntryHandler handler, IEntry entry)
{
#if ANDROID
handler.PlatformView.Background = null;
handler.PlatformView.SetBackgroundColor(Android.Graphics.Color.Transparent);
handler.PlatformView.SetPadding(0, 0, 0, 0);
#endif
}
}
// Регистрация в MauiProgram.cs
builder.ConfigureMauiHandlers(handlers =>
{
handlers.AddHandler<BorderlessEntry, BorderlessEntryHandler>();
});
Кода стало немного больше, но структура значительно чище. И, что важно, вы можете модифицировать маппинг извне, не создавая подклассов.
6.3 Быстрая модификация существующих обработчиков
А вот это — моя любимая фича. Во многих случаях не нужно создавать полноценный обработчик. .NET MAUI позволяет модифицировать поведение стандартных контролов буквально парой строк:
// В MauiProgram.cs — глобальная модификация всех Entry
Microsoft.Maui.Handlers.EntryHandler.Mapper.AppendToMapping("BorderlessEntry",
(handler, view) =>
{
#if ANDROID
handler.PlatformView.Background = null;
handler.PlatformView.SetBackgroundColor(Android.Graphics.Color.Transparent);
#elif IOS || MACCATALYST
handler.PlatformView.BorderStyle = UIKit.UITextBorderStyle.None;
#endif
});
Элегантно, правда?
7. Обновление навигации
7.1 Переход на Shell
Если вы ещё не использовали Shell в Xamarin.Forms, миграция — отличный момент для перехода. Shell даёт единую модель навигации с поддержкой URI-маршрутизации:
<!-- AppShell.xaml -->
<Shell xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:views="clr-namespace:MyApp.Views"
x:Class="MyApp.AppShell">
<FlyoutItem Title="Главная" Icon="home.png">
<Tab Title="Лента">
<ShellContent ContentTemplate="{DataTemplate views:HomePage}" />
</Tab>
<Tab Title="Профиль">
<ShellContent ContentTemplate="{DataTemplate views:ProfilePage}" />
</Tab>
</FlyoutItem>
</Shell>
// Регистрация маршрутов
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
// Регистрация маршрутов для страниц, не входящих в визуальную иерархию
Routing.RegisterRoute(nameof(DetailPage), typeof(DetailPage));
Routing.RegisterRoute(nameof(SettingsPage), typeof(SettingsPage));
}
}
// Навигация по маршрутам
await Shell.Current.GoToAsync(nameof(DetailPage), new Dictionary<string, object>
{
{ "ItemId", selectedItem.Id }
});
7.2 Передача параметров через атрибуты
// ViewModel с параметрами навигации
[QueryProperty(nameof(ItemId), "ItemId")]
public partial class DetailViewModel : ObservableObject
{
[ObservableProperty]
private int itemId;
partial void OnItemIdChanged(int value)
{
// Загрузка данных при получении параметра
LoadItemAsync(value);
}
}
Работает чисто и предсказуемо. Единственный нюанс — для сложных объектов лучше передавать ID и загружать данные в целевой ViewModel, а не пытаться протолкнуть весь объект через параметры навигации.
8. Обновление ресурсов и ассетов
8.1 Единая система ресурсов
В .NET MAUI все ресурсы управляются из одного места. Перетащите изображения, шрифты и прочие ассеты в папку Resources:
<!-- В .csproj -->
<ItemGroup>
<!-- Изображения автоматически масштабируются для каждой платформы -->
<MauiImage Include="Resources\Images\*" />
<!-- Шрифты -->
<MauiFont Include="Resources\Fonts\*" />
<!-- Splash-экран -->
<MauiSplashScreen Include="Resources\Splash\splash.svg"
Color="#512BD4"
BaseSize="128,128" />
<!-- Иконка приложения -->
<MauiIcon Include="Resources\AppIcon\appicon.svg"
ForegroundFile="Resources\AppIcon\appiconfg.svg"
Color="#512BD4" />
<!-- Сырые ассеты -->
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>
Больше не нужно хранить отдельные версии изображений для Android (drawable-hdpi, drawable-xhdpi и т.д.) и iOS (@2x, @3x). .NET MAUI автоматически генерирует нужные размеры из SVG или растрового изображения с достаточным разрешением. Это экономит кучу времени и нервов.
8.2 Миграция стилей и тем
Стили и ресурсы из App.xaml переносятся практически без изменений — нужно только обновить пространства имён:
<!-- Resources/Styles/Styles.xaml -->
<ResourceDictionary xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<Style TargetType="Entry">
<Setter Property="TextColor" Value="{AppThemeBinding Light=#1A1A1A, Dark=#FFFFFF}" />
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light=#FFFFFF, Dark=#2D2D2D}" />
<Setter Property="FontSize" Value="14" />
</Style>
<!-- Поддержка тёмной темы через AppThemeBinding -->
<Color x:Key="PrimaryColor">#512BD4</Color>
<Color x:Key="SecondaryColor">#DFD8F7</Color>
</ResourceDictionary>
9. Использование .NET Upgrade Assistant
9.1 Установка и запуск
Microsoft предоставляет .NET Upgrade Assistant — инструмент, который автоматизирует многие рутинные шаги миграции. В Visual Studio 2026 также можно использовать GitHub Copilot Agent для модернизации, но CLI-версия тоже отлично справляется:
# Установка .NET Upgrade Assistant
dotnet tool install -g upgrade-assistant
# Анализ проекта
upgrade-assistant analyze MyApp.sln
# Запуск миграции
upgrade-assistant upgrade MyApp.sln
Upgrade Assistant автоматически выполнит:
- Преобразование файла проекта в SDK-стиль
- Обновление целевого фреймворка
- Замену пространств имён Xamarin на MAUI
- Обновление совместимых NuGet-пакетов
- Базовую адаптацию XAML-файлов
9.2 Что потребует ручной доработки
Автоматика не всесильна. После работы Upgrade Assistant вам всё равно придётся руками поправить:
- Кастомные рендереры (преобразование в обработчики)
- Платформенно-специфичный код в отдельных проектах
- Устаревшие API (DependencyService, MessagingCenter)
- Сторонние библиотеки без автоматической замены
- Пользовательские Effects (замена на Platform Behaviors)
Не расстраивайтесь — даже частичная автоматизация экономит часы работы.
10. Обработка распространённых проблем миграции
10.1 Изменения в поведении Layout
.NET MAUI содержит заметные изменения в логике расположения элементов. На этом месте многие спотыкаются, так что будьте внимательны:
StackLayoutв .NET MAUI не добавляет дополнительных отступов по умолчанию. Если ваш UI «поехал», первым делом проверьтеSpacingиPaddingGridтеперь строже обрабатывает определения строк и столбцов- Используйте
VerticalStackLayoutиHorizontalStackLayoutвместоStackLayoutдля лучшей производительности
<!-- Было -->
<StackLayout Orientation="Vertical" Spacing="10">
<Label Text="Заголовок" />
<Entry Placeholder="Введите текст" />
</StackLayout>
<!-- Стало (оптимизированная версия) -->
<VerticalStackLayout Spacing="10">
<Label Text="Заголовок" />
<Entry Placeholder="Введите текст" />
</VerticalStackLayout>
10.2 MessagingCenter заменён
В .NET MAUI 10 класс MessagingCenter стал internal. Если вы ещё не перешли на WeakReferenceMessenger из CommunityToolkit.Mvvm — самое время:
// Установка пакета
// dotnet add package CommunityToolkit.Mvvm
// Определение сообщения
public class ItemUpdatedMessage : ValueChangedMessage<Item>
{
public ItemUpdatedMessage(Item value) : base(value) { }
}
// Отправка сообщения
WeakReferenceMessenger.Default.Send(new ItemUpdatedMessage(updatedItem));
// Подписка на сообщение
WeakReferenceMessenger.Default.Register<ItemUpdatedMessage>(this, (recipient, message) =>
{
var item = message.Value;
// Обработка обновления
});
WeakReferenceMessenger, на мой взгляд, лучше MessagingCenter по всем параметрам — он типобезопасен, использует слабые ссылки и не вызывает утечек памяти.
10.3 Замена Effects на Platform Behaviors
// Было (Xamarin.Forms Effect)
public class ShadowEffect : RoutingEffect
{
public ShadowEffect() : base("MyApp.ShadowEffect") { }
}
// Стало (.NET MAUI PlatformBehavior)
public partial class ShadowBehavior : PlatformBehavior<View>
{
public static readonly BindableProperty RadiusProperty =
BindableProperty.Create(nameof(Radius), typeof(float), typeof(ShadowBehavior), 10f);
public float Radius
{
get => (float)GetValue(RadiusProperty);
set => SetValue(RadiusProperty, value);
}
#if ANDROID
protected override void OnAttachedTo(View bindable, Android.Views.View platformView)
{
base.OnAttachedTo(bindable, platformView);
platformView.Elevation = Radius;
}
#elif IOS
protected override void OnAttachedTo(View bindable, UIKit.UIView platformView)
{
base.OnAttachedTo(bindable, platformView);
platformView.Layer.ShadowRadius = Radius;
platformView.Layer.ShadowOpacity = 0.3f;
}
#endif
}
11. Тестирование после миграции
11.1 Стратегия тестирования
После миграции критически важно провести тщательное тестирование. Не пропускайте этот этап, даже если «всё компилируется и вроде работает»:
- Юнит-тесты — запустите существующие тесты. Большинство должно работать после обновления пространств имён
- UI-тесты — проверьте визуальное отображение на всех целевых платформах
- Интеграционные тесты — убедитесь, что API, база данных и сторонние сервисы работают корректно
- Регрессионное тестирование — пройдите все основные пользовательские сценарии вручную
11.2 Пример юнит-теста
// xUnit проект для тестирования ViewModel
[Fact]
public async Task LoadItems_ReturnsExpectedData()
{
// Arrange
var mockService = new Mock<IApiService>();
mockService.Setup(s => s.GetItemsAsync())
.ReturnsAsync(new List<Item> { new Item { Id = 1, Name = "Тест" } });
var viewModel = new MainViewModel(mockService.Object);
// Act
await viewModel.LoadItemsCommand.ExecuteAsync(null);
// Assert
Assert.Single(viewModel.Items);
Assert.Equal("Тест", viewModel.Items[0].Name);
}
Ключевые области для проверки (не ленитесь, проверьте каждый пункт):
- Навигация между страницами (прямая, обратная, глубокие ссылки)
- Жизненный цикл приложения (фоновый режим, восстановление)
- Платформенные разрешения (камера, геолокация, файлы)
- Push-уведомления
- Локализация и RTL-макеты
- Адаптивный дизайн на разных размерах экрана
- Производительность прокрутки и анимаций
12. Оптимизация производительности после миграции
12.1 Compiled Bindings
Обязательно включите компилируемые привязки — они дают заметный прирост производительности биндинга:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodels="clr-namespace:MyApp.ViewModels"
x:DataType="viewmodels:MainViewModel">
<!-- Привязки компилируются, ошибки обнаруживаются на этапе сборки -->
<Label Text="{Binding UserName}" />
<CollectionView ItemsSource="{Binding Items}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Item">
<Label Text="{Binding Name}" />
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</ContentPage>
Бонус: ошибки в биндингах теперь ловятся при компиляции, а не в рантайме. Это одна из тех фич, к которым быстро привыкаешь и уже не можешь без них.
12.2 Native AOT (экспериментально)
В .NET 10 появилась экспериментальная поддержка Native AOT для мобильных платформ. Это значительно сокращает время запуска приложения:
<PropertyGroup>
<PublishAot>true</PublishAot>
<!-- Включение для iOS -->
<MtouchInterpreter>-all</MtouchInterpreter>
</PropertyGroup>
Будьте осторожны с AOT — фича ещё экспериментальная, и не все библиотеки с ней дружат. Тестируйте тщательно.
13. Пошаговый чек-лист миграции
Для удобства — итоговый чек-лист. Распечатайте его или скопируйте в свой таск-трекер:
- Провести аудит зависимостей и составить таблицу совместимости
- Обновить Xamarin.Forms до версии 5.0+
- Перевести проект на .NET Standard 2.0+
- Создать ветку для миграции в системе контроля версий
- Запустить .NET Upgrade Assistant для первоначального преобразования
- Преобразовать .csproj в SDK-стиль с мульти-таргетингом
- Объединить платформенные проекты в единую структуру
- Заменить пространства имён (C# и XAML)
- Создать MauiProgram.cs и перенести конфигурацию
- Заменить DependencyService на встроенный DI
- Мигрировать кастомные рендереры на обработчики
- Заменить Effects на PlatformBehavior
- Заменить MessagingCenter на WeakReferenceMessenger
- Обновить систему навигации (Shell)
- Перенести ресурсы в единую папку Resources
- Обновить стили и темы
- Запустить и исправить ошибки компиляции
- Провести юнит-тестирование
- Провести UI-тестирование на всех целевых платформах
- Провести регрессионное тестирование
- Профилировать производительность
- Обновить CI/CD пайплайн
- Развернуть в тестовую среду
- Опубликовать в App Store и Google Play
Заключение
Миграция с Xamarin.Forms на .NET MAUI — это серьёзный, но необходимый шаг для любого .NET-проекта мобильной разработки. Да, процесс может показаться масштабным. Но при правильном планировании и пошаговом подходе он вполне управляем — проверено на собственном опыте.
Что вы получите после миграции:
- Активная поддержка — регулярные обновления безопасности и совместимости
- Улучшенная производительность — обработчики вместо рендереров, Compiled Bindings, Native AOT
- Единая структура проекта — проще управлять и поддерживать
- Встроенный DI — стандартная модель внедрения зависимостей без сторонних библиотек
- Современный .NET — доступ ко всем возможностям .NET 10 и C# 13
- Совместимость с новыми ОС — поддержка Android API 36+, iOS 17+ и актуальных требований магазинов
Не откладывайте миграцию. Чем дольше вы остаётесь на неподдерживаемом Xamarin, тем больше копится технический долг и растут риски безопасности. Начните с аудита, выберите стратегию и двигайтесь шаг за шагом.
Удачной миграции!