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