Wpf Openfiledialog with the Mvvm Pattern

WPF OpenFileDialog with the MVVM pattern?

What I generally do is create an interface for an application service that performs this function. In my examples I'll assume you are using something like the MVVM Toolkit or similar thing (so I can get a base ViewModel and a RelayCommand).

Here's an example of an extremely simple interface for doing basic IO operations like OpenFileDialog and OpenFile. I'm showing them both here so you don't think I'm suggesting you create one interface with one method to get around this problem.

public interface IOService
{
string OpenFileDialog(string defaultPath);

//Other similar untestable IO operations
Stream OpenFile(string path);
}

In your application, you would provide a default implementation of this service. Here is how you would consume it.

public MyViewModel : ViewModel
{
private string _selectedPath;
public string SelectedPath
{
get { return _selectedPath; }
set { _selectedPath = value; OnPropertyChanged("SelectedPath"); }
}

private RelayCommand _openCommand;
public RelayCommand OpenCommand
{
//You know the drill.
...
}

private IOService _ioService;
public MyViewModel(IOService ioService)
{
_ioService = ioService;
OpenCommand = new RelayCommand(OpenFile);
}

private void OpenFile()
{
SelectedPath = _ioService.OpenFileDialog(@"c:\Where\My\File\Usually\Is.txt");
if(SelectedPath == null)
{
SelectedPath = string.Empty;
}
}
}

So that's pretty simple. Now for the last part: testability. This one should be obvious, but I'll show you how to make a simple test for this. I use Moq for stubbing, but you can use whatever you'd like of course.

[Test]
public void OpenFileCommand_UserSelectsInvalidPath_SelectedPathSetToEmpty()
{
Mock<IOService> ioServiceStub = new Mock<IOService>();

//We use null to indicate invalid path in our implementation
ioServiceStub.Setup(ioServ => ioServ.OpenFileDialog(It.IsAny<string>()))
.Returns(null);

//Setup target and test
MyViewModel target = new MyViewModel(ioServiceStub.Object);
target.OpenCommand.Execute();

Assert.IsEqual(string.Empty, target.SelectedPath);
}

This will probably work for you.

