LoginSignup
1
0

More than 1 year has passed since last update.

MauiのCollectionViewに行番号を表示する方法(Converterを使う)

Last updated at Posted at 2022-11-12

はじめに

Microsoft Maui の CollectionView に行番号を表示する方法の一つをここに残します。
ちなみに私は、行番号を表示するという見かけ上の要件のために ViewModel に項目を追加することは悪手だという考えの持ち主ですので、ここで触れる方法はView側だけで対応するものになっています。

この記事で得られるもの

  • Microsoft Maui の CollectionView に、Converter を使って行番号を表示する方法。
  • MarkupExtension を使って行番号を表示する方法は WinUI 以外ではうまくいかないという罠の学び。

前提スキル

検証環境

(Windows)

  • Visual Studio 2022 17.3.6
  • dotnet sdk 6.0.402

(Mac)

  • Visual Studio for Mac 17.4 Preview (build 2366)
  • XCode 14.0.1

(iOS)

  • iPhone SE (2gen) iOS 16.0

(Android)

  • ZenFone Live (L1) Android 8.0

【2022/11/13 追記】以下の環境でも同じ動作/現象となることを確認しました。

  • Visual Studio 2022 17.4
  • dotnet sdk 6.0.403

お急ぎの方へ

  • CollectionView の ItemsSource と 行データインスタンスを元に、行番号を計算する「RowNumberConverter」を作成します。
  • 作成したソースコードはこちら: Github - atsuteru/MauiAppCollectionWithRowNumber

本編

これから説明する内容は、Maui の CollectionView を使って、次のスクリーンショットのように行番号を表示する方法を説明するものです。

WinUI

MauiAppCollectionWithRowNumber_WinUI.png

MacCatalyst

MauiAppCollectionWithRowNumber_MacCatalyst.JPEG

iOS

MauiAppCollectionWithRowNumber_iOS.png

Android

MauiAppCollectionWithRowNumber_Android.jpg

概要

  • 行番号の表示箇所は、IValueConverter を実装する RowNumberConverter を作成し、実行時に行番号を計算させます。

詳細

1. RowNumberConverterを作成します。

  • 変換対象の値(value)は行データを想定します。
  • コレクションにおける行データの位置を計算するため、コレクションを CollectionView#ItemsSource から受け取ります。そのために BindableProperty ItemsSourceProperty を宣言します。
  • ついでに 行番号の表示フォーマットを ConverterParameter で指定できるようにします。
  • あとは、行番号を計算するメソッド ToRowNumber(行データ, コレクション, 表示フォーマット) を実装すれば完成です。
RowNumberConverter.cs
using System.Collections;
using System.Globalization;

namespace MauiAppCollectionWithRowNumber
{
    public class RowNumberConverter : BindableObject, IValueConverter
    {
        public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create(
            nameof(ItemsSource), typeof(IEnumerable), typeof(RowNumberConverter), defaultValue: null, defaultBindingMode: BindingMode.OneWay);

        public IEnumerable ItemsSource { get => (IEnumerable)GetValue(ItemsSourceProperty); set => SetValue(ItemsSourceProperty, value); }

        object IValueConverter.Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return ToRowNumber(rowData: value, rows: ItemsSource, format: parameter.ToString());
        }

        object IValueConverter.ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return null;
        }

        private string ToRowNumber(object rowData, IEnumerable rows, string format)
        {
            return (rows.Cast<object>().TakeWhile(x => !ReferenceEquals(x, rowData)).Count() + 1).ToString(format);
        }
    }
}

2. RowNumberConverterを利用して、CollectionViewで行番号を表示します。

  • RowNumberConverterを、CollectionView の Resource として宣言し、ItemsSource をバインドします。
  • 宣言された RowNumberConverter を、行番号の表示箇所で StaticResource として指定します。この時、Binding#Path の指定は省略します(行データそのものをRowNumberConverterに渡すため)
  • 行番号の表示箇所ではついでに、ConverterParameter に表示フォーマットも渡しています。
MainPage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage 
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:MauiAppCollectionWithRowNumber"
    x:Class="MauiAppCollectionWithRowNumber.MainPage">

    <CollectionView
        x:Name="PersonCollectionView"
        ItemsSource="{Binding Persons}"
        Margin="40">
        <CollectionView.Resources>
            <local:RowNumberConverter x:Key="RowNumberConverter" ItemsSource="{Binding Source={Reference PersonCollectionView}, Path=ItemsSource}"/>
        </CollectionView.Resources>
        <CollectionView.ItemTemplate>
            <DataTemplate>
                <Grid MinimumHeightRequest="40">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="0.2*"/>
                        <ColumnDefinition Width="0.8*"/>
                    </Grid.ColumnDefinitions>
                    <Label Grid.Column="0" Text="{Binding Converter={StaticResource RowNumberConverter}, ConverterParameter='000'}"/>
                    <Label Grid.Column="1" Text="{Binding Name}"/>
                </Grid>
            </DataTemplate>
        </CollectionView.ItemTemplate>
    </CollectionView>

</ContentPage>

3. 全体のソースコード

