Обработка диалогов в WPF с помощью MVVM

В шаблоне MVVM для WPF диалоговые окна обработки являются одной из наиболее сложных операций. Поскольку ваша модель представления ничего не знает о представлении, диалог может быть интересным. Я могу открыть ICommand, когда при вызове view появится диалоговое окно.

Кто-нибудь знает хороший способ обработки результатов из диалогов? Я говорю о диалоговых окнах Windows, таких как MessageBox.

Один из способов, которым мы это сделали, - это событие на модели представления, которое будет подписано, когда потребуется диалог.

public event EventHandler<MyDeleteArgs> RequiresDeleteDialog;

Это нормально, но это означает, что для просмотра требуется код, который я бы хотел избегать.

Ответ 1

Я предлагаю отказаться от модальных диалогов 1990 года и вместо этого реализовать элемент управления как оверлей (холст + абсолютное позиционирование) с видимостью, привязанной к логическому возврату в виртуальной машине. Ближе к управлению типом ajax.

Это очень полезно:

<BooleanToVisibilityConverter x:Key="booltoVis" />

как в:

<my:ErrorControl Visibility="{Binding Path=ThereWasAnError, Mode=TwoWay, Converter={StaticResource booltoVis}, UpdateSourceTrigger=PropertyChanged}"/>

Вот как я реализовал его как пользовательский элемент управления. Нажатие на "x" закрывает элемент управления в строке кода в коде usercontrol. (Поскольку у меня есть мои представления в .exe и ViewModels в dll, я не чувствую себя плохо в отношении кода, который управляет пользовательским интерфейсом.)

Wpf dialog

Ответ 2

Для этого вам следует использовать посредник. Посредник - это общий шаблон дизайна, также известный как Messenger в некоторых его реализациях. Это парадигма типа Register/Notify и позволяет вашим ViewModel и представлениям взаимодействовать с помощью мешанинов с низкой связью.

Вы должны проверить группу google WPF Disciples и просто выполнить поиск посредника. Вы будете очень довольны ответами...

Однако вы можете начать с этого:

http://joshsmithonwpf.wordpress.com/2009/04/06/a-mediator-prototype-for-wpf-apps/

Наслаждайтесь!

Изменить: вы можете увидеть ответ на эту проблему с помощью MVVM Light Toolkit здесь:

http://mvvmlight.codeplex.com/Thread/View.aspx?ThreadId=209338

Ответ 3

Хорошее диалоговое окно MVVM должно:

  • Объявляется только с помощью XAML.
  • Получить все это из привязки данных.

К сожалению, WPF не предоставляет эти функции. Для отображения диалогового окна требуется вызов по коду ShowDialog(). Класс Window, поддерживающий диалоги, не может быть объявлен в XAML, поэтому он не может быть легко привязан к DataContext.

Чтобы решить эту проблему, я написал элемент управления XAML, который находится в логическом дереве и привязывает привязку данных к окну и обрабатывает отображение и скрытие диалогового окна. Вы можете найти его здесь: http://www.codeproject.com/KB/WPF/XAMLDialog.aspx

Это действительно просто использовать и не требует каких-либо странных изменений в вашей ViewModel и не требует событий или сообщений. Основной вызов выглядит следующим образом:

<dialog:Dialog Content="{Binding Path=DialogViewModel}" Showing="True" />

Вероятно, вы хотите добавить стиль, который устанавливает Showing. Я объясняю это в своей статье. Надеюсь, это поможет вам.

Ответ 4

Я использую этот подход для диалогов с MVVM.

Все, что мне нужно сделать, это вызвать из моей модели представления следующее.

var result = this.uiDialogService.ShowDialog("Dialogwindow title goes here", dialogwindowVM);

Ответ 5

Мое текущее решение решает большинство затронутых вами проблем, но полностью абстрагируется от конкретных вещей платформы и может быть повторно использовано. Кроме того, я не использовал ссылку на код только для связывания с DelegateCommands, которые реализуют ICommand. Диалог - это в основном View - отдельный элемент управления, который имеет свой собственный ViewModel, и он отображается из ViewModel основного экрана, но запускается из пользовательского интерфейса с помощью привязки DelagateCommand.

Смотрите полное решение Silverlight 4 здесь Модальные диалоги с MVVM и Silverlight 4

Ответ 6

Я действительно боролся с этой концепцией некоторое время, когда учился (все еще учился) MVVM. То, что я решил, и то, что я думаю, что другие уже решили, но не ясное для меня, таково:

