Long story short:
The solution is to show the OpenFileDialog
from a class, that is part of the view component.
This means, such a class must be a class that is unknown to the view model and therefore can't be invoked by the view model.
The solution of course can involve code-behind implementations as code-behind is not relevant when evaluating whether a solution complies with MVVM or not.
Beside answering the original question, this answer also tries to provide an alternative view on the general problem why controlling a UI component like a dialog from the view model violates the MVVM design pattern and why workarounds like a dialog service don't solve the problem.
Critique
Almost all answers are following the misconception that MVVM is a pattern, that targets class level dependencies and also requires empty code-behind files. But it's an architectural pattern, that tries to solve a different problem - on application/component level: keeping the business domain decoupled from the UI.
Most people (here on SO) agree that the view model should not handle dialogs, but then propose to move the UI related logic to a helper class (doesn't matter if it's called helper or service), which is still controlled by the view model.
This (especially the service version) is also known as dependency hiding. Many patterns do this. Such patterns are considered anti-patterns. Service Locator is the most famous dependency hiding anti-pattern.
That is why I would call any pattern that involves extraction of the UI logic from the view model class to a separate class an anti-pattern too. It does not solve the original problem: how to change the application structure or class design in order to remove the UI related responsibilities from a view model (or model) class and move it back to a view related class.
In other words: the critical logic remains being a part of the view model component.
For this reason I do not recommend to implement solutions, like the accepted one, that involve a dialog service (whether it is hidden behind an interface or not). If you are concerned to write code that complies with the MVVM design pattern, then simply don't handle dialog views inside the view model.
Introducing an interface to decouple class level dependencies is called Dependency Inversion principle (the D in SOLID) and has nothing to do with MVVM. When it has no relevance in terms of MVVM, it can't solve an MVVM related problem. When room temperature does not have any relevance whether a structure is a four story building or a skyscraper, then changing the room temperature can never turn such building into a skyscraper. Otherwise MVVM would mean: use Dependency Inversion to achieve the initial goal or MVVM is an alias of Dependency Inversion.
MVVM is an architectural pattern while Dependency Inversion is an OO language principle that has nothing to do with structuring an application (aka software architecture). It's not the interface (or the abstract type) that structures an application, but abstract objects or entities like components or modules e.g. Model - View - View Model. An interface can only help to "physically" decouple the components or modules. It doesn't remove component associations.
Why dialogs or handling Window
in general feels so odd?
We have to keep in mind that the dialog controls like Microsoft.Win32.OpenFileDialog
are "low level" native Windows controls. They don't have the necessary API to smoothly integrate them into a MVVM environment. Because of their true nature, they have some limitations the way they can integrate into a high level framework like WPF. Dialogs are a known "weakness" of the framework or high level frameworks in general. Especially when talking about native OS dialogs.
Dialogs are commonly based on the Window
or the abstract CommonDialog
class. The Window
class is a ContentControl
and therefore allows styles and templates to target the content.
One big limitation is that a Window
must always be the root element. You can't add it as a direct child to the visual tree and e.g. show/launch it using triggers.
In case of the CommonDialog
, it can't be added to the visual tree, because it doesn't extend UIElement
.
Therefore Window
or CommonDialog
based types must always be shown from code-behind, which I guess is the reason for the big confusion about handling this kind control properly. In addition many developers, especially beginners that are new to MVVM, have the perception that code-behind violates MVVM.
For some irrational reason they find it less violating to handle the dialog views in the view model component.
Due to it's API a Window
looks like a simple control. But underneath it's a control that hooks into to the low level of the OS. There is a lot of unmanaged code necessary to achieve this. Developers coming from low level C++ frameworks like MFC know exactly what's going on under the hoods.
The Window
and CommonDialog
class are both true hybrids: they are part of the WPF framework, but in order to behave like or actually to be a native OS window, they must be also part of the low level OS infrastructure.
The WPF Window
as well as the CommonDialog
class are basically wrappers around the complex low level OS API. That's why this controls have sometimes a strange feel (from the developer point of view), when compared to common and pure framework controls.
That Window
is sold as a simple ContentControl
is quite deceptive. But since WPF is a high level framework, all low level details are hidden from the API by design.
We have to accept that we have to handle controls based on Window
and CommonDialog
using C# only and that code-behind does not violate any design pattern at all.
If you are willing to waive the native look and feel, you can improve the handling by creating a custom dialog that exposes relevant properties as DependencyProperty
and then set up data bindings in code-behind. Since CommonDialog
doesn't extend DependencyObject
, you are stuck to extend either Window
or Control
(of course Control
comes without the common and expected native window features like OS integration e.g., task bar etc.).
Why MVVM?
Without a sophisticated design pattern or application structure, developers would e.g., directly load database data to a table control of the UI by mixing UI object references with business object references. In such a scenario changing to a different database would break the UI. But more worse changing the UI would require to change the logic that deals with the database. And when changing the logic you would also need to change the related unit tests.
The real application is the business logic and not the fancy GUI.
You want to write unit tests for the business logic - without being forced to test the UI in order to be able to test the business logic.
You want to modify the UI without modifying the business logic.
MVVM is a pattern that solves the problem and allows to decouple the UI from the business logic. It does this more efficiently than the related design patterns MVC and MVP.
We don't want to have the UI bleed into the lower levels of the application. We want to separate data from data presentation and especially their rendering (data views). For example, we want to handle database access without having to care which libraries or controls are used to view the data. That's why we choose MVVM in the first place. For this sake, we can't allow to implement UI logic in components other than the view.
Why moving UI logic from a class named ViewModel
to a separate class still violates MVVM
To apply MVVM you partition the application in three components: model, view and view model. This is not about classes, but application components or a smart application structure.
You may follow the widely spread pattern to name or suffix a class ViewModel
, but you must know that the view model component usually contains more classes of which some are not named or suffixed with ViewModel
- it's a component.
Example: when you extract functionality, like creating a data source collection, from a big class named MainViewModel
and you move this functionality to a class named ItemCreator
, then this class ItemCreator
is also part of the view model (given that the extracted functionality was in the correct component before).
You can project this example onto the often proposed dialog service: extracting the dialog logic from the view model to a dedicated class named DialogService
doesn't move the logic outside the view model component: the view model still depends on this extracted functionality. The view model still participates in the UI logic e.g by controlling the moment the dialog is shown or hidden and by controlling the dialog type itself (e.g., file open, folder select, color picker etc.), which all requires knowledge of the UI's business details.
Things like responsibilities don't simply change because you name this new class a DialogService
instead of e.g. DialogViewModel
.
The DialogService
is therefore an anti-pattern, which hides the real problem: having implemented view model classes, that depend on UI and execute UI logic.
Does writing code-behind violates the MVVM design pattern?
Since MVVM is a design pattern and design patterns are per definition library independent, framework independent and language or compiler independent. Therefore code-behind is not a topic when talking about MVVM.
The code-behind file is absolutely a valid context to write UI code. It's just another file that contains C# code. Code-behind means "a file with a .xaml.cs extension".
Apart from event handlers and name scope related references, it really doesn't matter if you write code into this code-behind file or into a separate class file. Since the code-behind .xaml.cs file is a part of the .xaml it allows to reference the fields created to hold the named XAML elements.
Why does the mantra "No code in code-behind" exist? For people that are new to WPF, UWP or Xamarin, like those skilled and experienced developers coming from frameworks like WinForms, we have to stress that using XAML should be the preferred way to write UI code. Implementing a Style
or DataTemplate
using C# (e.g. in the code-behind file) is too complicated and produces code that is very difficult to read => difficult to understand => difficult to maintain.
XAML is just perfect for such tasks. The visually verbose markup style perfectly expresses the UI's structure far better, than C# could ever do. Despite markup languages like XAML may feel inferior to some or not worth learning it, it's definitely the first choice when implementing GUI. We should strive to write as much GUI code as possible using XAML.
But such considerations are absolutely irrelevant in terms of the MVVM design pattern.
Code-behind is simply a C# language compiler concept, realized by the partial
directive. That's why code-behind has nothing to do with any design pattern. That's why neither XAML nor C# has anything to do with design patterns.
Solution
Like the OP correctly concludes:
"I don't really want to do this [open a file picker dialog] in my
ViewModel(where 'Browse' is referenced via a DelegateCommand).
Because I believe that goes against MVVM methodology.
Some fundamental considerations:
- A dialog is a UI control, a view.
- The handling of a dialog control or a control in general e.g. showing/hiding is UI logic.
- MVVM requirement: the view model does not know about the existence of a UI or users. Because of this, a control flow that requires the view model to actively wait for user input, really requires some re-design.
- Showing a dialog requires knowledge about when to show it and when to close it.
- Showing the dialog requires to know about the UI and user, because the only reason to show a dialog is to interact with the user.
- Showing the dialog requires knowledge about the current UI context (in order to choose the appropriate dialog type).
- It is not the dependency on assemblies or classes like
OpenFileDialog
or UIElement
that breaks the MVVM pattern, but the implementation or reference of UI logic in the view model component or model component (although such a dependency can be a valuable hint).
- For the same reasons it would be wrong to show the dialog from the model component too.
- The only component responsible for UI logic is the view component.
- From an MVVM point of view there is nothing like C#, XAML, C++ or VB.NET. Which means there is nothing like
partial
or the related infamous code-behind file. The concept of code-behind exists to allow the compiler to merge the XAML part of a class with its C# part. After that merge, both files are treated as a single class: it's a pure compiler concept. partial
is the magic that enables to write class code using XAML (the true compiler language is still C# or any other IL compliant language).
ICommand
is an interface of the .NET library and therefore not a topic when talking about MVVM. It's wrong to believe that every action has to be triggered by an ICommand
implementation in the view model.
Events are still a very useful concept that conform with MVVM, as long as the unidirectional dependency between the components is maintained. Always forcing the use of ICommand
instead of using events leads to unnatural and smelly code like the code presented by the OP.
- There is no such "rule" that
ICommand
must only be implemented by a view model class. It can be implemented by a view class too.
In fact, views commonly implement RoutedCommand
(or RoutedUICommand
), which both are implementions of ICommand
, and can also be used to trigger the display of a dialog from e.g., a Window
or any other control.
We have data binding to allow the UI to exchange data (anonymously
from the data source point of view). But since data binding can't
invoke operations (at least in WPF - e.g., UWP allows this), we have
ICommand
and ICommandSource
to realize this.
- Interfaces in general are not a relevant concept of MVVM. Therefore introducing an interface can never solve a MVVM related problem.
- Services or helper classes are not a concept of MVVM. Therefore introducing services or helper classes can never solve a MVVM related problem.
- Class names or type names in general are not relevant in terms of MVVM. Moving view model code to a separate class, even if that class is not named or suffixed with
ViewModel
, can't solve a MVVM related problem.
The solution is to show the OpenFileDialog
from a class, that is part of the view component.
This means, such a class must be a class that is unknown to the view model and therefore can't be invoked by the view model.
This logic could be implemented directly in the code-behind file or inside any other class (file). The implementation can be a simple helper class or a more sophisticated (attached) behavior. The point is: the dialog i.e. the UI component must be triggered by the view component, as this is the only component that contains UI related logic. Since the view model does not have any knowledge of a view, it can't act actively to communicate with the view. Only passive communication is allowed (data binding).
Update
Since people are questioning the fact that you don't need a view model to handle the dialog views, by coming up with extra "complicated" requirements like data validation to proof their point, I am forced to provide more complex examples to address these more complex scenarios (that were not initially requested by the OP).
There exist solutions using the view-model-first approach, which is superior when compared to a "dialog service", but still doesn't solve the problem of badly designed responsibilities or components as all dialog views i.e. UI flow is still controlled by the view model component.
Scenarios
The scenario is a simple input form to collect an user input like an album name and then use the OpenFileDialog
to pick a destination folder where the album name is saved to.
Three simple solutions:
- Very simple and basic scenario, that meets the exact requirements of the question
- Solution that enables to use data validation in the view model. For brevity the implementation of
INotifyDataErrorInfo
is omitted. The property set method only invokes a Validate()
member.
- Another solution that uses an
ICommand
and the ICommandSource.CommandParameter
to send the dialog result to the view model and execute the operation.
Scenario 1
The following example provides a simple and intuitive solution to show the OpenFileDialog
in a MVVM compliant way.
The solution allows the view model to remain unaware of any UI components or logic. All it does is to expose/receive data to/from the view via data binding and to pass this data to the model for data persistence by exposing a public SaveAlbumName()
method to the view:
View
MainWindow.xaml
<Window>
<Window.DataContext>
<MainViewModel />
</Window.DataContext>
<StackPanel>
<TextBox Text="{Binding AlbumName}" />
<Button Click="AppendAlbumNameToFile_OnClick" />
</StackPanel>
</Window>
MainWindow.xaml.cs
partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void AppendAlbumNameToFile_OnClick(object sender, EventArgs e)
{
var dialog = new OpenFileDialog();
if (dialog.ShowDialog() == true)
{
string destinationFilePath = dialog.FileName;
(this.DataContext as MainViewModel)?.SaveAlbumName(destinationFilePath);
}
}
}
View Model
MainViewModel.cs
class MainViewModel : INotifyPropertyChanged
{
private string albumName;
public string AlbumName
{
get => this.albumName;
set
{
this.albumName = value;
OnPropertyChanged();
}
}
private DataRepository DataRepository { get; }
public MainViewModel() => this.DataRepository = new DataRepository();
public void SaveAlbumName(string destinationFilePath)
{
this.DataRepository.SaveData(this.AlbumName, destinationFilePath);
}
}
Scenario 2
A more realistic solution is to alternate the input flow by adding a dedicated TextBox
as input field to collect the destination file path.
A button will open the optional file picker view to allow the user to alternatively browse the filesystem for a destination path.
The browser result is assigned to the TextBox
, which is bound to the view model class. This way the file path can be validated e.g., by implementing INotifyDataErrorInfo
or using binding validation.
The view model action is finally invoked using an ICommand
registered wit a "Save" button:
View
MainWindow.xaml
<Window>
<Window.DataContext>
<MainViewModel />
</Window.DataContext>
<StackPanel>
<TextBox Text="{Binding AlbumName}" />
<TextBox x:Name="FilePathTextBox" Text="{Binding TextValue, ValidatesOnNotifyDataErrors=True}" />
<Button Content="Browse" Click="PickFile_OnClick" />
<Button Content="Save" Command="{Binding SaveAlbumNameCommand}" />
</StackPanel>
</Window>
MainWindow.xaml.cs
partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void PickFile_OnClick(object sender, EventArgs e)
{
var dialog = new OpenFileDialog();
if (dialog.ShowDialog() == true)
{
this.FilePathTextBox.Text = dialog.FileName;
this.FilePathTextBox.GetBindingExpression(TextBox.TextProperty).UpdateSource();
}
}
}
View Model
MainViewModel.cs
class MainViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
{
private string albumName;
public string AlbumName
{
get => this.albumName;
set
{
this.albumName = value;
OnPropertyChanged();
}
}
private string destinationPath;
public string DestinationPath
{
get => this.destinationPath;
set
{
this.destinationPath= value;
OnPropertyChanged();
ValidateDestinationFilePath();
}
}
public ICommand SaveAlbumNameCommand => new RelayCommand(
commandParameter =>
{
ExecuteSaveAlbumName(this.TextValue);
},
commandParameter => true);
private DataRepository DataRepository { get; }
public MainViewModel() => this.DataRepository = new DataRepository();
private void ExecuteSaveAlbumName(string destinationFilePath)
{
this.DataRepository.SaveData(this.AlbumName, destinationFilePath);
}
}
Scenario 3
The following solution uses the ICommandSource.CommandParameter
property to send the dialog result to the view model and the associated ICommandSource.Command
to trigger the action:
View
MainWindow.xaml
<Window x:Name="Window">
<Window.DataContext>
<MainViewModel />
</Window.DataContext>
<StackPanel>
<TextBox Text="{Binding AlbumName}" />
<TextBox x:Name="FilePathTextBox" Text="{Binding TextValue, ValidatesOnNotifyDataErrors=True}" />
<Button Content="Browse" Click="PickFile_OnClick" />
<Button Content="Save"
CommandParameter="{Binding ElementName=Window, Path=DestinationPath}"
Command="{Binding SaveAlbumNameCommand}" />
</StackPanel>
</Window>
MainWindow.xaml.cs
partial class MainWindow : Window
{
public static readonly DependencyProperty DestinationPathProperty = DependencyProperty.Register(
"DestinationPath",
typeof(string),
typeof(MainWindow),
new PropertyMetadata(default(string)));
public string DestinationPath
{
get => (string) GetValue(MainWindow.DestinationPathProperty);
set => SetValue(MainWindow.DestinationPathProperty, value);
}
public MainWindow()
{
InitializeComponent();
}
private void PickFile_OnClick(object sender, EventArgs e)
{
var dialog = new OpenFileDialog();
if (dialog.ShowDialog() == true)
{
this.DestinationPath = dialog.FileName;
}
}
}
View Model
MainViewModel.cs
class MainViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
{
private string albumName;
public string AlbumName
{
get => this.albumName;
set
{
this.albumName = value;
OnPropertyChanged();
}
}
public ICommand SaveAlbumNameCommand => new RelayCommand(
commandParameter =>
{
ExecuteSaveAlbumName(commandParameter as string);
},
commandParameter => true);
private DataRepository DataRepository { get; }
public MainViewModel() => this.DataRepository = new DataRepository();
private void ExecuteSaveAlbumName(string destinationFilePath)
{
this.DataRepository.SaveData(this.AlbumName, destinationFilePath);
}
}