在Structure Map中解析窗口或如何在WPF MVVM中管理多个窗口?

12 wpf dependency-injection window inversion-of-control mvvm

我一直在阅读Mark Seeman关于.NET中依赖注入的书,我很难在WPF应用程序中配置组合根.

我的容器将在应用程序启动方法中注册:

protected override void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);

    var container = new Container();
    container.Configure(r =>
                        {
                            r.For<IAccountServices>().Use<AccountServicesProxy>();
                            r.For<MainWindow>().Use<MainWindow>();
                        });
}
Run Code Online (Sandbox Code Playgroud)

这有意义,因为应用程序启动代表我的组合根.

我的应用程序中的WPF窗口基于视图模型.视图模型使用构造函数注入.例如,我可以通过注入实现来构成视图模型IAccountServices.

在创建主窗口时,我可以在OnStartup方法中执行以下操作:

var mainWindow = container.GetInstance<MainWindow>();
mainWindow.Show();
Run Code Online (Sandbox Code Playgroud)

一旦我进入主窗口,我可能想要打开另一个窗口.到目前为止,我已经能够提出一种方法,即创建一个窗口工厂并请求窗口工厂解析窗口的实例.我必须确保窗口​​工厂在每个可能需要打开新窗口的视图模型中都可用.在我看来,这与在我的应用程序中传递IoC容器一样糟糕(服务定位器反模式会浮现在脑海中).

这种方法对你来说是否正确?我的直觉告诉我这是错的,但我还没有想出一个更好的方法来实现这个目标.

Ana*_*aev 12

我认为在实现行为模式(例如a Mediator等)之前,需要决定一个通用模式以便于应用程序结构.为此目的,即为创建独立窗口,非常适合的Abstract factory模式.

可以ViewModel使用诸如的方法在侧面实现窗口的创建IDialogService.但我认为这个任务应该在侧面实现View,因为Window对象指的是View而不是ViewModel.因此,您必须创建MVVM样式架构,它允许使用设计模式创建独立窗口.

我创建了一个项目,在该项目中使用附加的行为Abstract factory创建一个Window View.Abstract factory还实现了Singleton模式以创建全局访问点并确保新构造的对象的唯一性.附加行为隐式实现了模式Decorator,它是在XAML一侧使用的抽象工厂的包装器.对于a Abstract factory不引用位于其中的对象ViewModel使用代理模式,该模式是具有DataTemplate而没有DataType的ContentControl.还使用Command模式在对象之间进行独立操作.因此,该项目使用以下模式:

  • 抽象工厂
  • 独生子
  • 装饰
  • 代理
  • 命令

项目结构如下所示:

在此输入图像描述

在附加的行为中附加了依赖属性Name,该属性在新窗口的名称中传输.对于他注册PropertyChangedEvent,这是一个调用Make方法的抽象工厂:

private static void IsFactoryStart(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
    var window = sender as Window;

    if (window == null)
    {
        return;
    }

    if (e.NewValue is String && String.IsNullOrEmpty((string)e.NewValue) == false)
    {
        _typeWindow = (string)e.NewValue;

        if (_typeWindow != null)
        {
            var newWindow = WindowFactory.Instance.Make(_typeWindow);
            newWindow.Show();
        }        
    }
}
Run Code Online (Sandbox Code Playgroud)

WindowFactory 与Singleton模式一起看起来像这样:

public class WindowFactory : IWindowFactory
{
    #region WindowFactory Singleton Instance

    private static WindowFactory _instance = null;
    private static readonly object padlock = new object();

    public static WindowFactory Instance
    {
        get
        {
            lock (padlock)
            {
                if (_instance == null)
                {
                    _instance = new WindowFactory();
                }

                return _instance;
            }
        }
    }

    #endregion

