📚

「DataGridの行が取れない…」から学ぶWPFの基本概念

に公開

WPFの難しさを痛感した話

最近仕事でこのような改修を行いました。
「選択を保持しているデータとは別にダブルクリックしたら
 そのデータの詳細を開きたい」
最初自分はこう考えました。
「SelectedItemを使えばいけるだろう」と。
しかし現実はそんなに甘くなかった、、
「あれできない、というかすでにSelectedItemでやってるじゃん!?」となりました。
これがきっかけで「WPF の UI とデータは別物」という大事な概念を理解しました。

WPF の UI とデータは完全に別物!

SelectedItem = 選択されている「データ」(ViewModel)
VisualTree = 実際に操作された「UI要素」
この2つは一致しません。
だから SelectedItem を参照しても「どの UI がクリックされたか」は分からない。

SelectedItemとは

SelectedItemは「選択されているデータオブジェクト」であってUI(VisulTree)とは
無関係!!!

UI要素(DataGridRow / DataGridCell / TextBlock …)とは
関係ありません!
これだけ聞いてもわからないですよね😅

例:

var data = dataGrid.SelectedItem;

//取得できるのはデータ
RamenViewModel
{
    Num = "123",
    Model = "Ramen",
    Taste = "Shio"
}

これはデータ(モデル)であって、UIではない!
UI要素(DataGridRow, DataGridCell等)は取得できません。

SelectedItemの限界を知る

SelectedItemはあくまで"どのデータが選ばれているか"しか教えてくれない

つまりこの二つは別の概念

SelectedItem = データ
クリック位置 = UIの階層(VisualTree)

SelectedItem = ViewModel(データ、情報)
ということです。

解決方法:FindParentメソッド

今回の依頼を実現するためにFindParentメソッドを作成し、ダブルクリックの時にこのメソッド
を使用することで解決できました!!
クリックされた要素は TextBlock や Border なので
親を辿って DataGridRow を取得する必要がある

FindParentメソッド

"クリックされた要素の親をたどって、目的の型の親を探す"ためのメソッド

private T FindParent<T>(DependencyObject child) where T : DependencyObject
{
    if (child == null) return null;

    DependencyObject parent = VisualTreeHelper.GetParent(child);

    while (parent != null && !(parent is T))
    {
        parent = VisualTreeHelper.GetParent(parent);
    }

    return parent as T;
}

コードの説明

private T FindParent<T>(DependencyObject child) where T : DependencyObject

・T は探したい親の型(例:DataGridRow)

・child はクリックされた UI 要素(TextBlock など)

if (child == null) return null;

万が一nullが来たら終了

DependencyObject parent = VisualTreeHelper.GetParent(child);

・VisualTree の 1 つ上の親を取得
 例:TextBlock → Border → Grid → DataGridCell → DataGridRow…

DependencyObject

TextBlock, Border, Grid, Cell等からその上にある
特定の親(例:DataGridRow)を探す関数

while (parent != null && !(parent is T))
{
    parent = VisualTreeHelper.GetParent(parent);
}

while 文の役割:親を上へ上へと辿っていく
・parent が null → もう上に何もない → 終了
・parent is T → 探している型に到達 → 終了
このループでVisualTreeを逆方向に辿り続けます

return parent as T;

・最後に見つけた親を返す
 例:T = DataGridRow → DataGridRow を返す
・見つからなければ null を返す

これをダブルクリックイベントで使用する

private void listBox_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
    var original = e.OriginalSource as DependencyObject;

    // DataGridRow を上に向かって検索
    var row = FindParent<DataGridRow>(original);
    if (row == null) return; // 行以外をダブルクリックした場合

    // DataContext がその行のオブジェクト(RamenlViewModel)
    var clickedItem = row.DataContext as RamenlViewModel;
    if (clickedItem == null) return;

    // 正しくクリックした行(A/B問わず)が取れる
    DetailWindow subWindow = new DetailWindow(clickedItem.num);
    subWindow.Owner = Window.GetWindow(this);
    subWindow.VM = this.DataContext as ViewModelMainWindow;
    subWindow.ShowDialog();
}

こうすることで依頼内容を実現できました!!!!

なぜうまくいったのか

DataGrid の VisualTree はこうなっている!

DataGrid
 └─ DataGridRowsPresenter
     └─ DataGridRow
         └─ DataGridCellsPresenter
             └─ DataGridCell
                 └─ ContentPresenter
                     └─ TextBlock / Grid / Border など

という階層なので、
TextBlock → … → DataGridRow
まで遡る必要がある

「本当に触った行」を UI 上から取得できるため、
選択保持中でもダブルクリック行を正しく識別できる!
これを頭に入れておくことでWPFのUI操作処理の理解が深まります

TypeScriptに例えると…

DOMツリーで event.target.closest("tr") を使う感覚と同じ。
Webを触っていた人なら腑に落ちやすい!とChatGpt君が言っていました😊

学んだこと

UIを触ったつもりでも触っているのは実はデータとは別の存在!
・SelectedItem = データ
・VisualTree = UI
・DataContext = データとUIの橋渡し

自分にとってWPFは難しいですが頑張っていこうと思います!

最後に

自分にはまだまだWPFは難しいですが、今回の経験で少し前に進めた気がします。
改めて実務はすごく勉強になると感じました!!

Discussion