MAUI 一覧画面のBinding

下記の例は「ページ + ページ用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 として注入している構造。
<?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>
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」 へ分割。
<?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>
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行分の表示ロジックを内包することで、データ取得や変換処理をカプセル化。
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;
}
}
}
モデル
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 の紐付けが完成。
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;
}
}
登録
builder.Services.RegisterForNavigation<UserAcountListPage, UserAcountListPageViewModel>();