    public Window Make(string TypeWindow)
    {
        if (TypeWindow.Equals("WindowOneViewProxy"))
        {
            var windowOne = new Window();                

            windowOne.Width = 450;
            windowOne.Height = 250;
            windowOne.WindowStartupLocation = WindowStartupLocation.CenterScreen;
            windowOne.Title = TypeWindow;
            windowOne.ContentTemplate = Application.Current.Resources[TypeWindow] as DataTemplate;

            return windowOne;
        }
        else if (TypeWindow.Equals("WindowTwoViewProxy"))
        {
            var windowTwo = new Window();
            windowTwo.Width = 500;
            windowTwo.Height = 200;
            windowTwo.WindowStartupLocation = WindowStartupLocation.CenterScreen;
            windowTwo.Title = TypeWindow;
            windowTwo.ContentTemplate = Application.Current.Resources[TypeWindow] as DataTemplate;

            return windowTwo;
        }
        else if (TypeWindow.Equals("WindowThreeViewProxy")) 
        {
            var windowThree = new Window();
            windowThree.Width = 400;
            windowThree.Height = 140;
            windowThree.WindowStartupLocation = WindowStartupLocation.CenterScreen;
            windowThree.Title = TypeWindow;
            windowThree.ContentTemplate = Application.Current.Resources[TypeWindow] as DataTemplate;

            return windowThree;
        }
        else
            throw new Exception("Factory can not create a: {0}" + TypeWindow);
    }
}
Run Code Online (Sandbox Code Playgroud)

对于属性Window.ContentTemplate设置DataTemplate来自资源.ContentTemplate负责可视化表示,为了从ViewModel绑定属性,需要将对象设置为Content.但在这种情况下,将Abstract factory引用ViewModel,并避免它们并使用代理模式,如下所示:

WindowOneProxyView

<DataTemplate x:Key="WindowOneViewProxy">
    <ContentControl ContentTemplate="{StaticResource WindowOneViewRealObject}">
        <ViewModels:WindowOneViewModel />
    </ContentControl>
</DataTemplate>
Run Code Online (Sandbox Code Playgroud)

WindowOneViewRealObject

<DataTemplate x:Key="WindowOneViewRealObject" DataType="{x:Type ViewModels:WindowOneViewModel}">
    <Grid>
        <Label Content="{Binding Path=WindowOneModel.TextContent}" 
               HorizontalAlignment="Center"
               VerticalAlignment="Top"
               HorizontalContentAlignment="Center"
               VerticalContentAlignment="Center"
               Background="Beige" />

        <Button Content="One command" 
                Width="100"
                Height="30"
                HorizontalAlignment="Center"
                Command="{Binding OneCommand}" />
    </Grid>
</DataTemplate>
Run Code Online (Sandbox Code Playgroud)

DataTemplate代理中没有指定DataType,但它是在真实对象中.

MainViewModel命令中只需设置窗口名称,它将为附加行为提供输入:

MainModel

public class MainModel : NotificationObject
{
    #region TypeName

    private string _typeName = null;

    public string TypeName
    {
        get
        {
            return _typeName;
        }

        set
        {
            _typeName = value;
            NotifyPropertyChanged("TypeName");
        }
    }

    #endregion
}
Run Code Online (Sandbox Code Playgroud)

MainViewModel

public class MainViewModel
{
    #region MainModel

    private MainModel _mainModel = null;

    public MainModel MainModel
    {
        get
        {
            return _mainModel;
        }

        set
        {
            _mainModel = value;
        }
    }

    #endregion

    #region ShowWindowOneCommand

    private ICommand _showWindowOneCommand = null;

    public ICommand ShowWindowOneCommand
    {
        get
        {
            if (_showWindowOneCommand == null)
            {
                _showWindowOneCommand = new RelayCommand(param => this.ShowWindowOne(), null);
            }

            return _showWindowOneCommand;
        }
    }

    private void ShowWindowOne()
    {
        MainModel.TypeName = "WindowOneViewProxy";
    }

    #endregion

    #region ShowWindowTwoCommand

    private ICommand _showWindowTwoCommand = null;

    public ICommand ShowWindowTwoCommand
    {
        get
        {
            if (_showWindowTwoCommand == null)
            {
                _showWindowTwoCommand = new RelayCommand(param => this.ShowWindowTwo(), null);
            }

            return _showWindowTwoCommand;
        }
    }

