6
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[C#/WPF/MVVM/ReactiveUI] MVVMでダイアログ処理を行う(Interaction)

Posted at

困っていたこと

MVVM(Model-View-ViewModel)パターンに則ってアプリを作成していると、ダイアログの処理をどのように実装すればよいか困ることがあります。
例えばWPFにおいてファイル保存ダイアログ(SaveFileDialog)を表示してからファイルを保存したい場合、

  • Viewのコードビハインド(~.xaml.cs)にクリック時のイベントを実装しその中で呼び出す
  • ViewModelのコマンド内でSaveFileDialogを呼び出す

などが考えられます。
最も簡単なのは前者ですが、SaveFileDialogを呼び出したいためだけにViewにクリックイベントを実装するのは好ましくありません。
だからといって後者のようにViewModelにダイアログの処理を持ってくるのはMVVMパターンに反してしまいます。

Viewにダイアログの処理を記述し、ViewModelはその入出力だけを利用するような方法はないでしょうか?

解決策: Interactionを使ったダイアログ処理

.NETのMVVMフレームワークライブラリであるReactiveUIにはInteractionという機能を使うことによって解決できることがわかりました。

実装例

例題としてMVVMパターンに則ったWPFアプリでSaveFileDialogを使う場合を考えます。
プログラムの詳細はこちらで公開しています。

Model

SaveFileDialogの例題なのでModelにはファイルを保存する機能を持たせます。

全体のコード
Model
using System;
using System.IO;

namespace MvvmDialog.Models
{
    public class SaveText
    {
        public SaveTextResponse Save(SaveTextRequest request)
        {
            SaveTextResponse response = null;

            try
            {
                using (StreamWriter sw = new StreamWriter(request.FilePath, false))
                {
                    sw.Write(request.InputText);
                }

                response = new SaveTextResponse()
                {
                    Succeed = true,
                    Message = "File saved successfully."
                };
            }
            catch (Exception ex)
            {
                response = new SaveTextResponse()
                {
                    Succeed = false,
                    Message = ex.Message
                };
            }

            return response;
        }
    }

    public class SaveTextRequest
    {
        public string InputText { get; set; }

        public string FilePath { get; set; }
    }

    public class SaveTextResponse
    {
        public bool Succeed { get; set; }

        public string Message { get; set; }
    }
}

ViewModel

本題のSaveFileDialogのプロパティとしてInteractionを定義します。

public Interaction<string, string> SaveFileDialog { get; set; }

Inputには起動時に表示されるファイル名が入力されるとし、またOutputとしてはファイルのフルパスを出力すると決めておきます。
ViewModel内でInteractionSaveFileDialogは次のように呼び出すことができます。

IObservable<string> output = SaveFileDialog.Handle(input);

このInteractionを保存ボタン押下コマンド内に記述することで、ボタン押下→ダイアログ表示→保存処理を実現することができます。

全体のコード
ViewModel
using MvvmDialog.Models;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using System;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;

namespace MvvmDialog.ViewModels
{
    public class MainViewModel : ReactiveObject, IActivatableViewModel
    {
        [Reactive]
        public string InputText { get; set; }

        [Reactive]
        public string FileName { get; set; }

        [Reactive]
        public string ResultMessage { get; set; }

        public ReactiveCommand<Unit, SaveTextRequest> SaveFileCommand { get; private set; }

        public Interaction<string, string> SaveFileDialog { get; set; }

        public ViewModelActivator Activator { get; }


        public MainViewModel()
        {
            Activator = new ViewModelActivator();

            this.WhenActivated(d =>
            {
                HandleViewModelBound(d);
            });
        }


        void HandleViewModelBound(CompositeDisposable d)
        {
            SaveFileDialog = new Interaction<string, string>();

            SaveFileCommand = ReactiveCommand.CreateFromObservable(() => SaveFileDialog.Handle(FileName).Select(x => new SaveTextRequest() { FilePath = x, InputText = this.InputText }));

            SaveFileCommand.Select(x => new SaveText().Save(x)).Subscribe(x => ResultMessage = x.Message).DisposeWith(d);
        }
    }
}

View

ViewではViewModelで定義したSaveFileDialogプロパティに具体的なInteractionの処理をバインドしていきます。

this.BindInteraction(
    ViewModel,
    vm => vm.SaveFileDialog,
    async interaction =>
    {
        var result = await Task.Run(() =>
        {
            var dialog = new SaveFileDialog()
            {
                FileName = interaction.Input,
                AddExtension = true,
                DefaultExt = "txt"
            };

            if(dialog.ShowDialog()?? false)
            {
                return dialog.FileName;
            }
            else
            {
                return null;
            }
        });

        interaction.SetOutput(result);
    })
    .DisposeWith(d);
全体のコード
View
using Microsoft.Win32;
using MvvmDialog.ViewModels;
using ReactiveUI;
using System.Reactive.Disposables;
using System.Threading.Tasks;

namespace MvvmDialog.Views
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : ReactiveWindow<MainViewModel>
    {

        public MainWindow()
        {
            InitializeComponent();

            ViewModel = new MainViewModel();

            this.WhenActivated(d =>
            {
                HandleViewModelBound(d);
            });
        }

        protected void HandleViewModelBound(CompositeDisposable d)
        {
            this.BindInteraction(
                ViewModel,
                vm => vm.SaveFileDialog,
                async interaction =>
                {
                    var result = await Task.Run(() =>
                    {
                        var dialog = new SaveFileDialog()
                        {
                            FileName = interaction.Input,
                            AddExtension = true,
                            DefaultExt = "txt"
                        };

                        if(dialog.ShowDialog()?? false)
                        {
                            return dialog.FileName;
                        }
                        else
                        {
                            return null;
                        }
                    });

                    interaction.SetOutput(result);
                })
                .DisposeWith(d);

            this.OneWayBind(ViewModel, vm => vm.ResultMessage, v => v.ResultTextBlock.Text).DisposeWith(d);

            this.Bind(ViewModel, vm => vm.FileName, v => v.FileNameTextBox.Text).DisposeWith(d);

            this.Bind(ViewModel, vm => vm.InputText, v => v.InputTextBox.Text).DisposeWith(d);

            this.BindCommand(ViewModel, vm => vm.SaveFileCommand, v => v.SaveFileButton).DisposeWith(d);
        }
    }
}
6
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?