Перед каждым разработчиком, поддерживающим большое WPF или WinForms приложение, рано или поздно встает неудобный вопрос. Клиенты видят современные интерфейсы в новых программах и спрашивают: почему у нас все выглядит так устаревше? Менеджмент интересуется: можем ли мы добавить эти красивые элементы Fluent Design? А технический долг накапливается, словно снежный ком, который все труднее сдвинуть с места.
Полная переделка приложения требует месяцев работы и огромного бюджета. Оставить как есть значит смириться с постепенным устареванием продукта. Между этими крайностями существует третий путь, который я изучал последние три года на реальных проектах. Windows App SDK с его инструментарием предлагает возможность постепенной модернизации без остановки разработки основной функциональности.
Сталкиваясь с задачами миграции, я понял простую истину: универсального рецепта не существует. Каждый проект уникален, но паттерны и подходы повторяются. Технология XAML Islands в сочетании с WinUI 3 дает набор инструментов, которые можно комбинировать в зависимости от конкретной ситуации.
Фундамент: что стоит за аббревиатурами
Windows App SDK представляет собой эволюцию подхода Microsoft к desktop-разработке. Ранее он назывался Project Reunion, что отражало его суть: объединение разрозненных API в единую платформу. Ключевое отличие от предыдущих решений заключается в отделении от версий операционной системы. Вам больше не нужно ждать, пока пользователи обновятся до последней Windows 11, чтобы использовать новые возможности UI.
WinUI 3 это UI-фреймворк, который Microsoft развивает как замену устаревшим решениям. В отличие от UWP XAML, который был привязан к универсальным приложениям, WinUI 3 работает в обычных desktop-приложениях. Начиная с версии 1.6, Windows App SDK поддерживает Native Ahead-Of-Time (AOT) компиляцию, что дает более быстрый запуск приложения и меньший объем памяти. В версии 1.6 WinUI 3 перешел на типографическую модель выбора шрифтов вместо устаревшей модели weight/stretch/style, что открывает доступ к современным шрифтам вроде Segoe UI Variable.
XAML Islands технология, которая позволяет встраивать современные WinUI 3 контролы в окна существующих приложений. Представьте, что у вас старая квартира с добротным фундаментом, но устаревшей отделкой. Вместо сноса и новостройки вы делаете поэтапный ремонт комнат. Windows App SDK 1.4 добавил официальную поддержку XAML Islands для WinUI 3, хотя до этого технология существовала только для UWP контролов.
Важный момент, который часто упускают: DesktopWindowXamlSource это низкоуровневый класс, выступающий мостом между старым HWND-окном и новым XAML-контентом. Раньше Windows Community Toolkit предоставлял удобную обертку WindowsXamlHost для WPF и WinForms, но в версии 8 её убрали, рассчитывая на появление аналога в Windows App SDK 1.4. Этого не произошло, и разработчики создают собственные обертки или используют напрямую низкоуровневое API.
Стратегии миграции: выбираем дорогу под конкретный проект
Работая с различными командами, я выделил три основных подхода к миграции. Каждый имеет свою область применения.
Паттерн островной миграции подходит для крупных приложений, где переписать все сразу невозможно. Вы определяете участки интерфейса, требующие обновления в первую очередь. Допустим, старый DataGrid с ограниченными возможностями сортировки и фильтрации. Вместо доработки устаревшего контрола создается новый раздел на WinUI 3 с ItemsRepeater, который встраивается через XAML Island. Остальная часть приложения продолжает работать без изменений.
Основная сложность здесь управление фокусом клавиатуры. Когда пользователь нажимает Tab, переходя между элементами, фокус должен корректно перемещаться между старой и новой частями UI. DesktopWindowXamlSource предоставляет метод NavigateFocus, но вам нужно вручную перехватывать клавиши навигации и программно передавать управление.
Паттерн параллельных окон наиболее безопасный вариант. Основное приложение остается на WPF или WinForms, но новые функциональные блоки создаются как отдельные WinUI 3 окна. Когда пользователь открывает новый модуль, запускается современное окно с Fluent Design. Визуально будет заметна разница между окнами, зато вы избегаете технических проблем интеграции разных UI-фреймворков в одном процессе. Этот подход работал в проекте финансовой системы, где новые отчеты создавались на WinUI 3, а основной интерфейс оставался на WPF.
Полная миграция на WinUI 3 через создание нового проекта имеет смысл для относительно небольших приложений или когда бизнес готов инвестировать в серьезную модернизацию. Вы получаете единообразный современный интерфейс, лучшую производительность и полный доступ к новым API. TabView получил существенное обновление в версии 1.6 с новым режимом CanTearOutTabs, который позволяет перетаскивать вкладки из приложения подобно Microsoft Edge и Google Chrome. Такие возможности доступны только в полноценных WinUI 3 приложениях.
Архитектура данных: решаем головоломку data binding
Один из самых болезненных моментов миграции связывание данных между старым кодом и новыми контролами. ViewModel из WPF использует типы из пространства имен System.Windows.Input для команд, а также кисти и другие визуальные элементы из System.Windows.Media. WinUI 3 ожидает типы из Microsoft.UI.Xaml.Media и связанных пространств имен. Напрямую связать их невозможно.
Решение заключается в проектировании ViewModels, агностичных к конкретному UI-фреймворку. Выносите все ViewModels в отдельную библиотеку на .NET Standard 2.0 или современный .NET 6/8. Используйте CommunityToolkit.Mvvm, который предоставляет ObservableObject, RelayCommand и AsyncRelayCommand. Эти базовые классы работают одинаково хорошо в WPF, WinForms и WinUI 3.
Критический момент: XAML Island не наследует DataContext автоматически от родительского окна WPF. После создания экземпляра DesktopWindowXamlSource и загрузки в него WinUI 3 UserControl необходимо явно присвоить DataContext:
var xamlSource = new DesktopWindowXamlSource();
var myWinUI3Control = new MyWinUI3UserControl();
// Связывание данных (ключевой момент)
myWinUI3Control.DataContext = this.DataContext;
xamlSource.Content = myWinUI3Control;
В WinUI 3 рекомендуется x:Bind вместо классического Binding. Компилируемая привязка дает проверку типов на этапе компиляции и лучшую производительность. Однако в контексте XAML Islands есть нюанс: x:Bind по умолчанию ищет свойства в самом контроле (code-behind), а не в DataContext. Поэтому в WinUI 3 UserControl создайте типизированное свойство:
public sealed partial class MyWinUI3UserControl : UserControl
{
public MyWinUI3UserControl()
{
this.InitializeComponent();
}
public MySharedViewModel ViewModel => (MySharedViewModel)this.DataContext;
}
Затем в XAML используйте:
<TextBlock Text="{x:Bind ViewModel.UserName, Mode=OneWay}" />
Для коллекций данных применяйте ObservableCollection<T>. Этот класс реализует INotifyCollectionChanged и автоматически уведомляет UI об добавлении или удалении элементов. Если свойства внутри элементов коллекции тоже изменяются, каждый элемент должен реализовывать INotifyPropertyChanged.
Технические детали работы с Islands: глубже в реализацию
Интеграция WinUI 3 контролов в WPF требует понимания работы с HWND (дескрипторами окон Windows). В основе лежит класс DesktopWindowXamlSource из пространства имен Microsoft.UI.Xaml.Hosting.
Первый шаг инициализация Windows App SDK Runtime. Для unpackaged приложений (обычных exe-файлов без MSIX упаковки) необходимо вызвать Bootstrap.Initialize перед созданием первого WinUI контрола. Это загружает необходимые библиотеки в процесс приложения.
Затем создается экземпляр DesktopWindowXamlSource. В его свойство Content загружается нужный WinUI 3 UserControl или Page. После этого через COM-интерфейс IDesktopWindowXamlSourceNative нужно присоединить Island к дескриптору родительского окна. В WPF используйте WindowInteropHelper для получения HWND главного окна, затем вызовите метод AttachToWindow.
Изменение размеров требует особого внимания. WPF окно может менять размер при изменении пользователем или в коде, но Island автоматически не следует за этими изменениями. Подпишитесь на событие SizeChanged родительского контейнера и через Win32 API SetWindowPos корректируйте размер окна Island:
private void ParentContainer_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (_xamlSource != null && _xamlSourceWindowHandle != IntPtr.Zero)
{
var newSize = e.NewSize;
SetWindowPos(_xamlSourceWindowHandle, IntPtr.Zero,
0, 0,
(int)newSize.Width, (int)newSize.Height,
SWP_SHOWWINDOW);
}
}
[DllImport("user32.dll", SetLastError = true)]
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter,
int X, int Y, int cx, int cy, uint uFlags);
private const uint SWP_SHOWWINDOW = 0x0040;
Управление фокусом ввода еще одна головная боль. Когда пользователь переводит фокус в Island нажатием Tab или щелчком мыши, WPF теряет контроль над навигацией. Класс DesktopWindowXamlSource предоставляет метод NavigateFocus(XamlSourceFocusNavigationRequest), который позволяет программно передавать фокус. Требуется перехватывать нажатия клавиш Tab, Shift+Tab в обоих направлениях и вручно управлять передачей фокуса между старой и новой частями интерфейса.
Подводные камни, которые стоит знать заранее
Проблема DPI Scaling возникает практически в каждом проекте миграции. WinUI 3 поддерживает Per-Monitor V2 DPI awareness, что обеспечивает четкое отображение на мониторах с разной плотностью пикселей. Если старое WinForms приложение работает в режиме System Aware или вообще не поддерживает DPI, Islands будут выглядеть размытыми или иметь неправильный масштаб.
Решение обновить манифест приложения. Добавьте или измените секцию dpiAwareness:
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
PerMonitorV2
</dpiAwareness>
</windowsSettings>
</application>
После этого приложение будет корректно масштабироваться на разных мониторах, но потребуется протестировать все существующие экраны на разных DPI.
Airspace Issue известная проблема разработчиков WPF. Island представляет собой дочернее HWND-окно, которое рендерится поверх WPF-контента. Нельзя нарисовать элемент WPF над Island окно WinUI 3 всегда будет сверху, независимо от настроек Z-index. Это ограничивает возможности дизайна. Выпадающие меню WPF, всплывающие подсказки или диалоги будут перекрываться областью Island.
Приходится продумывать компоновку интерфейса с учетом этого ограничения. Если критически важно иметь всплывающие элементы поверх Island, рассмотрите вариант реализации этих элементов тоже на WinUI 3 внутри того же Island.
Развертывание приложения с Windows App SDK требует дополнительного внимания. Для unpackaged приложений нужно убедиться, что на машине пользователя установлен Windows App Runtime. Есть два варианта: включить runtime в установщик приложения или использовать self-contained deployment, когда все зависимости упаковываются вместе с exe-файлом. Последний вариант увеличивает размер дистрибутива на 50-100 МБ, но устраняет проблемы с версиями runtime на машинах пользователей.
Известная проблема: если отладчик настроен на перехват всех исключений C++, он будет останавливаться на некоторых исключениях при запуске в коде BCP47 (Windows Globalization). Это не влияет на работу приложения, но мешает отладке.
Паттерны для контролов: создаем переиспользуемые компоненты
При миграции часто возникает необходимость создавать кастомные контролы. WinUI 3 поддерживает templated controls компоненты с отделяемыми визуальными шаблонами.
Создание начинается с класса, наследующего Control из пространства имен Microsoft.UI.Xaml.Controls. Определяются dependency properties для параметров, которые можно устанавливать извне:
public class BgLabelControl : Control
{
public string Label
{
get => (string)GetValue(LabelProperty);
set => SetValue(LabelProperty, value);
}
public static readonly DependencyProperty LabelProperty =
DependencyProperty.Register(nameof(Label), typeof(string),
typeof(BgLabelControl),
new PropertyMetadata(default(string), OnLabelChanged));
private static void OnLabelChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var control = (BgLabelControl)d;
// Логика при изменении Label
}
public BgLabelControl()
{
this.DefaultStyleKey = typeof(BgLabelControl);
}
}
Визуальный шаблон описывается в файле Themes/Generic.xaml:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:MyApp.Controls">
<Style TargetType="local:BgLabelControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:BgLabelControl">
<Grid Background="{TemplateBinding Background}">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{TemplateBinding Label}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
Это позволяет изменять внешний вид контрола без изменения его логики. Потребители могут переопределить стиль и шаблон в своих приложениях.
Для сложных контролов с состояниями используется Visual State Manager. Он управляет анимациями и изменениями внешнего вида в зависимости от взаимодействия пользователя. Кнопка может иметь состояния Normal, PointerOver, Pressed, Disabled. Переходы между состояниями описываются декларативно в XAML, что делает код чище и понятнее.
Практический опыт: что работает на реальных проектах
В одном крупном проекте мы мигрировали WPF-приложение для управления производственными процессами. Полная переделка была невозможна из-за сроков и критичности системы для бизнеса. Решили начать с модернизации панели навигации.
Старое TreeView выглядело устаревшим и имело проблемы с производительностью при большом количестве элементов. NavigationView один из ключевых контролов WinUI 3, предоставляющий современный способ организации навигации. Заменили через XAML Island.
Первая попытка выявила проблему с темами. Старая часть приложения использовала корпоративные цвета с оттенками синего, а NavigationView по умолчанию приходил с системной темой Windows 11. Визуально это выглядело ужасно словно в одном окне жили два разных приложения.
Пришлось создать ResourceDictionary в WinUI части, где переопределили основные цвета и кисти:
<ResourceDictionary>
<SolidColorBrush x:Key="NavigationViewDefaultPaneBackground"
Color="#FF1E3A5F"/>
<SolidColorBrush x:Key="NavigationViewExpandedPaneBackground"
Color="#FF1E3A5F"/>
<SolidColorBrush x:Key="NavigationViewItemForeground"
Color="#FFFFFFFF"/>
</ResourceDictionary>
Application.Current.Resources в WPF и WinUI это разные словари, их нельзя просто так связать. Приходится дублировать ключевые стили или создавать сервис управления темами, который синхронизирует палитру при старте.
Следующий этап миграция сложных форм ввода данных. Здесь критически помогло выделение всех ViewModels в отдельную сборку. Использовали CommunityToolkit.Mvvm для реализации ObservableObject и команд. Это позволило иметь одну ViewModel, которая одинаково работала в старых WPF формах и новых WinUI 3 UserControls. Постепенно заменяли формы одну за другой без изменения бизнес-логики.
Проблемы с производительностью возникли при работе с большими списками данных. Старый DataGrid в WPF показывал 10000 записей, но жутко тормозил при скроллинге. Переход на ItemsRepeater в WinUI 3 с правильной виртуализацией дал заметное улучшение пользователь мог плавно скроллить десятки тысяч записей. Однако потребовалось оптимизировать data binding: использование x:Bind вместо Binding, применение функций вместо value converters где возможно, правильная реализация INotifyPropertyChanged без лишних уведомлений.
Развертывание решили делать через MSIX, хотя приложение изначально было unpackaged. Это дало доступ к автоматическим обновлениям через Microsoft Store for Business и упростило установку Windows App Runtime. Миграция на packaged модель потребовала изменений в коде некоторые API требуют package identity. Например, для использования share contract пришлось применять IDataTransferManagerInterop. Но результат того стоил.
Выводы из практики и взгляд в будущее
Технология XAML Islands продолжает развиваться. В Windows App SDK 1.7 добавлен современный TitleBar контрол, упрощающий кастомизацию верхней части окна приложения. Версия 1.8.3 расширила поддержку Windows ML на Windows 10 (версия 1809 и выше) для CPU и GPU нагрузок. Microsoft постепенно устраняет ограничения, которые мешали использовать всю мощь WinUI 3 в legacy-приложениях.
Для команд, начинающих миграцию, рекомендую следующий подход. Начните с аудита существующего кода. Определите, какие части UI наиболее проблемны, требуют новых возможностей или вызывают больше всего жалоб пользователей. Не пытайтесь мигрировать все сразу выберите один модуль, сделайте proof of concept, изучите проблемы на практике.
Инвестиции в правильную архитектуру данных окупаются многократно. Если ваши ViewModels зависят от WPF-специфичных типов вроде System.Windows.Media.Brush или используют Window.Current из UWP, рефакторинг займет значительную часть времени миграции. Чистая архитектура с разделением на слои, применение принципов SOLID и dependency injection существенно упрощают переход на новые технологии.
Миграция legacy-приложений это марафон, а не спринт. Нужно запастись терпением и методично двигаться к цели. Но с правильным подходом и инструментами можно постепенно обновить даже большие проекты, сохраняя стабильность работы и продолжая добавлять новую функциональность. Windows App SDK предоставляет для этого все необходимые средства, а опыт разработчиков по всему миру помогает избежать типичных ошибок.
Каждый успешно мигрированный модуль это маленькая победа на пути к современному приложению. Пользователи замечают улучшения, команда получает опыт работы с новыми технологиями, а бизнес видит реальную пользу от инвестиций в обновление технологического стека.