Разрешение окон в Структурной карте или управление несколькими окнами в WPF MVVM?

Я читал книгу Марка Симана по вложению зависимостей в .NET, и я изо всех сил пытаюсь настроить состав root в приложении 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();

Как только я окажусь внутри главного окна, я могу открыть другое окно. До сих пор я смог придумать один из способов сделать это: создать окно factory и спросить окно factory для разрешения экземпляра окна. Я должен убедиться, что окно factory доступно в каждой модели представлений, которая может потребоваться для открытия нового окна. На мой взгляд, это так же плохо, как перенос контейнера IoC вокруг моего приложения (на ум приходит анти-шаблон службы поиска).

Этот подход кажется вам правильным? Чувство моего чувства говорит мне, что это неправильно, но я не придумал лучшего способа достичь этого (пока).

Ответ 1

Я думаю, что перед тем, как реализовать шаблоны поведения, такие как Mediator и т.п., необходимо принять решение об общем шаблоне для легкой структуры приложения. Для этой цели, а именно для создания независимых окон, хорошо подходит шаблон Abstract factory.

Создание окон может быть реализовано на стороне ViewModel с использованием таких методов, как IDialogService. Но я думаю, что эта задача должна быть реализована на стороне View, потому что объект Window относится к View, а не к ViewModel. Таким образом, вы должны создать архитектуру стиля MVVM, которая позволяет создавать независимые окна с использованием шаблонов проектирования.

Я создал проект, в котором Abstract factory создает окно со стороны View, используя прикрепленное поведение. Abstract factory также реализует шаблон Singleton для создания глобальной точки доступа и обеспечения уникальности вновь созданного объекта. Приложенное поведение неявно реализует шаблон Decorator, который является оберткой для абстрактного factory, который используется на стороне XAML. Для Abstract factory не относится к объектам, расположенным в ViewModel, используется шаблон прокси, который является ContentControl с DataTemplate без DataType. Также используется шаблон Command для независимого действия между объектами. В результате в этом проекте используются следующие шаблоны:

  • Аннотация factory
  • Singleton
  • декоратор
  • Proxy
  • Command

Структура проекта выглядит следующим образом:

enter image description here

В прикрепленном поведении установлено свойство зависимостей Name, которое передается в имени нового окна. Для него зарегистрирован PropertyChangedEvent, который является вызовом Make method абстрактным 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 вместе с шаблоном 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>

Тест 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

enter image description here

WindowOne

enter image description here

WindowTwo

enter image description here

WindowThree

enter image description here

Ответ 2

IMHO, нет необходимости усложнять решение ради чистоты MVVM. Вы рискуете, что последующие разработчики не поймут ваше элегантное решение и сломают его. На самом деле есть хорошие шансы на то, что "чистые" реализации, как правило, не читаются из-за сложности.

IMHO, любое решение, где проблема постоянно разрешается при абстракции с минимальными накладными расходами кода и простота в ее использовании, лучше, чем выполнение значительных накладных расходов каждый раз, когда решение используется, даже если достигается "чистота" (он не будет использоваться любой цели). Проблема отображения диалога в приложении должна быть решена один раз, и в будущем это будет легко использовать.

Композиционные модели представлений совершенно прекрасны и могут облегчить жизнь, позволяя модели просмотра взаимодействовать без драмы

Может быть создана служба диалога, которая будет действовать как оболочка для всех ваших потребностей в диалоговом окне приложения. Вы можете ввести модели Dialog Service и детское представление, которые должны отображаться в окне, в родительскую модель представления. Когда вам нужно отобразить окно, попросите службу 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);                           
            }
          });

        }
    }

Связь между событиями и сообщениями может использоваться между модулями. Использование его для связанных моделей взглядов в модуле является чрезмерным IMHO.

Ответ 3

Нажатие экземпляра контейнера через конструктор - плохая идея в 99% случаев, поскольку контейнер является локатором службы. Основными недостатками этого подхода являются:

  • зависимость от конкретной реализации контейнера;
  • нечеткий API ваших классов, что также приводит к хрупким модульным тестам.

Существует много способов создания окна в режиме MVVM: