简体   繁体   English

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

[英]Resolving windows in Structure Map or how to manage multiple windows in WPF MVVM?

I have been reading Mark Seeman's book on dependency injection in .NET and I'm struggling to configure composition root in WPF application. 我一直在阅读Mark Seeman关于.NET中依赖注入的书,我正在努力在WPF应用程序中配置组合根。

My container will be registered in the application startup method: 我的容器将在应用程序启动方法中注册:

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>();
                        });
}

This makes sense as the application startup represents my composition root. 这有意义,因为应用程序启动代表我的组合根。

WPF windows in my application are based on view models. 我的应用程序中的WPF窗口基于视图模型。 View models use constructor injection. 视图模型使用构造函数注入。 Eg I may compose a view model by injecting implementation of IAccountServices . 例如,我可以通过注入IAccountServices实现来组成视图模型。

When it comes to creating my main window, I can do the following inside of the OnStartup method: 在创建主窗口时,我可以在OnStartup方法中执行以下操作:

var mainWindow = container.GetInstance<MainWindow>();
mainWindow.Show();

Once I'm inside of the main window, I might want open up another window. 一旦我进入主窗口,我可能想要打开另一个窗口。 So far I've been able to come up with one way of doing this, which is to create a window factory and ask window factory to resolve instance of the window. 到目前为止,我已经能够提出一种方法,即创建一个窗口工厂并请求窗口工厂解析窗口的实例。 I'll have to make sure that window factory is available in every view model that might need to open a new window. 我必须确保窗口​​工厂在每个可能需要打开新窗口的视图模型中都可用。 In my mind this is as bad as passing IoC container around my application (service locator anti-pattern comes to mind). 在我看来,这与在我的应用程序中传递IoC容器一样糟糕(服务定位器反模式会浮现在脑海中)。

Does this approach seem right to you? 这种方法对你来说是否正确? My gut feeling tells me that this is wrong, but I haven't come up with a better way of achieving this (yet). 我的直觉告诉我这是错的,但我还没有想出一个更好的方法来实现这个目标。

I think before implement patterns of behavior, such as a Mediator , and the like, need to decide on a generic pattern for easy application structure. 我认为在实现行为模式之前,例如Mediator等,需要决定一个通用模式以便于应用程序结构。 For this purpose, namely, for the create independent windows, well suited Abstract factory pattern. 为此目的,即为创建独立窗口,非常适合Abstract factory模式。

Creation of the windows can be implemented on the side ViewModel using methods such as IDialogService . 可以使用诸如IDialogService方法在ViewModel侧实现窗口的创建。 But I think that this task should be implemented on the side View , because the Window object refers to the View and not to ViewModel . 但我认为这个任务应该在View侧面实现,因为Window对象是指View而不是ViewModel So, you must create MVVM style architecture that it allows create independent windows using design patterns. 因此,您必须创建MVVM样式架构,它允许使用设计模式创建独立窗口。

I created a project in which an Abstract factory creates a Window on the side of the View using the attached behavior. 我创建了一个项目,其中一个Abstract factory使用附加的行为在View的一侧创建一个Window。 Abstract factory also implements the Singleton pattern to create a global point of access and to ensure the uniqueness of the newly constructed object. Abstract factory还实现了Singleton模式,以创建全局访问点并确保新构造对象的唯一性。 Attached behavior implicitly implements pattern Decorator who is a wrapper for an abstract factory that is used on the side of XAML. 附加行为隐式实现了模式Decorator,它是在XAML一侧使用的抽象工厂的包装器。 To an Abstract factory does not refer to objects which are located in ViewModel is used a Proxy pattern which is a ContentControl with DataTemplate without DataType. 对于Abstract factory ,不引用位于ViewModel对象使用代理模式,该模式是具有不带DataType的DataTemplate的ContentControl。 Also used Command pattern for independent action between objects. 还使用Command模式在对象之间进行独立操作。 As a result, this project uses the following patterns: 因此,该项目使用以下模式:

  • Abstract factory 抽象工厂
  • Singleton 独生子
  • Decorator 装饰
  • Proxy 代理
  • Command 命令

