感觉必须有一些半简单的解决方案,但我就是想不出来。

编辑:前面的示例更清楚地显示了无限循环,但这提供了更多背景信息。查看预编辑以快速了解问题。

以下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();
    }
}

除非有要求,否则我不会费心显示模型(菜谱和类别),但它们基本上负责业务逻辑(例如,向类别添加菜谱也会添加链接的另一端,即如果一个类别包含一个菜谱,那么该菜谱也包含在该类别中)并且基本上决定了事情的进展。ViewModel 为 WPF 数据绑定提供了一个很好的接口。这就是包装类的原因

由于无限循环位于构造函数中并且它正在尝试创建新对象,因此我不能仅设置布尔标志来防止这种情况,因为这两个对象都没有完成构造。

我的想法是(作为单例或传递给构造函数或两者兼而有之) Dictionary<Recipe, RecipeViewModel>Dictionary<Category, CategoryViewModel> 这将延迟加载视图模型,但如果已经存在,则不会创建一个新的视图模型,但我还没有抽出时间来尝试看看它是否能工作,因为已经很晚了,我有点厌倦了处理这个问题过去 6 个小时左右。

不能保证这里的代码能够编译,因为我取出了一堆与当前问题无关的东西。

有帮助吗?

解决方案

男人,我的答案是不如那些DI作为清凉。但...

在简单地说,我觉得你开始与他们之前,您必须创建包装。遍历FOOS的完整清单,创建FooWrappers。然后横杆,创造BarWrappers。然后读取源FOOS,在相关联的FooWrapper增加了MyBarWrappers适当BarWrapper参考文献,以及用于酒吧反之亦然。

如果你坚持既创造了一个Foo实例的包装,并立即建立关系,以每次酒吧实例,则必须“破发”的标记周期,这的Foo您正在使用的,即FOO_1,并让每个在BarWrapper情况下,不知道创造另一个FooWrapper_1实例内是MyFooWrappers集合。毕竟,你是,事实上,已经创造了FooWrapper_1上涨(或进一步下跌,因为它是)调用堆栈。

底线:作为代码理智的问题,包装构造不应被创造更多的包装。在很大部分 - 它应该只知道/发现一个独特的包装存在别的地方各Foo和酒吧,也许创建包装,只有当它没有在别的地方找到

其他提示

回到你原来的问题(和代码)。如果你想要的是有许多-2-许多关系是自动同步,然后阅读。 找一个复杂的代码,处理这些情况的最佳位置是任何ORM框架的源代码,它是工具,这个领域非常普遍的问题。我想看看NHibernate的源代码( HTTPS://nhibernate.svn .sourceforge.net / svnroot / NHibernate的/中继/ 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);
        }
    }
}

所有的 DI 的是不会解决你的问题,但有一点始终涉及到的 DI 的这是会解决你的问题首先是一个使用的容器(或者与查找能力的上下文)

<强>解决方案:

您的代码在这些地方失败:

var catVM = new CategoryViewModel(cat); //Causes infinite loop
...
var recipeVM = new RecipeViewModel(item); //Causes infinite loop

问题是由所创建的某个对象的包装物(xxxViewModel),即使它已经存在这样的事实引起的。相反,再次创造了同一个对象的包装,你需要检查,如果该模型的包装已经存在,并且用它来代替。所以,你需要一个容器来跟踪所有创建的对象。您的选项是:

选项1:使用简单的一拉工厂模式来创建对象,但也跟踪它们:

class CategoryViewModelFactory
{
    // TODO: choose your own GOOD implementation - the way here is for code brevity only
    // Or add the logic to some other existing container
    private static IDictionary<Category, CategoryViewModel>  items = new Dictionary<Category, CategoryViewModel>();
    public static CategoryViewModel GetOrCreate(Category cat)
    {
        if (!items.ContainsKey(cat))
            items[cat] = new CategoryViewModel(cat);
        return items[cat];
    }
}

然后,你做方侧的同样的事情,问题代码是固定的:

  // OLD: Causes infinite loop
  //var catVM = new CategoryViewModel(cat);
  // NEW: Works 
  var catVM = CategoryViewModelFactory.GetOrCreate(cat);

<强>小心:可能的内存泄漏

有一件事你应该知道的(这也是为什么你不应该使用的假一拉工厂的实施)的事实是,这些的创作者的对象将继续引用这两个模型对象及其查看包装。因此,GC将无法从存储器清除它们。

选项-1A:最有可能你有你的应用程序已经是一个控制器(或上下文),对此观点有机会获得。在这种情况下,而不是创造者的一拉工厂的,我也只是移动GetOrCreate方法来此背景下。在这种情况下当所述上下文被去(窗体关闭),也这些字典将被解引用和泄漏问题也没有了。

