WPFのMVVMで非同期に一覧データを取得したいとき、Task<List<Person>>をそのままItemsSourceへバインドしたくなる場面があります。しかしItemsSourceが受け取れるのはコレクションであり、Taskはそのまま表示できません。本記事では理由と、実務で崩れにくい非同期ロードの実装を具体例付きで解説します。
結論:Task<List<Person>>はItemsSourceに直接バインドできない
WPFのItemsControl(ListBox、ListView、DataGridなど)のItemsSourceは、基本的にIEnumerable(列挙可能なコレクション)を期待します。ところがTask<T>は「将来Tが手に入る」ことを表す非同期処理であり、列挙可能なコレクションではありません。
そのため、次のようにTask型プロパティを公開しても、WPFは中身のList<Person>を自動で待ってくれません。
public Task<List<Person>> PersonsTask => GetPersonsAsync();
さらに厄介なのは、データバインディングは「必要に応じてプロパティを何度も評価する」ことがある点です。上のようなゲッター実装だと、画面の更新や再評価のタイミングでGetPersonsAsync()が何度も呼ばれ、意図せず複数回ロードが走る可能性があります(DB/APIに余計な負荷がかかる、順不同で結果が入れ替わる、など)。
まず押さえる:WPFのバインディングはTaskをawaitしない
「XAMLでawaitできればいいのに」と思うのは自然ですが、WPFの標準バインディングはTaskの完了を待ちません。よく混同されるのがBinding.IsAsyncです。
- Binding.IsAsync:値の取得自体を別スレッドで行い、UIのブロックを避けるための仕組み
- Task/await:非同期処理の完了を待って結果を得るための言語機能
IsAsyncはTaskを自動的にawaitしてくれるわけではないため、ItemsSourceにTaskを渡しても期待通りには動きません。
「Taskをそのままバインドしたい」気持ちの背景
MVVMでは「Viewはデータにバインドするだけ」「ViewModelが状態と処理を持つ」という整理がしやすいので、非同期取得も「プロパティとして公開してXAML側で完結させたい」と考えがちです。特に、次のような要求があるとTaskバインドに惹かれます。
- 画面表示と同時に自動で読み込みたい
- 読み込み中・完了・失敗をXAMLだけで表現したい
- コードビハインドを極力ゼロにしたい
しかしWPF標準の範囲では、Taskを直接ItemsSourceに流し込む構造は無理が出やすいため、ViewModel側で「結果コレクション」と「ロード状態」を分けて持つのが安定します。
回避策:IValueConverterでTask→Listに変換は“形としては”可能だが危険
コンバーターでTaskをList<Person>に変換してItemsSourceへ流す、という発想はよく出ます。例えば以下のようなコードです。
public sealed class TaskToResultConverter : IValueConverter
{
public object? Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is Task<List<Person>> task)
{
// ❌ 非推奨:UIスレッドをブロックしやすい / デッドロックの原因になり得る
return task.Result;
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> Binding.DoNothing;
}
この方法が危険な理由は次の通りです。
- UIが固まる:非同期のはずが、.Resultで同期的に待つため、完了まで画面が止まります
- デッドロックの可能性:タスクがUIコンテキストに戻ろうとしているのにUIが.Resultで待っていると詰まります
- 例外処理が辛い:Taskの例外がAggregateExceptionで表面化しやすく、表示やログ設計が崩れがちです
- コンバーターはasyncにできない:本当にやりたい「awaitして返す」が標準のIValueConverterではできません
結果として「一見動くが、遅い環境や本番負荷で壊れる」形になりやすく、長期運用には向きません。
推奨パターン:ObservableCollectionを公開し、非同期結果を投入する
WPF MVVMで最も扱いやすいのは、ItemsSourceには常にObservableCollection<Person>をバインドし、ロードの完了後に中身を更新する方式です。
| 方式 | ItemsSourceに渡すもの | 更新通知 | 実務での安定度 |
|---|---|---|---|
| Task直バインド | Task<List<Person>> | そもそも表示できない | 低い |
| List差し替え | List<Person> | PropertyChangedが必要 | 中(シンプルな画面なら可) |
| ObservableCollection投入 | ObservableCollection<Person> | CollectionChangedで自動反映 | 高い(定番) |
この方式のメリットは、XAMLは常に同じコレクション(参照)にバインドし続けられるため、UI側の差し替えコストやバインディングの揺れが小さいことです。さらに、追加・削除が自動反映されるので、ページングや部分更新にも発展させやすくなります。
サンプル:Personモデル
public sealed class Person
{
public int Id { get; init; }
public string Name { get; init; } = "";
public int Age { get; init; }
}
サンプル:サービス層(非同期取得)
データ取得はViewModelに直書きせず、サービス(リポジトリ)に切り出すとテストもしやすく、MVVMらしく整理できます。
public interface IPersonService
{
Task<IReadOnlyList<Person>> GetPersonsAsync(CancellationToken cancellationToken);
}
サンプル:ViewModel(ObservableCollectionへ反映)
以下は「読み込み」「キャンセル」「エラー表示」「読み込み中フラグ」まで含めた、実務でよく使う形です。コミュニティで定番のCommunityToolkit.Mvvmを例にします(自前でINotifyPropertyChanged実装でも考え方は同じです)。
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Collections.ObjectModel;
using System.Threading;
public partial class PersonsViewModel : ObservableObject
{
private readonly IPersonService _service;
private CancellationTokenSource? _cts;
public ObservableCollection<Person> Persons { get; } = new();
[ObservableProperty]
private bool isLoading;
[ObservableProperty]
private string? errorMessage;
public IAsyncRelayCommand LoadCommand { get; }
public IRelayCommand CancelCommand { get; }
public PersonsViewModel(IPersonService service)
{
_service = service;
LoadCommand = new AsyncRelayCommand(LoadAsync);
CancelCommand = new RelayCommand(Cancel, () => IsLoading);
}
partial void OnIsLoadingChanged(bool value)
{
CancelCommand.NotifyCanExecuteChanged();
LoadCommand.NotifyCanExecuteChanged();
}
private async Task LoadAsync()
{
ErrorMessage = null;
// 連続実行を防ぎたい場合は、ここでIsLoadingを見てreturnするなどもあり
IsLoading = true;
_cts?.Cancel();
_cts = new CancellationTokenSource();
try
{
var list = await _service.GetPersonsAsync(_cts.Token);
Persons.Clear();
foreach (var p in list)
{
Persons.Add(p);
}
}
catch (OperationCanceledException)
{
// キャンセルはエラー扱いにしないのが一般的
}
catch (Exception ex)
{
// 実務ではログ基盤に送る/ユーザー向け文言に変換する
ErrorMessage = ex.Message;
}
finally
{
IsLoading = false;
}
}
private void Cancel()
{
_cts?.Cancel();
}
}
サンプル:XAML(ItemsSourceはPersonsにバインド)
XAML側はシンプルに「表示対象は常にPersons」とし、読み込みはコマンドで行います。
<Window x:Class="Sample.PersonsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Persons" Height="420" Width="640">
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BoolToVis" />
</Window.Resources>
<Grid Margin="12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Margin="0,0,0,8">
<Button Content="読み込み" Command="{Binding LoadCommand}" />
<Button Content="キャンセル" Command="{Binding CancelCommand}" Margin="8,0,0,0" />
<TextBlock Text="{Binding ErrorMessage}"
Margin="12,0,0,0"
VerticalAlignment="Center"
Foreground="Red" />
</StackPanel>
<ListView Grid.Row="1" ItemsSource="{Binding Persons}">
<ListView.View>
<GridView>
<GridViewColumn Header="ID" DisplayMemberBinding="{Binding Id}" Width="80" />
<GridViewColumn Header="氏名" DisplayMemberBinding="{Binding Name}" Width="240" />
<GridViewColumn Header="年齢" DisplayMemberBinding="{Binding Age}" Width="80" />
</GridView>
</ListView.View>
</ListView>
<ProgressBar Grid.Row="2"
Height="6"
IsIndeterminate="True"
Margin="0,8,0,0"
Visibility="{Binding IsLoading, Converter={StaticResource BoolToVis}}" />
</Grid>
</Window>
この形にしておくと、ItemsSourceには常にコレクションが入るため、非同期のタイミングに左右されず安定して表示できます。また読み込み状態(IsLoading)を別プロパティにしているので、プログレス表示やボタンの有効/無効制御も自然に書けます。
Listで差し替えるだけでも動くが、更新戦略に注意
「一覧は毎回丸ごと差し替えで十分」「追加・削除はしない」という画面なら、ObservableCollectionではなくList<Person>を差し替える形でも問題ありません。ただしその場合は、プロパティ自体を差し替え、必ずPropertyChangedを通知する必要があります。
private List<Person> _persons = new();
public List<Person> Persons
{
get => _persons;
private set => SetProperty(ref _persons, value);
}
private async Task LoadAsync()
{
var list = await _service.GetPersonsAsync(CancellationToken.None);
Persons = list.ToList(); // 参照を差し替える
}
ただしList差し替えは次のような場面で拡張しにくいことがあります。
- ロード途中で1件ずつ追加して進捗を見せたい
- 差分更新(変更のあった要素だけ差し替え)をしたい
- フィルタ・ソートをかけたビューを維持したい
将来的に機能が増えそうなら、最初からObservableCollectionにしておくほうが手戻りが少ないです。
MVVMらしくする:Loadedイベントに頼りすぎないロードの起点
「画面が表示されたら自動でロードしたい」場合、ViewのLoadedイベントからViewModelのメソッドを呼ぶ例を見かけます。これは短期的には簡単ですが、画面遷移や再表示で複数回呼ばれる・テストしにくい、といった問題が起きがちです。MVVMで整理するなら、ロードもコマンドとして公開し、呼び出し契機だけView側で用意するのが安全です。
| 起点 | メリット | 注意点 |
|---|---|---|
| ボタン(明示的) | 動作が分かりやすい/二重ロードしにくい | 初期表示で空になるのでUXは工夫が必要 |
| 画面表示時に自動(Loaded等) | ユーザー操作なしでデータが出る | 再表示で複数回ロードになりやすい |
| ナビゲーション(遷移時) | 「画面に入る」を契機にできる | フレームワーク依存(Prism等) |
自動ロードしたい場合の現実的な落としどころ
フレームワークを使っていない場合は「ViewのコンストラクタでDataContextを設定した直後にLoadCommand.Execute(null)」のような最小限のコードビハインドを許容するのが、結果として一番トラブルが少ないことも多いです。重要なのは、ロード処理そのものはViewModelに閉じ、Viewは「呼ぶだけ」にすることです。
落とし穴集:非同期ロードが不安定になる典型パターンと対策
Taskを直接バインドできない問題を回避しても、非同期ロードは実務でつまずきやすいポイントがいくつもあります。ここを先に押さえておくと、後からの不具合調査が激減します。
| よくある問題 | 症状 | 対策 |
|---|---|---|
| Task.Result / Wait() を使う | 画面が固まる/時々デッドロック | 必ずawaitで待つ。UIスレッドをブロックしない |
| プロパティゲッターでGetPersonsAsync()を呼ぶ | 複数回ロード/結果が入れ替わる | コマンドや明示的なメソッドで一度だけ呼ぶ |
| 例外を握りつぶす | 空表示のまま原因不明 | ErrorMessage/ログ出力を用意し、ユーザーにフィードバック |
| キャンセルがない | 画面遷移しても裏で通信が続く | CancellationTokenを流し、画面破棄時にCancel |
| ConfigureAwait(false)の扱いミス | コレクション更新で例外(別スレッド) | UI更新はUIスレッドで。サービス層のみConfigureAwait(false)を検討 |
UIスレッドとDispatcherの考え方
WPFのObservableCollectionは、基本的にUIスレッドから更新する前提です。通常、ViewModelのawaitはUIコンテキストに戻るため、次のような形ならDispatcher不要で更新できます。
var list = await _service.GetPersonsAsync(ct); // 通常ここでUIに戻る
Persons.Clear();
foreach (var p in list) Persons.Add(p);
一方で、サービス層やライブラリ側でConfigureAwait(false)を多用していたり、明示的にバックグラウンドスレッドで処理している場合は、UIスレッドに戻らないことがあります。そのときはViewModel側でDispatcherを使って更新します。
var list = await _service.GetPersonsAsync(ct).ConfigureAwait(false);
// ここがUIスレッドでない可能性があるのでDispatcherへ
await Application.Current.Dispatcher.InvokeAsync(() =>
{
Persons.Clear();
foreach (var p in list) Persons.Add(p);
});
ポイントは「UI更新はUIスレッド」という原則を守ることです。逆に言えば、データ取得(I/O)部分はUIと切り離して最適化しやすくなります。
読み込み中表示を“それっぽく”する:UXを上げる小技
非同期ロードの実装は「動く」だけでなく、「待たされている感」を減らす工夫が効きます。以下はWPFで手堅いパターンです。
- 読み込み中はボタンを無効化する(AsyncRelayCommandは標準でやりやすい)
- 空データ時は「データがありません」を表示する
- エラー時は一覧ではなくメッセージを優先して表示する
例えば、ItemsControlの上に読み込み中オーバーレイを重ねると、画面が切り替わらずユーザーが安心します。
<Grid>
<ListView ItemsSource="{Binding Persons}" />
<Border Background="#80FFFFFF"
Visibility="{Binding IsLoading, Converter={StaticResource BoolToVis}}">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<ProgressBar IsIndeterminate="True" Width="240" Height="6" />
<TextBlock Text="読み込み中..." Margin="0,8,0,0" HorizontalAlignment="Center" />
</StackPanel>
</Border>
</Grid>
こうした表示は見た目の問題に見えますが、実際は「連打」「二重ロード」「閉じたのに裏で動いている」などの二次被害も抑えやすくなります。
大量データの場合:仮想化と段階的ロードもセットで考える
Person一覧が数千〜数万件になる可能性があるなら、非同期ロードと同時にUI側の負荷も意識しましょう。代表的には以下です。
- UI仮想化:画面に見えている行だけ生成する
- 段階的ロード:最初は先頭だけ、スクロールやページングで追加取得する
例えばListViewでは、仮想化が無効になる条件(ScrollViewer.CanContentScrollやレイアウトの組み方など)があるため、意図せず重くなることがあります。まずは仮想化が有効になっているか確認し、必要に応じて設定します。
<ListView ItemsSource="{Binding Persons}"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling"
ScrollViewer.CanContentScroll="True" />
段階的ロードは設計が一段上がりますが、ObservableCollection方式なら「追加で取得した分をAddする」だけでUIに自然に反映できるため、Task直バインドより発展させやすいのが利点です。
どうしてもTaskを“状態込みで”バインドしたい場合の現実解
「読み込み中かどうか」「例外が出たか」「結果が揃ったか」をXAMLで扱いたい場合、TaskそのものをItemsSourceに流すのではなく、Taskを監視するラッパーを用意し、Resultや状態プロパティをバインドする方法があります。
これは“Taskを直接バインド”ではありませんが、「Taskの完了を待ってResultを通知する」点で要求に近く、コンバーターより安全です。
Task監視ラッパー例(NotifyTaskCompletion風)
using System.ComponentModel;
public sealed class TaskNotifier : INotifyPropertyChanged
{
public Task Task { get; }
public bool IsCompleted => Task.IsCompleted;
public bool IsSuccessfullyCompleted => Task.Status == TaskStatus.RanToCompletion;
public bool IsFaulted => Task.IsFaulted;
public Exception? Exception => Task.Exception;
public T? Result => IsSuccessfullyCompleted ? Task.Result : default;
public event PropertyChangedEventHandler? PropertyChanged;
public TaskNotifier(Task<T> task)
{
Task = task;
_ = WatchAsync();
}
private async Task WatchAsync()
{
try { await Task.ConfigureAwait(false); }
catch { /* 例外は状態プロパティで扱う */ }
OnPropertyChanged(nameof(IsCompleted));
OnPropertyChanged(nameof(IsSuccessfullyCompleted));
OnPropertyChanged(nameof(IsFaulted));
OnPropertyChanged(nameof(Exception));
OnPropertyChanged(nameof(Result));
}
private void OnPropertyChanged(string name)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
ViewModel側:TaskNotifierのResultをItemsSourceへ
public TaskNotifier<IReadOnlyList<Person>> PersonsTask { get; }
public PersonsViewModel(IPersonService service)
{
PersonsTask = new TaskNotifier>(
service.GetPersonsAsync(CancellationToken.None));
}
<ListView ItemsSource="{Binding PersonsTask.Result}" />
この方法の注意点は、結果が到着した後にResultの変更通知が出る設計になっているか、そして再読み込み時にTaskNotifier自体を差し替えるならTaskNotifierプロパティにも通知が必要、という点です。シンプルな一覧ならObservableCollection方式のほうが実装が読みやすく、デバッグもしやすいことが多いです。
よくある質問と判断基準
Taskプロパティを公開するのは完全にダメ?
ダメではありません。例えば「ロード状態を表すためにTaskを保持する」「画面遷移時にキャンセルしたいので現在のTaskを追跡する」など、ViewModel内部の管理用途としては有効です。ただし、ItemsSourceに直接渡して表示させようとすると設計が歪みやすい、というのがポイントです。
GetPersonsAsync()をプロパティのゲッターで呼ぶのが危ないのはなぜ?
WPFのバインディングは、再描画・レイアウト・トリガーなどの都合でプロパティを複数回読むことがあります。ゲッターが副作用(通信、DBアクセス)を持つと、読み取りのたびに処理が走ってしまいます。非同期かどうかに関係なく、プロパティのゲッターは副作用なしを基本にすると安全です。
DataGridでも同じ?
同じです。DataGrid.ItemsSourceも列挙可能なコレクションを期待するので、Taskを直接渡すのは不可です。ObservableCollectionやListをバインドし、非同期取得後に中身(または参照)を更新する設計にします。
最小実装はどれ?
まず動かすだけなら、次の組み合わせが最小で安定します。
- ViewModelに
ObservableCollection<Person>を1つ用意 LoadAsyncでawaitしてからClear→Add- XAMLは
ItemsSource="{Binding Persons}"だけ
そこにIsLoadingとErrorMessageを足すと、実務でも困りにくい完成度になります。
まとめ:ItemsSourceは「結果のコレクション」に、Taskは「取得処理」に分離する
WPF MVVMで非同期データ読み込みを扱うコツは、UIが欲しいのはTaskではなく“表示できるコレクション”だと割り切ることです。Task<List<Person>>を直接ItemsSourceへバインドするのではなく、
ItemsSourceにはObservableCollection<Person>(またはList)をバインド- 読み込みは
awaitで実行し、完了後にコレクションへ反映 - 読み込み中・エラー・キャンセルなど、状態を別プロパティで管理
という分離を徹底すると、UIが固まらず、再評価で二重ロードも起きにくい、保守しやすいMVVMになります。

コメント