Creating an Mvvm Friendly Dialog Strategy

Creating an MVVM friendly dialog strategy

When designing a UI with MVVM the goal is to separate the concerns of the View from the concerns of the ViewModel. Ideally, the ViewModel should not rely on any view components. However, this is the idal and another rule of MVVM is that you should design your application as you wish.

In the area providing a service showing dialogs there are two different approaches floating arround:

  1. Implementing the DialogService on the View (e.g. see http://geekswithblogs.net/lbugnion/archive/2011/04/13/deep-dive-mvvm-samples-mix11-deepdivemvvm.aspx Sample 03).
  2. Implementing a service component that does is not attached to the view (e.g. see http://blog.roboblob.com/2010/01/19/modal-dialogs-with-mvvm-and-silverlight-4/)

Both approaches rely on an interface that defines the functionality the service provides. The implementation for this Service is then injected into the ViewModel.

Also, do both approaches have their specific advantages and disadvantages.

  • The first approach works also well for WP7, however, it requires a common view base class as this contains the implementation of the view service.
  • The second approach works well for SilverLight and WPF and appleals as it keeps the service separate from the view and does not impose any restictions on the view.

Another possible solution is to use messaging to show the dialogs.

Whatever approach you are using try to keep the View and the ViewModel de-coupled by using an IoC (inversion of control) pattern, i.e. define an interface so that you can use different implementations. To bind the services into the ViewModel use injection, i.e. passing the service into the constructor of the ViewModel or by setting a property.

Opening save file dialog in MVVM using OnPropertyChange is Ok or Not

Right? There is no right or wrong, only the best choice at the current time. The only way for purists would be to abstract away the process of gaining such input from the user behind an interface, then create a View class which serves this interface and inject it somehow (IoC/DI, or perhaps by composition in the xaml if your ViewModels are instantiated that way).

Personally, I wouldn't spend too much time worrying about this, unless you must worry about this. I've done it both ways. For your average MVVM app, I think it is no great crime to just use a MessageBox, OpenFileDialog, etc. For heavily tested apps, you'll need to abstract it away. And there are other situations that demand it as well. For example, I have code that exists in an application and in a Visual Studio extension. VS has its own dialog types which should be used instead of, say, MessageBox. So abstraction is appropriate. But I wouldn't invest the work unless I had a reason to.

Implementing WPF MVVM Dialog Service

You can always use the NuGet package called MvvmDialogs, or look at its implementation. After all It's open source on GitHub.

Friendly advice, be aware of answers regarding MVVM that are absolute. These answers often come from developers in the learning phase, where rules and guidelines are followed without understanding their benefit. As you progress you start to question "why", and become more pragmatic. The idea that the view model shouldn't be aware of a view service is just silly. It is a view model, the view is in it's name.

What are the best practices to dynamically create views and add controls to view using MVVM pattern

This post discusses the strategies for creating views (dialogs) from your view models.

Edit:

From your comment I take it that you got an user interface that has an add and delete button. The add button should add an item (type ?) to a ItemsControl ... hope that's correct.

So, how would I do this, well I would create a view model that has an ObservableCollecion<ItemViewModel>. The ItemViewModle is the view mode that represents the item that should be added to the ItemsControl (so in your case the view model backing your "rangeView").

Then I would add two commands that handle the addition and deletion of items. Both commands just add/remove ItemViewModels from your collection.

To show the items in the view I would bind the ItemControl.ItemsSource property to the collection in your main view model (i.e. the one holding the ItemViewModel instances). The I would supply an ItemTemplate to render the items on the screen.

Ok, here is an example of what I think you are trying to do (at least conceptionally). Complete Source Code here. In the example I used a ListBox as it allows me easily to determine which item is selected, this depends on your szenario. Also note that you have complete freedom to customize the Template, the ItemPanelTemplate, and DataTemplate to fit your needs. You can even use this approacht to create PanoramaPages in WP7!

2nd edit: ItemsControl does not have a SelectedItem property. To get to it you have to use a control inheriting from Selector (e.g. a ListBox as I did) or you can use the Selector directly.

ItemViewModel:

public class ItemViewModel : ViewModelBase
{
#region [Name]

public const string NamePropertyName = "Name";

private string _name = null;

public string Name {
get {
return _name;
}

set {
if (_name == value) {
return;
}

var oldValue = _name;
_name = value;

RaisePropertyChanged(NamePropertyName);
}
}

#endregion
}

MainViewModel:

public class MainViewModel : ViewModelBase
{
public MainViewModel() {
if (IsInDesignMode) {
this.Items = new ObservableCollection<ItemViewModel>(Enumerable.Range(0, 10).Select((x, i) => new ItemViewModel() { Name = "Design Time Item " + i }));
} else {
// Code runs "for real"
}
}

#region [AddCommand]

private RelayCommand _addCommand;

public RelayCommand AddCommand {
get {
return _addCommand ?? (_addCommand = new RelayCommand(
() => {
this.Items.Add(new ItemViewModel() { Name = "New item - " + DateTime.Now });
}
));
}
}

#endregion

#region [DeleteCommand]

private RelayCommand _deleteCommand;

public RelayCommand DeleteCommand {
get {
return _deleteCommand ?? (_deleteCommand = new RelayCommand(
() => {
this.Items.Remove(this.SelectedItem);
},
() => { return this.SelectedItem != null; }
));
}
}

#endregion

#region [Items]

public const string ItemsPropertyName = "Items";

private ObservableCollection<ItemViewModel> _items = new ObservableCollection<ItemViewModel>();

public ObservableCollection<ItemViewModel> Items {
get {
return _items;
}

set {
if (_items == value) {
return;
}

var oldValue = _items;
_items = value;

RaisePropertyChanged(ItemsPropertyName);
}
}

#endregion

#region [SelectedItem]

public const string SelectedItemPropertyName = "SelectedItem";

private ItemViewModel _selectedItem = null;

public ItemViewModel SelectedItem {
get {
return _selectedItem;
}

set {
if (_selectedItem == value) {
return;
}

var oldValue = _selectedItem;
_selectedItem = value;

RaisePropertyChanged(SelectedItemPropertyName);

// important in SL to notify command that can execute has changed !
this.DeleteCommand.RaiseCanExecuteChanged();
}
}

#endregion
}

MainPage.xaml

<UserControl 
x:Class="MvvmLight1.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Height="300"
Width="300"
DataContext="{Binding Main, Source={StaticResource Locator}}"
>

<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Skins/MainSkin.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>

<Grid x:Name="LayoutRoot">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<ListBox Grid.Row="0" ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem, Mode=TwoWay}">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="Margin" Value="0"/>
<Setter Property="Padding" Value="0"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<!-- we are dealing with ItemViewModels now -->
<Border BorderThickness="0,0,0,1" BorderBrush="Gray" Padding="10,5">
<TextBlock Text="{Binding Name}"/>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Margin="5,10" Content="Add" Command="{Binding AddCommand}" Width="70"/>
<Button Margin="5,10" Content="Delete" Command="{Binding DeleteCommand}" Width="70"/>
</StackPanel>
</Grid>
</UserControl>

MVP vs MVVM: how to manage alert dialogs in MVVM and improve testability

The solution I use for MVVM is mixed, as follows.

From the article from Jose Alcérreca mentioned in the Medium post LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case) referred in the SO answer to Show Dialog from ViewModel in Android MVVM Architecture, I choose the forth option "Recommended: Use an Event wrapper". The reason being that I'm able to peek the message if needed. Also, I added the observeEvent() extension method from this comment in Jose's Gist.

