しっぽを追いかけて

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

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

Xamarin で交互に背景色を変えたリストを表示したい

ItemsControl は便利なコントロールなのでどんどん拡張していきたい!

というわけで、リストの背景色を交互に替える表示を ItemsControl を利用して作りたいと思います

今回のソリューションはこれまでのものを拡張して下記のようになります

f:id:matatabi_ux:20150129204334p:plain

スポンサードリンク

まずは DataTemplateSelector

/// <summary>
/// DataTemplateSelector の基底クラス
/// </summary>
public class DataTemplateSelector
{
    /// <summary>
    /// テンプレートを選択する
    /// </summary>
    /// <param name="item">アイテムのデータソース</param>
    /// <param name="container">アイテムのコンテナ</param>
    /// <param name="index">アイテムのインデックス</param>
    /// <returns>選択されたテンプレート</returns>
    public virtual DataTemplate SelectTemplate(object item, BindableObject container, int? index = null)
    {
        return null;
    }
}

継承前提でメソッド1つだけの基底クラスです

これを継承した AlternateItemTemplateSelector

/// <summary>
/// 交互にテンプレートを切り替える ItemTemplateSelector
/// </summary>
public class AlternativeItemTemplateSelector : DataTemplateSelector
{
    /// <summary>
    /// 偶数行のテンプレート
    /// </summary>
    public DataTemplate DefaultTemplate { get; set; }

    /// <summary>
    /// 奇数行のテンプレート
    /// </summary>
    public DataTemplate AlternateTemplate { get; set; }

    /// <summary>
    /// テンプレートを選択する
    /// </summary>
    /// <param name="item">アイテムのデータソース</param>
    /// <param name="container">アイテムのコンテナ</param>
    /// <param name="index">アイテムのインデックス</param>
    /// <returns>選択されたテンプレート</returns>
    public override DataTemplate SelectTemplate(object item, BindableObject container, int? index = null)
    {
        if (!index.HasValue || index.Value%2 == 0 || this.AlternateTemplate == null)
        {
            return this.DefaultTemplate;
        }

        return this.AlternateTemplate;
    }
}

アイテムの連番から偶数・奇数を判断しています・・・仮想化 UI が組めるようになったら ViewModel で判断した方がよさそうですがとりあえずこれで

次に ItemsControl はちょっと大改造

/// <summary>
/// ItemsControl 風 View
/// </summary>
public class ItemsControl : ContentView
{
    #region ItemsPanel

    /// <summary>
    /// ItemsPanel BindableProperty
    /// </summary>
    public static readonly BindableProperty ItemsPanelProperty = BindableProperty.Create<ItemsControl, Layout<View>>(
        p => p.ItemsPanel,
        new StackLayout(),
        BindingMode.OneWay,
        null,
        OnItemsPanelChanged);

    /// <summary>
    /// ItemsPanel CLR プロパティ
    /// </summary>
    public Layout<View> ItemsPanel
    {
        get { return (Layout<View>)this.GetValue(ItemsPanelProperty); }
        set { this.SetValue(ItemsPanelProperty, value); }
    }

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

        if (oldValue != null)
        {
            oldValue.Children.Clear();
        }

        if (newValue == null)
        {
            return;
        }

        control.ItemsRender();
        control.Content = newValue;
        control.UpdateChildrenLayout();
        control.InvalidateLayout();
    }

    #endregion //ItemsPanel

    #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.ItemsRender();

        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

    #region ItemTemplateSelector

    /// <summary>
    /// ItemTemplate BindableProperty
    /// </summary>
    public static readonly BindableProperty ItemTemplateSelectorProperty = BindableProperty.Create<ItemsControl, DataTemplateSelector>(
        p => p.ItemTemplateSelector,
        default(DataTemplateSelector),
        BindingMode.OneWay,
        null,
        OnItemTemplateSelectorChanged);

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

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

        control.ItemsRender();
    }

    #endregion //ItemTemplate

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

    /// <summary>
    /// アイテムの表示更新
    /// </summary>
    private void ItemsRender()
    {
        this.ItemsPanel.Children.Clear();

        var index = 0;
        foreach (var item in this.ItemsSource)
        {
            var template = this.ItemTemplateSelector != null
                ? this.ItemTemplateSelector.SelectTemplate(item, null, index)
                : this.ItemTemplate;
            var content = template.CreateContent();
            View view;
            var cell = content as ViewCell;
            if (cell != null)
            {
                view = cell.View;
            }
            else
            {
                view = (View)content;
            }

            view.BindingContext = item;
            this.ItemsPanel.Children.Add(view);
            index++;
        }
    }

    /// <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();
    }
}

重要なのは次の箇所です

    #region ItemTemplateSelector

    /// <summary>
    /// ItemTemplate BindableProperty
    /// </summary>
    public static readonly BindableProperty ItemTemplateSelectorProperty = BindableProperty.Create<ItemsControl, DataTemplateSelector>(
        p => p.ItemTemplateSelector,
        default(DataTemplateSelector),
        BindingMode.OneWay,
        null,
        OnItemTemplateSelectorChanged);

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

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

        control.ItemsRender();
    }

    #endregion //ItemTemplate

    /// <summary>
    /// アイテムの表示更新
    /// </summary>
    private void ItemsRender()
    {
        this.ItemsPanel.Children.Clear();

        var index = 0;
        foreach (var item in this.ItemsSource)
        {
            var template = this.ItemTemplateSelector != null
                ? this.ItemTemplateSelector.SelectTemplate(item, null, index)
                : this.ItemTemplate;
            var content = template.CreateContent();
            View view;
            var cell = content as ViewCell;
            if (cell != null)
            {
                view = cell.View;
            }
            else
            {
                view = (View)content;
            }

            view.BindingContext = item;
            this.ItemsPanel.Children.Add(view);
            index++;
        }
    }

ItemTemplateSelector なる BindableProperty を追加して、変更されたら ItemTemplateSelector で DataTemplate を選択し直しています

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:s="clr-namespace:XamarinControl.Selectors;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.ItemTemplateSelector>
      <s:AlternativeItemTemplateSelector>
        
        <!-- 奇数行のテンプレート -->
        <s:AlternativeItemTemplateSelector.DefaultTemplate>
          <DataTemplate>
            <Grid Padding="20,10" BackgroundColor="White">
              <Label Text="{Binding Title}" TextColor="Black"/>
            </Grid>
          </DataTemplate>
        </s:AlternativeItemTemplateSelector.DefaultTemplate>
        
        <!-- 偶数行のテンプレート -->
        <s:AlternativeItemTemplateSelector.AlternateTemplate>
          <DataTemplate>
            <Grid Padding="20,10" BackgroundColor="#FFFF6638">
              <Label Text="{Binding Title}" TextColor="Black"/>
            </Grid>
          </DataTemplate>
        </s:AlternativeItemTemplateSelector.AlternateTemplate>
      
      </s:AlternativeItemTemplateSelector>
    </c:ItemsControl.ItemTemplateSelector>
  </c:ItemsControl>
</ContentPage>

ItemsControl と ItemTemplateSelector が頑張ってくれるので XAML の記述は簡単です

いつものように実行

f:id:matatabi_ux:20150129203247p:plain

紅白の交互背景色が出ました