Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / DevOps / testing

Exposing DataContext For Automation

4.89/5 (2 votes)
12 Aug 2011CPOL2 min read 15.3K  
Using an attached behavior to expose the value of a DataContext for Automation
UI Automation[^] is a core part of being able to perform automated UI testing. The UI Automation API is used to examine the state of the application from outside of the running process.

Unfortunately for WPF, most UIs contain numerous styles/templates that customize a control beyond the ability of the UI Automation API to support. It often leads to less than optimal solutions regarding exposing the data through code sprinkled throughout your application.

I came across this blog[^] and noticed that this idea could be developed further through the use of behaviors.

The initial idea proposed was to place all relevant information in the AutomationProperties.ItemStatusProperty of a control. Unfortunately, this would require deriving classes, which is messy and cumbersome. Plus, it is not an option for third party code.

By using an attached dependency property and an IValueConverter, a binding could be created to link the DataContext of any FrameworkElement to the AutomationProperties.ItemStatusProperty. In the example code provided, the IValueConverter serializes the DataContext into an XML string, which could easily be read from an external process to determine the state of an application.

(As a note, I've wrapped this code in a "#if DEBUG" because this should never go into production code...)

C#
public class FrameworkElementBehavior
{
    public static readonly DependencyProperty SerializeDataContextProperty = DependencyProperty.RegisterAttached
    (
        "SerializeDataContext",
        typeof(bool),
        typeof(FrameworkElementBehavior),
        new UIPropertyMetadata(false, FrameworkElementBehavior.OnSerializeDataContextPropertyChanged)
    );

    public static bool GetSerializeDataContext(DependencyObject obj)
    {
        return false;
    }
    public static void SetSerializeDataContext(DependencyObject obj, bool value)
    {
        /* Intentionally left blank. */
    }

    private static void OnSerializeDataContextPropertyChanged(DependencyObject dpo, DependencyPropertyChangedEventArgs args)
    {
#if DEBUG
        FrameworkElement fe;
        XmlSerializerConverter converter;

        if ((fe = (dpo as FrameworkElement)) != null)
        {
            Binding binding = new Binding();
            binding.Source = dpo;
            binding.Path = new PropertyPath("DataContext");
            binding.Mode = BindingMode.OneWay;
            binding.Converter = converter = new XmlSerializerConverter();

            // Save off the binding expression, so that it can be used to update the binding.
            converter.BindingExpression = fe.SetBinding(AutomationProperties.ItemStatusProperty, binding);
        }
#endif
    }

    private class XmlSerializerConverter : IValueConverter
    {
        public BindingExpressionBase BindingExpression { get; set; }

        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value != null)
            {
                // Unwire any prior listeners.
                if (this._inpc != null)
                    this._inpc.PropertyChanged -= new PropertyChangedEventHandler(this.ValueChangedHandler);
                if (this._incc != null)
                    this._incc.CollectionChanged -= new NotifyCollectionChangedEventHandler(this.ValueChangedHandler);

                // Wire up the new listeners.
                if ((this._inpc = (value as INotifyPropertyChanged)) != null)
                    this._inpc.PropertyChanged += new PropertyChangedEventHandler(this.ValueChangedHandler);
                if ((this._incc = (value as INotifyCollectionChanged)) != null)
                    this._incc.CollectionChanged += new NotifyCollectionChangedEventHandler(this.ValueChangedHandler);

                return XmlSerializerConverter.SerializeObject(value, culture);
            }
            else
            {
                return null;
            }
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }

        private INotifyCollectionChanged _incc;
        private INotifyPropertyChanged _inpc;

        private void ValueChangedHandler(object sender, EventArgs e)
        {
            if (this.BindingExpression != null)
                this.BindingExpression.UpdateTarget();
        }

        private static string SerializeObject(object value, CultureInfo culture)
        {
            XmlSerializer serializer = new XmlSerializer(value.GetType());

            using (StringWriter sw = new StringWriter(culture))
            {
                serializer.Serialize(sw, value);

                return sw.ToString();
            }
        }
    }
}


Now, within your XAML, you need only perform the following for each item you want to serialize the DataContext for:
FrameworkElementBehavior.SerializeDataContext="True"


Then within your UI Automation, you will be able to inspect the DataContext of any item you've applied SerializeDataContextProperty to by looking at the ItemStatus.

UPDATE: I realized that the original version wouldn't work well with INotifyCollectionChanged or INotifyCollectionChanged, as the bound object would be changing underneath without updating the ItemStatus. So, I had to change up the XmlSerializerConverter to add/remove listeners for the appropriate events. It is not as elegant as before, but it gets the job done.

***Your mileage may vary because I have not tested whether or not other items use ItemStatus, but I hope you've found this useful.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)