Моя первоначальная мысль заключалась в том, что ViewModel не должен позволять напрямую обращаться к диалоговому окну, поскольку у него нет бизнеса, решающего, как должно появиться диалоговое окно. Исходя из этого, я начал думать о том, как передавать сообщения так же, как и в MVP (т.е. View.ShowSaveFileDialog()). Однако, я думаю, что это неправильный подход.

OK, чтобы ViewModel вызывал диалог напрямую. Однако, когда вы тестируете ViewModel, это означает, что диалог будет всплывать во время теста или не сработает вместе (никогда не пробовал это).

Итак, что должно произойти, во время тестирования нужно использовать "тестовую" версию вашего диалога. Это означает, что на любом диалоговом окне вам нужно создать интерфейс и либо выставить отклик на ответ диалога, либо создать тестовый макет, который будет иметь поведение по умолчанию.

Вы уже должны использовать какой-то сервисный локатор или IoC, которые вы можете настроить, чтобы предоставить вам правильную версию в зависимости от контекста.

Используя этот подход, ваш ViewModel все еще можно проверить и в зависимости от того, как вы издеваетесь над вашими диалогами, вы можете контролировать поведение.

Надеюсь, что это поможет.

Ответ 7

Есть два хороших способа сделать это: 1) услуга диалога (простая, чистая) и 2) просмотр помощи. View assisted предоставляет некоторые опрятные функции, но обычно не стоит.

ОБСЛУЖИВАНИЕ DIALOG

a) интерфейс диалогового сервиса, например, через конструктор или некоторый контейнер зависимостей:

interface IDialogService { Task ShowDialogAsync(DialogViewModel dlgVm); }

b) Ваша реализация IDialogService должна открыть окно (или ввести некоторый элемент управления в активное окно), создать представление, соответствующее имени данного типа dlgVm (использовать регистрацию или соглашение контейнера или ContentPresenter с соответствующими DataTemplates типа), ShowDialogAsync должен создать объект TaskCompletion и вернуть его .Task proptery. Сам класс DialogViewModel требует события, которое вы можете вызвать в производном классе, когда хотите закрыть, и посмотреть в диалоговом окне, чтобы фактически закрыть/скрыть диалог и завершить TaskCompletionSource.

b) Чтобы использовать, просто вызовите await this.DialogService.ShowDialog(myDlgVm) в свой экземпляр некоторого класса, созданного DialogViewModel. После ожидания возврата просмотрите свойства, которые вы добавили в свой диалог VM, чтобы определить, что произошло; вам даже не нужен обратный вызов.

СМОТРЕТЬ ПОМОЩЬ

Это ваше мнение, прослушивание события на viewmodel. Все это может быть завершено в Blend Behavior, чтобы избежать использования кода и использования ресурсов, если вы так склонны (FMI, подкласс класса "Поведение", чтобы увидеть вид смешаемого прикрепленного свойства на стероидах). Пока что мы сделаем это вручную для каждого вида:

a) Создайте OpenXXXXXDialogEvent с пользовательской полезной нагрузкой (производный класс DialogViewModel).

b) Представьте, что представление подписано на событие в его событии OnDataContextChanged. Не забудьте скрыть и отменить подписку, если старое значение!= Null и в событии "Выгружено окно".

c) Когда событие срабатывает, просмотрите представление, которое может быть в ресурсе на вашей странице, или вы можете найти его по соглашению в другом месте (например, в подходе к службе диалога).

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

Это позволяет диалоги находиться близко к месту, в котором они фактически используются, только уменьшает часть приложения, связанную с текущей деятельностью, и позволяет пользователю перемещаться в приложении без необходимости вручную отталкивать диалоги, даже на разных вкладках или под-представлениях открываются несколько квазимодальных диалогов.

Ответ 8

Используйте команду freezable

<Grid>
        <Grid.DataContext>
            <WpfApplication1:ViewModel />
        </Grid.DataContext>


        <Button Content="Text">
            <Button.Command>
                <WpfApplication1:MessageBoxCommand YesCommand="{Binding MyViewModelCommand}" />
            </Button.Command>
        </Button>

</Grid>
public class MessageBoxCommand : Freezable, ICommand
{
    public static readonly DependencyProperty YesCommandProperty = DependencyProperty.Register(
        "YesCommand",
        typeof (ICommand),
        typeof (MessageBoxCommand),
        new FrameworkPropertyMetadata(null)
        );


    public static readonly DependencyProperty OKCommandProperty = DependencyProperty.Register(
        "OKCommand",
        typeof (ICommand),
        typeof (MessageBoxCommand),
        new FrameworkPropertyMetadata(null)
        );


    public static readonly DependencyProperty CancelCommandProperty = DependencyProperty.Register(
        "CancelCommand",
        typeof (ICommand),
        typeof (MessageBoxCommand),
        new FrameworkPropertyMetadata(null)
        );


