dotnet / dotnet/wpf

ItemsControl automation: all child DataItem's are empty, except first DataItem

Open
#9,328 3 comments 0 reactions 0 assignees View on GitHub
:mailbox_with_no_mail: waiting-author-feedback
Dominant language
C#
Stars
7.7k
Forks
1.3k
Avg merge
1d 11h
Merged PRs (30d)
61

Description

### Description

In net8 `ItemsControl` support automation (`ItemsControlWrapperAutomationPeer` / `ItemsControlItemAutomationPeer` were added) - so in "Visual UI Automation Verify" `ItemsControl` is displayed as `ControlType.List` with each item as `ControlType.DataItem`. But in some cases all DataItems except first are empty.
```
List (3)
DataItem (3)
Text
ComboBox
ComboBox
DataItem (0) // empty
DataItem (0) // empty
```

### Reproduction Steps

Reproduces 100% in production app, I didn't succeed in creating minimal repro app.
Some observations:
- issue occur if `ItemTemplate` have 2 or more `ComboBox`/`ListBox` controls AND both their `SelectedItem` are set initially to valid value. Leaving one `ComboBox`, or removing `SelectedItem="..."`, or setting it to null makes everything work. Any number of other controls in `ItemTemplate` did not cause issue.
- this `ItemsControl` is located inside second `TabItem` - moving it to first `TabItem` or setting `TabControl`'s `TabIndex="1"` makes everything work.

Looks like caching/virtualization/optimization issue.

### Expected behavior

All DataItems have child elements

### Actual behavior

All DataItems except first are empty

### Regression?

_No response_

### Known Workarounds

a] Reset `item.ChildrenValid` to false if `item.Children is null`:
(copied original `ItemsControlWrapperAutomationPeer` / `ItemsControlItemAutomationPeer` because they are internal)
```cs
public class ItemsControl2 : ItemsControl
{
protected override AutomationPeer OnCreateAutomationPeer() =>
new ItemsControlWrapperAutomationPeer2(this);

class ItemsControlWrapperAutomationPeer2 : ItemsControlAutomationPeer
{
public ItemsControlWrapperAutomationPeer2(ItemsControl owner) : base(owner) { }
protected override ItemAutomationPeer CreateItemAutomationPeer(object item) =>
new ItemsControlItemAutomationPeer2(item, this);
protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.List;
protected override string GetClassNameCore() => "ItemsControl";

protected override List GetChildrenCore()
{
var items = base.GetChildrenCore();
foreach (ItemsControlItemAutomationPeer2 item in items ?? [])
if (item.Children is null && item.ChildrenValid)
item.ChildrenValid = false;
return items;
}
}
class ItemsControlItemAutomationPeer2 : ItemAutomationPeer
{
public ItemsControlItemAutomationPeer2(object item, ItemsControlWrapperAutomationPeer2 p) : base(item, p) { }
protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.DataItem;
protected override string GetClassNameCore() => "ItemsControlItem";

public object Children => _children.GetValue(this);
static readonly FieldInfo _children = typeof(AutomationPeer)
.GetField("_children", BindingFlags.Instance | BindingFlags.NonPublic);

public bool ChildrenValid
{
get => (bool)_childrenValid.GetValue(this);
set => _childrenValid.SetValue(this, value);
}
static readonly FieldInfo _childrenValid = typeof(AutomationPeer)
.GetField("_childrenValid", BindingFlags.Instance | BindingFlags.NonPublic);
}
}
```

---
or b) In previous net version all ItemsControl's child automation elements were merged as plain list into ItemsControl's parent automation element - this behavior can be restored on application startup using:
```cs
var accessibilitySwitches = Type.GetType("System.Windows.AccessibilitySwitches, WindowsBase, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089");
var field = accessibilitySwitches.GetField("_ItemsControlDoesNotSupportAutomation", BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.DeclaredOnly);
field.SetValue(null, 1);
```
So another workaround to have all items child automation elements, and have them grouped - is to set this switch and wrap `ItemItemplate` into some control that have automation element, for example custom Grid:
```cs
public class Grid2 : Grid
{
protected override AutomationPeer OnCreateAutomationPeer() => new Grid2Peer(this);

class Grid2Peer(Grid2 owner) : FrameworkElementAutomationPeer(owner) { }
}
```
```xml





```

### Impact

_No response_

### Configuration

Windows x64 10.0.19045.4529, net 8.0.5

### Other information

Adding logging to `ItemsControlWrapperAutomationPeer` / `ItemsControlItemAutomationPeer` shows before workaround (ch is `base.GetChildrenCore()`):
```
ItemPeer=1 ctor()
ItemPeer=2 ctor()
ItemPeer=3 ctor()
ItemsControl ch=3
ItemPeer=1 GetChildrenCore(): ch=1
ItemPeer=2 GetChildrenCore(): ch=null (*)
ItemPeer=3 GetChildrenCore(): ch=null
ItemsControl ch=3
ItemPeer=1 GetChildrenCore(): ch=1
ItemsControl ch=3
ItemPeer=1 GetChildrenCore(): ch=1
ItemsControl ch=3
ItemPeer=1 GetChildrenCore(): ch=1
```

After workaround a):
```
ItemPeer=1 ctor()
ItemPeer=2 ctor()
ItemPeer=3 ctor()
ItemsControl ch=3
ItemPeer=1 GetChildrenCore(): ch=1
ItemPeer=2 GetChildrenCore(): ch=null (*)
ItemPeer=3 GetChildrenCore(): ch=null
ItemPeer=2 GetChildrenCore(): ch=null
ItemPeer=3 GetChildrenCore(): ch=null
ItemsControl ch=3
ItemPeer=1 GetChildrenCore(): ch=1
ItemPeer=1 GetChildrenCore(): ch=1
ItemPeer=2 GetChildrenCore(): ch=1
ItemsControl ch=3
ItemPeer=1 GetChildrenCore(): ch=1
ItemPeer=3 GetChildrenCore(): ch=null
ItemPeer=1 GetChildrenCore(): ch=1
ItemPeer=3 GetChildrenCore(): ch=null
ItemsControl ch=3
ItemPeer=1 GetChildrenCore(): ch=1
ItemPeer=2 GetChildrenCore(): ch=1
ItemPeer=1 GetChildrenCore(): ch=1
ItemPeer=2 GetChildrenCore(): ch=1
ItemPeer=3 GetChildrenCore(): ch=1
ItemsControl ch=3
ItemPeer=1 GetChildrenCore(): ch=1
ItemPeer=2 GetChildrenCore(): ch=1
ItemPeer=1 GetChildrenCore(): ch=1
ItemPeer=2 GetChildrenCore(): ch=1
ItemsControl ch=3
ItemPeer=1 GetChildrenCore(): ch=1
ItemPeer=2 GetChildrenCore(): ch=1
ItemPeer=3 GetChildrenCore(): ch=1
ItemPeer=1 GetChildrenCore(): ch=1
ItemPeer=2 GetChildrenCore(): ch=1
ItemPeer=3 GetChildrenCore(): ch=1
```
in places (*) while stopping in breakpoint I can clearly see that only first ItemsControl's item is rendered in UI, so technically ch=null is correct, but looks like `_childrenValid=true` is cached and `GetChildrenCore()` in items 2,3 are never invoked again, even after children appear

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.