WPF - 在没有 VisualTreeHelper 的情况下从 DataGridRow 走到 DataGridCell

WPF - walk from DataGridRow to DataGridCell without VisualTreeHelper

我在调试 WPF 应用程序 (.NET Framework 4.8) 时看到这张图片

我找出了树的一部分:

现在我需要从 DataGridCellsPresenter 走到 DataGridCell 数组。

我必须硬编码它——请不要告诉我我不会——这是手头的任务。我必须在不使用任何 VisualTreeHelpers

的情况下使用硬编码路径到达 _b

版本 1 - 使用传统的树行走:

child = UIHelper.FindChild<Grid>(row, "_b");

版本 2 - 硬编码树行走

var bx =  VisualTreeHelper.GetChild(row, 0) as Border;
var cp =  ((bx.Child as SelectiveScrollingGrid).Children[0] as DataGridCellsPresenter);
var ip =  VisualTreeHelper.GetChild(cp,  0) as ItemsPresenter;
var cpl = VisualTreeHelper.GetChild(ip,  0) as DataGridCellsPanel;
var c =   VisualTreeHelper.GetChild(cpl, 2) as DataGridCell;
var g =   VisualTreeHelper.GetChild(c,   0) as Grid;

1,000,000 次迭代版本 1 与版本 2(在我的机器上打勾)即版本 2 快 750%:

FindChild: 12,845,015
Hardcode: 1,706,232

你能提供一个更快的方法吗?

我不知道如何摆脱 GetChild - 许多方法和属性是受保护的或私有的。


查找孩子:

public static T FindChild<T>(DependencyObject parent, string childName) where T : DependencyObject
{
    int childrenCount = VisualTreeHelper.GetChildrenCount(parent);

    for (int i = 0; i < childrenCount; i++)
    {
        var child = VisualTreeHelper.GetChild(parent, i);

        T child_Test = child as T;

        if (child_Test == null)
        {
            var c = FindChild<T>(child, childName);

            if (c != null) return c;
        }
        else
        {
            FrameworkElement child_Element = child_Test as FrameworkElement;

            if (child_Element.Name == childName)
            {
                return child_Test;
            }

            var c = FindChild<T>(child, childName);

            if (c != null) return c;
        }
    }

    return null;
}

编辑: 最终解决方案具有 1100% 的效率提升:

DependencyObject cellContent = bColumn.GetCellContent(row);
child = VisualTreeHelper.GetParent(VisualTreeHelper.GetParent(cellContent)) as Grid;

其中rowDataGridRow我在LoadingRow事件中,bColumn是我要装饰的已知专栏

“慢”的不是 VisualTreeHelper。这是您遍历树以查找目标元素的方式。 “正确”使用 VisualTreeHelper 将显着改善搜索。

遍历树数据结构有不同的算法。您当前的版本实现了 Pre-order 遍历:它访问从根到叶的分支的每个节点。在最坏的情况下,目标是最后一个分支的叶子。此时,您已经访问了树的每个节点。

根据不同的场景,不同的算法或它们的组合可以表现得更好。

最好的情况是,您知道这棵树并且可以相信它的节点顺序是恒定的。
但一般来说,如果你不是树的建造者,就不能指望树的排列是一成不变的。
基于视觉树的性质,我们可以假设广度优先搜索比广泛传播 Pre-orde 搜索执行得更好,if 预期的树不是太宽并且目标节点不是叶子。
根据观察,我们可以假设视觉树倾向于在深度而不是宽度上增长。大多数容器都有一个 child。像 Grid 这样的容器通常没有很多列,即包含很多兄弟姐妹。

这意味着,基于此假设,Pre-order 搜索会取得最差的结果,因为它将逐个分支向下遍历完整的深度分支。
因此,在进入下一个级别之前检查节点的兄弟节点可能比先走完整个分支受到更少的伤害。
因此,在满足先前假设的情况下,广度优先遍历必须优于常见的 Pre-order 遍历。

如果访问的节点是 ItemsControl(可能包含大量兄弟姐妹),我们将使用 ItemContainerGenerator 获得 child 树。这样,在启用UI虚拟化的场景下,我们也可以保证访问每一个容器。此场景没有针对性,只需要将感兴趣的项目带入视图(以触发容器实现)。

下面是比普通 Pre-order 搜索(在此给定场景中)表现更好的算法的四个示例。遍历分为两步以避免不必要的分支遍历:首先找到DataGridCell宿主(一个ItemsControl),然后使用ItemContainerGenerator找到根元素。使用 ItemContainerGenerator 将进一步提高搜索性能。

我没有测过他们的效率,但是根据检查,我给了他们一个排名。
更高的数字意味着更好的性能。 23 可能会调换位置。所有示例都尝试找到第一行的第一个单元格:

