2009-10-24 3 views
85

Ho appena iniziato a imparare il pattern MVVM per WPF. Colpisco un muro: cosa fai quando devi mostrare un OpenFileDialog?WPF OpenFileDialog con lo schema MVVM?

Ecco un esempio di interfaccia utente che sto cercando di usarlo su:

alt text

Quando il pulsante Sfoglia viene cliccato, un OpenFileDialog dovrebbe essere mostrato. Quando l'utente seleziona un file da OpenFileDialog, il percorso del file deve essere visualizzato nella casella di testo.

Come posso farlo con MVVM?

Aggiornamento: Come posso fare questo con MVVM e renderlo in grado di testare l'unità? La soluzione seguente non funziona per il test dell'unità.

+0

Questo è un duplicato di: http://stackoverflow.com/questions/1043918/ open-file-dialog-mvvm –

+0

Sicuro. :-) Ho fatto alcune ricerche su SO prima di pubblicare questa domanda, non è venuto fuori. Oh bene. –

+0

Ho votato per chiudere questa domanda, in quanto è un duplicato esatto. –

risposta

84

Quello che faccio in genere è creare un'interfaccia per un servizio applicativo che esegue questa funzione. Nei miei esempi presumo che tu stia utilizzando qualcosa come MVVM Toolkit o qualcosa di simile (quindi posso ottenere un ViewModel di base e un RelayCommand).

Ecco un esempio di un'interfaccia estremamente semplice per eseguire operazioni di I/O di base come OpenFileDialog e OpenFile. Li sto mostrando entrambi qui, quindi non pensi che ti stia suggerendo di creare un'interfaccia con un metodo per aggirare questo problema.

public interface IOService 
{ 
    string OpenFileDialog(string defaultPath); 

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

Nell'applicazione, si fornisce un'implementazione predefinita di questo servizio. Ecco come lo consumeresti.

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

Quindi è piuttosto semplice. Ora per l'ultima parte: testabilità. Questo dovrebbe essere ovvio, ma ti mostrerò come fare un semplice test per questo. Io uso Moq per lo stub, ma puoi usare quello che vuoi ovviamente.

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

Questo probabilmente funzionerà per voi.

C'è una biblioteca su CodePlex chiamato "SystemWrapper" (http://systemwrapper.codeplex.com) che si potrebbe evitare di dover fare un sacco di questo genere di cose. Sembra che FileDialog non sia ancora supportato, quindi dovrai sicuramente scrivere un'interfaccia per quello.

Spero che questo aiuti.

Edit:

Mi sembra di ricordare che si favorendo TypeMock Isolator per il vostro quadro falsificazione. Ecco lo stesso test utilizzando 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); 
} 

Spero che questo sia utile pure.

+0

Questo ha senso per me: avere un servizio che funzioni in finestre di dialogo come questa e utilizzare quel servizio tramite un'interfaccia nel ViewModel. Eccellente grazie. (p.es proverò con RhinoMocks, FYI, ma posso capire quella parte senza problemi.) –

+0

Shucks. Qui ho pensato che mi stavo immaginando. Sono contento di poterti aiutare. –

+0

Errore minore nel secondo paragrafo FYI :). Grazie per la risposta! – Jeff

2

In primo luogo, si consiglia di iniziare con un WPF MVVM toolkit. Questo ti dà una buona selezione di comandi da utilizzare per i tuoi progetti. Una caratteristica particolare che è stata resa famosa dall'introduzione del modello MVVM è RelayCommand (ci sono altre versioni manny ovviamente, ma mi limito ad usare il più comunemente usato). È un'implementazione dell'interfaccia ICommand che consente di creare un nuovo comando nel ViewModel.

Torna alla tua domanda, ecco un esempio di come potrebbe apparire il tuo 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 e RelayCommand sono entrambi dalla MVVM Toolkit. Ecco come può apparire XAML.

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

e il codice XAML.CS dietro.

DataContext = new OpenFileDialogVM(); 
InitializeComponent(); 

Questo è tutto.

Man mano che si acquisisce maggiore familiarità con i comandi, è anche possibile impostare le condizioni per quando si desidera disattivare il pulsante Sfoglia, ecc. Spero che questo indichi la direzione desiderata.

+6

Dovrei riformulare: come posso rendere questa unità testabile? La tua soluzione apparirebbe una finestra di dialogo durante l'esecuzione dei test unitari. –

+0

Se si ospita ViewModel nella propria DLL, non dovrebbe avere un riferimento a PresentationFramework.dll. –

+1

Perché stai usando "RegisterCommands();" invece di scrivere direttamente 'OpenCommand = new RelayCommand (ExecuteOpenFileDialog);'? – aloisdg

4

Il WPF Application Framework (WAF) fornisce un'implementazione per Open e SaveFileDialog.

L'applicazione di esempio Writer mostra come utilizzarli e come è possibile testare il codice dell'unità.

1

A mio parere, la soluzione migliore è la creazione di un controllo personalizzato.

Il controllo personalizzato genere creo è composta da:

  • Casella di testo o blocco di testo
  • Pulsante con un'immagine come modello
  • proprietà di dipendenza
  • String in cui il percorso del file sarà avvolto da

Quindi il file * .xaml sarebbe così

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

e il file * .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; 
     } 
    } 

Alla fine è possibile associare al vostro modello di vista:

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

Dal mio punto di vista l'opzione migliore è la biblioteca prisma e InteractionRequests. L'azione per aprire la finestra di dialogo rimane all'interno di xaml e viene attivata da Viewmodel mentre Viewmodel non ha bisogno di sapere nulla sulla vista.

Vedi anche

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

A titolo di esempio si veda:

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