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)
Лучшая статья про баиндинг!
Большое спасибо! Очень помогли!