かずきのBlog@hatena

日本マイクロソフトに勤めています。XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

Xamarin.Android + ReactivePropertyでListViewを使う

特にReactivePropertyでサポートはしてないので自前でやるっきゃないです!ということでこういうクラスを書いてみました。

ReadOnlyReactiveCollection型をIListAdapterに変換するコードです。

public static class ReadOnlyCollectionExtensions
{
    /// <summary>
    /// ReadOnlyReactiveCollectionをIListAdapterに変換する
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="self"></param>
    /// <param name="createRowView">行のデータを表示するためのViewを作る処理</param>
    /// <param name="setRowData">行にデータを設定する処理</param>
    /// <returns></returns>
    public static IListAdapter ToAdapter<T>(this ReadOnlyReactiveCollection<T> self, 
        Func<View> createRowView,
        Action<T, View> setRowData)
    {
        return new ReadOnlyReactiveCollectionAdapter<T>(self, createRowView, setRowData);
    }
}

/// <summary>
/// ReadOnlyReactiveCollection用のAdapterクラス
/// </summary>
/// <typeparam name="T"></typeparam>
class ReadOnlyReactiveCollectionAdapter<T> : BaseAdapter<T>
{
    // もとになるコレクション
    private ReadOnlyReactiveCollection<T> source;
    // 行のデータを表示するためのViewを作る処理
    private Func<View> createRowView;
    // 行にデータを設定する処理
    private Action<T, View> setRowData;

    public ReadOnlyReactiveCollectionAdapter(
        ReadOnlyReactiveCollection<T> source,
        Func<View> createRowView,
        Action<T, View> setRowData)
    {
        this.source = source;
        this.createRowView = createRowView;
        this.setRowData = setRowData;
    }

    public override T this[int position]
    {
        get { return source[position]; }
    }

    public override int Count
    {
        get { return this.source.Count; }
    }

    public override long GetItemId(int position)
    {
        return position;
    }

    public override View GetView(int position, View convertView, ViewGroup parent)
    {
        if (convertView == null)
        {
            convertView = this.createRowView();
        }
        this.setRowData(this[position], convertView);
        return convertView;
    }
}

使い方は簡単です。以下のようなコマンドを実行するとコレクションにデータを追加するだけのViewModelがあったとします。

public class MainActivityViewModel
{
    private ObservableCollection<string> source = new ObservableCollection<string> { "a", "b", "c" };
    public ReadOnlyReactiveCollection<string> Items { get; private set; }

    public ReactiveCommand AddItemCommand { get; private set; }

    public MainActivityViewModel()
    {
        this.Items = source.ToReadOnlyReactiveCollection();

        this.AddItemCommand = new ReactiveCommand();
        this.AddItemCommand.Subscribe(_ =>
        {
            this.source.Add("item" + DateTime.Now);
        });
    }
}

そして、ボタンとListViewを置いた画面でさくっと紐づけ。

[Activity(Label = "App1", MainLauncher = true, Icon = "@drawable/icon")]
public class MainActivity : Activity
{
    private MainActivityViewModel viewModel = new MainActivityViewModel();

    protected override void OnCreate(Bundle bundle)
    {
        base.OnCreate(bundle);

        // Set our view from the "main" layout resource
        SetContentView(Resource.Layout.Main);

        this.FindViewById<Button>(Resource.Id.button1).Click += viewModel.AddItemCommand.ToEventHandler();

        var listView = this.FindViewById<ListView>(Resource.Id.listView1);
        listView.Adapter = viewModel.Items.ToAdapter(
            () => LayoutInflater.FromContext(this).Inflate(Resource.Layout.layout1, null),
            (x, v) => v.FindViewById<TextView>(Resource.Id.textView1).Text = x);
        this.viewModel.Items.CollectionChangedAsObservable().Subscribe(_ => listView.InvalidateViews());
    }
}

一応レイアウトファイルも

main.axml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <Button
        android:text="Button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/button1" />
    <ListView
        android:minWidth="25px"
        android:minHeight="25px"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/listView1" />
</LinearLayout>

layout1.axml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <ImageView
        android:src="@android:drawable/ic_menu_gallery"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:id="@+id/imageView1" />
    <TextView
        android:text="Medium Text"
        android:textAppearance="?android:attr/textAppearanceMedium"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:id="@+id/textView1" />
</LinearLayout>