Повторная реализация WindowManager с использованием комбинации ModernUI + Caliburn.Micro

Здесь Caliburn.Micro успешно сочетается с ModernUI. Но если мы хотим использовать несколько окон, нам также нужно повторно реализовать WindowManager Caliburn для правильной работы с ModernUI. Как это сделать?

ОБНОВЛЕНИЕ: (дополнительный вопрос о IoC-Container/Dependency Injection)

Хорошо, как я понимаю: здесь я использовал инъекцию конструктора:

public class BuildingsViewModel : Conductor<IScreen>

{
    public BuildingsViewModel(IWindowManager _windowManager)
    {
        windowManager = _windowManager;
    }
}

Поскольку BuildingsViewModel разрешается из контейнера IoC, сам контейнер вводит ModernWindowManager реализацию интерфейса IWindowManager из-за этой строки в методе Configure() Bootstrapper:

container.Singleton<IWindowManager, ModernWindowManager>();

Если я разрешаю экземпляр объекта из контейнера, он внедряет все необходимые зависимости. Как дерево.

1) Итак, теперь мне интересно, как я могу заменить эту строку с помощью инъекции (с интерфейсом)? _windowManager.ShowWindow(new PopupViewModel());

2) Если я хочу, чтобы весь мой проект соответствовал шаблону DI, все экземпляры объектов должны быть введены в ModernWindowViewModel, который сначала разрешается из контейнера?

3) Можно ли использовать SimpleContainer Caliburn для всего проекта или лучше использовать зрелую структуру, такую ​​как Castle Windsor? Следует ли мне избегать смешивания?

ОБНОВЛЕНИЕ 2:

4) Интеграция контейнера IoC в существующее приложение требует сначала создания этого контейнера (например, в методе Main() консольного приложения), а затем все экземпляры объектов должны расти из него с внедренными зависимостями?


person AsValeO    schedule 16.03.2015    source источник
comment
Привет, ValeO, на какой ты платформе, SL/WPF?   -  person Charleh    schedule 17.03.2015
comment
По сути, вы хотите наследовать от WindowManager и переопределить EnsureWindow. Здесь CM создает экземпляр окна и предоставляет его для интерфейса IWindowManager. Исходный код находится здесь: github.com/Caliburn-Micro/Caliburn.Micro/blob/master/src/ — возможно, вам придется немного подправить, поскольку ModernUI отображается первым, поэтому вы можете изменить методы ShowXX, чтобы связать окно. правильно. Вы также можете изменить вложенный класс WindowConductor, так как ModernWindow может иметь другие методы.   -  person Charleh    schedule 17.03.2015
comment
WPF. Хм.. Мой навык слишком низок, чтобы реализовать это. Спасибо, в любом случае.   -  person AsValeO    schedule 17.03.2015
comment
Я могу попробовать - у меня, наверное, в любом случае есть старый проект на моем HD. Сегодня я понижаю версию некоторых SQL-серверов, что требует большого ожидания, поэтому у меня есть немного свободного времени - я отпишусь, если у меня будет минутка, чтобы взглянуть   -  person Charleh    schedule 17.03.2015
comment
Привет, ValeO, у меня это работает до некоторой степени - это действительно зависит от того, как вы хотите, чтобы эти всплывающие окна работали. Поскольку ModernWindow ожидает, что вы загрузите контент через Uri, вам придется либо использовать ModernDialog, либо предоставить контент каким-либо другим способом. Вы просто хотите убедиться, что новое окно имеет правильный стиль, или вы хотите предоставить всплывающее окно, которое позволяет использовать все функции навигации, которые предоставляет ваш основной вид?   -  person Charleh    schedule 17.03.2015
comment
Функции навигации не требуются. Просто хочу получить PopupView.xaml, PopupViewModel.cs, привязанные в волшебном стиле MVVM Caliburn, как и главное окно. И возможность передавать параметры из главного окна во всплывающее окно и обратно (это необязательно).   -  person AsValeO    schedule 17.03.2015


