Вопрос

Такое ощущение, что должно быть какое-то полупростое решение, но я просто не могу его понять.

Редактировать:В предыдущем примере бесконечный цикл был показан более четко, но это дает немного больше контекста.Ознакомьтесь с предварительным редактированием, чтобы получить краткий обзор проблемы.

Следующие 2 класса представляют модели представления модели представления модели представления (МВВМ) шаблон.

/// <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.Вот причина классов-оболочек

Поскольку бесконечный цикл находится в конструкторе и пытается создать новые объекты, я не могу просто установить логический флаг, чтобы предотвратить это, потому что ни один из объектов никогда не завершает создание.

Я думаю о том, чтобы иметь (либо как синглтон, либо передать конструктору, либо и то, и другое) Dictionary<Recipe, RecipeViewModel> и Dictionary<Category, CategoryViewModel> это будет лениво загружать модели представления, но не создавать новую, если она уже существует, но я не удосужился проверить, сработает ли это, так как уже поздно, и я немного устал с этим иметь дело в течение последних 6 часов или около того.

Нет никакой гарантии, что код здесь скомпилируется, поскольку я удалил кучу вещей, не связанных с рассматриваемой проблемой.

Это было полезно?

Решение

Чувак, мой ответ не такой крутой, как у DI.Но...

Проще говоря, я думаю, что вы должны создать свои оболочки, прежде чем начинать их связывать.Просмотрите весь список Foos, создав FooWrappers.Затем пройдитесь по Bars и создайте BarWrappers.Затем прочитайте исходный Foos, добавив соответствующие ссылки BarWrapper на MyBarWrappers в связанном FooWrapper и наоборот для Bars.

Если вы настаиваете как на создании оболочки для экземпляра Foo, так и на немедленном создании связей с каждым из его экземпляров Bar, тогда вы должны «разорвать» цикл, отметив, над каким Foo вы работаете, т.е.Foo_1 и сообщите каждому экземпляру BarWrapper, что НЕ следует создавать еще один экземпляр FooWrapper_1 внутри коллекции MyFooWrappers.В конце концов, вы, по сути, уже создаете FooWrapper_1 выше (или как бы ниже) стека вызовов.

Нижняя граница:В целях здравого смысла кода конструкторы-оболочки не должны создавать больше оболочек.В лучшем случае - он должен только знать/обнаружить, что где-то еще существует одна уникальная оболочка для каждого Foo и Bar, и МОЖЕТ БЫТЬ создать оболочку ТОЛЬКО, если он не находит ее где-либо еще.

Другие советы

Вернемся к исходному вопросу (и коду).Если вам нужно иметь отношение «многие-2-многие», которое автоматически синхронизируется, читайте дальше.Лучшее место для поиска сложного кода, который обрабатывает эти случаи, — это исходный код любой платформы ORM, и это очень распространенная проблема для этой области инструментов.Я бы посмотрел исходный код nHibernate (https://nhibernate.svn.sourceforge.net/svnroot/nhibernate/trunk/nhibernate/), чтобы увидеть, как он реализует коллекции, которые обрабатывают отношения 1-N и M-N.

Вы можете попробовать просто создать свой собственный небольшой класс коллекции, который позаботится об этом.Ниже я удалил исходные классы-оболочки и добавил коллекцию BiList, которая инициализируется объектом (владельцем коллекции) и именем другой стороны свойства для синхронизации (работает только для M-N, но 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);
        }
    }
}

Прежде всего ДИ не решит вашу проблему, но всегда есть одна вещь, связанная с ДИ который решит вашу проблему, это использование Контейнер (или контекст с возможностью поиска)

Решение:

Ваш код не работает в этих местах:

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

Проблема вызвана тем, что вы создаете обертку (xxxViewModel) для объекта, даже если он уже существует.Вместо того, чтобы снова создавать оболочку для того же объекта, вам нужно проверить, существует ли уже оболочка для этой модели, и использовать ее.Итак, вам нужен контейнер для отслеживания всех созданных объектов.Ваши варианты:

Опция 1: используйте простой шаблон в стиле Factory для создания объектов, но также отслеживайте их:

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 не сможет очистить их из памяти.

вариант-1а: скорее всего, в вашем приложении уже есть контроллер (или контекст), к которому представления имеют доступ.В этом случае вместо создания таких а-ля фабрики, я бы просто переместил методы GetOrCreate в этот контекст.В этом случае, когда контекст исчезнет (форма будет закрыта), ссылки на эти словари также будут удалены, и проблема утечки исчезнет.

Я бы рекомендовал вам избавиться от взаимной зависимости, например, с помощью принципа инверсии зависимостей, http://en.wikipedia.org/wiki/Dependency_inversion_principle -- иметь хотя бы одну из двух сторон Foo и Bar (или их оболочек) зависимыми от абстрактного интерфейса, который реализует другая сторона, вместо того, чтобы два конкретных класса напрямую зависели друг от друга, что может легко создавать циклическую зависимость и взаимную зависимость -рекурсивные кошмары, подобные тому, который вы наблюдаете.Кроме того, существуют альтернативные способы реализации отношений «многие-ко-многим», которые, возможно, стоит рассмотреть (и которые, возможно, легче подвергнуть инверсии зависимостей посредством введения подходящих интерфейсов).

Я собираюсь сказать Заводской образец.Таким образом, вы можете построить каждый по очереди, затем добавить их друг к другу, а затем вернуть их все, спрятанные от посторонних глаз фабрикой.

Это напоминает мне, как сериализация предотвращает бесконечные циклы, когда объекты содержат другие объекты.Он сопоставляет хэш-код каждого объекта с его байтовым массивом, поэтому, когда объект содержит ссылку на другой объект, он:а) не сериализует один и тот же объект дважды и б) не сериализует себя в бесконечный цикл.

У вас по сути та же проблема.Решение может быть таким же простым, как использование какой-либо карты вместо коллекции списков.Если вы имеете в виду «многие ко многим», вы просто создаете карту списков.

параметры:

  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 просто вернут коллекции базовой модели...

Структура, которую вы имеете, представляет собой представление «инвертированного индекса» отношения «многие ко многим», что довольно часто встречается для оптимизированного поиска и управления зависимостями.Это сводится к паре отношений один-ко-многим.Посмотрите на пример GamesViewModel в статье MSDN. Обратите внимание, что свойство Games просто

ObservableCollection<Game>

и не

ObservableCollection<GameWrapper>

Итак, Фу и Бар — Модели.Foo — это список Bars, а Bar — это список Foos.Если я правильно понимаю, у вас есть два объекта, которые являются не чем иным, как контейнерами друг друга.A — это множество всех B, а B — это множество всех A?Разве это не круг по самой своей природе?Это бесконечная рекурсия по самому своему определению.Включает ли реальный случай больше поведения?Возможно, именно поэтому людям трудно объяснить решение.

Моя единственная мысль заключается в том, что если это действительно намеренно, то следует использовать статические классы или использовать статическую переменную для записи того, что классы были созданы один и только один раз.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top