wpf mvvm openfiledialog

wpf - Abrir archivo de diálogo MVVM



openfiledialog (6)

El ViewModel no debe abrir diálogos o incluso saber de su existencia. Si la VM está alojada en una DLL separada, el proyecto no debería tener una referencia a PresentationFramework.

Me gusta usar una clase de ayuda en la vista para los cuadros de diálogo comunes.

La clase auxiliar expone un comando (no un evento) al que se une la ventana en XAML. Esto implica el uso de RelayCommand dentro de la vista. La clase auxiliar es DepencyObject por lo que puede vincularse al modelo de vista.

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

La clase auxiliar necesita una referencia a la instancia de ViewModel. Ver el diccionario de recursos. Justo después de la construcción, se establece la propiedad ViewModel (en la misma línea de XAML). Esto es cuando la propiedad FileName en la clase auxiliar está vinculada a la propiedad FileName en el modelo de vista.

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

Ok, realmente me gustaría saber cómo los desarrolladores expertos de MVVM manejan un diálogo de archivo abierto en WPF.

Realmente no quiero hacer esto en mi ViewModel (donde se hace referencia a ''Browse'' a través de un DelegateCommand)

void Browse(object param) { //Add code here OpenFileDialog d = new OpenFileDialog(); if (d.ShowDialog() == true) { //Do stuff } }

Porque creo que va en contra de la metodología MVVM.

¿Qué debo hacer?


Lo he resuelto de esta manera:

  • En ViewModel he definido una interfaz y trabajo con ella en ViewModel
  • En View he implementado esta interfaz.

CommandImpl no se implementa en el código a continuación.

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.. }); } }

Ver:

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>


Lo mejor que puedes hacer aquí es usar un servicio.

Un servicio es solo una clase a la que accede desde un repositorio central de servicios, a menudo un contenedor de IOC. El servicio luego implementa lo que necesita, como OpenFileDialog.

Entonces, suponiendo que tiene un IFileDialogService en un contenedor de Unity, podría hacer ...

void Browse(object param) { var fileDialogService = container.Resolve<IFileDialogService>(); string path = fileDialogService.OpenFileDialog(); if (!string.IsNullOrEmpty(path)) { //Do stuff } }


Me hubiera gustado comentar una de las respuestas, pero, por desgracia, mi reputación no es lo suficientemente alta como para hacerlo.

Tener una llamada como OpenFileDialog () viola el patrón MVVM porque implica una vista (diálogo) en el modelo de vista. El modelo de vista puede llamar a algo como GetFileName () (es decir, si el enlace simple no es suficiente), pero no debería importar cómo se obtiene el nombre del archivo.


Tener un servicio es como abrir una vista desde el modelo de vista. Tengo una propiedad Dependency a la vista, y en el cambio de la propiedad, abro FileDialog y leo la ruta, actualizo la propiedad y consecuentemente la propiedad enlazada de la VM


Utilizo un servicio que, por ejemplo, puedo pasar al constructor de mi viewModel o resolverlo a través de la inyección de dependencia. p.ej

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

y una clase que lo implementa, usando OpenFileDialog bajo el capó. En ViewModel, solo uso la interfaz y, por lo tanto, puedo simular / reemplazar si es necesario.