我刚开始学习 WPF 的 MVVM 模式。我碰壁了:当你需要展示的时候你会怎么做OpenFileDialog
?
这是我尝试在其上使用的示例 UI:
单击浏览按钮时,OpenFileDialog
应显示一个。当用户从 中选择文件时OpenFileDialog
,文件路径应显示在文本框中。
我怎样才能用 MVVM 做到这一点?
更新:如何使用 MVVM 执行此操作并使其可进行单元测试?下面的解决方案不适用于单元测试。
我刚开始学习 WPF 的 MVVM 模式。我碰壁了:当你需要展示的时候你会怎么做OpenFileDialog
?
这是我尝试在其上使用的示例 UI:
单击浏览按钮时,OpenFileDialog
应显示一个。当用户从 中选择文件时OpenFileDialog
,文件路径应显示在文本框中。
我怎样才能用 MVVM 做到这一点?
更新:如何使用 MVVM 执行此操作并使其可进行单元测试?下面的解决方案不适用于单元测试。
我通常做的是为执行此功能的应用程序服务创建一个接口。在我的示例中,我假设您使用的是 MVVM Toolkit 之类的东西或类似的东西(所以我可以获得基本的 ViewModel 和 a RelayCommand
)。
这是一个非常简单的接口示例,用于执行基本的 IO 操作,例如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;
}
}
}
所以这很简单。现在是最后一部分:可测试性。这应该是显而易见的,但我将向您展示如何对此进行简单的测试。我使用最小起订量进行存根,但你当然可以使用任何你想要的东西。
[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);
}
希望这也有帮助。
WPF 应用程序框架 (WAF)提供了 Open 和 SaveFileDialog 的实现。
Writer 示例应用程序展示了如何使用它们以及如何对代码进行单元测试。
首先,我建议您从WPF MVVM 工具包开始。这为您提供了一个很好的命令选择,可用于您的项目。自 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();
就是这样。
随着您对命令的熟悉程度越来越高,您还可以设置何时禁用“浏览”按钮等条件。我希望能够为您指明您想要的方向。
从我的角度来看,最好的选择是 prism 库和 InteractionRequests。打开对话框的操作保留在 xaml 中,并从 Viewmodel 触发,而 Viewmodel 不需要了解有关视图的任何信息。
也可以看看
https://plainionist.github.io///Mvvm-Dialogs/
例如见:
在我看来,最好的解决方案是创建一个自定义控件。
我通常创建的自定义控件由以下部分组成:
所以 *.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}"/>