Уже больше года занимаюсь поддержкой XAML-языков в нашем продукте, за это время пришлось разобраться с множеством деталей XAML-разметки и сломать голову о равномерно расставленные грабли мелкие отличия в поведении того или иного очередного модного XAML-фреймворка от Microsoft.

Написать этот пост меня подтолкнула история с x:FieldModifier - этот атрибут в XAML позволяет управлять модификатором доступа поля, автоматически генерируемого для элементов дерева, имеющих атрибут x:Name. В истории участвует и брат-близнец, атрибут x:ClassModifer - позволяет управлять модификатором доступа типа, который непосредственно декларирует XAML-файл (будет-ли ваш UserControl публичным или скрыт в internal?).

На самом деле атрибут x:FieldModifier имеет достаточно сомнительную полезность - если я создал окошко/страницу и разместил на нём кнопку с замечательным именем btn1, то зачем делать поле каким-либо, кроме как private/protected? К тому же поля мутабельные и, если кто-то вздумает их изменить снаружи, может начаться коллапс.

Ещё одна проблема с этим атрибутом - ответ на вопрос: какие значения атрибута допустимы? Ведь XAML-фреймворки позволяют использовать разные языки для codebehind-составляющей XAML-файла (если таковая имеется, конечно), а в разных языках (читай C# и VB.NET) приняты немного разные обозначения модификаторов доступа (Friend в VB.NET и internal в C#, например). Не представляю по какой причине дизайнеры первых версий XAML для WPF решили не определять независящий от языка codebehind’а набор допустимых значений x:FieldModifier и x:ClassModifier из основных модификаторов доступа, выражаемых в любом CLI-языке (public, internal, protected, private).

Тем не менее, design decisions уже давно были сделаны, пусть и не в пользу ide tools и здравого смысла. И надо как-то с этим жить и поддерживать, правильно парсить, подсвечивать неправильные значения атрибутов, сообщать синтетическим полям из элементов с x:Name правильные модификаторы доступа, показывать адекватный code completion у атрибутов и прочее. Поддержка этого добра в нашем продукте на самом деле очень простая, была сделана года эдак 4 назад и не вызывает опасений.

Но тут появился новый XAML-фреймворк, который я даже не знаю как правильно назвать в свете недавних копирайтных гонений слова “Metro”. Оригинально тип приложений в VS назывался “Metro style application”, потом перед релизом был переименован в “Windows Store application” (ну и причём здесь Store?), а в ленте твитера гуляют упоминания “Windows 8 style application”. Я буду называть этот XAML-фреймворк как “WinRT XAML”, так как все эти стрёмные названия скрывают самую суть - подсистема XAML написана на нативном коде, включена прямо в ядро Windows 8 и доступна через WinRT (новое модное объектно-ориентированное системное API, какбэ такое же фундаментальное для ОС, как великий и ужасный Win32 API).

Удивительно, но не смотря на то, что WinRT XAML написан на нативном коде, отличий от XAML в фреймворке Silverlight (и его профилях типа WP7) не так много, как могло бы быть. Однако отличия есть и разбросаны они по всему фреймворку ровным слоем. Есть и позитивные отличия, а есть и всякие новые форматы ссылок на пространства имён, которые делают любой XAML-код непереносимым на WinRT XAML без модификаций, что очень мило.

Недавно на меня пришёл реквест по поводу того, что поведение x:FieldModifier в WinRT XAML изменилось - теперь поля по-умолчанию (если не указан явный модификатор) являются private, хотя в WPF и всех Silverlight-фреймворках поля по-умолчанию становились internal (ну зачем, зачем так сделали???). Изменение с одной стороны позитивное, но возникают вопросы: зачем было делать breaking change, потенциально усложнить перенос кода? Почему private, а не protected?

Окей, смирившись с неизбежными изменениями, реализую корректную поддержку модификаторов по-умолчанию WinRT XAML. Тут что-то толкнуло меня проверить, что будет, если написать “Public” при codebehind’е на C#? Это не работает в WPF и SL, но работает в WinRT XAML, зараза такая! В сгенерированной XAML-дизайнером части будет написан модификатор “public” (в правильном регистре) и код будет валидным C#-кодом. Страница msdn говорит, что на вопросы регистра и вообще конвертирования строк к реальным модификаторам доступа отвечает CodeDom-провайдер того или иного языка. Почему CodeDom-провайдер ведёт себя иначе для WinRT? Скорее всего XAML-компилятор для WinRT вовсе не использует CodeDom…

Окей, дописываем разбор модификаторов доступа без учёта регистра для WinRT XAML, так как не хочется пугать юзера красным кодом от нашей поддержки, в случае если он написал капсом x:FieldModifier=”PUBLIC”, а проект компилируется и работает. Тут стало интересно, как же XAML-компилятор для WinRT дружит с разными языками, пишу в C# проекте x:FieldModifier=”Friend” и компиляция начинает падать, потому что в нагенеренный дизайнером C#-код пролезает модификатор “friend”. Всё понятно, в XAML-компиляторе для WinRT просто захардкоден набор модификаторов (объединение допустимых модификаторов C# и VB.NET), на которые он не ругается как на невалидные, а просто протягивает в дизайнерскую часть автоматически исправляя регистр, если нужно (то есть в WinRT XAML + VB.NET можно написать internal, это проглотит компилятор XAML, но упадёт уомпилятор VB.NET). Как это расширяется ещё один потенциальный язык - загадка (подозреваю, что никак). И тут я понимаю, что с VB.NET в качестве codebehind-языка наша XAML-поддержка тоже не использует разбор без учёта регистра…

Окей, исправляем эту досадную оплошность, в некотором роде повторяя логику из CodeDom-провайдеров. Фикс простой и красивый, но грусть в том, что каждый такой фикс мне нужно проверить N * M раз, где N - количество основных XAML фреймворков (WPF, SL5, WinRT, иногда ещё WP7, иногда отдельно WPF3.5/4), M - количество codebehind-языков (C# и VB.NET).

Окей, модификаторы в XAML с codebehind’ом на языке VB.NET действительно всегда проверяются без учёта регистра. Теперь пришло время проверить N * M раз поддержку атрибута x:ClassModifier. Сначала обнаруживается, что в WinRT XAML вообще не существует атрибута x:ClassModifier

Окей, выпиливаю x:ClassModifier из code completion только для WinRT XAML и вообще использование этого атрибута для вычисления модификатора доступа типов, определённых в XAML. Надо сказать, что изменение в каком-то смысле положительное, хоть и нарушающее обратную совместимость - в дизайнерских файлах XAML-компилятор не генерирует вообще модификаторов доступа для partial-частей классов (в отличие от XAML-компиляторов для WPF/SL). Это значит, что модификатор доступа всего XAML типа можно поменять одним изменением модификатора в codebehind-коде (в WPF/SL придётся ещё указывать соответствующий x:ClassModifier в XAML-частях, что несколько неудобно). Это же значит, что в XAML-файлах вовсе без codebehind-части изменить модификатор доступа вообще нельзя и тип всегда будет виден как internal (по правилам C#, страшно подумать что потенциально могут быть другие).

Окей, в страданиях доделываю поддержку “не существования x:ClassModifier” только для WinRT XAML. Слава Вселенной, что наша модель быстро позволила справиться с проблемой - XAML-файлу легко изобразил из себя partial-часть типа без явного модификатора доступа. Но на этом история не заканчивается, потому что проверяя Silverlight у меня закончились нервные клетки:

“Окей” тоже закончились. Добрый CodeDom-провайдер (на скриншоте XAML-файл в VB.NET-проекте) прекрасно валидирует содержимое атрибута x:FieldModifier и пропускает только модификатор Friend, однако случается что-то невероятное и в таком похожем атрибуте x:ClassModifier модификатор Friend становится невалидным и XAML-компилятор начинает требовать там C#’ный модификатор internal. Аррррррр. Но почему-то без учёта регистра, что не свойственно C#. РРРРРРРРР! В WPF+VB.NET такой проблемы конечно же нет, скорее всего это натуральный баг. Ну и как это поддерживать?

Попытаюсь сформулировать мораль сей истории. Да, описанные выше проблемы с большой вероятности никогда не каснутся обычных XAML-разработчиков и вообще не имеют серъёзных последствий. Тем не менее, история даёт представление о тех, кто придумывал изначальный дизайн XAML, о качестве/дизайне/внимании к мелочам/качеству тестирования в современных XAML-фреймворках.

Я искренне не понимаю как можно было допустить такую фрагментацию XAML-фреймворков, такую тонкую но тотальную несовместимость реализаций, иногда абсолютную непереносимость кода. Как можно допускать баги в таких фундумантельных механизмах, как модификаторы доступа? Как можно было так запутать разработчиков в стеках разработки и профилях фреймворков? Куда всё это катится? Какой из фреймворков не умрёт в агонии хотя бы через 5 лет? Почему для code completion самой продвинутой среде разработки в мире в 2012 году нужен компилируемый проект, в конце концов?

Единственное, что мотивирует работать над поддержкой XAML - уверенность в том, что можно победить зло на некотором достаточном уровне и отгружать разработчикам всё более достойный тул.