    public static readonly DependencyProperty NoCommandProperty = DependencyProperty.Register(
        "NoCommand",
        typeof (ICommand),
        typeof (MessageBoxCommand),
        new FrameworkPropertyMetadata(null)
        );


    public static readonly DependencyProperty MessageProperty = DependencyProperty.Register(
        "Message",
        typeof (string),
        typeof (MessageBoxCommand),
        new FrameworkPropertyMetadata("")
        );

    public static readonly DependencyProperty MessageBoxButtonsProperty = DependencyProperty.Register(
        "MessageBoxButtons",
        typeof(MessageBoxButton),
        typeof(MessageBoxCommand),
        new FrameworkPropertyMetadata(MessageBoxButton.OKCancel)
        );

    public ICommand YesCommand
    {
        get { return (ICommand) GetValue(YesCommandProperty); }
        set { SetValue(YesCommandProperty, value); }
    }

    public ICommand OKCommand
    {
        get { return (ICommand) GetValue(OKCommandProperty); }
        set { SetValue(OKCommandProperty, value); }
    }

    public ICommand CancelCommand
    {
        get { return (ICommand) GetValue(CancelCommandProperty); }
        set { SetValue(CancelCommandProperty, value); }
    }

    public ICommand NoCommand
    {
        get { return (ICommand) GetValue(NoCommandProperty); }
        set { SetValue(NoCommandProperty, value); }
    }

    public MessageBoxButton MessageBoxButtons
    {
        get { return (MessageBoxButton)GetValue(MessageBoxButtonsProperty); }
        set { SetValue(MessageBoxButtonsProperty, value); }
    }

    public string Message
    {
        get { return (string) GetValue(MessageProperty); }
        set { SetValue(MessageProperty, value); }
    }

    public void Execute(object parameter)
    {
        var messageBoxResult = MessageBox.Show(Message);
        switch (messageBoxResult)
        {
            case MessageBoxResult.OK:
                OKCommand.Execute(null);
                break;
            case MessageBoxResult.Yes:
                YesCommand.Execute(null);
                break;
            case MessageBoxResult.No:
                NoCommand.Execute(null);
                break;
            case MessageBoxResult.Cancel:
                if (CancelCommand != null) CancelCommand.Execute(null); //Cancel usually means do nothing ,so can be null
                break;

        }
    }

    public bool CanExecute(object parameter)
    {
        return true;
    }

    public event EventHandler CanExecuteChanged;


    protected override Freezable CreateInstanceCore()
    {
        throw new NotImplementedException();
    }
}

Ответ 9

Я считаю, что обработка диалога должна отвечать за представление, и для представления необходимо, чтобы код поддерживал это.

Если вы измените взаимодействие ViewModel - View для обработки диалогов, то ViewModel зависит от этой реализации. Самый простой способ справиться с этой проблемой - сделать View ответственным за выполнение задачи. Если это означает показ диалога, то он отлично, но также может быть сообщением статуса в строке состояния и т.д.

Моя точка зрения состоит в том, что вся точка шаблона MVVM состоит в том, чтобы отделить бизнес-логику от графического интерфейса, поэтому вам не следует смешивать логику GUI (чтобы отобразить диалог) на бизнес-уровне (ViewModel).

Ответ 10

Интересной альтернативой является использование контроллеров, которые отвечают за отображение представлений (диалогов).

Как это работает, показано WPF Application Framework (WAF).

Ответ 11

Почему бы просто не поднять событие в виртуальной машине и не подписаться на событие в представлении? Это будет поддерживать логику приложения и представление отдельно и по-прежнему позволяет использовать дочернее окно для диалогов.

Ответ 12

Я реализовал поведение, которое прослушивает сообщение из ViewModel. Это основано на решении Laurent Bugnion, но поскольку он не использует код и больше подходит для повторного использования, я считаю его более элегантным.

Как заставить WPF вести себя так, как если MVVM поддерживается из коробки

Ответ 13

Я думаю, что представление может иметь код для обработки события из модели представления.

В зависимости от события/сценария он также может иметь триггер события, который подписывается на просмотр событий модели, и одно или несколько действий для вызова в ответ.

Ответ 14

У меня была такая же ситуация и завершена MessageBox в невидимый элемент управления дизайнера. Подробности в моем блоге

http://geekswithblogs.net/mukapu/archive/2010/03/12/user-prompts-messagebox-with-mvvm.aspx

То же самое можно расширить до любых модальных диалогов, управления просмотром файлов и т.д.

Ответ 15

