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
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.
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.
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 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
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
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:
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.
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
TelemetryChannelView have their Data Contexts set to their ViewModels upon initialisation, which are described in the section below.
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).
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.
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
TelemetryChannelView then has a binding to the
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.
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
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.
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.
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.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.
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
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
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
Data Context Changed Handler
In order to register change events on the Data Context within the Telemetry Application, Jeremy Likeness'
DataContextChangedHelper class was used.
The styles, brushes, and custom templates for the charts are all defined in the App.xaml file.
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.