XAML и Data Binding: Расширенные возможности разметки и связывания данных в Silverlight

WPF, MVVM, Silverlight | создано: 26.04.2011 | опубликовано: 26.04.2011 | обновлено: 13.01.2024 | просмотров: 52471 | всего комментариев: 2

В этой статье я постараюсь показать, как можно использовать дополнительные возможности XAML-разметки. А также некоторые интересные моменты Data Binding как в XAML, так и в code-behind.

Комментарии в XAML

Начнем с самого простого, но не менее полезного. Комментарии – помогают читать код, отключать/включать временно куски кода при отладке.

<!-- Это простой комментарий -->

Это тоже простой комментарий, но только внутри закомментирован некий код:

<!--<Image Margin="0,0,0,0" Source="Untitled-2.png" Stretch="Fill" x:Name="BackgroundPng"/>—>

Однако, учтите, что вложенные комментарии сделать не получится. Например, в данном примере внутрь тега Grid вложен комментарий, такая разметка выдаст ошибку:

<Grid>
       <!-- <TextBlock Text="{Binding Path=Name}" /> -->
</Grid>

На заметкуДля того чтобы закомментировать выделенный фрагмент кода, можно нажать сочетание клавиш CTRL+K+C, а для обратного эффекта нужно нажать CTRL+K+U.

Определение констант в XAML

В разметку XAML можно определять константы некоторых простых типов, например, string, int, bool. Для того чтобы можно было сделать требуется добавить namespace System в документ:

xmlns:sys="clr-namespace:System;assembly=mscorlib"

Теперь в разметке можно задать константы в Resources :

<UserControl.Resources>
  <sys:String x:Key="OKText">OK</sys:String>
</UserControl.Resources>

Теперь это значение можно использовать по такому же принципу как и StaticResource:

<Button Content="{StaticResource OKText}" />

На заметкуОднако, по такому принципу нельзя определить в XAML константу типа DateTime.

Перечисление в XAML

Довольно часто приходится применять в XAML перечисления (enum), которые в code-behind должны выглядеть следующим образом. Есть такое перечисление:

public enum MyLovelyEnum
{
    Normal, Super, Simple
}

В code-behind использование было бы таким образом:

MyLovelyEnum Lovely = MyLovelyEnum.Normal | MyLovelyEnum.Super;

Так вот, в XAML данное определение будет выглядеть так:

Lovely="Normal,Super"

DataContext vs Source (Data Binding)

В первую очередь хотелось бы показать применение свойства Source у объекта Binding. Дело в том, что можно использовать и свойство Source и DataContext.

<TextBox DataContext="{StaticResource productResource}" Text="{Binding Name}" />

Или вот второй вариант:

<TextBox Text="{Binding Name, Source={StaticResource productResource}}" />

Эффект от применения того или иного способа будет одинаковым, за исключением некоторого нюанса. Если используется DataContext, то его “действие” распространяется на все контролы расположенные ниже по иерархии в визуальном дереве (VisualTree). Такого не происходит, если использовать Source, то есть привязка, таким образом, происходит “точечно” или “целенаправленно”. Это оправдано, когда в контексте одной формы требуется использовать несколько поставщиков данных.

ElementName Binding

Название параметра ElementName в классе Binding говорит само за себя. Привязка осуществляется к именованному контролу. Вот простой пример привязки:

<StackPanel>
   <TextBox Name="FirstTextBox" />
   <TextBox Name="SecondTextBox" Text="{Binding Text, ElementName=FirstTextBox}" />
</StackPanel>

Такая привязка приведет к тому, что при изменении текста в поле TextBox с именем FirstTextBox незамедлительно изменится текст в у контрола с именем SecondTextBox. Обратного действа не произойдет, потому что по умолчанию такой важный параметр как Mode у Binding имеет значение OneWay (в одну сторону). Но если установить значение этого параметра TwoWay, то при изменении текста в любом из контролов, второй тут же получит измененное значение.

Еще одним немаловажным свойством, которое чаще всего пишется, но подразумевается по умолчанию – это свойство Path. Разметка типа:

Text="{Binding Text, ElementName=FirstTextBox}"

и

Text="{Binding Path=Text, ElementName=FirstTextBox}"

идентичны.

RelativeSource Binding и TemplatedBinding

Хочется верить, что Вы уже не только сталкивались, но и не раз использовали способы привязки данных указанных в заголовке раздела. Внесу пояснения: RelativeSource применяется когда необходимо связать данные с источником относительно цели. Например, для получения ссылки на источник, чтобы привязаться к его свойству, необходимо использовать RelativeSource. Правда, в отличие от WPF, в Silverlight существует некоторое ограничение. Вы не сможете получить доступ к источнику используя AncesstorType, но можете довольствоваться Self и TemplatedBinding.

