На дворе наступал Новый год, а из головы никак не выходила мысль, что XAML может быть лучше. И, чтобы ему быть лучше, ему нужно перестать быть. Так родилась затея написать альтернативу кошмарному и ужасному XAML'ю: без
Фичи:
Звучит классно? А выглядит оно так:
Когда/если допилю язык, будет выглядеть примерно так:
Сначала немного о грустном: это не библиотека, которую можно засунуть в проект, чтобы сразу стало хорошо. Это Proof-Of-Concept. Смысл написания библиотеки — доказать, что её можно написать, что не разверзнутся врата Ада, что работающий код может быть написан за вменяемый срок.
На разработку ушло немногим больше недели (да-да, я именно так провожу новогодние праздники). Вчера писал документацию на гитхабе, сегодня пишу эту статью. Поэтому к библиотеке не прилагается и прилагаться не может следующее: поддержка типов помимо встроенных во фреймворк, вменяемые и воспринимаемые человеками сообщения об ошибках, безбажность хотя бы в простых случаях, юнит-тесты.
Сейчас решительно закрыла вкладку в браузере наша прагматичная половина читателей. С идеалистами и мечтателями — продолжаем.
Для тех, кто хочет попачкать ручонки и не брезгует документацией на кривом английском — прошу в гости на гитхаб. Там в ReadMe и Wiki вся необходимая информация: процесс установки, синтаксис и прочее. Здесь — то же самое, но более кратко.
JSON по сравнению с XML довольно ограничен: у его «объектов» нет ни «типа», ни «содержимого». Поэтому тип (то, что в XML после <) задаётся денежным свойством "$", а внутренности (то, что в XML между
Ещё в денежное свойство можно заложить информацию про идентификатор (x:Name/x:Key), видимость (x:ClassModifier/x:FieldModifier), а в случае стилей и триггеров ещё и «неявные» ключи (TargetType/DataType). Полный синтаксис (квадратные скобки — опциональность):
Как уже было сказано, денежное свойство можно опускать, если тип «очевиден» (то есть свойство, в которое мы кладём объект, сообщает, что в него можно класть: T для IEnumerable<T>, ControlTemplate для ControlTemplate).
А теперь — полный список.
Можно по вкусу разбавлять выражения пробелами:
Ближе к делу. Вот пример, в котором значением байндинга является
Отделение выражения на C# от аргументов расширения разметки — для регулярных выражений задача непосильная, и единственное, что они могут — это проверить парность фигурных скобок. Собственно, выражение заканчивается на первой запятой после последнего суб-байндинга. Если такое поведение вас не устраивает, то можно задать конец выражения явно с помощью
Если выражение получается слишком сложное, то можно либо написать статический метод в code-behind, либо вынести логику в вообще другой класс — по вкусу.
Триггеры пишутся в объект «on». Имя свойства — байндинг. Значение — внутренность триггера (в том числе с объектом «set»). Можно писать байндинги-выражения, как описано выше. Триггер считается сработавшим, если он принимает булево значение True. Например:
Ну, и на закуску свойство
Выражаю благодарность авторам Json.NET, T4MultiFile, а также индусам, чей код я выковыривал из потрохов System.Xaml, чтобы распарсить «расширения разметки».
Пожалуй, самое большое неудобство со своим языком — это что поддержка всякими Решарперами отсутствует в принципе. Рефакторинг XAML решарпером сосёт и причмокивает, но он хоть какой-то есть. Плюс спустя шесть лет после появления WPF в решарпере таки родили хоть какую-то работу с DataContext и прочим. Если шесть лет ушло на официальный со всех сторон XAML, то про какой-то левый язык — и мечтать не приходится. И сбоку через плагин, наверное, ничего не приделать. В общем, придётся писать JAML, но постоянно заглядывать в XAML. Впрочем, как по мне, так всё равно лучше.
В общем, какие мысли? Как вам затея?
<Setter.Value>
, без {Binding Path=Name, RelativeSource={RelativeSource AncestorType={x:Type Button}}, Converter={StaticResource Converter}}
, без FirstValueEqualsToSecondValueOrThirdValueEqualsNullConverter
, без <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition/> <ColumnDefinition/> <ColumnDefinition/> </Grid.ColumnDefinitions>
, без <MultiDataTrigger> <MultiDataTrigger.Triggers> <DataTrigger> <DataTrigger.Binding> <MultiDataBinding>...
, без xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
, без всего этого барахла, от написания которого в десятый раз возникают позывы нежно погладить компьютер табуретом и вспоминаются далёкие индусские родственники разработчиков WPF.Фичи:
- Тёплый ламповый синтаксис JSON без кавычек вместо дьявольских уголовых скобок XML.
- Краткий и вменяемый синтаксис для markup extensions: километровые один-раз-написал-потом-читать-страшно-байндинги
{Binding Path=Name, RelativeSource={RelativeSource AncestorType={x:Type Button}}, Converter={StaticResource Converter}}
превращаются в почти присваивания{= ~Button.Name, Converter={@Converter} }.
- Кошерные выражения на C# на замену некошерным конвертерам:
{= ${=Property1} == ${=Property2} || ${=Property3} == null }
. - Смерть «элементной» записи свойств —
<Setter.Value>
уходят в небытие. - Зубодробительное повторение повторений объявляется устаревшим: если куда-то можно положить только
ColumnDefinition
, не надо повторять это десять раз. - Сеттеры и триггеры перестают быть многобуквенными сериализованными костылями: сеттеры выглядят как присваивание свойств, триггеры выглядят как условия.
- Смерть дублированию десяти «clr-namespace» с указанием имён соборок и прочей нечисти.
Звучит классно? А выглядит оно так:
_={
$: 'Window root',
Resources: [{
$: 'Style MyButtonStyle Button',
set: {
Background: 'Red', Foreground: 'Green'
},
on: {
'{=this.IsMouseOver}': {set: {
Background: 'Yellow', Foreground: 'Blue'
}}
}
}],
_: [{
$: 'Grid',
RowDefinitions: [ { Height: '*' } ],
ColumnDefinitions: [ { Width: '*' } ],
_: [{
$: 'Button btnPressMe', Content: 'Press me!', Style: '{@MyButtonStyle}'
}]
}]
}
Когда/если допилю язык, будет выглядеть примерно так:
Window root {
Resources: [
Style MyButtonStyle Button {
set: {
Background: Red, Foreground: Green
},
on: {
{=this.IsMouseOver}: {set: {
Background: Yellow, Foreground: Blue
}}
}
}
],
Grid {
RowDefinitions: [ { Height: * } ],
ColumnDefinitions: [ { Width: * } ],
Button btnPressMe {
Content: 'Press me!', Style: {@MyButtonStyle}
}
}
}
Сначала немного о грустном: это не библиотека, которую можно засунуть в проект, чтобы сразу стало хорошо. Это Proof-Of-Concept. Смысл написания библиотеки — доказать, что её можно написать, что не разверзнутся врата Ада, что работающий код может быть написан за вменяемый срок.
На разработку ушло немногим больше недели (да-да, я именно так провожу новогодние праздники). Вчера писал документацию на гитхабе, сегодня пишу эту статью. Поэтому к библиотеке не прилагается и прилагаться не может следующее: поддержка типов помимо встроенных во фреймворк, вменяемые и воспринимаемые человеками сообщения об ошибках, безбажность хотя бы в простых случаях, юнит-тесты.
Сейчас решительно закрыла вкладку в браузере наша прагматичная половина читателей. С идеалистами и мечтателями — продолжаем.
Для тех, кто хочет попачкать ручонки и не брезгует документацией на кривом английском — прошу в гости на гитхаб. Там в ReadMe и Wiki вся необходимая информация: процесс установки, синтаксис и прочее. Здесь — то же самое, но более кратко.
О странностях
Возможно, вы обратили внимание, что файл начинается с "_=". Это фиктивное присваивание убеждает Visual Studio, что файл — это самый что ни на есть JavaScript, а не какой-то неведомый природе JSON (к сожалению, даже в 2013 году редактора JSON не существует в природе, такие дела). Дополнение кода (IntelliSense) мы и так теряем, а так, покопавшись в настройках (Tools > Options > Text Editor > File Extension), мы можем получить подсветку и проверку скобок хотя бы на уровне Script Editor.JSON по сравнению с XML довольно ограничен: у его «объектов» нет ни «типа», ни «содержимого». Поэтому тип (то, что в XML после <) задаётся денежным свойством "$", а внутренности (то, что в XML между
и
) — свойством "_". Это временное неудобство, я подумываю над изменением языка. Посему XAML <Button Visibility="Visible">
<Button.ToolTip>
<TextBlock Text="Tool tip text"/>
</Button.ToolTip>
<TextBlock Text="Button text"/>
</Button>
приобретает вид JAML {
$: 'Button', Visibility: 'Visible',
Tooltip: { $: 'TextBlock', Text: "Tool tip text" },
_: { $: 'TextBlock', Text: "Button text" }
}
Ещё в денежное свойство можно заложить информацию про идентификатор (x:Name/x:Key), видимость (x:ClassModifier/x:FieldModifier), а в случае стилей и триггеров ещё и «неявные» ключи (TargetType/DataType). Полный синтаксис (квадратные скобки — опциональность):
[visibility] typeName [identifier [implicit identifier]]
Примеры: Button
Button btnCancel
private Button btnCancel
DataTemplate {~Button}
DataTemplate MyButtonTemplate {~Button}
Как уже было сказано, денежное свойство можно опускать, если тип «очевиден» (то есть свойство, в которое мы кладём объект, сообщает, что в него можно класть: T для IEnumerable<T>, ControlTemplate для ControlTemplate).
О markup extension'ах
JAML содержит огромное количество сокращений синтаксиса встроенных «расширений разметки», но в корне всех сокращений заложено очень маленькое (и, надеюсь, легко запоминаемое) количество идей:Сокращение | Значение |
---|---|
= |
Вычислять значение динамически (по большей части байндинги) |
@ |
Получить значение по ключу (по большей части ресурсы) |
~ |
Получить тип |
ref |
Получить элемент по имени |
Сокращение | Одним вызовом T4 превращается в неизящный... |
---|---|
{@Key} |
{StaticResource Key} |
{@=Key} |
{DynamicResource Key} |
{~TypeName} |
{x:Type TypeName} |
{static.TypeName.Property} |
{x:Static TypeName.Property} |
null |
{x:Null} (родной тип JSON) |
{tpl.Property} |
{TemplateBinding Property} |
{=PropertyPath} |
{Binding PropertyPath} |
{=} |
{Binding} |
{=ref.controlName.PropertyPath} |
{Binding PropertyPath, ElementName=controlName} |
{=this.PropertyPath} |
{Binding PropertyPath, RelativeSource={RelativeSource Self}} |
{=tpl.PropertyPath} |
{Binding PropertyPath, RelativeSource={RelativeSource TemplatedParent}} |
{=~TypeName.PropertyPath} |
{Binding PropertyPath, RelativeSource={RelativeSource AncestorType=TypeName}} |
{=@{...}.PropertyPath} |
{Binding PropertyPath, Source={...}} |
{= ${=Prop1} + 42 + ${=Prop2} } |
<Binding> или <MultiBinding> (см. ниже) |
{= ref.controlName.PropertyPath }
(но не переусердствуйте: всё-таки там внутри помесь слоноподобных регулярок с костыльными IndexOf, приправленные выковыренными потрохами System.Xaml).О выражениях и конвертерах
Теперь в байндингах можно писать выражения на C#. Суб-байндинги создаваемого мульти-байндинга (или байндинга, если суб-байндинг один) заключаются в скобки${...}
. За кулисами выражения превращаются в классы, реализующие интерфейсы IValueConverter
и IMultiValueConverter
. Не нужно забывать, что в конвертеры приходят object'ы, то есть обычно без преобразования типов не обойтись (автоматическое определение типов выражений — в планах). Напротив, проверки на DependencyProperty.UnsetValue
не нужны — проверка добавляется сама.Ближе к делу. Вот пример, в котором значением байндинга является
IsMouseOver == IsMouseDirectlyOver
: {= (bool)${=this.IsMouseOver} == (bool)${=this.IsMouseDirectlyOver} }
Это конвертируется в следующий XAML: <MultiBinding Converter="{x:Static my:MainWin._jaml_MainWindowConverter1}">
<Binding Path="IsMouseOver" RelativeSource="{RelativeSource Mode=Self}" />
<Binding Path="IsMouseDirectlyOver" RelativeSource="{RelativeSource Mode=Self}" />
</MultiBinding>
Отделение выражения на C# от аргументов расширения разметки — для регулярных выражений задача непосильная, и единственное, что они могут — это проверить парность фигурных скобок. Собственно, выражение заканчивается на первой запятой после последнего суб-байндинга. Если такое поведение вас не устраивает, то можно задать конец выражения явно с помощью
${}
. Например: {= string.Format("Visibility = {0}, Name = {1}", ${=this.Visibility}, param) ${}, ConverterParameter={@Name} }
Ещё вам не повезло, если фигурные скобки непарные (типа string.Format("{{{0}", value)
— не знаю, зачем надо, но мало ли) — тут вам в помощь эскейп-последовательности.Если выражение получается слишком сложное, то можно либо написать статический метод в code-behind, либо вынести логику в вообще другой класс — по вкусу.
О сеттерах в стилях и триггерах
Оные записываются как обычные свойства у объекта в свойстве «set».{
$: 'Style MyButtonStyle',
set: {
Width: 16, Height: 16,
Background: 'Red'
}
}
Если нужно задать Setter.TargetName
, то используйте синтаксис ref.controlName.PropertyPath
. set: {
'ref.btnCancel.Width': 16, 'ref.btnCancel.Height': 16,
'ref.btnCancel.Background': 'Red'
}
Если вы ненавидите скобки вокруг имён свойств, то можно писать доллары вместо точек: ref$controlName$PropertyPath
(доллар в JavaScript и JSON считается полноценным символом, в JAML он заменяется на точку). Такой же способ работает с attached properties.Триггеры пишутся в объект «on». Имя свойства — байндинг. Значение — внутренность триггера (в том числе с объектом «set»). Можно писать байндинги-выражения, как описано выше. Триггер считается сработавшим, если он принимает булево значение True. Например:
{
$: 'Style CheckBox',
on: {
'{=this.IsChecked}': {
set: { Background: 'Red', Foreground: 'Green' }
},
'{= (bool)${=this.IsMouseOver} && (bool)${=this.IsChecked} }': {
set: { Background: 'Yellow', Foreground: 'Blue' }
}
}
}
Синтаксис ref.controlName.PropertyPath
в триггерах шаблонов тоже работает.Ну, и на закуску свойство
Grid$
— оно принимает от одного до четырёх целых чисел и превращает их в Grid.Row, Grid.Column, Grid.RowSpan, Grid.ColumnSpan, соответственно. Пишешь Grid$='2 3'
— получаешь Grid.Row="2" Grid.Column="3"
.Ссылки
- Проект Jaml на GitHub, ReadMe прилагается.
- Wiki проекта Jaml на GitHub, инструкция по применению и примеры прилагаются.
Выражаю благодарность авторам Json.NET, T4MultiFile, а также индусам, чей код я выковыривал из потрохов System.Xaml, чтобы распарсить «расширения разметки».
Что дальше?
Сейчас непонятно, что с этим экспериментом делать, потому что есть куда более срочные дела, чем изменение мира (кушать-то хочется). Скорее всего, через некоторое время вернусь к проекту и доведу до состояния, когда могу использовать в своём реальном проекте. А пока мне интересно мнение окружающих: надо ли оно? будет ли кто-нибудь использовать? нужно переписать с нуля, или можно довести до ума имеющееся?Пожалуй, самое большое неудобство со своим языком — это что поддержка всякими Решарперами отсутствует в принципе. Рефакторинг XAML решарпером сосёт и причмокивает, но он хоть какой-то есть. Плюс спустя шесть лет после появления WPF в решарпере таки родили хоть какую-то работу с DataContext и прочим. Если шесть лет ушло на официальный со всех сторон XAML, то про какой-то левый язык — и мечтать не приходится. И сбоку через плагин, наверное, ничего не приделать. В общем, придётся писать JAML, но постоянно заглядывать в XAML. Впрочем, как по мне, так всё равно лучше.
В общем, какие мысли? Как вам затея?
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Ваше мнение о JAML?
3.07% Бросаю все дела! Бегу писать код и делать пулл-реквесты!18
11.93% Я бы такое использовал, если бы оно работало70
16.52% Я бы такое использовал, если бы оно поддерживалось студией и/или решарпером97
9.54% Я бы такое использовал, если бы Microsoft официально заменил XAML на JAML56
2.9% Мне строго фиолетово, на чём писать17
17.21% Я бы такое не использовал: не вижу существенных преимуществ над XAML101
4.43% Я бы такое не использовал: прочь грязные ручонки от тёплого лампового XAML26
6.81% Предать анафеме! Сжечь на ритуальном ыксымыльном костре!40
14.99% Я не пишу на WPF, поэтому сложно делать выводы88
12.61% Что такое WPF? Что такое XAML? Кто все эти люди?74
Проголосовали 587 пользователей. Воздержались 136 пользователей.