Дело было так… мне понадобился односторонний биндер. Только не от контрола к источнику, а наоборот. Я в коде сто тыщ раз меняю значение источника — и не хочу, чтоб голова болела о всяких там 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"); // Событие объекта-источника.
Вот и все.
Комментарии (11)
ekulakov
29.10.2015 17:481) if (this.PropertyIsChanged != null) { this.PropertyIsChanged.Invoke(); — это кусок кода обязательно необходим, поскольку в противном случае, можно нарваться на nullReference-исключение.
И в этом случае тоже может быть null ref, потому что после проверки последний подписчик может отписаться от события.
Поэтому правильней делать так:
EventHandler handler = this.PropertyIsChanged; if (handler != null) { handler(this, EventArgs.Empty); }
52hertz
29.10.2015 18:01Это как? Если задействован set-аксессор поля Property, то пока не выработается его логика, никто ни от чего отписаться не может. Не уверен насчет кросс-поточности, но в любом случае этот код падает при попытке изменить контрол с НЕ-UI потока. Я уже писал выше.
ekulakov
29.10.2015 18:07Разговор конечно о многопоточности.
Тут немного по теме: http://codeblog.jonskeet.uk/2015/01/30/clean-event-handlers-invocation-with-c-6/52hertz
29.10.2015 19:06при многопоточности этот код и так бабахнется.
падает конечно. я специально опустил этот момент, чтобы не грузить новичков нипанятным. а вообще, там стандартная схема if(...InvokeRequired). ну и через делегат, который вызывает тот же самый метод. все летает прекрасно. вообще голова теперь не болит за обновление контролов
Zagrebelion
29.10.2015 19:23кстати, обычные биндинги из winforms работают с разными потоками. Я там выше давал ссылку на репозитарий — есть чекбокс про потоки.
Zagrebelion
А что сложного в INPC и contorl.DataBinding.Add()?
Ну, кроме того, что в WinForms нужно в Add передавать строкой названия свойств.
Zagrebelion
накидал на коленке пример.
https://github.com/Zagrebelin/WinformsDatabinding
52hertz
Да наверно ничего сложного нет. Вопрос только в том, что обычно биндинги обеспечивают от контрола к источнику связь. Я знаю, что есть двухсторонний режим, но мне реально быстрее было один раз написать свое, чем лезть на msdn.com и вычитывать там как настроить двухстороннюю связь при многопоточности. У них серьезные проблемы с документацией по этому вопросу.
Zagrebelion
А у вас Binding_SetValueToControl нормально работает в многопоточной среде? TargetControl....SetValue() не падает, если свойство модели было установлено не в UI потоке?
52hertz
падает конечно. я специально опустил этот момент, чтобы не грузить новичков нипанятным. а вообще, там стандартная схема if(...InvokeRequired). ну и через делегат, который вызывает тот же самый метод. все летает прекрасно. вообще голова теперь не болит за обновление контролов.