WPF OpenFileDialog с шаблоном MVVM? - PullRequest
93 голосов
/ 25 октября 2009

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

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

alt text

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

Как я могу сделать это с MVVM?

Обновление : Как я могу сделать это с MVVM и сделать его способным к юнит-тестированию? Приведенное ниже решение не работает для модульного тестирования.

Ответы [ 5 ]

90 голосов
/ 26 октября 2009

Что я обычно делаю, это создаю интерфейс для службы приложений, которая выполняет эту функцию. В моих примерах я предполагаю, что вы используете что-то вроде 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 для заглушки, но вы, конечно, можете использовать все, что захотите.

[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 не поддерживается тем не менее, вам определенно придется написать интерфейс для этого.

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

Редактировать

Кажется, я помню, что вы предпочитали 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);
}

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

4 голосов
/ 26 октября 2009

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

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

2 голосов
/ 18 января 2018

С моей точки зрения, лучшим вариантом является библиотека призмы и запросы 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

2 голосов
/ 25 октября 2009

Во-первых, я бы порекомендовал вам начать с 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();

Вот так.

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

1 голос
/ 24 июня 2016

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

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

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

Так что файл * .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}"/>
...