При связывании с использованием режима Self возвращает контрол (сам себя). Такой режим полезен, когда необходимо связать свойства этого же контрола. Например, требуется у TextBox в выпадающую подсказку (ToolTip) привязать значение свойства Text. Для такой задачи как раз и очень подходит RelativeSource:

<TextBox Text="{Binding Path=CurrentItem}"
     ToolTipService.ToolTip="{Binding Text,  RelativeSource={RelativeSource Self}}" /> 

В отличие от Self-режима, TemplatedBinding используется  только когда связывание идет шаблоне контрола (Control Template) или в шаблоне данных (Data Template). TemplatedBinding возвращает представление контента (content presenter) для этого шаблонного контрола. Например, это выражения привязки получит значение актуальной высоты контента представления (content presenter):

"{Binding RelativeSource={RelativeSource TemplatedParent}, Path=ActualHeight}"

На заметку"{Binding RelativeSource={RelativeSource TemplatedParent}}" и "{TemplateBinding}" эквивалентны. Единственное отличие, так это то что TemplateBinding всегда использует режим направления связывания OneWay.

Привязка свойства контрола к DataContext

Порой приходится в свойство контрола, а еще чаще в параметр как-либо команды передать DataContext текущего контрола (объекта). Тут, на самом деле, всё просто да безобразия, надо просто указать "пустую" привязку без параметров:

 "{Binding}"

Кстати, если Вы читаете эту статью сначала, то в курсе, что необязательный параметр Path присутствует всегда неявно. И в данном случаи, при такой привязке, он примет такое значение:

"{Binding Path="."}"

что эквивалентно

"{Binding Path=}"

Привязка свойств в Code-Behind

Допустим, что требуется привязать какое-то свойство из Code-Behind класса в какому-нибудь свойству какого-то контрола. Существует два варианта решения. Первое, привязать DataContext верхнего в иерархии UI-объекта таким образом:

DataContext="{Binding RelativeSource={RelativeSource Self}}" 

А потом требуемые свойства привязывать используя тот самый DataContext:

<TextBlock Text="{Binding UserName}" /> 

И второй вариант, использовать ресурсы.

Использование Resources для привязки данных

В этом примере для полноты картины я буду использовать подход с применением MVVM, но Вы можете не использовать, а по-прежнему пользовать code-behind концепцию. Итак, чтобы осуществить привязку к какому-либо свойству какого-либо класса, надо этот самый класс создать:

public class SampleClassViewModel
}
    public string Name { get; set; }
    public string OrderDescription { get; set; }
}

Этот класс может магическим образом наполняется данными где-то в code-behind, а можно и наполнить этот класс данными и в XAML. Итак, нам требуется создать экземпляр этого класса в XAML. Для этого добавим на страниц необходимый namespace:

xmlns:vm="clr-namespace:SilverlightApplication12.ViewModels" 

А теперь в ресурсах UI-элемента самого верхнего уровня (у меня он называется UserControl) создам требуемый экземпляр:

<vm:SampleClassViewModel x:Key="sampleClass" Name="Накладная"
       OrderDescription="Простой документ для тестирования" /> 

На заметкуТолько некоторые типы данных можно наполнить в XAML. Можно: string, int, double. Для других более комплексных типов, таких как decimal и DateTime, требуется конверторы для применения к свойствам.

А теперь к созданному в ресурсе объекту можно осуществить привязку. Более того, все данные привязанные таким образом, будут доступны в desing-time в привязанных контролах:

<TextBox Text="{Binding Name, Source={StaticResource sampleClass}}" /> 

Привязка вложенных свойств (Nested Properties Binding)

Как уже было сказано выше, осуществить привязку к свойству объекта можно так:

<TextBox DataContext="{StaticResource riaServiceWrapper}"
     Text="{Binding TotalRecordsLoaded}" /> 

Однако, мой класс обертка на RIA-Service имеет вложенные свойства, например, TotalRecordsLoaded имеет свойствами значения для всех таблиц. То есть, я могу запросив у класса программно значение свойства TotalRecordsLoaded.Person получить количество записей в таблице Persons. Такой же принцип можно использовать и в XAML:

<TextBox DataContext="{StaticResource riaServiceWrapper}"
            Text="{Binding TotalRecordsLoaded.Person}" /> 

Примем "путешествовать" вниз по иерархии можно на несколько уровней и на столько далеко, на сколько Вам нужно.

Привязка к индексируемым свойствам (Indexed properties)

