Пилим простенький Binder…

от автора

image

Дело было так… мне понадобился односторонний биндер. Только не от контрола к источнику, а наоборот. Я в коде сто тыщ раз меняю значение источника — и не хочу, чтоб голова болела о всяких там textbox’ах. Хочу, чтоб они сами обновлялись…

Вообще-то, у буржуев уже есть встроенный биндер, очень мощный и крутой. Настолько мощный и крутой, что почти без документации. Точнее, ее черезмерно много — и везде невнятно, нечетко. Короче, плюнул я на буржуйские технологии и решил ВОТ ЭТИМИ РУКАМИ запилить свой биндер… Теперь вот показываю новичкам — наверно, кому-то пригодиться для расширения С#-кругозора.

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

Концептуально для решения этой задачи нужны две вещи: namespace System.Reflection и событие в set-аксессоре объекта-источника. Что-то типа «PropertyChanged». Вот как это выглядит в коде:

Класс объекта-источника

string _Property;         public string Property         {             get { return this._Property; }             set             {                 this._Property = value;                 if (this.PropertyIsChanged != null) { this.PropertyIsChanged.Invoke(); }             }         }  public event Action PropertyIsChanged; 

Прошу обратить внимание на две вещи:

1) if (this.PropertyIsChanged != null) { this.PropertyIsChanged.Invoke(); — это кусок кода обязательно необходим, поскольку в противном случае, можно нарваться на nullReference-исключение.

2) public event Action PropertyIsChanged; — событие, как и общепринято, объявлено с модификатором public, и в его основу положен встроенный делегат Action, возвращающий void и не принимающий никаких параметров. Почему именно этот делегат, а не какой-нибудь другой? Да потому что именно к этому событию наш биндер подключит свой обработчик событий, который только и будет делать одно дело: считывать новое значение источника (в момент работы set-аксессора оно и устанавилось, там же сработал event — если у него был хоть один слушатель, конечно) и присваивать его указанному свойству (например .Text) конкретного контрола. Другими словами, биндовский обработчик события возвращает void и не имеет входных параметров.

Все, объект-источник готов. Теперь, собственно, остается сам код биндера.

Значит, у моего простенького биндера всего четыре основные поля:

Поля класса Binder

Ссылка на контрол и название его свойства, которое привязывается

Control _targetControl;         /// <summary>         /// Get; set-once.         /// Возвращает Control (цель), который привязан к объекту-источнику.         /// </summary>         public Control TargetControl         {             get { return this._targetControl; }             set              {                 if (this._targetControl != null) { /* do nothing */ }                 else { this._targetControl = value; }             }         }          string _targetControlProperty;         /// <summary>         /// Get; set-once.         /// Возвращает название свойства Control'a,         /// которое привязано к объекту-источнику.         /// </summary>         public string TargetControlProperty         {             get { return this._targetControlProperty; }             set              {                 if (this._targetControlProperty != null) { /* do nothing */ }                 else { this._targetControlProperty = value; }              }         } 

Ссылка на объект-источник и название его свойства

/// <summary>         /// Объект, к полю которого будет привязан Control.         /// </summary>         object _dataSourceObject;          /// <summary>         /// Get; set-once.         /// Возвращает ссылку на объект-источник,         /// к которому будет привязан Control.         /// </summary>         public Object DataSourceObject         {             get { return this._dataSourceObject; }             set             {                 if (this._dataSourceObject != null) { /* do nothing */ }                 else { this._dataSourceObject = value; }             }         }          string _dataSourceObjectProperty;         /// <summary>         /// Get; set-once.         /// Возврашает название свойства,          /// к которому привязан Control(цель).         /// </summary>         public string DataSourceObjectProperty         {             get { return this._dataSourceObjectProperty; }             set              {                 if (this._dataSourceObjectProperty != null) { /* do nothing */ }                 else { this._dataSourceObjectProperty = value; }             }         } 

Идем дальше. Конструктор.

Конструктор биндера

