web-dev-qa-db-fra.com

WPF OpenFileDialog avec le modèle MVVM?

Je viens de commencer à apprendre le modèle MVVM pour WPF. J'ai frappé un mur: que faites-vous lorsque vous devez montrer un OpenFileDialog?

Voici un exemple d'interface utilisateur sur laquelle j'essaie de l'utiliser:

alt text

Lorsque le bouton Parcourir est cliqué, un OpenFileDialog doit être affiché. Lorsque l'utilisateur sélectionne un fichier dans OpenFileDialog, le chemin du fichier doit être affiché dans la zone de texte.

Comment puis-je faire cela avec MVVM?

Mise à jour: Comment puis-je faire cela avec MVVM et le rendre testable unitaire? La solution ci-dessous ne fonctionne pas pour les tests unitaires.

94

Ce que je fais généralement est de créer une interface pour un service d'application qui exécute cette fonction. Dans mes exemples, je suppose que vous utilisez quelque chose comme MVVM Toolkit ou quelque chose de similaire (afin que je puisse obtenir un ViewModel de base et un RelayCommand).

Voici un exemple d'une interface extrêmement simple pour effectuer des opérations de base IO comme OpenFileDialog et OpenFile. Je les montre toutes les deux ici, donc vous ne pensez pas que je vous suggère de créer une interface avec une pour contourner ce problème.

public interface IOService
{
     string OpenFileDialog(string defaultPath);

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

Dans votre application, vous fourniriez une implémentation par défaut de ce service. Voici comment vous le consommeriez.

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

C'est donc assez simple. Maintenant pour la dernière partie: la testabilité. Celui-ci devrait être évident, mais je vais vous montrer comment faire un test simple pour cela. J'utilise Moq pour le stubbing, mais vous pouvez bien sûr utiliser ce que vous voulez.

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

Cela fonctionnera probablement pour vous.

Il existe une bibliothèque sur CodePlex appelée "SystemWrapper" ( http://systemwrapper.codeplex.com ) qui pourrait vous éviter d'avoir à faire un beaucoup de ceci sorte de chose. Il semble que FileDialog ne soit pas encore pris en charge, vous devrez donc certainement écrire une interface pour celle-ci.

J'espère que cela t'aides.

Modifier :

Je crois me souvenir que vous avez préféré Isolateur TypeMock pour votre faux cadre. Voici le même test utilisant 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);
}

J'espère que cela vous sera utile également.

90
Anderson Imes

WPF Application Framework (WAF) fournit une implémentation pour Open et SaveFileDialog.

L'exemple d'application Writer montre comment les utiliser et comment le code peut être testé unitaire.

4
jbe

De mon point de vue, la meilleure option est la bibliothèque de prisme et InteractionRequests. L'action pour ouvrir la boîte de dialogue reste dans le xaml et est déclenchée à partir de Viewmodel tandis que le Viewmodel n'a besoin de rien savoir sur la vue.

Voir également

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

Comme exemple, voir:

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

2
plainionist

Tout d'abord, je vous recommande de commencer avec un WPF MVVM toolkit . Cela vous donne une belle sélection de commandes à utiliser pour vos projets. Une caractéristique particulière qui a été rendue célèbre depuis l'introduction du modèle MVVM est la RelayCommand (il existe de nombreuses autres versions bien sûr, mais je m'en tiens uniquement à la plus couramment utilisée). C'est une implémentation de l'interface ICommand qui vous permet de créer une nouvelle commande dans votre ViewModel.

Revenons à votre question, voici un exemple de l'apparence de votre 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 et RelayCommand sont tous deux issus de MVVM Toolkit . Voici à quoi peut ressembler le XAML.

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

et votre code XAML.CS derrière.

DataContext = new OpenFileDialogVM();
InitializeComponent();

C'est ça.

Au fur et à mesure que vous vous familiarisez avec les commandes, vous pouvez également définir les conditions de désactivation du bouton Parcourir, etc. J'espère que cela vous a indiqué la direction que vous vouliez.

2
Tri Q Tran

À mon avis, la meilleure solution consiste à créer un contrôle personnalisé.

Le contrôle personnalisé que je crée habituellement est composé de:

  • Zone de texte ou bloc de texte
  • Bouton avec une image comme modèle
  • Propriété de dépendance de chaîne dans laquelle le chemin du fichier sera encapsulé

Ainsi, le fichier * .xaml serait comme ceci

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

Et le fichier * .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;
    }
}

À la fin, vous pouvez le lier à votre modèle de vue:

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