My final code is:

import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer

/**
* Used as a wrapper for data that is exposed via a LiveData that represents an event.
* See:
* https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150
* https://gist.github.com/JoseAlcerreca/e0bba240d9b3cffa258777f12e5c0ae9
*/
open class LiveDataEvent<out T>(private val content: T) {

@Suppress("MemberVisibilityCanBePrivate")
var hasBeenHandled = false
private set // Allow external read but not write

/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}

/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}

inline fun <T> LiveData<LiveDataEvent<T>>.observeEvent(owner: LifecycleOwner, crossinline onEventUnhandledContent: (T) -> Unit) {
observe(owner, Observer { it?.getContentIfNotHandled()?.let(onEventUnhandledContent) })
}

Usage is like so (my example triggers the event when data synchronization finishes):

class ExampleViewModel() : ViewModel() {
private val _synchronizationResult = MutableLiveData<LiveDataEvent<SyncUseCase.Result>>()
val synchronizationResult: LiveData<LiveDataEvent<SyncUseCase.Result>> = _synchronizationResult

fun synchronize() {
// do stuff...
// ... when done we get "result"
_synchronizationResult.value = LiveDataEvent(result)
}
}

And consuming it by using observeEvent() to have nice, concise code:

exampleViewModel.synchronizationResult.observeEvent(this) { result ->
// We will be delivered "result" only once per change
}