The project structure looks like this: 项目结构如下所示:

在此输入图像描述

In the attached behavior has attached dependency property Name , which is transmitted in the name of the new window. 在附加的行为中附加了依赖属性Name ,它以新窗口的名称传输。 For him registered PropertyChangedEvent , which is a call Make method an abstract factory: 为他注册了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();
        }        
    }
}

WindowFactory together with the Singleton pattern looks like this: 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);
    }
}

For the property Window.ContentTemplate set DataTemplate from resources. 对于属性Window.ContentTemplate从资源中设置DataTemplate。 ContentTemplate is responsible for the visual representation, in order to bind properties from ViewModel, you need to set the object to Content. ContentTemplate负责可视化表示,为了从ViewModel绑定属性,需要将对象设置为Content。 But in this case, the Abstract factory reference will to ViewModel, and to avoid them and using the proxy pattern as follows: 但在这种情况下, Abstract factory引用将为ViewModel,并避免它们并使用代理模式如下:

WindowOneProxyView

<DataTemplate x:Key="WindowOneViewProxy">
    <ContentControl ContentTemplate="{StaticResource WindowOneViewRealObject}">
        <ViewModels:WindowOneViewModel />
    </ContentControl>
</DataTemplate>

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>

In DataTemplate proxy is not specified DataType, but it is in the real object. DataTemplate代理不是指定DataType,而是在真实对象中。

In MainViewModel has commands to simply set the window name, which will give input for attached behavior: MainViewModel有命令来简单地设置窗口名称,它将为附加的行为提供输入:

MainModel

public class MainModel : NotificationObject
{
    #region TypeName

    private string _typeName = null;

    public string TypeName
    {
        get
        {
            return _typeName;
        }

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

    #endregion
}

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();
    }
}

MainWindow looks as: 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>

Test View-ViewModel for the first window looks like this (they practically identical): 第一个窗口的Test 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
}

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();
    }
}

This project is available at this link . 该项目可在此link

Output

MainWindow

在此输入图像描述

WindowOne

在此输入图像描述

WindowTwo

在此输入图像描述

WindowThree

在此输入图像描述

IMHO, there is no need to over complicate the solution for the sake of MVVM purity. 恕我直言,为了MVVM纯度,没有必要使解决方案复杂化。 You risk the subsequent developers not understanding your elegant solution and break it. 您可能会冒后续开发人员不理解您的优雅解决方案并将其打破。 In fact there is a good chance of that as "pure" implementations tend to be not that readable because of the complexity. 事实上,由于复杂性,“纯”实现往往不具有可读性,因此很有可能。

