• Microsoft .NET
  • LINQ
  • Привязка ElementName в Silverlight посредством прикрепленных поведений

Привязка ElementName в Silverlight посредством прикрепленных поведений

Для тех, кто не знаком с принципом, мы сделаем краткий обзор. При привязке свойств ваших визуальных элементов в XAML, источником данной привязки будет объект, связанный с элементами DataContext. Это работает для привязки ваших бизнес-объектов к графическому интерфейсу, который раскрывает их свойства. Тем не менее, с WPF привязка даст вам гораздо больше, чем просто механизм для отображения ваших бизнес-данных, он также позволяет вам привязать свойства между визуальными элементами. Это мощный принцип, который позволяет вам создать сложную разметку, что возможно при помощи предоставленных структурой панелей (Panels). Для того, чтобы это было возможным, WPF предоставляет привязки ElementName и RelativeSource, что даст вам мощный механизм определения других элементов в пределах вашего визуального дерева, к которым стоит осуществить привязку. Простой пример, где ширина (Width) прямоугольника привязана к именному слайдеру, предоставлен ниже:

<Rectangle Width="{Binding Path=Value, ElementName=MySlider}" Height="20" Fill="Green"/>
<Slider x:Name="MySlider" Value="25" Minimum="0" Maximum="300"/>

К сожалению, Silverlight не имеет такой функциональности.

Сначала приходит идея указать DataContext целевого элемента на исходный элемент для того, чтобы позволить осуществить привязку свойств между ними. Тем не менее, к нашему удивлению, свойства зависимости Silverlight не поддерживают оповещение об изменении свойств.

Решением этой проблемы станет использование объекта Relay Object. Объект с одним единственным свойством Value, который реализует INotifyPropertyChanged, привязан к двум визуальным элементам. Простой пример представлен выше:

<UserControl.Resources>
    <local:RelayObject x:Key="Relay">       
        <local:RelayObject.Value>
            <system:Double>20</system:Double>
        </local:RelayObject.Value>
    </local:RelayObject>
</UserControl.Resources>
 
<StackPanel x:Name="LayoutRoot" Background="White">
    <Rectangle Width="{Binding Path=Value, Source={StaticResource Relay}, Mode=TwoWay" Height="20" Fill="Green"/>
    <Slider Value="{Binding Path=Value, Source={StaticResource Relay}, Mode=TwoWay}" Minimum="0" Maximum="300"/>      
</StackPanel>

Тут экземпляр объекта RelayObject привязан к обоим свойствам - ширине прямоугольника (Width) и значению слайдера (Slider’s Value), эффективно привязывая данные два свойства. Все работает, но тем не менее это кажется неполноценной привязкой WPF ElementName, более того, вам необходимо добавить новый экземпляр RelayObject для каждой привязки.

Наше решение заключается в использовании шаблона Attached Behaviour , который становится все популярнее в WPF и Silverlight. Сначала мы определим свойство, которое использует тип, ограничивающий информацию, необходимую нам для создания привязки, то есть свойства источника и назначения, а также название элемента, к которому мы хотим привязать.

public class BindingProperties
{
    public string SourceProperty { get; set; }
    public string ElementName { get; set; }
    public string TargetProperty { get; set; }
}
 
public static class BindingHelper
{
    public static BindingProperties GetBinding(DependencyObject obj)
    {
        return (BindingProperties)obj.GetValue(BindingProperty);
    }
 
    public static void SetBinding(DependencyObject obj, BindingProperties value)
    {
        obj.SetValue(BindingProperty, value);
    }
 
    public static readonly DependencyProperty BindingProperty =
        DependencyProperty.RegisterAttached("Binding", typeof(BindingProperties), typeof(BindingHelper),
        new PropertyMetadata(null, OnBinding));
 
 
    /// <summary>
    /// Обработких события изменения свойства SelectAllButtonTemplate
    /// </summary>
    private static void OnBinding(
        DependencyObject depObj, DependencyPropertyChangedEventArgs e)
    {
        FrameworkElement targetElement = depObj as FrameworkElement;
 
        targetElement.Loaded += new RoutedEventHandler(TargetElement_Loaded);
    }
 
    private static void TargetElement_Loaded(object sender, RoutedEventArgs e)
    {
        FrameworkElement targetElement = sender as FrameworkElement;
 
        // получение значения нашего прикрепленного свойства
        BindingProperties bindingProperties = GetBinding(targetElement);
 
        // выполнение поиска 'ElementName'
        FrameworkElement sourceElement = targetElement.FindName(bindingProperties.ElementName) as FrameworkElement;
 
        // привязка
        CreateRelayBinding(targetElement, sourceElement, bindingProperties);
    }   
}

Указанный выше код определяет привязанное свойство типа BindingProperties. Метод OnBinding вызывается тогда, когда свойство привязывается к зависимому объекту. В пределах данного обработчика события мы добавляем обработчик для события Loaded данного элемента. Данное событие происходит тогда, когда элемент располагается в пределах визуального дерева и готов к работе, и на этом этапе мы можем произвести поиск ElementName.

