Справочник по разметке

Разметка IcyUI — XML-диалект для описания деревьев интерфейса. Она намеренно похожа на XAML из WPF и Avalonia, но частые случаи упрощены: встроенным элементам управления не нужны объявления пространств имён, а там, где элемент ожидает содержимое, можно написать обычный текст.

<StackPanel Orientation="Vertical" Padding="24">
  <TextBlock FontSize="28">Hello</TextBlock>
  <Button x:Name="ok" Padding="12,6">Click Me</Button>

  <Grid>
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="*"/>
      <ColumnDefinition Width="Auto"/>
    </Grid.ColumnDefinitions>
    <TextBlock Grid.Column="1">Cell</TextBlock>
  </Grid>
</StackPanel>

Разметка загружается классом MarkupLoader (см. Ваш первый экран) или через конвейер ресурсов с помощью MarkupImporter. Эта страница описывает сам язык.

Пространства имён

Форма Значение
(нет) Встроенные элементы управления IcyUI. Пространство имён по умолчанию подразумевается.
xmlns="https://icyui.dev/markup" То же встроенное пространство имён, указанное явно.
xmlns:x="https://icyui.dev/markup/x" Пространство имён директив. Неявно привязано к префиксу x, если вы не переопределили его.
xmlns:game="clr-namespace:MyGame.UI;assembly=MyGame" Пространство имён CLR в указанной сборке.
xmlns:game="clr-namespace:MyGame.UI" Пространство имён CLR в сборке распознавателя по умолчанию.

https://icyui.dev/markup — адрес английской версии этой страницы — служит идентификатором пространства имён разметки IcyUI.

Форма clr-namespace: работает как в WPF. Если assembly= опущено, типы ищутся в DefaultAssembly.

Как имя элемента превращается в тип

Для элемента <Name> или <prefix:Name>:

  1. С префиксом: определяется пространство имён префикса, затем Namespace.Name ищется в объявленной сборке.
  2. Без префикса: сначала ищутся встроенные элементы управления IcyUI, затем реестр коротких имён.
  3. Если ничего не найдено, загрузка завершается исключением MarkupException с именем тега и подсказкой, если существует тип с похожим именем.

Встроенные элементы управления всегда имеют приоритет над реестром коротких имён, а регистрация короткого имени, совпадающего со встроенным элементом, — ошибка в момент регистрации. Чтобы использовать свой тип вместо встроенного, укажите явный префикс.

Короткие имена для своих элементов управления

Элементы, которые игра использует постоянно, можно зарегистрировать под коротким именем, чтобы не писать префикс:

IcyConfiguration configuration = game.GetIcyConfiguration();
configuration.Types.Markup.RegisterShortName<HealthBar>();          // <HealthBar/>
configuration.Types.Markup.RegisterShortName<HealthBar>("Health");  // <Health/>

Форма clr-namespace: всегда работает без регистрации; короткие имена лишь избавляют от префиксов.

Атрибуты

Каждый атрибут разбирается в таком порядке, побеждает первое совпадение:

  1. Объявление пространства имён (xmlns, xmlns:*): обрабатывается парсером.
  2. Директива из пространства имён x: (см. Директивы).
  3. Присоединённое свойство: в имени есть . (см. Присоединённые свойства).
  4. Свойство типа элемента: сначала зарегистрированное свойство IcyUI, затем любое открытое записываемое CLR-свойство.
  5. Иначе загрузка завершается ошибкой. Неизвестные атрибуты — ошибки, а не молча пропускаются, поэтому опечатка не превратится в бездействие. Атрибут с именем события — тоже ошибка: подключать обработчики событий из разметки пока нельзя (используйте команды или подпишитесь в коде).

Зарегистрированные свойства предпочтительнее, потому что записанные через них значения участвуют в той же системе приоритетов, что стили, визуальные состояния и анимации. Запасной путь через CLR покрывает то, что не зарегистрировано: коллекции, которые наполняются, а не присваиваются (Panel.Children), и простые объекты данных (ColumnDefinition.Width).

Значения

  • Значение, являющееся расширением разметки, вычисляется, и используется его результат.
  • Любое другое значение преобразуется из строки к типу свойства, поэтому работают Padding="12,6", Background="#FF3C64C8" и Orientation="Vertical".

Значения из разметки — локальные значения, как и значения из кода: они побеждают стили и анимации.

Директивы

Директива Где допустима Значение
x:Name на любом UIElement Задаёт Name элемента и регистрирует его в области имён документа, чтобы код мог найти его через FindControl<T>(name). Имена в документе должны быть уникальны.
x:Class на корневом элементе Тип, создаваемый вместо типа тега (см. Связанные классы).
x:Key на элементе словаря Ключ, под которым хранится элемент, например в Resources.
x:DataType на любом элементе Объявляет ожидаемый тип контекста данных поддерева. Пути {Binding} внутри проверяются при загрузке и читаются через однажды найденные члены; опечатка в типе или пути — ошибка загрузки. Действует на элемент и всё внутри него, кроме шаблонов (DataTemplate, ControlTemplate): DataTemplate объявляет тип своих элементов собственным x:DataType. См. Виды привязок.