Перейду сразу к примеру. Предположим, что есть какой-то класс, у которого есть индексированное свойства Address. Чтобы привязать к индексу значение требуется проделать следующее:

<TextBox DataContext="{StaticResource personResource}"
         Text="{Binding Address[0].AddressLine1}" />

Более того, если у индексируемое свойство типа string (такое как Dictionary<string, string>), то можно привязку осуществить даже так:

<TextBox DataContext="{StaticResource personResource}"
         Text="{Binding Address[HOME].AddressLine1}" /> 

То есть использовать вместо индекса именование ключа.

Привязка коллекции (Collection Binding)

Привязку свойства к коллекции можно осуществить используя Resources, о котором говорилось выше. Пример Binding к коллекции. Для начала создадим простой класс Person, который будет "участвовать" в коллекции, и класс SampleData, который будет держать коллекцию:

1:public class Person
{
    public string Name { get; set; }
}

public class SampleData
{
    public PagedCollectionView PeopleView { get; set; }

    public SampleData()
    {
        List<Person> people = new List<Person>();
        people.Add(new Person() { Name = "Homer" });
        people.Add(new Person() { Name = "Marge" });
        people.Add(new Person() { Name = "Bart" });
        people.Add(new Person() { Name = "Lisa" });
        people.Add(new Person() { Name = "Maggie" });
        PeopleView = new PagedCollectionView(people);
    }
}

А теперь создадим экземпляр коллекции в ресурсах:

<models:SampleData x:Key="sampleDataResource" /> А теперь осуществим привязку:
<StackPanel DataContext="{Binding Path=PeopleView,
            Source={StaticResource sampleDataResource}}">
    <ListBox ItemsSource="{Binding}" DisplayMemberPath="Name" Height="120" Width="120" />
    <TextBox Text="{Binding Name}" Width="120" />
</StackPanel>

На заметкуВместо PagedCollectionView можно воспользоваться классом CollectionViewSource как прокси-классом от коллекции PagedCollectionView, для того чтобы осуществлять привязку прямо в XAML.

Дополнительные параметры привязки (Extending Binding)

В Silverlight 4 появился параметр StringFormat. Использование этого параметра позволяет отформатировать привязанные данные в соответствие со стандартами языка приложения. Ознакомьтесь с примерами применения.

Денежный (currency):

 <TextBox Text="{Binding TotalCost, Mode=TwoWay, StringFormat=C /> 

Дата и время:

 <TextBox Text="{Binding StartDate, Mode=TwoWay, StringFormat=D />

Дата и время специальный формат:

<TextBox Text="{Binding StartDate, Mode=TwoWay, StringFormat=yyyy-MM-dd />

Дата и время со специальным символами необходимо брать в одинарные кавычки:

<TextBox Text="{Binding StartDate, Mode=TwoWay, StringFormat=’MMM dd, yyyy’ />

Или использовать специальный символ:

<TextBox Text="{Binding StartDate, Mode=TwoWay, StringFormat=MMM dd\, yyyy />

Числовой и с плавающей точкой:

<TextBox Text="{Binding PI, Mode=TwoWay, StringFormat=0.00 />

Еще один полезный, на мой взгляд, параметр TargetNullValue. Если значение свойства может получать null, то этот параметр автоматически подменит null на указанное Вами значение. Пример применения:

<TextBlock Text="{Binding Total, TargetNullValue=0 />

Здесь, если значение Total не задано изначально, то по идеи должно быть null, так вот благодаря этому параметру вы увидете не пустое место, а цифру 0.

Еще одним полезным параметром в связывании является FallbackValue, принцип применения которого аналогичен принципу применения предыдущего параметра, только здесь речь идёт о значение null в контексте DataContext.

<TextBox Text="{Binding Name, Mode=TwoWay, FallbackValue=Пусто! />

Следующий по порядку в нашем списке параметр по имени UpdateSourceTrigger. Как гласит описание параметра на MSDN: "Получает или задает значения, определяющего график обновления источника привязки." По умолчанию значение свойства - Default, что означает автоматически обновлять значение источника привязки. В Silverlight доступно еще и значение Explicit, которые возложит обязанности по обновлению источника на программиста. А теперь примеры:

В таком варианте привязки:

<TextBox Name="NameTextBox" Text="{Binding Name, Mode=TwoWay}" />

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

<TextBox Name="NameTextBox"
         Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=Explicit}" />      

В таком варианте, когда контрол TextBox потеряет фокус, значение связанных свойств не изменяться. Но, например, при нажатии на кнопку закрытия формы, или подтверждения какой-либо операции можно применить изменения:

BindingExpression expr = NameTextBox.GetBindingExpression(TextBox.TextProperty);
expr.UpdateSource();

