programing tip

MVVM 패턴이있는 WPF OpenFileDialog?

itbloger 2020. 8. 29. 09:34
반응형

MVVM 패턴이있는 WPF OpenFileDialog?


방금 WPF 용 MVVM 패턴을 배우기 시작했습니다. 벽에 부딪 혔습니다. OpenFileDialog를 표시해야 할 때 어떻게해야 합니까?

다음은 사용하려는 UI의 예입니다.

대체 텍스트

찾아보기 버튼을 클릭하면 OpenFileDialog가 표시됩니다. 사용자가 OpenFileDialog에서 파일을 선택하면 파일 경로가 텍스트 상자에 표시되어야합니다.

MVVM으로 어떻게 할 수 있습니까?

업데이트 : MVVM으로 어떻게이 작업을 수행하고 단위 테스트가 가능하도록 만들 수 있습니까? 아래 솔루션은 단위 테스트에서 작동하지 않습니다.


제가 일반적으로하는 일은이 기능을 수행하는 응용 프로그램 서비스를위한 인터페이스를 만드는 것입니다. 내 예에서는 MVVM Toolkit 또는 유사한 것을 사용한다고 가정합니다 (기본 ViewModel 및 RelayCommand를 얻을 수 있음).

다음은 OpenFileDialog 및 OpenFile과 같은 기본 IO 작업을 수행하기위한 매우 간단한 인터페이스의 예입니다. 이 두 가지를 모두 여기에 표시하고 있으므로이 문제를 해결하기 위해 하나의 방법으로 하나의 인터페이스를 만드는 것이 좋습니다.

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

이것은 아마도 당신을 위해 일할 것입니다.

There is a library out on CodePlex called "SystemWrapper" (http://systemwrapper.codeplex.com) that might save you from having to do a lot of this kind of thing. It looks like FileDialog is not supported yet, so you'll definitely have to write an interface for that one.

Hope this helps.

Edit:

I seem to remember you favoring TypeMock Isolator for your faking framework. Here's the same test using 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);
}

Hope this is helpful as well.


The WPF Application Framework (WAF) provides an implementation for the Open and SaveFileDialog.

The Writer sample application shows how to use them and how the code can be unit tested.


Firstly I would recommend you to start off with a WPF MVVM toolkit. This gives you a nice selection of Commands to use for your projects. One particular feature that has been made famous since the MVVM pattern's introduction is the RelayCommand (there are manny other versions of course, but I just stick to the most commonly used). Its an implementation of the ICommand interface that allows you to crate a new command in your ViewModel.

Back to your question,here is an example of what your ViewModel may look like.

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 and RelayCommand are both from the MVVM Toolkit. Here is what the XAML may look like.

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

and your XAML.CS code behind.

DataContext = new OpenFileDialogVM();
InitializeComponent();

Thats it.

As you get more familiar with the commands, you can also set conditions as to when you want the Browse button to be disabled, etc. I hope that pointed you in the direction you wanted.


From my perspective the best option is the prism library and InteractionRequests. The action to open the dialog remains within the xaml and gets triggered from Viewmodel while the Viewmodel does not need to know anything about the view.

See also

https://plainionist.github.io///Mvvm-Dialogs/

As example see:

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


In my opinion the best solution is creating a custom control.

The custom control I usually create is composed from:

  • 텍스트 상자 또는 텍스트 블록
  • 이미지가 템플릿으로 포함 된 버튼
  • 파일 경로가 래핑 될 문자열 종속성 속성

따라서 * .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}"/>

참고 URL : https://stackoverflow.com/questions/1619505/wpf-openfiledialog-with-the-mvvm-pattern

반응형