しっぽを追いかけて

ぐるぐるしながら考えています

Unity と猫の話題が中心   掲載内容は個人の私見であり、所属組織の見解ではありません

Xamarin で ItemsControl っぽいコントロールを作りたい(1)

以前 プラットフォームごとに配色の異なるアナログ時計を表示するアプリを実装した際に ItemsControl 風の AbsoluteLayout を作りました

これに少し手を入れてもうちょい ItemsControl に近いコントロールを Xamarin.Forms で作ってみたいと思います

とはいえ今後 Xamarin の中の人が正式な ItemsControl を作ってくれるはず!なので、本家 ItemsControl には遠く及ばない簡易的なものにはなると思いますが・・・

まずは何はなくとも ItemsControls.cs

/// <summary>
/// ItemsControl 風 View
/// </summary>
public class ItemsControl : ContentView
{
    /// <summary>
    /// ItemsPanel
    /// </summary>
    private Layout<View> itemsPanel = null;

    /// <summary>
    /// ItemsPanel CLR プロパティ
    /// </summary>
    public Layout<View> ItemsPanel
    {
        get { return this.itemsPanel; }
        set { this.itemsPanel = value; }
    }

    #region ItemsSource

    /// <summary>
    /// ItemsSource BindableProperty
    /// </summary>
    public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create<ItemsControl, IEnumerable>(
        p => p.ItemsSource,
        new ObservableCollection<object>(),
        BindingMode.OneWay,
        null,
        OnItemsSourceChanged);

    /// <summary>
    /// ItemsSource CLR プロパティ
    /// </summary>
    public IEnumerable ItemsSource
    {
        get { return (IEnumerable)this.GetValue(ItemsSourceProperty); }
        set { this.SetValue(ItemsSourceProperty, value); }
    }

    /// <summary>
    /// ItemsSource 変更イベントハンドラ
    /// </summary>
    /// <param name="bindable">BindableObject</param>
    /// <param name="oldValue">古い値</param>
    /// <param name="newValue">新しい値</param>
    private static void OnItemsSourceChanged(BindableObject bindable, IEnumerable oldValue, IEnumerable newValue)
    {
        var control = bindable as ItemsControl;
        if (control == null)
        {
            return;
        }

        var oldCollection = oldValue as INotifyCollectionChanged;
        if (oldCollection != null)
        {
            oldCollection.CollectionChanged -= control.OnCollectionChanged;
        }

        if (newValue == null)
        {
            return;
        }

        control.ItemsPanel.Children.Clear();

        foreach (var item in newValue)
        {
            var content = control.ItemTemplate.CreateContent();
            View view;
            var cell = content as ViewCell;
            if (cell != null)
            {
                view = cell.View;
            }
            else
            {
                view = (View)content;
            }

            view.BindingContext = item;
            control.ItemsPanel.Children.Add(view);
        }

        var newCollection = newValue as INotifyCollectionChanged;
        if (newCollection != null)
        {
            newCollection.CollectionChanged += control.OnCollectionChanged;
        }

        control.UpdateChildrenLayout();
        control.InvalidateLayout();
    }

    #endregion //ItemsSource

    #region ItemTemplate

    /// <summary>
    /// ItemTemplate BindableProperty
    /// </summary>
    public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create<ItemsControl, DataTemplate>(
        p => p.ItemTemplate,
        default(DataTemplate));

    /// <summary>
    /// ItemTemplate CLR プロパティ
    /// </summary>
    public DataTemplate ItemTemplate
    {
        get { return (DataTemplate)this.GetValue(ItemTemplateProperty); }
        set { this.SetValue(ItemTemplateProperty, value); }
    }

    #endregion //ItemTemplate

    /// <summary>
    /// コンストラクタ
    /// </summary>
    public ItemsControl()
    {
        this.itemsPanel = new StackLayout();
        this.Content = this.itemsPanel;
    }

    /// <summary>
    /// Items の変更イベントハンドラ
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.OldItems != null)
        {
            this.ItemsPanel.Children.RemoveAt(e.OldStartingIndex);
            this.UpdateChildrenLayout();
            this.InvalidateLayout();
        }

        var collection = this.ItemsSource as ObservableCollection<object>;
        if (e.NewItems == null || collection == null)
        {
            return;
        }
        foreach (var item in e.NewItems)
        {
            var content = this.ItemTemplate.CreateContent();

            View view;
            var cell = content as ViewCell;
            if (cell != null)
            {
                view = cell.View;
            }
            else
            {
                view = (View)content;
            }

            view.BindingContext = item;
            this.ItemsPanel.Children.Insert(collection.IndexOf(item), view);
        }

        this.UpdateChildrenLayout();
        this.InvalidateLayout();
    }
}

以前の ItemsControl 風 AbsoluteLayout とは異なり、継承ではなくコンポジションで ItemsPanel を持っています あとは ItemsSource の変更時の処理を少し細工してコレクションの型を XAML で指定しなくてもよいようにしました

お次は ViewModel

/// <summary>
/// トップ画面の ViewModel
/// </summary>
public class TopPageViewModel : BindableBase
{
    /// <summary>
    /// アイテム
    /// </summary>
    private ObservableCollection<string> items;

    /// <summary>
    /// アイテム
    /// </summary>
    public ObservableCollection<string> Items
    {
        get { return this.items; }
        set { this.SetProperty<ObservableCollection<string>>(ref this.items, value); }
    }

    /// <summary>
    /// コンストラクタ
    /// </summary>
    public TopPageViewModel()
    {
        this.items = new ObservableCollection<string>()
        {
            "ラグドール",
            "ノルウェージャンフォレストキャット",
            "メインクーン",
            "アメリカンショートヘア",
            "スコティッシュフォールド",
            "マンチカン",
        };
    }

Items を持っているだけの単純な ViewModel です

最後に XAML

<?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:b="clr-namespace:XamarinControl.Behaviors;assembly=XamarinControl"
             xmlns:c="clr-namespace:XamarinControl.Controls;assembly=XamarinControl"
             xmlns:vm="clr-namespace:XamarinControl.ViewModels;assembly=XamarinControl"
             x:Class="XamarinControl.Views.TopPage">

  <ContentPage.BindingContext>
    <vm:TopPageViewModel/>
  </ContentPage.BindingContext>
  
  <c:ItemsControl ItemsSource ="{Binding Items}">
    <c:ItemsControl.ItemTemplate>
      <DataTemplate>
        <Grid Padding="20,10">
          <Label Text="{Binding}"/>
        </Grid>
      </DataTemplate>
    </c:ItemsControl.ItemTemplate>
  </c:ItemsControl>
</ContentPage>

本家 ItemsControl のように ItemsSource と ItemTemplate を指定しています

最終的なソリューション構成はこんな感じ

f:id:matatabi_ux:20150125154144p:plain

いつものように実行あるのみ

f:id:matatabi_ux:20150125154213p:plain

ちゃんと表示されましたね