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...)
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)
{
}
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();
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)
{
if (this._inpc != null)
this._inpc.PropertyChanged -= new PropertyChangedEventHandler(this.ValueChangedHandler);
if (this._incc != null)
this._incc.CollectionChanged -= new NotifyCollectionChangedEventHandler(this.ValueChangedHandler);
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.