数据绑定引擎在底层是如何工作的?

22
技术上,数据绑定引擎是如何在幕后工作的?特别是,数据绑定中的“同步器”机制是什么样子,如何工作?
在许多框架(如.NET、Java、Flex等)中,它们提供了数据绑定引擎。我一直在使用API调用,所以一切都很容易,因为我只需要调用API。
现在,我有兴趣尝试为我正在开发的游戏编写一个相对简单的数据绑定引擎。虽然我正在使用C#,但由于某些原因,我不能使用内置的WinForms和数据绑定引擎(请参见下面的背景信息)。既然我不能在C#中使用现有的数据绑定引擎,我想我可能必须自己编写一个。因此,我需要知道数据绑定通常在幕后是如何工作的细节。我的意思不是如何在C#中使用数据绑定。我的意思是,数据绑定在内部和架构上是如何工作的。
我试图搜索关于数据绑定的教程和文章,但大多数结果都是关于如何在C#中使用现有的数据绑定,这不是我想要的。
因此,在我开始计划编写自己的数据绑定器之前,我认为我需要知道数据绑定引擎在底层是如何工作的?更重要的是,数据绑定引擎中“同步器”的机制是什么样子并且如何工作,即无论是单向绑定还是双向绑定,数据都始终保持同步?
一些背景信息:以前,我提出了一个question,询问如何在未使用标准WinForms的UI中使用C#中的数据绑定。我得到的答案是,C#中的数据绑定引擎与WPF / Windows Forms UI紧密耦合。所以,我猜我不能使用现有的C#数据绑定引擎,可能必须自己创建一个。这是为了游戏而进行的工作。游戏通常具有自己的自定义UI(非WinForm)。我的意图是为UI和游戏对象设置类似MVVM的设计。

.NET 最棒的地方就是你可以使用像 http://ilspy.net/ 这样的工具来查看这些东西的实现 - 你可以真正地看到“引擎盖下”的内容 :-) - dash
3个回答

20

你的问题非常有趣,但它的范围实际上非常广。

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

我要提出的一件事是以下声明:

我得到的答案是C#中的数据绑定引擎与WPF / Windows Forms 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中更改并发送回数据源时),所有通知都通过事件管理。

下一个问题是,真正的事件管理者是谁?简单的答案是源和目标都需要。这就是为什么实现INotifyPropertyChanged很重要的原因,所有绑定实际上都是为双方应该如何订阅彼此的变化创建的一个协议——这实际上是目标和源紧密耦合的协议。

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

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

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

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

玩得开心;-)

编辑:

我坐下来创建了两个类MyCharacterMyCharacterAttribute,旨在建立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。这会触发一个事件,通过之前设置的绑定,也会从Character.Health值中减去10.0。点击DrinkHealth按钮会将Character.Health恢复20.0,并将CharacterAttributes.HealthValue增加20.0。

还要注意的是,这些都确实嵌入在UI框架中 - FrameworkElement(从UIElement继承)已经实现了SetBinding和GetBinding。这很有道理 - 数据绑定GUI元素是用户界面的一个完全有效的场景!如果你更深入地了解,SetValue只是在内部接口上调用BindingOperations.SetBinding,因此我们可以实现它而不必使用UIElement(如上例所示)。但是,我们必须传递的一个依赖项是DependencyObject和DependencyProperty - 这些对于数据绑定是强制性的,但只要您的对象继承自DependencyObject,您就不需要接近文本框 :-)

然而,缺点是一些绑定相关的内容已经通过内部方法实现,因此您可能会遇到需要编写额外代码的情况,因为您无法像本地类那样访问框架实现。然而,像上面的双向绑定一样的TwoWay数据绑定是完全可能的,正如所示。


谢谢,dash!+1!从你所说的DataBinding实现可以将任何对象/变量设置为目标和源,这是否意味着我实际上可以重用C#内置的DataBinding引擎来达到我的目的,而无需编写自己的引擎?我找不到任何关于此的文章或教程。所有数据绑定文章都只谈论WPF和WinForms。 - Carven
@xEnOn 是的!只要在您的对象上实现必要的接口(例如,两个集合可以相互数据绑定),那么您仍然可以使用内置的数据绑定。 - dash
有没有任何关于如何仅使用mscorlib和System.Data库(非WinForm/WPF)将数据绑定到彼此的教程、文章或示例?此外,我尝试通过ILSpy查看不同的绑定类,但我仍然不确定哪个类是用于查看数据绑定中“同步器”工作的类。同步器部分对我来说似乎最具挑战性,我仍然不知道如何实现同步器,除了实现一个巨大的观察者管理器之类的东西。 - Carven
我明白了,谢谢!我还在阅读相关内容。如果你有时间能够用例子给我展示一个小样本就太好了。我认为这将会真正有助于理解概念。非常感谢你的帮助! - Carven
@Atru - 是的,我在倒数第二段中提到了这一点。希望它们不是这样,但从实际角度来看,只要你针对完整的 .Net 4 功能集,这并不是什么大问题。 - dash
显示剩余2条评论

9
这篇文章中,“绑定前的生活”部分更容易让我理解如何创建双向绑定。
这个想法与James描述的相同。当调用属性设置器时,您会触发一个事件。但是,仅当属性值已更改时才这样做。然后您订阅该事件。在订阅者中,您更改依赖属性。对于依赖属性,您执行相同的操作(以获得双向绑定)。由于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)拦截属性设置器调用,从而摆脱后台字段。 你的类会像这样:

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
这是一个相当简单的想法,但并不一定容易实现。您需要进行双向事件通知。您的模型对象在更改时通知数据绑定框架,UI则通知数据绑定框架有关任何用户交互的信息。
在模型方面,这意味着编写您的模型以通知属性更改(例如实现INotifyPropertyChanged接口)和集合更改(例如使用ObservableColleciton)。在UI方面,您只需连接到UI系统提供的事件即可。
如果您不想更改模型(即希望数据绑定在POCO上工作),那么您需要某些触发器来使用反射告诉数据绑定系统检查模型是否有更改。每当代码更改模型时,您可能会手动调用它。
之后,只需连接所有事件即可。这可能会变得混乱,因为您需要一个不同类型的绑定对象库,将各种类型的数据连接到各种类型的UI上。

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


谢谢,我会仔细查看 knockout.js 的文档。很奇怪像这样的东西竟然有如此少的文献资料。除了使用数据绑定 API 之外,我找不到太多关于实现数据绑定的信息。是的,你也担心的是我非常担心的事情,那就是事件的管道。我正在考虑如何设计它,使其不会混乱,对象也不会为了无意义的事情而纠缠和耦合在一起。毕竟,我的目的是通过数据绑定使事物变得不那么紧密耦合。 - Carven

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接