Проведя некоторое время с этим, я наконец придумал следующее решение. Несколько ключевых преимуществ этого подхода:

  • Реализует MVVM Light собственный IDialogService.
  • View не нужно добавлять ссылку MVVM Light.
  • VM не нужно выполнять какие-либо действия на уровне презентации. Даже не нужна ссылка PresentationFramework.
  • Использует MVVM Light собственный канал Messenger, поэтому уровни презентации и VM разделены.
  • Поддерживает диалоги с возвращаемым значением, например Да/Нет вопросов или ОК/Отмена ситуаций.
  • Поддержка настраиваемых диалогов.

Здесь реализация IDialogService (входит в проект ViewModel):

using System;
using System.Linq;
using System.Threading.Tasks;

namespace VM
{
  public enum MessageBoxButtonVM
  {
    OK,
    OKCancel,
    YesNo
  }

  public enum MessageBoxImageVM
  {
    None,
    Information,
    Question,
    Error
  }

  public class MessageBoxArgs
  {
    public MessageBoxButtonVM Buttons { get; set; }
    public MessageBoxImageVM Icon { get; set; }
    public string Title { get; set; }
    public string Message { get; set; }
  }

  //For custom dialogs that return a value
  public class MessageBoxNotificationWithAction<T>
  {
    private readonly Action<T> _callback;

    public MessageBoxArgs Notification { get; set; }

    public MessageBoxNotificationWithAction(MessageBoxArgs notification, Action<T> callback)
    {
      Notification = notification;

      CheckCallback(callback);
      _callback = callback;
    }

    public virtual void Execute(T argument)
    {
      _callback.Invoke(argument);
    }

    private static void CheckCallback(Delegate callback)
    {
      if (callback == null)
      {
        throw new ArgumentNullException(nameof(callback), "Callback must not be null");
      }
    }
  }

  /// <summary>
  /// Provides an implementation-agnostic way of communicating with the user through dialog boxes. Clients must register for communication messages using
  /// MVVM Light messaging system.
  /// </summary>
  public class DialogService : GalaSoft.MvvmLight.Views.IDialogService
  {
    private static GalaSoft.MvvmLight.Messaging.IMessenger Messenger = GalaSoft.MvvmLight.Messaging.Messenger.Default;

    private string _ProductName = "";

    public string ProductName
    {
      get
      {
        if (_ProductName == "")
        {
          //The following statement returns the Title attribute of the current assembly, as defined in project properties (Assembly Information dialog).
          var TitleAttrib = System.Reflection.Assembly.GetExecutingAssembly().GetCustomAttributesData().First(x => x.AttributeType.Name == "AssemblyTitleAttribute");

          if (TitleAttrib != null)
          {
            _ProductName = TitleAttrib.ConstructorArguments[0].Value.ToString();
          }
          else
          {
            _ProductName = "Default Application Name";
          }
        }

        return _ProductName;
      }
    }

    public Task ShowError(Exception error, string title, string buttonText, Action afterHideCallback)
    {
      return ShowError(error.Message, title, buttonText, afterHideCallback);
    }

    public Task ShowMessage(string message, string title)
    {
      return Task.Run(() => MessengerSend(message, title, MessageBoxButtonVM.OK, MessageBoxImageVM.Error));
    }

    public Task ShowError(string message, string title, string buttonText, Action afterHideCallback)
    {
      return Task.Run(() =>
      {
        MessengerSend(message, title, MessageBoxButtonVM.OK, MessageBoxImageVM.Error);
        afterHideCallback?.Invoke();
      });
    }

    public Task ShowMessage(string message, string title, string buttonText, Action afterHideCallback)
    {
      return Task.Run(() =>
      {
        MessengerSend(message, title);
        afterHideCallback?.Invoke();
      });
    }

    public Task<bool> ShowMessage(string message, string title, string buttonConfirmText, string buttonCancelText, Action<bool> afterHideCallback)
    {
      if ((buttonConfirmText == "OK" && buttonCancelText == "Cancel") ||
        (buttonConfirmText == "Yes" && buttonCancelText == "No"))
      {
        return Task.Run<bool>(() =>
        {
          MessageBoxButtonVM btn;
          if (buttonConfirmText == "OK")
            btn = MessageBoxButtonVM.OKCancel;
          else
            btn = MessageBoxButtonVM.YesNo;


          bool Response = false;
          Messenger.Send(new MessageBoxNotificationWithAction<bool>(
                                                      new MessageBoxArgs()
                                                      {
                                                        Buttons = btn,
                                                        Icon = MessageBoxImageVM.Question,
                                                        Title = (string.IsNullOrEmpty(title) ? _ProductName : title),
                                                        Message = message
                                                      },
                                                      (result) => Response = result
                                                        ));

          afterHideCallback?.Invoke(Response);

          return Response;
        });
      }
      else
        throw new ArgumentException($"{nameof(buttonConfirmText)} and {nameof(buttonCancelText)} must either be OK/Cancel or Yes/No.");
    }

