如何在 ReactiveUI 6.5 中显示新的模态表单

How to display new modal form in ReactiveUI 6.5

我是目前正在维护使用 WinForms UI 编写的大型应用程序套件的开发人员团队中的一员。

为了提高我们应用程序的可测试性,我们希望转向 MVVM 风格,将 UI 从业务逻辑中分离出来。但是,我们需要继续使用 WinForms UI,以尽量减少对我们的用户的影响,因为他们使用套件中的不同应用程序。

在试用 ReactiveUI 时,我掌握了如何将表单控件和命令绑定到我的视图模型,但找不到关于如何弹出模态表单以请求或请求的文档或示例显示附加信息。例如,这些关于路由的文档页面提到了每个受支持的 UI 框架,但 WinForms 除外:http://docs.reactiveui.net/en/user-guide/routing/index.html, https://github.com/reactiveui/ReactiveUI/blob/docs/docs/basics/routing.md

不幸的是,ReactiveUI "good examples page" 似乎没有任何基于 WinForms 的示例,我可以使用 [=38 找到所有其他 ReactiveUI / WinForms 示例=]只是一个单一的形式。

我绝对希望将 forms/views 排除在视图模型之外以保持可测试性。

我认为正确的方法是由视图中的某些用户操作(例如单击按钮、选择菜单项)触发的 ReactiveCommand,但是:

对于简单的消息和 yes/no 答案,我会查看 Wayne Maurer 的使用示例 UserError。我在 Winform 项目中使用了他的示例。

对于更复杂的事情,我在寻找任何用于路由的 Winforms 示例时遇到了同样的困难。我的 google 搜索最终让我进入了 ReactiveUI.Winforms 的源代码,在那里我发现 Paul 已经有一个用于 Winforms 的 UserControl,它将托管路由的 UserControl 视图。它被称为RoutedControlHost

使用该代码,我破解了一些可以显示模态形式的东西。我敢肯定这不是最好的方法,但它可能会给你一些想法。

RoutedModalHost

using Microsoft.Win32.SafeHandles;
using ReactiveUI;
using System;
using System.ComponentModel;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace ReactiveUI_Test_Routing
{
    public class RoutedModalHost : ReactiveObject, IDisposable
    {
        readonly CompositeDisposable disposables = new CompositeDisposable();

    RoutingState _Router;
    IObservable<string> viewContractObservable;        

    public RoutedModalHost()
    {

        this.ViewContractObservable = Observable.Return(default(string));

        var vmAndContract =
            this.WhenAnyObservable(x => x.Router.CurrentViewModel)
                .CombineLatest(this.WhenAnyObservable(x => x.ViewContractObservable),
                    (vm, contract) => new { ViewModel = vm, Contract = contract });

        Form viewLastAdded = null;
        this.disposables.Add(vmAndContract.Subscribe(x => {

            if (viewLastAdded != null)
            {
                viewLastAdded.Dispose();
            }

            if (x.ViewModel == null)
            {                    
                return;
            }

            IViewLocator viewLocator = this.ViewLocator ?? ReactiveUI.ViewLocator.Current;
            IViewFor view = viewLocator.ResolveView(x.ViewModel, x.Contract);
            view.ViewModel = x.ViewModel;

            viewLastAdded = (Form)view;
            viewLastAdded.ShowDialog();
        }, RxApp.DefaultExceptionHandler.OnNext));
    }

    [Category("ReactiveUI")]
    [Description("The router.")]
    public RoutingState Router
    {
        get { return this._Router; }
        set { this.RaiseAndSetIfChanged(ref this._Router, value); }
    }

    [Browsable(false)]
    public IObservable<string> ViewContractObservable
    {
        get { return this.viewContractObservable; }
        set { this.RaiseAndSetIfChanged(ref this.viewContractObservable, value); }
    }

    [Browsable(false)]
    public IViewLocator ViewLocator { get; set; }

    bool disposed = false;
    SafeHandle handle = new SafeFileHandle(IntPtr.Zero, true);

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposed)
            return;

        if (disposing)
        {
            handle.Dispose();
            // Free any other managed objects here.
            //
            this.disposables.Dispose();
        }

        // Free any unmanaged objects here.
        //
        disposed = true;
    }

}
}

MainViewModel

using ReactiveUI;
using System.Reactive.Linq;
using System;    

namespace ReactiveUI_Test_Routing
{
public class MainViewModel : ReactiveObject, IScreen
{
    public RoutingState Router { get; private set; }
    public ReactiveCommand<object> ShowTestModalForm { get; protected set; }

    public MainViewModel(RoutingState modalRouter)
    {
        Router = modalRouter;

        ShowTestModalForm = ReactiveCommand.Create();
        ShowTestModalForm.Subscribe(x => Router.Navigate.Execute(new TestModalFormViewModel(this)));
    }

}
}

主视图

using System.Windows.Forms;
using Splat;
using ReactiveUI;

namespace ReactiveUI_Test_Routing
{
    public partial class MainView : Form, IViewFor<MainViewModel>
    {
        public MainView()
        {
            InitializeComponent();

        IMutableDependencyResolver dependencyResolver = Locator.CurrentMutable;

        dependencyResolver.Register(() => new TestModalFormView(), typeof(IViewFor<TestModalFormViewModel>));

        RoutingState router = new RoutingState();            

        RoutedModalHost modalHost = new RoutedModalHost();
        modalHost.Router = router;

        this.BindCommand(ViewModel, vm => vm.ShowTestModalForm, v => v.ShowTestModalForm);

        ViewModel = new MainViewModel(router);

    }

    public MainViewModel ViewModel { get; set; }

    object IViewFor.ViewModel
    {
        get { return ViewModel; }
        set { ViewModel = (MainViewModel)value; }            
    }
}
}

TestModalFormViewModel

using ReactiveUI;

namespace ReactiveUI_Test_Routing
{
    public class TestModalFormViewModel : ReactiveObject, IRoutableViewModel
    {
        public IScreen HostScreen { get; protected set; }

    public string UrlPathSegment { get { return "ModalForm"; } }

    public TestModalFormViewModel(IScreen screen)
    {
        HostScreen = screen;
    }
}
}