Mit Hilfe des UI Automation Frameworks (UIA) können Oberflächen aus einer Testanwendung heraus "ferngesteuert" werden. Damit lassen sich z.B. UI-Tests bauen. Die Grundlagen hierzu habe ich in zwei Blogposts bzw. Webcasts beschrieben.
MSDN WebCast zum UI-Recording
MSDN WebCast zum UI-Testing mit dem UI Automation Framework
Wenn man sich mit dieser Technologie etwas intensiver beschäftigt wird man früher oder später auf ein paar Probleme stoßen. So veröffentlichen beispielsweise nicht alle Third-Party-Controls alle erforderlichen Funktionalitäten über die UIAutomation Patterns. Und leider gibt es auch bei Standard-Winforms-Controls noch ein paar Lücken. Ein Beispiel hierzu ist das MenuStrip-Control. Dieses Control bietet leider nicht die erforderlichen Events um die Auswahl eines Menüeintrags aufzuzeichnen. Und für die Auswahl eines Menüeintrages gibt es auch kein geeignetes Invoke-Pattern o.ä.
Das schöne am UI Automation Framework ist jedoch, dass man diese Funktionalitäten selber nachrüsten kann. Dies wollen wir nun am Beispiel des MenuStrips mal durchspielen. Zum Einsatz kommen sog. Serverside Provider. Wir gehen zunächst her und erstellen uns ein eigenes Control das wir von MenuStrip und entsprechenden Interfaces aus dem UIA ableiten.
public class ExtendedMenuStrip : MenuStrip, IRawElementProviderSimple, IValueProvider
Über das Interface IValueProvider geben wir an, dass das Control das ValuePattern implementieren soll. Wir verwenden hier das ValuePattern statt des InvokePatterns da wir ja angeben müssen, welches Menüelement aufgerufen werden soll.
Zunächst werden wir das Interface IRawElementProviderSimple implementieren.
1: #region IRawElementProviderSimple Members
2: public object GetPatternProvider(int patternId)
3: { 4: if (patternId == ValuePatternIdentifiers.Pattern.Id)
5: { 6: return this;
7: }
8: else
9: { 10: return null;
11: }
12: }
13:
14: /// <summary>
15: /// Get the value of properties
16: /// </summary>
17: /// <param name="propertyId"></param>
18: /// <returns></returns>
19: public object GetPropertyValue(int propertyId)
20: { 21: if (propertyId == AutomationElementIdentifiers.ClassNameProperty.Id)
22: { 23: return "ExtendedMenuStrip";
24: }
25: else if (propertyId == AutomationElementIdentifiers.ControlTypeProperty.Id)
26: { 27: return ControlType.MenuBar.Id;
28: }
29:
30: if (propertyId == AutomationElementIdentifiers.HelpTextProperty.Id)
31: { 32: return "Help for ExtendedMenuStrip";
33: }
34:
35: if (propertyId == AutomationElementIdentifiers.AutomationIdProperty.Id)
36: { 37: return this.Name;
38: }
39:
40: if (propertyId == AutomationElementIdentifiers.IsEnabledProperty.Id)
41: { 42: return true;
43: }
44:
45: if (propertyId == AutomationElementIdentifiers.ItemStatusProperty.Id)
46: { 47: return SelectedItemID;
48: }
49:
50: else
51: { 52: return null;
53: }
54: }
55:
56: /// <summary>
57: /// Get the host rawelement provider
58: /// </summary>
59: public IRawElementProviderSimple HostRawElementProvider
60: { 61: get
62: { 63: return AutomationInteropProvider.HostProviderFromHandle(Handle);
64: }
65: }
66:
67: /// <summary>
68: /// Get the provider options
69: /// </summary>
70: public ProviderOptions ProviderOptions
71: { 72: get
73: { 74: return ProviderOptions.ServerSideProvider;
75: }
76: }
77:
78: #endregion
Die Methode GetPatternProvider gibt das Objekt selbst zurück, wenn ein Provider für ein ValuePattern angefordert wird. Da unser Control nur dieses Pattern implementiert, reagiert die Funktion nur auf dieses Pattern. Wir können das Control selbst zurückgeben, da dieses ja das ValuePattern implementiert. Altzernativ könnte man natürlich auch einen expliziten Provider definieren und hier zurückgeben. Die Methode GetPropertyValue gibt je nach übergebenen PropertyID einen entsprechenden Wert zurück. Hier wird z.B. die ID des Controls als AutomationID zurückgegeben. Die beiden Properties HostRawElementProvider und ProviderOptions sind readonly und geben einen Hostprovider bzw. den Typ der Providers zurück.
Die Implementierung des IValueProvider Interface ist ebenfalls recht einfach:
1: #region IValueProvider Members
2: /// <summary>
3: /// Get readonly as false
4: /// </summary>
5: public bool IsReadOnly
6: { 7: get
8: { 9: return false;
10: }
11: }
12:
13: /// <summary>
14: /// Set value: Invoke the click event of the item
15: /// </summary>
16: /// <param name="value"></param>
17: public void SetValue(string value)
18: { 19: object[] param = new object[1];
20: param[0] = null;
21: ToolStripMenuItem toolStripMenuItem = getToolStripMenuItemByName(this.Items, value);
22: if (toolStripMenuItem != null)
23: toolStripMenuItem.GetType().GetMethod("OnClick", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(toolStripMenuItem, param); 24: }
25:
26: /// <summary>
27: /// Get the ID of the selected item
28: /// </summary>
29: public string Value
30: { 31: get
32: { 33: return SelectedItemID;
34: }
35: }
36: #endregion
Entscheidend ist hier die Methode SetValue. Hier ermitteln wir das entsprechende Element innerhalb des MenuStrips (ToolStripMenuItem) und rufen hier einen Click-Event auf. Da die OnClick Methode nicht public ist, müssen wir hier Reflection verwenden um diese aufrufen zu können (siehe auch Events von WinForms Controls von Außen aufrufen). Das Aufrufen des Events ist an dieser Stelle notwendig, da ja mehrere Eventhandler registriert sein können und die wollen wir alle aufrufen. Durch den Click-Event verhält sich das Control am ähnlichsten zum Anklicken in der UI.
Nun brauchen wir noch einen Event der ausgelöst wird wenn ein Menüeintrag ausgewählt wird.
1: /// <summary>
2: /// Set eventhandlers
3: /// </summary>
4: /// <param name="items"></param>
5: public void SetExtendedMenuStripEventHandlers(ToolStripItemCollection items)
6: { 7: for (int i = 0; i <= items.Count - 1; i++)
8: { 9: ToolStripMenuItem toolStripMenuItem = items[i] as ToolStripMenuItem;
10: if(toolStripMenuItem != null)
11: { 12: toolStripMenuItem.Click += new EventHandler(ItemRaiseAutomationEvent);
13: SetExtendedMenuStripEventHandlers(toolStripMenuItem.DropDownItems);
14: }
15: }
16: }
17:
18: /// <summary>
19: /// Raise automation event
20: /// </summary>
21: /// <param name="sender"></param>
22: /// <param name="e"></param>
23: private void ItemRaiseAutomationEvent(object sender, EventArgs e)
24: { 25: if ((AutomationInteropProvider.ClientsAreListening))
26: { 27: AutomationEventArgs args = new AutomationEventArgs(InvokePatternIdentifiers.InvokedEvent);
28: this.SelectedItemID =((ToolStripMenuItem)sender).Name;
29: AutomationInteropProvider.RaiseAutomationEvent(InvokePatternIdentifiers.InvokedEvent, this, args);
30: }
31: }
Die Methode SetExtendedMenuStripEventHandlers registriert auf jedem ToolStripMenuItem einen EventHandler. Dieser löst dann den AutomationEvent aus. Hier übergeben wir im Feld SelectedItemID den Name des ToolStripMenuItems das angeklickt wurde.
Nun müssen wir noch die Methode wndProc überschreiben damit diese bei WM_GETOBJECT den entsprechenden AutomationProvider zurückgibt.
1: /// <summary>
2: /// Process Windows-based messages
3: /// </summary>
4: /// <param name="m"></param>
5: [PermissionSetAttribute(SecurityAction.Demand, Unrestricted = true)]
6: protected override void WndProc(ref Message m)
7: { 8: // 0x3D == WM_GETOBJECT
9: Int32 param = 0;
10: if (Int32.TryParse(m.LParam.ToString(), out param))
11: { 12: if ((m.Msg == 0x3D) && (param == AutomationInteropProvider.RootObjectId))
13: { 14: m.Result = AutomationInteropProvider.ReturnRawElementProvider(
15: Handle, m.WParam, m.LParam, (IRawElementProviderSimple)this);
16: return;
17: }
18: }