2009-10-24 26 views
85

Acabo de empezar a aprender el patrón MVVM para WPF. Golpeé una pared: ¿qué haces cuando necesitas mostrar un OpenFileDialog?WPF OpenFileDialog con el patrón MVVM?

He aquí un ejemplo de interfaz de usuario que trato de usarlo en:

alt text

Cuando se hace clic en el botón Examinar, un OpenFileDialog se debe mostrar. Cuando el usuario selecciona un archivo de OpenFileDialog, la ruta del archivo debe mostrarse en el cuadro de texto.

¿Cómo puedo hacer esto con MVVM?

Actualización: ¿Cómo puedo hacer esto con MVVM y hacerlo unitario de prueba? La solución a continuación no funciona para las pruebas unitarias.

+0

Este es un duplicado de: http://stackoverflow.com/questions/1043918/ open-file-dialog-mvvm –

+0

Claro que sí. :-) Hice algunas búsquedas en SO antes de publicar esta pregunta, no apareció. Oh bien. –

+0

He votado para cerrar esta pregunta, ya que es un duplicado exacto. –

Respuesta

84

Lo que generalmente hago es crear una interfaz para un servicio de aplicación que realiza esta función. En mis ejemplos, supongo que está utilizando algo como MVVM Toolkit o algo similar (para que pueda obtener un ViewModel base y un RelayCommand).

Aquí hay un ejemplo de una interfaz extremadamente simple para realizar operaciones básicas de IO como OpenFileDialog y OpenFile. Los mostraré aquí, así que no creo que sugiera que cree una interfaz con un método para evitar este problema.

public interface IOService 
{ 
    string OpenFileDialog(string defaultPath); 

    //Other similar untestable IO operations 
    Stream OpenFile(string path); 
} 

En su aplicación, debe proporcionar una implementación predeterminada de este servicio. Aquí es cómo lo consumirías.

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

Así que eso es bastante simple. Ahora para la última parte: comprobabilidad. Este debería ser obvio, pero te mostraré cómo hacer una prueba simple para esto. Yo uso Moq para anotación, pero puedes usar lo que quieras, por supuesto.

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

Esto probablemente funcione para usted.

Hay una biblioteca a cabo en CodePlex llamado "SystemWrapper" (http://systemwrapper.codeplex.com) que pueden salvar de tener que hacer un montón de este tipo de cosas. Parece que FileDialog aún no es compatible, por lo que definitivamente tendrá que escribir una interfaz para ese.

Espero que esto ayude.

Editar:

creo recordar que favorece Typemock aislador para su marco falsificación. Aquí está la misma prueba utilizando aislador:

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

la esperanza que esto es útil también.

+0

Esto tiene sentido para mí: tener algún servicio que realice diálogos como este y usar ese servicio a través de una interfaz en ViewModel. Excelente gracias. (p.s.yo probaré con RhinoMocks, para su información, pero puedo descifrar esa parte sin problema.) –

+0

Shucks. Aquí pensé que estaba siendo elegante. Me alegro de poder ayudar. –

+0

Error menor en el segundo párrafo FYI :). ¡Gracias por la respuesta! – Jeff

2

En primer lugar, le recomendaría que empiece con WPF MVVM toolkit. Esto le ofrece una buena selección de Comandos para usar en sus proyectos. Una característica particular que se ha hecho famosa desde la introducción del patrón MVVM es el RelayCommand (hay otras versiones de Manny, por supuesto, pero me limito a las más utilizadas). Es una implementación de la interfaz de ICommand que le permite crear un nuevo comando en su ViewModel.

Volver a su pregunta, aquí hay un ejemplo de cómo se verá su ViewModel.

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 y RelayCommand son ambos de la MVVM Toolkit. Aquí está lo que el XAML puede parecer.

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

y su código XAML.CS detrás.

DataContext = new OpenFileDialogVM(); 
InitializeComponent(); 

Eso es todo.

A medida que se familiarice con los comandos, también puede establecer las condiciones en cuanto a cuándo desea deshabilitar el botón Examinar, etc. Espero que lo haya apuntado en la dirección que deseaba.

+6

Debería volver a formular: ¿cómo puedo hacer que esta unidad sea comprobable? Su solución mostraría un cuadro de diálogo al ejecutar las pruebas unitarias. –

+0

Si aloja su ViewModel en su propia DLL, no debería tener una referencia a PresentationFramework.dll. –

+1

¿Por qué está utilizando 'RegisterCommands();' en lugar de escribir directamente 'OpenCommand = new RelayCommand (ExecuteOpenFileDialog);'? – aloisdg

4

El WPF Application Framework (WAF) proporciona una implementación para Open y SaveFileDialog.

La aplicación de ejemplo Writer muestra cómo usarlos y cómo se puede probar el código de la unidad.

1

En mi opinión, la mejor solución es crear un control personalizado.

El control personalizado Por lo general crear se compone de:

  • cuadro de texto o bloque de texto
  • Botón con una imagen como plantilla
  • cadena propiedad de dependencia en la ruta del archivo será envuelto a

Así que el archivo * .xaml sería así

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

y el archivo * .cs:

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

Al final se puede enlazar a su modelo de vista:

<controls:customFilePicker Text="{Binding Text}"}/> 
0

Desde mi punto de vista, la mejor opción es la biblioteca y InteractionRequests prisma. La acción para abrir el diálogo permanece dentro del xaml y se activa desde Viewmodel, mientras que el modelo de vista no necesita saber nada sobre la vista.

Ver también

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

Como ejemplo, ver:

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