Расширения разметки

Значение атрибута считается расширением разметки, если оно начинается с { и заканчивается }. С пробелом в начале значение становится обычным текстом.

extension := '{' name ( WS arguments )? '}'
arguments := positional ( ',' named )*  |  named ( ',' named )*
named     := name '=' value
<TextBlock Text="{Binding UserName}"/>
<TextBlock Text="{Binding Path=UserName, Mode=OneWay}"/>
<Button Command="{Binding SubmitCommand}"/>

Первый аргумент может быть позиционным — что он означает, определяет каждое расширение. Значение можно заключить в одинарные кавычки, чтобы в нём были запятая или фигурная скобка: {Binding Path='A, B'}.

Чтобы записать буквальное значение, начинающееся с {, поставьте перед ним пустую пару фигурных скобок, как в XAML:

<TextBlock Text="{}{not an extension}"/>

Встроенные расширения

Расширение Назначение
{Binding ...} Привязывает свойство к данным. Аргументы: Path (позиционный), Source, ElementName, Mode, UpdateTargetTrigger, Compiled (True для компилируемого пути; нужен известный тип источника). См. Виды привязок и BindingExtension.
{Localize Key} Локализованная строка для ключа в культуре конфигурации; меняется вместе с культурой. Аргументы: Key (позиционный), Context. См. Локализация и LocalizeExtension.
{StaticResource Key} Ищет ресурс по ключу: в Resources создаваемого элемента, затем его предков вплоть до корня документа. Вычисляется один раз, при загрузке. См. StaticResourceExtension.
{TemplateBinding Path} Внутри шаблона элемента управления привязывает к свойству элемента, к которому применён шаблон. См. TemplateBindingExtension.

Привязка свойства, которое не может нести привязки (непривязываемого или незарегистрированного), — ошибка, а не предупреждение: иначе привязка молча пропала бы.

Свои расширения

Реализуйте IMarkupExtension и зарегистрируйте его через RegisterExtension. Тогда {MyThing} будет соответствовать вашему типу; имя по умолчанию — имя типа без суффикса Extension. MarkupExtensionContext передаёт расширению целевой элемент и член, область имён, конфигурацию и позицию в исходном файле. Возврат Unset сообщает загрузчику, что расширение само присвоило свойство (так {Binding} устанавливает привязку).

Контекст данных

{Binding} без Source и ElementName читает данные из DataContext. Контекст данных наследуется: элемент без собственного использует контекст ближайшего предка. Привязки продолжают следить за унаследованным значением, поэтому установка DataContext на корень уже после загрузки, как на странице Ваш первый экран, обновляет все привязки ниже.

Присоединённые свойства

Присоединённые свойства записываются как Owner.Property на элементе, к которому они относятся:

<TextBlock Grid.Column="1" Grid.Row="0">Cell</TextBlock>
<Widget game:Dock.Side="Left"/>

Часть до последней . разбирается как тип — так же, как имя элемента, — и свойство ищется у этого типа: Grid.Row принадлежит Grid, а не TextBlock.

Содержимое, дочерние элементы и элементы свойств

Свойство содержимого

Тип помечает одно своё свойство как содержимое атрибутом ContentPropertyAttribute. Ему присваиваются дочерние элементы и текст.

Тип Свойство содержимого Вид
Panel (StackPanel, Grid, ...) Children коллекция: каждый дочерний элемент добавляется
Border Child одно значение: присваивается
ContentControl (Button, Window, ...) Content одно значение: присваивается
TextBlock Text строка

Второй дочерний элемент у однозначного свойства содержимого — ошибка.

Элементы свойств

Свойство, значение которого нельзя записать строкой, задаётся элементом свойства <Type.Property>:

<Grid>
  <Grid.ColumnDefinitions>
    <ColumnDefinition Width="*"/>
  </Grid.ColumnDefinitions>
</Grid>

Что произойдёт, решает текущее значение свойства: в список добавляется каждый дочерний элемент, в словарь — каждый под своим x:Key, иначе присваивается единственный дочерний элемент.

Текстовое содержимое

Если у элемента есть текст и нет дочерних элементов, текст становится значением свойства содержимого:

<TextBlock FontSize="28">Hello</TextBlock>
<Button>Click Me</Button>

У Button содержимое — элемент, а не строка, поэтому текст автоматически оборачивается в TextBlock. То же происходит со строкой, заданной атрибутом или через привязку, везде, где ожидается элемент:

<Button Content="Play"/>
<Button Content="{Binding Title}"/>

Привязка создаёт новый TextBlock при каждом изменении значения; для часто меняющегося текста привязывайте сам текст: <Button><TextBlock Text="{Binding Score}"/></Button>. Текст без FontFamily использует шрифт по умолчанию из конфигурации (Segoe UI с настройками движков по умолчанию); если шрифт не найден, IcyUI пишет предупреждение в журнал.

Ресурсы и стили

У каждого элемента есть словарь Resources. Его записи объявляются как элементы свойства, и {StaticResource} находит их из самого элемента и всего, что внутри него. Объявляйте Resources до дочерних элементов, которые их используют: {StaticResource} вычисляется во время загрузки документа и видит только уже объявленные ресурсы.

<!-- Resources declared on an element apply to it and everything inside it. -->
<StackPanel Orientation="Vertical">
  <StackPanel.Resources>
    <!-- No x:Key: an implicit style, applied to every Button in this subtree that has no Style of its own. -->
    <Style TargetType="Button" Padding="16,6" Background="#FF3C64C8">
      <Style.StateGroups>
        <VisualStateGroup Name="CommonStates">
          <VisualState Name="Hovered" State="Hovered" Duration="0:0:0.2" Background="#FF5A87E6" />
        </VisualStateGroup>
      </Style.StateGroups>
    </Style>
    <!-- With x:Key: used only where a {StaticResource} names it. -->
    <Style x:Key="DangerButton" TargetType="Button" Padding="16,6" Background="#FFB03A3A" />
  </StackPanel.Resources>

  <Button Margin="0,0,0,8">New game</Button>
  <Button Style="{StaticResource DangerButton}">Delete save</Button>
</StackPanel>
  • Style задаёт свойства каждому элементу управления, к которому применяется. Его атрибуты — значения свойств, как у любого другого элемента.
  • Style без x:Key — неявный: он применяется к каждому элементу поддерева, чей тип в точности равен его TargetType (не производный тип) и у которого нет собственного Style.
  • Style с x:Key применяется только там, где его называет Style="{StaticResource Key}".
  • Style.StateGroups объявляет визуальные состояния, например цвет при наведении с плавным переходом.
  • Отдельный документ <ResourceDictionary> может объединять другие словари через <ResourceDictionary.MergedDictionaries>, в том числе файлы, загружаемые через конвейер ресурсов с Source="...".
  • Внутри словаря запись может использовать записи, объявленные до неё (Template="{StaticResource Skin}"), как и в Resources элемента.

Встроенная тема — именно такой словарь. Загрузите её методом UseDefaultTheme() (см. Icy.Configuration).

Связанные классы (x:Class)

Документ разметки может указать тип, который он создаёт:

<Page x:Class="MyGame.UI.MainMenuPage">
  <StackPanel>...</StackPanel>
</Page>

Загрузчик проверяет, что указанный тип наследует тип тега, и создаёт его вместо тега. Без x:Class создаётся сам тип тега. Это работает для любого элемента, не только для страниц.

Страницы и навигация

Page — элемент с содержимым, где содержимое — интерфейс страницы. Frame показывает страницы и владеет службой навигации (Frame.Navigation):

  • NavigateTo(page) показывает страницу, вызывая её OnNavigatedFrom/OnNavigatedTo.
  • Navigate(path) сначала загружает разметку страницы через конвейер ресурсов. Для несуществующего пути возвращается null, а не исключение.
  • История «назад/вперёд» хранит страницу, только если у неё установлен KeepAlive. Переход на новую страницу очищает историю «вперёд», как в браузере.
  • Page.Initialize() выполняется один раз на экземпляр страницы. Page.Prepare() — при каждой активации, включая возврат к сохранённой странице.

Ошибки

Каждая ошибка разметки — исключение MarkupException с путём к файлу, строкой и столбцом:

MyGame/UI/MainMenu.xml(14,6): Unknown property 'Bakground' on 'Button'. Did you mean 'Background'?
  • Загрузка останавливается на первой ошибке; частично построенное дерево не возвращается.
  • Сообщение называет файл, позицию, ошибочный символ и тип, в котором он встретился.
  • Для неизвестных имён свойств и типов предлагается похожий вариант.

Схемы

Две XML-схемы описывают неизменные части языка — для редакторов, которые дают автодополнение и проверку по XSD:

Примечание

Эти схемы — заготовки: они описывают пространства имён и директивы, но не каждый элемент управления и свойство. Схемы, сгенерированной по доступным элементам управления, пока нет.

Пока не поддерживается

Примечание
  • Обработчики событий в разметке (Click="OnPlay"): используйте команды или подписывайтесь на события в коде.
  • Вложенные расширения разметки, например {Binding Source={StaticResource Data}}.
  • Наследуемые текстовые свойства: FontFamily, FontSize и Foreground не передаются от родителя дочерним элементам. Задавайте их на каждом элементе или неявным Style.
  • Объявление новых свойств в разметке — для компонентов, целиком описанных разметкой.
  • Генератор исходного кода для документов с x:Class (типизированные поля для элементов с x:Name).