Introduction

About a month ago, I was using the MoTeC i2 Data Analysis software to look through some telemetry data that I had exported from a popular racing simulator game. Although MoTeC is feature rich and a great tool for motor racing professionals and enthusiasts, I had the idea to create a simplified, styled version using Silverlight. I had been looking for an interesting idea to test out my newfound Silverlight skills for a while, and this seemed like the perfect opportunity.

Before reading the rest of the article, you may like to view the sample application, located at the bottom of this post.

Using the Code

Architecture

The application as it is presented here is not the first version that I created. I had worked on an initial version before this which did not use the MVVM pattern. As a result of this, making changes were difficult, duplicate strings representing the same series name were littered throughout the application, the code behind was duplicated for each of the three chart components, and performance was far less than optimal. In my defense, the application I first envisioned was a single, simple chart with one series and no further information. But when I started to expand the application, all of these issues arose and started to get progressively worse.

So I decided to start again, switching to a cleaner, more manageable architecture that made use of data binding and limited code behind. The code is now structured using the MVVM architectural pattern . This should hopefully be clear by examining the solution and source code provided alongside the article.

Model

When creating the model, the aim was for it to be as reusable as possible. Originally I had dependencies on the Visiblox Charting Component (see the Charts section). However, these were removed, in favour of a model with simple lists of values. It would be the responsibility of the ViewModel to convert these values into the appropriate format before they were displayed by the charts in the view.

The model in the solution provided is in the TelemetryData Project. This project reads and parses the .csv values exported from MoTeC. TelemetryDataProvider implements ITelemetryDataProvider and creates and populates a list of TelemetrySeries in its constructor. One of the main goals when creating the TelemetryDataProvider was for it to be as generic as possible. Users of the model can therefore obtain a list of strings representing the names of all the series by reading the SeriesList property. This means the string representations of the series names do not need to be replicated anywhere else in the code. Users can also obtain a particular telemetry series by calling the FindSeriesFromName method and passing a string representation of the TelemetrySeries.

A TelemetrySeries represents a series of TimestampedDataPoints along with the Name of the series and the index of the column in the csv file that the series data is stored in.

The next challenge was how to populate the SeriesList in TelemetryDataProvider. In the ReadFile(StreamReader) method, the provider creates a TelemetrySeriesDictionaryPopulator, passing its constructor the location of the .csv file and the SeriesList. For every valid line in the .csv file, the TelemetrySeriesDictionaryPopulator will call its FillFromCSV method, which will add the relevant values to each AllSeries entry, based on the entry's IndexInCSV value.

One of my favourite things about the way TelemetryDataProvider is implemented is how easy it is to add new Series data to the application. To add a TelemetrySeries to the application, only one line of code is required:

_allSeries.Add(new TelemetrySeries("Series_Name", IndexInCSVFile));

This will add a series to the application, and no other references in the project need to be updated for the series to be visible. The FillFromCSV method mentioned earlier will also need no modifications. I chose to display the 10 data sets which I saw to be most appropriate, but this is easily configurable through TelemetryDataProvider. There are 115 columns to choose from in the exported MoTeC file.

View

The View presents the data provided by the View Model. Both the View and View Model are located in the TelemetryPrototype project. There is very little code behind in the .xaml view files. The only code that is present is strictly necessary and could not be easily achieved through XAML. TelemetryChannelCollectionView for example, rebuilds the ChannelView grid depending on how many ChannelViews are currently being displayed. Through reducing the code behind, I was able to ensure that the View and the ViewModel became almost independent of each other. This allows other Views to be attached to the same ViewModel in future, without any loss of functionality.

The views are attached to the view model through the use of .xaml bindings, which update automatically when the respective property in the ViewModel changes.The TelemetryView, TelemetryChannelCollectionView and TelemetryChannelView have their Data Contexts set to their ViewModels upon initialisation, which are described in the section below.

ViewModel

