「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