【问题标题】:Resolving windows in Structure Map or how to manage multiple windows in WPF MVVM?解析结构图中的窗口或如何管理 WPF MVVM 中的多个窗口?
【发布时间】:2013-12-13 02:46:15
【问题描述】:

我一直在阅读 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>();
                        });
}

这是有道理的,因为应用程序启动代表我的组合根。

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

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

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

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

您觉得这种方法合适吗?我的直觉告诉我这是错误的,但我还没有想出更好的方法来实现这一点。

【问题讨论】:

    标签: wpf mvvm dependency-injection window inversion-of-control


    【解决方案1】:

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

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

    我创建了一个项目,其中Abstract factory 使用附加的行为在View 的一侧创建了一个窗口。 Abstract factory 还实现了单例模式来创建全局访问点并确保新构造对象的唯一性。附加行为隐式实现了模式装饰器,它是在 XAML 一侧使用的抽象工厂的包装器。对于Abstract factory 不指代位于ViewModel 中的对象,使用代理模式,该模式是带有DataTemplate 的ContentControl,没有DataType。还使用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();
            }        
        }
    }
    

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

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

    对于属性Window.ContentTemplate,从资源中设置DataTemplate。 ContentTemplate 负责可视化表示,为了从 ViewModel 绑定属性,您需要将对象设置为 Content。但在这种情况下,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>
    

    DataTemplateproxy中没有指定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
    }
    

    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 看起来像:

    <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>
    

    第一个窗口的测试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();
        }
    }
    

    此项目可在此link 获得。

    Output

    MainWindow

    WindowOne

    WindowTwo

    WindowThree

    【讨论】:

    • 父视图模型如何从关闭的窗口中获取信息?例如,用户是否点击了窗口中的确定或取消按钮?
    • @Jimmy:有几个变种。在这种情况下,我将添加MainViewModel IsWindowOneClosed 属性,并使用中介者模式将发送来自WindowOneView 的信号。 Here 你可以看到我使用中介者模式的例子。
    • 嘿,谢谢你这么详细的帖子。这是一个很好的解决方案。到目前为止,我一直在使用一种不太友好的方法,即通过 PRISM 消息打开窗口。例如,我将发送一条包含相关信息的消息以及呼叫者。您的方法的唯一问题是我无法像使用消息一样将数据传递到新窗口。
    【解决方案2】:

    恕我直言,没有必要为了 MVVM 纯度而使解决方案过于复杂。您冒着后续开发人员不理解您的优雅解决方案并破坏它的风险。事实上,这很有可能是因为“纯”实现由于复杂性而往往不那么可读。

    恕我直言,任何在抽象下以最少的代码开销和简单的方式永久解决问题的解决方案都比每次使用解决方案时都做相当大的开销要好,即使实现了“纯度”(它不会提供任何目的)。在应用程序中显示对话框的问题必须解决一次,并且将来应该易于使用。

    组合视图模型非常好,并且可以让视图模型在没有戏剧性的情况下进行交互,从而使生活更轻松

    可以创建一个对话服务,它将充当应用程序中所有对话需求的包装器。您可以将对话框服务和需要在窗口中显示的子视图模型注入父视图模型。当您需要显示窗口时,请 Dialog 服务执行此操作,并将视图模型实例和视图名称传递给它。

    注意:代码未经编译或测试

     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.
    }
    

    用法

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

    可以在模块之间使用基于事件/消息的通信。恕我直言,将它用于模块中的相关视图模型是过分的。

    【讨论】:

    • 也许我错了,在我的例子中你看到了困难吗?这是通过DataTemplate 使用MVVM 的标准方式。唯一添加的 - 是一个在条件上创建窗口的抽象工厂。这个工厂是通过一个附加的行为来实现的,这在 MVVM 中很常用。关于工厂,这可能是28最简单的模式,我知道。除了创建的模式之外,其他人可以毫无问题地理解您的解决方案,这是根据标准创建的。
    • 如果你还没有下载我的例子,我建议你下载。代码非常简单明了,没有任何hack和additional frameworks
    • @AnatoliyNikolaev,我确实下载了您的解决方案并查看了它。感谢您设法使视图模型独立。通过这样做,视图模型不可能通过自然接口与其子视图模型对话,而是使用基于事件/消息的通信。应用程序最终可能会包含 100 多个事件定义,用于视图模型之间的通信以及模块(如果有)之间的通信,而只有在后一种情况下它是绝对必要的。除非您的 Windows 本身非常复杂,否则没有明显的优势。
    • 我想说的另一点是通过使用中介/工厂模式,在应用程序中显示对话框的问题变成了一个重复出现的问题。每次您需要创建视图模型/视图以在对话框中使用时,您都必须使用样板代码来管理“独立性”。恕我直言,这很糟糕。
    【解决方案3】:

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

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

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

    1. 使用调解器(如 MvvmLight 中的 IMessenger,Caliburn.Micro 中的 IEventAggregator);
    2. 使用special IDialogService;
    3. 使用附加行为;
    4. 使用Action that inserted via ViewModel constructor;
    5. 使用Controllers

    【讨论】:

    • 最初我使用了消息传递解决方案,但现在它变得非常难以维护,因为我们在一个有十个窗口的区域中使用。单个窗口有多个退出点,所以我在 GoToHomeScreenMessage、StartNewProcessXMessage、StartNewProcessYMessage 等行上有消息。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-02-28
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多