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

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

コントロール配列の呪いを解きたい

放浪軍師のXamarin.Formsによるアプリ開発
今回はコントロール配列からの脱却を考察します。
まず、初めて訪問された方は以下をお読みください。

www.gunshi.info

コントロール配列は嫌だ!

前回書かせていただきましたが、VB6の象徴であるところのコントロール配列からの脱却が今回のテーマです。
www.gunshi.info
恥ずかしげもなく助けを求めたは良いけれど、ただそれを待つだけではいつ解決するかもわかりませんし、色々と足掻いてみようと思います。

とりあえず普通に書いてみる

まずはコントロールが1つの場合のコードを書いてみます。ボタンを押すとボタンの文字(Text)と文字色(TextColor)と背景色(BackgroundColor)が変化します。
[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"
             x:Class="AntiControlArray.Views.MainPage"
             Title="アンチコントロール配列">

    <StackLayout>
        <Button Command="{Binding Command}" 
                Text="{Binding Text.Value}" 
                TextColor="{Binding TextColor.Value}"
                BackgroundColor="{Binding BackgroundColor.Value}"
                />
    </StackLayout>

</ContentPage>

[ViewModel]

using Prism.Navigation;
using Reactive.Bindings;
using System;
using Xamarin.Forms;
//using System.Drawing; ← これは罠

namespace AntiControlArray.ViewModels
{
    public class MainPageViewModel : ViewModelBase
    {

        public ReactiveCommand Command { get; } = new ReactiveCommand();
        public ReactiveProperty<string> Text { get; set; } = new ReactiveProperty<string>("まだ押してないよ!");
        public ReactiveProperty<Color> TextColor { get; set; } = new ReactiveProperty<Color>(Color.Black);
        public ReactiveProperty<Color> BackgroundColor { get; set; } = new ReactiveProperty<Color>(Color.Pink);

        public MainPageViewModel(INavigationService navigationService)
            : base(navigationService)
        {
            Command.Subscribe(_ =>
            {
                Text.Value = "今押したよ!";
                TextColor.Value = Color.Red;
                BackgroundColor.Value = Color.Yellow;
            });
        }
    }
}

[UWP]
f:id:roamschemer:20180906011326g:plain
まぁここは問題ないと思います。

あ、ちなみにTextColorとかはstring型じゃなくてColor型の方が良いよと、コメントにて教えていただいたのですが、using System.Drawing; を使うと反映しないので注意が必要です。using Xamarin.Forms;を使う必要があります。軽くハマりました。

ボタンを増やして色々やってみる

さて、ここからが本番。Task.Runの時みたいに色々試してみますよ~。ちなみに俺はいつもこうやって実際にミニプログラムを作って実験して、その結果を手元に置いてたりします。これって多分GitHubとかで公開すると初心者には便利だとは思うんですが、まだGitHubは良くわかってないんですよね…ワタシエイゴワカリマセン

1.ガチガチのコントロール配列

俺の素の実力はこんなもんです。
[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"
             x:Class="AntiControlArray.Views.MainPage"
             Title="アンチコントロール配列">

    <StackLayout>
        <Button Command="{Binding Command[0]}" 
                Text="{Binding Text[0].Value}" 
                TextColor="{Binding TextColor[0].Value}"
                BackgroundColor="{Binding BackgroundColor[0].Value}"
                />
        <Button Command="{Binding Command[1]}" 
                Text="{Binding Text[1].Value}" 
                TextColor="{Binding TextColor[1].Value}"
                BackgroundColor="{Binding BackgroundColor[1].Value}"
                />
        <Button Command="{Binding Command[2]}" 
                Text="{Binding Text[2].Value}" 
                TextColor="{Binding TextColor[2].Value}"
                BackgroundColor="{Binding BackgroundColor[2].Value}"
                />
        <Button Command="{Binding Command[3]}" 
                Text="{Binding Text[3].Value}" 
                TextColor="{Binding TextColor[3].Value}"
                BackgroundColor="{Binding BackgroundColor[3].Value}"
                />
    </StackLayout>

</ContentPage>

[ViewModel]

using Prism.Navigation;
using Reactive.Bindings;
using System;
using Xamarin.Forms;

namespace AntiControlArray.ViewModels
{
    public class MainPageViewModel : ViewModelBase
    {
        public ReactiveCommand[] Command { get; } = new ReactiveCommand[4];
        public ReactiveProperty<string>[] Text { get; set; } = new ReactiveProperty<string>[4];
        public ReactiveProperty<Color>[] TextColor { get; set; } = new ReactiveProperty<Color>[4];
        public ReactiveProperty<Color>[] BackgroundColor { get; set; } = new ReactiveProperty<Color>[4];

        public MainPageViewModel(INavigationService navigationService)
            : base(navigationService)
        {
            for (int i = 0; i < Command.Length; i++)
            {
                Command[i] = new ReactiveCommand();
                Text[i] = new ReactiveProperty<string>("まだ押してないよ!");
                TextColor[i] = new ReactiveProperty<Color>(Color.Black);
                BackgroundColor[i] = new ReactiveProperty<Color>(Color.Pink);

                int j = i; //←こうしないとエラーになる。
                Command[j].Subscribe(_ =>
                {
                    Text[j].Value = "今押したよ!";
                    TextColor[j].Value = Color.Red;
                    BackgroundColor[j].Value = Color.Yellow;
                });

            }

        }
    }
}

[UWP]
f:id:roamschemer:20180906020400g:plain
もうすげー泣きたくなるコードだが仕方ないので晒します。俺が普通に考えたらこんな感じになります。まぁ当然ながら想定した動きはするものの、コード的には宣言時に[4]って其々に付いてるのが兎に角ダサいですね。また、for文も今一どうかと思います。極めつけは int j = iで、なにやってんって感じですがこうしないとボタンを押した後にエラーとなります。forを抜けた後i=4となったままになるので、ボタンを押すと存在しない配列を参照してしまうようです。まぁ思ったよりはコンパクトではあるが…うーん…

2.ReactiveCommand< T >とCommandParameterを使う。

コメントにて通りすがりさんに教えていただいた方法になります。最近個人的にHOTなReactiveCommand< T >を使うようです。
[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"
             x:Class="AntiControlArray.Views.MainPage"
             Title="アンチコントロール配列">

    <StackLayout>
        <Button Command="{Binding Command}"
                CommandParameter="0"
                Text="{Binding Text[0].Value}" 
                TextColor="{Binding TextColor[0].Value}"
                BackgroundColor="{Binding BackgroundColor[0].Value}"
                />
        <Button Command="{Binding Command}"
                CommandParameter="1"
                Text="{Binding Text[1].Value}" 
                TextColor="{Binding TextColor[1].Value}"
                BackgroundColor="{Binding BackgroundColor[1].Value}"
                />
        <Button Command="{Binding Command}"
                CommandParameter="2"
                Text="{Binding Text[2].Value}" 
                TextColor="{Binding TextColor[2].Value}"
                BackgroundColor="{Binding BackgroundColor[2].Value}"
                />
        <Button Command="{Binding Command}"
                CommandParameter="3"
                Text="{Binding Text[3].Value}" 
                TextColor="{Binding TextColor[3].Value}"
                BackgroundColor="{Binding BackgroundColor[3].Value}"
                />
    </StackLayout>

</ContentPage>

[ViewModel]

using Prism.Navigation;
using Reactive.Bindings;
using System;
using Xamarin.Forms;

namespace AntiControlArray.ViewModels
{
    public class MainPageViewModel : ViewModelBase
    {
        public ReactiveCommand<string> Command { get; } = new ReactiveCommand<string>();
        public ReactiveProperty<string>[] Text { get; set; } = new ReactiveProperty<string>[4];
        public ReactiveProperty<Color>[] TextColor { get; set; } = new ReactiveProperty<Color>[4];
        public ReactiveProperty<Color>[] BackgroundColor { get; set; } = new ReactiveProperty<Color>[4];

        public MainPageViewModel(INavigationService navigationService) : base(navigationService)
        {
            for (int i = 0; i < Text.Length; i++)
            {
                Text[i] = new ReactiveProperty<string>("まだ押してないよ!");
                TextColor[i] = new ReactiveProperty<Color>(Color.Black);
                BackgroundColor[i] = new ReactiveProperty<Color>(Color.Pink);
            }

            Command.Subscribe(x =>
            {
                int i = int.Parse(x);
                Text[i].Value = "今押したよ!";
                TextColor[i].Value = Color.Red;
                BackgroundColor[i].Value = Color.Yellow;
            });
        }
    }
}

ん~…まぁReactiveCommandが一本化されたのと、int i=j が無くなったので少しは良いのかもしれませんが、そう劇的な改善とはいきませんでした。もしかしたら教えてくださった方は他の使い方をするつもりだったのかもしれません。もうちょっと何とかならないものか…

3.ListViewを使う

ちょっと大きく方向転換。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"
             x:Class="AntiControlArray.Views.MainPage"
             Title="アンチコントロール配列">

    <ListView ItemsSource="{Binding ListView}" SelectedItem="{Binding SelectItem.Value}">
        <ListView.ItemTemplate>
            <DataTemplate>
                <ViewCell>
                    <StackLayout>
                        <Button Command="{Binding Command}"
                                Text="{Binding Text.Value}" 
                                TextColor="{Binding TextColor.Value}"
                                BackgroundColor="{Binding BackgroundColor.Value}"
                        />
                    </StackLayout>
                </ViewCell>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>

</ContentPage>

[ViewModel]

using Prism.Navigation;
using Reactive.Bindings;
using System;
using System.Linq;
using Xamarin.Forms;

namespace AntiControlArray.ViewModels
{
    /// <summary>
    /// ListViewの中身
    /// </summary>
    public class ListItem
    {
        public ReactiveCommand Command { get; } = new ReactiveCommand();
        public ReactiveProperty<string> Text { get; set; } = new ReactiveProperty<string>("まだ押してないよ!");
        public ReactiveProperty<Color> TextColor { get; set; } = new ReactiveProperty<Color>(Color.Black);
        public ReactiveProperty<Color> BackgroundColor { get; set; } = new ReactiveProperty<Color>(Color.Pink);
        public ListItem()
        {
            Command.Subscribe(_ =>
            {
                Text.Value = "今押したよ!";
                TextColor.Value = Color.Red;
                BackgroundColor.Value = Color.Yellow;
            });
        }
    }

    public class MainPageViewModel : ViewModelBase
    {
        private int FastListCount = 20;//起動時のリスト数
        public ReactiveCollection<ListItem> ListView { get; set; } = new ReactiveCollection<ListItem>();
        public ReactiveProperty<ListItem> SelectItem { get; set; } = new ReactiveProperty<ListItem>();

        public MainPageViewModel(INavigationService navigationService) : base(navigationService)
        {
            ListView = new ReactiveCollection<ListItem>();
            foreach (var i in Enumerable.Range(1, FastListCount))
            {
                ListView.Add(new ListItem());
            }
        }
    }
}

[UWP]
f:id:roamschemer:20180906224155g:plain
おお、中々良さそうじゃないですか!?FastListCount の値次第で自由自在にボタンを増やすことができますし、スクロールもできます!コードもかなりすっきりで、まさにコントロール配列からの脱却が達成できています。ただ、難点としてはこれあくまでもリストなんで複雑に並べたりできないんですよね…Gridとは相性が悪いです。

あと今回は余談となりますが、

foreach (var i in Enumerable.Range(1, FastListCount))
{
    ListView.Add(new ListItem());
}

の部分って、多分LinQ一行で書けるんじゃないかと思うんですよ。でも書けませんでした…どうすればいいんだろ?

結局完璧な解決策は見つからず…

うーん、ListViewを使う方法が見た目はいいんだけどねぇ…ReactiveCommand< T >と組み合わせれば何とかなるのかもしれません。
とりあえず今回はここまでとします。ListView…イカすぜ!