How to use a FolderBrowserDialog from a WPF application with MVVM
Asked Answered
B

6

5

I'm trying to use the FolderBrowserDialog from my WPF application - nothing fancy. I don't much care that it has the Windows Forms look to it.

I found a question with a suitable answer (How to use a FolderBrowserDialog from a WPF application), except I'm using MVVM.

This was the answer I "implemented", except I can't get the window object and I'm just calling ShowDialog() without any parameters.

The problem is this:

var dlg = new FolderBrowserDialog();
System.Windows.Forms.DialogResult result = dlg.ShowDialog(this.GetIWin32Window());

In my ViewModel there the this has no GetIWin32Window() method for me to get the Window context.

Any ideas on how to make this work?

Bartholemy answered 5/9, 2012 at 20:16 Comment(0)
U
4

First, you could use the ShowDialog signature that does not require a window.

var dlg = new FolderBrowserDialog();
DialogResult result = dlg.ShowDialog();

Second, you could send the main window of the Application as the owning window.

var dlg = new FolderBrowserDialog();
DialogResult result = dlg.ShowDialog(Application.Current.MainWindow.GetIWin32Window());

The second option might not be considered very MVVMish.

See the answer by @Dr. ABT in this question for a way to inject a pointer to your View into your ViewModel (not sure if this is a good idea or a bad idea, but I'm not going to let that stop me) With this technique, you would have access in your VM to the corresponding View if you really want to make that View be the owner of the FolderBrowserDialog.

@ChrisDD is right about defining an interface and wrapping FolderBrowserDialog. That is how we do it:

  public interface IFolderBrowserDialog
  {
    string Description { get; set; }
    Environment.SpecialFolder RootFolder { get; set; }
    string SelectedPath { get; set; }
    bool ShowNewFolderButton { get; set; }
    bool? ShowDialog();
    bool? ShowDialog(Window owner);
  }

  //Decorated for MEF injection
  [Export(typeof(IFolderBrowserDialog))]
  [PartCreationPolicy(CreationPolicy.NonShared)]
  internal class WindowsFormsFolderBrowserDialog : IFolderBrowserDialog
  {
    private string _description;
    private string _selectedPath;

    [ImportingConstructor]
    public WindowsFormsFolderBrowserDialog()
    {
      RootFolder = System.Environment.SpecialFolder.MyComputer;
      ShowNewFolderButton = false;
    }

    #region IFolderBrowserDialog Members

    public string Description
    {
      get { return _description ?? string.Empty; }
      set { _description = value; }
    }

    public System.Environment.SpecialFolder RootFolder { get; set; }

    public string SelectedPath
    {
      get { return _selectedPath ?? string.Empty; }
      set { _selectedPath = value; }
    }

    public bool ShowNewFolderButton { get; set; }

    public bool? ShowDialog()
    {
      using (var dialog = CreateDialog())
      {
        var result = dialog.ShowDialog() == DialogResult.OK;
        if (result) SelectedPath = dialog.SelectedPath;
        return result;
      }
    }

    public bool? ShowDialog(Window owner)
    {
      using (var dialog = CreateDialog())
      {
        var result = dialog.ShowDialog(owner.AsWin32Window()) == DialogResult.OK;
        if (result) SelectedPath = dialog.SelectedPath;
        return result;
      }
    }
    #endregion

    private FolderBrowserDialog CreateDialog()
    {
      var dialog = new FolderBrowserDialog();
      dialog.Description = Description;
      dialog.RootFolder = RootFolder;
      dialog.SelectedPath = SelectedPath;
      dialog.ShowNewFolderButton = ShowNewFolderButton;
      return dialog;
    }
  }

  internal static class WindowExtensions
  {
    public static System.Windows.Forms.IWin32Window AsWin32Window(this Window window)
    {
      return new Wpf32Window(window);
    }
  }

  internal class Wpf32Window : System.Windows.Forms.IWin32Window
  {
    public Wpf32Window(Window window)
    {
      Handle = new WindowInteropHelper(window).Handle;
    }

    #region IWin32Window Members

    public IntPtr Handle { get; private set; }

    #endregion
  }

Then we make the VM/Command where we want to use the FolderBrowser import IFolderBrowserDialog. In application, IFolderBrowserDialog.ShowDialog shows the dialog. In unit test, we mock IFolderBrowserDialog so we can verify that it was called with correct parameters and/or send the selected folder back to the sut so that the test can continue.

Univalent answered 5/9, 2012 at 20:46 Comment(0)
U
2

If you're determined to use FolderBrowserDialog, I'd use this kind of design.

First, create a DependencyProperty on your View to expose its handle.

public static readonly DependencyProperty WindowHandleProperty =
    DependencyProperty.Register("WindowHandle", typeof(System.Windows.Forms.IWin32Window), typeof(MainWindow), new PropertyMetadata(null));

// MainWindow.cs
public System.Windows.Forms.IWin32Window WindowHandle
{
    get { return (System.Windows.Forms.IWin32Window)GetValue(WindowHandleProperty); }
    set { SetValue(WindowHandleProperty, value); }
}

Now, when your window loads, you can retrieve the handle using the extensions provided in the question you linked to:

// MainWindow.cs
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
    var binding = new Binding();
    binding.Path = new PropertyPath("WindowHandle");
    binding.Mode = BindingMode.OneWayToSource;
    SetBinding(WindowHandleProperty, binding);

    WindowHandle = this.GetIWin32Window();
}