1 (Pre-order)

常见的Pre-order遍历
这种情况需要了解模板(元素名称)。

int rowIndexToVisit = 0;
int columnIndexToVisit = 0;

DependencyObject rowItemContainer = dataGrid.ItemContainerGenerator.ContainerFromIndex(rowIndexToVisit);
if (TryFindVisualChildElementByName(rowItemContainer, "_b", out Border border))
{  
  DependencyObject cellVisualRoot = border;
}
public static bool TryFindVisualChildElementByName<TChild>(
  DependencyObject parent,
  string childElementName,
  out TChild resultElement) where TChild : FrameworkElement
{
  resultElement = null;

  if (parent is Popup popup)
  {
    parent = popup.Child;
    if (parent == null)
    {
      return false;
    }
  }

  for (var childIndex = 0; childIndex < VisualTreeHelper.GetChildrenCount(parent); childIndex++)
  {
    DependencyObject childElement = VisualTreeHelper.GetChild(parent, childIndex);

    if (childElement is TChild frameworkElement)
    {
      if (string.IsNullOrWhiteSpace(childElementName)
            || frameworkElement.Name.Equals(childElementName, StringComparison.Ordinal))
      {
        resultElement = frameworkElement;
        return true;
      }
    }

    if (TryFindVisualChildElementByName(childElement, childElementName, out resultElement))
    {
      return true;
    }
  }

  return false;
}

2(广度优先/w ItemContainerGenerator

需要了解 DataGrid 类型层次结构(特别是 DataGridCellsPresenterDataGridCell 项目宿主)。

int rowIndexToVisit = 0;
int columnIndexToVisit = 0;

DependencyObject rowItemContainer = dataGrid.ItemContainerGenerator.ContainerFromIndex(rowIndexToVisit);
if (TryFindVisualChildElementBreadthFirst(rowItemContainer, out DataGridCellsPresenter dataGridCellsPresenter))
{
  var cellItemContainer = dataGridCellsPresenter.ItemContainerGenerator.ContainerFromIndex(columnIndexToVisit) as DataGridCell;
  DependencyObject cellVisualRoot = VisualTreeHelper.GetChild(cellItemContainer, 0);
}
public static bool TryFindVisualChildElementBreadthFirst<TChild>(
  DependencyObject parent,
  string name,
  out TChild resultElement) where TChild : FrameworkElement
{
  resultElement = null;

  if (parent is Popup popup)
  {
    parent = popup.Child;
    if (parent == null)
    {
      return false;
    }
  }

  var pendingSubtree = new Queue<DependencyObject>();
  for (var childIndex = 0; childIndex < VisualTreeHelper.GetChildrenCount(parent); childIndex++)
  {
    DependencyObject childElement = VisualTreeHelper.GetChild(parent, childIndex);
    if (childElement is TChild frameworkElement)
    {
      resultElement = frameworkElement;
      return true;
    }

    pendingSubtree.Enqueue(childElement);
  }

  while (pendingSubtree.TryDequeue(out DependencyObject subtreeRoot))
  {
    if (TryFindVisualChildElementBreadthFirst(subtreeRoot, name, out resultElement))
    {
      return true;
    }
  }

  return false;
}

3(手动逻辑树)

需要确切了解 ControlTemplate:

int rowIndexToVisit = 0;
int columnIndexToVisit = 0;

DependencyObject rowItemContainer = dataGrid.ItemContainerGenerator.ContainerFromIndex(rowIndexToVisit);
var rowItemContainerTemplate = rowItemContainer.Template as ControlTemplate;
var templateRootBorder = rowItemContainerTemplate.FindName("DGR_Border", rowItemcontainer) as Border;
var selectiveScrollingGrid = templateRootBorder.Child as Panel;
var cellsPresenter = selectiveScrollingGrid.Children.OfType<DataGridCellsPresenter>().First();
var cellItemContainer = cellsPresenter.ItemContainerGenerator.ContainerFromIndex(columnIndexToVisit) as DataGridCell;

DependencyObject cellVisualRoot = VisualTreeHelper.GetChild(cellItemContainer, 0);

4(直接访问数据网格列)

应该是最快的,而且不需要任何内部知识。

int rowIndexToVisit = 0;
int columnIndexToVisit = 0;

DependencyObject rowItemContainer = dataGrid.ItemContainerGenerator.ContainerFromIndex(rowIndexToVisit);
DataGridColumn column = dataGrid.Columns[columnIndexToVisit];
DependencyObject cellContent = column.GetCellContent(rowItemContainer);
DependencyObject cellVisualRoot = cellContent;
while ((cellContent = VisualTreeHelper.GetParent(cellContent)) is not DataGridCell)
{
  cellVisualRoot = cellContent;
}