What is good choice between Modal Window & Dialog in order to display some data using MVVM Light Tool Kit?

In my opinion choise between modal and non-modal window depends only on the UI experience you want to achieve. If for some reason update of the row item can't be done simultanously to other actions on the grid items you need modal one.

As described here Dialog boxes overview

A modal dialog box is displayed by a function when the function needs additional data from a user to continue. Because the function depends on the modal dialog box to gather data, the modal dialog box also prevents a user from activating other windows in the application while it remains open.

Modal dialog would be easier solution as you dont need extra validation on the row you are editing - you simply can edit one row at the time (and I assume you it wouldn't be possible to remove this row in main view while editing in edit window).

If you want to have changes done in child window reflected in your main grid, just use observable collection of items in your main view and pass certain item from this collection as a DataContext of your child window.

How to use a FolderBrowserDialog from a WPF application with MVVM

First, you could use the ShowDialog signature that does not require a window.

var dlg = new FolderBrowserDialog();
DialogResult result = dlg.ShowDialog();

Second, you could send the main window of the Application as the owning window.

var dlg = new FolderBrowserDialog();
DialogResult result = dlg.ShowDialog(Application.Current.MainWindow.GetIWin32Window());

The second option might not be considered very MVVMish.

See the answer by @Dr. ABT in this question for a way to inject a pointer to your View into your ViewModel (not sure if this is a good idea or a bad idea, but I'm not going to let that stop me) With this technique, you would have access in your VM to the corresponding View if you really want to make that View be the owner of the FolderBrowserDialog.

@ChrisDD is right about defining an interface and wrapping FolderBrowserDialog. That is how we do it:

  public interface IFolderBrowserDialog
{
string Description { get; set; }
Environment.SpecialFolder RootFolder { get; set; }
string SelectedPath { get; set; }
bool ShowNewFolderButton { get; set; }
bool? ShowDialog();
bool? ShowDialog(Window owner);
}

//Decorated for MEF injection
[Export(typeof(IFolderBrowserDialog))]
[PartCreationPolicy(CreationPolicy.NonShared)]
internal class WindowsFormsFolderBrowserDialog : IFolderBrowserDialog
{
private string _description;
private string _selectedPath;

[ImportingConstructor]
public WindowsFormsFolderBrowserDialog()
{
RootFolder = System.Environment.SpecialFolder.MyComputer;
ShowNewFolderButton = false;
}

#region IFolderBrowserDialog Members

public string Description
{
get { return _description ?? string.Empty; }
set { _description = value; }
}

public System.Environment.SpecialFolder RootFolder { get; set; }

public string SelectedPath
{
get { return _selectedPath ?? string.Empty; }
set { _selectedPath = value; }
}

public bool ShowNewFolderButton { get; set; }

public bool? ShowDialog()
{
using (var dialog = CreateDialog())
{
var result = dialog.ShowDialog() == DialogResult.OK;
if (result) SelectedPath = dialog.SelectedPath;
return result;
}
}

public bool? ShowDialog(Window owner)
{
using (var dialog = CreateDialog())
{
var result = dialog.ShowDialog(owner.AsWin32Window()) == DialogResult.OK;
if (result) SelectedPath = dialog.SelectedPath;
return result;
}
}
#endregion

private FolderBrowserDialog CreateDialog()
{
var dialog = new FolderBrowserDialog();
dialog.Description = Description;
dialog.RootFolder = RootFolder;
dialog.SelectedPath = SelectedPath;
dialog.ShowNewFolderButton = ShowNewFolderButton;
return dialog;
}
}

internal static class WindowExtensions
{
public static System.Windows.Forms.IWin32Window AsWin32Window(this Window window)
{
return new Wpf32Window(window);
}
}

internal class Wpf32Window : System.Windows.Forms.IWin32Window
{
public Wpf32Window(Window window)
{
Handle = new WindowInteropHelper(window).Handle;
}

#region IWin32Window Members

public IntPtr Handle { get; private set; }

#endregion
}

Then we make the VM/Command where we want to use the FolderBrowser import IFolderBrowserDialog. In application, IFolderBrowserDialog.ShowDialog shows the dialog. In unit test, we mock IFolderBrowserDialog so we can verify that it was called with correct parameters and/or send the selected folder back to the sut so that the test can continue.



Related Topics



Leave a reply



Submit