.NET
Development for Windows Phone
March 2012 30

Понимание XAML

From Sandbox
Для кого эта статья: для людей, которые только начинают своё знакомство с технологиями использующими XAML. Чтобы не усложнять статью, я не касаюсь многих деталей вроде Markup Extensions, управления ресурсами и т.п. Прочитав данную статью, я надеюсь, вы сможете понять что происходит под капотом XAML парсера и более чётко представлять как из вашего текстового документа получается граф объектов в памяти, с различными свойствами.

XAML — это язык разметки, который появился вместе с первой версией WPF от Microsoft. Сейчас он также используется в Silverlight и Windows Phone 7 (сути тот же Silverlight). Таким образом, сейчас довольно много людей активно используют XAML. Однако для эффективной работы полезно будет понять концепции, которые стоят за я языком, чтобы отдельные конструкции не казались странными.

В первую очередь стоит провести маленькое лирическое отступление. Существует два основных вида языков программирования: императивные и декларативные.

Императивные языки — это всем известные языки программирования, вроде C, C++, C#, Pascal, Basic и множество других. Основная идея в том, что в императивном языке мы говорим, что нужно сделать. Но не говорим, что должно получиться (обычно это мы должны описать и проверить в unit-тестах).

Декларативные языки, в обратную сторону, позволяют нам описать состояние, которого мы хотим добиться, но не требуют (и обычно не дают) описать как прийти в это состояние. Примеры таких языков: XAML (да и вообще все основанные на иерархической разметке XML, HTML и т.п.), также SQL.

Итак в чём разница?

Допустим я хочу создать TextBox и задать ему в текст «Habr», на C# это будет выглядеть так:
var tb = new TextBox();
tb.Text = "Habr";

На XAML это будет выглядеть так:
<TextBox Text="Habr"/>

Разница очевидна. В первом случае, я сказал:
1. Создать экземпляр класса TextBox и присвоить его переменной tb.
2. Присвоить свойству переменной tb.Text значение «Habr».

Во втором, я сказал, что хочу получить в итоге TextBox со значением «Habr» в тексте. А уже как это будет сделано меня не волнует, этим занимается XAML парсер. Такое длинное отступление важно, чтобы понять как работает парсер.

Итак, теперь более подробный пример, с объяснением работы парсера:
<UserControl x:Class="WpfApplication1.UserControl1"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <TextBox Text="Habr" Foreground="Yellow">
        <TextBox.Background>
            <SolidColorBrush Color="Red"/>
        </TextBox.Background>
    </TextBox>
</UserControl>

Что же тут просиходит?
Начнём с простого: все тэги, в которых нет точки "." в XAML заставляют парсер создать экземпляр класса. Как парсер знает какой класс создать? Это делается за счёт установки соответствия между пространствами имён XML указанными в начале XAML и пространствами имён .Net.
Записи вида xmlns=«schemas.microsoft.com/winfx/2006/xaml/presentation» указывают парсеру какие будут использоваться пространства имён. Причём некоторые пространства связаны с .Net пространствами по умолчанию (то есть не требуют указания пространства из .Net), как в данном примере. «schemas.microsoft.com/winfx/2006/xaml/presentation» связано с System.Windows.Controls из PresentationFramework.dll. Для других нужно явно указывать сооветствие: xmlns:CustomPrefix=«clr-namespace:WpfApplication1»
В этом примере
  • CustomPrefix — любой идентификатор легальный в XML, который вы будете использовать для обращения к своим объектам.
  • clr-namespace: — специальный префикс, который обозначает, что дальше пойдёт пространство имён .Net
  • WpfApplication1 — собственно ваше пространство имён.


После того как вы объявите ваше собственное пространство имён, вы можете создавать элементы из него:

<CustomPrefix:CustomObject/>


Итак наш XAML заставляет парсер создать экземпляр класса WpfApplication1.UserControl1, потом парсер видит, что мы хотим, чтобы в свойстве Content нашего контрола находился TextBox, парсер и это сделает и так далее.

Хорошо, с объектами разобрались. Но ведь есть ещё свойства. Для свойств есть два варианта синтаксиса:
  1. Атрибуты:
    <TextBox Text="Habr"/>
  2. Тэги:
    
    <TextBox>
        <TextBox.Text>Habr</TextBox.Text>
    </TextBox>
  3. Есть ещё вариант 2.1. Когда для наиболее часто используемого свойства можно задать содержимое просто указав его внутри объекта:
    
    <TextBox>Habr</TextBox>
    Эта запись эквивалента пункту 2, потому что объект TextBox отмечен атрибутом
    [ContentProperty("Text")]

Теперь подробнее о двух вариантах:
1. В XML атрибутах, вполне очевидно, можно хранить только строки. Поэтому какой бы ни был тип свойства, которое вы хотите установить, на самом деле вы задаёте строковое значение. А уже во время создания объекта парсер конвертирует это значение из строки к тому типу, который требуется.
Примитивные типы, вроде int, DateTime, парсер умеет конвертировать сам, по сути он просто вызывает метод Parse соответствующего типа. Но как же быть со сложными объектами?
Простой пример:
<TextBox Background="Red"/>

Свойство TextBox.Background имеет тип Brush — это абстрактный класс, у которого есть несколько конкретных реализаций, например SolidColorBrush, LinerGradientBrush и другие. Так как же наша строка «Red» превращается в подкласс Brush? За это отвечает конвертер. Чтобы указать какой конвертер применить для определённого типа на тип устанавливается TypeConverterAttribute. Для многих встроенных типов уже есть конвертеры, в том числе как в нашем примере, есть конвертер из строки в Brush, который создаёт экземляр SolidColorBrush и задаёт ему цвет указанный в строке.
Что делать, если вы хотите установить значение свойству не поддерживаемому стандартным конвертером или просто провести какую-то операцию над значением перед установкой? — Использовать собственный конвертер. Для этого достаточно реализовать интерфейс IValueConverter, в нём записать все необходимые манипуляции, а потом использовать конвертер в XAML следующим образом:
<TextBox 	Background="{Binding Source='Red-Green', Converter={StaticResource GradientColorConverter}}">

Конечно данный пример выглядит несколько странно, но чаще всего данные берутся из объектов бизнес-логики, тогда всё встанет на свои места.
И конечно, чтобы пример сработал, перед использованием конвертер нужно добавить в ресурсы, например так:

<TextBox  Background="{Binding Source='Red-Green', Converter={StaticResource GradientColorConverter}}">
        <TextBox.Resources>
            <WpfApplication1:GradientColorConverter x:Key="GradientColorConverter"/>
        </TextBox.Resources>        
    </TextBox>

Чтобы не захламлять статью, я не буду тут подробнее рассказывать о ресурсах, лучше почитать другие статьи или примеры.

2. Второй вариант как можно задать свойству значение в виде сложного объекта: использовать объектный синтаксис:

<TextBox Text="Habr">
        <TextBox.Background>
            <SolidColorBrush Color="Blue"/>
        </TextBox.Background>        
    </TextBox>

Тут всё уже должно быть ясно. Создаём отдельный тэг для свойства объекта TextBox, а в нём создаём экземпляр SolidColorBrush или любого другого подтипа Brush с нужными нам параметрами.

На этом введение в концепцию XAML стоит закончить, надеюсь после прочтение этой статьи, некоторые конструкции языка станут понятнее, а главное будет легче создавать собственную разметку.

UPD Обновил часть про конвертеры благодаря комментариям afsherman.
+8
58.9k 110
Comments 14
Top of the day