Question

Ok I really would like to know how expert MVVM developers handle an openfile dialog in WPF.

I don't really want to do this in my ViewModel(where 'Browse' is referenced via a DelegateCommand)

void Browse(object param)
{
    //Add code here
    OpenFileDialog d = new OpenFileDialog();

    if (d.ShowDialog() == true)
    {
        //Do stuff
    }
}

Because I believe that goes against MVVM methodology.

What do I do?

Was it helpful?

Solution

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
    }
}

OTHER TIPS

I would have liked to comment on one of the answers, but alas, my reputation is not high enough to do so.

Having a call such as OpenFileDialog() violates the MVVM pattern because it implies a view (dialog) in the view model. The view model can call something like GetFileName() (that is, if simple binding is not sufficient), but it should not care how the file name is obtained.

I use a service which i for example can pass into the constructor of my viewModel or resolve via dependency injection. e.g.

public interface IOpenFileService
{
    string FileName { get; }
    bool OpenFileDialog()
}

and a class implementing it, using OpenFileDialog under the hood. In the viewModel, i only use the interface and thus can mock/replace it if needed.

The ViewModel should not open dialogs or even know of their existence. If the VM is housed in a separate DLL, the project should not have a reference to PresentationFramework.

I like to use a helper class in the view for common dialogs.

The helper class exposes a command (not an event) which the window binds to in XAML. This implies the use of RelayCommand within the view. The helper class is a DepencyObject so it can bind to the view model.

class DialogHelper : DependencyObject
{
    public ViewModel ViewModel
    {
        get { return (ViewModel)GetValue(ViewModelProperty); }
        set { SetValue(ViewModelProperty, value); }
    }

    public static readonly DependencyProperty ViewModelProperty =
        DependencyProperty.Register("ViewModel", typeof(ViewModel), typeof(DialogHelper),
        new UIPropertyMetadata(new PropertyChangedCallback(ViewModelProperty_Changed)));

    private static void ViewModelProperty_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (ViewModelProperty != null)
        {
            Binding myBinding = new Binding("FileName");
            myBinding.Source = e.NewValue;
            myBinding.Mode = BindingMode.OneWayToSource;
            BindingOperations.SetBinding(d, FileNameProperty, myBinding);
        }
    }

    private string FileName
    {
        get { return (string)GetValue(FileNameProperty); }
        set { SetValue(FileNameProperty, value); }
    }

    private static readonly DependencyProperty FileNameProperty =
        DependencyProperty.Register("FileName", typeof(string), typeof(DialogHelper),
        new UIPropertyMetadata(new PropertyChangedCallback(FileNameProperty_Changed)));

    private static void FileNameProperty_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        Debug.WriteLine("DialogHelper.FileName = {0}", e.NewValue);
    }

    public ICommand OpenFile { get; private set; }

    public DialogHelper()
    {
        OpenFile = new RelayCommand(OpenFileAction);
    }

    private void OpenFileAction(object obj)
    {
        OpenFileDialog dlg = new OpenFileDialog();

        if (dlg.ShowDialog() == true)
        {
            FileName = dlg.FileName;
        }
    }
}

The helper class needs a reference to the ViewModel instance. See the resource dictionary. Just after construction, the ViewModel property is set (in the same line of XAML). This is when the FileName property on the helper class is bound to the FileName property on the view model.

<Window x:Class="DialogExperiment.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:DialogExperiment"
        xmlns:vm="clr-namespace:DialogExperimentVM;assembly=DialogExperimentVM"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <vm:ViewModel x:Key="viewModel" />
        <local:DialogHelper x:Key="helper" ViewModel="{StaticResource viewModel}"/>
    </Window.Resources>
    <DockPanel DataContext="{StaticResource viewModel}">
        <Menu DockPanel.Dock="Top">
            <MenuItem Header="File">
                <MenuItem Header="Open" Command="{Binding Source={StaticResource helper}, Path=OpenFile}" />
            </MenuItem>
        </Menu>
    </DockPanel>
</Window>

Having a service is like opening up a view from viewmodel. I have a Dependency property in view, and on the chnage of the property, I open up FileDialog and read the path, update the property and consequently the bound property of the VM

I have solved it for me this way:

  • In ViewModel I have defined an interface and work with it in ViewModel
  • In View I have implemented this interface.

CommandImpl is not implemented in code below.

ViewModel:

namespace ViewModels.Interfaces
{
    using System.Collections.Generic;
    public interface IDialogWindow
    {
        List<string> ExecuteFileDialog(object owner, string extFilter);
    }
}

namespace ViewModels
{
    using ViewModels.Interfaces;
    public class MyViewModel
    {
        public ICommand DoSomeThingCmd { get; } = new CommandImpl((dialogType) =>
        {
            var dlgObj = Activator.CreateInstance(dialogType) as IDialogWindow;
            var fileNames = dlgObj?.ExecuteFileDialog(null, "*.txt");
            //Do something with fileNames..
        });
    }
}

View:

namespace Views
{
    using ViewModels.Interfaces;
    using Microsoft.Win32;
    using System.Collections.Generic;
    using System.Linq;
    using System.Windows;

    public class OpenFilesDialog : IDialogWindow
    {
        public List<string> ExecuteFileDialog(object owner, string extFilter)
        {
            var fd = new OpenFileDialog();
            fd.Multiselect = true;
            if (!string.IsNullOrWhiteSpace(extFilter))
            {
                fd.Filter = extFilter;
            }
            fd.ShowDialog(owner as Window);

            return fd.FileNames.ToList();
        }
    }
}

XAML:

<Window

    xmlns:views="clr-namespace:Views"
    xmlns:viewModels="clr-namespace:ViewModels"
>    
    <Window.DataContext>
        <viewModels:MyViewModel/>
    </Window.DataContext>

    <Grid>
        <Button Content = "Open files.." Command="{Binding DoSomeThingCmd}" CommandParameter="{x:Type views:OpenFilesDialog}"/>
    </Grid>
</Window>
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top