    /// <summary>
    /// For debugging purpose only
    /// </summary>
    /// <param name="message"></param>
    /// <param name="title"></param>
    /// <returns></returns>
    public Task ShowMessageBox(string message, string title) => ShowMessage(message, title);

    private void MessengerSend(string msg, string title = "", MessageBoxButtonVM btn = MessageBoxButtonVM.OK, MessageBoxImageVM icon = MessageBoxImageVM.Information)
    {
      Messenger.Send(new MessageBoxArgs()
      {
        Buttons = MessageBoxButtonVM.OK,
        Icon = MessageBoxImageVM.Information,
        Title = (string.IsNullOrEmpty(title) ? _ProductName : title),
        Message = msg
      });
    }
  }
}

Здесь уровень представления (входит в проект Вид)

using System.Windows;
using VM;

namespace View
{
  class DialogPresenter
  {
    private Window _Parent;

    public DialogPresenter()
    {
      //For simple information boxes
      GalaSoft.MvvmLight.Messaging.Messenger.Default.Register<MessageBoxArgs>(this, (arg) => ShowDialog(arg));

      //For Yes/No or OK/Cancel dialog boxes.
      GalaSoft.MvvmLight.Messaging.Messenger.Default.Register<MessageBoxNotificationWithAction<bool>>(this, (arg) => arg.Execute(ShowDialog(arg.Notification)));

      //For notifications that require a string response (such as Manual Timeslot Description)
      GalaSoft.MvvmLight.Messaging.Messenger.Default.Register<MessageBoxNotificationWithAction<string>>(this,
        (arg) => arg.Execute(ShowStringInputDialog(arg.Notification.Title, arg.Notification.Message)));
    }

    private bool ShowDialog(MessageBoxArgs arg)
    {
      MessageBoxButton btn = MessageBoxButton.OK;
      MessageBoxImage ico = MessageBoxImage.None;

      switch (arg.Buttons)
      {
        case MessageBoxButtonVM.OK: btn = MessageBoxButton.OK; break;
        case MessageBoxButtonVM.OKCancel: btn = MessageBoxButton.OKCancel; break;
        case MessageBoxButtonVM.YesNo: btn = MessageBoxButton.YesNo; break;
      }

      switch (arg.Icon)
      {
        case MessageBoxImageVM.Error: ico = MessageBoxImage.Error; break;
        case MessageBoxImageVM.Information: ico = MessageBoxImage.Information; break;
        case MessageBoxImageVM.None: ico = MessageBoxImage.None; break;
        case MessageBoxImageVM.Question: ico = MessageBoxImage.Question; break;
      }

      bool Result = false;
      _Parent.Dispatcher.Invoke(() =>
      {
        var Res = MessageBox.Show(arg.Message, arg.Title, btn, ico);
        Result = (Res == MessageBoxResult.OK || Res == MessageBoxResult.Yes);
      });

      return Result;
    }

    private string ShowStringInputDialog(string title, string description, string value = "", int maxLength = 100)
    {
      string Result = null;

      _Parent.Dispatcher.Invoke(() =>
      {
        //InputBox is a WPF Window I created for taking simple
        //string values from the user. This also shows that you can
        //any custom dialog using this approach.

        InputBox input = new InputBox();
        input.Title = title;
        input.Owner = _Parent;
        if (input.ShowDialog(description, value, maxLength).Value)
          Result=input.Value;
        else
          Result=null;
      });

      return Result;
    }

    //Call this somewhere at application startup so that the dialog boxes
    //appear as child windows.
    public void SetParentWindow(Window parent)
    {
      _Parent = parent;
    }
  }
}

Ответ 17

Карл Шиффлетт создал образец приложения для отображения диалоговых окон с использованием подхода к сервису и подхода Prism InteractionRequest.

Мне нравится подход к сервису - он менее гибкий, поэтому пользователи с меньшей вероятностью могут что-то сломать:) Он также согласуется с частью WinForms моего приложения (MessageBox.Show) Но если вы планируете показать много разных диалогов, то InteractionRequest - лучший способ пойти.

http://karlshifflett.wordpress.com/2010/11/07/in-the-box-ndash-mvvm-training/

Ответ 18

Я знаю, это старый вопрос, но когда я сделал этот поиск, я нашел много связанных вопросов, но я не нашел действительно четкого ответа. Поэтому я делаю свою собственную реализацию диалогового окна /messagebox/popin, и я делюсь ею!
Я думаю, что это "MVVM proof", и я стараюсь сделать его простым и правильным, но я новичок в WPF, поэтому не стесняйтесь комментировать или даже делать запрос на тягу.