Github - atsuteru/MauiAppCollectionWithRowNumber をご覧ください。

蛇足

実はこの検証の際、「Converter よりも MarkupExtension を使ったほうがおしゃれに書けんじゃね?」と思ってやってみたのですが、その結果は「WinUI では問題なく動いているように見えるが、MacCatalyst/iOS/Android ではスクロールすると行番号が狂う」というものでした。同じ無駄を踏む方が一人でも減るよう、その失敗作をご紹介しておきます。

起きた現象

1つ目の列が Converter, 2つ目の列が、MarkupExtension を使った結果です。
2つ目の列は行番号が狂っていますね。スクロールするとこうなってしまうのですが、原因はおそらく「セルの再利用機構」です。
MarkupExtensionの場合、そのExtensionのインスタンスがセルの値として登録されてしまい、スクロール時の再計算判定において『再計算不要』とジャッジされてしまうからではないか、と推測しています。

iOS

MauiAppCollectionWithRowNumber_iOS_ext.png

ダメだったソースコード

こう書けたらおしゃれだと思ったのですが。。。

MainPage.xamlのCollectionView2列目
<Label Grid.Column="1" Text="{local:RowNumber Item={Binding}, ItemsSource={Binding Source={Reference PersonCollectionView}, Path=ItemsSource}, Format='000'}"/>

スクロール時に ProvideValueメソッド が呼ばれていませんでした。

RowNumberExtension.cs
using System.Collections;
using System.Runtime.CompilerServices;

namespace MauiAppCollectionWithRowNumber
{
    public class RowNumberExtension : BindableObject, IMarkupExtension<BindingBase>
    {
        public static readonly BindableProperty ItemsSourceProperty =
            BindableProperty.Create(nameof(ItemsSource), typeof(IEnumerable), typeof(RowNumberExtension), null, BindingMode.OneWay);
        public static readonly BindableProperty ItemProperty =
            BindableProperty.Create(nameof(Item), typeof(object), typeof(RowNumberExtension), null, BindingMode.OneWay);
        public static readonly BindableProperty FormatProperty =
            BindableProperty.Create(nameof(Format), typeof(string), typeof(RowNumberExtension), null, BindingMode.OneWay);
        public static readonly BindableProperty ValueProperty =
            BindableProperty.Create(nameof(Value), typeof(string), typeof(RowNumberExtension), string.Empty, BindingMode.OneWayToSource);

        public IEnumerable ItemsSource
        {
            get => (IEnumerable)GetValue(ItemsSourceProperty);
            set => SetValue(ItemsSourceProperty, value);
        }
        public object Item
        {
            get => (object)GetValue(ItemProperty);
            set => SetValue(ItemProperty, value);
        }
        public string Format
        {
            get => (string)GetValue(FormatProperty);
            set => SetValue(FormatProperty, value);
        }
        public string Value
        {
            get => (string)GetValue(ValueProperty);
            set => SetValue(ValueProperty, value);
        }

        BindingBase IMarkupExtension<BindingBase>.ProvideValue(IServiceProvider serviceProvider)
        {
            return (BindingBase)((IMarkupExtension)this).ProvideValue(serviceProvider);
        }

        object IMarkupExtension.ProvideValue(IServiceProvider serviceProvider)
        {
            ProvideContext(serviceProvider);
            return new Binding(ValueProperty.PropertyName, source: this);
        }
        private void ProvideContext(IServiceProvider serviceProvider)
        {
            var target = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
            var targetObject = (BindableObject)target.TargetObject;
            EventHandler bindingChanged = null;
            bindingChanged = (sender, e) =>
            {
                BindingContext = targetObject.BindingContext;
                targetObject.BindingContextChanged -= bindingChanged;
            };
            targetObject.BindingContextChanged += bindingChanged;
        }

        protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            switch (propertyName)
            {
                case nameof(ItemsSource):
                case nameof(Item):
                case nameof(Format):
                    Value = ToRowNumber(Item, ItemsSource, Format);
                    break;
                default:
                    break;
            }
            base.OnPropertyChanged(propertyName);
        }

        protected virtual string ToRowNumber(object rowData, IEnumerable rows, string format)
        {
            if (rows == null || rowData == null)
            {
                return string.Empty;
            }
            return (rows.Cast<object>().TakeWhile(x => !ReferenceEquals(x, rowData)).Count() + 1).ToString(format ?? "#");
        }
    }
}

MarkupExtensionを使ってみたパターンの全体のソースコードは、
Github - atsuteru/MauiAppCollectionWithRowNumber/tree/try_extension をご覧ください。

もしこの問題の解決方法をご存じの方は、よろしければご教授ください。。。🙇‍♂️

おわりに

この記事を書いている時(2022/11/12)に、気が付いたのですが…
その4日前(2022/11/8)に VisualStudio 17.4 と dotnet 6.0.11 / 7.0.0 がリリースされていました。

公開日時点で、古い環境での投稿となってしまうとは・・・
でももうそのまま上げます。しょっちゅうリリースされているので、付き合ってたらキリがないとうことで!😂

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0