22

技術的には、データ バインディング エンジンは内部でどのように機能するのでしょうか? 特に、データバインディングにおける「シンクロナイザー」のメカニズムはどのように見え、どのように機能しますか?

.NET、Java、Flex などの多くのフレームワークでは、データ バインディング エンジンが提供されます。私は API 呼び出しを使用しているだけなので、API を呼び出すだけでよいので、すべてが簡単です。

現在、私が取り組んでいるゲーム用の比較的単純なデータ バインディング エンジンを作成することに興味があります。私は C# を使用していますが、組み込みの WinForms とデータ バインディング エンジンを使用できない理由があります (理由については、以下の背景情報を参照してください)。既存のデータ バインディング エンジンを C# で使用できないため、自分で作成する必要があるのではないかと考えました。そのため、データ バインディングが内部で通常どのように機能するかについて、詳細を知る必要があります。これは、C# でデータ バインディングを使用する方法を意味するものではありません。つまり、データ バインディングは内部的およびアーキテクチャ的にどのように機能するのでしょうか。

データ バインディングに関するチュートリアルや記事を Web で検索しようとしましたが、ほとんどの結果は、C# で既存のデータ バインディングを使用する方法であり、私が望んでいるものではありませんでした。

そこで、独自のデータ バインダーの作成を計画する前に、データ バインディング エンジンが内部でどのように機能するかを知る必要があると考えました。さらに重要なことは、データバインディングエンジンの「シンクロナイザー」のメカニズムはどのように見え、どのように機能するのか、つまり、一方向または双方向のバインディングでデータが常に同期されているかということです。

私がこの質問をした理由の背景情報:

少し前に、標準の WinForms を使用していない UI に対して C# でデータ バインディングを使用する方法について質問しました。私が得た答えは、C# のデータ バインディング エンジンは WPF/Windows フォーム UI と密接に結合されているということでした。したがって、C# で既存のデータ バインディング エンジンを使用することはできず、おそらく独自に作成する必要があると思います。これの目的はゲームのためであり、私は取り組んでいます。通常、ゲームには独自のカスタム UI (非 WinForm) があります。私の意図は、ゲーム内の UI とゲーム オブジェクトに MVVM のようなデザインをセットアップすることです。

4

3 に答える 3

20

あなたの質問は非常に興味深いものですが、その範囲は実際には非常に大きいです。

このような状況で非常に便利なツールは、フレームワークの実装を確認できるILSpyです。

私が問題にするのは、次のステートメントです。

私が得た答えは、C# のデータ バインディング エンジンは WPF/Windows フォーム UI と密接に結合されているということでした。

同意しません; データ バインディング エンジンは .Net イベントの実装と密接に結合されていますが、ターゲットとソースは何でもかまいません。.Net 言語の最も一般的なフロント エンドであるため、ほとんどの例は Windows フォーム、WPF、または ASP.Net になります。 UI がなくても、他のシナリオでマルチ バインディングを使用することは完全に可能です。

双方向バインディングを追加するとどうなりますか? さて、MultiBindingのソースを見ると、興味深いことがいくつかわかります。

  • バインディング シナリオを記述するBindingModeプロパティを公開します。通常は、OneWayまたはTwoWay
  • 2 つの興味深いイベントが公開されています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は、データ ソース内の更新を UI にプロモートし、UI 内のデータへの変更を基になるデータ ソースに戻すために、GUI アプリケーションで広く使用されているため、興味深いテスト ケースです。

BlockReentrancy(コードを見て)物事が変化したことを伝えるための実際のイベントが非常に単純であることに注意しCheckReentrancyてください。操作はアトミックであり、サブスクライバーは変更が発生した順序で変更を通知され、基になるコレクションが更新されたものと一致することが保証されます。

これは、操作全体の中で本当に難しい部分です。

