Как работают механизмы привязки данных под капотом? - программирование
Подтвердить что ты не робот

Как работают механизмы привязки данных под капотом?

Технически, как работают механизмы привязки данных под капотом? В частности, как выглядит механизм "синхронизатора" в привязке данных и работает как?

Во многих средах, таких как .NET, Java, Flex и т.д., они обеспечивают механизм привязки данных. Я просто использовал API-вызовы, и поэтому все легко для меня, так как все, что мне нужно сделать, это сделать вызовы API.

Теперь я заинтересован, возможно, попытаться написать относительно простой механизм привязки данных для игры, над которой я работаю. Хотя я использую С#, у меня есть причины не использовать встроенные WinForms и механизм привязки данных (см. Справочную информацию ниже по причине). Поскольку я не могу использовать существующий механизм привязки данных в С#, я думал, что мне, возможно, придется написать его самостоятельно. Итак, мне нужно знать подробные подробные сведения о том, как привязка данных обычно работает под капотом. Таким образом, я не имею в виду, как использовать привязку данных в С#. Я имею в виду, как привязка данных работает как внутри, так и по архитектуре.

Я попытался найти в Интернете учебные пособия и статьи о привязке данных, но большинство результатов пришли ко мне как к использованию существующей привязки данных в С#, чего я не хочу.

Итак, прежде чем я смог приступить к планированию написания собственного связующего данных, я подумал, что мне нужно знать, как работают механизмы привязки данных под капотом? И что еще более важно, как выглядит механизм работы "синхронизатора" в механизме привязки данных, например, как синхронизировать данные все время в одностороннем или двустороннем привязке?

Немного информации о том, почему я задал этот вопрос:

Некоторое время назад я сделал question о том, как использовать привязку данных в С# для пользовательских интерфейсов, которые не используют стандартные WinForms. Ответ, который я получил, заключается в том, что механизм привязки данных в С# тесно связан с интерфейсом WPF/Windows Forms. Итак, я думаю, я не могу использовать существующий механизм привязки данных в С# и должен, вероятно, создать его самостоятельно. Цель этого - игра, и я работаю над ней. Обычно игры имеют собственный пользовательский интерфейс (не WinForm). Я намерен настроить MVVM-дизайн для пользовательского интерфейса и игровых объектов в игре.

4b9b3361

Ответ 1

Ваш вопрос действительно интересный, но его объем действительно очень большой.

В этой ситуации действительно полезным инструментом является ILSpy, что позволяет вам взглянуть на реализацию фреймворка.

Одна вещь, с которой я столкнулся бы, это следующее утверждение:

Ответ, который я получил, заключается в том, что механизм привязки данных в С# в сочетании с интерфейсом WPF/Windows Forms UI

Я не согласен; механизм привязки данных тесно связан с реализацией событий .Net, но Target и Source могут быть любыми - большинство примеров будут Windows Forms, WPF или ASP.Net, потому что они являются наиболее распространенными интерфейсами для .Net-языков, но это вполне возможно использовать множественную привязку в других сценариях без пользовательского интерфейса.

Что произойдет, если вы добавите двустороннюю привязку? Ну, если мы посмотрим на источник MultiBinding, отметим несколько интересных вещей:

  • Он предоставляет свойство BindingMode, которое описывает сценарий привязки - обычно либо OneWay, либо TwoWay
  • В нем представлены два интересных события: NotifyOnSourceUpdated и NotifyOnTargetUpdated

Какие имеют базовую форму:

// System.Windows.Data.MultiBinding
/// <summary>Gets or sets a value that indicates whether to raise the <see cref="E:System.Windows.FrameworkElement.SourceUpdated" /> event when a value is transferred from the binding target to the binding source.</summary>
/// <returns>true if the <see cref="E:System.Windows.FrameworkElement.SourceUpdated" /> event will be raised when the binding source value is updated; otherwise, false. The default value is false.</returns>
[DefaultValue(false)]
public bool NotifyOnSourceUpdated
{
    get
    {
        return base.TestFlag(BindingBase.BindingFlags.NotifyOnSourceUpdated);
    }
    set
    {
        bool flag = base.TestFlag(BindingBase.BindingFlags.NotifyOnSourceUpdated);
        if (flag != value)
        {
            base.CheckSealed();
            base.ChangeFlag(BindingBase.BindingFlags.NotifyOnSourceUpdated, value);
        }
    }
}

то есть. мы используем события, чтобы сообщить нам, когда источник обновлен (OneWay), и когда цель также обновлена ​​(для привязки TwoWay)

Обратите внимание, что существует также класс PriorityBinding, который работает аналогичным образом, за исключением того, что вы можете подписаться на несколько источников данных, и он будет расставлять приоритеты для тех, кто быстро возвращает данные.

Итак, форма того, как это работает, понятна - когда мы создаем привязку, мы подписываемся на изменения с одной стороны (для обновлений только для чтения) или с обеих сторон (когда данные могут быть изменены в GUI, например, и отправляется обратно в источник данных), причем все уведомления управляются посредством событий.

