-1

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

EDIT: final solution has 1100% efficiency boost:

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

where row is DataGridRow I have in LoadingRow event, and bColumn is a known column I want to adorn

Boppity Bop
  • 9,613
  • 13
  • 72
  • 151
  • I'm confused, can you express in plain English what you want from us? You're saying that the `VisualTreeHelper` version is 75x faster than your code, but you don't want to use `VisualTreeHelper` and ..what? Want us to remake it from scratch for you or something? What is it that you want? – Blindy May 16 '22 at 18:42
  • Also "I have to hardcode it" -- no you don't, you just chose to do it that way. The task you have is to implement and/or fix something (the lord only knows how much that code needs fixing, from the little you've shown), and there are much better ways to do this kind of stuff in WPF. In fact, one word for you: templates. – Blindy May 16 '22 at 18:43
  • I dont want to walk tree as in `FindChild` method which is fairly common. I found faster way but it still uses visualtreehelper. I am asking - can you do faster? P.S. i like your input in C++ which I suck at. But in .NET I am comfortable and I know what I am doing and the lord defo agrees with me :) (it would take 2 x A4 pages to explain why.. – Boppity Bop May 16 '22 at 20:09
  • Would be interesting to know your goal. Do you actually need the cell content? – BionicCode May 16 '22 at 22:40
  • yes i am adorning it with a custom adorner. i have tons of text to render. it worked for 10 yrs. i am optimising it now cos i have time to do so.. – Boppity Bop May 16 '22 at 23:41

1 Answers1

2

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;
}
BionicCode
  • 1
  • 4
  • 28
  • 44
  • I optimised the #4 a little and now it is 30% faster than my previous fast solution. so overall 1100% faster than the original one. thank you – Boppity Bop May 18 '22 at 14:27
  • 1
    I'm happy I could help. 30% is a lot. What is equally important is the way the solution scales. The fact that it doesn't require any knowledge of the visual tree is a huge benefit. – BionicCode May 18 '22 at 15:05
  • no, actually without knowledge (as it is in your answer) it is only 8% faster. i added the knowledge and it became 30%.. scalability is not my concern at all. – Boppity Bop May 18 '22 at 15:12
  • 1
    P.S. I added final solution to the question so other ppl can see it – Boppity Bop May 18 '22 at 15:20
  • 1
    I see. You avoided the ItemContainerGenerator and the associated type cast of the returned container, as the container is provided to you as an event argument. Knowing the structure helps you to avoid the last iteration (the while loop in my example) where the condition fails (it avoids the evaluation of a condition in general). In case you modify the ControlTemplate of the DataGridCell, your version will potentially fail (in case the depth of the template tree changes. To pass the root element to the Adorner, casting to UIElement is sufficient. Casting to Grid will tighten the constraint. – BionicCode May 18 '22 at 15:49