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

Car*_*ven 21 c# data-binding 2-way-object-databinding

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

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

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

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

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

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

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

das*_*ash 18

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

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

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

我得到的答案是C#中的数据绑定引擎与WPF/Windows Forms 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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

即我们使用事件告诉我们何时更新源(OneWay)以及何时更新目标(用于TwoWay绑定)

请注意,PriorityBinding除了可以订阅多个数据源之外,还有一个以类似方式运行的类,它将优先处理最快返回数据的类.

所以它的工作方式很明显 - 当我们创建一个绑定时,我们订阅一方的更改(对于只读更新)或双方(例如,当在GUI中更改数据时,可以发送回到数据源),通过事件管理所有通知.

接下来的问题是,谁管理这些事件?简单的答案是Target和Source都有.这就是为什么实施INotifyPropertyChanged很重要的原因,例如 - 所有Bindings真正做的是创建一个双方应如何订阅彼此变化的合同 - 这是Target和Source紧密耦合的合同,真的.

ObservableCollection是一个有趣的测试用例,因为它广泛用于GUI应用程序,用于将数据源中的更新提升到UI,以及将UI中的数据更改发送回底层数据源.

请注意(通过查看代码)如何实现事件的实际事件更改非常简单,但管理Adds,Removes,Updates的代码实际上非常依赖于SimpleMonitor属性(BlockReentrancyCheckReentrancy)的一致性- 它有效地保证了这些操作是原子操作,并且订阅者会收到发生顺序的更改通知,并且基础集合与更新后的集合一致.

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

简而言之,.Net中的DataBinding实现并没有与GUI技术紧密耦合; 只是大多数示例将在Windows窗体,WPF或ASP.Net应用程序的上下文中呈现DataBinding.实际数据绑定是事件驱动的,并且,为了您利用它,同步和管理数据更改更为重要 - 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)
    {
    }
}
Run Code Online (Sandbox Code Playgroud)

最重要的事情需要注意,这里是来自继承的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);
}
Run Code Online (Sandbox Code Playgroud)

单击HitCharacter按钮会将CharacterAttributes.HealthValue属性减少10.这会触发一个事件,通过我们之前设置的Binding,它也会从Character.Health值中减去10.0 .点击DrinkHealth按钮可以恢复Character.Health20.0并且还会增加CharacterAttributes.HealthValue20.0.

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

然而,缺点是某些Binding内容已经通过internal方法实现,因此您可能会遇到要实现的绑定操作可能需要您编写其他代码的情况,因为您根本无法访问框架实现,如本机课程可以.然而,正如已经示出的那样,如上例所示的TwoWay数据绑定是完全可能的.


Sta*_*lav 6

在部分"结合之前的生活" 这个帖子是我更容易得到怎样的结合可以创造双向理解.

这个想法和詹姆斯描述的一样.在调用属性设置器时触发事件.但是只有在属性值发生变化时才会这样做.然后你订阅了这个活动.在订户中,您可以更改从属属性.对于依赖属性,您也可以执行相同的操作(以获得双向绑定).这个模式不会因堆栈溢出而死亡,因为如果值没有改变,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);
    }
}
Run Code Online (Sandbox Code Playgroud)

您可以使用方面(例如PostSharp)来拦截属性设置器调用,从而摆脱支持字段.你的课程将如下所示:

public class Ui : Bindable
{
    [Bindable]
    public string Title { get; set; }
    [Bindable]
    public string Name { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

使用反射,您可以将绑定代码减少到:

        Binder.Bind(() => ui.Title, () => model.Title);
        Binder.Bind(() => ui.Name, () => model.Name);
Run Code Online (Sandbox Code Playgroud)

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