Конверторы (IValueConverter)

Предположим, что у вас какой-то класс имеет свойство IsVisible, которое имеет тип Boolean. UIElement реализует видимость объекта немного по-другому, у него есть свойство Visibility, которое принимает значения Visible и Collapsed. Таким образом, привязать видимость свойство из вашего класса к объекту типа UIElement не получится. Тут на помощь и приходят конвертеры. Конверторы, это классы, которые реализуют интерфейс IValueConverter, который в свою очередь, предоставляет два метода: Convert и ConvertBack.

Пример конвертора (BoolToVisibility)

public class BoolToVisibilityValueConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter,
                            System.Globalization.CultureInfo culture)
    {
        return (bool)value ? Visibility.Visible : Visibility.Collapsed;
    }
    public object ConvertBack(object value, Type targetType, object parameter,
                                System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
} 

Использование конвертора предполагает регистрацию его в ресурсах:

<vc:BoolToVisibilityValueConverter x:Key="VisibilityConverter" />

при необходимости следует добавить namespace:

xmlns:cnv="clr-namespace:SilverlightApplication12.Converters"

и далее его можно подключать как один из параметров привязки:

<Button Content="Save" Height="23" Width="75"
   Visibility="{Binding IsDirty, Converter={StaticResource VisibilityConverter}}" />

К вышесказанному можно добавить, что конверторы принимают параметры (ConverterParameter), которые на самом деле, очень полезны. Потому что именно значения параметров для конвертора можно использовать для выбора типа, направления, формата и т.д. возвращаемого значения.

Привязка данных из кода (Data Binding in Code)

Рано или поздно, в жизни любого программиста Silverlight наступает время когда требуется привязку данных осуществить в коде. И тут на помощь приходит GetBindingExpression и SetBinding методы у соответствующего контрола. Создадим новый Binding для свойства Text контрола TextBox:

Binding binding = new Binding("Name");
binding.Mode = BindingMode.TwoWay;
NameTextBox.SetBinding(TextBox.TextProperty, binding); 

Этот простой код создаёт двунаправленную привязку. Конечно же вы можете использовать все остальные параметры (StringFormat, TargetNullValue, FallbackValue и Source) класса Binding и в коде.

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

BindingExpression expr = NameTextBox.GetBindingExpression(TextBox.TextProperty);

Обратите внимание, что вы получаете не Binding объект, а именно BindingExpression, в котором есть два свойства ParentBinding и DataItem. Так вот свойство ParentBinding как раз и есть тот самый Binding, который мы создали выше.

Получение и установка значений присоединенных свойств

В коде также не составить труда получить значение присоединённых свойств (Attached Properties). Примером может послужить следующий код:

int row = (int)NameTextBox.GetValue(Grid.RowProperty);

В данном примере мы получаем значение ряда разметки, в которой TextBox находится. Для того чтобы установить присоединенное свойство контролу воспользуемся кодом:

NameTextBox.SetValue(Grid.RowProperty, row);

Синтаксис привязки (Binding using Property Element)

Ну, и на последок, хотелось бы еще немного показать другой формат привязки данных, о котором частенько забывают. Это, так называемый, синтаксис на основе свойств (в отличии от общепринятого "на основе атрибутов"). Возможно писанины он несет в себе больше, причем в некоторых случаях, гораздо больше чем обычный, но тем не менее тоже бывает очень полезно знать об его наличии:

<TextBox>
   <TextBox.Text>
       <Binding Path="Name" />
   </TextBox.Text>
</TextBox>

В этом примере, свойство Text у контрола TextBox привязывается значение свойства Name. В обычной схеме это выглядит, например, таким образом:

<TextBox Text="{Binding Name}" />

Заключение

В этой статье постарался собрать воедино всё, что связано с привязкой данных. Конечно, я не упомянул об отладке привязки, но это только потому, что ее, как таковой не существует в версии Silverlight 4. Пятая версия уже готовит нам такой приятный подарок как отладка привязки. А пока могу только поделиться ссылкой, некоторым образом решает вопросы отладки (http://karlshifflett.wordpress.com/2009/06/08/glimpse-for-silverlight-viewing-exceptions-and-binding-errors/. Я также не упомянул про MultiBinding, который присутствует в WPF (у большого брата). Могу в ответ, предоставить очередную ссылку на вариант решения (http://www.scottlogic.co.uk/blog/colin/2010/05/silverlight-multibinding-solution-for-silverlight-4/. Вот наверное и всё. Если я что-нибудь упустил, пожалуйста, найдите время и напишите мне в комментарии. Спасибо. И да прибудет сила с Вами!

Комментарии к статье (2)