There is a library out on CodePlex called "SystemWrapper" (http://systemwrapper.codeplex.com) that might save you from having to do a lot of this kind of thing. It looks like FileDialog is not supported yet, so you'll definitely have to write an interface for that one.

Edit:

I seem to remember you favoring TypeMock Isolator for your faking framework. Here's the same test using Isolator:

[Test]
[Isolated]
public void OpenFileCommand_UserSelectsInvalidPath_SelectedPathSetToEmpty()
{
IOService ioServiceStub = Isolate.Fake.Instance<IOService>();

//Setup stub arrangements
Isolate.WhenCalled(() => ioServiceStub.OpenFileDialog("blah"))
.WasCalledWithAnyArguments()
.WillReturn(null);

//Setup target and test
MyViewModel target = new MyViewModel(ioServiceStub);
target.OpenCommand.Execute();

Assert.IsEqual(string.Empty, target.SelectedPath);
}

WPF OpenFileDialog using MVVM (Model-View-ViewModel) in c#

In my opinion this kind of things doesn't belong in to the ViewModel. It's View specific logic. The View alone handles user input and then sends it to the ViewModel. ViewModel never asks the View to do something. This would invert the dependency chain and couple the ViewModel to the View. The dependencies have to be like this:
View --> ViewModel --> Model. The ViewModel doesn't even know about the type of views nor that there is a View at all.

You have to open the dialog from your view and then send the result to the view model.
For this you could create a simple event handler in your code-behind and attach it to a button's click event. You take the picked file and use an ICommand to invoke e.g. an open file action. That's MVVM (or MVP). Separate the concerns of the views from your models.

MainWindow.xaml:

<Window x:Class="WpfOpenDialogExample.OpenFileDialogSample"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="OpenFileDialogSample" Height="300" Width="300">
<Window.DataContext>
<ViewModel />
</Window.DataContext>

<Grid>
<Button Name="ShowFilePickerButton" Click="ShowFilePicker_OnClick" Content="Open file" />
</Grid>
</Window>

MainWindow.xaml.cs:

using System;
using System.IO;
using System.Windows;
using Microsoft.Win32;

namespace WpfOpenDialogExample
{
public partial class OpenFileDialogSample : Window
{
public OpenFileDialogSample()
{
InitializeComponent();
}

private void ShowFilePicker_OnClick(object sender, RoutedEventArgs e)
{
var viewModel = this.DataContext as ViewModel;
OpenFileDialog openFileDialog = new OpenFileDialog();
if(openFileDialog.ShowDialog() == true && viewModel.OpenFileCommand.CanExecute(openFileDialog.FileName))
{
viewModel.OpenFileCommand.Execute(openFileDialog.FileName);
}
}

private void ShowFolderPicker_OnClick(object sender, RoutedEventArgs e)
{
var viewModel = this.DataContext as ViewModel;
FolderBrowserDialog openFolderDialog = new FolderBrowserDialog();
if(openFolderDialog.ShowDialog() == DialogResul.Ok && viewModel.OpenFolderCommand.CanExecute(openFolderDialog.SelectedPath ))
{
viewModel.OpenFolderCommand.Execute(openFolderDialog.SelectedPath );
}
}
}
}

ViewModel.cs:

public ICommand OpenFileCommand { get => new RelayCommand(OpenFile, CanOpenFile); }  

private void OpenFile(string filePath)
{
...
}

private bool CanOpenFile(string filePath)
{
return File.Exists(filePath);
}

public ICommand OpenFolderCommand { get => new RelayCommand(OpenFolder, CanOpenFolder); }

private void OpenFolder(string folderPath)
{
...
}

private bool CanOpenFolder(string folderPath)
{
return Directory.Exists(filePath);
}

RelayCommand.cs:

using System;
using System.Windows.Input;

namespace WpfOpenDialogExample
{
/// <summary>
/// An implementation independent ICommand implementation.
/// Enables instant creation of an ICommand without implementing the ICommand interface for each command.
/// The individual Execute() an CanExecute() members are suplied via delegates.
/// <seealso cref="System.Windows.Input.ICommand"/>
/// </summary>
/// <remarks>The type of <c>RelaisCommand</c> actually is a <see cref="System.Windows.Input.ICommand"/></remarks>
public class RelayCommand : ICommand
{
/// <summary>
/// Default constructor to declare the concrete implementation of Execute(object):void and CanExecute(object) : bool
/// </summary>
/// <param name="executeDelegate">Delegate referencing the execution context method.
/// Delegate signature: delegate(object):void</param>
/// <param name="canExecuteDelegate">Delegate referencing the canExecute context method.
/// Delegate signature: delegate(object):bool</param>
public RelayCommand(Action<object> executeDelegate , Predicate<object> canExecuteDelegate)
{
this.executeDelegate = executeDelegate;
this.canExecuteDelegate = canExecuteDelegate;
}

/// <summary>
/// Invokes the custom <c>canExecuteDelegate</c> which should check wether the command can be executed.
/// </summary>
/// <param name="parameter">Optional parameter of type <see cref="System.Object"/></param>
/// <returns>Expected to return tue, when the preconditions meet the requirements and therefore the command stored in <c>executeDelegate</c> can execute.
/// Expected to return fals when command execution is not possible.</returns>
public bool CanExecute(object parameter)
{
if (this.canExecuteDelegate != null)
{
return this.canExecuteDelegate(parameter);
}
return false;
}

/// <summary>
/// Invokes the custom <c>executeDelegate</c>, which references the command to execute.
/// </summary>
/// <param name="parameter">Optional parameter of type <see cref="System.Object"/></param>
public void Execute(object parameter)
{
if (this.executeDelegate != null)
this.executeDelegate(parameter);
}

/// <summary>
/// The event is triggered every time the conditions regarding the command have changed. This occures when <c>InvalidateRequerySuggested()</c> gets explicitly or implicitly called.
/// Triggering this event usually results in an invokation of <c>CanExecute(object):bool</c> to check if the occured change has made command execution possible.
/// The <see cref="System.Windows.Input.CommandManager"/> holds a weakrefernce to the observer.
/// </summary>
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}

private readonly Action<object> executeDelegate;
private readonly Predicate<object> canExecuteDelegate;
}
}

Open File Dialog MVVM

The best thing to do here is use a service.

A service is just a class that you access from a central repository of services, often an IOC container. The service then implements what you need like the OpenFileDialog.

So, assuming you have an IFileDialogService in a Unity container, you could do...

void Browse(object param)
{
var fileDialogService = container.Resolve<IFileDialogService>();

string path = fileDialogService.OpenFileDialog();

if (!string.IsNullOrEmpty(path))
{
//Do stuff
}
}

WPF - Should OpenFileDialog be in the ViewModel

Dialog boxes don't fit into the MVVM paradigm very well, due to the tight coupling they have with the OS. As a general rule though, anything you want directly unit-tested belongs in the view model, while anything that creates Windows GUI objects at runtime belongs in your view layer. With that in mind, the view is the appropriate layer for calling OpenFileDialog. You may find that you still need to break the clean MVVM architecture to do this, so abstracting it away into a service that can be injected will at least keep it away from the rest of your code and maintain good seperation of concerns.

