WPF的OpenFileDialog与MVVM模式?
我刚开始学习WPF的MVVM模式。 我碰壁了: 当你需要显示一个OpenFileDialog时,你做了什么?
下面是一个示例UI,我试图使用它:
当浏览button被点击时,应该显示一个OpenFileDialog。 当用户从OpenFileDialog中select一个文件时,文件path应该显示在文本框中。
我怎样才能用MVVM做到这一点?
更新 :我怎样才能做到这一点与MVVM,使其unit testing能力? 下面的解决scheme不适用于unit testing。
我通常做的是为执行此function的应用程序服务创build一个接口。 在我的例子中,我假设你正在使用像MVVM工具包或类似的东西(所以我可以得到一个基本的ViewModel和RelayCommand)。
下面是一个非常简单的界面,用于执行基本IO操作,如OpenFileDialog和OpenFile。 我在这里向他们展示,所以你不认为我build议你用一种方法创build一个接口来解决这个问题。
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; } } }
所以这很简单 现在是最后一部分:可testing性。 这一点应该是显而易见的,但我会告诉你如何做一个简单的testing。 我使用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。 这是使用隔离器的相同testing:
[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示例应用程序显示如何使用它们以及如何对代码进行unit testing。
首先,我会build议你从一个WPF MVVM工具包开始 。 这给你一个很好的select用于你的项目的命令。 自MVVM模式引入以来,一个特殊的特性就是RelayCommand(当然还有其他的版本,但我只是坚持最常用的)。 它是一个ICommand接口的实现,允许你在你的ViewModel中创build一个新的命令。
回到你的问题,这里是你的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();
而已。
当你熟悉这些命令的时候,你也可以设置条件,当你想要浏览button被禁用,等等。我希望你指出你想要的方向。
在我看来,最好的解决scheme是创build一个自定义控件。
我通常创build的自定义控件由以下内容组成:
- 文本框或文本块
- button与图像作为模板
- 文件path将被包装到的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}"}/>