Karakuri.com

Fintechではたらくアプリケーションエンジニアの技術録

WPFやXAMLで画面やウインドウ、ページが表示されたときに自動で処理をスタートさせる方法

WPFにおいてページやウインドウが開かれた時に自動で処理を開始したいときって結構あると思います。しかし意外と最適解が探しても見つからず、苦労したので今までの試行錯誤で得たノウハウを書き残しておきます。

ViewModelのコンストラクタで処理する

最初にやりがちなのはViewModelのコンストラクタで最初に実行する処理のメソッドを呼んでしまう方法です。

public ViewModel()
{
     Start();
}

private void Start()
{
     ....
}

これだとインスタンスを作るときにStartメソッドが走るので、Startメソッドが終了するまでインスタンスの生成が終わらず、当然終わるまではViewとのバインディングも実行されないという状況に陥ります。

var ViewModel = new ViewModel();

結果、ページやウインドウが開かれたときに処理は終わってしまっているわけです。ただ、内容によってはそれで表示されて実行されたように見える場合もあるかもしれません。最悪なのはViewからのコマンド待ちなどがある場合で、フリーズしてしまうこともあります。


ウインドウやページのLoadedイベント後にViewModelのメソッドを実行する

コンストラクタで処理を呼び出すとフリーズする可能性があり、また気持ち悪さもあります。そのため通常はLoadedイベントでViewModelの処理を駆動する場合がほとんどでしょう。

private ViewModel viewModel;

public View()
{
     InitializeComponent();
     viewModel = new ViewModel();
     DataContext = viewModel;
}

private void Loaded(object sender, EventArgs args)
{
     viewModel.Loaded();
}

また、.Net Framework 4.5(4.0だっけ?)以上を使っている場合はLoadedイベントをコマンドにバインディングすることもできます。

 <Window x:Class="Karakuri.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
        Title="MainWindow" Height="350" Width="525">
<i:Interaction.Triggers>
    <i:EventTrigger EventName="Loaded">
        <i:InvokeCommandAction Command="{Binding LoadedCommand}" />
    </i:EventTrigger>
</i:Interaction.Triggers>
    <Grid>

    </Grid>
 </Window>

それでも画面が表示されない場合

実は上記の方法を使っても次のページが表示されないことがあります。実はLoadedイベントの後に画面のレンダリングが走るらしく、ViewModelでの処理が長く重い処理だと、いつまでもレンダリングに処理がまわらず画面が遷移しないということが起きます。Windows Formの場合はShownイベントなどがあったのですが、WPFからはそれもなくなっています。ではどうするか。これが最善の策かどうか分かりませんが、僕は非同期処理を利用しています。

public class View
{
     private ViewModel viewModel;

     public View()
     {
          InitializeComponent();
          viewModel = new ViewModel();
          DataContext = viewModel;
     }

     private void Loaded(object sender, EventArgs args)
     {
          viewModel.Loaded();
     }
}

public class ViewModel
{
     public async void Loaded()
     {
          await Task.Run(()=>
          {
               ...
          });
     }
}

ViewModelで呼ぶメソッドの返り値をvoidにすることで、処理が返るのでレンダリングが実行されるようになります。今のところ、この方法が万能で応用もしやすいですね。

2017/11/24 追記

WindowクラスにはWindow.ContentRenderedイベントがレンダリング後に走るそうで、ここでCommandを実行すればいいことを知りました。でもPageにはない。だめじゃん。
また、Commandを非同期にしても描画が遅いことがあります。どうもタスクの生成やawaitの処理などがUIスレッドを使用してしまうため結局レンダリングが開始されていない様子。下記のように書き直すと改善する可能性があります。

public class View
{
     private ViewModel viewModel;

     public View()
     {
          InitializeComponent();
          viewModel = new ViewModel();
          DataContext = viewModel;
     }

     private void Loaded(object sender, EventArgs args)
     {
          viewModel.Loaded();
     }
}

public class ViewModel
{
     public void Loaded()
     {
          Task.Run(()=>
          {
               ...
          });
     }
}

はい。そもそもawaitがいらなかったんじゃないかという話です。恥ずかしながら気付くのに時間かかりました。。さらにさらに、下記のように書くこともできます。

public class View
{
     private ViewModel viewModel;

     public View()
     {
          InitializeComponent();
          viewModel = new ViewModel();
          DataContext = viewModel;
     }

     private void Loaded(object sender, EventArgs args)
     {
          viewModel.Loaded();
     }
}

public class ViewModel
{
     public void Loaded()
     {
          Application.Current.Dispatcher.BeginInvoke(new Action { ()=>
          {
               ...
          }});
     }
}

Application.Current.Dispatcher.BeginInvokeは非同期に実行しつつUIスレッドが必要な処理は上手く処理してくれる便利な存在です。優先度も設定できるようです。僕も試してみたのですが、Task.Runのやり方のほうが早かったので使いませんでした。最適化するにはTask.Runの中でApplication.Current.Dispatcher.BeginInvokeを使うのがいいのかもしれません。僕の利用シーンでは表示順も重要だったので、それでもまた採用できませんでしたが。。