https://github.com/Plasma-Paris/Plasma.WpfUtils

Вы можете использовать его следующим образом:

public RelayCommand YesNoMessageBoxCommand { get; private set; }
async void YesNoMessageBox()
{
    var result = await _Service.ShowMessage("This is the content of the message box", "This is the title", System.Windows.MessageBoxButton.YesNo);
    if (result == System.Windows.MessageBoxResult.Yes)
        // [...]
}

Или, если вам нужно более сложное приложение:

var result = await _Service.ShowCustomMessageBox(new MyMessageBoxViewModel { /* What you want */ });

И он показывает такие вещи:

2

Ответ 19

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

Мое текущее решение выглядит так:

public class SelectionTaskModel<TChoosable> : ViewModel
    where TChoosable : ViewModel
{
    public SelectionTaskModel(ICollection<TChoosable> choices);
    public ReadOnlyCollection<TChoosable> Choices { get; }
    public void Choose(TChoosable choosen);
    public void Abort();
}

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

Ответ 20

Я боролся с той же проблемой. Я придумал способ взаимодействовать между View и ViewModel. Вы можете инициировать отправку сообщения из ViewModel в представление, чтобы сообщить ему, чтобы он отображал сообщение, и он будет отчитываться с результатом. Затем ViewModel может ответить на результат, возвращенный из представления.

Я демонстрирую это в мой блог:

Ответ 21

Я написал довольно обширную статью на эту тему, а также разработал поп-библиотеку для MVVM Dialogs. Строгое соблюдение MVVM не только возможно, но и очень чисто при правильной реализации, и его можно легко распространить на сторонние библиотеки, которые сами этого не придерживаются:

https://www.codeproject.com/Articles/820324/Implementing-Dialog-Boxes-in-MVVM

Ответ 22

Извините, но я должен вмешаться. Я прошел через несколько из предложенных решений, прежде чем нашел пространство имен Prism.Wpf.Interactivity в проекте Prism. Вы можете использовать запросы взаимодействия и действие всплывающего окна, чтобы либо свернуть пользовательское окно, либо для более простых нужд есть встроенные всплывающие окна уведомлений и подтверждений. Они создают настоящие окна и управляются как таковые. Вы можете передать объект контекста с любыми зависимостями, которые вам нужны в диалоге. Мы используем это решение на моей работе, так как я его нашел. У нас здесь много старших разработчиков, и никто не придумал ничего лучшего. Нашим предыдущим решением было использование диалогового сервиса в виде оверлея и использование класса презентатора, чтобы это произошло, но вы должны были иметь фабрики для всех моделей представления диалога и т.д.

Это не тривиально, но и не супер сложно. И он встроен в Призму и поэтому является лучшей (или лучшей) практикой ИМХО.

Мои 2 цента!

Ответ 23

Стандартный подход

Потратив годы на решение этой проблемы в WPF, я наконец нашел стандартный способ реализации диалогов в WPF. Вот преимущества этого подхода:

  1. ЧИСТАЯ
  2. Не нарушает шаблон проектирования MVVM
  3. ViewModal никогда не ссылается ни на одну из библиотек пользовательского интерфейса (WindowBase, PresentationFramework и т.д.)
  4. Идеально подходит для автоматического тестирования
  5. Диалоги можно легко заменить.

Так в чем ключ. Это DI + IoC.