    private void ShowWindowTwo()
    {
        MainModel.TypeName = "WindowTwoViewProxy";
    }

    #endregion

    #region ShowWindowThreeCommand

    private ICommand _showWindowThreeCommand = null;

    public ICommand ShowWindowThreeCommand
    {
        get
        {
            if (_showWindowThreeCommand == null)
            {
                _showWindowThreeCommand = new RelayCommand(param => this.ShowWindowThree(), null);
            }

            return _showWindowThreeCommand;
        }
    }

    private void ShowWindowThree()
    {
        MainModel.TypeName = "WindowThreeViewProxy";
    }

    #endregion

    public MainViewModel() 
    {
        MainModel = new MainModel();
    }
}
Run Code Online (Sandbox Code Playgroud)

MainWindow 看起来像:

<Window x:Class="WindowFactoryNamespace.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:this="clr-namespace:WindowFactoryNamespace.ViewModels"
        xmlns:AttachedBehaviors="clr-namespace:WindowFactoryNamespace.AttachedBehaviors"
        AttachedBehaviors:WindowFactoryBehavior.Name="{Binding Path=MainModel.TypeName}"
        WindowStartupLocation="CenterScreen"
        Title="MainWindow" Height="300" Width="300"> 

<Window.DataContext>
    <this:MainViewModel />
</Window.DataContext>

<WrapPanel>
    <Button Content="WindowOne"
            Margin="10"
            Command="{Binding ShowWindowOneCommand}" /> 

    <Button Content="WindowTwo"
            Margin="10"
            Command="{Binding ShowWindowTwoCommand}" />

    <Button Content="WindowThree"
            Margin="10"
            Command="{Binding ShowWindowThreeCommand}" />
    </WrapPanel>
</Window>
Run Code Online (Sandbox Code Playgroud)

View-ViewModel第一个窗口的测试看起来像这样(它们几乎完全相同):

WindowOneModel

public class WindowOneModel : NotificationObject
{
    #region TextContent

    private string _textContent = "Text content for WindowOneView";

    public string TextContent
    {
        get
        {
            return _textContent;
        }

        set
        {
            _textContent = value;
            NotifyPropertyChanged("TextContent");
        }
    }

    #endregion
}
Run Code Online (Sandbox Code Playgroud)

WindowOneViewModel

public class WindowOneViewModel
{
    #region WindowOneModel

    private WindowOneModel _windowOneModel = null;

    public WindowOneModel WindowOneModel
    {
        get
        {
            return _windowOneModel;
        }

        set
        {
            _windowOneModel = value;
        }
    }

    #endregion

    #region OneCommand

    private ICommand _oneCommand = null;

    public ICommand OneCommand
    {
        get
        {
            if (_oneCommand == null)
            {
                _oneCommand = new RelayCommand(param => this.One(), null);
            }

            return _oneCommand;
        }
    }

    private void One()
    {
         WindowOneModel.TextContent = "Command One change TextContent";
    }

    #endregion

    public WindowOneViewModel() 
    {
        WindowOneModel = new WindowOneModel();
    }
}
Run Code Online (Sandbox Code Playgroud)

该项目可在此处获得link.

Output

MainWindow

在此输入图像描述

WindowOne

在此输入图像描述

WindowTwo

在此输入图像描述

WindowThree

在此输入图像描述


Vla*_*aev 1

在 99% 的情况下,通过构造函数推送容器实例是一个坏主意,因为容器是一个服务定位器。这种方法的主要缺点是:

  • 对容器具体实现的依赖;
  • 类的 API 不明确,这也会导致单元测试脆弱。

以 MVVM 方式创建窗口的方法有很多种:

  1. 使用中介器(如 MvvmLight 中的 IMessenger、Caliburn.Micro 中的 IEventAggregator);
  2. 使用特殊的 IDialogService
  3. 使用附加行为;
  4. 使用通过 ViewModel 构造函数插入的 Action
  5. 使用控制器