2

これには半単純な解決策があるに違いないように感じますが、私には理解できません。

編集:前の例は無限ループをより明確に示しましたが、これはもう少しコンテキストを与えます。問題の概要については、事前編集を確認してください。

次の2つのクラスは、モデルビュービューモデル(MVVM)パターンのビューモデルを表します。

/// <summary>
/// A UI-friendly wrapper for a Recipe
/// </summary>
public class RecipeViewModel : ViewModelBase
{
    /// <summary>
    /// Gets the wrapped Recipe
    /// </summary>
    public Recipe RecipeModel { get; private set; }

    private ObservableCollection<CategoryViewModel> categories = new ObservableCollection<CategoryViewModel>();

    /// <summary>
    /// Creates a new UI-friendly wrapper for a Recipe
    /// </summary>
    /// <param name="recipe">The Recipe to be wrapped</param>
    public RecipeViewModel(Recipe recipe)
    {
        this.RecipeModel = recipe;
        ((INotifyCollectionChanged)RecipeModel.Categories).CollectionChanged += BaseRecipeCategoriesCollectionChanged;

        foreach (var cat in RecipeModel.Categories)
        {
            var catVM = new CategoryViewModel(cat); //Causes infinite loop
            categories.AddIfNewAndNotNull(catVM);
        }
    }

    void BaseRecipeCategoriesCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        switch (e.Action)
        {
            case NotifyCollectionChangedAction.Add:
                categories.Add(new CategoryViewModel(e.NewItems[0] as Category));
                break;
            case NotifyCollectionChangedAction.Remove:
                categories.Remove(new CategoryViewModel(e.OldItems[0] as Category));
                break;
            default:
                throw new NotImplementedException();
        }
    }

    //Some Properties and other non-related things

    public ReadOnlyObservableCollection<CategoryViewModel> Categories 
    {
        get { return new ReadOnlyObservableCollection<CategoryViewModel>(categories); }
    }

    public void AddCategory(CategoryViewModel category)
    {
        RecipeModel.AddCategory(category.CategoryModel);
    }

    public void RemoveCategory(CategoryViewModel category)
    {
        RecipeModel.RemoveCategory(category.CategoryModel);
    }

    public override bool Equals(object obj)
    {
        var comparedRecipe = obj as RecipeViewModel;
        if (comparedRecipe == null)
        { return false; }
        return RecipeModel == comparedRecipe.RecipeModel;
    }

    public override int GetHashCode()
    {
        return RecipeModel.GetHashCode();
    }
}

/// <summary>
/// A UI-friendly wrapper for a Category
/// </summary>
public class CategoryViewModel : ViewModelBase
{
    /// <summary>
    /// Gets the wrapped Category
    /// </summary>
    public Category CategoryModel { get; private set; }

    private CategoryViewModel parent;
    private ObservableCollection<RecipeViewModel> recipes = new ObservableCollection<RecipeViewModel>();

    /// <summary>
    /// Creates a new UI-friendly wrapper for a Category
    /// </summary>
    /// <param name="category"></param>
    public CategoryViewModel(Category category)
    {
        this.CategoryModel = category;
        (category.DirectRecipes as INotifyCollectionChanged).CollectionChanged += baseCategoryDirectRecipesCollectionChanged;

        foreach (var item in category.DirectRecipes)
        {
            var recipeVM = new RecipeViewModel(item); //Causes infinite loop
            recipes.AddIfNewAndNotNull(recipeVM);
        }
    }

    /// <summary>
    /// Adds a recipe to this category
    /// </summary>
    /// <param name="recipe"></param>
    public void AddRecipe(RecipeViewModel recipe)
    {
        CategoryModel.AddRecipe(recipe.RecipeModel);
    }

    /// <summary>
    /// Removes a recipe from this category
    /// </summary>
    /// <param name="recipe"></param>
    public void RemoveRecipe(RecipeViewModel recipe)
    {
        CategoryModel.RemoveRecipe(recipe.RecipeModel);
    }

    /// <summary>
    /// A read-only collection of this category's recipes
    /// </summary>
    public ReadOnlyObservableCollection<RecipeViewModel> Recipes
    {
        get { return new ReadOnlyObservableCollection<RecipeViewModel>(recipes); }
    }


    private void baseCategoryDirectRecipesCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        switch (e.Action)
        {
            case NotifyCollectionChangedAction.Add:
                var recipeVM = new RecipeViewModel((Recipe)e.NewItems[0], this);
                recipes.AddIfNewAndNotNull(recipeVM);
                break;
            case NotifyCollectionChangedAction.Remove:
                recipes.Remove(new RecipeViewModel((Recipe)e.OldItems[0]));
                break;
            default:
                throw new NotImplementedException();
        }
    }

    /// <summary>
    /// Compares whether this object wraps the same Category as the parameter
    /// </summary>
    /// <param name="obj">The object to compare equality with</param>
    /// <returns>True if they wrap the same Category</returns>
    public override bool Equals(object obj)
    {
        var comparedCat = obj as CategoryViewModel;
        if(comparedCat == null)
        {return false;}
        return CategoryModel == comparedCat.CategoryModel;
    }

    /// <summary>
    /// Gets the hashcode of the wrapped Categry
    /// </summary>
    /// <returns>The hashcode</returns>
    public override int GetHashCode()
    {
        return CategoryModel.GetHashCode();
    }
}

要求されない限り、モデル(レシピとカテゴリ)を表示することはしませんが、基本的にビジネスロジックを処理します(たとえば、カテゴリにレシピを追加すると、リンクのもう一方の端も追加されます。つまり、カテゴリにレシピ、そしてレシピもそのカテゴリに含まれています)そして基本的に物事がどうなるかを指示します。ViewModelsは、WPFデータバインディングのための優れたインターフェイスを提供します。それがラッパークラスの理由です

無限ループはコンストラクター内にあり、新しいオブジェクトを作成しようとしているため、どちらのオブジェクトも作成が完了しないため、これを防ぐためにブールフラグを設定することはできません。

私が考えているのは(シングルトンとして、またはコンストラクターに渡されるか、あるいはその両方として)aDictionary<Recipe, RecipeViewModel>でありDictionary<Category, CategoryViewModel>、ビューモデルを遅延ロードしますが、ビューモデルが既に存在する場合は新しいモデルを作成しませんが、私は理解していません遅くなってからうまくいくかどうか試してみると、過去6時間ほどこれに対処するのにちょっとうんざりしています。

手元の問題とは関係のないものをたくさん取り出したので、ここのコードがコンパイルされる保証はありません。

4

8 に答える 8

2

