Управление ViewModels в Caliburn.Micro - PullRequest
1 голос
/ 01 мая 2020

Я меняю приложение, которое я разрабатываю, на шаблон MVVM, используя Caliburn.Micro Framework.

Когда я привык к этому, сначала я использовал интерфейс IConductor для навигации по унаследовав Conductor<object> в MainViewModel, а затем перемещаясь по экранам с помощью метода ActivateItem.

Я не использовал контейнер, но вместо этого каждый раз создавал новую модель ViewModel.

Например, чтобы перейти к FirstViewModel, я использовал ActivateItem(new FirstViewModel());

ViewModelels - легкие ресурсы, поэтому эта реализация не была заметна. Тем не менее, я обнаружил, что экземпляр ViewModel не был утилизирован, и я начал использовать таймеры, чтобы проверить, работает ли экземпляр, выполняя его в фоновом режиме.

С тех пор я пробую все виды реализаций для управления управлением ViewModels. Я хочу решить, буду ли я ссылаться на уже созданную модель ViewModel или создать новую. Кроме того, я хочу решить, буду ли я использовать ViewModel или оставить его работающим, чтобы позже подключиться к нему.

Итак, читая документацию, я реализовал SimpleContainer в BootStrapperBase

public class Bootstrapper : BootstrapperBase
    {
        private SimpleContainer _container = new SimpleContainer();
        public Bootstrapper()
        {
            Initialize();
        }

        protected override void Configure()
        {
            _container.Instance(_container);
            _container
                .Singleton<IWindowManager, WindowManager>()
                .Singleton<IEventAggregator, EventAggregator>();

            GetType().Assembly.GetTypes()
                .Where(type => type.IsClass)
                .Where(type => type.Name.EndsWith("ViewModel"))
                .ToList()
                .ForEach(viewModelType => _container.RegisterPerRequest(viewModelType, viewModelType.ToString(), viewModelType));

        }
        protected override object GetInstance(Type service, string key)
        {
            var instance = _container.GetInstance(service, key);
            if (instance != null)
                return instance;
            throw new InvalidOperationException("Could not locate any instances.");
        }

        protected override IEnumerable<object> GetAllInstances(Type service)
        {
            return _container.GetAllInstances(service);
        }
        protected override void BuildUp(object instance)
        {
            _container.BuildUp(instance);
        }

        protected override void OnStartup(object sender, StartupEventArgs e)
        {

            DisplayRootViewFor<ShellViewModel>();   

        }
    }

Я думал, что IoC.Get<FirstViewModel>() создаст экземпляр новой ViewModel или повторно использует открытую, если она уже была создана. Однако каждый раз он создает новую модель ViewModel.

Кроме того, я не могу понять, как утилизировать ViewModel при активации другой модели. Например, я поместил OnDeactivate в FirstViewModel, который срабатывает при переключении на другую ViewModel, но я не знаю, какой код я должен поместить туда, чтобы избавиться от этого экземпляра. Я пробовал эту программу установки, реализуя интерфейс IDisposable, но я получаю исключение System.StackOverflowException.

protected override void OnDeactivate(bool close)
        {

            Dispose();
            Console.WriteLine("deactivated");
        }
public void Dispose()
        {
            base.TryClose();
        }

Разве SimpleContainer из Caliburn.Micro не достаточно для управления моделями представления или мне следует изучить другой подход?

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

Читая документацию, я натолкнулся на концепцию Lifecycle, которая Я думаю, что это концепция, которая справилась бы с моими проблемами, но я не нашел дальнейшего объяснения.

Документация по Caliburn.Micro не дает много примеров, и мне трудно понять, как это использовать. рамки правильно без примеров.

Ответы [ 2 ]

0 голосов
/ 02 мая 2020

Вы были правы, глядя на IConductor, это то, что Caliburn ожидает от нас для управления жизненным циклом компонентов. Для полноты изложения существуют также методы расширения ActivateWith, DeactivateWith и ConductWith, которые связывают жизненные циклы Screen без вмешательства Conductor, но я склонен избегать их. Хотя я мог бы использовать их в сценарии модульного тестирования exoti c.

Как упоминалось в документации, деактивация может иметь несколько значений. Давайте использовать TabControl в качестве примера в сочетании с Conductor<IScreen>.Collection.OneActive.

  • Мы могли бы переключаться с одной вкладки на другую. Мы не хотим закрывать вкладку, с которой мы начали, мы просто хотим ее деактивировать.
  • Мы могли бы закрыть текущую вкладку, переключившись на (активировав) ту, которая была в предыдущем индексе (по умолчанию Caliburn).

Благодаря этой гибкости, ie множеству возможностей, Caliburn не навязывает вам ни одно из этих действий. Конечно, это означает, что вы должны сделать соответствующие вызовы самостоятельно.

Первый случай прост, назначение нового ActiveItem автоматически деактивирует предыдущий.

Второй случай требует от вас закройте вкладку явно. Это, однако, заставит Caliburn назначить новый ActiveItem. Вы можете использовать стратегию по умолчанию, или реализовать свою собственную, или вы можете убедиться, что элемент больше не является активным после его закрытия. В этом случае Caliburn не нужно искать в другом месте.

Примечательные методы расширения в этом контексте определены в ScreenExtensions.cs .