If you really want to do this properly then you have to implement some boiler-plate code similar to what the WPF team wrote for "regular" windows. I wrote a long article about it here, along with a library for easily adding dialog box functionality to your own MVVM projects:

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

OpenFileDialog using MvvmCross in a Wpf Core application

"@BionicCode excuse my french but what are you talking about? "The
click handler should be in the code-behind of the view"? I think you
had a little switcheroo there. My MainViewModel has the method and
command while my MainView binds to it. I think I did everything
exactly right. edit: I also clarified that i did NOT intend to make my
core dependant on the wpf project."

No problem. I think because you are French, you misunderstood me completely.

I will start again: it looks like you are using the MVVM pattern.

To implement MVVM correctly, you must follow some rules (the pattern).

One rule is not to mix responsibilities of the view component with those of the view model component.
Per definition, the view component only displays data to the user and handles user input e.g., collect data or interact with the user.

User input is always part of the user interface (UI). A dialog is a control (application interface) intended to interact with the user. In your case the dialog collects user input (a file path).

That's why this logic belongs to the view => instead of having OpenFile_Clicked in your MainViewModel only to show the OpenFileDialog to the user, OpenFile_Clicked should be an event handler in the code-behind of the UI e.g., MainWindow.xaml.cs.

This event handler shows the dialog and passes the result (the picked file path) to the MainViewModel (using data binding, command pattern or via direct reference).

"Wouldn't a dependency on the wpf project ruin the entire mvvm
pattern?!"

No, not the reference to a WPF project would ruin the MVVM pattern, but the reference to the OpenFileDialog class.

If you would follow the previously explained principle and remove the dependency to OpenFileDialog from your MainViewModel, you will get back the advantages of MVVM.

In other words: don't use OpenFileDialog or any other view component in MainViewModel at all.

"How am I supposed to call OpenFileDialog in my ViewModel when the
Core library has to be of type .NET Standard?"

Again: don't use OpenFileDialog or any other view component in MainViewModel at all:

MainWindow.xaml.cs (WPF .NET Core assembly)

partial class MainWindow
{
// Event handler for Button.Click
private void OnOpenDialogButton_Click(object sender, EventArgs e)
{
var dialog = new System.Windows.Forms.OpenFileDialog();
dialog.Show();
string selectedFilePath = dialog.FileName;

this.ViewModel.HandleSelectedFile(selectedFilePath);
}
}

MainViewModel.cs (.NET Standard library assembly)

public void HandleSelectedFile(string filePath)
{
// TODO::Handle file e.g. open and read
}

Using OpenFileDialog forces the referencing assembly to give up the .NET Standard compliance. Because OpenFileDialog is a class defined in System.Windows.Forms, the referencing assembly requires to use either the .NET Framework or the .NET Core library.

Following the above example will remove this dependency from MainViewModel and makes your library to comply to .NET Standard again.



"The MvvmCross Inversion of Control documentation looked useful to me
but I still don't get how this would work in my case."

Inversion of Control tackles a different problem and has nothing to do with MVVM or showing dialogs from your view model (don't do this). It enables an application to be extensible by exchanging parts.

Since you are using .NET Core you can use the build in IoC container: Overview of dependency injection

BrowseFolderDialog - Does this break MVVM?

With MVVM, what's considered a pattern vs. an anti-pattern is highly subjective and often equates to religious dogma.

MVVM is a great pattern because it helps you separate concerns, allows you to more easily test your code, and opens up the possibility of easily changing your UI in the future.

How you handle the FolderBrowserDialog and other things similar to it, depend on your app and your particular needs.

If your app is small, or you're only calling the dialog once, or you're not unit testing your view-model, then just call the FolderBrowserDialog from your view-model and be done with it.

However, if strict separation of concerns is important to you and your app, then consider creating a service class to handle folder dialog work that you can call from your app's view-model.

Service Class

This is a simple public class that gives your app's view-models an API to open a FolderBrowserDialog. Here is a super-simple example:

public class FolderBrowserDialogService
{
public FolderBrowserResponse ShowFolderBrowserDialog()
{
var dialog = new FolderBrowserDialog();
var result = dialog.ShowDialog();

// TODO: Convert result to FolderBrowserResponse
// FolderBrowserResponse is a custom type you create so your view-model
// doesn't know about DialogResult

return folderBrowserResponse;
}
}

Then, in your app's view-model, you can call the service class like so:

var service = new FolderBrowserDialogService();
var result = service.ShowFolderBrowserDialog();

//TODO: Process the result...

I hope this gives you some ideas.



Related Topics



Leave a reply



Submit