WPF OpenFileDialog с шаблоном MVVM?

Я только начал изучать шаблон MVVM для WPF. Я ударил стену: что вы делаете, когда вам нужно показать OpenFileDialog?

Вот пример пользовательского интерфейса, на котором я пытаюсь использовать его:

alt text

При нажатии кнопки обзора открывается OpenFileDialog. Когда пользователь выбирает файл из OpenFileDialog, путь к файлу должен отображаться в текстовом поле.

Как это сделать с MVVM?

Обновление. Как это сделать с MVVM и сделать его unit test -able? Нижеприведенное решение не работает для модульного тестирования.

Ответ 1

Что я обычно делаю, так это создать интерфейс для службы приложений, которая выполняет эту функцию. В моих примерах я предполагаю, что вы используете что-то вроде MVVM Toolkit или аналогичной вещи (поэтому я могу получить базу ViewModel и RelayCommand).

Вот пример чрезвычайно простого интерфейса для выполнения основных операций ввода-вывода, таких как OpenFileDialog и OpenFile. Я показываю их оба здесь, поэтому вы не думаете, что я предлагаю вам создать один интерфейс с одним методом, чтобы обойти эту проблему.

public interface IOService
{
     string OpenFileDialog(string defaultPath);

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

В вашем приложении вы предоставите стандартную реализацию этой службы. Вот как вы его потребляете.

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

Итак, это довольно просто. Теперь для последней части: тестируемость. Это должно быть очевидно, но я покажу вам, как сделать простой тест для этого. Я использую Moq для stubbing, но вы можете использовать все, что захотите, конечно.

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

Это, вероятно, будет работать для вас.

В CodePlex есть библиотека, называемая "SystemWrapper" (http://systemwrapper.codeplex.com), которая может избавить вас от необходимости делать много этого Такие вещи. Похоже, FileDialog еще не поддерживается, поэтому вам обязательно нужно написать интерфейс для этого.

Надеюсь, что это поможет.

Edit

Кажется, я помню, как вы предпочитаете TypeMock Isolator для вашей фальшивой структуры. Здесь же тест с использованием 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);
}

Надеюсь, что это тоже полезно.

Ответ 2

WPF Application Framework (WAF) предоставляет реализацию для Open и SaveFileDialog.

Пример приложения Writer показывает, как использовать их и как код может быть проверен модулем.

Ответ 3

Во-первых, я бы посоветовал вам начать с WPF MVVM toolkit. Это дает вам хороший набор команд для ваших проектов. Одна особенность, которая была известна, поскольку введение шаблона MVVM является RelayCommand (есть, конечно, и другие варианты, но я просто придерживаюсь наиболее часто используемых). Это реализация интерфейса ICommand, который позволяет вам создавать новую команду в ViewModel.

Вернемся к вашему вопросу, вот пример того, как может выглядеть ваш 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 и RelayCommand являются как из MVVM Toolkit. Вот что может выглядеть XAML.

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

и ваш код XAML.CS позади.

DataContext = new OpenFileDialogVM();
InitializeComponent();

Вот оно.

По мере того, как вы более знакомы с командами, вы также можете установить условия, когда вы хотите, чтобы кнопка "Обзор" была отключена, и т.д. Я надеюсь, что указал на вас в том направлении, в котором вы хотели.

Ответ 4

На мой взгляд, лучшим решением является создание настраиваемого элемента управления.

Пользовательский элемент управления, который я обычно создаю, состоит из:

  • Текстовое поле или текстовый блок
  • Кнопка с изображением в качестве шаблона
  • Свойство String зависимости, в котором путь к файлу будет завернут в

Таким образом, файл *.xaml будет таким образом

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

И файл *.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;
        }
    }

В конце вы можете привязать его к вашей модели представления:

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

Ответ 5

С моей точки зрения лучшим вариантом является библиотека призмы и InteractionRequests. Действие для открытия диалога остается в пределах xaml и запускается из Viewmodel, в то время как ViewModel не нуждается в знании о представлении.

Смотрите также

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

В качестве примера см.

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