Накипело
Уже больше года занимаюсь поддержкой 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 - уверенность в том, что можно победить зло на некотором достаточном уровне и отгружать разработчикам всё более достойный тул.