繁体   English   中英

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

[英]Resolving windows in Structure Map or how to manage multiple windows in WPF 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>();
                        });
}

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

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

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

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

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

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

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

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

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

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

对于属性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>

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
}

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>

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

该项目可在此link

Output

MainWindow

在此输入图像描述

WindowOne

在此输入图像描述

WindowTwo

在此输入图像描述

WindowThree

在此输入图像描述

恕我直言,为了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);                           
            }
          });

        }
    }

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

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

  • 从具体实施容器的依赖;
  • 您的类的API不清楚,这也会导致脆弱的单元测试。

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

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

暂无
暂无

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

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