Обработка диалогов в WPF с помощью MVVM
В шаблоне MVVM для WPF обработка диалогов является одной из более сложных операций. Так как ваша модель представления ничего не знает о представлении, диалоговое общение может быть интересным. Я могу выставить ICommand, чтобы, когда представление вызывает его, диалоговое окно могло появиться.
Кто-нибудь знает хороший способ обработки результатов из диалогов? Я говорю о диалоговых окнах, таких как MessageBox.
Одним из способов сделать это было событие в модели представления, на которое представление будет подписываться, когда потребуется диалоговое окно.
public event EventHandler<MyDeleteArgs> RequiresDeleteDialog;
Это нормально, но это означает, что для представления требуется код, от которого я хотел бы остаться в стороне.
26 ответов
Я предлагаю отказаться от модальных диалогов 1990-х годов и вместо этого реализовать элемент управления в качестве наложения (холст + абсолютное позиционирование) с видимостью, связанной с логическим значением обратно в ВМ. Ближе к контролю типа AJAX.
Это очень полезно:
<BooleanToVisibilityConverter x:Key="booltoVis" />
как в:
<my:ErrorControl Visibility="{Binding Path=ThereWasAnError, Mode=TwoWay, Converter={StaticResource booltoVis}, UpdateSourceTrigger=PropertyChanged}"/>
Вот как я реализовал один пользовательский элемент управления. Нажатие на "х" закрывает элемент управления в строке кода в коде пользовательского элемента управления. (Поскольку у меня есть Views в.exe и ViewModels в dll, я не чувствую себя плохо в коде, который манипулирует UI.)
Вы должны использовать посредника для этого. Посредник - это распространенный шаблон проектирования, также известный как Messenger в некоторых его реализациях. Это парадигма типа Register / Notify, которая позволяет вашим ViewModel и Views взаимодействовать через механизм обмена сообщениями с низкой связью.
Вы должны проверить группу учеников Google WPF и просто искать посредника. Вы будете очень довольны ответами...
Однако вы можете начать с этого:
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
Хороший диалог 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" />
Вы, вероятно, хотите добавить стиль, который устанавливает Показ. Я объясняю это в моей статье. Я надеюсь, это поможет вам.
Я использую этот подход для диалогов с MVVM.
Все, что мне нужно сделать сейчас, это вызвать следующее из моей модели представления.
var result = this.uiDialogService.ShowDialog("Dialogwindow title goes here", dialogwindowVM);
Мое текущее решение решает большинство проблем, о которых вы упомянули, но оно полностью абстрагировано от платформы и может быть использовано повторно. Кроме того, я не использовал связывание кода только с DelegateCommands, которые реализуют ICommand. Диалог - это, по сути, View - отдельный элемент управления, имеющий собственный ViewModel, который отображается из ViewModel основного экрана, но запускается из пользовательского интерфейса через привязку DelagateCommand.
Смотрите полное решение Silverlight 4 здесь Модальные диалоги с MVVM и Silverlight 4
Я действительно боролся с этой концепцией некоторое время, изучая (все еще изучая) MVVM. То, что я решил, и то, что я думаю, что другие уже решили, но что мне было непонятно, таково:
Моя первоначальная мысль заключалась в том, что ViewModel нельзя разрешать вызывать диалоговое окно напрямую, так как он не решает, как должен выглядеть диалог. Из-за этого я начал думать о том, как я мог бы передавать сообщения так же, как и в MVP (т.е. View.ShowSaveFileDialog()). Однако я считаю, что это неправильный подход.
Это нормально для ViewModel для непосредственного вызова диалога. Тем не менее, когда вы тестируете ViewModel, это означает, что диалоговое окно будет либо всплывать во время вашего теста, либо потерпеть неудачу все вместе (на самом деле никогда не пробовал)
Итак, во время тестирования необходимо использовать "тестовую" версию вашего диалога. Это означает, что для любого имеющегося у вас диалога вам нужно создать интерфейс и либо смоделировать ответ диалога, либо создать тестовый макет, который будет иметь поведение по умолчанию.
Вы уже должны использовать какой-либо Service Locator или IoC, который вы можете настроить, чтобы предоставить вам правильную версию в зависимости от контекста.
Используя этот подход, ваша ViewModel все еще тестируема, и в зависимости от того, как вы макетируете свои диалоги, вы можете контролировать поведение.
Надеюсь это поможет.
Есть два хороших способа сделать это: 1) диалоговая служба (простая, чистая) и 2) просмотр с помощью. View Assisted предоставляет некоторые полезные функции, но, как правило, не стоит.
ДИАЛОГ СЕРВИС
a) интерфейс службы диалога, например, через конструктор или некоторый контейнер зависимостей:
interface IDialogService
{
Task ShowDialogAsync(DialogViewModel dlgVm);
}
б) Ваша реализация IDialogService должна открыть окно (или внедрить некоторый элемент управления в активное окно), создать представление, соответствующее имени данного типа dlgVm (используйте регистрацию контейнера или соглашение или ContentPresenter с типом, связанным с DataTemplates). ShowDialogAsync должен создать TaskCompletionSource и вернуть его.Task proptery. Класс DialogViewModel сам нуждается в событии, которое вы можете вызвать в производном классе, когда хотите закрыть, и посмотреть в диалоговом окне, чтобы фактически закрыть / скрыть диалоговое окно и завершить TaskCompletionSource.
б) Для использования просто вызовите await this.DialogService.ShowDialog(myDlgVm) в вашем экземпляре некоторого производного от DialogViewModel класса. После возвращения await посмотрите на свойства, которые вы добавили в диалоговую виртуальную машину, чтобы определить, что произошло; вам даже не нужен обратный звонок.
ВИД ПОМОЩЬ
Это ваш взгляд на прослушивание события на модели представления. Все это может быть заключено в Blend Behavior, чтобы избежать выделения кода и использования ресурсов, если вы так склонны (FMI, подкласс класса "Behavior", чтобы увидеть своего рода Blendable присоединенное свойство на стероидах). Сейчас мы сделаем это вручную для каждого вида:
a) Создайте OpenXXXXXDialogEvent с пользовательской полезной нагрузкой (производный класс DialogViewModel).
б) иметь представление подписаться на событие в своем событии OnDataContextChanged. Не забудьте скрыть и отписаться, если старое значение!= Null и в событии Unloaded окна.
c) Когда событие запускается, откройте представление, которое может находиться в ресурсе на вашей странице, или вы можете найти его в соответствии с соглашением в другом месте (как в подходе службы диалога).
Этот подход является более гибким, но требует больше работы для использования. Я не использую это много. Одним приятным преимуществом является, например, возможность размещать представление физически внутри вкладки. Я использовал алгоритм, чтобы поместить его в границы текущего пользовательского элемента управления или, если он недостаточно велик, перемещаться по визуальному дереву, пока не будет найден достаточно большой контейнер.
Это позволяет диалоговым окнам быть ближе к месту, где они фактически используются, только затемняет часть приложения, связанную с текущей активностью, и позволяет пользователю перемещаться внутри приложения без необходимости вручную отталкивать диалоговые окна, даже иметь несколько квази-приложений. модальные диалоги открываются на разных вкладках или под-представлениях.
Используйте команду 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();
}
}
Интересной альтернативой является использование контроллеров, которые отвечают за отображение представлений (диалогов).
Как это работает, показано в WPF Application Framework (WAF).
Я думаю, что обработка диалога должна быть обязанностью представления, и представление должно иметь код для поддержки этого.
Если вы измените взаимодействие ViewModel - View для обработки диалогов, тогда ViewModel зависит от этой реализации. Самый простой способ справиться с этой проблемой - сделать View ответственным за выполнение задачи. Если это означает показ диалога, тогда все в порядке, но также может быть сообщение о состоянии в строке состояния и т. Д.
Моя точка зрения заключается в том, что весь смысл шаблона MVVM состоит в том, чтобы отделить бизнес-логику от GUI, поэтому не следует смешивать логику GUI (для отображения диалога) на бизнес-уровне (ViewModel).
Я реализовал Поведение, которое слушает Сообщение от ViewModel. Он основан на решении Laurent Bugnion, но, поскольку он не использует код и его можно использовать повторно, я думаю, что он более элегантный.
Как заставить WPF вести себя так, как будто MVVM поддерживается из коробки
Почему бы просто не вызвать событие в виртуальной машине и подписаться на событие в представлении? Это сохранит логику приложения и представление отдельно и все же позволит вам использовать дочернее окно для диалогов.
Я думаю, что представление может иметь код для обработки события из модели представления.
В зависимости от события / сценария, он также может иметь триггер события, который подписывается на просмотр событий модели, и одно или несколько действий для вызова в ответ.
У меня была такая же ситуация и я обернул MessageBox в невидимый элемент управления дизайнера. Подробности в моем блоге
http://geekswithblogs.net/mukapu/archive/2010/03/12/user-prompts-messagebox-with-mvvm.aspx
То же самое можно распространить на любые модальные диалоги, управление просмотром файлов и т. Д.
Стандартный подход
Потратив годы на решение этой проблемы в WPF, я наконец понял стандартный способ реализации диалогов в WPF. Вот преимущества этого подхода:
- ЧИСТЫЙ
- Не нарушает шаблон проектирования MVVM
- ViewModal никогда не ссылается ни на одну из библиотек пользовательского интерфейса (WindowBase, PresentationFramework и т. Д.)
- Идеально подходит для автоматизированного тестирования
- Диалоги можно легко заменить.
Так в чем же ключ. Это DI + IoC.
Вот как это работает. Я использую MVVM Light, но этот подход можно распространить и на другие фреймворки:
- Добавьте в решение проект приложения WPF. Назовите это приложением.
- Добавьте библиотеку классов ViewModal. Назовите это ВМ.
- Приложение ссылается на проект виртуальной машины. Проект виртуальной машины ничего не знает о приложении.
- Добавьте ссылку NuGet на MVVM Light в оба проекта. В настоящее время я использую MVVM Light Standard, но вас тоже устраивает полная версия Framework.
Добавьте интерфейс IDialogService в проект ВМ:
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(); }
Выставить общедоступное статическое свойство
IDialogService
введите свойViewModelLocator
, но оставьте часть регистрации для выполнения слоя View. Это ключ.:public static IDialogService DialogService => SimpleIoc.Default.GetInstance<IDialogService>();
Добавьте реализацию этого интерфейса в проект приложения.
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 }; } }
- Хотя некоторые из этих функций являются общими (
ShowMessage
,AskBooleanQuestion
и т. д.), другие относятся к этому проекту и используют индивидуальныеWindow
с. Таким же образом вы можете добавить дополнительные окна. Ключ состоит в том, чтобы сохранить элементы, специфичные для пользовательского интерфейса, на уровне представления и просто предоставлять возвращаемые данные с помощью POCO на уровне виртуальной машины. Выполните регистрацию IoC вашего интерфейса на слое просмотра с помощью этого класса. Вы можете сделать это в конструкторе основного представления (после
InitializeComponent()
вызов):SimpleIoc.Default.Register<IDialogService, DialogPresenter>();
Вот так. Теперь у вас есть доступ ко всем функциям диалогов как на уровне виртуальной машины, так и на уровне просмотра. Уровень виртуальной машины может вызывать эти функции следующим образом:
var NoTrump = ViewModelLocator.DialogService.AskBooleanQuestion("Really stop the trade war???", "");
- Так чисто, видите ли. Слой виртуальной машины ничего не знает о том, как вопрос "Да / Нет" будет представлен пользователю уровнем пользовательского интерфейса, и может успешно работать с результатом, возвращаемым из диалогового окна.
Другие бесплатные льготы
- Для написания модульного теста вы можете предоставить собственную реализацию
IDialogService
в вашем тестовом проекте и зарегистрируйте этот класс в IoC в конструкторе вашего тестового класса. - Вам нужно будет импортировать некоторые пространства имен, такие как
Microsoft.Win32
для доступа к диалогам открытия и сохранения. Я оставил их, потому что есть также версия этих диалогов WinForms, плюс кто-то может захотеть создать свою собственную версию. Также обратите внимание, что некоторые идентификаторы, используемые вDialogPresenter
это имена моих собственных окон (например,SettingsWindow
). Вам нужно будет либо удалить их как из интерфейса, так и из реализации, либо предоставить свои собственные окна. - Если ваша виртуальная машина выполняет многопоточность, вызовите MVVM Light's
DispatcherHelper.Initialize()
в начале жизненного цикла вашего приложения. Кроме
DialogPresenter
который вводится в слой просмотра, другие ViewModals должны быть зарегистрированы вViewModelLocator
а затем публичное статическое свойство этого типа должно быть предоставлено для использования слоем представления. Что-то вроде этого:public static SettingsVM Settings => SimpleIoc.Default.GetInstance<SettingsVM>();
По большей части в ваших диалогах не должно быть кода программной части для таких вещей, как привязка или установка 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}" />
- Настройка
DataContext
таким образом вы получаете все виды преимуществ во время разработки, такие как Intellisense и автозаполнение.
Надеюсь, это поможет всем.
Я написал довольно обширную статью на эту тему, а также разработал всплывающую библиотеку для MVVM Dialogs. Строгое соблюдение MVVM не только возможно, но и очень чисто при правильной реализации, и его можно легко распространить на сторонние библиотеки, которые сами этого не придерживаются:
https://www.codeproject.com/Articles/820324/Implementing-Dialog-Boxes-in-MVVM
Потратив некоторое время с этим, я наконец нашел следующее решение. Несколько ключевых преимуществ этого подхода:
- Реализует собственный MVVM Light
IDialogService
, - Представлению не нужно добавлять ссылку на MVVM Light.
- ВМ не нужно выполнять какие-либо действия на уровне презентации. Даже не нужно
PresentationFramework
ссылка. - Использует собственный канал Messenger MVVM Light, поэтому уровни представления и виртуальной машины не связаны между собой.
- Поддерживает диалоги с возвращаемым значением, такие как "Да / Нет вопросов" или "ОК / Отмена".
- Поддерживает пользовательские диалоги.
Вот реализация 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
});
}
}
}
Вот слой представления (входит в проект View)
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;
}
}
}
Я знаю, что это старый вопрос, но когда я выполнил этот поиск, я нашел много связанных вопросов, но я не нашел действительно четкого ответа. Поэтому я делаю свою собственную реализацию диалогового окна / сообщения / попина и делюсь этим!
Я думаю, что это "доказательство MVVM", и я пытаюсь сделать его простым и корректным, но я новичок в 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 */ });
И это показывает такие вещи:
Карл Шиффлетт создал образец приложения для отображения диалоговых окон, используя сервисный подход и подход Prism InteractionRequest.
Мне нравится сервисный подход - он менее гибок, поэтому у пользователей меньше шансов что-то сломать:) Он также согласуется с частью моего приложения в WinForms (MessageBox.Show). Но если вы планируете показывать много разных диалогов, тогда InteractionRequest лучший способ пойти.
http://karlshifflett.wordpress.com/2010/11/07/in-the-box-ndash-mvvm-training/
Я свернул свой собственный загрузчик окон, описанный в ответе на этот вопрос:
Самый простой способ: используйте библиотеку HanumanInstitute.MvvmDialogs.
Если вы будете следовать документации, вы сможете использовать ее так же красиво, с полной отвязкой от пользовательского интерфейса.
var presetName = await dialogService.ShowSavePresetViewAsync(this);
Я размышлял над подобной проблемой, когда спрашивал, как должна выглядеть модель представления для задачи или диалога.
Мое текущее решение выглядит так:
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()
функция по выбору пользователя.
Я, возможно, придерживаюсь крайне непопулярного мнения, что полагаться исключительно на XAML, по вашему мнению, строго ограничивает. Я считаю, что мы можем лучше разделить View и ViewModel, используя выделенный код для представления, чтобы помочь нам реализовать представление определенного обмена сообщениями или сбора данных. Позволяя ViewModel выполнять свою собственную логику и возвращать результаты таких, как свойства или вызывая события, мы можем спроектировать представление так, чтобы оно выполняло практически все, что мы хотим (включая диалоговые окна).
В одном конкретном случае мне понадобился диалог ввода имени, который просто берет имя семейства и устанавливает свойство в ViewModel для последующего использования. Из-за большого количества информации, которую мне нужно было собрать, наличие диалогов для отслеживания таких вещей, как имена, упростило пользовательский опыт настолько, что он не выглядел "подавляющим".
Зная это, я использовал событие нажатия на конкретную кнопку, чтобы вызвать новый пользовательский диалог, который я разработал для этой цели. При вызове диалога я передавал DataContext родительского окна как объект в конструкторе диалога. Затем я устанавливаю DataContext диалога через этот параметр.
public NameEntryDialog(object dataContext)
{
InitializeComponent();
DataContext = dataContext;
}
Теперь, когда у двух представлений был общий DataContext, я смог установить любое свойство в ViewModel, привязав его к узлам моего диалога. Это не требует каких-либо серьезных обходных путей или лазеек для правильного функционирования.
Аналогичным образом, после проверки данных в ViewModel (без специальных символов или имен из одного символа и т. Д.) Я установил ViewModel два свойства: 1) свойство validationSuccessful и 2) свойство ErrorMessage.
Как только диалоговое окно было закрыто, мой программный код просто проверил эти свойства и вызвал MessageBox, если проверка не прошла успешно (отображение сообщения об ошибке, установленного в свойстве моей ViewModel). Вы также можете сделать это, подписавшись на события в вашей ViewModel, которые при запуске могут вызывать диалог или MessageBox в вашем представлении. У меня также была логика ViewModel, чтобы свойство CanExecute последующей команды "сохранить" "ложь", чтобы пользователь не мог продолжить, пока эти ошибки (и другие во время сбора данных) не были исправлены.
Используя выделенный код, я смог полностью отделить View от ViewModel, а также мог выполнять тестирование без необходимости представления вообще.
Это также позволяет мне создавать новые представления, которые могут взаимодействовать с теми же свойствами в ViewModel, независимо от необходимости в диалоге. Я мог бы использовать UserControl, диалоговое окно, одно окно и т. Д. Пока я обращаюсь ко всем свойствам, представленным в моей ViewModel, небо - это предел. Одна вещь, которую я обнаружил, чтобы быть правдой почти в каждой запутанной попытке справиться с этой проблемой, заключалась в том, что диалоговое окно в конечном итоге называется КУДА-ТО (где-то сильный акцент) в коде. Почему бы просто не пропустить все эти проблемы и сделать это прямо в вашем представлении?
Я боролся с той же проблемой. Я придумал способ связи между View и ViewModel. Вы можете инициировать отправку сообщения из ViewModel в View, чтобы показать ему окно сообщения, и оно сообщит результат. Затем ViewModel может ответить на результат, возвращенный из View.
Я демонстрирую это в своем блоге:
Извините, но я должен вмешаться. Я прошел через несколько из предложенных решений, прежде чем найти пространство имен Prism.Wpf.Interactivity в проекте Prism. Вы можете использовать запросы взаимодействия и действие всплывающего окна, чтобы либо свернуть пользовательское окно, либо для более простых нужд есть встроенные всплывающие окна уведомлений и подтверждений. Они создают настоящие окна и управляются как таковые. Вы можете передать объект контекста с любыми зависимостями, которые вам нужны в диалоге. Мы используем это решение на моей работе, так как я нашел его. У нас здесь много старших разработчиков, и никто не придумал ничего лучшего. Нашим предыдущим решением было использование диалогового сервиса в оверлее и использование класса презентатора, чтобы это произошло, но вам нужно было иметь фабрики для всех моделей представления диалога и т. Д.
Это не тривиально, но и не супер сложно. И он встроен в Призму и поэтому является лучшей (или лучшей) практикой ИМХО.
Мои 2 цента!
РЕДАКТИРОВАТЬ: да, я согласен, что это не правильный подход MVVM, и сейчас я использую что-то похожее на то, что предлагает вслепую.
Один из способов, которым вы могли бы это
В вашей модели основного вида (где вы открываете модальный):
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.
}
}
И в вашем модальном окне 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: как закрыть окно