Ответы (1)


Просто создайте свой собственный производный WindowManager и переопределите EnsureWindow:

public class ModernWindowManager : WindowManager
{
    protected override Window EnsureWindow(object rootModel, object view, bool isDialog)
    {
        var window = view as ModernWindow;

        if (window == null)
        {
            window = new ModernWindow();
            window.SetValue(View.IsGeneratedProperty, true);
        }

        return window;
    }
}

Любые представления, которые вы хотите использовать в качестве всплывающих окон, должны основываться на ModernWindow и должны либо использовать LinkGroupCollection, либо вы должны установить свойство ContentSource окна, иначе содержимого не будет.

Возможно, вы могли бы сделать это View-First, но оно работает ViewModel-First, используя описанный выше метод.

например чтобы открыть мой PopupView, я сделал следующее

PopupView.xaml

<mui:ModernWindow x:Class="TestModernUI.ViewModels.PopupView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mui="http://firstfloorsoftware.com/ModernUI"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300" ContentSource="/ViewModels/ChildView.xaml">
 </mui:ModernWindow>

PopupViewModel.cs

public class PopupViewModel : Screen
{
    // Blah
}

Код для всплывающего представления из другой ViewModel:

public void SomeMethod()
{
    _windowManager.ShowWindow(new PopupViewModel()); // Or use injection etc
}

Не забудьте зарегистрировать ModernWindowManager вместо WindowManager в своем контейнере!

например. с помощью SimpleContainer CM

container.Singleton<IWindowManager, ModernWindowManager>();

Очевидно, единственный недостаток, который я вижу в вышеизложенном, заключается в том, что вы не можете поместить контент непосредственно в ModernWindow, поэтому вам нужно иметь два UserControls для каждого всплывающего окна!

Обходным путем может быть изменение EnsureWindow в ModernWindowManager, чтобы он создал UserControl на основе ModernWindow и установил ContentSource в URI представления, которое вы хотите загрузить, это вызовет загрузчик контента и подключит ваш ViewModel. Я обновлю, если у меня будет минутка, чтобы попробовать.

Обновление:

Итак, на данный момент это очень хакерский, но это может быть отправной точкой для чего-то полезного. В основном я создаю URI на основе пространства имен и имени представления.

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

protected override Window EnsureWindow(object rootModel, object view, bool isDialog)
{
    var window = view as ModernWindow;

    if (window == null)
    {
        window = new ModernWindow();

        // Get the namespace of the view control
        var t = view.GetType();

        var ns = t.Namespace;

        // Subtract the project namespace from the start of the full namespace
        ns = ns.Remove(0, 12);

        // Replace the dots with slashes and add the view name and .xaml
        ns = ns.Replace(".", "/") + "/" + t.Name + ".xaml";

        // Set the content source to the Uri you've made
        window.ContentSource = new Uri(ns, UriKind.Relative);
        window.SetValue(View.IsGeneratedProperty, true);
    }

    return window;
}

Мое полное пространство имен для моего представления было TestModernUI.ViewModels.PopupView, а сгенерированный URI был /ViewModels/PopupView.xaml, который затем автоматически загружался и связывался через загрузчик контента.

Обновление 2

К вашему сведению, вот мой метод настройки Bootstrapper:

protected override void Configure()  
{
    container = new SimpleContainer();

    container.Singleton<IWindowManager, ModernWindowManager>();
    container.Singleton<IEventAggregator, EventAggregator>();
    container.PerRequest<ChildViewModel>();
    container.PerRequest<ModernWindowViewModel>();
    container.PerRequest<IShell, ModernWindowViewModel>();
}

Здесь я создаю контейнер и регистрирую несколько типов.

Службы CM, такие как WindowManager и EventAggregator, зарегистрированы для соответствующих интерфейсов и как синглтоны, поэтому во время выполнения будет доступен только 1 экземпляр каждой из них.

Модели представления зарегистрированы как PerRequest, что создает новый экземпляр каждый раз, когда вы запрашиваете его из контейнера — таким образом, вы можете иметь одно и то же всплывающее окно несколько раз без странного поведения!

