XAML обрабатывается следующим образом. Парсер проходит все элементы подряд, и превращает их в объекты. Из главного элемента (root element) выпарсиваются определения используемых пространств имён. Экземпляр объекта создаётся при встрече элемента (атрибута). Далее, если это атрибут, то его значение присваивается свойству описываемого объекта сразу, а если это вложенный элемент, то его содержимое будет создано рекурсивно из того, что внутри этого элемента. Присваивание сконструированного объекта свойству вышестоящего объекта происходит в момент завершения парсинга описания его содержимого, то есть при нахождении парного закрывающего тега. В этот момент объект полностью сконфигурирован вместе со всеми вложенными объектами, и будет присвоен свойству вышестоящего объекта (либо добавлен в него, если свойство вышестоящего объекта - одна из поддерживаемых коллекций). Если же мы дошли до конца XAML-документа, то сконструированный объект возвращается в качестве результата.
<Window>
<Window.Content>
<Panel>
<TextBlock Name="text" HorizontalAlignment="Center"></TextBlock>
<Button Name="btnMaximize" Caption="Maximize"></Button>
<Button Name="btnRestore" Caption="Restore"></Button>
</Panel>
</Window.Content>
</Window>
Как выполняется обработка этого XAML-документа:
- Создаётся
Window, вызывается конструктор по умолчанию. - Встретили вложенный элемент
Window.Content. Так как он начинается так же, как и тег текущего конфигурируемого объекта, то, это определение свойства -Content. Если бы вложенный тег не начинался сWindow., то парсер бы решил, что мы начали таким образом определять Content-свойство. Content-свойство по умолчанию имеет названиеContent, но может быть переопределено атрибутомContentPropertyAttribute. - Создаётся Panel, вызывается конструктор по умолчанию
- Встретили вложенный элемент, не начинающийся с
Panel.. Значит, мы должны определить Content-свойство для класса Panel - в этом случае это будет свойствоUIElementCollection Children. Все вложенные элементы (TextBlock и два Button) теперь будут определять значение этого свойства. - Создаётся
TextBlock(тоже конструктор по умолчанию), устанавливаются его свойстваNameиHorizontalAlignment. Тип свойстваName- String, поэтому конвертеры не применяются. Тип свойстваHorizontalAlignment- enum HorizontalAlignment, и к нему может быть применен стандартный механизм преобразования из String в Enum. TextBlockсоздан и все свойства сконфигурированы, мы встретили закрывающий тег, и теперь должны понять, куда его присвоить. Текущий конфигурируемый объект -Panel, и мы сейчас находимся в его Content-свойстве -Children. Оно реализуетIList, поэтому созданный TextBlock добавляется в него вызовом методаAdd.- Аналогичным образом в Panel.Children добавляются обе кнопки
- Мы достигли закрывающего тега
</Panel>. Это означает, что панель полностью сконфигурирована и будет присвоена свойству вышестоящего объекта -Window.Content. Свойство обычное (не коллекция), тип - Control, поэтому преобразований не нужно. - Тег
</Window.Content>говорит нам о том, что определение свойстваWindow.Contentзакончено. - И наконец тег
</Window>завершает конфигурирование объекта Window, который и возвращается в качестве результата.
Таким образом, приведённый XAML эквивалентен следующему коду:
Window window = new Window();
Panel panel = new Panel();
TextBlock textBlock = new TextBlock();
textBlock.Name = "text";
textBlock.HorizontalAlignment = HorizontalAlignment.Center;
panel.Children.Add(textBlock);
Button button1 = new Button();
button1.Name = "btnMaximize";
button1.Caption = "Maximize";
panel.Children.Add(button1);
Button button2 = new Button();
button2.Name = "btnRestore";
button2.Caption = "Restore";
panel.Children.Add(button2);
window.Content = panel;
По умолчанию названием Content-свойства будет Content. Если вы хотите, чтобы в качестве Content-свойства выступало другое свойство, то нужно пометить класс атрибутом ContentPropertyAttribute:
[ContentProperty("Controls")]
public class Grid : ControlПреобразование типов производится только встроенное - из строк в числа, перечисления и некоторые встроенные
конвертеры для структур (Thickness - для задания Margin). Если нужно использовать кастомный конвертер, его нужно вызвать с помощью расширения разметки Convert:
<Window xmlns:x="http://consoleframework.org/xaml.xsd"
xmlns:converters="clr-namespace:Binding.Converters;assembly=Binding">
<Window.Resources>
<string x:Key="testItem" x:Id="testStr">5</string>
<converters:StringToIntegerConverter x:Key="2" x:Id="str2int"></converters:StringToIntegerConverter>
</Window.Resources>
<Panel>
<TextBox MaxLenght="{Convert Converter={Ref str2int}, Value={Ref testStr}}"/>
</Panel>
</Window>Поддерживаются любые коллекции, реализующие IList, ICollection<T> или IDictionary<string, T>.
Если при обработке закрывающего тега выясняется, что свойство вышестоящего элемента (свойство, которое мы
определяем текущим объектом) реализует ICollection<T>, то текущий сконфигурированный объект будет добавлен
в эту коллекцию вызовом Add(T obj) - вместо стандартного поиска конвертера и последующего вызова сеттера.
Таким образом, для коллекций сеттер не нужен - нужен только геттер.
Аналогично обрабатываются свойства типа IDictionary<string, T>. Пример:
<Window.Resources>
<item x:Key="1">Строка</item>
<item x:Key="2">Строка 2</item>
</Window.Resources>Во время обработки тега первого закрывающего тега значение "Строка" будет добавлено в коллекцию Window.Resources.
Так как в случае параметризованных свойств парсер знает тип T, то перед тем, как вызвать метод Add (это
справедливо и для коллекций, и для словарей) будет произведена проверка на совместимость типа текущего
сконфигурированного объекта и T-типа-аргумента коллекции/словаря. Если типы не совместимы, будет выполнена
попытка поиска соответствующего конвертера. Если же свойство реализует непараметризованный IList, то объект будет добавлен без преобразований.
При вызове парсера XAML ему в качестве параметров передаётся набор пространств имён по умолчанию. Это список пространств имён CLR (путь к неймспейсу плюс имя сборки), в которых парсер будет искать типы создаваемых объектов (по именам тегов) и используемых расширений разметки. Все пространства имён, которые не включены в список дефолтных, должны быть указаны в декларации главного элемента XAML-документа.
Пример:
<my:Window Name="window2" Title="Очень длинное название окна"
xmlns:x="http://consoleframework.org/xaml.xsd"
xmlns:my="clr-namespace:ConsoleFramework.Controls;assembly=ConsoleFramework"
xmlns:converters="clr-namespace:Binding.Converters;assembly=Binding"
xmlns:xaml="clr-namespace:ConsoleFramework.Xaml;assembly=ConsoleFramework">
<!-- Здесь можно использовать типы и расширения разметки из всех указанных
пространств имён (не забывая указывать при этом префиксы) -->
</my:Window>Обычные объекты, создаваемые в XAML, требуют наличия конструктора по умолчанию. Если необходимо создать экземпляр класса, у которого нет конструктора по умолчанию, можно создать factory-класс и сделать это с помощью него. Но есть и встроенный класс ObjectFactory, который умеет создавать объекты любого типа, вызывать конструктор с определёнными аргументами и наполнять его свойства значениями, аналогично тому, как это бы работало, если бы мы создавали этот объект в XAML напрямую. Например, у нас есть класс
class TestClass<T>
{
public TestClass( int intProperty ) {
IntProperty = intProperty;
}
public int IntProperty { get; set; }
public string StringProperty { get; set; }
public T TProperty { get; set; }
}
С помощью ObjectFactory создать его экземпляр в XAML можно следующим образом:
<object TypeName="ConsoleFramework.Xaml.TestClass`1[System.String]">
<int x:Key="1">66</int>
<string x:Key="IntProperty">55</string>
</object>То, что передаётся в TypeName, резолвится через вызов Type.GetType(string assemblyQualifiedName), поэтому
возможно, что придётся указать полное имя типа вместе с именем сборки.
Устанавливая x:Key в число, мы говорим фабрике, что это - аргумент конструктора (с соответствующим индексом).
Если же x:Key не является числом, то x:Key интерпретируется как название свойства.
При определении значений аргументов конструкторов и свойств можно использовать все обычно доступные в этом
контексте инструменты - задание текстом или расширением разметки, или опять же с помощью расширения разметки
получить ссылку на ранее созданный объект по x:Id.
Работает это так: создавая экземпляр ObjectFactory, мы заполняем его Content-свойство, которое является
Dictionary<string, object>. Но после завершения конфигурирования этот объект заменяется тем, который мы
в нём описываем. Механика аналогична тому, как работают "примитивы" string, int и так далее, более того,
они все реализуют интерфейс IFactory, который позволяет подменять объект на другой (определяемой логикой
фабрики) в момент завершения разбора завершающего тега.
Позволяет задавать идентификатор любого инстанцируемого внутри XAML объекта, чтобы ссылаться на него потом при помощи расширения разметки Ref.
Задаёт ключи для коллекций IDictionary<string, T>.
Важно!
Чтобы использовать стандартные атрибуты (x:Key или x:Id), в рутовом элементе XML обязательно должна
быть ссылка на соответствующее пространство имён, причём заданное не через clr-namespace, а обычным образом - ссылкой на xaml.xsd:
<Window xmlns:x="http://consoleframework.org/xaml.xsd">
</Window>
Префикс x может быть заменён на любой другой.
Позволяет получить ссылку на другой объект по его x:Id:
{Ref Ref=myObject}
или
{Ref myObject}
Поддерживаются ссылки вперёд (механизм реализации полностью аналогичен WPF - через fixup tokens).
Позволяет получить тип (объект типа Type) по имени:
{xaml:Type TestProject1.Xaml.TypeExtensionTest.ObjectToCreate\, TestProject1\, Version\=1.0.0.0\, Culture\=neutral\, PublicKeyToken\=null}
Если вы знакомы с WPF, то проще не изучать документ целиком, а просто прочитать отличия:
- Нет Attached Properties
- Не генерируется код метода
InitializeComponent()partial класса, а создаётся объект в рантайме - Так как не генерируется код, то нет подписки на события - как обычные, так и Attached Events
- Так как не генерируется код, то и не генерируются поля классов по
x:Name - Нет поддержки включения словарей (хотя это, скорее всего, будет потом добавлено)
- Вместо
x:Name-x:Id - Возможные расхождения в способах обработки преобразований, добавления в коллекции - для уточнения механизма нужно всё-таки изучить соответствующие разделы этого описания
- Не поддерживаются XML-токены типа
"- если нужно что-то заэкранировать, используйте обратный слеш - В одинарных кавычках нельзя писать неэкранированные символы
=,{}- всё нужно экранировать - В расширения разметки всегда передаётся один объект в качестве Data Context, а не резолвится как в WPF относительно текущего значения Dependency property
DataContext. Это во-первых из-за отсутствия поддержки Dependency properties, а во-вторых, для простоты концепции XAML парсера и для возможности не интегрировать его с абстракциями, необходимыми именно для создания UI.