数据绑定引擎如何在引擎盖下工作?

时间:2012-10-18 08:18:35

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

从技术上讲,数据绑定引擎如何在幕后工作?特别是,数据绑定中“同步器”的机制如何看起来像是什么样的?

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

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

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

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

我提出这个问题的原因的一些背景信息:

前段时间,我就如何在C#中使用未使用标准WinForms的UI的数据绑定做了question。我得到的答案是C#中的数据绑定引擎与WPF / Windows Forms UI紧密耦合。所以,我想我不能在C#中使用现有的数据绑定引擎,并且可能必须自己创建一个。这个目的是为了一场比赛,我正在努力。游戏通常有自己的自定义UI(非WinForm)。我的目的是为游戏中的UI和游戏对象设置类似MVVM的设计。

3 个答案:

答案 0 :(得分:18)

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

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

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

  

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

我不同意;数据绑定引擎与.Net事件实现紧密耦合,但Target和Source可以是任何东西 - 大多数示例将是Windows Forms,WPF或ASP.Net,因为它们是.Net语言最常见的前端,但它& #39;完全可以在没有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中的数据更改发送回基础数据源。

注意(通过查看代码)用于传递事物的实际事件如何变化非常简单,但管理Adds,Removes,Updates的代码实际上非常依赖于SimpleMonitor属性的一致性({{1} }和BlockReentrancy) - 它有效地保证了操作是原子的,并且订阅者被告知它们发生的顺序的变化以及底层集合与更新后的集合是一致的。

这确实是整个行动的棘手部分。

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

玩得开心; - )

修改

我坐下来创建了两个类CheckReentrancyMyCharacter,目的是在MyCharacterAttributeHealth属性之间设置TwoWay数据绑定:

HealthValue

这里要注意的最重要的事情是DependencyObject的继承和DependencyProperty的实施。

在实践中,接下来会发生以下情况。我创建了一个简单的WPF表单并设置了以下代码:

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)
    {
    }
}

单击HitCharacter按钮会将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); } 属性减少10.这会触发一个事件,通过我们之前设置的Binding,它还会从CharacterAttributes.HealthValue值中减去10.0。点击DrinkHealth按钮可将Character.Health恢复20.0,同时将Character.Health增加20.0。

另请注意,这些内容确实存在于UI框架中 - CharacterAttributes.HealthValue(继承自FrameworkElement)已在其上实施UIElementSetBinding。这是有道理的 - DataBinding GUI元素是用户界面的完美有效场景!但是,如果你看得更深,例如GetBinding只是在内部接口上调用SetValue,那么我们可以实现它而不必实际使用BindingOperations.SetBinding(根据示例)以上)。但是,我们必须继承的一个依赖项是UIElementDependencyObject - 这些对于DataBinding来说是必需的,但是,只要您的对象继承自DependencyProperty,您就不会#39; t需要去文本框附近的任何地方: - )

然而,缺点是某些Binding内容已经通过DependencyObject方法实现,因此您可能会遇到要实现的绑定操作可能需要您编写其他代码的情况,因为您只需& #39;访问类似本机类的框架实现。但是,如上所示,TwoWay数据绑定是完全可能的,如图所示。

答案 1 :(得分:6)

部分&#34;绑定前的生活&#34;在this post中,我更容易理解如何创建双向绑定。

这个想法与James描述的想法相同。在调用属性设置器时触发事件。但是如果属性值已更改,则只执行 。然后你订阅了这个活动。在订户中,您可以更改从属属性。对于依赖属性,您也可以执行相同的操作(以获得双向绑定)。这个模式不会因堆栈溢出而死亡,因为如果值没有改变,setter会立即返回。

我将the post中的代码缩减为双向绑定的手动实现:

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

您可以使用aspect(例如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

答案 2 :(得分:3)

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

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

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

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

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