Следующий вопрос, действительно, кто управляет событиями? Простой ответ заключается в том, что и Target, и Source делают. То, почему важно реализовать INotifyPropertyChanged, например, - все, что действительно делают Bindings, - это создать контракт о том, как обе стороны должны подписываться друг на друга, - это контракт, с которым Target и Source тесно связаны.

ObservableCollection является интересным тестовым примером для изучения, поскольку он широко используется в приложениях GUI для продвижения обновлений в источнике данных в пользовательском интерфейсе и для отправка изменений данных в пользовательском интерфейсе обратно в исходный источник данных.

Обратите внимание (смотря на код), как фактическое событие для обмена информацией о вещах изменилось очень просто, НО код для управления надстройками, удалениями, обновлениями на самом деле очень зависит от согласованности через свойство SimpleMonitor (BlockReentrancy и CheckReentrancy) - это эффективно гарантирует, что операции являются атомарными и что подписчики уведомляются об изменениях в порядке их возникновения и что базовая коллекция соответствует обновленным.

Это действительно сложная часть всей операции.

Короче говоря, реализация DataBinding в .Net не тесно связана с технологиями GUI; это просто то, что в большинстве примеров будет представлен DataBinding в контексте приложений Windows Forms, WPF или ASP.Net. Фактическое привязывание данных управляется событиями, и для того, чтобы вы могли использовать его, более важно синхронизировать и управлять изменениями в ваших данных. Структура DataBinding позволит вам одновременно связывать Target и Source вместе с общими обновлениями данных через контракт ( Интерфейсы), который он определяет.

Удачи, -)

EDIT:

Я сел и создал два класса MyCharacter и MyCharacterAttribute с явной целью настройки привязки данных TwoWay между атрибутами Health и HealthValue:

public class MyCharacter : DependencyObject
{
    public static DependencyProperty HealthDependency =
        DependencyProperty.Register("Health",
                                    typeof(Double),
                                    typeof(MyCharacter),
                                    new PropertyMetadata(100.0, HealthDependencyChanged));

    private static void HealthDependencyChanged(DependencyObject source,
            DependencyPropertyChangedEventArgs e)
    {
    }

    public double Health
    {
        get
        {
            return (double)GetValue(HealthDependency);
        }
        set
        {
            SetValue(HealthDependency, value);
        }
    }

    public void DrinkHealthPotion(double healthRestored)
    {
        Health += healthRestored;
    }
}

public class MyCharacterAttributes : DependencyObject
{
    public static DependencyProperty HealthDependency = 
        DependencyProperty.Register("HealthValue",
                                    typeof(Double),
                                    typeof(MyCharacterAttributes),
                                    new PropertyMetadata(100.0, HealthAttributeDependencyChanged));

    public double HealthValue
    {
        get
        {
            return (Double)GetValue(HealthDependency);
        }
        set
        {
            SetValue(HealthDependency, value);
        }
    }

    public List<BindingExpressionBase> Bindings { get; set; }

    public MyCharacterAttributes()
    {
        Bindings = new List<BindingExpressionBase>(); 
    }

    private static void HealthAttributeDependencyChanged(DependencyObject source,
            DependencyPropertyChangedEventArgs e)
    {
    }
}

Наиболее важные вещи, которые следует отметить здесь, - это наследование от DependencyObject и реализация DependencyProperty.

На практике тогда происходит следующее. Я создал простую форму WPF и установил следующий код:

MyCharacter Character { get; set; }

MyCharacterAttributes CharacterAttributes = new MyCharacterAttributes();

public MainWindow()
{
    InitializeComponent();

    Character = new MyCharacter();
    CharacterAttributes = new MyCharacterAttributes();

    // Set up the data binding to point at Character (Source) and 
    // Property Health (via the constructor argument for Binding)
    var characterHealthBinding = new Binding("Health");

    characterHealthBinding.Source = Character;
    characterHealthBinding.NotifyOnSourceUpdated = true;
    characterHealthBinding.NotifyOnTargetUpdated = true;
    characterHealthBinding.Mode = BindingMode.TwoWay;
    characterHealthBinding.IsAsync = true;

    // Now we bind any changes to CharacterAttributes, HealthDependency 
    // to Character.Health via the characterHealthBinding Binding
    var bindingExpression = 
        BindingOperations.SetBinding(CharacterAttributes, 
                                     MyCharacterAttributes.HealthDependency,
                                     characterHealthBinding);

    // Store the binding so we can look it up if necessary in a 
    // List<BindingExpressionBase> in our CharacterAttributes class,
    // and so it "lives" as long as CharacterAttributes does, too
    CharacterAttributes.Bindings.Add(bindingExpression);
}

private void HitChracter_Button(object sender, RoutedEventArgs e)
{
    CharacterAttributes.HealthValue -= 10.0;
}

private void DrinkHealth_Button(object sender, RoutedEventArgs e)
{
    Character.DrinkHealthPotion(20.0);
}