So, you are binding one-way to source using a "WindowHandle" property. So if your ViewModel has a WindowHandle property, it will be kept up to date with the valid IWin32Handle for the related view:

// ViewModel.cs
private System.Windows.Forms.IWin32Window _windowHandle; 
public System.Windows.Forms.IWin32Window WindowHandle
{
    get
    {
        return _windowHandle;
    }
    set
    {
        if (_windowHandle != value)
        {
            _windowHandle = value;
            RaisePropertyChanged("WindowHandle");
        }
    }
}

This is a good solution because you're not hard-coding one ViewModel to be paired with one specific View. If your use multiple Views with the same ViewModel, it should just work. If you create a new View but you don't implement the DependencyProperty, it will just operate with a null handle.

EDIT:

As a side note, have you actually tested just not providing an IWin32Owner parameter? For me, it still automatically opens as a modal dialog for the application and blocks the user from interacting with all of the application's windows. Is there something else you need it to do instead?

Unchristian answered 5/9, 2012 at 21:18 Comment(0)
D
1

MVVM + WinForms FolderBrowserDialog as behavior

public class FolderDialogBehavior : Behavior<Button>
{
    public string SetterName { get; set; }

    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.Click += OnClick;
    }

    protected override void OnDetaching()
    {
        AssociatedObject.Click -= OnClick;
    }

    private void OnClick(object sender, RoutedEventArgs e)
    {
        var dialog = new FolderBrowserDialog();
        var result = dialog.ShowDialog();
        if (result == DialogResult.OK && AssociatedObject.DataContext != null)
        {
            var propertyInfo = AssociatedObject.DataContext.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public)
            .Where(p => p.CanRead && p.CanWrite)
            .Where(p => p.Name.Equals(SetterName))
            .First();

            propertyInfo.SetValue(AssociatedObject.DataContext, dialog.SelectedPath, null);
        }
    }
}

Usage

     <Button Grid.Column="3" Content="...">
            <Interactivity:Interaction.Behaviors>
                <Behavior:FolderDialogBehavior SetterName="SomeFolderPathPropertyName"/>
            </Interactivity:Interaction.Behaviors>
     </Button>

Blogpost: http://kostylizm.blogspot.ru/2014/03/wpf-mvvm-and-winforms-folder-dialog-how.html

Declinatory answered 9/4, 2014 at 10:37 Comment(0)
V
0

MVVM way:

define a new interface for FolderBrowserDialog. Create a new class & implement that interface. (Implementing is done with actual FolderBrowserDialog class).

This way you will not tie MVVM to specific implementation and the actual logic can be later tested.

Ventriculus answered 5/9, 2012 at 20:50 Comment(2)
He still needs to pass an IWin32Window to the FolderBrowserDialog for it to show as a proper modal dialog. He wants to know how to program a ViewModel to be able to retrieve the IWin32Window of its current view.Unchristian
Yes and that is implementation detail. Like I said, if you implement interface, implementation can do whatever it wants to do. Viewmodel doesn't need to know anything. You can obtain current view by Application.Current.Windows.Where(x => x.IsActive = true)Ventriculus
A
0

To handle any kind of dialog stuff within the mvvm pattern, you should go with a kind of Dialog-Service. In this post you will find some hints to go with this approach.

Putting dialog stuff into a service keeps the mvvm pattern untouched. The service takes care of all the creation of the dialogs and can provide the results. The view-model just calls methods and subscribes events provided by a service.

if you use dependency injection for the service (interface), you get the advantage to keep you solution testable by mocking. Or you could replace the forms folderbrowserdialog if there will be a wpf one.

Antimatter answered 5/9, 2012 at 23:10 Comment(0)
R
0

It's handy to use Behaviors in this case. Add a dependency property, and you can use that to bind the value from the dialog to a property in your viewmodel.

public class FolderBrowserDialogBehavior : Behavior<System.Windows.Controls.Button>
{
    /// <summary>
    /// Dependency Property for Path
    /// </summary>
    public static readonly DependencyProperty PathProperty =
        DependencyProperty.Register(nameof(Path), typeof(string), typeof(FolderBrowserDialogBehavior));

    /// <summary>
    /// Property wrapper for Path
    /// </summary>
    public string Path
    {
        get => (string) this.GetValue(PathProperty);
        set => this.SetValue(PathProperty, value);
    }

    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.Click += OnClick;
    }

    protected override void OnDetaching()
    {
        AssociatedObject.Click -= OnClick;
    }

    /// <summary>
    /// Triggered when the Button is clicked.
    /// </summary>
    private void OnClick(object sender, RoutedEventArgs e)
    {
        using (var dialog = new FolderBrowserDialog())
        {
            try
            {
                if (dialog.ShowDialog() == DialogResult.OK)
                {
                    FilePath = dialog.SelectedPath;
                }
            }
            catch (Exception ex)
            {
                //Do something...
            }
        }
    }
}

In the view;

<Button ...>
    <i:Interaction.Behaviors>
        <behaviors:FolderBrowserDialogBehavior FilePath="{Binding Path=SomePropertyInViewModel, Mode=TwoWay}"/>
    </i:Interaction.Behaviors>
</Button>
Renewal answered 27/2, 2019 at 14:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.