我会建议你通过依赖倒置原则摆脱相互依存的,例如, HTTP: //en.wikipedia.org/wiki/Dependency_inversion_principle - 具有双方Foo和Bar(或它们的包装)中的至少一个依赖于一个抽象的接口在其上的另一侧工具,而不是具有两个具体类直接依赖于对方,这很容易产生循环依赖性和相互递归恶梦像你观察一个。而且,存在实现许多一对多关系,这可能是值得考虑的(并且可以通过引入合适的接口更容易受到依赖的反转)。

的替代方式

我要说工厂模式。这样,你可以依次构建每个,然后将它们彼此相加,然后返回他们所有隐藏的由工厂窥视。

此提醒我的序列化的方式可以防止无限循环时对象包含其他对象。它映射每个对象到其字节数组的哈希码,因此,在对象包含到另一个对象的引用:a)不序列相同的对象两次,和b)不本身序列化为一个无限循环

您有基本相同的问题。该解决方案可就像使用某种形式的地图,而不是名单收集的那样简单。如果有什么你在说是多到多,那么你只需要创建一个映射列表中。

选项:

  1. 实施成员资格测试,例如添加之前检查 bar-is-member-of-foo
  2. 将多对多关系移至其自己的类中

我认为后者是首选 - 它更相关

当然,对于 foo-bar 的例子,我们真的不知道目标是什么,所以你的里程可能会有所不同

编辑:给定原始问题中的代码,#1 将不起作用,因为无限递归发生在将任何内容添加到任何列表之前。

这种方法/问题存在几个问题,可能是因为它已经被抽象到近乎愚蠢的地步 - 适合说明编码问题,但不太适合解释最初的意图/目标:

  1. 包装类实际上并不包装任何东西或添加任何有用的行为;这让人很难理解为什么需要它们
  2. 使用给定的结构,您无法在构造函数中初始化列表 根本不 因为每个包装器列表立即创建另一个包装器列表的新实例
  3. 即使您将初始化与构造分开,您仍然具有隐藏成员资格的循环依赖关系(即包装器相互引用,但在包含检查中隐藏 foo/bar 元素;这并不重要,因为代码永远不会向任何列表添加任何内容!)
  4. 直接关系方法可行,但需要搜索机制并假设包装器将根据需要而不是提前创建,例如具有搜索功能的数组或一对字典(例如Dictionary>, Dictionary>) 适用于映射,但可能不适合您的对象模型

结论

我不认为结构 如上所述 将工作。DI 不行,工厂不行,根本不行——因为包装器在隐藏子列表时互相引用。

这种结构暗示了未阐明的错误假设,但在没有上下文的情况下,我们无法找出它们可能是什么。

请在原始上下文中使用现实世界的对象和期望的目标/意图重述问题。

或者至少说明您认为示例代码的结构 应该 生产。;-)

附录

感谢您的澄清,这使情况更容易理解。

我没有使用过 WPF 数据绑定 - 但我浏览过 这篇 MSDN 文章 - 因此以下内容可能有帮助和/或正确,也可能没有帮助:

  • 我认为视图模型类中的类别和食谱集合是多余的
    • 您已经在基础类别对象中拥有 M:M 信息,那么为什么要在视图模型中复制它
    • 看起来你的集合更改处理程序也会导致无限递归
    • 集合更改的处理程序似乎不会更新包装的配方/类别的基础 M:M 信息
  • 我认为视图模型的目的是公开底层模型数据,而不是单独包装其每个组件。
    • 这似乎是多余的并且违反了封装
    • 这也是无限递归问题的根源
    • 天真地,我希望 ObservableCollection 属性仅返回底层模型的集合......

您拥有的结构是多对多关系的“倒排索引”表示,这对于优化查找和依赖关系管理来说很常见。它简化为一对一对多的关系。查看 MSDN 文章中的 GamesViewModel 示例 - 请注意,Games 属性只是

ObservableCollection<Game>

并不是

ObservableCollection<GameWrapper>

所以,Foo和Bar是模型。 foo是酒吧的列表,酒吧是FOOS的列表。如果我读取正确你有两个对象这不过是对方的容器。 A是集所有烧烤和B是集合所有作为?那是一个循环就其本质?这是由于其本身的定义无限递归。是否真实世界的案例包括更多的行为?也许这就是为什么人们有困难的时候解释的解决方案。

我唯一的想法是,如果这是真正的目的,然后使用静态类或使用一个静态变量来记录类已经创建一次,只有一次。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top