MultiBindings в Silverlight: присвоение множества привязок одному свойству

ОГЛАВЛЕНИЕ

Данная статья описывает метод ассоциации множества привязок с одним единственым свойством зависимости в приложениях Silverlight. WPF уже обладает данной функциональностью в виде MultiBindings, а код в приведенной статье копирует данную функциональность.

Данная статья описывает метод ассоциации множества привязок с одним единственным свойством зависимости в приложениях Silverlight. WPF уже обладает данной функциональностью в виде MultiBindings, а код в приведенной статье копирует данную функциональность.

Простое приложение демонстрирует данную технику, при этом у нас будет три текстовых блока для ввода данных, привязанных к индивидуальным свойствам простого объекта Person, а текстовый блок заголовка будет привязан к обоим свойствам Forename (Имя) и Surname (Фамиля). Попробуйте отредактировать фамилию или имя и вы увидите, как будет обновлен заголовок.

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

<TextBlock Foreground="White" FontSize="13">
    <local:BindingUtil.MultiBinding>
        <local:MultiBinding TargetProperty="Text"
        Converter="{StaticResource TitleConverter}">
            <Binding Path="Surname"/>                            
            <Binding Path="Forename"/>
        </local:MultiBinding>
    </local:BindingUtil.MultiBinding>
</TextBlock>
 
<TextBlock Text="Surname:"/>
<TextBox  Text="{Binding Path=Surname, Mode=TwoWay}"/>
 
<TextBlock Text="Forename:"/>
<TextBox Text="{Binding Path=Forename, Mode=TwoWay}"/>
 
<TextBlock Text="Age:"/>
<TextBox Text="{Binding Path=Age, Mode=TwoWay}"/>

Решение

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


Привязки свойств Forename и Surname являются привязками MultiBinding (какие именно свойства мы узнаем далее). MultiBinding имеет ассоциированный Converter типа IMultiValueConverter - этот класс, предоставленный клиентом, реализует процесс преобразования:

public class TitleConverter : IMultiValueConverter
{
  public object Convert(object[] values, Type targetType,
    object parameter, System.Globalization.CultureInfo culture)
  {
    string forename = values[0] as string;
    string surname = values[1] as string;
 
    return string.Format("{0}, {1}", surname, forename);
  }
}

Интерфейс IMultiValueConverter практически такой же, как IValueConverter, только в этом случае, массив объектов передается преобразователю, где каждый объект содержит текущее значение привязки для каждой привязки по порядку.

Имея данный преобразователь значений, класс MultiBinding может определить изменения в двух привязках, затем пересчитать значение ConvertedValue , которое привязано целевому свойству нашего TextBlock. В этом и заключается идея, тем не менее не все так просто.


Получение DataContext

Обычно, при определении привязки мы опускаем свойство Source, то есть {Binding Path=Forename}. Когда Binding, которое данное свойство представляет, ассоциируется с элементом, источником привязки будет (наверняка унаследован) DataContext целевого элемента. Потому для того, чтобы позволить осуществлять привязку по классу MultiBinding, он должен быть FrameworkElement, что дает нам свойство DataContext и метод SetBinding().

Тем не менее, есть одна проблема - DataContext каждого элемента наследуется от родителя в пределах визуального дерева. Наше MultiBinding не находится в пределах визуального дерева и мы не хотим, чтобы оно там было, поэтому оно не будет участвовать в наследовании DataContext. Нам необходимо обеспечить то, что когда DataContext будет изменяться по элементу, который имеет MultiBinding , связанный с ним, то мы ‘передаем’ данный DataContext к MultiBinding. С WPF это выполнить довольно легко, FrameWorkElement раскрывает событие DataContextChanged, (для зависимостей, которые не раскрывают события, всегда существует DependencyPropertyDescriptor). Тем не менее, с Silverlight нам недоступна ни одна из этих опций.

Наше решение заключается в том, чтобы создать новое привязанное свойство и привязать его к целевому (в данном случае это наш TextBlock), что потянет за собой DataContext. Следующий код скопирован из класса BindingUtil - когда класс MultiBinding ассоциируется с целевым элементом в качестве привязанного свойства, мы также привяжем его привязанное свойство DataContextPiggyBack. Мы определяем статический ( static ) метод, которые вызывается тогда, когда DatatContext целевого объекта изменяется, и мы продвигаем новый DataContext к классу MultiBinding.

/// <summary>
/// Вызывается тогда, когда свойство MultiBinding устанавливается в элементе структуры
/// </summary>
private static void OnMultiBindingChanged(DependencyObject depObj,
  DependencyPropertyChangedEventArgs e)
{
  FrameworkElement targetElement = depObj as FrameworkElement;
 
  // Привязка целевых  элементов DataContext, к нашему свойству DataContextPiggyBack
  // Это повзоляет нам получить события изменения свойства тогда, когда изменяется targetElement DataContext
  targetElement.SetBinding(BindingUtil.DataContextPiggyBackProperty, new Binding());
}
 
