web-dev-qa-db-fra.com

Boîte de dialogue Ouvrir un fichier MVVM

Ok, je voudrais vraiment savoir comment les développeurs MVVM experts gèrent une boîte de dialogue OpenFile dans WPF.

Je ne veux pas vraiment le faire dans mon ViewModel (où 'Browse' est référencé via un DelegateCommand)

void Browse(object param)
{
    //Add code here
    OpenFileDialog d = new OpenFileDialog();

    if (d.ShowDialog() == true)
    {
        //Do stuff
    }
}

Parce que je crois que cela va à l'encontre de la méthodologie MVVM.

Que fais-je?

45
Jose

La meilleure chose à faire ici est d'utiliser un service.

Un service n'est qu'une classe à laquelle vous accédez à partir d'un référentiel central de services, souvent un conteneur IOC. Le service implémente ensuite ce dont vous avez besoin comme OpenFileDialog.

Donc, en supposant que vous avez un IFileDialogService dans un conteneur Unity, vous pouvez faire ...

void Browse(object param)
{
    var fileDialogService = container.Resolve<IFileDialogService>();

    string path = fileDialogService.OpenFileDialog();

    if (!string.IsNullOrEmpty(path))
    {
        //Do stuff
    }
}
36
Cameron MacFarland

J'aurais aimé commenter l'une des réponses, mais hélas, ma réputation n'est pas assez élevée pour le faire.

Un appel tel que OpenFileDialog () viole le modèle MVVM car il implique une vue (boîte de dialogue) dans le modèle de vue. Le modèle de vue peut appeler quelque chose comme GetFileName () (c'est-à-dire si une simple liaison n'est pas suffisante), mais il ne devrait pas se soucier de la façon dont le nom de fichier est obtenu.

10
JAB

J'utilise un service que je peux par exemple passer dans le constructeur de mon viewModel ou résoudre via l'injection de dépendances. par exemple.

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

et une classe l'implémentant, utilisant OpenFileDialog sous le capot. Dans le viewModel, j'utilise uniquement l'interface et je peux donc la simuler/la remplacer si nécessaire.

8
Botz3000

Le ViewModel ne doit pas ouvrir de boîtes de dialogue ni même connaître leur existence. Si le VM est hébergé dans une DLL distincte, le projet ne doit pas avoir de référence à PresentationFramework.

J'aime utiliser une classe d'assistance dans la vue pour les dialogues courants.

La classe d'assistance expose une commande (pas un événement) à laquelle la fenêtre se lie en XAML. Cela implique l'utilisation de RelayCommand dans la vue. La classe d'assistance est un DepencyObject afin de pouvoir se lier au modèle de vue.

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 classe d'assistance a besoin d'une référence à l'instance de ViewModel. Voir le dictionnaire des ressources. Juste après la construction, la propriété ViewModel est définie (dans la même ligne de XAML). C'est lorsque la propriété FileName sur la classe d'assistance est liée à la propriété FileName sur le modèle de vue.

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

Je l'ai résolu pour moi de cette façon:

  • Dans ViewModel J'ai défini une interface et je travaille avec elle dans ViewModel
  • Dans Voir J'ai implémenté cette interface.

CommandImpl n'est pas implémenté dans le code ci-dessous.

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

Vue:

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

Avoir un service, c'est comme ouvrir une vue depuis le viewmodel. J'ai une propriété de dépendance en vue, et sur le chnage de la propriété, j'ouvre FileDialog et lis le chemin, met à jour la propriété et par conséquent la propriété liée de la machine virtuelle

2
Jilt