つまり、.Net での DataBinding の実装は、GUI テクノロジと密接に結合されていません。ほとんどの例は、Windows フォーム、WPF、または ASP.Net アプリケーションのコンテキストで DataBinding を提示するだけです。実際のデータ バインディングはイベント ドリブンであり、それを活用するには、データの変更を同期して管理することがより重要です。インターフェイス) が定義します。

楽しむ ;-)

編集:

私は座って 2 つのクラスを作成し、 とMyCharacter属性のMyCharacterAttribute間に TwoWay データバインディングを設定するという明確な目的を持っていました。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 ボタンを押すと、20.0 回復Character.Healthし、20.0 増加しCharacterAttributes.HealthValueます。

また、このようなものは実際にUIフレームワークに組み込まれていることに注意してください- FrameworkElement(から継承UIElement)が実装されていますSetBindingGetBindingこれは理にかなっています-DataBinding GUI要素は、ユーザーインターフェイスにとって完全に有効なシナリオです! ただし、さらに詳しく見るとSetValue、たとえば、 はBindingOperations.SetBinding内部インターフェイスを呼び出しているだけなので、実際に a を使用しなくてもUIElement(上記の例のように) 実装できます。ただし、引き継がなければならない 1 つの依存関係はDependencyObjectandですDependencyProperty。これらは DataBinding が機能するために必須ですが、オブジェクトが から継承している限りDependencyObject、テキスト ボックスの近くに移動する必要はありません :-)

ただし、欠点は、バインディングの一部がinternalメソッドを介して実装されていることです。そのため、ネイティブのようなフレームワークの実装にアクセスできないため、実装したいバインディング アクションで追加のコードを記述する必要があるシナリオに遭遇する可能性があります。クラスはできます。ただし、上記の例のような TwoWay データバインディングは完全に可能です。

于 2012-10-18T09:18:49.273 に答える
9

この投稿の「バインディング前の生活」の部分は、双方向バインディングを作成する方法を理解するのに簡単でした。

アイデアは、 Jamesの説明と同じです。プロパティ セッターが呼び出されたときにイベントを発生させます。ただし、プロパティの値が変更された場合にのみ行います。次に、イベントにサブスクライブします。サブスクライバーでは、依存プロパティを変更します。依存プロパティについても同じことを行います (双方向バインディングを取得するため)。値が変更されていない場合、セッターは即座に戻るため、このスキーマはスタック オーバーフローで停止しません。

投稿のコードを、双方向バインディングのこの手動実装に減らしました。

    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

于 2014-10-21T10:59:09.553 に答える
3

これは非常に単純なアイデアですが、実装は必ずしも単純ではありません。双方向のイベント通知が必要です。モデル オブジェクトは変更時にデータ バインディング フレームワークに通知し、UI はデータ バインディング フレームワークにユーザー インタラクションを通知します。

モデル側では、これは、プロパティの変更 ( INotifyPropertyChangedインターフェイスの実装など) やコレクションへの変更 ( ObservableCollecitonの使用など)を通知するモデルを作成することを意味します。UI 側では、UI システムによって提供されるイベントに接続するだけです。

モデルを変更したくない場合 (つまり、POCO でデータ バインディングを機能させたい場合) は、リフレクションを使用してモデルの変更をチェックするようにデータ バインディング システムに指示するトリガーが必要です。コードがモデルを変更するたびに、おそらくこれを手動で呼び出すでしょう。

その後は、すべてのイベントを組み立てるだけですが、さまざまな種類のデータをさまざまな種類の UI に接続するさまざまな種類のバインド オブジェクトのライブラリが必要になるため、おそらくここが厄介になります。

ノックアウト js ( http://knockoutjs.com/ )のドキュメントを確認する価値があるかもしれませんが、これは 明らかに Web ソリューションですが、プリンシパルは同じであり、ライブラリにあるコンポーネントについて詳しく説明されています。原則として、あらゆるシステムのコンポーネントと非常によく似ています。

于 2012-10-18T08:39:46.117 に答える