はじめに
Microsoft Maui の CollectionView に行番号を表示する方法の一つをここに残します。
ちなみに私は、行番号を表示するという見かけ上の要件のために ViewModel に項目を追加することは悪手だという考えの持ち主ですので、ここで触れる方法はView側だけで対応するものになっています。
この記事で得られるもの
- Microsoft Maui の CollectionView に、Converter を使って行番号を表示する方法。
- MarkupExtension を使って行番号を表示する方法は WinUI 以外ではうまくいかないという罠の学び。
前提スキル
- VisualStudio と Maui のセットアップを完了し、CollectionView の基本的なレイアウト方法(.NET MAUI CollectionView レイアウトの指定)を理解していること。
検証環境
(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

MacCatalyst

iOS

Android

概要
- 行番号の表示箇所は、IValueConverter を実装する RowNumberConverter を作成し、実行時に行番号を計算させます。
詳細
1. RowNumberConverterを作成します。
- 変換対象の値(value)は行データを想定します。
- コレクションにおける行データの位置を計算するため、コレクションを CollectionView#ItemsSource から受け取ります。そのために BindableProperty ItemsSourceProperty を宣言します。
- ついでに 行番号の表示フォーマットを ConverterParameter で指定できるようにします。
- あとは、行番号を計算するメソッド ToRowNumber(行データ, コレクション, 表示フォーマット) を実装すれば完成です。
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 に表示フォーマットも渡しています。
<?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

ダメだったソースコード
こう書けたらおしゃれだと思ったのですが。。。
<Label Grid.Column="1" Text="{local:RowNumber Item={Binding}, ItemsSource={Binding Source={Reference PersonCollectionView}, Path=ItemsSource}, Format='000'}"/>
スクロール時に ProvideValueメソッド が呼ばれていませんでした。
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 がリリースされていました。
公開日時点で、古い環境での投稿となってしまうとは・・・
でももうそのまま上げます。しょっちゅうリリースされているので、付き合ってたらキリがないとうことで!😂