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

前端 未结 3 791
伪装坚强ぢ
伪装坚强ぢ 2020-12-14 21:09

I have been reading Mark Seeman\'s book on dependency injection in .NET and I\'m struggling to configure composition root in WPF application.

My container will be re

相关标签:
3条回答
  • 2020-12-14 21:39

    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. For this purpose, namely, for the create independent windows, well suited Abstract factory pattern.

    Creation of the windows can be implemented on the side ViewModel using methods such as IDialogService. 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. So, you must create MVVM style architecture that it allows create independent windows using design patterns.

    I created a project in which an Abstract factory creates a Window on the side of the View using the attached behavior. Abstract factory also implements the Singleton pattern to create a global point of access and to ensure the uniqueness of the newly constructed object. Attached behavior implicitly implements pattern Decorator who is a wrapper for an abstract factory that is used on the side of 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. Also used Command pattern for independent action between objects. As a result, this project uses the following patterns:

    • Abstract factory
    • Singleton
    • Decorator
    • Proxy
    • Command

    The project structure looks like this:

    enter image description here

    In the attached behavior has attached dependency property Name, which is transmitted in the name of the new window. For him registered PropertyChangedEvent, which is a call Make method an abstract factory:

    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:

    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. ContentTemplate is responsible for the visual representation, in order to bind properties from ViewModel, you need to set the object to Content. But in this case, the Abstract factory reference will to ViewModel, and to avoid them and using the proxy pattern as follows:

    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.

    In MainViewModel has commands to simply set the window name, which will give input for attached behavior:

    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:

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

    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.

    Output

    MainWindow

    enter image description here

    WindowOne

    enter image description here

    WindowTwo

    enter image description here

    WindowThree

    enter image description here

    0 讨论(0)
  • 2020-12-14 21:51

    IMHO, there is no need to over complicate the solution for the sake of MVVM purity. 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.

    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.

    0 讨论(0)
  • 2020-12-14 21:55

    Pushing container instance through constructor is a bad idea in 99% of cases, because container is a service locator. 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.

    There are many ways to create window in MVVM fashion:

    1. using Mediators (like IMessenger in MvvmLight, IEventAggregator in Caliburn.Micro);
    2. using special IDialogService;
    3. using attached behaviours;
    4. using Action that inserted via ViewModel constructor;
    5. using Controllers.
    0 讨论(0)
提交回复
热议问题