msdn magazin
Deutsche Ausgabe
Ausgabe 4
Oktober-November
If you are interested in the complete sample code of my article you can download it at http://go.microsoft.com/?linkid=5470731 .
The central idea of my article is to show that WPF and XAML makes it possible to create graphically impressive applications not only in the web (using e. g. DHTML, AJAX, Flash, etc.) but also in good old full-client windows applications. To proof this I created a library which can be used to integrate maps in WPF-based business applications in minutes.
From SVG to XAML
The starting point is the question where to get the countries' coordinates in XAML format from. At the time when I was writing my article I was not able to find XAML-based geographical data about world maps in the WWW. I spent quite a while looking for such data but I did not succeed. However, I found a large number of maps in AI ( Adobe Illustrator ) or SVG ( Scaleable Vector Graphics ) format. Of course I could have used one of the existing converters for AI and SVG files (e. g. Michael Swanson's XAML exporter vor Adobe Illustrator). In my article I did not want to do that. I wanted to take a look behind the scenes and show how easy it is to create maps in XAML based on existing SVG resources.
Maps in SVG format are essentially a collection of paths representing coastlines and national borders. A path consists of a starting point and a series of line-operations. Here is a short sample of a SVG-file (you can save it as a .svg file and open it using a svg viewer application):
<svg>
<g>
<path id="Test" d="M20,20v50h50v-50h-50zM100,100l25,25l-25,25l-25,-25l25,-25z"/>
</g>
</svg>
Simlar to SVG XAML also knows a Path element. In XAML you can specify paths in two ways: Either you use the element-syntax (consists of XML-elements like PathGeometry , PathSegmentCollection , LineSegment , etc.) or the PathGeometry Markup Syntax . The latter is nearly equivalent to the path syntax of SVG; for our purposes we can use SVG's path syntax without any change in XAML. Here is the XAML code for the SVG sample shown above (you can view is using XamlPad ):
<Page xmlns="<A href="http://schemas.microsoft.com/winfx/2006/xaml/presentation">http://schemas.microsoft.com/winfx/2006/xaml/presentation</A>">
<Canvas>
<Path Data="M20,20v50h50v-50h-50zM100,100l25,25l-25,25l-25,-25l25,-25z"
Fill="Black" />
</Canvas>
</Page>
As far as paths are concerned SVG and XAML are quite similar. Therefore you can take any available SVG map file, write some XSLT scripts and convert it into a XAML map.
Building a reusable component for maps
As you saw above creating a XAML graphic for a world map is not hard. However, we want to do more. We want to create a reusable component for displaying world maps in WPF applications. Here is a short overview about the classes of our solution:
Country
Implements the logic that stands behind each country displayed on the map. Contains only business logic, lookless control! Derived from System.Windows.Shapes.Shape. Shape because a country is nothing else than a path and therefore Shape is the ideal base class for our purposes.
CountryGroup
Combines multiple Country objects so that they can react to events (e. g. mouse click) as a single entity. Can for example be used to build a world map on which the user can click on a continent.
WorldMap and WorldMapPanel
These two classes care for displaying the map on the screen. WorldMap is a so called Items Control , it can contain child elements (in our case Country objects). Additionally it offers the ItemsSource property through which it can be bound to a .NET collection or database resultset. For better understanding take a look at the following code snippet. It shows how WorldMap will be used together with Country :
<Page […]
xmlns:worldMap=
"clr-namespace:Cubido.WorldMapControls;assembly=Cubido.WorldMapControls">
…
<worldMap:WorldMap>
<worldMap:Country IsoCode="AT" />
<worldMap:Country IsoCode="DE" />
<worldMap:Country IsoCode="CH" />
</worldMap:WorldMap>
…
</Page>
As you can see using WorldMap is quite easy. Just add Country or CountryGroup objects to WorldMap 's Items collection and you are done!
WorldMapPanel is derived from System.Windows.Controls.Panel . It is responsible for arranging the countries on the screen.
Implementation of Country
The business logic behind Country takes any valid ISO code of a country and retrieves its name, its shape and its position on a world map. In my sample the countries' paths are defined in the component's generic.xaml file. Note that the coordinates of the countries' shapes have been transformed so that the left upper corner is at 0/0. The offsets of the left upper corner on a complete world map for each country is also stored in generic.xaml . Take a look at the following lines from this file:
<ResourceDictionary …>
…
<!-- Paths and offsets of countries… -->
<Path x:Key="AF" Data="M13,46v-1 … z" />
<Point x:Key="AF_Offset" X="3841" Y="1948" />
<Path x:Key="AL" Data="M24,36v2h-1 … z" />
<Point x:Key="AL_Offset" X="3183" Y="1881" />
…
</ResourceDictionary>
Note that Country makes use of dependency properties, a new concept in WPF. Dependency properties go beyond usual C# properties. The can be used for data binding, for animation, etc. It is recommended that you implement all properties of custom controls (like Country) as dependency properties. Here is a code snippet showing the implementation of an important dependency property in Country:
…
public class Country : Shape
{
private static ResourceDictionary pathResourceDict = null;
static Country()
{
// Get the resource dictionary of generic.xaml and store a reference to it
try
{
pathResourceDict = new ResourceDictionary();
pathResourceDict.Source = new
Uri(@"Cubido.WorldMapControls;;;Component\themes/generic.xaml",
System.UriKind.Relative);
}
catch (Exception ex)
{
throw new ApplicationException(
"Could not load resource dictionary with paths for coutries.", ex);
}
}
…
public static readonly DependencyProperty IsoCodeProperty =
DependencyProperty.Register("IsoCode", typeof(String), typeof(Country),
new UIPropertyMetadata(new PropertyChangedCallback(OnIsoCodeInvalidated)));
public String IsoCode
{
get { return (String)GetValue(IsoCodeProperty); }
set { SetValue(IsoCodeProperty, value); }
}
private static void OnIsoCodeInvalidated(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
// extract country and new iso code from parameters.
Country country = (Country)d;
String newIsoCode = (String)e.NewValue;
// check if we know this iso code (i.e. we have a path for this iso code in our
// generic.xaml file).
if (!pathResourceDict.Contains(newIsoCode))
throw new ApplicationException(String.Format("Unknown country \"{0}\".",
newIsoCode));
// set the country's path to the path from the generic.xaml file
country.Path = ((Path)pathResourceDict[newIsoCode]).Data;
// set the country's position to the position from the generic.xaml file
country.Position = ((Point)pathResourceDict[newIsoCode + "_Offset"]);
// read the description for the country from the resource file
country.Description =
Cubido.WorldMapControls.Properties.Resources.ResourceManager.GetString(
newIsoCode);
}
…
}
The implementation of CountryGroup is quite simple. Its main job is to combine the paths of multiple countries taking their relative position on the world map into account:
GeometryGroup resultingGeometry = new GeometryGroup();
foreach (Country country in countryGroup.Items)
{
PathGeometry countryGeometry = new PathGeometry();
countryGeometry.AddGeometry(country.Path);
TranslateTransform transform = new TranslateTransform( country.Position.X
- offset.X, country.Position.Y - offset.Y);
countryGeometry.Transform = transform;
resultingGeometry.Children.Add(countryGeometry);
}
Implementation of WorldMap and WorldMapPanel
WorldMap is just the container for the countries that have to be displayed on the screen. It is important that WorldMap has a template assigned in generic.xaml:
<Style TargetType="{x:Type local:WorldMap}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:WorldMap}">
<local:WorldMapPanel IsItemsHost="True" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
An application that uses our WorldMap control could overwrite this template!
As you can see in the template above WorldMapPanel is used to display the countries. Note that WorldMapPanel has the IsItemsHost property set to true. With this WorldMapPanel can access the Items collection of WorldMap. The important methods of WorldMapPanel are MeasureOverride and ArrangeOverride. They care for arranging the countries in the control.
How the component can be used
In our sample the general appearance of a country is defined in App.xaml:
<Application x:Class="WorldMapControlsTester.App"
xmlns:worldMap=
"clr-namespace:Cubido.WorldMapControls;assembly=Cubido.WorldMapControls" >
<Application.Resources>
…
<Style TargetType="{x:Type worldMap:Country}">
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="Fill" Value="DarkGreen" />
<Setter Property="Stroke" Value="LightGray" />
<Setter Property="StrokeThickness" Value="1" />
</Style>
…
</Application>
GeoGallery.xaml shows various ways how to embed maps in WPF windows. Even mouse-over-effects for Country- and CountryGroup-objects are included.
<Page x:Class="WorldMapControlsTester.GeoGallery"
xmlns:worldMap=
"clr-namespace:Cubido.WorldMapControls;assembly=Cubido.WorldMapControls">
…
<Style BasedOn="{StaticResource {x:Type worldMap:Country}}"
TargetType="{x:Type worldMap:Country}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="False">
<Setter Property="Opacity" Value="0.5" />
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Opacity" Value="1" />
</Trigger>
</Style.Triggers>
</Style>
…
<Border Grid.Column="0" Grid.Row="0" Style="{StaticResource RoundedBorder}">
<Viewbox SnapsToDevicePixels="True">
<worldMap:WorldMap>
<worldMap:Country IsoCode="DE" />
<worldMap:Country IsoCode="AT" />
<worldMap:Country IsoCode="CH" />
</worldMap:WorldMap>
</Viewbox>
</Border>
…
</Page>
>