IMHO, any solution where a problem is permanently solved under an abstraction with minimal code overhead and simplicity in its usage is better than doing considerable overhead every time the solution is used even if "purity" is achieved(it won't serve any purpose). 恕我直言,任何以抽象方式永久解决问题的解决方案,只需最少的代码开销和简单的使用,就比每次使用解决方案时花费大量开销更好,即使达到“纯度”(它不会用于任何目的) 。 The problem of showing dialog in the application has to be solved once and it should be easy to use it in the future. 在应用程序中显示对话框的问题必须解决一次,并且将来应该很容易使用它。

Composing view models is perfectly fine, and could make life easier by allowing view models to interact without drama 组合视图模型非常精细,通过允许视图模型在没有戏剧性的情况下进行交互,可以使生活更轻松

A dialog service can be created which will act as a wrapper for all your dialog needs in the application. 可以创建一个对话框服务,它将充当应用程序中所有对话框需求的包装器。 You can inject the Dialog Service and the child view models which needs to be displayed in a window, to your parent view model. 您可以将对话框服务和需要在窗口中显示的子视图模型注入父视图模型。 When you need to display the window, ask the Dialog service to do it, passing it the view model instance and view name. 当您需要显示窗口时,请求Dialog服务执行此操作,并将视图模型实例和视图名称传递给它。

Note:code is not complied or tested 注意:代码未被编译或测试

 public class DialogService : IDialogService
{

 IEventAggregator _eventAggregator;
 bool _fatalError;

//Provides a wrapper function which will connect your view and view model and open a     
//dialog
 public Window ShowCustomDialog<TViewModel>(string name, TViewModel viewModel, bool 
      modal, double left, double top, Action<bool?> OnClose, int width, int height)
  {
            if (_fatalError == true)
            {
                return null;
            }

            Window view = new Window(name);           

            if (viewModel != null)
            {
                view.DataContext = viewModel;
            }

            if (left != -1.0 && top != -1.0)
            {
                view.WindowStartupLocation = WindowStartupLocation.Manual;
                view.Left = left;
                view.Top = top;
            }
            else
            {
                view.WindowStartupLocation = WindowStartupLocation.CenterScreen;
            }

            if (width != -1 && height != -1)
            {
                view.Width = width;
                view.Height = height;
            }

            view.Closed += (o, e) =>
                {
                    _eventAggregator.GetEvent<NotifyDialogAction>().Publish(false);

                    if (OnClose != null)
                    {
                        OnClose(e.DialogResult);
                    }
                };


            view.Loaded += (o, e) =>
                {
                    _eventAggregator.GetEvent<NotifyDialogAction>().Publish(true);

                    Window window = o as Window;
                    if (window != null)
                    {
                        double dialogWidth = window.ActualWidth;
                        double screenWidth = 
                             Application.Current.RootVisual.RenderSize.Width;
                        double dialogLeft = window.Left;

                        if (dialogLeft + dialogWidth > screenWidth)
                        {
                            window.Left = screenWidth - dialogWidth;
                        }

                        double dialogHeight = window.ActualHeight;
                        double screenHeight = 
                            Application.Current.RootVisual.RenderSize.Height;
                        double dialogTop = window.Top;

                        if (dialogTop + dialogHeight > screenHeight)
                        {
                            window.Top = screenHeight - dialogHeight;
                        }

                    }
                };

            if (modal)
            {
                view.ShowDialog();
            }
            else
            {
                view.Show();
            }

            return view;
        }

//Add more functions. For example to pop up a message box etc.
}

Usage 用法

 public class ComposedVM
   {
       public ViewModelA objA{get;set;}
       public ViewModelB objB{get;set;}
       IDialogService dialogService{get;set;}

       public ComposedVM(ViewModelA  a, ViewModelB b, IDialogService dlg )
       {
         objA = a;
         objB = b;
         dialogService = dlg

        }


      public void OnShowWindowACommand()
      {
         dialogService .ShowCustomDialog<object>(
         DialogNames.ViewA/*view name constant*/, objA, true, -1.0, -1.0,
         result =>
         {
            if (result == true)
            {                                                                         
               dialogService.ShowMessageDialog(ApplicationStrings.SuccessFulOperation);                           
            }
          });

        }
    }

An event/message based communication can be used between modules. 可以在模块之间使用基于事件/消息的通信。 Using it for related view models in a module is an overkill IMHO. 将它用于模块中的相关视图模型是一种矫枉过正的恕我直言。

Pushing container instance through constructor is a bad idea in 99% of cases, because container is a service locator. 在99%的情况下,通过构造函数推送容器实例是一个坏主意,因为容器是服务定位器。 The main disadvantages of this approach are: 这种方法的主要缺点是:

  • dependency from concrete implementation of container; 从具体实施容器的依赖;
  • unclear API of your classes, which also leads to fragile unit tests. 您的类的API不清楚,这也会导致脆弱的单元测试。

There are many ways to create window in MVVM fashion: 有许多方法可以用MVVM方式创建窗口:

  1. using Mediators (like IMessenger in MvvmLight, IEventAggregator in Caliburn.Micro); 使用Mediators(如MvvmLight中的IMessenger,Caliburn.Micro中的IEventAggregator);
  2. using special IDialogService ; 使用特殊的IDialogService ;
  3. using attached behaviours; 使用附加行为;
  4. using Action that inserted via ViewModel constructor ; 使用通过ViewModel构造函数插入的Action ;
  5. using Controllers . 使用控制器

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM