Open1

MAUI 一覧画面のBinding

y_a_yy_a_y

下記の例は「ページ + ページ用VM → 内部のカスタムView +カスタムView専用VM → アイテムVM」 という 多層ViewModel構造 を取った MVVM 構成。
View(XAML)はBindingContext を切り替えながら必要な部分にVMをバインドしている。

UserAcountListPage.xaml (Page)
└─ [BindingContext] → UserAcountListPageViewModel (DIで自動注入)
     └─ ListViewModel : UserAcountListViewModel
       └─ (BindingContext を渡す) → UserAcountListView.xaml (ContentView)
          └─ [BindingContext] → UserAcountListViewModel
             └─ Items : ObservableCollection<UserAcountListItemViewModel>
                ├─ [Item] UserAcountListItemViewModel
                │  └─ Data : UserAcount (Model)
                ├─ [Item] UserAcountListItemViewModel
                │  └─ Data : UserAcount (Model)
                └─ …(以下同様)

UserAcountListPage.xaml + UserAcountListPageViewModel

役割: 「ページ全体」の画面を表現。
XAML では x:DataType="vm:UserAcountListPageViewModel" を指定しているので、型安全なバインディング。
BindingContext は UserAcountListPageViewModel(コードビハインドではなくDI)
その中で <view:UserAcountListView BindingContext="{Binding ListViewModel}" /> として、子ビューに子VMを渡す。
ページは「親 ViewModel」を持ち、子View (UserAcountListView) に対して子VM (ListViewModel) を BindingContext として注入している構造。

UserAcountListPage.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"
             x:Class="MvvmMauiApp.Views.UserAcountListPage"
             xmlns:vm="clr-namespace:MvvmMauiApp.ViewModels"
             xmlns:view="clr-namespace:MvvmMauiApp.Views"
             x:DataType="vm:UserAcountListPageViewModel"
             Title="UserAcountListPage">
    <VerticalStackLayout>
        <view:UserAcountListView BindingContext="{Binding ListViewModel}" />
    </VerticalStackLayout>
</ContentPage>
UserAcountListPageViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;

namespace MvvmMauiApp.ViewModels
{
    public partial class UserAcountListPageViewModel : ObservableObject
    {
        [ObservableProperty]
        private UserAcountListViewModel listViewModel;

        public UserAcountListPageViewModel()
        {
            this.ListViewModel = new UserAcountListViewModel();
        }
    }
}

UserAcountListView.xaml + UserAcountListViewModel

役割: ページ内の「ユーザーアカウント一覧部分」だけを切り出したコンポーネント。
x:DataType="vm:UserAcountListViewModel" → BindingContext が UserAcountListViewModel 前提。
内部で CollectionView ItemsSource="{Binding Items}" を持ち、VMの Items をバインド。
ItemTemplate 内では、さらに1アイテムを表す UserAcountListItemViewModel を使う。
ここで 「リスト全体を表すVM」 → 「1件を表すVM」 へ分割。

UserAcountListView.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:vm="clr-namespace:MvvmMauiApp.ViewModels"
             xmlns:view="clr-namespace:MvvmMauiApp.Views"
             x:DataType="vm:UserAcountListViewModel"
             x:Class="MvvmMauiApp.Views.UserAcountListView">
    <VerticalStackLayout>
        <CollectionView.ItemsLayout>
            <LinearItemsLayout Orientation="Vertical" ItemSpacing="4"/>
        </CollectionView.ItemsLayout>
        <CollectionView ItemsSource="{Binding Items}">
            <CollectionView.ItemTemplate>
                <DataTemplate x:DataType="vm:UserAcountListItemViewModel">
                    <Frame Padding="10" Margin="5" BorderColor="Gray" CornerRadius="5">
                        <HorizontalStackLayout Spacing="10">
                            <Label Text="{Binding Data.UserId}" />
                            <Label Text="{Binding Data.AccountName}" />
                        </HorizontalStackLayout>
                    </Frame>
                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>
    </VerticalStackLayout>
</ContentView>
UserAcountListViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using System.Collections.ObjectModel;

namespace MvvmMauiApp.ViewModels
{
    public partial class UserAcountListViewModel : ObservableObject
    {
        [ObservableProperty]
        private ObservableCollection<UserAcountListItemViewModel> items;

        public UserAcountListViewModel()
        {
            this.Items = new ObservableCollection<UserAcountListItemViewModel>();
            foreach (var item in new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 })
            {
                this.Items.Add(new UserAcountListItemViewModel(item));
            }
        }
    }
}

UserAcountListItemViewModel.cs

1アイテム単位のViewModel。
UserId をキーとして UserAcount.GetData(UserId) でモデルを引き当てる。
Data.AccountName や Data.UserId を XAML から直接バインドできるようにしている。
1行分の表示ロジックを内包することで、データ取得や変換処理をカプセル化。

UserAcountListItemViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using MvvmMauiApp.Models;

namespace MvvmMauiApp.ViewModels
{
    public partial class UserAcountListItemViewModel : ObservableObject
    {
        public int UserId { get; set; }
        public UserAcount Data => UserAcount.GetData(this.UserId); 

        public UserAcountListItemViewModel(int userId)
        {
            this.UserId = userId;
        }
    }
}

モデル

UserAcount.cs
namespace MvvmMauiApp.Models
{
    public class UserAcount
    {
        public int UserId { get; set; }
        public string AccountName { get; set; } = string.Empty;
        public static UserAcount GetData(int userId)
        {
            return new UserAcount
            {
                UserId = userId,
                AccountName = $"Account {userId}"
            };
        }
    }
}

View + ViewModelのDI登録

下記の拡張メソッドでDI に View と ViewModel を登録。
View は ファクトリ登録(AddTransient<TView>(sp => ...))にしておき、解決時に ViewModel を作って BindingContext に注入。
Shell のルート登録は RegisterRoute("RouteName", typeof(TView))。このとき Shell は DI コンテナを使って View を解決するため、前述のファクトリが実行され BindingContext に VM が自動で入る。
つまり 遷移時にコードビハインドを一切触らず、GoToAsync("RouteName") だけで View ↔ VM の紐付けが完成。

ServiceCollectionExtensions.cs
namespace MvvmMauiApp.Core;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection RegisterForNavigation<TView, TViewModel>(
        this IServiceCollection services,
        string? route = null)
        where TView : Page
        where TViewModel : class
    {
        // ViewModel 登録
        services.AddTransient<TViewModel>();

        // View 登録 (DI解決時に BindingContext を設定)
        services.AddTransient<TView>(sp =>
        {
            var view = ActivatorUtilities.CreateInstance<TView>(sp);
            view.BindingContext = sp.GetRequiredService<TViewModel>();
            return view;
        });

        // Shell のルートに型で登録
        Routing.RegisterRoute(route ?? typeof(TView).Name, typeof(TView));

        return services;
    }
}

登録

MauiProgram.cs
		builder.Services.RegisterForNavigation<UserAcountListPage, UserAcountListPageViewModel>();