Home > Back-end >  WPF - walk from DataGridRow to DataGridCell without VisualTreeHelper
WPF - walk from DataGridRow to DataGridCell without VisualTreeHelper

Time:05-18

I see this picture when I debug my WPF app (.NET Framework 4.8)

I figured out part of the tree:

Now I need to walk down from DataGridCellsPresenter to DataGridCell array.

I have to hardcode it - please refrain from telling me that I dont - its the task at hand. I must get to _b with hardcoded path without using any VisualTreeHelpers etc.

enter image description here

version 1 - using conventional tree walking:

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

version 2 - hardcoded tree walking

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 iterations version 1 vs 2 (ticks on my machine) i.e. version 2 is 750% faster:

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

Can you offer a faster method?

I cant figure how to get rid off of GetChild - lots of methods and properties are protected or private.


FindChild:

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;
}

CodePudding user response:

It's not the VisualTreeHelper that is "slow". It's the way you traverse the tree to find the target element. Using the VisualTreeHelper "properly" will improve the search significantly.

There are different algorithms to traverse a tree data structure. Your current version implements Pre-order traversal: it visits every node of a branch from root to the leaf. In the worst case, the target is the leaf of the last branch. At this point, you have already visited every node of the tree.

Depending on the scenario a different algorithms or combinations of them can perform better.

The best scenario is, where you know the tree and can rely on its node order to be constant.
But in general, if you are not the builder of the tree, you can't rely on the tree arrangement to be constant.
Based on the nature of the visual tree, we can assume that a Breadth First search performs much better than the wide spread Pre-orde search, if the expected tree is not too wide and the target node is not a leaf.
Based on observation, we can assume that the visual tree tends to grow in depth rather than in width. Most containers have a single child. Containers like the Grid usually don't have many columns i.e. contain many siblings.

This means, that based on this assumptions, the Pre-order search achieves the worst results as it will walk down the complete depth branch by branch.
Thus, inspecting the siblings of a node before entering the next level will potentially hurt less than going down the complete branch first.
A Breadth First traversal must therefore outperform the common Pre-order traversal in a scenario where the previous assumptions are met.

In case of visiting a node that is an ItemsControl (containing a potentially high number of siblings), we obtain the child tree by using the ItemContainerGenerator. This way, we can also ensure to visit every container, in a scenario where UI virtualization is enabled. This scenario is not targeted and would only require to bring the item of interest into view (to trigger container realization).

Below are a four examples of algorithms that perform better than the common Pre-order search (in this given scenario). Traversal is split into two steps to avoid unnecessary branch traversal: find the DataGridCell host (an ItemsControl) first and then use the ItemContainerGenerator to find the root element. Using the ItemContainerGenerator will further improve the search performance.

I have not measured their efficiency, but based on examination, I gave them a ranking.
A higher number means a better performance. 2 and 3 might switch positions. All examples try to find the first cell of the first row:

1 (Pre-order)

The common Pre-order traversal.
This case requires knowledge of the template (element name).

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 (Breadth First /w ItemContainerGenerator)

Requires knowledge of the DataGrid type hierarchy (in particular that DataGridCellsPresenter is the DataGridCell items host).

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 (manual logical tree)

Requires exact knowledge of the 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 (access data grid columns directly)

Should be the fastest and in addition doesn't require any knowledge of the internals.

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;
}
  • Related