Reputation: 29991
In the MVVM pattern for WPF, handling dialogs is one of the more complex operations. As your view model does not know anything about the view, dialog communication can be interesting. I can expose an ICommand
that when the view invokes it, a dialog can appear.
Does anyone know of a good way to handle results from dialogs? I am speaking about windows dialogs such as MessageBox
.
One of the ways we did this was have an event on the viewmodel that the view would subscribe to when a dialog was required.
public event EventHandler<MyDeleteArgs> RequiresDeleteDialog;
This is OK, but it means that the view requires code which is something I would like to stay away from.
Upvotes: 253
Views: 133480
Reputation: 3520
Dialogs are probably the most challenging topic to tackle in a pure MVVM way. While some argue that all logic for displaying and handling dialogs by definition is UI-specific and thus belongs in the view, with a bit of scaffolding we can actually make a dialog framework where the ultimate logic of displaying the dialog and processing its results is still driven by the view model, all without application-specific code-behind or even dependency injection.
Before I show the implementation, I'm going to first show an example of how an MVVM-friendly dialog framework would actually be consumed, so that the reader can see that the end result does, in fact, allow you to stay faithful to the design pattern using very simple application code. Following that I'll demonstrate the underlying implementation.
First, the example:
Platform-Independent Application Model and View Model:
public class MainViewModel : ViewModelBase
{
public DialogManagerViewModel DialogManager { get; } =
new DialogManagerViewModel();
#region ICommand PickFile Command
private Command _PickFileCommand;
public ICommand PickFileCommand
{
get
{
return _PickFileCommand ?? (_PickFileCommand = new Command(
async () =>
{
var fileDialog = new FileOpenDialogViewModel();
var result = await DialogManager.ShowDialogAsync(fileDialog);
if (result == null)
// cancel
return;
using (var stream = File.OpenRead(result))
{
// doo something with the file
}
}));
}
}
#endregion
#region ICommand NewPerson Command
private Command _NewPersonCommand;
public ICommand NewPersonCommand
{
get
{
return _NewPersonCommand ?? (_NewPersonCommand = new Command(
async () =>
{
var newPerson = new Person();
var personDialog = new PersonDialogViewModel(newPerson);
if (!await DialogManager.ShowDialogAsync(personDialog))
return;
// Do something with the now-populated newPerson object
}));
}
}
#endregion
}
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
public class PersonDialogViewModel : CustomDialogViewModel
{
private Person _person;
public PersonDialogViewModel(Person person)
{
_person = person;
}
public override string Title => "Person";
#region string FirstName property
private string _FirstName;
public string FirstName
{
get
{
return _FirstName;
}
set
{
if (_FirstName == value)
return;
_FirstName = value;
OnPropertyChanged();
}
}
#endregion
#region string LastName property
private string _LastName;
public string LastName
{
get
{
return _LastName;
}
set
{
if (_LastName == value)
return;
_LastName = value;
OnPropertyChanged();
}
}
#endregion
protected override async Task<bool> OnOkAsync()
{
this._person.FirstName = this.FirstName;
this._person.LastName = this.LastName;
return true;
}
}
Platform-Dependent Application View
<StackPanel Orientation="Vertical"
local:DialogManager.ShowDialog="{Binding DialogManager.CurrentDialog}">
<StackPanel.Resources>
<DataTemplate x:Key="{x:Type local:PersonDialogViewModel}"
DataType="{x:Type local:PersonDialogViewModel}">
<StackPanel>
<Label Target="{Binding ElementName=FirstName}"
Content="_First:" />
<TextBox x:Name="FirstName"
TabIndex="0"
Text="{Binding FirstName}" />
<Label Target="{Binding ElementName=LastName}"
Content="_Last:" />
<TextBox x:Name="LastName"
TabIndex="1"
Text="{Binding LastName}" />
</StackPanel>
</DataTemplate>
</StackPanel.Resources>
<StackPanel.DataContext>
<local:MainViewModel />
</StackPanel.DataContext>
<Button Command="{Binding PickFileCommand}">Open File</Button>
<Button Command="{Binding NewPersonCommand}">New Person</Button>
</StackPanel>
That is the sum total of code we have to write to open a file with a standard Windows file dialog and get the chosen result, or to make a custom dialog box with our own DataTemplate
. Note how: (1) there is no code-behind involved; and (2) the model and view model have no view or other WPF dependencies, injected or otherwise.
If you believe, philosophically, that view models shouldn't be opening dialogs, well then we have nothing else to discuss. :) Otherwise, if you want a way of controlling dialogs and obtaining data from them using only abstract view models and XAML, it's hard to imagine an easier approach than this. So let's look at how it's done, because what follows is our "helper library" which, remember, is reusable in all future applications:
Core Platform- and Application-Independent (Helper Library) Components
public enum DialogType
{
StandardFileOpen,
// others...
// For non-standard dialogs, where a view template will be needed
Custom
}
// A singleton instance of this class should exist
// somewhere in the view model layer, accessible to all.
public class DialogManagerViewModel : ViewModelBase
{
#region DialogViewModel CurrentDialog property
private DialogViewModel _currentDialog;
// This will be bound to the GUI-specific DialogManager
// through a bindable property.
public DialogViewModel CurrentDialog
{
get
{
return _currentDialog;
}
private set
{
if (_currentDialog == value)
return;
_currentDialog = value;
OnPropertyChanged();
}
}
#endregion
// Returns the result of the dialog depending on the type.
// For preset types like FileOpenDialogViewModel, the return
// value will be the file path string, or null if cancelled.
// For CustomDialogViewModel-derived dialogs, it will be
// true if OK or false if cancelled; the more detailed result
// would then be obtained from the other properties of the
// view model.
public async Task<T> ShowDialogAsync<T>(DialogViewModel<T> dialog)
{
if (this.CurrentDialog != null)
// Support for multiple open dialogs could be achieved
// by using an ObservableCollection of DialogViewModels
// instead of a single property, but I'll leave it to
// the reader to envision such a solution.
throw new InvalidOperationException(
$"Only one dialog may be open at a time.");
this.CurrentDialog = dialog;
var result = await dialog.DialogResult;
this.CurrentDialog = null;
return (result is T tresult) ? tresult : default;
}
}
public abstract class DialogViewModel : ViewModelBase
{
private TaskCompletionSource<object> _dialogResult = new TaskCompletionSource<object>();
public abstract DialogType DialogType { get; }
public async Task<bool> SetResultAsync(object result)
{
var confirmClose = await OnClosingAsync(result);
if (!confirmClose)
return false;
_dialogResult.SetResult(result);
return true;
}
// Only allow CustomDialogViewModel to be subclassed externally
internal DialogViewModel()
{
}
internal Task<object> DialogResult => _dialogResult.Task;
internal virtual Task<bool> OnClosingAsync(object result)
{
// Override for custom behavior, return false
// if dialog close should be cancelled due to
// invalidity or some other error.
return Task.FromResult(true);
}
}
public abstract class DialogViewModel<T> : DialogViewModel
{
}
public class FileOpenDialogViewModel : DialogViewModel<string>
{
public sealed override DialogType DialogType =>
DialogType.StandardFileOpen;
}
public class CustomDialogViewModel : DialogViewModel<bool>
{
// So external code can subclass
public CustomDialogViewModel() : base()
{
}
public sealed override DialogType DialogType => DialogType.Custom;
public virtual string Title { get; }
protected virtual Task<bool> OnOkAsync()
{
// Override for custom validation or submission logic;
// return false if submission and close should be
// cancelled
return Task.FromResult(true);
}
internal sealed override async Task<bool> OnClosingAsync(object result)
{
if (result is bool b && b)
return await OnOkAsync();
return await base.OnClosingAsync(result);
}
}
WPF Application-Independent (Helper Library) Components
C#:
public static class DialogManager
{
#region DialogViewModel ShowDialog dependency property
public static readonly DependencyProperty ShowDialogProperty =
DependencyProperty.RegisterAttached(
"ShowDialog",
typeof(DialogViewModel),
typeof(DialogManager),
new PropertyMetadata(OnShowDialogChanged));
public static DialogViewModel GetShowDialog(DependencyObject obj)
{
return (DialogViewModel)obj.GetValue(ShowDialogProperty);
}
public static void SetShowDialog(DependencyObject obj, DialogViewModel value)
{
obj.SetValue(ShowDialogProperty, value);
}
private static async void OnShowDialogChanged(
DependencyObject sender,
DependencyPropertyChangedEventArgs args)
{
if (!(args.NewValue is DialogViewModel dialog) ||
!(sender is FrameworkElement fe))
return;
// Ensures that the open is executed asynchronously so that,
// if for some reason the initiator didn't want to await the result,
// control would return to them before the modal is actually opened.
await Task.Yield();
switch (dialog.DialogType)
{
case DialogType.StandardFileOpen:
OpenFileDialog openDlg = new OpenFileDialog();
var result = openDlg.ShowDialog();
await dialog.SetResultAsync(result == true
? openDlg.FileName
: null);
break;
case DialogType.Custom:
ShowCustomDialog(fe, dialog);
break;
}
}
#endregion
private static void ShowCustomDialog(FrameworkElement sender, DialogViewModel viewModel)
{
var template = sender.TryFindResource(viewModel.GetType()) as DataTemplate;
if (template == null)
throw new Exception(
$"Dialog template not found for {viewModel.GetType().Name}; " +
$"A DataTemplate resource keyed with the {viewModel.GetType().Name} type must be within " +
$"the resource scope of the FrameworkElement to which the ShowDialog property is attached.");
var dlgWindow = new DialogWindow
{
Title = (viewModel as CustomDialogViewModel)?.Title ?? string.Empty,
Content = viewModel,
ContentTemplate = template,
};
var result = dlgWindow.ShowDialog();
}
}
public class DialogWindow : Window
{
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
PART_OkButton = GetTemplateChild(nameof(PART_OkButton)) as Button;
if (PART_OkButton != null)
PART_OkButton.Click += this.OnOk;
PART_CancelButton = GetTemplateChild(nameof(PART_CancelButton)) as Button;
if ( PART_CancelButton != null)
PART_CancelButton.Click += this.OnCancel;
}
protected override void OnClosing(CancelEventArgs e)
{
base.OnClosing(e);
if (_isComplete)
return;
this.DialogResult = false;
_ = (this.Content as DialogViewModel)?.SetResultAsync(false);
}
private async void OnOk(object sender, RoutedEventArgs e)
{
if (this.Content is DialogViewModel dvm)
{
PART_OkButton.IsEnabled = false;
try
{
var confirmClose = await dvm.SetResultAsync(true);
if (confirmClose != true)
return;
}
finally
{
PART_OkButton.IsEnabled = true;
}
}
_isComplete = true;
this.DialogResult = true;
}
private void OnCancel(object sender, System.Windows.RoutedEventArgs e)
{
this.Close();
}
private Button PART_OkButton;
private Button PART_CancelButton;
private bool _isComplete;
}
XAML (resource dictionary for the DialogWindow
):
<Style TargetType="local:DialogWindow">
<Setter Property="MinWidth"
Value="500" />
<Setter Property="SizeToContent"
Value="WidthAndHeight" />
<Setter Property="Padding"
Value="5" />
<Setter Property="ResizeMode"
Value="NoResize" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:DialogWindow">
<Border Background="{TemplateBinding Background}">
<Grid Margin="{TemplateBinding Padding}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ContentPresenter Grid.Row="0"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}" />
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Right"
Margin="0 5 0 0"
Grid.Row="1">
<StackPanel.Resources>
<Style TargetType="Button">
<Setter Property="Margin"
Value="5 0 0 0" />
<Setter Property="MinWidth"
Value="75" />
</Style>
</StackPanel.Resources>
<Button x:Name="PART_OkButton">OK</Button>
<Button x:Name="PART_CancelButton">Cancel</Button>
</StackPanel>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Again, that is all a bit involved, no question. But remember, everything above is fully reusable in any WPF app, and the platform-independent parts are reusable in any MVVM app. And if a particular application needs a more customized dialog box look and feel, it can simply retemplate DialogWindow
, as with any other control. Unlike bespoke code-behind or any other ad hoc approach, this is a long-term investment.
Conclusion
One of the great advantages of MVVM's recommendation to limit bespoke code behind and keep view models robust but platform-independent is that it minimizes the portions of code that are both application and GUI specific, and thus the least reusable and most expensive to write and maintain. Showing and maanging modal dialogs is no exception to this. By implementing the hard parts as application-independent library-style code, the remainder of the application can create rich modal dialogs in an entirely MVVM-friendly and scalable manner.
Upvotes: 1
Reputation: 7031
An interesting alternative is to use Controllers which are responsible to show the views (dialogs).
How this works is shown by the WPF Application Framework (WAF).
Upvotes: 3
Reputation: 31
I had the same situation and wrapped up the MessageBox into a designer invisible control. The details are in my blog
The same can be extended to any modal dialogs, file browse control etc.
Upvotes: 2
Reputation: 4024
Simplest way: use HanumanInstitute.MvvmDialogs library
If you follow the documentation, you can use it as beautifully as this, with full decoupling from the UI
var presetName = await dialogService.ShowSavePresetViewAsync(this);
Upvotes: 0
Reputation: 3401
EDIT: More than 10 years after, I can tell that using a Mediator or any other messaging pattern is a really bad idea at so many levels. Don't do it, just implement Jeffrey's answer or a IDialogService injected in your view model.
You should use a mediator for this. Mediator is a common design pattern also known as Messenger in some of its implementations. It's a paradigm of type Register/Notify and enables your ViewModel and Views to communicate through a low-coupled messaging mecanism.
You should check out the google WPF Disciples group, and just search for Mediator. You will be much happy with the answers...
You can however start with this:
http://joshsmithonwpf.wordpress.com/2009/04/06/a-mediator-prototype-for-wpf-apps/
Enjoy !
Edit: you can see the answer to this problem with the MVVM Light Toolkit here:
http://mvvmlight.codeplex.com/Thread/View.aspx?ThreadId=209338
Upvotes: 56
Reputation: 341
A good MVVM dialog should:
Unfortunately, WPF doesn't provide these features. Showing a dialog requires a code-behind call to ShowDialog()
. The Window class, which supports dialogs, can't be declared in XAML so it can't easily be databound to the DataContext
.
To solve this, I wrote a XAML stub control that sits in the logical tree and relays databinding to a Window
and handles showing and hiding the dialog. You can find it here: http://www.codeproject.com/KB/WPF/XAMLDialog.aspx
It's really simply to use and doesn't require any strange changes to your ViewModel and doesn't require events or messages. The basic call looks like this:
<dialog:Dialog Content="{Binding Path=DialogViewModel}" Showing="True" />
You probably want to add a style that sets Showing
. I explain it in my article. I hope this helps you.
Upvotes: 34
Reputation: 35380
After spending years dealing with this problem in WPF, I finally figured out the standard way of implementing dialogs in WPF. Here are the advantages of this approach:
So what's the key. It is DI + IoC.
Here is how it works. I'm using MVVM Light, but this approach may be extended to other frameworks as well:
Add an interface IDialogService to VM project:
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();
}
Expose a public static property of IDialogService
type in your ViewModelLocator
, but leave registration part for the View layer to perform. This is the key.:
public static IDialogService DialogService => SimpleIoc.Default.GetInstance<IDialogService>();
Add an implementation of this interface in the App project.
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
etc.), others are specific to this project and use custom Window
s. You can add more custom windows in the same fashion. The key is to keep UI-specific elements in the View layer and just expose the returned data using POCOs in the VM layer.Perform IoC Registration your interface in the View layer using this class. You can do this in your main view's constructor (after InitializeComponent()
call):
SimpleIoc.Default.Register<IDialogService, DialogPresenter>();
There you go. You now have access to all your dialog functionality at both VM and View layers. Your VM layer can call these functions like this:
var NoTrump = ViewModelLocator.DialogService.AskBooleanQuestion("Really stop the trade war???", "");
IDialogService
in your Test project and register that class in IoC in the constructor your test class.Microsoft.Win32
to access Open and Save dialogs. I have left them out because there is also a WinForms version of these dialogs available, plus someone might want to create their own version. Also note that some of the identifier used in DialogPresenter
are names of my own windows (e.g. SettingsWindow
). You'll need to either remove them from both the interface and implementation or provide your own windows.DispatcherHelper.Initialize()
early in your application's life cycle.Except for DialogPresenter
which is injected in the View layer, other ViewModals should be registered in ViewModelLocator
and then a public static property of that type should be exposed for the View layer to consume. Something like this:
public static SettingsVM Settings => SimpleIoc.Default.GetInstance<SettingsVM>();
For the most part, your dialogs should not have any code-behind for stuff like binding or setting DataContext etc. You shouldn't even pass things as constructor parameters. XAML can do that all for you, like this:
<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
this way gives you all kinds of design-time benefits such as Intellisense and auto-completion.Hope that helps everyone.
Upvotes: 2
Reputation: 954
Sorry, but I have to chime in. I have been through several of the suggested solutions, before finding the Prism.Wpf.Interactivity namespace in the Prism project. You can use interaction requests and popup window action to either roll a custom window or for simpler needs there are built in Notification and Confirmation popups. These create true windows and are managed as such. you can pass a context object with any dependencies you need in the dialog. We use this solution at my work since I found it. We have numerous senior devs here and noone has come up with anything better. Our previous solution was the dialog service into an overlay and using a presenter class to make it happen, but you had to have factories for all of the dialog viewmodels, etc.
This isn't trivial but it also isn't super complicated. And it is built in to Prism and is therefore best (or better) practice IMHO.
My 2 cents!
Upvotes: 0
Reputation: 16119
I've written a fairly comprehensive article about this very topic and also developed a pop-in library for MVVM Dialogs. Strict adherence to MVVM is not only possible but very clean when implemented properly, and it can be easily extended to third-party libraries that don't adhere to it themselves:
https://www.codeproject.com/Articles/820324/Implementing-Dialog-Boxes-in-MVVM
Upvotes: 1
Reputation: 7591
I've implemented a Behavior that listens to a Message from the ViewModel. It's based on Laurent Bugnion solution, but since it doesn't use code behind and is more reusable, I think it's more elegant.
How to make WPF behave as if MVVM is supported out of the box
Upvotes: 3
Reputation:
I struggled with the same problem. I have come up with a way to intercommunicate between the View and the ViewModel. You can initiate sending a message from the ViewModel to the View to tell it to show a messagebox and it will report back with the result. Then the ViewModel can respond to the result returned from the View.
I demonstrate this in my blog:
Upvotes: 0
Reputation: 1242
I know it's an old question, but when I did this search, I find a lot of related question, but I did not find a really clear response. So I make my own implementation of a dialogbox/messagebox/popin, and I share it!
I think it is "MVVM proof", and I try to make it simple and proper, but I am new to WPF, so feel free to comment, or even make pull request.
https://github.com/Plasma-Paris/Plasma.WpfUtils
You can use it like this:
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)
// [...]
}
Or like this if you want more sophisticated popin :
var result = await _Service.ShowCustomMessageBox(new MyMessageBoxViewModel { /* What you want */ });
And it is showing things like this :
Upvotes: 1
Reputation: 261
EDIT: yes I agree this is not a correct MVVM approach and I am now using something similar to what is suggested by blindmeis.
One of the way you could to this is
In your Main View Model (where you open the modal):
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.
}
}
And in your 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; }
or similar to what is posted here WPF MVVM: How to close a window
Upvotes: -1
Reputation: 226
There are two good ways to do this, 1) a dialog service (easy, clean), and 2) view assisted. View assisted provides some neat features, but is usually not worth it.
a) a dialog service interface like via constructor or some dependency container:
interface IDialogService
{
Task ShowDialogAsync(DialogViewModel dlgVm);
}
b) Your implementation of IDialogService should open a window (or inject some control into the active window), create a view corresponding to the name of the given dlgVm type (use container registration or convention or a ContentPresenter with type associated DataTemplates). ShowDialogAsync should create a TaskCompletionSource and return its .Task proptery. The DialogViewModel class itself needs an event you can invoke in the derived class when you want to close, and watch in the dialog view to actually close/hide the dialog and complete the TaskCompletionSource.
b) To use, simply call await this.DialogService.ShowDialog(myDlgVm) on your instance of some DialogViewModel-derived class. After await returns, look at properties you've added on your dialog VM to determine what happened; you don't even need a callback.
This has your view listening to an event on the viewmodel. This could all be wrapped up into a Blend Behavior to avoid code behind and resource usage if you're so inclined (FMI, subclass the "Behavior" class to see a sort of Blendable attached property on steroids). For now, we'll do this manually on each view:
a) Create an OpenXXXXXDialogEvent with a custom payload (a DialogViewModel derived class).
b) Have the view subscribe to the event in its OnDataContextChanged event. Be sure to hide and unsubscribe if the old value != null and in the Window's Unloaded event.
c) When the event fires, have the view open your view, which might be in a resource on your page, or you could locate it by convention elsewhere (like in the the dialog service approach).
This approach is more flexible, but requires more work to use. I don't use it much. The one nice advantage are the ability to place the view physically inside a tab, for example. I have used an algorithm to place it in the current user control's bounds, or if not big enough, traverse up the visual tree until a big enough container is found.
This allows dialogs to be close to the place they're actually used, only dim the part of the app related to the current activity, and let the user move around within the app without having to manually push dialogs away, even have multiple quasi-modal dialogs open on different tabs or sub-views.
Upvotes: 6
Reputation: 5977
I suggest forgoing the 1990's modal dialogs and instead implementing a control as an overlay (canvas+absolute positioning) with visibility tied to a boolean back in the VM. Closer to an ajax type control.
This is very useful:
<BooleanToVisibilityConverter x:Key="booltoVis" />
as in:
<my:ErrorControl Visibility="{Binding Path=ThereWasAnError, Mode=TwoWay, Converter={StaticResource booltoVis}, UpdateSourceTrigger=PropertyChanged}"/>
Here's how I have one implemented as a user control. Clicking on the 'x' closes the control in a line of code in the usercontrol's code behind. (Since I have my Views in an .exe and ViewModels in a dll, I don't feel bad about code that manipulates UI.)
Upvotes: 135
Reputation: 4692
Karl Shifflett has created a sample application for showing dialog boxes using service approach and Prism InteractionRequest approach.
I like the service approach - It's less flexible so users are less likely to break something :) It's also consistent with the WinForms part of my application (MessageBox.Show) But if you plan to show a lot of different dialogs, then InteractionRequest is a better way to go.
http://karlshifflett.wordpress.com/2010/11/07/in-the-box-ndash-mvvm-training/
Upvotes: 1
Reputation: 1200
Use a freezable command
<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();
}
}
Upvotes: 6
Reputation: 22435
I use this approach for dialogs with MVVM.
All I have to do now is call the following from my view model.
var result = this.uiDialogService.ShowDialog("Dialogwindow title goes here", dialogwindowVM);
Upvotes: 25
Reputation: 59316
I was pondering a similar problem when asking how the view model for a task or dialog should look like.
My current solution looks like 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();
}
When the view model decides that user input is required, it pulls up a instance of SelectionTaskModel
with the possible choices for the user. The infrastructure takes care of bringing up the corresponding view, which in proper time will call the Choose()
function with the user's choice.
Upvotes: 0
Reputation: 31
Why not just raise an event in the VM and subscribe to the event in the view? This would keep the application logic and the view seperate and still allow you to use a child window for dialogs.
Upvotes: 3
Reputation: 2205
I rolled my own window loader described in an answer to this question:
Managing multiple WPF views in an application
Upvotes: 1
Reputation: 958
I really struggled with this concept for a while when learning (still learning) MVVM. What I decided, and what I think others already decided but which wasn't clear to me is this:
My original thought was that a ViewModel should not be allowed to call a dialog box directly as it has no business deciding how a dialog should appear. Beacause of this I started thinking about how I could pass messages much like I would have in MVP (i.e. View.ShowSaveFileDialog()). However, I think this is the wrong approach.
It is OK for a ViewModel to call a dialog directly. However, when you are testing a ViewModel , that means that the dialog will either pop up during your test, or fail all together (never really tried this).
So, what needs to happen is while testing is to use a "test" version of your dialog. This means that for ever dialog you have, you need to create an Interface and either mock out the dialog response or create a testing mock that will have a default behaviour.
You should already be using some sort of Service Locator or IoC that you can configure to provide you the correct version depending on the context.
Using this approach, your ViewModel is still testable and depending on how you mock out your dialogs, you can control the behaviour.
Hope this helps.
Upvotes: 6
Reputation: 1795
My current solution solves most of the issues you mentioned yet its completely abstracted from platform specific things and can be reused. Also i used no code-behind only binding with DelegateCommands that implement ICommand. Dialog is basically a View - a separate control that has its own ViewModel and it is shown from the ViewModel of the main screen but triggered from the UI via DelagateCommand binding.
See full Silverlight 4 solution here Modal dialogs with MVVM and Silverlight 4
Upvotes: 16
Reputation: 5225
I think the view could have code to handle the event from the view model.
Depending on the event/scenario, it could also have an event trigger that subscribes to view model events, and one or more actions to invoke in response.
Upvotes: 2
Reputation: 71856
I think that the handling of a dialog should be the responsibility of the view, and the view needs to have code to support that.
If you change the ViewModel - View interaction to handle dialogs then the ViewModel is dependant on that implementation. The simplest way to deal with this problem is to make the View responsible for performing the task. If that means showing a dialog then fine, but could also be a status message in the status bar etc.
My point is that the whole point of the MVVM pattern is to separate business logic from the GUI, so you shouldn't be mixing GUI logic (to display a dialog) in the business layer (the ViewModel).
Upvotes: 3