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

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

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

我在想的是(作为单例或传递给构造函数或两者兼而有之)a Dictionary<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 回答
2

首先,DI不会解决您的问题,但始终与DI相关的要解决您的问题的一件事是使用容器(或具有查找功能的上下文)

解决方案:

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

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

问题是由于您为对象创建了包装器 (xxxViewModel),即使它已经存在。您无需再次为同一对象创建包装器,而是需要检查此模型的包装器是否已存在并使用它。所以你需要一个容器来跟踪所有创建的对象。您的选择是:

选项 1:使用简单的 a-la 工厂模式来创建您的对象,但也要跟踪它们:

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

当心:可能的内存泄漏?

您应该注意的一件事(这也是您不应该使用虚拟 a-la 工厂实现的原因)是这些创建者对象将保留对模型对象及其视图包装器的引用这一事实。因此 GC 将无法从内存中清除它们。

option-1a:很可能您的应用程序中已经有一个控制器(或上下文),视图可以访问它。在这种情况下,我不会创建那些a-la factory,而是将 GetOrCreate 方法移至此上下文。在这种情况下,当上下文消失(表单关闭)时,这些字典也将被取消引用,泄漏问题也消失了。

于 2009-05-28T21:27:10.003 回答
1

选项:

  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>
于 2009-05-25T01:05:10.867 回答
1

我建议您摆脱相互依赖,例如通过依赖倒置原则,http ://en.wikipedia.org/wiki/Dependency_inversion_principle - 至少具有 Foo 和 Bar 两侧之一(或它们的包装器)依赖于对方实现的抽象接口,而不是让两个具体类直接相互依赖,这很容易产生循环依赖和相互递归的噩梦,就像你正在观察的那样。此外,还有一些替代方法可以实现值得考虑的多对多关系(并且通过引入合适的接口可能更容易受到依赖反转的影响)。

于 2009-05-25T01:06:16.637 回答
1

我要说的是工厂模式。这样,您可以依次构建每个,然后将它们相互添加,然后将它们全部归还,以防止工厂窥探。

于 2009-05-25T02:11:16.643 回答
1

这让我想起了当对象包含其他对象时,序列化防止无限循环的方式。它将每个对象的哈希码映射到其字节数组,因此当一个对象包含对另一个对象的引用时:a) 不会将同一个对象序列化两次,并且 b) 不会将自身序列化为无限循环。

你基本上有同样的问题。解决方案可能就像使用某种地图而不是列表集合一样简单。如果您得到的是多对多,那么您只需创建一个列表映射。

于 2009-05-25T04:02:41.217 回答
1

伙计,我的回答不像那些 DI 的那么酷。但...

简单来说,我认为您必须在开始关联它们之前创建包装器。遍历整个 Foos 列表,创建 FooWrappers。然后遍历 Bars 并创建 BarWrappers。然后读取源 Foos,将适当的 BarWrapper 引用添加到关联的 FooWrapper 中的 MyBarWrapper,反之亦然。

如果你坚持为 Foo 实例创建一个包装器并立即创建与它的每个 Bar 实例的关系,那么你必须通过标记你正在处理的 Foo 来“打破”循环,即 Foo_1,并让每个 BarWrapper 实例知道不要在它的 MyFooWrappers 集合中创建另一个 FooWrapper_1 实例。毕竟,事实上,您已经在调用堆栈的上层(或下层)创建了 FooWrapper_1。

底线:作为代码健全的问题,包装器构造函数不应该创建更多的包装器。最多 - 它应该只知道/发现每个 Foo 和 Bar 在其他地方存在一个唯一的包装器,并且只有在其他地方找不到它时才可能创建包装器。

于 2009-05-25T04:27:52.277 回答
0

所以,Foo 和 Bar 是模型。Foo 是 Bars 的列表,Bar 是 Foos 的列表。如果我没看错,你有两个对象,它们只是彼此的容器。A 是所有 B 的集合,B 是所有 As 的集合?这不是循环的本质吗?根据其定义,它是无限递归。现实世界的案例是否包含更多行为?也许这就是人们难以解释解决方案的原因。

我唯一的想法是,如果这真的是故意的,那么使用静态类或使用静态变量来记录类已经创建了一次且仅一次。

于 2009-05-25T03:56:36.740 回答