Wie in diesem Blog bereits an anderen Stellen erläutert, eignet sich das UIA (UI Automation Framework) sehr gut, um UI-Tests aufzubauen. Wer sich mit dieser Möglichkeit beschäftigt wird aber früher oder später auf das Problem stoßen, dass UIA Support bei WinForms Controls nicht flächendeckend gegeben ist, vor allem bei 3rd Party Controls sieht es da oft eher mau aus.
Ich habe hier beschrieben, wie man mit einem ServerSide Provider diese Lücken selbst schließen kann. Das Standardvorgehen sieht dabei vor, dass man ein eigenes Control erstellt, das man dann von dem Ausgangscontrol ableitet. Diese Vorgehen ist in der Praxis allerdings nicht unproblematisch. Zum einen muss man die abgeleiteten Controls für jedes neue Release der Ausgangscontrols aktualisieren und zum zweiten ist es nicht gerade schön in einer bestehenden Anwendung alle Controls gegen die abgeleitete Variante austauschen zu müssen.
Deshalb möchte ich hier einen alternativen Weg vorstellen. Die Idee beruht darauf, dass die Controls, denen es an Accesibility fehlt jeweils in ein Panel platziert werden und auf diesem Panel dann die entsprechenden Patterns implementiert werden. Das Panel kann die Operationen dann an das Control in seinem Bauch weiterleiten.
Ich habe mal ein Beispiel für Janus Calendar Controls gebaut. Zunächst habe ich mir ein UIA-Panel erstellt, von dem ich dann die weiteren Panels für die spezifischen Controls ableiten kann.
1: using System;
2: using System.Drawing;
3: using System.Security.Permissions;
4: using System.Windows.Automation;
5: using System.Windows.Automation.Provider;
6: using System.Windows.Forms;
7:
8: namespace WindowsFormsApplication1
9: {
10: public partial class UIAPanel : Panel, IRawElementProviderSimple
11: {
12: public UIAPanel()
13: {
14: this.BackColor = Color.Yellow;
15: this.Height = 0;
16: this.Width = 0;
17: this.AutoSize = true;
18: }
19:
20: [PermissionSetAttribute(SecurityAction.Demand, Unrestricted = true)]
21: protected override void WndProc(ref Message m)
22: {
23: // 0x3D == WM_GETOBJECT
24: Int32 param = 0;
25: if (Int32.TryParse(m.LParam.ToString(), out param))
26: {
27: if ((m.Msg == 0x3D) && (param == AutomationInteropProvider.RootObjectId))
28: {
29: m.Result = AutomationInteropProvider.ReturnRawElementProvider(
30: Handle, m.WParam, m.LParam, (IRawElementProviderSimple)this);
31: return;
32: }
33: }
34: base.WndProc(ref m);
35: }
36:
37: #region IRawElementProviderSimple Members
38:
39: public object GetPatternProvider(int patternId)
40: {
41: if (patternId == ValuePatternIdentifiers.Pattern.Id)
42: {
43: return this;
44: }
45: else
46: {
47: return null;
48: }
49: }
50:
51: public object GetPropertyValue(int propertyId)
52: {
53: if (propertyId == AutomationElementIdentifiers.ClassNameProperty.Id)
54: {
55: return "CalendarPanel";
56: }
57: else if (propertyId == AutomationElementIdentifiers.ControlTypeProperty.Id)
58: {
59: return ControlType.MenuBar.Id;
60: }
61:
62: if (propertyId == AutomationElementIdentifiers.HelpTextProperty.Id)
63: {
64: return "Help for CalendarPanel";
65: }
66:
67: if (propertyId == AutomationElementIdentifiers.AutomationIdProperty.Id)
68: {
69: return this.Name;
70: }
71:
72: if (propertyId == AutomationElementIdentifiers.IsEnabledProperty.Id)
73: {
74: return true;
75: }
76:
77: else
78: {
79: return null;
80: }
81: }
82:
83: public IRawElementProviderSimple HostRawElementProvider
84: {
85: get
86: {
87: return AutomationInteropProvider.HostProviderFromHandle(Handle);
88: }
89: }
90:
91: public ProviderOptions ProviderOptions
92: {
93: get
94: {
95: return ProviderOptions.ServerSideProvider;
96: }
97: }
98:
99: #endregion
100:
101: }
102: }
Dieses Panel stellt einen ServerSide Provider zur Verfügung. Wir können nun von diesem Control ableiten und ein entsprechendes Pattern, z.B. das SetValue Pattern implementieren:
2: using System.Windows.Automation.Provider;
3: using System.Windows.Forms;
4:
5: namespace WindowsFormsApplication1
6: {
7: public partial class CalendarPanel : UIAPanel, IValueProvider
8: {
9: private Janus.Windows.Schedule.Calendar control;
10: public Janus.Windows.Schedule.Calendar Control
12: get
14: if (control == null)
15: {
16: if (this.Controls.Count > 0 && this.Controls[0].GetType() == typeof(Janus.Windows.Schedule.Calendar))
17: control = (Janus.Windows.Schedule.Calendar)this.Controls[0];
19: return control;
20: }
21: }
22:
23: #region IValueProvider Members
24:
25: public bool IsReadOnly
27: get
29: return false;
30: }
31: }
32:
33: public void SetValue(string value)
34: {
35: this.BeginInvoke((MethodInvoker)delegate()
36: {
37: DateTime date = DateTime.Parse(value);
38: Control.SelectionRange = new Janus.Windows.Schedule.DateRange(date, date);
39: });
40: }
41:
42: public string Value
43: {
44: get
45: {
46: return Control.SelectionRange.End.ToShortDateString();
47: }
49:
50: #endregion
51: }
52: }
Wenn wir nun das Calendar_Control nicht direkt auf unserer Form platzieren, sondern in einem solchen CalendarPanel ablegen, können wir eine Automatisierung über die UIA gegen dieses Panel implementieren. Was nun noch optimiert werden soll, ist dass die ganzen Controls nicht händisch in die jeweiligen Panels platziert werden sollen, sondern dies soll nach Möglichkeit automatisiert werden. der Ansatz hierbei ist, dass alle Controls auf der Form beim Laden untersucht werden und für die gewünschten Controls dynamisch entsprechende Panels erzeugt werden sollen, in die dann die Controls platziert werden. Dieser Ansatz bietet zudem den Vorteil, dass man die UIA-Panels nur dann nutz, wenn man UI-Test ausführen möchte. Bei der Release-Version sind diese dann nicht enthalten. Zwar unterscheidet sich dadurch Release und Test-Version geringfügig, jedoch sollten diese Implikationen vernachlässigbar sein, vor allem dann, wenn beim Entwickeln komplett auf die Panels verzichtet wird und diese wirklich nur für die UI-Tests genutzt werden.
Der Code dazu sieht dann so aus:
1: private void PlaceControlsIntoPanel(Control.ControlCollection controls)
2: {
3: Panel uiaPanel;
5: foreach (Control automationControl in controls.OfType<Control>().ToList())
7: switch (automationControl.GetType().ToString())
9: case "Janus.Windows.CalendarCombo.CalendarCombo":
10: {
11: uiaPanel = new CalendarComboPanel();
12: break;
13: }
14: case "Janus.Windows.Schedule.Calendar":
16: uiaPanel = new CalendarPanel();
17: break;
19: default:
20: {
21: if (automationControl.HasChildren)
23: PlaceControlsIntoPanel(automationControl.Controls);
24: }
25: continue;
26: }
27: }
28: uiaPanel.Name = "p_" + automationControl.Name;
29: uiaPanel.Top = automationControl.Top;
30: uiaPanel.Left = automationControl.Left;
31: uiaPanel.Controls.Add(automationControl);
32: automationControl.Top = 0;
33: automationControl.Left = 0;
34: controls.Add(uiaPanel);
36: }
Wird die Anwendung dann inkl. Test-Client ausgeführt, sieht das so aus:
Download Demo-Code
Remember Me
Disclaimer The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.