none
【WPF】MVVMでBusyIndicatorを使う場合(または非同期コマンドでUIをブロックする場合)の最適ソリューションて何だと思いますか? RRS feed

  • 質問

  • 訳あってWPFを使うことになり、MVVMで実装してるのですが、非同期コマンドでつまずきました。

    ボタンを押してデータベースからリストを取ってくる間、BusyIndicatorを表示させたい(UIをブロックしたい)のですが、MVVMでやると難しくないですか?

    結構当たり前にやる処理だと思ったのですが、調べてみても適当なソリューションが見つからず、皆さんどうやって実装してるのか気になってトピックを立てました。

    みなさんの実装をご紹介いただけると幸です。


    ちなみに、調べて見つかったのは

    ◆非同期プログラミング - 非同期 MVVM アプリケーションのパターン: コマンド

    https://msdn.microsoft.com/ja-jp/magazine/dn630647.aspx

    ↑この例は、UIブロックしてません。


    ◆Implementing a busy indicator using a visual overlay in MVVM

    http://awkwardcoder.blogspot.jp/2013/06/implementing-busy-indicator-using.html

    ↑この例ですと、かなり大変な実装になっています。

    もっと簡単に出来る方法があるんじゃないかと・・・。

    どうでしょうか。

    ※開発環境、フレームワークは最新、MVVMフレームワークはPrismを使っていますが、Prismにこだわらず紹介いただけると幸です。
    2016年5月30日 20:10

回答

  • ViewModelからBusyプロパティでインジケータの表示非表示を変えれば良いです。

    <Window x:Class="WpfApplication6.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:tool="http://schemas.xceed.com/wpf/xaml/toolkit"
            xmlns:local="clr-namespace:WpfApplication6"
            Title="MainWindow" Height="350" Width="525">
        <Window.DataContext>
            <local:MainWindowViewModel />
        </Window.DataContext>
        <Window.Resources>
            <BooleanToVisibilityConverter x:Key="conv" />
        </Window.Resources>
        <Grid>
            <Button Content="hoge" Width="100" Height="30" Command="{Binding LoadCommand}" />
            <Grid Visibility="{Binding IsBusy, Converter={StaticResource conv}}">
                <Grid.Background>
                    <SolidColorBrush Color="Black" Opacity="0.75" />
                </Grid.Background>
                <!--NuGet:Extended WPF Toolkit-->
                <tool:BusyIndicator IsBusy="True" BusyContent="Loading..." />
            </Grid>
            
        </Grid>
    </Window>
    
    class MainWindowViewModel : BindableBase
    {
    	private bool _IsBusy;
    	public bool IsBusy
    	{
    		get
    		{ 
    			return _IsBusy; 
    		}
    		set
    		{
    			SetProperty(ref _IsBusy, value);
    		}
    	}
    
    	private DelegateCommand _LoadCommand;
    	public DelegateCommand LoadCommand 
    	{
    		get
    		{
    			if(_LoadCommand == null)
    			{
    				_LoadCommand = new DelegateCommand(Loading);
    			}
    			return _LoadCommand;
    		}
    	}
    
    	private async void Loading()
    	{
    		IsBusy = true;
    		await Task.Run(() =>
    		{
    			Thread.Sleep(3000);
    		});
    		IsBusy = false;
    	}
    }
    各画面に予めインジケータを埋め込むのが手間であれば、
    先に挙げて頂いたURL2つ目のようにコントロールをホストしてインジケータを埋め込む感じでしょうか。
    それについてはURL先からダウンロードできるソースコードを読めばわかります。


    2016年5月31日 2:54
    モデレータ