Эти зависимости внедряются в конструктор любых объектов, разрешенных во время выполнения.

Обновление 3

В ответ на ваши вопросы IoC:

1) So now I wonder how can I replace this line using an injection(with interface)? _windowManager.ShowWindow(new PopupViewModel());

Поскольку ваши модели представления теперь обычно нуждаются в зависимостях, вам нужно каким-то образом внедрить их в экземпляры. Если бы у PopupViewModel было несколько зависимостей, вы могли бы внедрить их в родительский класс, но это каким-то образом связало бы родительскую модель представления с PopupViewModel.

Есть несколько других методов, которые вы можете использовать для получения экземпляра PopupViewModel.

Введите его!

Если вы зарегистрируете PopupViewModel как PerRequest, вы будете получать его новый экземпляр каждый раз, когда запрашиваете его. Если вам нужен только один экземпляр всплывающего окна в вашей модели представления, вы можете просто ввести его:

public class MyViewModel 
{
    private PopupViewModel _popup;
    private IWindowManager _windowManager;

    public MyViewModel(PopupViewModel popup, IWindowManager windowManager) 
    {
        _popup = popup;
        _windowManager = windowManager;
    }

    public void ShowPopup()
    {
        _windowManager.ShowPopup(_popup);
    }    
}

Единственным недостатком является то, что экземпляр будет одним и тем же, если вам нужно использовать его несколько раз в одной и той же модели представления, хотя вы могли бы внедрить несколько экземпляров PopupViewModel, если бы знали, сколько вам нужно одновременно

Используйте некоторую форму внедрения по запросу

Для зависимостей, которые потребуются позже, вы можете использовать инъекцию по требованию, например фабрику.

Я не думаю, что Caliburn или SimpleContainer поддерживают фабрики из коробки, поэтому альтернативой является использование IoC.Get<T>. IoC — это статический класс, который позволяет вам получить доступ к вашему контейнеру DI после создания экземпляра.

 public void ShowPopup()
 {
     var popup = IoC.Get<PopupViewModel>();
     _windowManager.ShowWindow(popup);
 }

Вам нужно убедиться, что вы правильно зарегистрировали контейнер в своем загрузчике и делегировали любые вызовы методов IoC CM контейнеру - IoC.Get<T> вызывает GetInstance и другие методы загрузчика:

Вот пример:

public class AppBootstrapper : BootstrapperBase {
    SimpleContainer container;

    public AppBootstrapper() {
        Initialize();
    }

    protected override void Configure() {
        container = new SimpleContainer();

        container.Singleton<IWindowManager, ModernWindowManager>();
        container.Singleton<IEventAggregator, EventAggregator>();
        container.PerRequest<IShell, ModernWindowViewModel>();

        // Register viewmodels etc here....
    }

    // IoC.Get<T> or IoC.GetInstance(Type type, string key) ....
    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.");
    }

    // IoC.GetAll<T> or IoC.GetAllInstances(Type type) ....
    protected override IEnumerable<object> GetAllInstances(Type service) {
        return container.GetAllInstances(service);
    }

    // IoC.BuildUp(object obj) ....
    protected override void BuildUp(object instance) {
        container.BuildUp(instance);
    }

    protected override void OnStartup(object sender, System.Windows.StartupEventArgs e) {
        DisplayRootViewFor<IShell>();
    }

Castle.Windsor поддерживает фабрики, так что вы можете Resolve и Release своих компонентов и более явно управлять их временем жизни, но я не буду вдаваться в подробности здесь.

2) If I want my whole project match DI pattern, all objects instances must be injected into ModernWindowViewModel, that resolves from container first?

Вам нужно только внедрить зависимости, которые нужны ModernWindowViewModel. Все, что требуется детям, автоматически разрешается и вводится, например:

public class ParentViewModel
{
    private ChildViewModel _child;

    public ParentViewModel(ChildViewModel child)
    {
        _child = child;
    }
}

