22

从技术上讲,数据绑定引擎如何在幕后工作?尤其是数据绑定中“同步器”的机制是怎样的?

在 .NET、Java、Flex 等许多框架中,它们提供了数据绑定引擎。我一直在使用 API 调用,所以一切对我来说都很容易,因为我所要做的就是调用 API。

现在,我有兴趣尝试为我正在开发的游戏编写一个相对简单的数据绑定引擎。虽然我使用的是 C#,但我有理由无法使用内置的 WinForms 和数据绑定引擎(请参阅下面的背景信息了解原因)。由于我无法使用 C# 中现有的数据绑定引擎,我想我可能不得不自己编写一个。因此,我需要了解数据绑定通常如何在幕后工作的基本细节。这并不是说如何在 C# 中使用数据绑定。我的意思是,数据绑定在内部和架构上是如何工作的。

我尝试在网上搜索有关数据绑定的教程和文章,但大多数结果都是关于如何在 C# 中使用现有数据绑定,这不是我想要的。

所以,在我开始计划编写自己的数据绑定器之前,我想我需要知道数据绑定引擎是如何工作的?更重要的是,数据绑定引擎中的“同步器”机制的外观和工作方式如何,即数据如何始终保持同步,无论是单向绑定还是双向绑定?

关于我为什么问这个问题的一些背景信息:

不久前,我提出了一个问题,关于如何在 C# 中为不使用标准 WinForms 的 UI 使用数据绑定。我得到的答案是 C# 中的数据绑定引擎与 WPF/Windows 窗体 UI 紧密耦合。所以,我想我不能使用 C# 中现有的数据绑定引擎,可能不得不自己创建一个。这样做的目的是为了一个游戏,我正在努力。游戏通常有自己的自定义 UI(非 WinForm)。我的目的是为游戏中的 UI 和游戏对象设置类似 MVVM 的设计。

4

3 回答 3

20

你的问题是一个非常有趣的问题,但它的范围实际上非常大。

在这种情况下,一个非常有用的工具是ILSpy,它允许您查看框架实现。

我会反对的一件事是以下声明:

我得到的答案是 C# 中的数据绑定引擎与 WPF/Windows 窗体 UI 紧密耦合

我不同意; 数据绑定引擎与 .Net 事件实现紧密耦合,但 Target 和 Source 可以是任何东西 - 大多数示例将是 Windows Forms、WPF 或 ASP.Net,因为它们是 .Net 语言最常见的前端,但它是完全可以在没有 UI 的其他场景中使用多重绑定。

添加双向绑定时会发生什么?好吧,如果我们查看MultiBinding的源代码,我们会注意到一些有趣的事情:

  • 它公开了一个BindingMode属性,该属性描述了绑定场景——通常是OneWay或者TwoWay
  • 它暴露了两个有趣的事件:NotifyOnSourceUpdatedNotifyOnTargetUpdated

其中有基本形式:

// 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很重要,例如——所有绑定真正做的是为双方应该如何订阅彼此的更改创建一个契约——它是目标和源紧密耦合的契约,真的。

ObservableCollection是一个值得研究的有趣测试用例,因为它广泛用于 GUI 应用程序中,用于将数据源中的更新推广到 UI,并将 UI 中数据的更改发送回底层数据源。

请注意(通过查看代码)用于传达事物已更改的实际事件非常简单,但是用于管理添加、删除、更新的代码实际上非常依赖于通过 SimpleMonitor 属性(BlockReentrancyCheckReentrancy)的一致性 - 它有效地保证了这些操作是原子的,并且订阅者会按照发生的顺序收到更改通知,并且基础集合与更新的集合一致。

这确实是整个操作的棘手部分。

简而言之,.Net 中的 DataBinding 实现并没有与 GUI 技术紧密耦合;只是大多数示例将在 Windows 窗体、WPF 或 ASP.Net 应用程序的上下文中呈现 DataBinding。实际的数据绑定是事件驱动的,为了利用它,更重要的是同步和管理对数据的更改 - DataBinding 框架只允许您通过合约将 Target 和 Source 在共享数据更新中耦合在一起(接口)它定义。

玩得开心 ;-)

编辑:

我坐下来创建了两个类,MyCharacter明确的目的是在和属性MyCharacterAttribute之间设置双向数据绑定:HealthHealthValue

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。这会触发一个事件,通过我们之前设置的 Binding,该事件也会从Character.Health值中减去 10.0。点击 DrinkHealth 按钮可恢复Character.Health20.0 并增加CharacterAttributes.HealthValue20.0。

另请注意,这些东西确实已融入 UI 框架FrameworkElement(继承自UIElementSetBindingGetBinding在其上实现。这是有道理的——DataBinding GUI 元素对于用户界面来说是一个完全有效的场景!但是,如果你看得更深入,SetValue例如,它只是调用BindingOperations.SetBinding一个内部接口,所以我们可以实现它而无需实际使用 a UIElement(如上例所示)。但是,我们必须继承的一个依赖项是DependencyObjectand DependencyProperty- 这些是 DataBinding 工作所必需的,但是,只要您的对象继承自DependencyObject,您就不需要靠近文本框 :-)

然而,缺点是一些 Binding 的东西是通过internal方法实现的,所以你可能会遇到想要实现的绑定操作可能需要你编写额外代码的场景,因为你根本无法访问原生的框架实现班可以。但是,如上例所示,TwoWay 数据绑定是完全可能的,如所示。

于 2012-10-18T09:18:49.273 回答
9

这篇文章中的“绑定之前的生活”部分让我更容易理解如何创建双向绑定。

这个想法与詹姆斯描述的相同。当调用属性设置器时触发一个事件。但是只有在属性值发生变化时才这样做。然后您订阅该事件。在订阅者中,您更改了一个依赖属性。对于从属属性,您执行相同的操作(以获取 2 路绑定)。这个模式不会因为堆栈溢出而死,因为如果值没有改变,setter 会立即返回。

将帖子中的代码简化为 2 向绑定的手动实现:

    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)来拦截属性设置器调用,从而摆脱支持字段。您的课程将如下所示:

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);

My proof of concept: https://gist.github.com/barsv/46650cf816647ff192fa

于 2014-10-21T10:59:09.553 回答
3

这是一个非常简单的想法,但实现起来并不一定简单。您需要 2 路事件通知。您的模型对象在更改时通知数据绑定框架,并且 UI 通知数据绑定框架任何用户交互。

在模型方面,这意味着编写模型以通知属性的任何更改(例如实现INotifyPropertyChanged接口)和集合的更改(例如使用ObservableColleciton)。在 UI 方面,您可以连接到 UI 系统提供的事件。

如果您不想更改模型(即您希望数据绑定在 POCO 上工作),那么您需要一些触发器来告诉数据绑定系统使用反射检查模型的更改。每当您的代码更改模型时,您可能会手动调用它。

之后它只是探测所有事件,这可能是它变得混乱的地方,因为您需要一个不同类型的绑定对象库,将各种类型的数据连接到各种类型的 UI。

可能值得查看 knockout.js 的文档, http: //knockoutjs.com/,显然是一个 Web 解决方案,但原理是相同的,并且它详细介绍了库中的组件原则上将与任何系统的组件非常相似。

于 2012-10-18T08:39:46.117 回答