Tuesday, January 11, 2011

Retrieving WpfGrid cell values using QTP

Let me just start off by saying this solution is not that straight forwards and requires some experience in all of the following:

  • Writing a C# assembly that uses Reflection
  • Accessing external C# Dlls using QTP's DotNetFactory
  • Being familiar with Microsoft's AutomationFramework (AutomationElement, TreeWalker, etc)

For anyone not too familiar with even one of these, i suggest continue waiting for better WPF support from mercury, unless you’re up for the challenge (;

Make sure your test object supports UI Automation

First thing you need to do is to make sure the test object you are trying to write support for implements the correct patterns for working with it as a grid. If the object is recognized in QTP as a WpfGrid most chances are it is OK. If not, you can use Reflector to check out what AutomationPatterns are being implemented by your control. You do that by opening the DLL containing the grid control in Reflector (e.g Xceed.Wpf.DataGrid for exceed) and navigating to the OnCreateAutomationPeer method. If The object created there is implementing IGridProvider (or ITableProvider that inherits it) than you're covered.

Start working with DispWrapper and AutomationElement in a new Class Library project

Open a new project of type Class Library in visual studio. Search your computer for the WpfAgent.dll file (the full name is Mercury.QTP.WpfAgent). The dll should be COM registered if you have QTP installed on the current machine, so you can find in the COM tab of the Add Reference dialog from visual studio. Now you should add references to UIAutomationClient.dll and UIAutomationTypes.dll, both can be found at C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v3.0. These dlls are also included as a part of the 3.5 and 4.0 client profiles, and of course as a part of the 4.0 framework.

Within the WpfAgent.dll assembly you will find a type called DispWrapper. All you need to know about it is that objects that will be passed by QTP to the custom library you're writing will be stored in a private field called _object (you can check it out using Reflector). You will need to extract that field's value using Reflection (hereinafter). You can assume that it is legal to cast that object to AutomationElement, as that's the only object type you will be passing from QTP.

Implement AutomationElement value extraction

Write a method called GetElementValue accepting one parameter of type DispWrapper and returning a string. Now use the AutomationFramework to extract a string value from the given AutomationElement. For now it is OK to presume that this is a single object, not a tab control or a listbox for instance. The code for this method should look a lot like this:

   1: using System.Reflection;
   2: using Mercury.WpfAgent;
   3: using System.Windows.Automation;
   4:  
   5: namespace ASC.Automation.QtpExtensions.Wpf
   6: {
   7:     public class WpfValueExtractor
   8:     {
   9:         public string GetElementValue(object dispWrapper)
  10:         {
  11:             DispWrapper wrapper = (DispWrapper) dispWrapper;
  12:  
  13:             AutomationElement element =
  14:                 (AutomationElement)
  15:                 typeof (DispWrapper)
  16:                 .GetField("_object", BindingFlags.Instance | BindingFlags.NonPublic)
  17:                 .GetValue(wrapper);
  18:  
  19:             object patternObject;
  20:             if (element.TryGetCurrentPattern(TextPattern.Pattern, out patternObject))
  21:             {
  22:                 var textPattern = (TextPattern) patternObject;
  23:                 return textPattern.DocumentRange.GetText(-1);
  24:             }
  25:             if (element.TryGetCurrentPattern(ValuePattern.Pattern, out patternObject))
  26:             {
  27:                 var valuePattern = (ValuePattern) patternObject;
  28:                 return valuePattern.Current.Value;
  29:             }
  30:             AutomationElement childTextBlock = element.FindFirst(
  31:                 TreeScope.Descendants,
  32:                 new PropertyCondition(AutomationElement.ClassNameProperty, "TextBlock",
  33:                                       PropertyConditionFlags.IgnoreCase));
  34:  
  35:             if (childTextBlock != null)
  36:             {
  37:                 return childTextBlock.Current.Name;
  38:             }
  39:  
  40:             return string.Empty;
  41:         }
  42:     }
  43: }

Note that I haven’t run this, but it should be very close to working.

Another thing you should notice is returning a child TextBlock’s name at the end – this is a by-design behavior by microsoft – because TextBlock doesn't support any AutomationPattern, its Name property is always identical to its value.

Accessing the utility from QTP

This should look very familiar to anyone who has ever worked with DotNetFactory. You create the utility class, and call it passing an automation element. The following code snippet extracts an AutomationElement out of an element that supports the GridPattern, and extracts it’s value:


   1: Dim qtpExtLib = DotNetFactory.CreateInstance("ASC.Automation.QtpExtensions.Wpf.WpfValueExtractor", "ASC.Automation.QtpExtensions.Wpf", "C:\ASC.Automation.QtpExtensions.Wpf.dll")
   2: Dim gridCell = WpfWindow("Window1").WpfGrid("dataGrid").AutomationPattern("Grid").GetItem(0,1)
   3: MsgBox qtpExtLib.GetElementValue(gridCell)


Summary

What we did was creating a small utility used to extract string values from automation elements. Note that you can pass any AutomationElement (retrieved from a Wpf test object) using the AutomationElement property. This code should work fine for some other object too.

Enjoy the power and control you have once the object is passed to C# – you can implement a complete library with either generic or specific support for just about any wpf object with UI automation support

Please feel free to contact me in any way if are experiencing any issues or have any further questions.