UWP ListView ItemTemplateでGrid列幅をObservableCollectionにバインドすると崩れる・x:Bind例外の原因と対処法

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:BindBinding
評価タイミング生成コードで高速評価(型付き)ランタイムで動的評価
インデックス参照(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を必要本数で初期化する、あるいはモデルのコンストラクタで常に初期値を持たせるだけで、多くのケースは解決します。さらに堅牢さが必要なら、インデックス参照を避けた設計(個別プロパティ化/親で列幅管理)まで踏み込むのが近道です。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次