放浪軍師のXamarin.Formsアプリ開発局

Xamarin.Forms+Prism+ReactivePropertyで素人がAndroidやUWPのアプリを右往左往しながら開発している様を発信していきます。性質上間違いも多いのでご注意ください。

どんなアプリにも必須のListViewを学ぶ

放浪軍師のXamarin.Formsによるアプリ開発
今回はListViewを学びたいと思います。…というか前回の記事でさらっと出してしまったけど順番逆になっちゃいましたね。
本当は前回の記事への返答やそれに対する記事を書いてくださった方の分を検証したいところなんですが、どうしてもListViewの知識は必要なので先にこっちをやっておきます。
まず、初めて訪問された方は以下をお読みください。

www.gunshi.info

スクロールし放題のListViewコントロール

例えばネラー御用達の、ChMate(旧2chmate)や、Twitterのようなアプリ、というか世に出回ってる殆どのアプリでは、縦にずらーっとスクロールできるというのは必須機能です。
業務の方で開発を進めているアプリでも勿論使う予定なので、ここできっちりと学んで記録しておこうと思います。またもや大苦戦したけどな!定型文とか言うな

ちなみにListViewには種類がいくつかあるので、分けて説明しておきます。

基本形

ただ単にリストを並べるだけの場合に使えばいいと思う。でもあんまり使う機会なさそう。
[View]

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="ListViewTest.Views.ListViewPage"
             Title="ListView">

    <StackLayout>
        <Label Text="{Binding Text.Value}"/>
        <ListView ItemsSource="{Binding ListView}" SelectedItem="{Binding SelectItem.Value}"/>
    </StackLayout>

</ContentPage>

[ViewModel]

using Prism.Mvvm;
using Reactive.Bindings;
using System;

namespace ListViewTest.ViewModels
{
    public class ListViewPageViewModel : BindableBase
    {
        public ReactiveCollection<string> ListView { get; set; } = new ReactiveCollection<string>();
        public ReactiveProperty<string> SelectItem { get; set; } = new ReactiveProperty<string>();
        public ReactiveProperty<string> Text { get; set; } = new ReactiveProperty<string>();
        public ListViewPageViewModel()
        {
            ListView = new ReactiveCollection<string>
            {
                "藤崎詩織", "ボンバーガール", "伝説の樹", "植林", "自爆", "無敵特攻", "無双", "悪い噂", "ファイヤーマン",
                "大倉都子", "爆発", "包帯", "怖い", "無理ゲー"
            };

            SelectItem.Subscribe(x => Text.Value = x);
        }
    }
}

[動作確認]
Android
f:id:roamschemer:20180918025111g:plain
UWP
f:id:roamschemer:20180918025114g:plain

結構見た目が違いますね。

ImageCell

先ほどの基本形より機能が増えていて、メインの情報以外にサブ情報の表示と、左側に画像を表示することができます。
ちなみにTextCellという奴もあるみたいですが、単純にImageCellの画像が表示できないバージョンなだけみたいです。
[View]

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
             xmlns:cv="clr-namespace:ListViewTest.Converters"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="ListViewTest.Views.ImageCellPage"
             Title="ImageCell">

    <ContentPage.Resources>
        <ResourceDictionary>
            <cv:ImageSourceConverter x:Key="ImageSourceConverter"/>
        </ResourceDictionary>
    </ContentPage.Resources>

    <StackLayout>
        <Label Text="{Binding Text.Value}"/>
        <ListView ItemsSource="{Binding ListView}" SelectedItem="{Binding SelectItem.Value}">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ImageCell Text="{Binding Title}" Detail="{Binding Target}" ImageSource="{Binding Image, Converter={StaticResource ImageSourceConverter}}" />
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </StackLayout>
    
</ContentPage>

画像表示には以前学んだImageSourceConverterを使用しています。
www.gunshi.info

[ViewModel]

using Prism.Mvvm;
using Reactive.Bindings;
using System;
using System.Linq;
using System.Reactive.Linq;

namespace ListViewTest.ViewModels
{
	public class ImageCellPageViewModel : BindableBase
	{
        public class MenuItem
        {
            public string Title { get; set; }
            public string Target { get; set; }
            public string Image { get; set; }
        }

        public ReactiveCollection<MenuItem> ListView { get; set; } = new ReactiveCollection<MenuItem>();
        public ReactiveProperty<MenuItem> SelectItem { get; set; } = new ReactiveProperty<MenuItem>();
        public ReactiveProperty<string> Text { get; set; } = new ReactiveProperty<string>();

