Дело было так… мне понадобился односторонний биндер. Только не от контрола к источнику, а наоборот. Я в коде сто тыщ раз меняю значение источника — и не хочу, чтоб голова болела о всяких там 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 и не имеет входных параметров.
Все, объект-источник готов. Теперь, собственно, остается сам код биндера.
Значит, у моего простенького биндера всего четыре основные поля:
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() без параметров.
private void Binding() { this.Binding_SetValueToControl(this._targetControlProperty, this.DataSourceObjectProperty); }
Как видно из кода, .Binding() свою очередь вызывает private-метод .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/
Добавить комментарий