Blog

View Blog

Oct 6

Written by: Rainer Stropek
10/6/2006


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

Tags:

8 comments so far...

Re: My msdn article "The World in XAML"

This is awesome! But it keep crashing in my environment, I am guessing I must have something wrong with my system:
Windows XP SP2
.NET 3.0 RC1, Orcas RC1, Vista SDK

Thanks

By Anonymous on   3/7/2008

Compiler Error: Cannot add content to object...

When I compile the Tester application, I get the following error:
Fehler 2 Cannot add content to object of type 'System.Windows.Navigation.NavigationWindow'. Line 15 Position 3. D:\PvSrc\vs05.net\ms.net 3.0\Sample Code\WorldMapControlsTester\MainWindow.xaml 15 3 WorldMapControlsTester
What's wrong?

By Anonymous on   3/7/2008

Re: My msdn article "The World in XAML"

Yep: Error 1 Cannot add content to object of type 'System.Windows.Navigation.NavigationWindow'. Line 15 Position 3. ...\MainWindow.xaml

By Anonymous on   3/7/2008

Re: My msdn article "The World in XAML"

The download delivers a corrupt zip file. Can this be corrected?

By Anonymous on   3/7/2008

Download Link Does Not Work

This is a great article but the link http://go.microsoft.com/?linkid=5470731 does not work. I would love to look at the complete source if anyone can point out where it is.

By Anonymous on   3/7/2008

Re: My msdn article "The World in XAML"

This is great article. so Plz add it in Codeproject.com

Thanks

By Anonymous on   9/22/2008

Re: My msdn article "The World in XAML"

Hi !

I'm interested by your article but i tested the application and KENYA is missing , do you have the path of this coutry or the reference of the map you used

Thank you

By Anonymous on   2/7/2009

Additional Features

Thanks for this great article, very useful to reuse this component in WPF apps.
It'd be very interesting too to implement additional features like zooming and panning to the control.
While zooming is quite easy, as you only have to add a ScaleTransform to zoom into any part of the control, is there a way to pan over the control in an easy way without re-painting every time the control when you move the x,y coordinates on, for example, a canvas containing the Map? Do you know about any resource that can be a start point to implement this feature?

Thanks

By JMAmor on   4/29/2009

Your name:
Title:
Comment:
Security Code
Enter the code shown above in the box below
Add Comment    Cancel  

Newsletter

Sie möchten im Newsletter über aktuelle technische Entwicklungen und Neuigkeiten rund um cubido informiert werden?

Newsletter abonnieren ...

Blog