public SimpleBinder(             Control targetControl,             string targetControlProperty,             object dataSourceObject,             string dataSourceProperty,             string dataSourcePropertyChanged = "")         {             // safety checks             CheckIfPropertyExists(targetControl, targetControlProperty);             CheckIfPropertyExists(dataSourceObject, dataSourceProperty);             // end safety              this._targetControl = targetControl;             this._targetControlProperty = targetControlProperty;              this._dataSourceObject = dataSourceObject;             this._dataSourceObjectProperty = dataSourceProperty;              if (dataSourcePropertyChanged == String.Empty) { this.Binding(); }             else              {                 CheckIfEventExists(dataSourceObject, dataSourcePropertyChanged);                 this.Binding(dataSourcePropertyChanged, null);              }         } 

Заметьте, в конструкторе четыре обязательных параметра (соответствуют вышеприведенным полям класса) и один свободный. Свободный параметр — это название public-события в объекте-источнике, которое отвечает за оповещение о том, что значение указанного свойства изменилось. Я код этого класса уже приводил выше. Повторюсь еще раз, любой объект, который претендует быть источником для контрола, должен позаботиться о наличии у себя такого события… это не проблема самого биндера.

А поскольку название события — параметр необязательный, то нужен вот такой кусок кода::

if (dataSourcePropertyChanged == String.Empty) { this.Binding(); }             else              {                 CheckIfEventExists(dataSourceObject, dataSourcePropertyChanged);                 this.Binding(dataSourcePropertyChanged, null);              } 

Если событие не указано, следовательно, автоматическое обновление не требуется. Тогда срабатывает метод .Binding() без параметров.

this.Binding()

private void Binding()         {             this.Binding_SetValueToControl(this._targetControlProperty, this.DataSourceObjectProperty);         }  

Как видно из кода, .Binding() свою очередь вызывает private-метод .Binding_SetValueToControl()… вот он:

.Binding_SetValueToControl()

private void Binding_SetValueToControl(             string targetControlProperty,             string dataSourceProperty)         {             this.TargetControl.GetType()                 .GetProperty(targetControlProperty) // СТРОКА С НАЗВАНИЕМ СВОЙСТВА                 .SetValue(this.TargetControl,                           this.DataSourceObject.GetType()                           .GetProperty(dataSourceProperty) // СТРОКА С НАЗВАНИЕМ СВОЙСТВА                           .GetValue(this.DataSourceObject)                           );         } 

Вот тут используются те самые механизмы рефлексии. В контекст этой статьи не входит подробный разбор методов .GetProperty() и других, но в принципе тут все интуитивно ясно. Мельком только отмечу, что вот почему мы в конструктор требовали строки с названием свойств контрола и объекта-источника — это было обусловлено устройством System.Reflection.

Остается последний вопрос — а как же наш биндер привязывает событие, если оно указано в конструкторе?

А вот так:

private void Binding_DataSourcePropertyChangedEvent(             string dataSourcePropertyChanged,             Delegate propertyChangedEventHandler = null             )         {             if (propertyChangedEventHandler != null)             {                 this.DataSourceObject.GetType()                     .GetEvent(dataSourcePropertyChanged)                     .AddEventHandler(this.DataSourceObject, propertyChangedEventHandler);             }             else             {                 SimpleBinder RefToThis = this;                 this.DataSourceObject.GetType()                     .GetEvent(dataSourcePropertyChanged)                     .AddEventHandler(this.DataSourceObject,                                      new Action(                                          () =>                                          { RefToThis.UpdateControl(RefToThis.GetDataSourcePropertyValue()); }                                          ));             }         }  

Используя все те же самые механизмы рефлексии, .AddEventHandler() подключает биндовский обработчик событий к тому public-событию, которое изначально существовало в классе объекта-источника (и запускалось в его set-аксессоре!). Здесь: 1) создается новый делегат типа Action и 2) ему передается метод, сформированный на основе лямбда-выражения (что это такое и как пользоваться — не в этой статье). В принципе все.

Теперь как этим пользоваться:

 SourceObj = new SomeClass("Text?"); // объект-источник. "Text?" - такой строкой инициализируется его свойство.              SimpleBinder txtBoxBinder = new SimpleBinder(this.label1, "Text", // Control и его свойство, которые будут привязаны                                              SourceObj, "Property", // Объект источник и его свойство                                              "PropertyIsChanged"); // Событие объекта-источника.   

Вот и все.

ссылка на оригинал статьи http://habrahabr.ru/post/269627/