DataGrid の SelectedItem にバインドしていると、ItemsSource 再設定時に落ちる

ちょっとばかし、ハマったのでメモ書きをしておく。

WPFのDataGridにデータバインドする

WPFでリスト表示をするときに、ListViewかDataGridを使うと思うのだが、ここで MVVM を使って ItemsSource にバインドする。

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="50"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <DataGrid Grid.Row="1" x:Name="lv"
                ItemsSource="{Binding Items}"
                SelectedItem="{Binding Item}"
                SelectedIndex="{Binding ItemIndex}"      >
    </DataGrid>

ViewModel を作って

public class ViewModel : ObservableObject
{
private List<商品> _items;
public List<商品> Items
{
    get { return _items; }
    set { SetProperty(ref _items, value, nameof(Items)); }
}

private 商品 _item;
public 商品 Item
{
    get { return _item; }
    set { SetProperty(ref _item, value, nameof(Item)); }
}
private int _itemIndex;
public int ItemIndex 
{
        get { return _itemIndex; }
        set { SetProperty(ref _itemIndex, value, nameof(ItemIndex)); }
    }
}

ロード時に表示させる。

private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
    _vm = new ViewModel();
    var ent = new Database1Entities();
    _vm.Items = ent.商品.ToList();
    this.DataContext = _vm;
}
ViewModel _vm;

実行すると、こんな感じ。

行を選択するとバインドしている SelectedItem にオブジェクト(ここでは商品オブジェクト)が入るので、詳細データとかを別画面で表示するには便利だったりする。
SelectedIndex でもいいような気もするのだが、実は、DataGrid はヘッダをクリックするとソートする機能が初めから入っていて、結構便利。でもって、ソートしたときはインデックスが変わってしまうので、SelectedIndex では困るので、SelectedItem から選択したオブジェクトを拾うと良いという訳。渡している List<商品> と DataGird 自身が持っている Items の中身が異なる(ソートされている)のでずれがでてくる。

リストをリフレッシュすると落ちる

さて、普段は DataGrid の中身は変わらないので更新することはないのだが、なんらかの理由でリストを再描画させたいとしよう。このときに、ItemsSource にバインドしている ViewModel 側の Items を更新すればよいと思って、次な感じにすると、

private void clickInit(object sender, RoutedEventArgs e)
{
    var ent = new Database1Entities();
    _vm.Items = ent.商品.ToList();
}

~~~
System.NullReferenceException が発生しました
HResult=0x80004003
Message=オブジェクト参照がオブジェクト インスタンスに設定されていません。
~~~

なる例外が発生して落ちる。それも、_vm.Items に代入しようとしているところで落ちるので始末が負えない。
実は、初期表示をしていて、カーソルを DataGird にあてない状態(選択行が無い状態)の場合は落ちなくて、一度選択した後には例外が発生して落ちるのである。
かなり不思議な現象である。ネットでもあちこち困っている人がいるもの、ぴっちりとした解決策は無いように見える、が、

実は、このように Item に null を代入すると落ちなくなる。

private void clickReload(object sender, RoutedEventArgs e)
{
    var ent = new Database1Entities();
    _vm.Item = null;	// ★
    _vm.Items = ent.商品.ToList();
}

そう、ViewModel を見るとわかるのだが、DataGrid の SelectedItem へのバインドが悪さをしている。Items に新しいリストを設定して、ItemsSource プロパティを更新してしまうと、SelectedItem が行先を見失う(?)ことになって NULL例外が発生するらしい。このため、先に SelectedItem に null を設定してやって、カーソルを外した後で ItemsSource に新しいリストを設定することになる。

おまけ

じゃあ、ObservableCollection を使って、一度クリアしたあとに1つずつ追加しけばいいじゃないか、と思うかもしれないが、実はそれでも落ちる。

private void clickReload(object sender, RoutedEventArgs e)
{
    var ent = new Database1Entities();
    _vm.Item = null;	// ★
    _vm.Items.Clear();
    foreach (var it in ent.商品) _vm.Items.Add(it);
}

普通の Items の更新と同じように、★部分の null の設定がないと、_vm.Items.Clear() 行で例外が発生して落ちてしまう。これも、SelectedItem が示し先を見失ってしまうためらしい。

カテゴリー: 開発, C#, XAML パーマリンク