I wanted all of the logic of the application to be contained in the ViewModel and not inside of the Model or the View. The ViewModel therefore abstracts from the View and provides a binding between the View and the Model. Any commands from the View are simply passed to the ViewModel via an ICommand object, which will cause some property on the ViewModel to update. Through bindings, this will then update the view accordingly.

TelemetryViewModel provides the top level data context, and contains a TelemetryChannelCollectionViewModel which stores and displays the ChannelViewModels currently added to the application. It also holds properties for the custom controls in the bottom panel, which are updated in ViewModelBase as well as Commands for playback speed, restart, full data, play/pause, import data and add chart.

Another of its main responsibilities is to update the charts during live playback of the data. The UpdateChannelViewModels method is called by a dispatcher timer every 50ms.It calculates the time elapsed since the last update, and then calls updateChart on the ChannelViewModels in the ChannelViewModel collection. The widgets are also updated based on the counter. If the chart is paused, the widgets are updated based on the selected index of the top chart (which should be synchronised with the other charts in the application).

The TelemetryChannelViewModel contains the properties and values required to display a chart. It provides the data context for a TelemetryChannelView and contains two data series to display on the chart from that view. It also contains behaviours and operations to modify, remove and add data series to the chart.

Charts

Custom Zoom Behaviour

To add the charts to the application, I used Visiblox Silverlight Charts. I chose to use Visiblox as I was looking for a high performance charting component. I knew I would be plotting a lot of data on the charts and the performance comparisons on Colin Eberhardt's blog here, as well as opinions on Stack Overflow led me to select Visiblox. I found the examples to be a great help when setting up the charts for the application.

Sticking to the MVVM architecture for the charts, the data for the charts comes from the Model, is converted by the ViewModel and displayed by the View, using the following method. Each chart is populated by its TelemetryChannelViewModel, which obtains data for the two series from the GetChannelData method in TelemetryViewModel. This method retrieves the relevant TelemetrySeries from the Data Provider in the Model, and calls TelemetryDataToVisibloxDataSeries, which adds each point in the TelemetrySeries to a Visiblox DataSeries type. The Visiblox LineSeries' DataSeries in TelemetryChannelView then has a binding to the TelemetryChannelViewModel's LivePrimaryChartDataSeries/LiveSecondaryChartDataSeries property.

<visi:LineSeries DataSeries="{Binding Path=LivePrimaryChartDataSeries}" ...

To keep the data series on each of the charts in context, I decided it would be a good idea to restrict the zoom behaviours to the X-Axis. There is no way to restrict the standard Visiblox ZoomBehaviour to the X-Axis, so I had to define a new one. XAxisZoomBehaviour implements the Visiblox BehaviourBase abstract Class. The MouseLeftButtonDown method sets a zoom rectangle to the height of the chart, and when the MouseMove method is called, the width of this rectangle is modified to the value of the mouse position minus the start position. The method also checks if the rectangle covers a large enough area on the chart to allow a zoom. By checking this, it stops the zoom area from becoming too small.If the mouse is released and the zoom area is large enough, the ZoomTo method is called.

Storyboard sb = new Storyboard();
DoubleAnimation b = new DoubleAnimation() { From = Chart.XAxis.Zoom.Scale, To = zoom.Scale };
b.Duration = new Duration(new TimeSpan(0, 0, 0, 0, milliseconds));
sb.Children.Add(b);
Storyboard.SetTarget(b, Chart.XAxis.Zoom);
Storyboard.SetTargetProperty(b, new PropertyPath("(Scale)"));

DoubleAnimation b2 = new DoubleAnimation() { From = Chart.XAxis.Zoom.Offset, To = zoom.Offset };
b2.Duration = new Duration(new TimeSpan(0, 0, 0, 0, milliseconds));
sb.Children.Add(b2);
Storyboard.SetTarget(b2, Chart.XAxis.Zoom);
Storyboard.SetTargetProperty(b2, new PropertyPath("(Offset)"));

