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
Selectively Use Default JSON Converter
Unity - Checking If the Player Is Grounded Not Working
Creating Custom Picturebox with Draggable and Resizable Selection Window
Does C# Allow Double Semicolon ; ; If So, Are There Any Special Ways
How to Convert Hex to a Byte Array
Thread Parameters Being Changed
How to Ignore Ssl_Client_Socket_Impl.Cc(1061)] Handshake Failed in Selenium C# Chromedriver
How to Make Linq Execute a (Sql) Like Range Search
An Error Occurred During Report Processing. -Rldc Reporting in ASP.NET MVC
Sending an Array of Values to Oracle Procedure to Use in Where in Clause
Refactoring Code to Avoid Anti-Pattern
Datetime Tostring Issue with Formatting Months with "Mm" Specifier
Why \B Does Not Match Word Using .Net Regex
C# Datagridview Checkbox Checked Event
Crud Operations Using Datagridview, Datatable and Dataadapter - Cannot Add New Row to Datagridview
How to Pass an Object to Httpclient.Postasync and Serialize as a JSON Body