【问题标题】:How do data binding engines work under the hood?数据绑定引擎如何在后台工作?
【发布时间】:2012-10-18 08:18:35
【问题描述】:

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

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

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

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

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

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

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

【问题讨论】:

  • .Net 的真正优点在于您可以使用ilspy.net 之类的工具来实际查看这些东西的实现——您实际上可以查看under the hood :-)

标签: c# data-binding 2-way-object-databinding


【解决方案1】:

你的问题很有意思,但范围其实很大。

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

我会反对的一件事是以下陈述:

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

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

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

  • 它公开了一个描述绑定场景的BindingMode 属性——通常是OneWayTwoWay
  • 它暴露了两个有趣的事件: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 很重要的原因,例如 - 所有 Bindings 真正做的就是为双方应该如何订阅彼此的更改创建一个合同 - 这是 Target 和 Source 紧密耦合的合同,真的。

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

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

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

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

玩得开心;-)

编辑:

我坐下来创建了两个类,MyCharacterMyCharacterAttribute,其明确目的是在 HealthHealthValue 属性之间设置 TwoWay 数据绑定:

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。这会触发一个事件,通过我们之前设置的绑定,它也会从 Character.Health 值中减去 10.0。点击 DrinkHealth 按钮可将 Character.Health 恢复 20.0,同时将 CharacterAttributes.HealthValue 增加 20.0。

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

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

【讨论】:

  • 谢谢,破折号! +1!从您所说的关于 DataBinding 实现能够将任何对象/变量设置为目标和源的内容,这是否意味着我实际上可以出于我的目的重用 C# 中的内置 DataBinding 引擎,而不必自己编写一个?我找不到任何关于此的文章或教程。所有数据绑定文章都讨论 WPF 和 WinForms。
  • @xEnOn 是的!只要您在对象上实现了必要的接口(例如,两个集合可以相互绑定数据),那么您仍然可以使用内置数据绑定。
  • 是否有任何教程或文章或示例说明如何仅使用 mscorlib 和 System.Data 库(非 WinForm/WPF)将数据相互绑定?此外,我尝试使用 ILSpy 查看不同的绑定类,但我仍然不确定要查看哪个类来查看“同步器”在数据绑定中的工作方式。同步器部分对我来说似乎最令人生畏,我仍然不知道如何实现同步器,除了实现一个巨大的观察者管理器之类的东西。
  • @xEnOn 没有真正的魔法同步器;该实现依赖于 .Net 框架中的事件在默认情况下是同步的这一事实。因此,只要您在本地(通过锁定或类似的阻塞机制)管理您的更改,一旦引发事件,所有订阅者都将接收并处理该事件。如果我有空闲时间,我会使用您的性格和健康示例为您写一个小样本。
  • 我明白了。谢谢!我还在读它。如果您有时间使用该示例展示一个小样本,那就太好了。我认为这将有助于具体化这个概念。非常感谢您的帮助!
【解决方案2】:

this post 中的“绑定前的生活”部分让我更容易理解如何创建双向绑定。

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

我将the post 中的代码简化为手动实现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);

我的概念证明:https://gist.github.com/barsv/46650cf816647ff192fa

【讨论】:

    【解决方案3】:

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

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

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

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

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

    【讨论】:

    • 谢谢,我会看一下 knockout.js 的文档。像这样的东西很少有文献记载,这真的很奇怪。除了使用数据绑定 API 之外,我找不到很多关于实现数据绑定的信息。是的,你有一些我也非常担心的事情,那就是事件的管道。我在想我应该如何设计它,这样它就不会乱七八糟,而且这些物体最终也不会无缘无故地相互纠缠和耦合。毕竟,我进行数据绑定的目的是降低耦合度。
    猜你喜欢
    • 2014-05-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-25
    • 1970-01-01
    相关资源
    最近更新 更多