wpf icon indicating copy to clipboard operation
wpf copied to clipboard

Opening a Popup or ToolTip causes AutomationPeers to be created

Open batzen opened this issue 4 years ago • 10 comments

  • .NET Core Version: Not relevant as it happens in all .NET versions
  • Windows version: Windows 11 (but happens on all OS versions)
  • Does the bug reproduce also in WPF for .NET Framework 4.8?: Yes

Problem description:

After the first Popup (or ToolTip) is opened WPF starts creating AutomationPeers and starts firing automation events, even if no automation client is listening. This massively degrades performance, especially in DataGrid or when a lot of controls are present/created. This is caused by ForceMsaaToUiaBridge in Popup https://github.com/dotnet/wpf/blob/1bc136f003f0879686f53a47f984c7809b183014/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Controls/Primitives/Popup.cs#L3451

In addition to this: What's also quite expensive is that the default value for AutomationProperties.ItemStatusProperty is string.Empty and the default value for _itemStatus in AutomationPeer is null. This causes change notifications being sent for AutomationElementIdentifiers.ItemStatusProperty for every AutomationPeer that gets created. Why also consumes a lot of CPU cycles. Applying a hack which changes the default value for AutomationProperties.ItemStatusProperty to null cuts down the time spent raising pointless change notifications in half.

Actual behavior:

AutomationPeers get created after first Popup is opened.

Expected behavior:

AutomationPeers are only created when an automation client is present.

Minimal repro:

  1. Create a new WPF application
  2. Add a control with a tooltip
  3. Hover over control to make tooltip visible
  4. AutomationPeers get created

batzen avatar Dec 12 '21 10:12 batzen

This might also be the reason automation peers cause memoryleaks, i think i've came across this atleast once too.

IAmTheCShark avatar Dec 12 '21 18:12 IAmTheCShark

hi just found this thing ... anyone found something ?

Galileon avatar Dec 09 '22 15:12 Galileon

For anyone trying to change the default value for AutomationProperties.ItemStatusProperty you can use:

private static readonly FieldInfo? propertyMetadataDefaultValueFieldInfo = typeof(PropertyMetadata).GetField("_defaultValue", BindingFlags.Instance | BindingFlags.NonPublic);

private static void ForceOverwriteDefaultValue(DependencyProperty property, object? newValue)
{
    if (property.DefaultMetadata.DefaultValue == newValue)
    {
        return;
    }

    propertyMetadataDefaultValueFieldInfo?.SetValue(property.DefaultMetadata, newValue);
}

and call it with ForceOverwriteDefaultValue(AutomationProperties.ItemStatusProperty, null);.

Use this at your own risk as modifying things through reflection might be dangerous. You should also use this hack as early in your application lifecycle as possible

batzen avatar Dec 10 '22 11:12 batzen

Any feedback on this?

If this is (and I think it is) related to what I am experiencing, sometimes it causes BIG performance degradations.

A simple DataGrid with 10 rows and a DataGridTemplateColumn with a CellTemplate containing a TextBlock with ToolTip causes WPF to go insane with AutomationPeer calls, without automation clients.

If it would be of help, I can try to create a simple repro repo of that scenario.

The hack provided by @batzen helped, but the problem persists.

This is a hot path of my application in .NET 7, without any automation client:

image

LoRdPMN avatar Apr 18 '23 19:04 LoRdPMN

I tried @batzen reflection solution but it did not help with my datagrid when a combobox is opened.

@LoRdPMN

It appears I was able to solve my data grid sluggish issue by making my own DataGrid class and returning null in OnCreateAutomationPeer. I'm not sure what ramifications this may have but it appears to work,.

    public class MyDataGrid : DataGrid
    {
        public MyDataGrid()
        {
        }


        protected override AutomationPeer OnCreateAutomationPeer()
        {
            return null;
            //return base.OnCreateAutomationPeer();
        }
    }

brswan avatar Oct 02 '24 21:10 brswan

hello i have it will send U morning :)

Galileon avatar Oct 02 '24 21:10 Galileon

hello i have it will send U morning :)

Thank you! I would love to see your solution.

brswan avatar Oct 02 '24 21:10 brswan

So i have used Harmony ( PackageReference Include="HarmonyX" Version="2.10.2" ) it makes override for part of the framework code and then:

  [ObfuscationAttribute(Exclude = true)]
  [HarmonyPatch(typeof(UIElementAutomationPeer), "CreatePeerForElement")]
  class PopupFix
  {
      static bool Prefix(UIElement element)
      {
          return false;
      }
  }

Galileon avatar Oct 02 '24 21:10 Galileon

I tried @batzen reflection solution but it did not help with my datagrid when a combobox is opened.

@LoRdPMN

It appears I was able to solve my data grid sluggish issue by making my own DataGrid class and returning null in OnCreateAutomationPeer. I'm not sure what ramifications this may have but it appears to work,.

    public class MyDataGrid : DataGrid
    {
        public MyDataGrid()
        {
        }


        protected override AutomationPeer OnCreateAutomationPeer()
        {
            return null;
            //return base.OnCreateAutomationPeer();
        }
    }

After more testing it doesn't seem to help datagrids with any sort of complexity to them. It's still an issue for me.

brswan avatar Oct 03 '24 13:10 brswan

I found a solution that works for my problem. I had to override the Windows class. From there you override the OnCreateAutomationPeer() method.

 public class CustomWindowAutomationPeer : FrameworkElementAutomationPeer
 {
     public CustomWindowAutomationPeer(FrameworkElement owner) : base(owner) { }

     protected override string GetNameCore()
     {
         return "CustomWindowAutomationPeer";
     }

     protected override AutomationControlType GetAutomationControlTypeCore()
     {
         return AutomationControlType.Window;
     }

     protected override List<AutomationPeer> GetChildrenCore()
     {
         return new List<AutomationPeer>();
     }
 }

In your custom Window class add

 protected override System.Windows.Automation.Peers.AutomationPeer OnCreateAutomationPeer()
   {
       return new CustomWindowAutomationPeer(this);
   }

brswan avatar Oct 03 '24 15:10 brswan