Вот как это работает. Я использую MVVM Light, но этот подход может быть распространен и на другие фреймворки:

  1. Добавьте проект приложения WPF в свое решение. Назовите это App.
  2. Добавьте библиотеку классов ViewModal. Назовите это VM.
  3. Приложение ссылается на проект VM. Проект VM ничего не знает о App.
  4. Добавьте ссылку NuGet на MVVM Light в оба проекта. Сейчас я использую MVVM Light Standard, но с полной версией Framework у вас тоже все в порядке.
  5. Добавьте интерфейс IDialogService в проект VM:

    public interface IDialogService
    {
      void ShowMessage(string msg, bool isError);
      bool AskBooleanQuestion(string msg);
      string AskStringQuestion(string msg, string default_value);
    
      string ShowOpen(string filter, string initDir = "", string title = "");
      string ShowSave(string filter, string initDir = "", string title = "", string fileName = "");
      string ShowFolder(string initDir = "");
    
      bool ShowSettings();
    }
    
  6. Предоставьте открытое статическое свойство типа IDialogService в вашем ViewModelLocator, но оставьте часть регистрации для слоя View для выполнения. Это ключ.

    public static IDialogService DialogService => SimpleIoc.Default.GetInstance<IDialogService>();
    
  7. Добавьте реализацию этого интерфейса в проект приложения.

    public class DialogPresenter : IDialogService
    {
        private static OpenFileDialog dlgOpen = new OpenFileDialog();
        private static SaveFileDialog dlgSave = new SaveFileDialog();
        private static FolderBrowserDialog dlgFolder = new FolderBrowserDialog();
    
        /// <summary>
        /// Displays a simple Information or Error message to the user.
        /// </summary>
        /// <param name="msg">String text that is to be displayed in the MessageBox</param>
        /// <param name="isError">If true, Error icon is displayed. If false, Information icon is displayed.</param>
        public void ShowMessage(string msg, bool isError)
        {
                if(isError)
                        System.Windows.MessageBox.Show(msg, "Your Project Title", MessageBoxButton.OK, MessageBoxImage.Error);
                else
                        System.Windows.MessageBox.Show(msg, "Your Project Title", MessageBoxButton.OK, MessageBoxImage.Information);
        }
    
        /// <summary>
        /// Displays a Yes/No MessageBox.Returns true if user clicks Yes, otherwise false.
        /// </summary>
        /// <param name="msg"></param>
        /// <returns></returns>
        public bool AskBooleanQuestion(string msg)
        {
                var Result = System.Windows.MessageBox.Show(msg, "Your Project Title", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes;
                return Result;
        }
    
        /// <summary>
        /// Displays Save dialog. User can specify file filter, initial directory and dialog title. Returns full path of the selected file if
        /// user clicks Save button. Returns null if user clicks Cancel button.
        /// </summary>
        /// <param name="filter"></param>
        /// <param name="initDir"></param>
        /// <param name="title"></param>
        /// <param name="fileName"></param>
        /// <returns></returns>
        public string ShowSave(string filter, string initDir = "", string title = "", string fileName = "")
        {
                if (!string.IsNullOrEmpty(title))
                        dlgSave.Title = title;
                else
                        dlgSave.Title = "Save";
    
                if (!string.IsNullOrEmpty(fileName))
                        dlgSave.FileName = fileName;
                else
                        dlgSave.FileName = "";
    
                dlgSave.Filter = filter;
                if (!string.IsNullOrEmpty(initDir))
                        dlgSave.InitialDirectory = initDir;
    
                if (dlgSave.ShowDialog() == DialogResult.OK)
                        return dlgSave.FileName;
                else
                        return null;
        }
    
    
        public string ShowFolder(string initDir = "")
        {
                if (!string.IsNullOrEmpty(initDir))
                        dlgFolder.SelectedPath = initDir;
    
                if (dlgFolder.ShowDialog() == DialogResult.OK)
                        return dlgFolder.SelectedPath;
                else
                        return null;
        }
    
    
        /// <summary>
        /// Displays Open dialog. User can specify file filter, initial directory and dialog title. Returns full path of the selected file if
        /// user clicks Open button. Returns null if user clicks Cancel button.
        /// </summary>
        /// <param name="filter"></param>
        /// <param name="initDir"></param>
        /// <param name="title"></param>
        /// <returns></returns>
        public string ShowOpen(string filter, string initDir = "", string title = "")
        {
                if (!string.IsNullOrEmpty(title))
                        dlgOpen.Title = title;
                else
                        dlgOpen.Title = "Open";
    
                dlgOpen.Multiselect = false;
                dlgOpen.Filter = filter;
                if (!string.IsNullOrEmpty(initDir))
                        dlgOpen.InitialDirectory = initDir;
    
                if (dlgOpen.ShowDialog() == DialogResult.OK)
                        return dlgOpen.FileName;
                else
                        return null;
        }
    
        /// <summary>
        /// Shows Settings dialog.
        /// </summary>
        /// <returns>true if User clicks OK button, otherwise false.</returns>
        public bool ShowSettings()
        {
                var w = new SettingsWindow();
                MakeChild(w); //Show this dialog as child of Microsoft Word window.
                var Result = w.ShowDialog().Value;
                return Result;
        }
    
        /// <summary>
        /// Prompts user for a single value input. First parameter specifies the message to be displayed in the dialog 
        /// and the second string specifies the default value to be displayed in the input box.
        /// </summary>
        /// <param name="m"></param>
        public string AskStringQuestion(string msg, string default_value)
        {
                string Result = null;
    
                InputBox w = new InputBox();
                MakeChild(w);
                if (w.ShowDialog(msg, default_value).Value)
                        Result = w.Value;
    
                return Result;
        }
    
        /// <summary>
        /// Sets Word window as parent of the specified window.
        /// </summary>
        /// <param name="w"></param>
        private static void MakeChild(System.Windows.Window w)
        {
                IntPtr HWND = Process.GetCurrentProcess().MainWindowHandle;
                var helper = new WindowInteropHelper(w) { Owner = HWND };
        }
    }
    
  8. Хотя некоторые из этих функций являются общими (ShowMessage, AskBooleanQuestion и т.д.), Другие специфичны для этого проекта и используют пользовательские Window. Вы можете добавить больше пользовательских окон таким же образом. Ключ заключается в том, чтобы сохранить специфичные для пользовательского интерфейса элементы на уровне просмотра и просто предоставить возвращенные данные с помощью POCO на уровне виртуальной машины.
  9. Выполните IoC-регистрацию вашего интерфейса в слое View, используя этот класс. Вы можете сделать это в конструкторе основного представления (после вызова InitializeComponent()):

    SimpleIoc.Default.Register<IDialogService, DialogPresenter>();
    
  10. Вот и ты. Теперь у вас есть доступ ко всем функциям диалогов на слоях VM и View. Ваш уровень VM может вызывать эти функции следующим образом:

    var NoTrump = ViewModelLocator.DialogService.AskBooleanQuestion("Really stop the trade war???", "");
    
  11. Так чисто вы видите. Уровень VM не знает ничего о том, как вопрос "да/нет" будет представлен пользователю уровнем UI, и все еще может успешно работать с возвращенным результатом из диалога.

Другие бесплатные льготы

  1. Для написания модульного теста вы можете предоставить пользовательскую реализацию IDialogService в вашем тестовом проекте и зарегистрировать этот класс в IoC в конструкторе вашего тестового класса.
  2. Вам нужно будет импортировать некоторые пространства имен, такие как Microsoft.Win32 чтобы получить доступ к диалогам открытия и сохранения. Я не учел их, потому что есть также версия этих диалоговых окон для WinForms, плюс кто-то может захотеть создать свою собственную версию. Также обратите внимание, что некоторые из идентификаторов, используемых в DialogPresenter являются именами моих собственных окон (например, SettingsWindow). Вам нужно будет либо удалить их как из интерфейса, так и из реализации или предоставить свои собственные окна.
  3. Если ваша виртуальная машина выполняет многопоточность, вызовите MVVM Light DispatcherHelper.Initialize() начале жизненного цикла приложения.
  4. За исключением DialogPresenter который внедряется в слой View, другие ViewModals должны быть зарегистрированы в ViewModelLocator а затем открытое статическое свойство этого типа должно быть открыто для использования слоем View. Что-то вроде этого:

    public static SettingsVM Settings => SimpleIoc.Default.GetInstance<SettingsVM>();
    
  5. По большей части ваши диалоги не должны иметь никакого кода для таких вещей, как привязка или установка DataContext и т.д. Вы даже не должны передавать вещи в качестве параметров конструктора. XAML может сделать все это для вас, вот так:

    <Window x:Class="YourViewNamespace.SettingsWindow"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:local="clr-namespace:YourViewProject"
      xmlns:vm="clr-namespace:YourVMProject;assembly=YourVMProject"
      DataContext="{x:Static vm:ViewModelLocator.Settings}"
      d:DataContext="{d:DesignInstance Type=vm:SettingsVM}" />
    
  6. Установка DataContext таким образом дает вам все преимущества времени разработки, такие как Intellisense и автозаполнение.

Надеюсь, что это помогает всем.

Ответ 24

EDIT: да, я согласен, что это не правильный подход MVVM, и теперь я использую нечто похожее на то, что предлагает blindmeis.

Один из способов сделать это:

В модели основного вида (где вы открываете модальный):

void OpenModal()
{
    ModalWindowViewModel mwvm = new ModalWindowViewModel();
    Window mw = new Window();
    mw.content = mwvm;
    mw.ShowDialog()
    if(mw.DialogResult == true)
    { 
        // Your Code, you can access property in mwvm if you need.
    }
}

И в вашем Modal Window View/ViewModel:

XAML:

<Button Name="okButton" Command="{Binding OkCommand}" CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}">OK</Button>
<Button Margin="2" VerticalAlignment="Center" Name="cancelButton" IsCancel="True">Cancel</Button>

ViewModel:

public ICommand OkCommand
{
    get
    {
        if (_okCommand == null)
        {
            _okCommand = new ActionCommand<Window>(DoOk, CanDoOk);
        }
        return _okCommand ;
    }
}

void DoOk(Window win)
{
    <!--Your Code-->
    win.DialogResult = true;
    win.Close();
}

bool CanDoOk(Window win) { return true; }

или аналогично тому, что размещено здесь WPF MVVM: как закрыть окно