Custom AutomationPeer classes

When you use custom controls in your silverlight application, you should implement custom AutomationPeer classes. These AutomationPeer classes will be used in your unittests. Below some different flavours I use in my project.

  • Inherited, when you inherit from a control that already has an AutomationPeer you can redirect everything to that implementation.
  • /// <summary>
    /// Special button that inherits from the Button control
    /// </summary>
    public class SpecialButton : System.Windows.Controls.Button
    {
       // your magic happens here
    }
    
    /// <summary>
    /// Exposes SpecialButton types to UI automation.
    /// </summary>
    public class SpecialButtonAutomationPeer : System.Windows.Automation.Peers.ButtonAutomationPeer
    {
       /// <summary>
       /// Initializes a new instance of the <see cref="SpecialButtonAutomationPeer"/> class.
       /// </summary>
       /// <param name="owner">The owner.</param>
       public SpecialButtonAutomationPeer(SpecialButton owner) : base(owner)
       {
       }
    }
    
  • Wrapped, when you wrap a control that already has an AutomationPeer you can redirect everything to that implementation.
  • /// <summary>
    /// Special textbox that wraps a TextBox control 
    /// </summary>
    public class SpecialTextBox
    {
       // internal to expose within the assembly
       // protected to expose to controls who inherit this SpecialTextBox
       internal protected System.Windows.Controls.TextBox _textBox;
       // your magic happens here
    }
    
    /// <summary>
    /// Exposes SpecialTextBox types to UI automation.
    /// </summary>
    public class SpecialTextBoxAutomationPeer : System.Windows.Automation.Peers.TextBoxAutomationPeer
    {
       /// <summary>
       /// Initializes a new instance of the <see cref="SpecialTextBoxAutomationPeer"/> class.
       /// </summary>
       /// <param name="owner">The owner.</param>
       public SpecialButtonAutomationPeer(SpecialTextBox owner) : base(owner._textBox)
       {
       }
    }
    
  • Composite 1, when you use multiple controls that already have an AutomationPeer you can redirect everything to that implementation, but you need to direct this. You expose an AutomationPeer for every control on the composite control
  • /// <summary>
    /// Special time control that is a composite of two ComboBox controls
    /// </summary>
    public class SpecialTimeControl : System.Windows.Controls.Control
    {
       // internal to expose within the assembly
       // protected to expose to controls who inherit this SpecialTimeControl
       internal protected ComboBox _hoursComboBox;
       internal protected ComboBox _minutesComboBox;
       // your magic happens here
    }
    
    /// <summary>
    /// Exposes ComboBoxAutomationPeer types to UI automation.
    /// </summary>
    public class SpecialTimeControlAutomationPeer : System.Windows.Automation.Peers.ComboBoxAutomationPeer
    {
       /// <summary>
       /// Enumeration for selecting the timepart control
       /// </summary>
       public enum TimePartControl
       {
          /// <summary>
          /// Control for providing the Hours value
          /// </summary>
          Hours = 0,
          /// <summary>
          /// Control for providing the Minutes value
          /// </summary>
          Minutes = 1
       }
    
       /// <summary>
       /// Initializes a new instance of the <see cref="SpecialTimeControlAutomationPeer"/> class.
       /// </summary>
       /// <param name="owner">The owner.</param>
       /// <param name="control">The part of the control to expose.</param>
       public SpecialTimeControlAutomationPeer(SpecialTimeControl owner, TimePartControl control) : base(SelectControl(owner, control))
       {
       }
    
       /// <summary>
       /// Selects the control.
       /// </summary>
       /// <param name="owner">The owner.</param>
       /// <param name="selection">The selection.</param>
       /// <returns></returns>
       private static ComboBox SelectControl(SpecialTimeControl owner, TimePartControl selection)
       {
          ComboBox result = null;
          switch (selection)
          {
             case TimePartControl.Hours:
                result = owner._hoursComboBox;
                break;
             case TimePartControl.Minutes:
                result = owner._minutesComboBox;
                break;
          }
          return result;
       }
    }
    
    // example: set the hour part of the time control to 10
    var hoursComboBoxPeer = new SpecialTimeControlAutomationPeer(ctrl, SpecialTimeControlAutomationPeer.TimePartControl.Hours);
    var hour = "10";
    var hoursComboBoxSelectionItem = (ISelectionItemProvider)hoursComboBoxPeer.GetChildren()
        .Where(x => x.GetName().Equals(hour))
        .First()
        .GetPattern(PatternInterface.SelectionItem);
    hoursComboBoxSelectionItem.Select();
    
  • Composite 2, when you use multiple controls that already have an AutomationPeer you can redirect everything to that implementation, but you need to direct this. You implement the provider interface on the custom AutomationPeerclass.
  • /// <summary>
    /// Exposes SpecialTimeControl types to UI automation.
    /// </summary>
    public class SpecialTimeControlAutomationPeer : System.Windows.Automation.Peers.SelectorAutomationPeer, 
                                                    System.Windows.Automation.Provider.IValueProvider
    {
        protected System.Windows.Automation.Peers.ComboBoxAutomationPeer _hoursComboBoxAutomationPeer;
        protected System.Windows.Automation.Provider.IValueProvider _hoursValueProvider;
        protected System.Windows.Automation.Peers.ComboBoxAutomationPeer _minutesComboBoxAutomationPeer;
        protected System.Windows.Automation.Provider.IValueProvider _minutesValueProvider;
    
        /// <summary>
        /// Initializes a new instance of the <see cref="SpecialTimeControlAutomationPeer"/> class.
        /// </summary>
        /// <param name="owner">The owner.</param>
        public SpecialTimeControlAutomationPeer(SpecialTimeControl owner) : base(owner._hoursComboBox)
        {
            // construct the AutomationPeer and IValueProvider objects that will do all the actual work
            _hoursComboBoxAutomationPeer = new System.Windows.Automation.Peers.ComboBoxAutomationPeer(owner._hoursComboBox);
            _hoursValueProvider = (System.Windows.Automation.Provider.IValueProvider)_hoursComboBoxAutomationPeer.GetPattern(System.Windows.Automation.Peers.PatternInterface.Value);
            _minutesComboBoxAutomationPeer = new System.Windows.Automation.Peers.ComboBoxAutomationPeer(owner._minutesComboBox);
            _minutesValueProvider = (System.Windows.Automation.Provider.IValueProvider) _minutesComboBoxAutomationPeer.GetPattern(System.Windows.Automation.Peers.PatternInterface.Value);
        }
    
        /// <summary>
        /// Gets the control pattern for the <see cref="T:System.Windows.Controls.Primitives.Selector"/> that is associated with this <see cref="T:System.Windows.Automation.Peers.SelectorAutomationPeer"/>.
        /// </summary>
        /// <param name="patternInterface">One of the enumeration values.</param>
        /// <returns>
        /// The object that implements the pattern interface, or null if the specified pattern interface is not implemented by this peer.
        /// </returns>
        public override object GetPattern(System.Windows.Automation.Peers.PatternInterface patternInterface)
        {
            // this class implements the IValueProvider so return it
            if (patternInterface == System.Windows.Automation.Peers.PatternInterface.Value) return this;
            else return base.GetPattern(patternInterface);
        }
    
        /// <summary>
        /// Gets a value that indicates whether the value of a control is read-only.
        /// </summary>
        /// <returns>true if the value is read-only; false if it can be modified. </returns>
        public bool IsReadOnly
        {
            get 
            { 
                return _hoursValueProvider.IsReadOnly || _hoursValueProvider.IsReadOnly;
            }
        }
    
        /// <summary>
        /// Sets the value of a control.
        /// </summary>
        /// <param name="value">The value to set. The provider is responsible for converting the value to the appropriate data type.</param>
        public void SetValue(string value)
        {
            // value should be a datetime
            var date = DateTime.Parse(value);
            // get the parts
            var hour = date.Hour.ToString();
            var minute = date.Minute.ToString("00");
            // find the item in the children that matches
            // the value for hour in the date
            var hoursComboBoxSelectionItem = 
                (System.Windows.Automation.Provider.ISelectionItemProvider)
                _hoursComboBoxAutomationPeer.GetChildren()
                .Where(x => x.GetName().Equals(hour))
                .First() // throws an expection if not found
                .GetPattern(System.Windows.Automation.Peers.PatternInterface.SelectionItem);
            // select the hour
            hoursComboBoxSelectionItem.Select();
            // find the item in the children that matches
            // the value for minute in the date
            var minutesComboBoxSelectionItem = 
                (System.Windows.Automation.Provider.ISelectionItemProvider)
                _minutesComboBoxAutomationPeer.GetChildren()
                .Where(x => x.GetName().Equals(minute))
                .First() // throws an expection if not found
                .GetPattern(System.Windows.Automation.Peers.PatternInterface.SelectionItem);
            // select the minute
            minutesComboBoxSelectionItem.Select();
        }
    
        /// <summary>
        /// Gets the value of the control.
        /// </summary>
        /// <returns>The value of the control.</returns>
        public string Value
        {
            get 
            {
                var hour = int.Parse(_hoursValueProvider.Value);
                var minute = int.Parse(_minutesValueProvider.Value);
                // use defaults for the year, month, day and seconds
                var date = new DateTime(2000, 1, 1, hour, minute, 0);
                return date.ToString();
            }
        }
    }
    
    // example: set the complete time to the current time
    var specialTimeAutomationPeer = new SpecialTimeControlAutomationPeer(ctrl);
    var specialTimeValueProvider = specialTimeAutomationPeer.GetPattern(System.Windows.Automation.Peers.PatternInterface.Value);
    specialTimeValueProvider.SetValue(DateTime.Now); // uses the current time
    

