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.

Friday, February 19, 2010

How to access a test object's AutomationPatterns

If your business is automating WPF applications you should know all about UI Automation Control Patterns. If you do and you wish you could access some automation pattern properties on your TO's, there's a very convenient way: you can access an automation pattern either by name or by index. for example:
WpfWindow("window").WpfListView("list").AutomationPattern("Selection").GetSelection()
will return an AutomationElement that is the current selection in the list.
I found this extremely useful when tackling text containers. The following call returns text from a TextBox, Label or any other TextPattern implementor:
WpfWindow("window").WpfEdit("textBox1").AutomationPattern("Text").DocumentRange.GetText(-1)

Next up: using a similiar technique to retrieve values from a wpf grid !

Tuesday, February 16, 2010

UISpy - the tool you must know

You dont have to use QTP's built in object spy on WPF controls

Now how good does that sound like ?
If you ever given QTP a chance with your WPF AUT, i bet it does.
Anyway, there's this wonderful tool that is based on MS's UIAutomation framework (this is the framework QTP's WPF support is built upon). What it does is actually quite similiar to the good ol' object spy, just that it actually works on WPF controls and showing you the entire application hierarchy (QTP 9.2's object spy stops at the moment it recognizes one control that isn't a window, though i think there might be a hotfix for that).

You can find further documentation on this msdn page (notice comments about it being mistakenly removed from the Windows SDK 6.0A, but it's back if you have windows 7 and you can download it separately anyway)

Enjoy