This code from ZoomTo creates two double animations, one for the Scale, and one for the Offset of the zoom. This is the code which actually modifies the offset and scale of the chart when the storyboard sb begins.

Controls

When the charts were added to the application, I still felt like it was missing something. I had the idea to add widget-style custom controls below the chart to add more interest and interactivity. Each of the controls is defined in its own class in the TelemetryPrototype.controls namespace within the controls folder. They all have at least one dependency property and subscribe to onPropertyChanged events, which allows the controls to automatically update whenever one of their dependency properties is modified. The layout and elements of all the controls are defined in the Generic.xaml file.

ThrottleBrakeControl.cs

Dependency Properties:Throttle, Brake

The throttle Control is a three column grid, with a throttle indicator on the left, a brake indicator on the right and labels in the centre. The indicators are created by displaying a rectangle inside of a border. The rectangles are styled appropriately (green for throttle and red for brake) and their height is bound to their respective dependency property.

CarLocationControl.cs

Dependency Properties:Position

The car location control shows where the car currently is on the track.The track itself was created in Microsoft Expression Blend 4 using a path. The Microsoft.Expression.Controls and Microsoft.Expression.Drawing .dll files are therefore included in the references of this project. A PathListBox then binds its LayoutPath SourceElement to this path. The car itself is simply an ellipse inside of this PathListBox. By binding the Start property of the LayoutPath to the Position dependency property, the ellipse will move around the path. Position must therefore be a percentage value. To calculate the percentage completion of a lap, it is necessary to have the start and end distance, which are calculated in the TelemetryDataProvider.cs when the data is first read into the application.

GForceControl.cs

Dependency Properties:Lateral, Long

There is slightly more code for the G-Force control and no bindings for the values on the chart, as these are set manually whenever a property changes in the OnLateralPropertyChanged and OnLongPropertyChanged methods. Within the OnApplyTemplate() method of the control, a DataPoint is created and added to a line series. This line series is then added to the chart.

The G-Force chart has also been retemplated to remove the border and to hide any axis values. The template is contained in App.xaml, with a target type of Chart. It has a key value of Gchart, which is referenced in the control's layout in the Generic.xaml by "Template="{StaticResource Gchart}".

SpeedometerControl.cs

The speedometer control is a modified version of the gauge control developed by Colin Eberhardt as part of a blog post entitled "Developing a (very) Lookless Silverlight Radial Gauge Control". The only modifications from the original source code from the article are the styles, layout and namespace declarations. The style is defined at the bottom of the generic.xaml file in the Themes folder of the SilverTrack project.

Util/Other Classes

Data Context Changed Handler

In order to register change events on the Data Context within the Telemetry Application, Jeremy Likeness' DataContextChangedHelper class was used.

App.xaml

The styles, brushes, and custom templates for the charts are all defined in the App.xaml file.

Generic.xaml

The layout for the controls and the templates are defined in the Generic.xaml file.

Points of Interest

This was the first time I had used the MVVM architecture, and I will certainly use it again in the future. I had developed a prototype version of this application without MVVM. I had planned on building on this to create the final version, but found that it was much harder to modify and add additional functionality to. Mixing UI code with Logic code is bad practice, and it is something I will now be keen to avoid in the future.

It was also the first time I had used a charting component in Silverlight, other than the toolkit version. I decided on Visiblox after reading this article from Colin Eberhardt and others like it, as well as reading charting recommendations from members of Stack Overflow.

Exporting Data From MoTeC

Using the import data button in the top left of the application, it is possible to load your own MoTeC data into SilverTrack. If you do not wish to use the sample files provided, and have your own way to import telemetry data to MoTeC, exporting to the .csv format used by the application is relatively straightforward:

  • Open a Log File, then select Export Data...
  • Set current range to a single lap, and output format as CSV.
  • Performance is optimal with sample rate of 20Hz.
  • Include Time Stamp and Distance Data.

NOTE: The car location control will only operate correctly if you load data from the Calder Road Course.

Get Microsoft Silverlight
Silverlight

Download