The choice between the two composite controls is up to you. Maybe you need both. The difference is the place where the controls are set. In composite 1 this is done in your testcode which gives you the possibility to do some wrong inputs. In composite 2 the code is kept out of the testcode which provides more robust tests.

About erictummers

Working in a DevOps team is the best thing that happened to me. I like challenges and sharing the solutions with others. On my blog I’ll mostly post about my work, but expect an occasional home project, productivity tip and tooling review.
This entry was posted in Test and tagged , , , , , . Bookmark the permalink.

6 Responses to Custom AutomationPeer classes

  1. Pingback: Silverlight unittest / automation | Erictummers's Blog

  2. libin says:

    Hi Erictummers’s ,

    Could you please suggest some methods for creating a custom automation peer using VBA?I was automating my application in Excel VBA using MS UI Automation.I came across a custom object which is not being identified as a control by UI spy.

    any help to identify these controls would be really appreciated.

    • erictummers says:

      Hi Libin,
      My Automation implementation is part of the program under test. I added the classes described in this post to the silverlight project and compiled them into the XAP.
      If you’re using custom controls in your VBA, you should add the automation classes there.
      Does this answere your question?

      • libin says:

        Hi Erictummers’s ,

        Thanks for your update. I am an automation Test script developer who doesn’t have access to the application source code. As far as i understood form your post, Source code needs to be modified, in order to implement the custom automation peer. Since i don’t have access to source code is there any way to access a custom object like how Microsoft UI automation access and manipulate a standard control during run time from an external source? To be more precise about the issue I am facing, i have a custom Listbox in my application which is not identified by UI spy or Inspect .In order to do a complete testing of a work flow I need to select an item from the custom Listbox.
        Could you please help me to write some code to Make UI automation identify Custom objects from an external source (May be dll or vbs file).

        Regards
        Libin Sebastian

      • erictummers says:

        We faced this problem with third party controls in a Windows Forms dotNET application. For our testsuite Rational Robot we were able to implement a custom proxy to get the automation working. One of my resources back then (2008) was http://www.wilsonmar.com/1robot.htm.

  3. libin says:

    Thanks,

    I was looking for a solution using MS UI Automation.As far as i understood,there is no way we can automate third Party controls effectively.We have evaluated many popular tools avilable in market.Most of them are providing only workarounds to overcome this issue.

    Regards
    Libin

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.