Bindings в WPF

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

Привязка к свойству внешнего объекта

Предположим, в рамках нашего приложения существует некий слой, который является источником данных для нашего пользовательского интерфейса. Этот слой может быть реализован различным образом - начиная от простого статического класса, заканчивая неким сервисом с публичным контрактом и сложной логикой инстанциации. В любом из этих сценариев мы можем декларативно осуществить привязку к этому объекту.

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

internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
{
  public static Settings Default
  {
    get
    {
        return defaultInstance;
    }
  }

Далее в этом классе определяются публичные поля, соответствующие настройкам нашего приложения. В демонстрационном примере я создам одну настройку Param1. Таким образом, в составе данного класса появится нужное свойство:

public string Param1
{
  get
  {
    return ((string)(this["Param1"]));
  }
  set
  {
    this["Param1"] = value;
  }
}

После этого, мы можем, например, императивно обратится к этому свойству и присвоить его значение какому-то элементу управления:

Text1.Text = Settings.Default.Param1;

Однако, эту же функциональность можно задать декларативно используя XAML:

<TextBox Name="Text1" Text="{Binding Source={x:Static config:Settings.Default}, Path=Param1}"/>

Как мы видим, здесь в качестве источника указывается статическое свойство класса, а в параметре Path указывается имя свойства уже инстанциированного класса. Кроме отображений настроек класса Settings очень удобно пользоваться Bindings для изменения этих настроек, для этого используется режим двунаправленного Binding:

<TextBox Name="Text1" Text="{Binding Source={x:Static config:Settings.Default}, Path=Param1, Mode=TwoWay}"/>

Как вы понимаете, подобным образом мы можем обращаться не только к Settings, но и к любым другим классам, например DateTime:

<TextBox Name="Text2" Text="{Binding Source={x:Static System:DateTime.Now}, Path=DayOfYear, Mode=OneWay}"/>

Здесь как мы видим используется режим однонаправленного связывания (Mode=OneWay), т.к. свойство DayOfYear класса DateTime предназанчено только для чтения.

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

public class DataSource1
{
  public static DataSource1 Default
  {
    get { return new DataSource1(); }
  }

  public IEnumerable List1
  {
    get { return GetType().GetMethods(); }
  }
}

Тогда привязка к элементу ListBox будет выглядеть следующим образом:

<ListBox Name="List1" ItemsSource="{Binding Source={x:Static WpfApplication38:DataSource1.Default}, Path=List1, Mode=OneWay}" />

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

<Window.Resources>
  <WpfApplication38:DataSource1 x:Key="Source1"/>
</Window.Resources>
...
<ListBox Name="List2" ItemsSource="{Binding Source={StaticResource Source1}, Path=List1, Mode=OneWay}" />

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

public static class DataSource2
{
  public static string Data1 { get; set; }
  public static string Data2 { get; set; }
}

В таком случае мы также можем определить привязку декларативно:

<TextBox Name="Text3" Text="{Binding Source={x:Static WpfApplication38:DataSource2.Data1}, Mode=OneWay}"/>
<TextBox Name="Text4" Text="{Binding Source={x:Static WpfApplication38:DataSource2.Data2}, Mode=OneWay}"/>

Привязка к методу внешнего объекта

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

Для того, чтобы определить связь с данными, возвращаемыми методом существует объект ObjectDataProvider. Он позволяет указать класс, в котором содержится метод, инстанциировать его если нужно и вызвать метод. Например у нас существует класс, в котором определен метод, возвращающий данные:

public class DataSource1
{
  //...

  public string GetData1()
  {
    return Guid.NewGuid().ToString();
  }
}

Для того, чтобы получить возможность определения Binding на основе такого класса в XAML-коде необходимо определить ObjectDataProvider и указать в нем имя класса и имя вызываемого метода:

<Window.Resources>
  <ObjectDataProvider x:Key="Source2"
                      ObjectType="{x:Type WpfApplication38:DataSource1}"
                      MethodName="GetData1">

  </ObjectDataProvider>
</Window.Resources>

После этого, можно ссылаться на Source2 в качестве источника:

<TextBox Name="Text5" Text="{Binding Source={StaticResource Source2}, Mode=OneWay}"/>

Точно также в качестве результата работы метода можно вернуть не просто какой-то простой тип, но и объект или коллекцию:

public class DataSource1
{
  // ...

  public object GetData2()
  {
    return new
    {
      Value1 = Guid.NewGuid().ToString(), 
      Value2 = DateTime.Now.ToString()
    };
  }

  public IEnumerable GetData3()
  {
    return GetType().GetMethods();
  }
}

И создать привязку к ним:

<Window.Resources>
    <ObjectDataProvider x:Key="Source3"
                        ObjectType="{x:Type WpfApplication38:DataSource1}"
                        MethodName="GetData2"/>

    <ObjectDataProvider x:Key="Source4"
                        ObjectType="{x:Type WpfApplication38:DataSource1}"
                        MethodName="GetData3"/>
</Window.Resources>

...

<TextBox Name="Text6" Text="{Binding Source={StaticResource Source3}, Path=Value1, Mode=OneWay}"/>
<ListBox Name="List3" ItemsSource="{Binding Source={StaticResource Source4}, Mode=OneWay}" />

Ну и наконец мы можем передавать значения параметров методов, если метод содержит параметры. Для этого мы можем использовать коллекцию MethodParameters объекта ObjectDataProvider:

<Window.Resources>
  ...
  <ObjectDataProvider x:Key="Source5"
                      ObjectType="{x:Type WpfApplication38:DataSource1}"
                      MethodName="GetData4">
    <ObjectDataProvider.MethodParameters>
      <System:String>qwerty</System:String>
    </ObjectDataProvider.MethodParameters>
  </ObjectDataProvider>
</Window.Resources>

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

UPDATE (спасибо Виталию Дильмухаметову): Если в качестве параметра нужно передать тип (typeof(...)), это можно сделать так:

<ObjectDataProvider x:Key="GetMembers"
                    ObjectType="{x:Type WpfApplication34:EnumService}"
                    MethodName="GetResult">
  <ObjectDataProvider.MethodParameters>
      <x:Type TypeName="WpfApplication34:MyEnum"/>
  </ObjectDataProvider.MethodParameters>
</ObjectDataProvider>

При этом вызываемый метод может выглядеть следующим образом:

public class EnumService
{
  public static IEnumerable<string> GetResult(Type type)
  {
    return from f in type.GetFields(BindingFlags.Public | BindingFlags.Static) select f.Name;
  }
}

Тогда в качестве значения параметра будет передано typeof(WpfApplication34.MyEnum).

Таким образом, мы видим, что имеем возможность получить доступ к данным, которые представлены через вызов методов класса.

Такие вот преимущества мы получаем используя подход декларативного связывания данных, который появился в WPF. В противовес этому, при использовании WinForms нам предлагается написать много императивного кода, особенно когда нужно реализовать сценарий двунаправленного связывания. Понятно как это скажется на читаемости кода и поддержке/развитии приложения. :)