nuits.jp blog

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

Xamarin.FormsでListView選択時にエレガントに詳細画面へ遷移する2つの方法 with 紙芝居 & BehaviorsPack

エレガントは言い過ぎ&タイトル長い上&ダイマでごめんなさい。

KAMISHIBAI for Xamarin.FormsとXamarin.Forms.BehaviorsPackの合わせ技でこんな簡単にListViewで選択されて画面遷移してパラメーター渡しするのが簡単に書けますよという紹介です。

f:id:nuitsjp:20170808094435g:plain

こんな感じのありがちなやつですが、これを至極簡単に書ける方法を紹介します。

サンプルの全コードはこちらのStylishListViewSampleをご覧ください。

さて、まず初期画面のViewModelをつぎのように記述します。

public class FruitsListPageViewModel : ViewModelBase
{
    public IReadOnlyList<Fruit> Fruits { get; }
        =  FruitsRepository.Fruits;

    public NavigationRequestCommand<Fruit> RequestDetail { get; }
        = new NavigationRequestCommand<Fruit>();
}

NavigationRequestCommandはICommandを実装したINavigationRequestの実装クラスです。
Command実行の受付と、画面遷移要求の発行を同時に行うやつです。
ユーザー操作から画面遷移までに、すべきことが少ない場合に使えます。(Actionをコンストラクタで登録して、簡単な処理を遷移前に実行する事も可能です。)

そしてXAMLをこんな感じで書きます。

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...>
    <ContentPage.BindingContext>
        <viewModels:FruitsListPageViewModel/>
    </ContentPage.BindingContext>
    <ContentPage.Behaviors>
        <mvvm:PushAsync Request="{Binding RequestDetail}" 
                        x:TypeArguments="views:FruitDetailPage" />
    </ContentPage.Behaviors>
    <ListView ItemsSource="{Binding Fruits}">
        <ListView.Behaviors>
            <behaviorsPack:SelectedItemBehavior 
                Command="{Binding RequestDetail}"/>
        </ListView.Behaviors>
        <ListView.ItemTemplate>
            <DataTemplate>
                <TextCell Text="{Binding Name}" TextColor="{Binding Color}"/>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</ContentPage>

ListViewのBehaviorsに登録されているSelectedItemBehaviorをまず見てください。

<behaviorsPack:SelectedItemBehavior Command="{Binding RequestDetail}"/>

SelectedItemBehaviorは行が選択された時に、良い感じで選択されたアイテムを渡しつつCommandを実行してくれる自分的にはうれしい奴です(再選択問題なんかも対応してありますよ)。
ListViewで行が選択されたら、選択された行にBindされているFruitを引数に、RequestDetailのExecuteを呼び出してくれます。

つづいてPageのBehaviorsを見てください。

<mvvm:PushAsync Request="{Binding RequestDetail}" 
                x:TypeArguments="views:FruitDetailPage" />

RequestDetailの要求を受けてFruitDetailPageにPushAsyncで遷移してね、と宣言されています。
これでListViewでいずれかが選択されたら、選択されたFruitを渡して画面遷移します。

あとは受け取る側を次のように実装します。

public class FruitDetailPageViewModel 
    : ViewModelBase, IPageInitializeAware<Fruit>
{
    public void OnInitialize(Fruit fruit) => Fruit = fruit;

OnInitializeメソッドのパラメーターとして型安全に受け取れます。受け取り後、このコードではFruitプロパティに値を設定しています。
良くないですか?

…ん?良くない?パラメーターはID渡しして、遷移先で値は取り直したいと?
OK、そのパターンを紹介しましょう。

初期画面のViewModelを次のように修正します。

// public NavigationRequestCommand<Fruit> RequestDetail { get; } 
//    = new NavigationRequestCommand<Fruit>();
public NavigationRequestCommand<int> RequestDetail { get; } 
    = new NavigationRequestCommand<int>();

型パラメーターをFruitからintに変更しただけです。
そして、XAMLのSelectedItemBehaviorでFruitのIdをCommandに渡すよう、次のように修正します。

before

<behaviorsPack:SelectedItemBehavior 
    Command="{Binding RequestDetail}"/>

after

<!-- 
<behaviorsPack:SelectedItemBehavior 
    Command="{Binding RequestDetail}" PropertyPath="Id"/>

最期に、受け取るところを受け取ったIdから値を再取得するように修正しましょう。

//public class FruitDetailPageViewModel
//    : ViewModelBase, IPageInitializeAware<Fruit>
public class FruitDetailPageViewModel 
    : ViewModelBase, IPageInitializeAware<int>
{
    // public void OnInitialize(Fruit fruit) => Fruit = fruit;
    public void OnInitialize(int id) => 
        Fruit = FruitsRepository.FindById(id);

これでOKです。

いや~、Xamarin.Forms.BehaviorsPackと紙芝居便利だな~(棒

おまけ

ところで、鋭い人は気が付いているかもしれませんが、これ画面遷移要求をViewModel通さなくても実装できちゃいますよね?
それがやっていい事かどうかは、判断に迷うところですが、できることはできます。
という事で、一応その方法も紹介しておきます。

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...>
    <ContentPage.BindingContext>
        <viewModels:FruitsListPageViewModel/>
    </ContentPage.BindingContext>
    <ContentPage.Behaviors>
        <mvvm:PushAsync x:TypeArguments="views:FruitDetailPage">
            <mvvm:PushAsync.Request>
                <mvvm:NavigationRequestCommand x:TypeArguments="x:Int32" 
                    x:Name="NavigationRequestCommand"/>
            </mvvm:PushAsync.Request>
        </mvvm:PushAsync>
    </ContentPage.Behaviors>
    <ListView ItemsSource="{Binding Fruits}">
        <ListView.Behaviors>
            <behaviorsPack:SelectedItemBehavior 
                Command="{Binding Source={x:Reference NavigationRequestCommand}}"  
                PropertyPath="Id"/>
        </ListView.Behaviors>
        <ListView.ItemTemplate>
            <DataTemplate>
                <TextCell Text="{Binding Name}" TextColor="{Binding Color}"/>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</ContentPage>

PushAsyncビヘイビアへ設定するRequestをXAML上で名前付きで定義し

<mvvm:PushAsync x:TypeArguments="views:FruitDetailPage">
    <mvvm:PushAsync.Request>
        <mvvm:NavigationRequestCommand x:TypeArguments="x:Int32" 
            x:Name="NavigationRequestCommand"/>
    </mvvm:PushAsync.Request>
</mvvm:PushAsync>

SelectedItemBehaviorでx:Referenceを利用してXAML上のNavigationRequestCommandを直接呼び出しています。

<behaviorsPack:SelectedItemBehavior 
    Command="{Binding Source={x:Reference NavigationRequestCommand}}"
    PropertyPath="Id"/>

ViewModelではもともと何もしていないに等しいわけで、ViewModelを通さなくていい気もしますし、そもそもViewModelに分けない&再利用されないコードならそもそもコードビハインドにゴリゴリ書いちゃえよとも思えるし、でもViewModel通さないの?むむむ。。。 皆さんならどうしますか?

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