        public ImageCellPageViewModel()
        {
            ListView = new ReactiveCollection<MenuItem>
            {
                //面倒だったので同じようなコードが並んでいるのはご容赦を。
                new MenuItem {Title="藤崎詩織",Target="幼馴染というだけでも嫌なのに",Image="ListViewTest.Resources.藤崎詩織.JPG" },
                new MenuItem {Title="陽ノ下光",Target="ちょっと無理だよ…ごめんね!",Image="ListViewTest.Resources.陽ノ下光.JPG" },
                new MenuItem {Title="麻生華澄",Target="教師とのアバンチュール",Image="ListViewTest.Resources.麻生華澄.JPG" },
                new MenuItem {Title="佐倉楓子",Target="遠距離恋愛",Image="ListViewTest.Resources.佐倉楓子.JPG" },
                new MenuItem {Title="伊集院メイ",Target="めっちゃかわいい",Image="ListViewTest.Resources.伊集院メイ.JPG" },
                new MenuItem {Title="藤崎詩織",Target="幼馴染というだけでも嫌なのに",Image="ListViewTest.Resources.藤崎詩織.JPG" },
                new MenuItem {Title="陽ノ下光",Target="ちょっと無理だよ…ごめんね!",Image="ListViewTest.Resources.陽ノ下光.JPG" },
                new MenuItem {Title="麻生華澄",Target="教師とのアバンチュール",Image="ListViewTest.Resources.麻生華澄.JPG" },
                new MenuItem {Title="佐倉楓子",Target="遠距離恋愛",Image="ListViewTest.Resources.佐倉楓子.JPG" },
                new MenuItem {Title="伊集院メイ",Target="めっちゃかわいい",Image="ListViewTest.Resources.伊集院メイ.JPG" },
                new MenuItem {Title="藤崎詩織",Target="幼馴染というだけでも嫌なのに",Image="ListViewTest.Resources.藤崎詩織.JPG" },
                new MenuItem {Title="陽ノ下光",Target="ちょっと無理だよ…ごめんね!",Image="ListViewTest.Resources.陽ノ下光.JPG" },
                new MenuItem {Title="麻生華澄",Target="教師とのアバンチュール",Image="ListViewTest.Resources.麻生華澄.JPG" },
                new MenuItem {Title="佐倉楓子",Target="遠距離恋愛",Image="ListViewTest.Resources.佐倉楓子.JPG" },
                new MenuItem {Title="伊集院メイ",Target="めっちゃかわいい",Image="ListViewTest.Resources.伊集院メイ.JPG" },
            };

            //LinQで書く
            SelectItem.Where(x => x != null).Subscribe(x => Text.Value = x.Target);

        }
    }
}

[動作確認]
Android
f:id:roamschemer:20180918025436g:plain
UWP
f:id:roamschemer:20180918025440g:plain
画像が入るだけで結構印象が良くなるので使いどころは多そうです。ただ、UWPの場合、画像のサイズにあわせてリストの高さや位置が決定するので、大きなサイズの画像を使ったり違うサイズを並べてしまうと変な感じになるので注意が必要っぽいです。なんか回避策あるのかな?

ViewCell

ListViewの中身を自由にカスタマイズできる凄い奴です。画像やボタン、その他もろもろリスト化できるので便利ですがかなり面倒です。めっちゃ苦戦しました。
[View]

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="ListViewTest.Views.ViewCellPage"
             Title="ViewCell">

    <StackLayout>
        <ListView CachingStrategy="RecycleElement" ItemsSource="{Binding ListView}" SelectedItem="{Binding SelectItem.Value}" >
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ViewCell>
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="*" />
                                <ColumnDefinition Width="3*" />
                                <ColumnDefinition Width="*" />
                                <ColumnDefinition Width="*" />
                            </Grid.ColumnDefinitions>
                            <Label Grid.Column="0" TextColor="Pink" 
                                   Text="{Binding Title}" />
                            <Label Grid.Column="1" HorizontalTextAlignment = "End"
                                   Text="{Binding Detail.Value}" />
                            <Button Grid.Column="2" Text="入力"
                                    Command="{Binding Input}" />
                            <Button Grid.Column="3" Text="消去"
                                    Command="{Binding Delete}" />
                        </Grid>
                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </StackLayout>
    
</ContentPage>

[ViewModel]

using Prism.Mvvm;
using Reactive.Bindings;
using System;
using System.Linq;
using System.Reactive.Linq;

namespace ListViewTest.ViewModels
{
	public class ViewCellPageViewModel : BindableBase
	{
        public class MenuItem
        {
            public string Title { get; set; }
            public ReactiveProperty<string> Detail { get; set; } = new ReactiveProperty<string>();
            public ReactiveCommand Input { get; set; } = new ReactiveCommand();
            public ReactiveCommand Delete { get; set; } = new ReactiveCommand();

            public MenuItem()
            {
                Input.Subscribe(() => Detail.Value = "入力ボタンを押しました");
                Delete.Subscribe(() => Detail.Value = "消去ボタンを押しました");
            }
        }

        public ReactiveCollection<MenuItem> ListView { get; set; } = new ReactiveCollection<MenuItem>();
        public ReactiveProperty<MenuItem> SelectItem { get; set; } = new ReactiveProperty<MenuItem>();

        public ViewCellPageViewModel()
        {
            ListView = new ReactiveCollection<MenuItem>();
            foreach (var i in Enumerable.Range(1, 1000))
            {
                ListView.Add(new MenuItem { Title = $"No.{i.ToString("00000")}" });
            }

            SelectItem.Where(x => x != null).Subscribe(x =>
            {
                x.Detail.Value = "リストを押しました";
            });
        }
	}
}

実際には前半のMenuItemクラスはModelに置いた方が良いみたいです。

[動作確認]
Android
f:id:roamschemer:20180918025714g:plain
UWP
f:id:roamschemer:20180918025727g:plain
ViewModelの部分が良くわからなくて苦戦しました。先にリストの中身(MenuItem)を作成して、それをメイン(ViewCellPageViewModel)でリスト化するというようなイメージで書くと解りやすかったです。

GitHubに公開

今回からGitHubへ公開してみました。VSから直接行けるみたいなので、思ったより簡単には出来たけど…出来てるのかな?
github.com

まとめ

文字を入力できるEntryCellや、スイッチを配置できるSwitchCellもあるけど使いどころが限定的なので今回は省略。使いそうなときにまた勉強します。
それにしても便利で使いどころ満載なこのListViewですけどAndroidとUWPでかなり見た目が変わってくるので、両方を考える場合は使い方に注意が必要かもしれません。
そして、実はこのListViewに関しては更に問題点が存在するのですが…それはまた次の機会に…。