使用 ModernUI + Caliburn.Micro 组合重新实现 WindowManager

Re-implementing WindowManager using ModernUI + Caliburn.Micro combination

Here Caliburn.Micro 与 ModernUI 成功结合。 但是如果我们想使用多个 windows 我们还需要重新实现 Caliburn 的 WindowManager 才能与 ModernUI 正常工作。怎么做到的?

更新: (关于 IoC-Container/Dependency 注入的附加问题)

好的,我明白了:我在这里使用了构造函数注入:

public class BuildingsViewModel : Conductor<IScreen>

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

BuildingsViewModel 从 IoC 容器解析而言, 由于 BootstrapperConfigure() 方法中的这一行,容器本身注入了 ModernWindowManager 接口的 IWindowManager 实现:

container.Singleton<IWindowManager, ModernWindowManager>();

如果我从容器解析对象实例,它会注入所有需要的依赖项。像一棵树。

1) 所以现在我想知道如何使用注入(带接口)替换这一行? _windowManager.ShowWindow(new PopupViewModel());

2) 如果我希望我的整个项目匹配 DI 模式,所有对象实例必须注入到 ModernWindowViewModel,首先从容器解析?

3) 整个项目用Caliburn的SimpleContainer好吗,还是用Castle Windsor这样成熟的框架更好?我应该避免混合吗?

更新2:

4) 将 IoC 容器集成到现有应用程序中需要先创建此容器(例如,在控制台应用程序的 Main() 方法中),然后所有对象实例都必须从它增长并注入依赖项?

只需创建您自己的派生 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 属性 window,否则没有内容

您可以制作此 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

例如使用 CM 的 SimpleContainer

container.Singleton<IWindowManager, ModernWindowManager>();

显然,我能看到的唯一缺点是您似乎无法将内容直接放在 ModernWindow 中,因此每个弹出窗口都必须有两个 UserControls

解决方法是更改​​ ModernWindowManager 中的 EnsureWindow,以便它基于 ModernWindow 创建一个 UserControl,并将 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>();
}

这里我创建容器,注册一些类型。

WindowManagerEventAggregator 等 CM 服务都针对各自的接口注册并作为单例,因此在 运行 时每个服务只有 1 个实例可用。

视图模型被注册为 PerRequest,每次您从容器请求一个实例时都会创建一个新实例 - 这样您就可以多次使用相同的 window 弹出窗口而不会出现奇怪的行为!

这些依赖项被注入到 运行 时解析的任何 objects 的构造函数中。

更新 3

回答您的 IoC 问题:

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

由于您的视图模型现在通常需要依赖项,因此您需要通过某种方式将它们注入到实例中。如果 PopupViewModel 有多个依赖项,您可以将它们注入 parent class 但这会以某种方式将 parent 视图模型耦合到 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 的多个实例,如果您知道需要多少个同时

使用某种形式的 on-demand 注入

对于稍后需要的依赖项,您可以使用 on-demand 注入,例如工厂

我认为 Caliburn 或 SimpleContainer 不支持开箱即用的工厂,因此替代方法是使用 IoC.Get<T>IoC 是一个静态 class 允许您在实例化后访问您的 DI 容器

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

您需要确保已在引导程序中正确注册容器并将对 CM 的 IoC 方法的任何调用委托给容器 - 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 支持工厂,因此您可以 ResolveRelease 您的组件并更明确地管理它们的生命周期,但我不会在这里讨论

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

你只需要注入ModernWindowViewModel需要的依赖。 children 所需的任何内容都会自动解析并注入,例如:

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 将获得所有依赖项。您不需要将它们注入 parent.

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 很好 - 温莎城堡有更多的功能,但你可能永远不需要它们(我只使用了一些高级功能)

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 应用程序中甚至在控制台 Window(对于 testing/debug)中托管引擎:

// 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 - 它们由容器处理