Overview
I’ve been looking at the Bing Maps (Virtual Earth) Silverlight Map Control and thinking about how to use it in a Silverlight application that implements the Model-View-ViewModel pattern. If you’re not familiar with this pattern, check out Shawn Wildermuth’s MSDN Article Model-View-ViewModel in Silverlight 2 Apps.
I’ve created a simple proof of concept application that implements the pattern with two Views: a Map based View and a Data Grid based View. The application displays a map of well known surf spots in the Los Angeles area, along with some basic information about each spot in an accompanying data grid.
[more]
There is no live example posted for this one, as Microsoft has not yet released the Silverlight Map Control with a “Go Live” license. But you can download source code from here to try it out on your own system.
Synchronizing Multiple Views with the ViewModel
Both Views work off the same ViewModel and stay synchronized via events that the ViewModel fires back to the Views after user gestures.
There are two main types of user gestures where this comes into play:
- When the map is first loaded or the user pan/zooms the map, the ViewModel is notified of the new map extent. The ViewModel then loads surf spots that fall within the new Map Extent from a Repository. An event is fired back to both Views and they update themselves to display only the surf spots that were loaded based on the new map extent.
- When a user clicks on a surf spot in either the map or the list, the ViewModel is notified of the newly selected surf spot. An event is fired back to both views and they update themselves to display the selected spot.
In the next section, I’ll take a closer look at the first example.
Loading Surf Spots from the Repository
The xaml file for the SurfSpotsMapView markup contains just a Map with a specified ViewChangeStart event.
SurfSpotsMapView.xaml
<UserControl x:Class="Map.Client.Views.SurfSpotsMapView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:m="clr-namespace:Microsoft.VirtualEarth.MapControl;assembly=Microsoft.VirtualEarth.MapControl"
mc:Ignorable="d"
d:DesignWidth="500"
d:DesignHeight="500">
<Grid x:Name="LayoutRoot" Background="White">
<m:Map Name="Map1" ViewChangeStart="Map_ViewChangeStart">
</m:Map>
</Grid>
</UserControl>
The ViewChangeStart event fires whenever the map extent is about to change. This event generally coincides with the beginning of the animation to the new map extent. This provides an excellent opportunity to start loading the surf spots while the map is doing its animation thing.
private void Map_ViewChangeStart(object sender, MapEventArgs e)
{
MapViewSpecification mvs = Map1.TargetView;
LocationRect bounds = Map1.GetBoundingRectangle(mvs);
viewModel.LoadSurfSpots(bounds.North, bounds.West, bounds.South, bounds.East);
}
In the Map_ViewChangeStart event, the map’s TargetView property contains the MapViewSpecification of the map extent that the map will be animating (panning or zooming) towards. The method examines the bounding rectangle of the target MapViewSpecification and asks the SurfSpotsViewModel to load the surf spots within that map extent via the LoadSurfSpots method.
Notice that the method just asks the ViewModel to load the surf spots, but it doesn’t expect an immediate response. Later, when the ViewModel has finished loading the data, it fires off an event telling both Views to refresh themselves with the new data. This allows for data to be loaded asynchronously, typically from a remote web service.
The SurfSpotsMapView class has a ViewModel property setter where we wire up the events that are expected to be fired from the ViewModel. This is called when the View is first created:
public SurfSpotsViewModel ViewModel
{
get { return viewModel; }
set
{
viewModel = value;
// Wire up the View Model
viewModel.SurfSpotsLoaded += new EventHandler(SurfSpotsLoadedHandler);
viewModel.SurfSpotsLoadError += new EventHandler(SurfSpotsLoadErrorHandler);
viewModel.SelectedSurfSpotChanged += new EventHandler(SelectedSurfSpotChangedHandler);
}
}
Like the ViewModel, the repository is also asynchronous in nature and the ViewModel’s LoadSurfSpots method simply delegates to an analogous method on the repository. Consider the following ISurfSpotsRespository interface:
public interface ISurfSpotRepository
{
// Load all surf spots
void LoadSurfSpots();
// Load surf spots within the specified extent
void LoadSurfSpots(double north, double west, double south, double east);
// Event to fire when surf spots have been loaded
event EventHandler<SurfSpotEventArgs> SurfSpotLoadingComplete;
// Event to fire when there is an error loading surf spots
event EventHandler<SurfSpotErrorEventArgs> SurfSpotLoadingError;
}
When the repository has finished loading the data, it fires the SurfSpotLoadingComplete event back to the ViewModel, which in turn fires its own event back to any listening Views.
To keep the example simple, ISurfSpotRespository is implemented as a mock in-memory repository called InMemorySurfSpotRepository. See the sample code download for the complete implementation. Note that a real web application would most likely load the data from a remote web service. See Johannes Kebeck’s Blog Post Database Connections with the Virtual Earth Silverlight MapControl CTP for a good example of how to do that.
Once the data loading is complete, the SurfSpotsLoadedHandler method of the SurfSpotsMapView class handles adding the new surf spots to the map:
private void SurfSpotsLoadedHandler(object sender, EventArgs e)
{
surfSpotsLayer.RemoveAllChildren();
foreach (SurfSpot s in viewModel.SurfSpots)
{
Ellipse point = new Ellipse()
{
Width = 10,
Height = 10,
Fill = new SolidColorBrush(Colors.Red),
Opacity = 0.65,
Tag = s
};
point.MouseLeftButtonDown += new MouseButtonEventHandler(
SurfSpotPoint_MouseLeftButtonDown);
MapLayer.SetMapPosition(point, new Location(s.Latitude, s.Longitude));
ToolTipService.SetToolTip(point, s.Name);
surfSpotsLayer.Children.Add(point);
}
}
In the SurfSpotsListView, keeping the list updated is even easier. The SurfSpots property of the ViewModel is an ObservableCollection<SurfSpot>, so we just need to set the ItemsSource property of inner DataGrid and it will keep itself updated automatically. This happens in the ViewModel property setter for the SurfSpotsListView:
public SurfSpotsViewModel ViewModel
{
get { return viewModel; }
set
{
viewModel = value;
// Wire up the View Model
viewModel.SelectedSurfSpotChanged += new EventHandler(SelectedSurfSpotChangedHandler);
viewModel.SurfSpotsLoaded += new EventHandler(SurfSpotsLoaded);
viewModel.SurfSpotsLoadError += new EventHandler(SurfSpotsLoadErrorHandler);
// Set the items source for the data grid. Since it is an
// ObservableCollection<> we don't need to handle the SurfSpotsLoaded
// event. The grid view will automatically keep itself updated.
DataGrid1.ItemsSource = viewModel.SurfSpots;
}
}
Unit Tests
Of course, one of the main reasons for using the Model-View-ViewModel pattern is testability. I’ve included a simple unit test of the View Model class in the Tests project to illustrate the concept. This test is modeled after the tests found in Shawn Wildermuth’s sample code. I didn’t create a full array of tests, as it wasn’t necessary for the proof of concept. I’ll leave that as an exercise for the reader.
Wrap Up
This post has presented a proof of concept sample application using the Model-View-ViewModel pattern with the Microsoft Bing Maps (Virtual Earth) Silverlight Map Control CTP. ViewModel synchronization with multiple Views is demonstrated in the sample. While we covered some of the implementation details in this post, there is more to be found in the downloadable sample – so be sure to check it out!
If you would be interested in a follow up post covering some of the additional implementation details, please let me know in the comments.
Hope this helps!
Resources