2009-10-24 41 views
85

我剛開始學習WPF的MVVM模式。我碰壁了:當你需要顯示OpenFileDialog時,你會做什麼?帶有MVVM模式的WPF OpenFileDialog?

這裏有一個例子UI我嘗試使用它:

alt text

當單擊瀏覽按鈕,一個OpenFileDialog應該顯示。當用戶從OpenFileDialog中選擇一個文件時,文件路徑應顯示在文本框中。

我該怎麼用MVVM做到這一點?

更新:我如何用MVVM做到這一點,並使其單元測試?下面的解決方案不適用於單元測試。

+0

這是一個重複:http://stackoverflow.com/questions/1043918/ open-file-dialog-mvvm –

+0

當然可以。 :-)在發佈這個問題之前,我在SO上做了一些搜索,但沒有出現。好吧。 –

+0

我投票結束這個問題,因爲它是一個確切的副本。 –

回答

84

我通常會做的是爲執行此功能的應用程序服務創建一個接口。在我的例子中,我假設你正在使用類似MVVM Toolkit或類似的東西(這樣我可以得到一個基本的ViewModel和一個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; 
      } 
    } 
} 

所以這很簡單。現在是最後一部分:可測試性。這一點應該很明顯,但我會告訴你如何爲此做一個簡單的測試。我使用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)的庫,可以幫助您免除必須執行的lot這類事情。它看起來像FileDialog還不支持,所以你一定要爲它寫一個接口。

希望這會有所幫助。

編輯

我好像記得你贊成TypeMock隔離您作假框架。以下是使用隔離器的相同測試:

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

希望這也是有幫助的。

+0

這對我來說很有意義:有一些服務可以做這樣的對話,並通過ViewModel中的一個接口使用該服務。非常好,謝謝。 (p.s.我將用RhinoMocks進行測試,僅供參考,但我可以認爲這部分沒有問題。) –

+0

Shucks。在這裏,我認爲我很喜歡。很高興我能幫上忙。 –

+0

在第二段FYI :)小錯字:)。感謝你的回答! – Jeff

2

首先,我會建議你從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; 
    } 
} 

ViewModelBaseRelayCommand均距離MVVM Toolkit。這是XAML的外觀。

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

和您的XAML.CS代碼背後。

DataContext = new OpenFileDialogVM(); 
InitializeComponent(); 

那就是它。

隨着您對這些命令的熟悉程度的提高,您還可以設置條件,以便您希望禁用「瀏覽」按鈕等。我希望能夠指引您朝着您想要的方向行進。

+6

我應該改寫一下:我如何使這個單元可測試?運行單元測試時,您的解決方案會彈出一個對話框。 –

+0

如果您的ViewModel存放在它自己的DLL中,它不應該引用PresentationFramework.dll。 –

+1

爲什麼使用'RegisterCommands();'而不是直接寫'OpenCommand = new RelayCommand(ExecuteOpenFileDialog);'? – aloisdg

4

WPF Application Framework (WAF)爲Open和SaveFileDialog提供了一個實現。

Writer示例應用程序顯示如何使用它們以及如何對代碼進行單元測試。

1

在我看來最好的解決方案是創建一個自定義控件。

定製控制我通常創建從由:

  • 文本框或文本塊
  • 按鈕用圖像作爲模板
  • 字符串依賴屬性在該文件路徑將被包裝到

所以* .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}"}/> 
0

從我的角度來看,最好的選擇是棱鏡圖書館和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