Of course our component can be used in interactive designer, too.
The cool stuff
MapBuilder.xaml demonstrates how a collection of Country-objects can be bound to a listbox. The WorldMap-control is then bound to the SelectedItems property of the listbox. Therefore you can select some countries in the listbox and WorldMap will draw them for you - without a single line of code!
<Viewbox>
<worldMap:WorldMap Name="Map" ItemsSource="{Binding ElementName=CountryList,
Path=SelectedItems}" />
</Viewbox>
ColoredCountries.xaml uses data binding and a converter to fill the countries on the map depending on a value. In our case the value is the number of years that each country is in the European Union (EU). You could also visualize your company's revenue per country using exactly the same technique. Here is the XAML code that shows how to use a converter to fill each country:
<worldMap:WorldMap>
<worldMap:WorldMap.Resources>
<Style TargetType="{x:Type worldMap:Country}">
<Setter Property="Fill" Value="{Binding RelativeSource={RelativeSource Self},
Path=IsoCode, Converter={StaticResource countryConverter}}"/>
</Style>
</worldMap:WorldMap.Resources>
<worldMap:Country IsoCode="NO"/>
<worldMap:Country IsoCode="IS"/>
…
</worldMap:WorldMap>
In our case the converter converts a country's ISO code into the appropriate Brush that should be used to fill the country:
[ValueConversion(typeof(String), typeof(Brush))]
public class EuMemberToBrushConverter : IValueConverter
{
private static Dictionary membersEu = new Dictionary();
static EuMemberToBrushConverter()
{
// Fill the membersEu collection (this data could also come from a database)
membersEu.Add("BE", 1952);
membersEu.Add("DE", 1952);
membersEu.Add("FR", 1952);
…
}
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
// check input parameters
if (!(value is String) || targetType != typeof(Brush))
throw new ApplicationException("Either value or target type are not supported by EuMemberToBrushConverter!");
// look up joining year
int memberSince;
if (!membersEu.TryGetValue((string)value, out memberSince))
// if the country hasn't joined use 0 as the joining year
memberSince = 0;
// Check if an appropriate brush has been defined in App.xaml
if (!Application.Current.Resources.Contains(memberSince.ToString("0000")))
throw new ApplicationException(String.Format("Unknown year \"{0}\".", memberSince.ToString("0000")));
// Return the appropriate brush
return (Brush)Application.Current.Resources[memberSince.ToString("0000")];
}
…
}
The brushes are defined in the App.xaml file:
<Application x:Class="WorldMapControlsTester.App"
xmlns:worldMap=
"clr-namespace:Cubido.WorldMapControls;assembly=Cubido.WorldMapControls" >
<Application.Resources>
…
<SolidColorBrush x:Key="0000" Color="White" />
<SolidColorBrush x:Key="1952" Color="#ff328e4a" />
<SolidColorBrush x:Key="1973" Color="#ff4ea464" />
<SolidColorBrush x:Key="1981" Color="#ff70ba83" />
<SolidColorBrush x:Key="1986" Color="#ff70ba83" />
<SolidColorBrush x:Key="1995" Color="#ff99d1a8" />
<SolidColorBrush x:Key="2004" Color="#ffc8e8d1" />
…
</Application>
Summary
Windows applications are back! Cool graphical effects are no longer the exclusive domain of Flash or DHTML-developers. As a result all companies that develop software should look out for partners who focus on UI- and graphical design. I future development projects you will need their expertise!
Links