Last time: so close, yet so far:
This series appears to have become a trilogy in four parts – at the end of the last episode everything worked, save for tombstoning, although we explicitly wrote test code for that. The app tombstoned, partially - two things were apparently missing:
- The route
- The locations to and from the route should run.
Making the tests fail
An important step when you have a bug in an app that passes all tests, is to make a test that fails because of the bug. In that case, you have reproduced the problem, and can start on fixing the bug. Logically, the bug is fixed when the test no longer fails – and should anyone start to mess around with your code an re-introduce the bug, the test will fail again, indicating something has gone wrong before you even ship. Your tests have become a smoke detector ;-)
Anyway, we observe there is no selected location, nor routes or waypoints after tombstoning. When we look at the comprehensive test for RoutingViewModel written in the 2nd post of this series, we see the following Assert statement with regard to the retrieved viewmodel:
Assert.IsTrue(vm.RouteCoordinates.Any()); Assert.IsTrue(vm.Maneuvers.Any()); Assert.IsTrue(retrievedVm.RouteCoordinates.Count == vm.RouteCoordinates.Count); Assert.IsTrue(retrievedVm.FromViewModel.SearchText == "Springerstraat Amersfoort Netherlands"); Assert.IsTrue(retrievedVm.Maneuvers.Count == vm.Maneuvers.Count);
Shockingly, we learn two things:
- We indeed don’t test the presence of either SelectedLocation or either FromViewModel and ToViewModel.
- We do test the presence of both RouteCoordinates and Maneuvers. So our viewmodel works in that respect – so the error must have to do something with data binding.
First, we add test code for SelectedLocation
Assert.IsNotNull(retrievedVm.FromViewModel.SelectedLocation); Assert.IsNotNull(retrievedVm.ToViewModel.SelectedLocation);
And sure enough:
Annoyingly, this does only say which test failed, but not what exactly failed in this test. You can of course follow the purist route and write a separate test method for every assert, or just be lazy like me and use the overload every Assert method has:
Assert.IsNotNull(retrievedVm.FromViewModel.SelectedLocation, "No FromViewModel.SelectedLocation tombstoned"); Assert.IsNotNull(retrievedVm.ToViewModel.SelectedLocation, "No ToViewModel.SelectedLocation tombstoned");
And there we are. A clear error message. There is no SelectedLocation after tombstoning
Fixing the SelectedLocation bug aka serialization under the hood
Let me introduce you to the wonderful world of serialization. Much as we move into the world of asynchronous and parallel programming, serialization is essentially a sequential process. First property A is written, then property B. When something is deserialized, things are also read from storage in a particular order, i.e. the order they are written.
Let’s get back to the RoutingViewModel. I’ve abbreviated the property implementation a bit, apart from the Model. There we see the following code:
public ManeuverViewModel SelectedManeuver public GeocodeViewModel ToViewModel public GeocodeViewModel FromViewModel public ObservableCollection<RouteGeometryViewModel> RouteCoordinates { get; set; } public ObservableCollection<ManeuverViewModel> Maneuvers { get; set; } private NavigationModel model; public NavigationModel Model { get { return model; } set { model = value; if (model != null) { ToViewModel = new GeocodeViewModel(model.To) { Name = "To" }; FromViewModel = new GeocodeViewModel(model.From) { Name = "From" }; } } }
Now let’s assume, for a moment, serialization simply reflects all public properties with both getter and setter, and writes them one by one to storage – and reads them in the same order. In no particular order – if this were a database that most probably means the order the in which records were put in. Could it be reflection works the same way? But then, deserializing would mean that first the SelectedManeuver would be deserialized, then ToViewModel and FromViewModeland, then the RouteCoordinates, then the Maneuvers, and finally the model. But smart Mr Me has implemented this clever method of initializing FromViewModel and ToViewModel upon calling of the setter. So whatever was deserialized into FromViewModel and ToViewModel gets overwritten after Model is deserialized!
So let’s make the Model property the very first property of the viewmodel, right after the constructors, run the test and see what happens…
You can imagine with this kind of arcane stuff going on behind the curtains, (unit) test code can be a really great tool to track and fix this kind of obscure errors – and make sure they never, ever occur suddenly again, just because someone changed the order in the way things are implemented!
Fixing the MapShapeDrawBehavior bug
This is a bit of odd one – apparently the developer that made the MapShapeDrawBehavior – a knucklehead who names himself “LocalJoost” ;-) - has made an error implementing data binding – while he implemented listening to all the collection events correctly, he never apparently anticipated the initial collection might have some values before data binding ensued. The advantage of open source is that we actually can see this. So, we either have to copy MapShapeDrawBehavior ‘s code and make a manual fix to make sure some event occurs that makes it go draw the stuff – or implement a band-aid that does not interfere with the existing code.
I pulled out the band, aid, and made the following class:
using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; namespace Wp7nl.Utilities { public class ResettableObservableCollection<T> : ObservableCollection<T> { public ResettableObservableCollection() { } public ResettableObservableCollection(List<T> list) : base(list) { } public ResettableObservableCollection(IEnumerable<T> list) : base(list) { } public void ForceReset() { OnCollectionChanged( new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } } }
I changed both RouteCoordinates and Maneuvers in RoutingViewmodel into ResettableObservableCollection, added the following command to RoutingViewmodel:
[DoNotSerialize] public ICommand MapLoadedCommand { get { return new RelayCommand( ()=> { RouteCoordinates.ForceReset(); Maneuvers.ForceReset(); }); } }
and finally, the following piece of XAML to the map:
<i:Interaction.Triggers> <i:EventTrigger EventName="Loaded"> <Command:EventToCommand Command="{Binding MapLoadedCommand}"/> </i:EventTrigger> </i:Interaction.Triggers
This will fire the command directly after the map has loaded. Data binding has already occurred then. And sure enough, even after restarting the app, everything is reloaded from storage. The behavior is tricked into believing it should draw it’s stuff. Now I only need to publish my app, and inform this LocalJoost character that his library contains bugs and if he can fix them ASAP, thank you very much.
Concluding remarks
This series did not only show you the basics of Windows Phone 8 navigation, but also how to develop geo-applications on Windows Phone 8 using unit/integration test to explore and test functionality, as well as using test as a way to hunt down and fix bugs. It also showed that if your test are incomplete, you might get bitten. And finally it showed you that, in the end, you still need to test manually to catch bugs that are caused by dunderheads making errors in their published code ;-)
The final solution can be found here,
1 comment:
Nice series. This is exactly the kind of stuff that is (part of) the reason why you are not only a Windows Phone MVP but also a Nokia Developer Champion.
Post a Comment