103

I just started learning the MVVM pattern for WPF. I hit a wall: what do you do when you need to show an OpenFileDialog?

Here's an example UI I'm trying to use it on:

alt text

When the browse button is clicked, an OpenFileDialog should be shown. When the user selects a file from the OpenFileDialog, the file path should be displayed in the textbox.

How can I do this with MVVM?

Update: How can I do this with MVVM and make it unit test-able? The solution below doesn't work for unit testing.

Eliahu Aaron
  • 4,103
  • 5
  • 27
  • 37
Judah Gabriel Himango
  • 58,906
  • 38
  • 158
  • 212

5 Answers5

103

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);
}
starball
  • 20,030
  • 7
  • 43
  • 238
Anderson Imes
  • 25,500
  • 4
  • 67
  • 82
  • This makes sense to me: have some service that does dialogs like this and use that service via an interface in the ViewModel. Excellent, thank you. (p.s. I'll be testing with RhinoMocks, FYI, but I can figure that part out no problem.) – Judah Gabriel Himango Oct 26 '09 at 05:19
  • Shucks. Here I thought I was being fancy. Glad I could help. – Anderson Imes Oct 26 '09 at 05:49
  • Minor typo in second paragraph FYI :). Thanks for the answer! – Jeff Aug 03 '13 at 21:11
  • 11
    Why would you want to open a dialog from the VM? Interacting with the user to get a filename is the responsibility of the view. – Paul Williams Apr 26 '14 at 20:13
  • @ImaDirtyTroll This is the more imperative way to do it in which you fulfill a command from the VM. You could certainly create something that is triggered in the view with a boolean is set to a value and the dialog's result is set to another property in your VM. Whether or not this is valuable for the sake of "correctness" is in the eye of the beholder. I consider this a bit more of an "IO" operation (you'll note that I coupled it with another basic IO operation). The limitations of the system dialog informed my design here, but you could go another way, certainly. – Anderson Imes Apr 27 '14 at 17:50
  • 3
    How to implement IOService on the View side? – Rico Jan 17 '17 at 09:07
  • Where is the implementation of the IOService? – Roshan Feb 28 '23 at 07:59
  • Can someone explain why this doesn't break MVVM? The model is not only aware of the view, but is explicitly creating it. – Dom Mar 09 '23 at 21:04
5

The WPF Application Framework (WAF) provides an implementation for the Open and SaveFileDialog.

The Writer sample application shows how to use them and how the code can be unit tested.

jbe
  • 6,976
  • 1
  • 43
  • 34
3

Firstly I would recommend you to start off with a WPF MVVM toolkit. This gives you a nice selection of Commands to use for your projects. One particular feature that has been made famous since the MVVM pattern's introduction is the RelayCommand (there are manny other versions of course, but I just stick to the most commonly used). Its an implementation of the ICommand interface that allows you to crate a new command in your ViewModel.

Back to your question,here is an example of what your ViewModel may look like.

public class OpenFileDialogVM : ViewModelBase
{
    public static RelayCommand OpenCommand { get; set; }
    private string _selectedPath;
    public string SelectedPath
    {
        get { return _selectedPath; }
        set
        {
            _selectedPath = value;
            RaisePropertyChanged("SelectedPath");
        }
    }

    private string _defaultPath;

    public OpenFileDialogVM()
    {
        RegisterCommands();
    }

    public OpenFileDialogVM(string defaultPath)
    {
        _defaultPath = defaultPath;
        RegisterCommands();
    }

    private void RegisterCommands()
    {
        OpenCommand = new RelayCommand(ExecuteOpenFileDialog);
    }

    private void ExecuteOpenFileDialog()
    {
        var dialog = new OpenFileDialog { InitialDirectory = _defaultPath };
        dialog.ShowDialog();

        SelectedPath = dialog.FileName;
    }
}

ViewModelBase and RelayCommand are both from the MVVM Toolkit. Here is what the XAML may look like.

<TextBox Text="{Binding SelectedPath}" />
<Button Command="vm:OpenFileDialogVM.OpenCommand" >Browse</Button>

and your XAML.CS code behind.

DataContext = new OpenFileDialogVM();
InitializeComponent();

Thats it.

As you get more familiar with the commands, you can also set conditions as to when you want the Browse button to be disabled, etc. I hope that pointed you in the direction you wanted.

Tri Q Tran
  • 5,500
  • 2
  • 37
  • 58
3

In my opinion the best solution is creating a custom control.

The custom control I usually create is composed from:

  • Textbox or textblock
  • Button with an image as template
  • String dependency property where the file path will be wrapped to

So the *.xaml file would be like this

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="Auto"/>
    </Grid.ColumnDefinitions>
    <TextBox Grid.Column="0" Text="{Binding Text, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
    <Button Grid.Column="1" Click="Button_Click">
        <Button.Template>
            <ControlTemplate>
                <Image Grid.Column="1" Source="../Images/carpeta.png"/>
            </ControlTemplate>                
        </Button.Template>
    </Button>        
</Grid>

And the *.cs file:

public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text",
        typeof(string),
        typeof(customFilePicker),
        new FrameworkPropertyMetadata(null,
            FrameworkPropertyMetadataOptions.BindsTwoWayByDefault | FrameworkPropertyMetadataOptions.Journal));

public string Text
{
    get
    {
        return this.GetValue(TextProperty) as String;
    }
    set
    {
        this.SetValue(TextProperty, value);
    }
}

public FilePicker()
{
    InitializeComponent();
}

private void Button_Click(object sender, RoutedEventArgs e)
{
    OpenFileDialog openFileDialog = new OpenFileDialog();

    if(openFileDialog.ShowDialog() == true)
    {
        this.Text = openFileDialog.FileName;
    }
}

At the end you can bind it to your view model:

<controls:customFilePicker Text="{Binding Text}"/>
FredM
  • 454
  • 9
  • 20
2

From my perspective the best option is the prism library and InteractionRequests. The action to open the dialog remains within the xaml and gets triggered from Viewmodel while the Viewmodel does not need to know anything about the view.

See also

https://plainionist.github.io///Mvvm-Dialogs/

As example see:

https://github.com/plainionist/Plainion.Prism/blob/master/src/Plainion.Prism/Interactivity/PopupCommonDialogAction.cs

https://github.com/plainionist/Plainion.Prism/blob/master/src/Plainion.Prism/Interactivity/InteractionRequest/OpenFileDialogNotification.cs

plainionist
  • 2,950
  • 1
  • 15
  • 27
  • I really like this solution. To me it looks elegant, follows the MVVM pattern nicely, separate concerns, and makes the viewmodel very easy to unit test. Well done! – Elfendahl Aug 02 '18 at 09:38