Щелчок по кнопке HitCharacter уменьшает свойство CharacterAttributes.HealthValue на 10. Это вызывает событие, которое через связывание, которое мы установили ранее, также вычитает 10.0 из значения Character.Health. Нажатие кнопки DrinkHealth восстанавливает Character.Health на 20.0, а также увеличивает CharacterAttributes.HealthValue на 20.0.

Также обратите внимание, что этот материал действительно испечен в инфраструктуре пользовательского интерфейса - FrameworkElement (который наследует от UIElement) имеет SetBinding и GetBinding, реализованные на нем. Что имеет смысл - элементы графического интерфейса DataBinding - это совершенно допустимый сценарий для пользовательских интерфейсов! Если вы посмотрите глубже, то SetValue, например, просто вызывает BindingOperations.SetBinding на внутреннем интерфейсе, поэтому мы можем реализовать его, не имея при этом необходимости использовать UIElement (как в примере выше). Однако одна из зависимостей, которую мы должны переносить, это DependencyObject и DependencyProperty - они обязательны для работы DataBinding, но, пока ваши объекты наследуются от DependencyObject, вам не нужно никуда идти рядом с текстовым полем: -)

Однако недостатком является то, что некоторые вещи Binding были реализованы с помощью методов internal, поэтому вы можете столкнуться с сценариями, в которых действия привязки, которые вы хотите реализовать, могут потребовать, чтобы вы написали дополнительный код, потому что вы просто не можете доступ к реализациям фреймворка, как родные классы. Однако, как было показано, возможно, что привязка данных TwoWay, подобная приведенному выше примеру, вполне возможна.

Ответ 2

Часть "Жизнь до привязки" в этот пост мне легче понять, как можно создать двустороннюю привязку.

Идея такая же, как Джеймс. Вы вызываете событие, когда вызывается средство настройки свойств. Но вы делаете это только в том случае, если значение свойства изменилось. Затем вы подписываетесь на мероприятие. А в подписчике вы меняете зависимое свойство. Для зависимого свойства вы делаете то же самое (для получения двухсторонней привязки). Эта схема не умирает с переполнением стека, так как setter мгновенно возвращает, если значение не изменилось.

Я сократил код в сообщение в эту ручную реализацию двухсторонней привязки:

    static void Main()
    {
        var ui = new Ui();
        var model = new Model();
        // setup two-way binding
        model.PropertyChanged += (propertyName, value) =>
        {
            if (propertyName == "Title")
                ui.Title = (string) value;
        };
        ui.PropertyChanged += (propertyName, value) =>
        {
            if (propertyName == "Title")
                model.Title = (string) value;
        };
        // test
        model.Title = "model";
        Console.WriteLine("ui.Title = " + ui.Title); // "ui.Title = model"
        ui.Title = "ui";
        Console.WriteLine("model.Title = " + model.Title);// "model.Title = ui"
        Console.ReadKey();
    }
}

public class Ui : Bindable
{
    private string _title;
    public string Title
    {
        get { return _title; }
        set
        {
            if (_title == value) return;
            _title = value; 
            OnChange("Title", value); // fire PropertyChanged event
        }
    }
}

public class Model : Bindable
{
    private string _title;
    public string Title
    {
        get { return _title; }
        set
        {
            if (_title == value) return;
            _title = value; 
            OnChange("Title", value); // fire PropertyChanged event
        }
    }
}

public class Bindable
{
    public delegate void PropertyChangedEventHandler(
        string propertyName, object value);
    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnChange(string propertyName, object value)
    {
        if (PropertyChanged != null)
            PropertyChanged(propertyName, value);
    }
}

Вы можете использовать аспекты (например, PostSharp) для перехвата вызовов set setter и, следовательно, избавиться от полей поддержки. Ваши классы будут выглядеть так:

public class Ui : Bindable
{
    [Bindable]
    public string Title { get; set; }
    [Bindable]
    public string Name { get; set; }
}

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

        Binder.Bind(() => ui.Title, () => model.Title);
        Binder.Bind(() => ui.Name, () => model.Name);

Мое доказательство концепции: https://gist.github.com/barsv/46650cf816647ff192fa

Ответ 3

Это довольно простая идея, но не обязательно простая в реализации. Вам требуется уведомление о двухстороннем событии. Объект модели уведомляет структуру привязки данных при ее изменении, а пользовательский интерфейс уведомляет структуру привязки данных для любых пользовательских взаимодействий.

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

Если вы не хотите изменять свою модель (т.е. хотите, чтобы привязка данных работала на POCOs), вам понадобится некоторый триггер, чтобы сообщить системе привязки данных проверить модель изменений с помощью отражения. Вы, вероятно, назовете это вручную, когда ваш код изменит модель.

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

Возможно, стоит рассмотреть документацию для knockout.js, http://knockoutjs.com/, очевидно, это веб-решение, но принципы одинаковы и он подробно описывает компоненты, которые находятся в библиотеке, которая в принципе будет очень похожа на компоненты любой системы.