
Contents
This article will address one of the problems a developer might run into when using the MVVM pattern, namely opening dialogs from ViewModels. Basic knowledge of the pattern is expected. Josh Smith has written a fantastic article in MSDN Magazine which can serve as the starting point for those that are unfamiliar with the pattern.
There already exist some solutions exploring this area, but none of them seem to be widely accepted among the MVVM community. Onyx on CodePlex is probably the most complete solution. Sacha Barber announced that he is working on a new MVVM framework, but hasn't set any release date. At the same time, WPF Disciples are teaming up to create a MVVM reference application, again with no release date yet.
I am possibly sticking my neck out here, but I would like to state some of my own interpretations of the MVVM pattern:
- The fundamental core is the separation of UI and logic. ViewModel and View can communicate using bindings, making it possible for the View to be happily unaware of the ViewModel and vice versa. I know some of you might say: "well, actually the View knows about the ViewModel since the ViewModel is stored within the DataContext property, and the View is declaring its bindings to match properties on the ViewModel, surely the View must know the ViewModel?". I don't think so; we have to analyze what knows actually means. The ViewModel is stored in the
DataContext property, that is correct, but DataContext is of type Object. From the View's point, the only thing it knows is whether the property has been set or not, nothing about the actual content. Let's get to the bindings declared in the View; do they say something about the ViewModel? I would have to say no here as well. The View might declare whatever bindings it wishes, but the ViewModel isn't enforced to implement them.
- The ViewModel should serve the View with data, but one might argue where the separation between the View and ViewModel is. If for instance the ViewModel is holding a list of items presented in a
ListView, I would argue that sorting, grouping, and filtering could be the responsibility of the View, not the ViewModel. Attached properties can solve this for us, making the ViewModel happily unaware of the item presentation.
- The ViewModel should not contain anything used purely in tests; either a class is testable or it isn't.
The concept of letting services handle relations between the ViewModel and View seems to be the most accepted solution. My contribution is IDialogService.
public interface IDialogService
{
ReadOnlyCollection<FrameworkElement> Views { get; }
void Register(FrameworkElement view);
void Unregister(FrameworkElement view);
bool? ShowDialog(object ownerViewModel, object viewModel);
bool? ShowDialog<T>(object ownerViewModel, object viewModel) where T : Window;
MessageBoxResult ShowMessageBox(
object ownerViewModel,
string messageBoxText,
string caption,
MessageBoxButton button,
MessageBoxImage icon);
DialogResult ShowOpenFileDialog(object ownerViewModel, IOpenFileDialog openFileDialog);
DialogResult ShowFolderBrowserDialog(object ownerViewModel,
IFolderBrowserDialog folderBrowserDialog);
}
The actual implementation of IDialogServiceis is DialogService.
class DialogService : IDialogService
{
private readonly HashSet<FrameworkElement> views;
private readonly IWindowViewModelMappings windowViewModelMappings;
public DialogService(IWindowViewModelMappings windowViewModelMappings = null)
{
this.windowViewModelMappings = windowViewModelMappings;
views = new HashSet<FrameworkElement>();
}
#region IDialogService Members
public ReadOnlyCollection<FrameworkElement> Views
{
get { return new ReadOnlyCollection<FrameworkElement>(views.ToList()); }
}
public void Register(FrameworkElement view)
{
Window owner = GetOwner(view);
if (owner == null)
{
view.Loaded += LateRegister;
return;
}
owner.Closed += OwnerClosed;
views.Add(view);
}
public void Unregister(FrameworkElement view)
{
views.Remove(view);
}
public bool? ShowDialog(object ownerViewModel, object viewModel)
{
Type dialogType =
windowViewModelMappings.GetWindowTypeFromViewModelType(viewModel.GetType());
return ShowDialog(ownerViewModel, viewModel, dialogType);
}
public bool? ShowDialog<T>(object ownerViewModel, object viewModel) where T : Window
{
return ShowDialog(ownerViewModel, viewModel, typeof(T));
}
public MessageBoxResult ShowMessageBox(
object ownerViewModel,
string messageBoxText,
string caption,
MessageBoxButton button,
MessageBoxImage icon)
{
return MessageBox.Show(FindOwnerWindow(ownerViewModel),
messageBoxText, caption, button, icon);
}
public DialogResult ShowOpenFileDialog(object ownerViewModel,
IOpenFileDialog openFileDialog)
{
OpenFileDialog dialog = new OpenFileDialog(openFileDialog);
return dialog.ShowDialog(new WindowWrapper(FindOwnerWindow(ownerViewModel)));
}
public DialogResult ShowFolderBrowserDialog(object ownerViewModel,
IFolderBrowserDialog folderBrowserDialog)
{
FolderBrowserDialog dialog = new FolderBrowserDialog(folderBrowserDialog);
return dialog.ShowDialog(new WindowWrapper(FindOwnerWindow(ownerViewModel)));
}
#endregion
#region Attached properties
public static readonly DependencyProperty IsRegisteredViewProperty =
DependencyProperty.RegisterAttached(
"IsRegisteredView",
typeof(bool),
typeof(DialogService),
new UIPropertyMetadata(IsRegisteredViewPropertyChanged));
public static bool GetIsRegisteredView(FrameworkElement target)
{
return (bool)target.GetValue(IsRegisteredViewProperty);
}
public static void SetIsRegisteredView(FrameworkElement target, bool value)
{
target.SetValue(IsRegisteredViewProperty, value);
}
private static void IsRegisteredViewPropertyChanged(DependencyObject target,
DependencyPropertyChangedEventArgs e)
{
if (DesignerProperties.GetIsInDesignMode(target)) return;
FrameworkElement view = target as FrameworkElement;
if (view != null)
{
bool newValue = (bool)e.NewValue;
bool oldValue = (bool)e.OldValue;
if (newValue)
{
ServiceLocator.Resolve<IDialogService>().Register(view);
}
else
{
ServiceLocator.Resolve<IDialogService>().Unregister(view);
}
}
}
#endregion
private bool? ShowDialog(object ownerViewModel, object viewModel, Type dialogType)
{
Window dialog = (Window)Activator.CreateInstance(dialogType);
dialog.Owner = FindOwnerWindow(ownerViewModel);
dialog.DataContext = viewModel;
return dialog.ShowDialog();
}
private Window FindOwnerWindow(object viewModel)
{
FrameworkElement view =
views.SingleOrDefault(v => ReferenceEquals(v.DataContext, viewModel));
if (view == null)
{
throw new ArgumentException("Viewmodel is not referenced by any registered View.");
}
Window owner = view as Window;
if (owner == null)
{
owner = Window.GetWindow(view);
}
if (owner == null)
{
throw new InvalidOperationException("View is not contained within a Window.");
}
return owner;
}
private void LateRegister(object sender, RoutedEventArgs e)
{
FrameworkElement view = sender as FrameworkElement;
if (view != null)
{
view.Loaded -= LateRegister;
Register(view);
}
}
private void OwnerClosed(object sender, EventArgs e)
{
Window owner = sender as Window;
if (owner != null)
{
IEnumerable<FrameworkElement> windowViews =
from view in views
where Window.GetWindow(view) == owner
select view;
foreach (FrameworkElement view in windowViews.ToArray())
{
Unregister(view);
}
}
}
private Window GetOwner(FrameworkElement view)
{
return view as Window ?? Window.GetWindow(view);
}
}
The design is pretty straightforward. A View is registering itself as part of the MVVM pattern by setting the attached property IsRegisteredView on any element in the logical tree.
<Window
x:Class="MVVM_Dialogs.View.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:service="clr-namespace:MVVM_Dialogs.Service"
service:DialogService.IsRegisteredView="True">
The View is now remembered by the dialog service. When a ViewModel is in need to show some kind of dialog, the correct View is found by the service, and its owning Window is used as the owner.
There exist two overloads for using the ShowDialog method. The first one lets the developer specify the dialog type.
public class MainWindowViewModel : ViewModelBase
{
...
private void ShowInformation(object o)
{
PersonViewModel selectedPerson = persons.Single(p => p.IsSelected);
PersonDialogViewModel personDialogViewModel =
new PersonDialogViewModel(selectedPerson.Person);
dialogService.ShowDialog<PersonDialog>(this, personDialogViewModel);
}
}
This overload specifies that a PersonDialog should be displayed with this as the owner and personDialogViewModel as DataContext. However, some of you might find it unsuitable that a View is specified in a ViewModel. For you, there is a second overload.
public class MainWindowViewModel : ViewModelBase
{
...
private void ShowInformation(object o)
{
PersonViewModel selectedPerson = persons.Single(p => p.IsSelected);
PersonDialogViewModel personDialogViewModel =
new PersonDialogViewModel(selectedPerson.Person);
dialogService.ShowDialog(this, personDialogViewModel);
}
}
public class WindowViewModelMappings : IWindowViewModelMappings
{
private IDictionary<Type, Type> mappings;
public WindowViewModelMappings()
{
mappings = new Dictionary<Type, Type>
{
{ typeof(PersonDialogViewModel), typeof(PersonDialog) }
};
}
public Type GetWindowTypeFromViewModelType(Type viewModelType)
{
return mappings[viewModelType];
}
}
This overload doesn't specify the dialog type; however, you are forced to register the Window-ViewModel mappings in a class implementing IWindowViewModelMappings.
I would like to hear your thoughts about the idea. Perhaps you have a better solution? Don't be afraid, leave a comment...
- 5 October 2010: Code update.
- Updated source according to comments by d302241.
- 4 April 2010: Code update.
- Updated source according to comments by Michael Sync.
- Converted to .NET 4.
- 18 June 2009: Code update.
- Code no longer throws exception in Designer mode.
- Fixed wrong interface summary.
- 2 June 2009: Code update.
- Added the
ShowOpenFileDialog method to IDialogService.
- Implemented a service locator instead of keeping
DialogService as a Singleton.
- 27 May 2009: Article update.
- Updated introduction after comments from William E. Kempf.
- 25 May 2009: Initial version.
Got my first computer in the 90's and loved it even though it sounded like a coffeemaker.
Now getting paid for designing cool WPF applications, and drinks the coffee instead of listening to it being made.