В пределах обработчика события TargetElement_Loaded мы используем FrameworkElement.FindName для поиска названного элемента источника для нашей привязки. Данный метод находит любой элемент с данным названием, который находится в пределах того же пространства имен XAML. Интересно то, что это тот же метод, который использует Visual Studio для создания некоторых переменных в пределе файла с фоновым кодом из названных элементов в вашем XAML. Как только названые элементы будут найдены, будет построена привязка типа relay между ними следующим образом:

private static readonly BindingFlags dpFlags = BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy;
 
private static void CreateRelayBinding(FrameworkElement targetElement, FrameworkElement sourceElement,
    string targetProperty, string sourceProperty, IValueConverter converter)
{
     // создание привязки relay между двумя элементами
     ValueObject relayObject = new ValueObject();
 
     // нахождение свойства зависимости источника
     FieldInfo[] sourceFields = sourceElement.GetType().GetFields(dpFlags);
     FieldInfo sourceDependencyPropertyField = sourceFields.First(i => i.Name == sourceProperty + "Property");
     DependencyProperty sourceDependencyProperty = sourceDependencyPropertyField.GetValue(null) as DependencyProperty;
 
     // инициализация объекта relay со значением свойства зависимости исходника
     relayObject.Value = sourceElement.GetValue(sourceDependencyProperty);
 
     // создание привязки для нашего целевого элемента с объектом Relay, эта привязка
     // включает преобразователь значений
     Binding targetToRelay = new Binding();
     targetToRelay.Source = relayObject;
     targetToRelay.Path = new PropertyPath("Value");
     targetToRelay.Mode = BindingMode.TwoWay;
     targetToRelay.Converter = converter;
 
     // нахождение свойства зависимости целевого элемента
     FieldInfo[] targetFields = targetElement.GetType().GetFields(dpFlags);
     FieldInfo targetDependencyPropertyField = targetFields.First(i => i.Name == targetProperty + "Property");
     DependencyProperty targetDependencyProperty = targetDependencyPropertyField.GetValue(null) as DependencyProperty;
 
     // установка привязки с нашим целевым элементом
     targetElement.SetBinding(targetDependencyProperty, targetToRelay);
 
     // создание привязки с нашим исходным элементом и объектом relay
     Binding sourceToRelay = new Binding();
     sourceToRelay.Source = relayObject;
     sourceToRelay.Path = new PropertyPath("Value");
     sourceToRelay.Mode = BindingMode.TwoWay;
 
     // установка привязки по исходному элементу
     sourceElement.SetBinding(sourceDependencyProperty, sourceToRelay);       
}

Данный код просто создает наш объект Relay, инициализируя его значение из исходного элемента, затем создавая привязки их объекта relay с исходником и целевым элементом. Единственным трюком является использование отражения для нахождения статических свойств зависимости каждого элемента.

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

<Rectangle Height="20" Fill="Green">
    <local:BindingHelper.Binding>
        <local:BindingProperties ElementName="Slider" SourceProperty="Value" TargetProperty="Width"/>
    </local:BindingHelper.Binding>
</Rectangle>
<Slider x:Name="Slider" Value="20" Minimum="0" Maximum="300"/>

Преимуществами данного подхода является то, что нам нет необходимости в явном создании объекта relay для каждой привязки ElementName, а во-вторых, значение свойства исходника используется для прямой инициализации объекта relay. Также очень простым заданием будет расширение указанного выше кода для добавления ValueConverters к привязке.

Но что, если мы хотим привязать два различных свойства к нашему слайдеру? Если, к примеру, мы хотим привязать ширину двух прямоугольников, нам необходимо обеспечить, чтобы между слайдером (Slider) и прямоугольниками (Rectangles) был создан один объект relay, разделяющий их. Простым решением данной проблемы будет поддержание словаря привязок исходных объектов и ассоциированных с ними объектов relay. Если мы привяжем более одного свойства целевого элемента с конкретным свойством исходника, то объект relay будет повторно использован. Любые преобразователи значений указываются в целевой привязке, тем самым мы можем привязать множество свойств с источником с различными преобразователями.

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

<Rectangle Height="20" Fill="Green">
    <local:BindingHelper.Binding>
        <local:BindingProperties ElementName="Slider" SourceProperty="Value"
            TargetProperty="Width" Converter="{StaticResource ScalingConverter}" ConverterParameter="2"/>
    </local:BindingHelper.Binding>
</Rectangle>
<Rectangle Height="20" Fill="Green">
    <local:BindingHelper.Binding>
        <local:BindingProperties ElementName="Slider" SourceProperty="Value"
            TargetProperty="Width" Converter="{StaticResource ScalingConverter}" ConverterParameter="4"/>
    </local:BindingHelper.Binding>
</Rectangle>
<Slider x:Name="Slider" Value="20" Minimum="0" Maximum="300"/>

Вам также стоит изучить проект предлагаемый к загрузке.

Данный подход также имеет несколько недостатков - к сожалению, синтаксис немного многословен. Также, на данный момент вы можете создавать только именную привязку по одному визуальному элементу. Тем не менее, интересной функциональностью данного метода является то, что он может быть быстро адаптирован к поиску исходных элементов другими способами, к примеру эмулировать WPF относительно исходных привязок. А пока вы можете загрузить исходный код данной статьи.

Автор: Colin Eberhardt