Neues rund um's Thema .Net, Team Foundation Server und SCRUM RSS 2.0
# Friday, July 03, 2009

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.

image

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:

   1: using System;
   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
  11:         {
  12:             get
  13:             {
  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];
  18:                 }
  19:                 return control;
  20:             }
  21:         }
  22:  
  23:         #region IValueProvider Members
  24:  
  25:         public bool IsReadOnly
  26:         {
  27:             get 
  28:             {
  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:             }
  48:         }
  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;
   4:  
   5:     foreach (Control automationControl in controls.OfType<Control>().ToList())
   6:     {
   7:         switch (automationControl.GetType().ToString())
   8:         {
   9:             case "Janus.Windows.CalendarCombo.CalendarCombo":
  10:                 {
  11:                     uiaPanel = new CalendarComboPanel();
  12:                     break;
  13:                 }
  14:             case "Janus.Windows.Schedule.Calendar":
  15:                 {
  16:                     uiaPanel = new CalendarPanel();
  17:                     break;
  18:                 }
  19:             default:
  20:                 {
  21:                     if (automationControl.HasChildren)
  22:                     {
  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);
  35:     }
  36: }

 

Wird die Anwendung dann inkl. Test-Client ausgeführt, sieht das so aus:

image

Friday, July 03, 2009 2:10:58 PM (Mitteleuropäische Sommerzeit, UTC+02:00)  #    Comments [0] -
UI Automation
Archive
<July 2014>
SunMonTueWedThuFriSat
293012345
6789101112
13141516171819
20212223242526
272829303112
3456789
About the author/Disclaimer

Disclaimer
The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

© Copyright 2014
Thomas
Sign In
Statistics
Total Posts: 620
This Year: 11
This Month: 0
This Week: 0
Comments: 356
Themes
All Content © 2014, Thomas
DasBlog theme 'Business' created by Christoph De Baene (delarou)