public class ChildViewModel
{
    private IWindowManager _windowManager;
    private IEventAggregator _eventAggregator;

    public ChildViewModel(IWindowManager windowManager, IEventAggregator eventAggregator) 
    {
        _windowManager = windowManager;
        _eventAggregator = eventAggregator;
    }
}

В приведенной выше ситуации, если вы разрешите ParentViewModel из контейнера, ChildViewModel получит все свои зависимости. Вам не нужно вводить их в родителя.

3) Is it okay to use Caliburn's SimpleContainer for whole project, or better use mature framework like Castle Windsor? Should I avoid mixing?

Вы можете смешивать, но это может привести к путанице, поскольку они не будут работать друг с другом (один контейнер не будет знать о другом). Просто придерживайтесь одного контейнера, и SimpleContainer будет в порядке — Castle Windsor имеет гораздо больше функций, но они могут вам никогда не понадобиться (я использовал только некоторые из расширенных функций)

4) Integrating an IoC container into an existing application requires creating this container first(in Main() method of console app for example), and then all object instanses must grow from it with injected dependencies?

Да, вы создаете контейнер, затем разрешаете корневой компонент (в 99,9% приложений есть один основной компонент, который называется корнем композиции), и затем строится полное дерево.

Вот пример загрузчика для сервисного приложения. Я использую Castle Windsor и хотел иметь возможность размещать движок в службе Windows, в приложении WPF или даже в окне консоли (для тестирования/отладки):

// The bootstrapper sets up the container/engine etc
public class Bootstrapper
{
    // Castle Windsor Container
    private readonly IWindsorContainer _container;

    // Service for writing to logs
    private readonly ILogService _logService;

    // Bootstrap the service
    public Bootstrapper()
    {
        _container = new WindsorContainer();

        // Some Castle Windsor features:

        // Add a subresolver for collections, we want all queues to be resolved generically
        _container.Kernel.Resolver.AddSubResolver(new CollectionResolver(_container.Kernel));

        // Add the typed factory facility and wcf facility
        _container.AddFacility<TypedFactoryFacility>();
        _container.AddFacility<WcfFacility>();

        // Winsor uses Installers for registering components
        // Install the core dependencies
        _container.Install(FromAssembly.This());

        // Windsor supports plugins by looking in directories for assemblies which is a nice feature - I use that here:
        // Install any plugins from the plugins directory
        _container.Install(FromAssembly.InDirectory(new AssemblyFilter("plugins", "*.dll")));

        _logService = _container.Resolve<ILogService>();
    }

    /// <summary>
    /// Gets the engine instance after initialisation or returns null if initialisation failed
    /// </summary>
    /// <returns>The active engine instance</returns>
    public IIntegrationEngine GetEngine()
    {
        try
        {
            return _container.Resolve<IIntegrationEngine>();
        }
        catch (Exception ex)
        {
            _logService.Fatal(new Exception("The engine failed to initialise", ex));
        }

        return null;
    }

    // Get an instance of the container (for debugging)
    public IWindsorContainer GetContainer()
    {
        return _container;
    }
}

Как только загрузчик создан, он настраивает контейнер и регистрирует все службы, а также DLL-плагины. Вызов GetEngine запускает приложение, разрешая Engine из контейнера, который создает полное дерево зависимостей.

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

Код услуги:

public partial class IntegrationService : ServiceBase
{
    private readonly Bootstrapper _bootstrapper;

    private IIntegrationEngine _engine;

    public IntegrationService()
    {
        InitializeComponent();

        _bootstrapper = new Bootstrapper();
    }

    protected override void OnStart(string[] args)
    {
        // Resolve the engine which resolves all dependencies
        _engine = _bootstrapper.GetEngine();

        if (_engine == null)
            Stop();
        else
            _engine.Start();
    }

    protected override void OnStop()
    {
        if (_engine != null)
            _engine.Stop();
    }
}

Консольное приложение:

public class ConsoleAppExample
{
    private readonly Bootstrapper _bootstrapper;