元の質問 (およびコード) に戻ります。自動的に同期される多対多の関係が必要な場合は、読み進めてください。これらのケースを処理する複雑なコードを探すのに最適な場所は、ORM フレームワークのソース コードであり、このツールのドメインでは非常に一般的な問題です。nHibernate のソース コード ( https://nhibernate.svn.sourceforge.net/svnroot/nhibernate/trunk/nhibernate/ ) を見て、1-N と MN の両方の関係を処理するコレクションを実装する方法を確認します。

簡単に試すことができるのは、それを処理する独自の小さなコレクション クラスを作成することです。以下では、元のラッパー クラスを削除し、BiList コレクションを追加しました。これは、オブジェクト (コレクションの所有者) と、同期を維持するプロパティの反対側の名前で初期化されます (MN に対してのみ機能しますが、1- N は簡単に追加できます)。もちろん、コードを洗練する必要があります。

using System.Collections.Generic;

public interface IBiList
{
    // Need this interface only to have a 'generic' way to set the other side
    void Add(object value, bool addOtherSide);
}

public class BiList<T> : List<T>, IBiList
{
    private object owner;
    private string otherSideFieldName;

    public BiList(object owner, string otherSideFieldName) {
        this.owner = owner;
        this.otherSideFieldName = otherSideFieldName;
    }

    public new void Add(T value) {
        // add and set the other side as well
        this.Add(value, true);
    }

    void IBiList.Add(object value, bool addOtherSide) {
        this.Add((T)value, addOtherSide);
    }

    public void Add(T value, bool addOtherSide) {
        // note: may check if already in the list/collection
        if (this.Contains(value))
            return;
        // actuall add the object to the list/collection
        base.Add(value);
        // set the other side
        if (addOtherSide && value != null) {
            System.Reflection.FieldInfo x = value.GetType().GetField(this.otherSideFieldName);
            IBiList otherSide = (IBiList) x.GetValue(value);
            // do not set the other side
            otherSide.Add(this.owner, false);
        }
    }
}

class Foo
{
    public BiList<Bar> MyBars;
    public Foo() {
        MyBars = new BiList<Bar>(this, "MyFoos");
    }
}

class Bar
{
    public BiList<Foo> MyFoos;
    public Bar() {
        MyFoos = new BiList<Foo>(this, "MyBars");
    }
}



public class App
{
    public static void Main()
    {
        System.Console.WriteLine("setting...");

        Foo testFoo = new Foo();
        Bar testBar = new Bar();
        Bar testBar2 = new Bar();
        testFoo.MyBars.Add(testBar);
        testFoo.MyBars.Add(testBar2);
        //testBar.MyFoos.Add(testFoo); // do not set this side, we expect it to be set automatically, but doing so will do no harm
        System.Console.WriteLine("getting foos from Bar...");
        foreach (object x in testBar.MyFoos)
        {
            System.Console.WriteLine("  foo:" + x);
        }
        System.Console.WriteLine("getting baars from Foo...");
        foreach (object x in testFoo.MyBars)
        {
            System.Console.WriteLine("  bar:" + x);
        }
    }
}
于 2009-05-25T08:38:29.970 に答える
1

オプション:

  1. メンバーシップテストを実装します。たとえば、追加する前にbar-is-member-of-fooを確認します。
  2. 多対多の関係を独自のクラスに移動する

後者の方が好ましいと思います-より関係的に健全です

もちろん、foo-barの例では、目標が何であるかが実際にはわからないため、マイレージは異なる場合があります

編集:元の質問のコードを考えると、リストに何かが追加される前に無限再帰が発生するため、#1は機能しません。

このアプローチ/質問にはいくつかの問題があります。おそらく、それがほぼ愚かになるまで抽象化されているためです。コーディングの問題を説明するのに適していますが、元の意図/目標を説明するのにはあまり適していません。

  1. ラッパークラスは実際には何もラップしたり、有用な動作を追加したりしません。これにより、なぜそれらが必要なのかを理解するのが難しくなります
  2. 指定された構造では、各ラッパーリストが他のラッパーリストの新しいインスタンスをすぐに作成するため、コンストラクターリストを初期化することはできません。
  3. 初期化と構築を分離した場合でも、メンバーシップが非表示の循環依存関係があります(つまり、ラッパーは相互に参照しますが、foo / bar要素はcontainsチェックから非表示にします。コードが何も追加しないため、これは実際には問題ではありません。とにかくどんなリストにも!)
  4. 直接リレーショナルアプローチは機能しますが、検索メカニズムが必要であり、ラッパーが事前にではなく必要に応じて作成されることを前提としています。たとえば、検索関数を含む配列または辞書のペア(Dictionary>、Dictionary>など)がマッピングに機能します。ただし、オブジェクトモデルに適合しない可能性があります

結論

説明したような構造はうまくいかないと思います。DIを使用しない、ファクトリを使用しない、まったく使用しない-ラッパーがサブリストを非表示にしているときに相互に参照するためです。

この構造は、述べられていない誤った仮定を示唆していますが、文脈がなければ、それらが何であるかを突き止めることはできません。

現実世界のオブジェクトと望ましい目標/意図を使用して、元のコンテキストで問題を言い換えてください。

または、少なくとも、サンプルコードで生成する必要があると思われる構造を記述してください。;-)

補遺

明確化してくれてありがとう、これは状況をより理解しやすくします。