public static readonly DependencyProperty DataContextPiggyBackProperty =
    DependencyProperty.RegisterAttached("DataContextPiggyBack",
        typeof(object), typeof(BindingUtil), new PropertyMetadata(null,
              new PropertyChangedCallback(OnDataContextPiggyBackChanged)));
 
public static object GetDataContextPiggyBack(DependencyObject d)
{
  return (object)d.GetValue(DataContextPiggyBackProperty);
}
 
public static void SetDataContextPiggyBack(DependencyObject d, object value)
{
  d.SetValue(DataContextPiggyBackProperty, value);
}
 
/// <summary>
/// Обрабатывает изменения свойства DataContextPiggyBack.
/// </summary>
private static void OnDataContextPiggyBackChanged(DependencyObject d,
                                                  DependencyPropertyChangedEventArgs e)
{
  FrameworkElement targetElement = d as FrameworkElement;
 
  // когда изменяется targeElement DataContext мы копируем значение обновленного свойства в наш MultiBinding.
  MultiBinding relay = GetMultiBinding(targetElement);
  relay.DataContext = targetElement.DataContext;
}


Создание целевых объектов для привязок (Binding)

Класс MultiBinding должен иметь свойство, которое является коллекцией типа Binding:

[ContentProperty("Bindings")]
public class MultiBinding : Panel, INotifyPropertyChanged
{
  ...
 
  /// <summary>
  /// Привязки, результаты которых будут предоставлены преобразователю.
  /// </summary>
  public ObservableCollection<Binding> Bindings { get; set; }
 
  ...
}

(Стоить отметить использование атрибута ContentProperty, означающего, что нам нет явной необходимости описывать коллекцию Binding в XAML используя синтаксис свойства элемента). Проблемой является то, что для оценки данных привязок, они должны быть привязаны целевому свойству. Мы можем добавить число свойств-пустышек к MultiBinding, PropertyOne, PropertyTwo и т.д. и привязать к ним, тем не менее данный подход слишком трудоемок и ограничен.

Наше решение заключается в том, что MultiBinding станет Panel, а это позволит ему иметь дочерние элементы, каждый из которых унаследует его DataContext. Когда класс MultiBinding будет инициализирован в момент своей привязки, будет вызван метод Initialise. Данный метод создает экземпляр BindingSlave - простой подкласс FrameworkElement с одним единым свойством Value , которое вызывает событие PropertyChanged для каждой привязки тогда, когда данное свойство будет изменено :

/// <summary>
/// Создает BindingSlave для каждого Binding и соответственно привязывает Value.
/// </summary>
internal void Initialise()
{
  foreach (Binding binding in Bindings)
  {
    BindingSlave slave = new BindingSlave();
    slave.SetBinding(BindingSlave.ValueProperty, binding);
    slave.PropertyChanged += new PropertyChangedEventHandler(Slave_PropertyChanged);
    Children.Add(slave);
  }            
}

Когда будет изменено подчиненное свойство, обработчик события MultiBinding получает текущие значения привязки и использует их для повторной оценки преобразователя:

/// <summary>
/// Вызывается тогда, когда изменяется какое-либо из свойств Value, принадлежащих BindingSlave.
/// </summary>
private void Slave_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
  List<object> values = new List<object>();
  foreach (BindingSlave slave in Children)
  {
    values.Add(slave.Value);
  }
  ConvertedValue = Converter.Convert(values.ToArray(), typeof(object), ConverterParameter,
    CultureInfo.CurrentCulture);
}

Свойство ConvertedValue привязано к целевому свойству целевого объекта, и будет обновлено для отражения изменений.

Данный метод очень прост по сравнению с другими. Экземпляры MultiBinding и BindingSlave могут быть представлены в виде виртуальных ветвей нашего визуального дерева:


Загрузка исходного кода

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

Несколько слов о MVVM

Шаблон MVVM очень популярен в разработке приложений Silverlight и WPF. При наличии данного шаблона DataContext вашего представления привязан к вашей модели представления. С данным шаблоном отпадает необходимость в множественной привязки. В нашем примере класс PersonViewModel просто раскрывает свойство Title , а оно выполняет ту же функциональность, что и TitleConverter. Делает ли этот шаблон нашу статью бесполезной?

Нам так не кажется. Хотя MVVM является хорошим шаблоном, существуют случаи, когда добавление другого слоя в ваше приложение может быть нежелательным, особенно в случае, если вашей исходной целью является простота. Более того, не всем нравится, когда их заставляют использовать определенные шаблоны просто потому, что структуре не хватает функциональности. Шаблон MVVM хорош для привязки настраиваемых приложений и позволяет проводить тестирование единиц графического интерфейса, тем не менее, если вам это не нужно, то вам не стоит использовать MVVM.

Автор: Colin Eberhardt

Загрузить исходный код проекта