    private IIntegrationEngine _engine;

    public ConsoleAppExample()
    {
        _bootstrapper = new Bootstrapper();

        // Resolve the engine which resolves all dependencies
        _engine = _bootstrapper.GetEngine();
        _engine.Start();
    }
}

Вот часть реализации IIntegrationEngine

public class IntegrationEngine : IIntegrationEngine
{
    private readonly IScheduler _scheduler;
    private readonly ICommsService _commsService;
    private readonly IEngineStateService _engineState;
    private readonly IEnumerable<IEngineComponent> _components;
    private readonly ConfigurationManager _configurationManager;
    private readonly ILogService _logService;

    public IntegrationEngine(ICommsService commsService, IEngineStateService engineState, IEnumerable<IEngineComponent> components,
        ConfigurationManager configurationManager, ILogService logService)
    {
        _commsService = commsService;
        _engineState = engineState;
        _components = components;
        _configurationManager = configurationManager;
        _logService = logService;

        // The comms service needs to be running all the time, so start that up
        commsService.Start();
    }

Все остальные компоненты имеют зависимости, но я не добавляю их в IntegrationEngine — они обрабатываются контейнером.

person Charleh    schedule 18.03.2015
comment
Чарле, объясните мне, как я могу передать _windowManager в ChildViewModel.cs. И откуда эта строка: container.Singleton<IWindowManager, ModernWindowManager>(); Должен ли я объявлять контейнер в загрузчике? Будет здорово, если вы измените код родительской темы, который работает «из коробки». Извините, мои базовые знания о внутренних классах Caliburn слишком чисты. Я постараюсь исправить это. - person AsValeO; 18.03.2015
comment
В идеале вы должны использовать внедрение зависимостей - если вы мало что знаете об этом, прочитайте об этом. Caliburn Micro поставляется с контейнером (SimpleContainer в более новых версиях, таких как 2.02) — вы регистрируете свои зависимости в контейнере, и они автоматически внедряются в конструктор ваших объектов, когда они разрешаются во время выполнения. В Caliburn.Micro.Start есть хороший пример, который вы можете установить с помощью диспетчера пакетов (используйте команду Install-Package Caliburn.Micro.Start) - person Charleh; 18.03.2015
comment
Если вам нужно знать о внедрении зависимостей, я могу дать вам несколько советов, если вы хотите создать комнату. - person Charleh; 18.03.2015
comment
Если вы хотите создать комнату - я не понимаю эту фразу)). В любом случае, Чарле, большое спасибо, ты сделал мой день! Очень подробно и полезно. Я исследую Injection Dependency, Container, WindowManager, Conductor, Screen для себя. Вы дали мне некоторые ориентиры. А потом вернуться к моему проекту. Конечно, ваши указатели будут полезны. - person AsValeO; 18.03.2015
comment
По комнатам — я имею в виду, что вы можете создавать чаты на Stack Overflow. - person Charleh; 18.03.2015
comment
Для этой функции требуется не менее 100 репутации. - person AsValeO; 18.03.2015
comment
chat.stackoverflow.com/rooms/73261/dependency-injection, если вы хотите пообщаться - person Charleh; 18.03.2015
comment
Привет, Чарле! Я успешно перешел на C#! Сейчас пытаемся исследовать Д.И. Я реализовал ваше решение для всплывающих окон, и оно работает хорошо, но теперь меня больше интересует перенос проекта на шаблон DI. Посмотрите на мои вопросы, пожалуйста! - person AsValeO; 22.03.2015
comment
Я опубликовал обновление, дайте мне знать, если у вас есть еще вопросы, возможно, было бы лучше разделить их на отдельный вопрос о SO, так как этот вопрос уже охватывает несколько тем. - person Charleh; 23.03.2015
comment
Вау, классный ответ! Еще раз большое спасибо! Я изучу его подробно. Хорошо, новые вопросы в следующий раз. Я обновил это, чтобы поддерживать связь с вами. - person AsValeO; 23.03.2015