nuits.jp blog

C#, Xamarin, WPFを中心に書いています。Microsoft MVP for Development Technologies。

【Xamarin.Forms】ListViewで行選択時に処理を実行する、たった一つの冴えたやり方?

2017.07.08 書いていた記事がなんだか全面的に誤っていたことに気が付いたのでリライトします。

ListViewで行が選択されたとき、それをコマンドに渡しつつ選択された行をクリアする最も冴えたやり方は次の通りでした。

  1. ListViewのSelectedItemをハンドルする
  2. その際に渡されるSelectedItemChangedEventArgsのSelectedItemプロパティをCommandに渡す
  3. その後、ListViewのSelectedItemをクリアする

クリアする際に、SelectedItemイベントが再度飛んでくるので、変更後のプロパティがnullだったら即リターンするようにするのがポイントです。
これをBehaviorにするといいでしょう。
コードはこんな感じになります。

ただ、これだけだと親のBindingContextが変わったときに正しく動かなかったりするので、以下のライブラリに含まれているので使うといいと思います(ダイマ!

www.nuget.org

まぁ、その辺をちゃんと対応したコードも公開しているので、そちらを使っていただいてもよいでしょう。

github.com

以下、下記の恥ずかしい残骸を一応残しておきます。

疑問形にすると(ry
しかも別に一つじゃないんですけどごめんなさい。

少し前に、かずきさんも類似の記事をアップされていました。

blog.okazuki.jp

やりたい事はほとんど同じです。
なにせ、BlueMonkeyプロジェクトで、どう実装するかSlackで話していてでた話だからです。
ちなみにBlueMonkeyプロジェクトって何?という方はぜひこちらをご覧ください。

ytabuchi.hatenablog.com

さて、そもそも何故にこれが検討の課題になったかというと、単純にやるといくつか問題があるためです。
まずはその点の整理から入っていきたいと思います。

前提条件

本エントリーは、イベントに反応してCommandを実行するBehaviorの知識を前提として進めていきます。
ご存じない方はまずこちらをご覧ください。

www.nuits.jp

課題の整理

さて、簡単に思える解決策は幾つかあります。

  • SelectedItemプロパティにバインドする方法
  • ItemTappedイベントをハンドルしてコマンドを実行する方法
  • ItemSelectedイベントをハンドルしてコマンドを実行する方法

これらを単純に採用するとイケていない理由をまず説明したいと思います。

SelectedItemプロパティにバインドする方法

これは単純な主に二つの理由があります。

  1. ListViewの初期化時にnullが渡されてくる為、無駄なif分を書かなくてはならない。ダサい
  2. 一度選択すると、選択状態が維持されるため二回目の選択時にイベントが発生しない

まぁゴリゴリやればこれらの問題は回避できます。

あと、Xamarin.Formsでは未確認ですが、WPFなどではプロパティのGet・Set時に例外が発生すると、その処理されない例外は握りつぶされグローバルな例外ハンドラなどでは処理できないという問題もありました。
そもそも、プロパティの取得・設定時に複雑な処理するのは好ましくありません(非同期処理とかどうするのか?など)、Commandを利用するべきでしょう。

ItemTappedイベントをハンドルしてコマンドを実行する方法

EventToCommandBehaviorでTappedイベントをハンドルして、IValueConverterを利用してItemTappedEventArgsから選択された行のオブジェクトを取得すればいい。
そんな風に考えていた時代が僕にもありました。
この方法は一見うまく行きます。 ただ、実際のケースを考えた時、うまく行かない場合があります。

ListViewで行を選択して何らかの処理をする場合、スマートフォンの場合は詳細画面へ遷移するケースが多いと思います。
そこまでは問題ありません。
問題は詳細画面から一覧画面へ戻ってきたときに、「選択状態が保持されたままになってかっちょ悪い」という事です。
Xamarin.FormsのNavigationPageを使った画面遷移では、遷移すると前画面はNavigationStack上にインスタンスが保持されているため、戻った時に選択状態が維持されたままとなるわけです。

じゃぁ、遷移前に選択状態を開放してあげればいいんじゃない?と思いますよね?
私も思いました。そしてハマりました。。。

「AndroidとiOSでItemTappedイベントとItemSelectedイベントの発行順がことなる」

のです。
イベントの順序に依存する解放はよろしく無いですし、SelectedItemを触りたいなら、ItemTappedイベントの中から触るのは避けた方が無難でしょう。

ItemSelectedイベントをハンドルしてコマンドを実行する方法

これで大体うまく行くのですが、「遷移して戻ってきたときに選択されっぱなし問題」は依然発生するため、次の解法をお勧めします。
あと、そのままだと同じ行を選択できない気もします。

おすすめの解決策

本質的には、かずきさんの方法とほぼ同じです。

  • ItemTappedかItemSelectedイベントをハンドルしてCommandを実行してあげる
  • ItemSelected発生時に、SelectedItemをクリアしておいてあげる

かずきさんの解法では、これらを二つのBehaviorで個別に対応していましたが、BlueMonkeyプロジェクトでは、次のような専用Behaviorを作成することにしました。
ふたつに分けた方が汎用性は高く、一つにまとめた方が実装効率や見通しは良くなるため、利用の際にはよく検討した上で選択してください。

ItemSelectedToCommandBehavior

ベースとなっているBaseBehaviorについてはこちらをご覧ください。

https://github.com/ProjectBlueMonkey/BlueMonkey/blob/master/client/BlueMonkey/BlueMonkey/BehaviorBase.cs

てことで、今日はここまで!
それではまた!