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"); // Событие объекта-источника.



Вот и все.

Комментарии (11)


  1. Zagrebelion
    27.10.2015 20:31

    А что сложного в INPC и contorl.DataBinding.Add()?
    Ну, кроме того, что в WinForms нужно в Add передавать строкой названия свойств.


    1. Zagrebelion
      27.10.2015 20:47

      накидал на коленке пример.
      https://github.com/Zagrebelin/WinformsDatabinding


      1. 52hertz
        27.10.2015 23:01

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


        1. Zagrebelion
          28.10.2015 00:26

          А у вас Binding_SetValueToControl нормально работает в многопоточной среде? TargetControl....SetValue() не падает, если свойство модели было установлено не в UI потоке?


          1. 52hertz
            28.10.2015 00:38
            -1

            падает конечно. я специально опустил этот момент, чтобы не грузить новичков нипанятным. а вообще, там стандартная схема if(...InvokeRequired). ну и через делегат, который вызывает тот же самый метод. все летает прекрасно. вообще голова теперь не болит за обновление контролов.


  1. dymanoid
    28.10.2015 23:17

    Хоспаде, я уж сначала испугался, что тут WPF-ные байндинги переписывают…


  1. ekulakov
    29.10.2015 17:48

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

    И в этом случае тоже может быть null ref, потому что после проверки последний подписчик может отписаться от события.

    Поэтому правильней делать так:
        EventHandler handler = this.PropertyIsChanged;
        if (handler != null)
        {
            handler(this, EventArgs.Empty);
        }
    


    1. 52hertz
      29.10.2015 18:01

      Это как? Если задействован set-аксессор поля Property, то пока не выработается его логика, никто ни от чего отписаться не может. Не уверен насчет кросс-поточности, но в любом случае этот код падает при попытке изменить контрол с НЕ-UI потока. Я уже писал выше.


      1. ekulakov
        29.10.2015 18:07

        Разговор конечно о многопоточности.
        Тут немного по теме: http://codeblog.jonskeet.uk/2015/01/30/clean-event-handlers-invocation-with-c-6/


        1. 52hertz
          29.10.2015 19:06

          при многопоточности этот код и так бабахнется.

          падает конечно. я специально опустил этот момент, чтобы не грузить новичков нипанятным. а вообще, там стандартная схема if(...InvokeRequired). ну и через делегат, который вызывает тот же самый метод. все летает прекрасно. вообще голова теперь не болит за обновление контролов


          1. Zagrebelion
            29.10.2015 19:23

            кстати, обычные биндинги из winforms работают с разными потоками. Я там выше давал ссылку на репозитарий — есть чекбокс про потоки.