私はWPFデータバインディングを使用していませんが、このMSDNの記事をざっと読んだので、次のことが役立つ場合とそうでない場合があります。

  • ビューモデルクラスのカテゴリとレシピコレクションは冗長だと思います
    • 基になるカテゴリオブジェクトにすでにM:M情報があるので、ビューモデルでそれを複製する理由
    • コレクションが変更されたハンドラーも無限再帰を引き起こすようです
    • コレクションが変更されたハンドラーは、ラップされたレシピ/カテゴリの基になるM:M情報を更新していないようです。
  • ビューモデルの目的は、基になるモデルデータを公開することであり、その各コンポーネントを個別にラップすることではないと思います。
    • これは冗長であり、カプセル化に違反しているようです
    • これは、無限再帰の問題の原因でもあります
    • 単純に、ObservableCollectionプロパティは、基になるモデルのコレクションを返すだけであると期待しています...

使用している構造は、多対多の関係を表す「転置インデックス」表現です。これは、最適化されたルックアップと依存関係の管理では非常に一般的です。それは1対多の関係のペアになります。MSDNの記事のGamesViewModelの例を見てください-Gamesプロパティは

ObservableCollection<Game>

ではなく

ObservableCollection<GameWrapper>
于 2009-05-25T01:05:10.867 に答える
1

たとえば、依存性逆転の原則を通じて、相互依存を取り除くことをお勧めします。http://en.wikipedia.org/wiki/Dependency_inversion_principle-FooとBar(またはそれらのラッパー)の2つの側面の少なくとも1つを持っています2つの具体的なクラスが互いに直接依存するのではなく、反対側が実装する抽象的なインターフェイスに依存します。これにより、観察しているような循環依存性と相互再帰の悪夢が簡単に発生する可能性があります。また、検討する価値のある多対多の関係を実装する別の方法があります(また、適切なインターフェイスを導入することで、依存関係の逆転の影響を受けやすくなる可能性があります)。

于 2009-05-25T01:06:16.637 に答える
1

これは、オブジェクトに他のオブジェクトが含まれている場合に、シリアライゼーションが無限ループを防ぐ方法を思い出させます。各オブジェクトのハッシュ コードをそのバイト配列にマップするため、オブジェクトに別のオブジェクトへの参照が含まれている場合、a) 同じオブジェクトを 2 回シリアル化せず、b) 自身を無限ループにシリアル化しない。

基本的に同じ問題があります。ソリューションは、リスト コレクションの代わりにある種のマップを使用するのと同じくらい簡単です。多対多の場合は、リストのマップを作成するだけです。

于 2009-05-25T04:02:41.217 に答える
1

男、私の答えはそれらのDIのものほどクールではありません。しかし...

簡単に言えば、関連付けを開始する前にラッパーを作成する必要があると思います。FooWrappers を作成して、Foos のリスト全体をトラバースします。次に、Bars をトラバースして BarWrappers を作成します。次に、ソース Foo を読み取り、関連する FooWrapper の MyBarWrappers に適切な BarWrapper 参照を追加します。

Foo インスタンスのラッパーを作成することと、その Bar インスタンスのそれぞれとの関係をすぐに作成することの両方を主張する場合は、作業中の Foo、つまり Foo_1 をマークしてサイクルを「中断」し、各 BarWrapper インスタンスを許可する必要があります。 MyFooWrappers コレクション内にさらに別の FooWrapper_1 インスタンスを作成しないことを知っています。結局のところ、実際には、すでに FooWrapper_1 をコール スタックの上位 (いわば下位) に作成しています。

結論: コードの健全性の問題として、ラッパー コンストラクターはそれ以上ラッパーを作成すべきではありません。せいぜい、Foo と Bar ごとに 1 つの一意のラッパーが別の場所に存在することだけを認識/検出する必要があります。

于 2009-05-25T04:27:52.277 に答える
0

つまり、Foo と Bar がモデルです。Foo は Bar のリストで、Bar は Foo のリストです。私が正しく読んでいれば、お互いのコンテナに過ぎない2つのオブジェクトがあります。A はすべての B の集合であり、B はすべての As の集合ですか? それは本質的に円形ではありませんか?それはまさにその定義による無限再帰です。実際のケースには、より多くの動作が含まれますか? おそらくそれが、人々が解決策を説明するのに苦労している理由です。

私の唯一の考えは、これが本当に意図的である場合、静的クラスを使用するか、静的変数を使用して、クラスが一度だけ作成されたことを記録することです。

于 2009-05-25T03:56:36.740 に答える