すべての返信

  • レスが付かないようですし、私が勘違いしているかもしれませんので、もう少し教えて下さい。
    一般的にUIをブロックしたくないので非同期で実行すると思うのですが、その辺りはどうなのでしょうか?
    UIをブロックするのであれば、UIと同期してデータベースからリストを取ってくるという普通の処理を行うだけで良いような気がするのですが・・・???
    つまり、非同期で実行している間にユーザーが何もできないのであれば、非同期で実行する意味がないのではないのかという疑問です。


    ★良い回答には回答済みマークを付けよう! MVP - .NET  http://d.hatena.ne.jp/trapemiya/

    2016年5月31日 1:54
    モデレータ
  • こんにちは。

    MVVMかどうかで難易度が変わりますか?
    IsBusyプロパティなどをバインドさせるか、Message機能を使ってViewで処理させるだけではないでしょうか。

    View側ではいくつか方法があると思いますが「UIブロック」とはどういう定義でしょう。
    操作不能であれば、後者のURLのようにレイヤーを上に被せるとか、
    WindowをDisableにしてIndicatorを別で表示させるなどになると思います。
    ※後者のURLは複雑な実装になってますが、要点としてはバインドさせたパラメータでWindowを覆ったGridの表示・非表示を切り替えているだけのように見えます。

    厳密にはアプリケーションの終了は出来ますので、操作不能にさせるよりも正しい中断処理を実装することのほうが重要でしょうか。

    2016年5月31日 1:55
    モデレータ
  • >つまり、非同期で実行している間にユーザーが何もできないのであれば、非同期で実行する意味がないのではないのかという疑問ですに

    ユーザーが何もできなくしたいですけど、UIが固まるのは避けたい(アニメーションを見せて待たせたい)です。

    Webだと普通にある要求で、グルグルを回したりします。

    JQueryとかだと簡単にできるのですが、WPFだとどうやるのがセオリーなのかなと思いまして・・・。

    2016年5月31日 2:19
  • >「UIブロック」とはどういう定義でしょう。

    WebやiOSアプリ等でよく見る、待ってる間にグルグルアニメーションなどを表示させて、他のボタンを押させない、みたいな実装です。

    別にアプリケーションの終了は構いません。画面内の他の機能をブロックしたい&アニメーションを見せたい、ということです。

    (普通に良くあることだと思ってて通じると思ってましたすみません)

    >IsBusyプロパティなどをバインド

    >WindowをDisableにしてIndicatorを別で表示させるなどになると思います。

    ここもう少し詳しく紹介して頂けないでしょうか?

    やっぱり各画面にBusyIndicatorを配置しておいて、TriggerActionを自作して、とかでしょうか?

    もっと簡単にできます?

    2016年5月31日 2:26
  • >MVVMかどうかで難易度が変わりますか?

    かなり変わると思ってトピック立てました。

    xaml.csに記述するなら

    1.BusyIndicator表示

    2.コマンド非同期実行

    3.非同期コールバックでBusyIndicator非表示

    のシーケンスでよく、至極単純だと思います。

    これをViewModelからやる場合どうするのかなあと。

    2016年5月31日 2:30
  • ViewModelからBusyプロパティでインジケータの表示非表示を変えれば良いです。

    <Window x:Class="WpfApplication6.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:tool="http://schemas.xceed.com/wpf/xaml/toolkit"
            xmlns:local="clr-namespace:WpfApplication6"
            Title="MainWindow" Height="350" Width="525">
        <Window.DataContext>
            <local:MainWindowViewModel />
        </Window.DataContext>
        <Window.Resources>
            <BooleanToVisibilityConverter x:Key="conv" />
        </Window.Resources>
        <Grid>
            <Button Content="hoge" Width="100" Height="30" Command="{Binding LoadCommand}" />
            <Grid Visibility="{Binding IsBusy, Converter={StaticResource conv}}">
                <Grid.Background>
                    <SolidColorBrush Color="Black" Opacity="0.75" />
                </Grid.Background>
                <!--NuGet:Extended WPF Toolkit-->
                <tool:BusyIndicator IsBusy="True" BusyContent="Loading..." />
            </Grid>
            
        </Grid>
    </Window>
    
    class MainWindowViewModel : BindableBase
    {
    	private bool _IsBusy;
    	public bool IsBusy
    	{
    		get
    		{ 
    			return _IsBusy; 
    		}
    		set
    		{
    			SetProperty(ref _IsBusy, value);
    		}
    	}
    
    	private DelegateCommand _LoadCommand;
    	public DelegateCommand LoadCommand 
    	{
    		get
    		{
    			if(_LoadCommand == null)
    			{
    				_LoadCommand = new DelegateCommand(Loading);
    			}
    			return _LoadCommand;
    		}
    	}
    
    	private async void Loading()
    	{
    		IsBusy = true;
    		await Task.Run(() =>
    		{
    			Thread.Sleep(3000);
    		});
    		IsBusy = false;
    	}
    }
    各画面に予めインジケータを埋め込むのが手間であれば、
    先に挙げて頂いたURL2つ目のようにコントロールをホストしてインジケータを埋め込む感じでしょうか。
    それについてはURL先からダウンロードできるソースコードを読めばわかります。


    2016年5月31日 2:54
    モデレータ
  • ここでいいかな?

    非同期処理というより、時間のかかる処理を行うので、何とかしたいということですよね?

    待機中を表すポップアップウィンドウを作って、それをモーダル表示(ShowDialog)するではだめですか?

    MVVM だからほかのポップアップがあってはだめということはありませんので、それが一番簡素だと思います。

    見た目に凝ったことをしたい(Webアプリのようにレイヤーをかぶせるなど)のなら、これでは実現できませんが、一般的なデスクトップの世界ならユーザー的には、一番当たり障りのない実装だと思います。


    とっちゃん@わんくま同盟, Visual Studio and Development Technologies http://blogs.wankuma.com/tocchann/default.aspx

    2016年5月31日 3:08
  • Tak1waさん

    サンプルありがとうございます!

    GridのVisibilityにバインドすれば良いのですか。思いつきませんでしたorz

    メッセンジャーかなあ、とか小難しいこと考えていました。これならそんなに負担にならずに実装できそうです。

    大変ありがとうございました!

    2016年5月31日 5:59
  • とっちゃんさん

    ポップアップウインドウよりBusyIndicatorの方が簡単かなあと思いまして、おたずねした次第です。

    でも簡単なサンプル頂けたので助かりました。いまのところこれ(GridのVisibilityにバインド)が一番楽そうですよね。

    2016年5月31日 6:03