Самый простой способ закрыть Элемент await conductor.TryCloseAsync(item) с дополнительным CancellationToken. Этот метод просто перенаправляет на conductor.DeactivateItemAsync(item, true, CancellationToken.None);.

. В случае Conductor<IScreen>.Collection.OneActive реализация дается далее.

/// <summary>
/// Deactivates the specified item.
/// </summary>
/// <param name="item">The item to close.</param>
/// <param name="close">Indicates whether or not to close the item after deactivating it.</param>
/// <param name="cancellationToken">The cancellation token to cancel operation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public override async Task DeactivateItemAsync(T item, bool close, CancellationToken cancellationToken = default)
{
    if (item == null)
        return;

    if (!close)
        await ScreenExtensions.TryDeactivateAsync(item, false, cancellationToken);
    else
    {
        var closeResult = await CloseStrategy.ExecuteAsync(new[] { item }, CancellationToken.None);

        if (closeResult.CloseCanOccur)
            await CloseItemCoreAsync(item, cancellationToken);
    }
}

Это все довольно понятно ты знаешь где искать. Флаг close - это разница между деактивацией и закрытием элемента. CloseStrategy - это способ Калибурна включить постепенное отключение, например «Вы уверены, что хотите закрыть элемент?» . CloseItemCoreAsync реализовано следующим в исходном файле, не стесняйтесь иметь look . ScreenExtensions.TryDeactivateAsync, используемый в любой из ветвей, в конечном итоге будет перенаправлен на DeactivateAsync на самом экране , который отвечает за очистку.

Вернемся к вашему варианту использования, как вы указали при навигации от одного элемента к другому, с возможностью возврата к существующему экземпляру в памяти, я советую вам использовать Conductor<IScreen>.Collection.OneActive. Затем вы можете запросить его коллекцию Items, чтобы выяснить, существует ли определенный экземпляр, чтобы активировать его или создать новый.

Подводя итог, лучше всего активировать и деактивировать через проводники.

Если вам нужна явная утилизация, вы можете изменить свой образец на приведенный ниже.

protected override void OnDeactivate(bool close)
{
    if (close) 
    {
        Dispose();
    }
}

public void Dispose()
{
    Console.WriteLine("disposed");       
}

Однако звонить по номеру base.TryClose(); в Dispose не нужно, и это приведет к бесконечному oop между OnDeactivate и TryClose. Шаблон Dispose необходим только для очистки неуправляемых ресурсов, таких как дескрипторы файлов, ref MSDN .


Обновление

Использование Conductor.Collection.OneActive не закрывает ViewModel, но затем, когда я использую ActivateItem (Io C .Get ());, ViewModel создается снова, потому что я вижу, как он снова запускает конструктор. Я что-то упускаю.

Лично я являюсь сильным сторонником пропасти успеха , я всегда нахожу это несколько разочаровывающим, когда хорошо спроектированная структура, такая как Caliburn, выставляет Stati c Сервисный локатор. Когда мы застреваем, мы легко склоняемся к темной стороне.

Как уже упоминалось:

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

Чтобы узнать , если определенный экземпляр уже существует , нам нужен способ его идентификации. Он может быть основан на типе, но для простоты давайте используем свойство int Id. Скажем, все (или некоторые) из моделей представлений в коллекции Items украшены интерфейсом IHasEntity (который предоставляет опору Id), и мы ищем Id == 3.

Все Вы должны сделать из контекста проводника что-то в следующих строках:

var match = Items.OfType<IHasEntity>().FirstOrDefault(vm => vm.Id == 3);
if (match != null) // Activate it
{
    ActiveItem = match;
}
else // Create a new instance
{
    var entity = await _repo.GetId(3);
    ActiveItem = new MyViewModel(entity);
}

Заключительная мысль: если бы все ваши модели представлений реализовали общую абстракцию IHasEntity, вы могли бы определить свой проводник как Conductor<IHasEntity>.Collection.OneActive и .OfType<IHasEntity>() фильтр больше не нужен.

0 голосов
/ 01 мая 2020

RegisterSingleton в SimpleContainer выполнит эту работу ...

Так что, если вы хотите создать экземпляр по своему выбору, вы можете использовать помощник, который проверяет конструкторы типа с их параметрами по умолчанию: (некоторые знания для отражения) после вас можно адаптировать код ..

, но если вы находите его слишком сложным, сначала посмотрите Activator.Createinstance .

public static class HelperConstructor
{
  public static T MyCreateInstance<T>()
    where T : class
  {
    return (T) MyCreateInstance(typeof (T));
  }

  public static object MyCreateInstance(Type type)
  {
    var ctor = type
        .GetConstructors()
        .FirstOrDefault(c => c.GetParameters().Length > 0);

    return ctor != null
        ? ctor.Invoke
            (ctor.GetParameters()
                .Select(p =>
                    p.HasDefaultValue? p.DefaultValue :
                    p.ParameterType.IsValueType && Nullable.GetUnderlyingType(p.ParameterType) == null
                        ? Activator.CreateInstance(p.ParameterType)
                        : null
                ).ToArray()
            )
        : Activator.CreateInstance(type);
  }
}

. Вы используете этот помощник, задав тип:

var instanceviewModel = HelperConstructor.MyCreateInstance (classType);

позже при калибровке автоматически создается экземпляр представления ...

...