UWPのListViewで表形式UIを作ると、ItemTemplate内Gridの列幅をデータにバインドしたくなります。しかし行追加後に列幅が崩れたり、x:Bindで例外が出ることがあります。原因は意外とシンプルで、追加した新規アイテム側の列幅データが未初期化なだけです。
やりたいこと:ItemTemplate内のGrid列幅を「行データ」に持たせてバインドしたい
たとえば「家計簿」「請求書」「財務データ一覧」のようなUIでは、1行(1アイテム)に複数列を持ち、列幅を可変にしたくなる場面が多いです。
そこで、各行のデータモデル(例:FinancialItem)に列幅(GridLength)を格納し、ItemTemplate内の Grid.ColumnDefinitions の Width にバインドして列幅を制御します。
| 目的 | 実現したいUI | 代表的な持ち方 |
|---|---|---|
| 行ごとに列幅を持つ | 行によって列幅が違ってもよい | ObservableCollection<GridLength> Columns |
| 全行で列幅を共有する | 表として列がきれいに揃ってほしい | 親ViewModelに列幅を持ち、各行は参照する |
発生する症状:行追加後に列幅が崩れる/x:Bindで例外になる
「+」ボタンなどで新規行(新しいFinancialItem)を ObservableCollection に挿入したとき、次のような現象が起きがちです。
- 行自体は追加できる
- しかし追加した行だけ列幅が期待通りにならない(Autoになったり、妙に狭い/広い)
x:Bindを使うと例外が出る(追加した瞬間に落ちる、またはスクロールで落ちる){Binding}にしても列幅が反映されないように見える
ここで重要なのは、問題の本質はバインド方式(x:Bind / Binding)そのものではなく、「追加した新規アイテムの列幅データが空」である点です。
結論:Insertした新規アイテム側のColumnsが空のままになっている
ItemTemplate内で列幅を次のように参照しているとします。
Columns[0]Columns[1]
このとき、追加した新規アイテムが Columns を空のまま(Count=0)でListViewに表示されると、
x:Bindは生成コードでColumns[0]を実行し、インデックス範囲外で例外になりやすい{Binding}は評価に失敗して値が入らない(Unset)ため、列幅が既定値のままになったり、コンテナ再利用の影響で別の行の幅が残っているように見える
つまり「列幅が反映されない」のではなく、反映するべき値が新規アイテムに存在していないのが原因です。
最小構成で理解する:モデルとXAMLの典型例
データモデル例:FinancialItemがColumnsを持つ
行データに列幅を持たせる場合、概念としては次のような形になります。
using System.Collections.ObjectModel;
using Windows.UI.Xaml;
public class FinancialItem
{
public ObservableCollection<GridLength> Columns { get; }
= new ObservableCollection<GridLength>();
public string Name { get; set; }
public string Amount { get; set; }
}
XAML例:ItemTemplateのColumnDefinition.WidthをColumns[0]へバインド
ItemTemplate内で列幅を参照する例です(x:Bind版)。
<ListView ItemsSource="{x:Bind ViewModel.Items}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:FinancialItem">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{x:Bind Columns[0], Mode=OneWay}" />
<ColumnDefinition Width="{x:Bind Columns[1], Mode=OneWay}" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{x:Bind Name}" />
<TextBlock Grid.Column="1" Text="{x:Bind Amount}" />
<Button Content="+" HorizontalAlignment="Right"
Click="AddRow_Click" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
このXAMLは「各行が Columns を最低2要素持っている」ことが前提です。ここが崩れると、途端にトラブルが表面化します。
x:Bindだと例外、Bindingだと崩れるだけ…の理由
同じ Columns[0] 参照でも、x:BindとBindingでは失敗時の挙動がかなり異なります。これが「x:BindはダメでBindingならOKなのでは?」という誤解につながります。
| 観点 | x:Bind | Binding |
|---|---|---|
| 評価タイミング | 生成コードで高速評価(型付き) | ランタイムで動的評価 |
| インデックス参照(Columns[0]) | 実際にget_Item(0)が呼ばれやすい | 内部で評価失敗しても黙ってスキップされやすい |
| Columnsが空のとき | 例外になりやすい | 値が入らず、既定値のまま/古い値が残ることがある |
| デバッグしやすさ | 落ちるので原因に気づきやすい | 落ちないので原因が埋もれやすい |
つまり、x:Bindは「厳密に問題を表面化する」、Bindingは「問題を黙って飲み込む」だけで、根っこは同じです。
よくある追加処理の落とし穴:列幅を入れているのが“追加先”ではない
質問で非常に多いパターンがこれです。
「+」ボタンを押したとき、クリック元の行(既存アイテム)を取得し、そこを基準に新規アイテムを作ってInsertします。
ところが実装をよく見ると、列幅(Columns)を追加しているのが既存アイテム側であり、実際にInsertしている新規アイテム側にはColumnsを入れていない、というズレが起きます。
悪い例(考え方の例):fiにColumnsを追加してしまっている
private void AddRow_Click(object sender, RoutedEventArgs e)
{
var fe = (FrameworkElement)sender;
var fi = (FinancialItem)fe.DataContext; // クリック元の既存アイテム
var fiToAdd = new FinancialItem(); // 追加する新規アイテム
// ❌ 列幅を fi(既存)に入れてしまっている
fi.Columns.Add(new GridLength(120));
fi.Columns.Add(new GridLength(1, GridUnitType.Star));
// ✅ しかしInsertしているのは fiToAdd(新規)
ViewModel.Items.Insert(0, fiToAdd);
}
この状態だと、画面に追加された行(fiToAdd)は Columns が空のため、XAMLが参照する Columns[0] / Columns[1] が存在せず、
- x:Bind → 例外になりやすい
- Binding → 幅が入らず崩れる(または既定のまま)
となります。
解決策:Insertする新規アイテム fiToAdd のColumnsを事前に初期化する
対処はシンプルです。ItemTemplateが参照する分だけ、新規アイテム側にColumnsを確実に用意してからInsertします。
解決例:クリック元と同じ列幅をコピーして追加する
「追加する行も同じ列幅で始まってほしい」なら、クリック元のColumnsをコピーするのが自然です。
private void AddRow_Click(object sender, RoutedEventArgs e)
{
var fe = (FrameworkElement)sender;
var fi = (FinancialItem)fe.DataContext;
var fiToAdd = new FinancialItem
{
Name = "",
Amount = ""
};
// ✅ Insertする新規アイテム側へ列幅を用意する
// GridLengthは値型なので「値をコピー」するイメージでOK
fiToAdd.Columns.Add(fi.Columns.Count > 0 ? fi.Columns[0] : new GridLength(120));
fiToAdd.Columns.Add(fi.Columns.Count > 1 ? fi.Columns[1] : new GridLength(1, GridUnitType.Star));
var index = ViewModel.Items.IndexOf(fi);
ViewModel.Items.Insert(index + 1, fiToAdd);
}
ポイントは「Columnsが足りない状態を作らない」ことです。これだけで、x:Bindの例外も、Bindingの“反映されないように見える”問題も消えていきます。
解決例:FinancialItemのコンストラクタで必ず初期値を持たせる
より安全なのは、モデルの生成時点で必要要素数を確保してしまう方法です。これなら追加処理のたびにColumns初期化を書く必要が減り、事故も起きにくくなります。
public class FinancialItem
{
public ObservableCollection<GridLength> Columns { get; }
= new ObservableCollection<GridLength>
{
new GridLength(120), // 例:左列は固定幅
new GridLength(1, GridUnitType.Star) // 例:右列は伸縮
};
public string Name { get; set; }
public string Amount { get; set; }
}
この方式にすると、Insert時に単純に new FinancialItem() するだけで、ItemTemplate側の Columns[0] / Columns[1] が常に成立します。
さらに堅牢にするなら:Columns[0]のようなインデックス参照をやめる設計も有効
Columns[0] 方式は、可変列を扱える反面、要素不足に弱いのが難点です。UIは「必ず2列」と決まっているのに、データだけコレクションで持つと、初期化漏れで落ちやすくなります。
運用で事故を減らすなら、次のような設計に寄せると安定します。
| 設計 | メリット | デメリット | 向いているケース |
|---|---|---|---|
| Columns[0]方式(コレクション) | 列数を増減しやすい | 要素不足で落ちる/崩れる | 列数が動的、列を繰り返し生成する |
| Column0Width / Column1Width 方式(個別プロパティ) | 欠損が起きにくい、読みやすい | 列数が増えるとプロパティが増える | 列数が固定(2〜5列程度) |
| 親ViewModelに列幅を集約(全行共有) | 表として列が揃う、管理が一箇所 | 行ごとの差は出しづらい | 「表」UIで全行同じ列幅にしたい |
個別プロパティ化の例(事故が減る)
public class FinancialItem
{
public GridLength Column0Width { get; set; } = new GridLength(120);
public GridLength Column1Width { get; set; } = new GridLength(1, GridUnitType.Star);
public string Name { get; set; }
public string Amount { get; set; }
}
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{x:Bind Column0Width, Mode=OneWay}" />
<ColumnDefinition Width="{x:Bind Column1Width, Mode=OneWay}" />
</Grid.ColumnDefinitions>
この形なら、Columns[0] が存在しない問題そのものが消えます。「列数は固定」なのに「コレクションで持つ」ことで起きる事故を構造的に回避できます。
“列幅が勝手におかしくなる”に見える裏事情:ListViewのコンテナ再利用
Bindingに変えたのに列幅が変になる、追加行だけ幅が変、スクロールすると他の行まで崩れる……。こうした現象の一部は、ListViewが内部で行コンテナ(UI要素)を再利用していることでも説明できます。
- ListViewはパフォーマンスのため、表示外の行UIを作り直さず再利用することがあります
- そのとき、バインドが失敗してWidthが更新されないと、前に表示していた行のWidthが残ることがあります
- 結果として「列幅がランダムに見える」「追加した行だけ変」と感じやすくなります
ここでもやはり根本原因は「新規アイテム側にColumnsの値が無く、Widthが正しくセットされない」点です。コンテナ再利用は“症状を派手にする要因”になりやすい、と覚えておくと切り分けが早くなります。
チェックリスト:同じ問題を再発させないための確認ポイント
実装が増えてくると、どこでColumnsが空になったのか追いづらくなります。次の観点で確認すると、原因に最短で到達しやすいです。
| チェック項目 | 見るべきポイント | よくあるNG |
|---|---|---|
| 新規アイテム作成直後 | fiToAdd.Columns.Count が必要数あるか | Count=0のままInsert |
| クリック元と追加先の取り違え | 列幅を追加しているのは誰か | fi.Columns.Add してしまう |
| テンプレート側の参照数 | XAMLで Columns[1] まで参照していないか | 3列参照しているのに2つしか入れていない |
| 初期化の責務 | 追加処理で毎回初期化するか、モデルで必ず初期化するか | 場所が分散して漏れる |
| 将来の変更耐性 | 列数固定なら個別プロパティ化を検討 | 固定列なのにコレクションで持ち続ける |
実務でのおすすめ方針:まず「モデルで必ず初期化」→必要なら設計を変える
結局、運用で最も効くのは次の順番です。
- まずはFinancialItem生成時にColumnsを必ず必要本数で初期化して、欠損をゼロにする
- 列数が固定(2〜5列など)なら、Columns[0]参照をやめて個別プロパティ化する
- 表として列を揃えたいなら、列幅は親ViewModelに集約して全行で共有する
この方針にしておくと、x:BindでもBindingでも挙動が安定し、追加・削除・並び替えなどの操作が増えても崩れにくくなります。
まとめ:列幅バインドの問題は「新規行のColumns未初期化」がほぼ原因
UWPのListView ItemTemplateでGrid列幅を ObservableCollection<GridLength> にバインドする場合、行追加後に列幅が崩れたりx:Bindで例外になる主因は、Insertした新規アイテム側がColumnsを持っていない(空)ことです。
Insert前に新規アイテムのColumnsを必要本数で初期化する、あるいはモデルのコンストラクタで常に初期値を持たせるだけで、多くのケースは解決します。さらに堅牢さが必要なら、インデックス参照を避けた設計(個別プロパティ化/親で列幅管理)まで踏み込むのが近道です。

コメント