Exporting Data from Apps Using Automation

January 17th 2020 Windows E2E Testing

With the SportTracks 3 end of life just around the corner and no assurances that the application will still work after that, it's time to export old data from it and start using other applications and services. While the application makes it easy to export activity data, there doesn't seem to be any built-in feature for exporting the weight data.

SportTracks Athlete Data

After considering different options for exporting data despite this limitation, I decided to give Microsoft UI Automation a try. Since I had no previous experience with this API, I made my life easier by using the FlaUI wrapper for it. Both are primarily meant for writing end-to-end tests and accessibility tools but what I needed to do was in many aspects very similar - programmatically manipulate the application and read values from its UI.

The first step was identifying the controls I needed to interact with. FlaUI provides a useful tool for that - FlaUInspect. At the start it lets you choose the Automation API to use.

Choosing the API version for FlaUInspect

I tried both and although SportTracks 3 is based on Windows Forms, UIA3 seemed to work better for my needs so that's what I decided to use.

SportTracks controls in FlaUInspect

I could identify the list view control with the full weight history. However, I couldn't find any way to read the values from it programmatically. It might be simply because of my lack of experience with the API. I didn't spend too much time trying to get it to work since I could also read the values I needed from the title pane (the date) and the input field (the weight).

Getting references to the controls identified in FlaUInspect was pretty straightforward:

var app = Application.Attach("SportTracks.exe");
using (var automation = new UIA3Automation())
{
    var window = app.GetMainWindow(automation);
    var historyList = window
        .FindFirstDescendant(e => e.ByAutomationId("historyList"));
    var dateBanner = window
        .FindFirstDescendant(e => e.ByAutomationId("leftSideBanner"))
        .Patterns.LegacyIAccessible.Pattern;
    var weightInput = window
        .FindFirstDescendant(e => e.ByAutomationId("weightTextBox"))
        .Patterns.LegacyIAccessible.Pattern;
}

As you can see from the images above, the values I was interested in were only a part of the text displayed in the controls. It wasn't to difficult to parse them, though. The key part was using the correct CultureInfo. The application uses the default UI locale (Slovenian) so that's what I also used in my tool, i.e. I didn't specify any CultureInfo value anywhere:

var dateTimeString = dateBanner.Name.Value.Split(':')[0];
var weightString = weightInput.Name.Value.Split(' ')[0];
if (!weightString.Contains(":")) // is there any weight data?
{
    var weightEntry = new WeightEntry()
    {
        Date = DateTime.Parse(dateTimeString),
        Weight = decimal.Parse(weightString)
    };
}

The two controls I chose only showed the values for the selected item in the list view. This meant that I had to navigate through the full list view from top to bottom to read all the values. Fortunately, FlaUI can generate keyboard events as well. I only had to make sure that the correct control in SportTracks 3 had focus:

historyList.Focus();
Keyboard.Type(VirtualKeyShort.HOME);
DateTime? previousDate = null;
var weightEntries = new List<WeightEntry>();
while (true)
{
    Keyboard.Type(VirtualKeyShort.DOWN);
    historyList.Focus();
    // skipped data parsing
    if (weightEntry.Date == previousDate)
    {
        break;
    }
    weightEntries.Add(weightEntry);
    previousDate = weightEntry.Date;
    }
}

In the code above, there are a couple of things worth noticing:

  • Before the loop, I move to the top of the list view by pressing the Home key.
  • In the loop, I check that I'm always reading different data than in the previous iteration. If that's not true anymore it means that I've reached the last row.
  • Before parsing the data, I focus the list view once more. I do that to wait for the key press to be processed and the data in the UI to be updated before reading it. It might not be the best way to do it but it works.

When the loop ends, all the data is read. I saved it to a CSV file using CsvHelper:

public sealed class WeightEntryClassMap : ClassMap<WeightEntry>
{
    public WeightEntryClassMap()
    {
        Map(m => m.Date);
        Map(m => m.Weight);
    }
}

using (var writer = new StreamWriter("weight.csv"))
{
    using (var csv = new CsvWriter(writer))
    {
        csv.Configuration.CultureInfo = CultureInfo.InvariantCulture;
        csv.Configuration.HasHeaderRecord = true;
        csv.Configuration.Delimiter = ";";
        csv.Configuration.RegisterClassMap<WeightEntryClassMap>();
        var options = new TypeConverterOptions { Formats = new[] { "yyyy-MM-dd" } };
        csv.Configuration.TypeConverterOptionsCache.AddOptions<DateTime>(options);
        csv.WriteRecords(weightEntries);
    }
}

In case you're interested, you can find the full code on GitHub.

Copyright
Creative Commons License