Start template for Xaml MapControl

 The Xaml.MapControl comes with a nice example that sow different options. This can be found in the examples directory at https://github.com/ClemensFischer/XAML-Map-Control.

This post is meant to show a more simple solution. And my hope is that I can use this as a start template.

The goal is to end up with this view:


And this is the steps to get this going:


Create new project and setup 

Create a new project in Visual Studio. When writing this I selected the tradistional .Net Framework 4.8.

Then I installed the Map Control by the package manager console:

    PM> Install-Package XAML.MapControl

For this template I didn't use any MVVM Frameworks. This is to make less depentant on third-party solutions.

I used the base MVVM template I wrote aboute in: Absolute Mini MVVM

Copy those two classes to the folder explained below.

 

Then I made the following folders in Visual Studio:

  • Models
  • Views
  • ViewModels
  • AbsoluteMiniMvvm
  • Extensions


For this solution I have opted to have thing startup as standard Visual Studio.

So the App.xaml have a StartupUri="MainWindow.xaml".

And the MainWindow get a dataContext to point to the viewModel.

Of couse: This is done for simlicity and ease of finding things... For a real life solution you can use a more robust approach.


Setting up the content

In folder ViewModels, make a new file "MainWindowViewModel". And set this to inherit the "AbsoluteMiniMvvm.PropertyChangedBase". And here's the code including this example:

using System.Collections.ObjectModel;
using System.Linq;

namespace XamlMapControl.ShowShapes.ViewModels
{
    public class MainWindowViewModel : AbsoluteMiniMvvm.PropertyChangedBase
    {

        public MainWindowViewModel()
        {
            // Get the parks from classes in "Models"

            // TODO: Change this to the appropriate application structure (either by framework or your own)

            var pl = new Models.ParkListModel();
            Parks = pl.Parks;
            SelectedPark = Parks.Last();
        }

        private Models.ParkModel selectedPark;
        public Models.ParkModel SelectedPark
        {
            get { return selectedPark; }
            set {
                if (selectedPark == value)
                    return;
                selectedPark = value; 
                RaisePropertyChanged(); 
            }
        }

        private ObservableCollection<Models.ParkModel> parks = new ObservableCollection<Models.ParkModel>();
        public ObservableCollection<Models.ParkModel> Parks
        {
            get { return parks; }
            set {
                    if (parks != null && value != null)
                    {
                        parks.Clear();
                        foreach (var p in value)
                        {
                            parks.Add(p);
                        }
                    }
                    else
                    {
                        parks = value;  //ToDo: RaiseCollectionChanged ... not fired when replaced
                    }
                    RaisePropertyChanged();
            }
        }

    }
}

The code above contains collection of data from a model and setting the selected item.

 

Create a record structure for example data

So, in the folder "Models", add a new file "ParkModel"

namespace XamlMapControl.ShowShapes.Models
{
    public class ParkModel : AbsoluteMiniMvvm.PropertyChangedBase
    {

        private string _name;
        public string Name
        {
            get { return _name; }
            set { _name = value.Trim();  RaisePropertyChanged(); }
        }
        private string _wkt;

        public string WKT
        {
            get { return _wkt; }
            set { _wkt = value.Trim(); RaisePropertyChanged(); }
        }

        private MapControl.LocationCollection locations;

        public MapControl.LocationCollection Locations
        {
            get { return locations; }
            set { locations = value; }
        }


    }
}

This is can be a completely user-defined class. But make a note of the property "Locations": It is of type "MapControl.Locations".

This will make it a bit easier to show it on the map control. For advanced usage scenarios you can roll your own solution. But then you may involve converters and this is out of  scope for this blog post.


Get example data

Just to get some example data I will also make provide some example data. This is done by making a new class in Models: 

using MapControl;
using System;
using System.Collections.ObjectModel;
using System.Globalization;

namespace XamlMapControl.ShowShapes.Models
{
    public class ParkListModel : AbsoluteMiniMvvm.PropertyChangedBase
    {
        // NOTE: Simulates a data-model storage with hard-coded example data
        
        // Source data from OpenStreetMaps credit “© OpenStreetMap contributors”. (Data is available under the Open Database License)

        private string exampleData = @"Botanisk hage;POLYGON((10.766224 59.9176964, 10.7662485 59.9175923, 10.7665468 59.9173363, 10.7667197 59.917199, 10.7669364 59.9170001, 10.7677221 59.9164993, 10.7684 59.9160434, 10.7685469 59.9160761, 10.7698976 59.9158412, 10.77027 59.9158202, 10.7712362 59.9158916, 10.7714457 59.9159387, 10.7729221 59.9166, 10.7730575 59.9176777, 10.7731055 59.9183368, 10.773166 59.9191191, 10.7732337 59.9198885, 10.7732748 59.9208705, 10.7709214 59.9209107, 10.7697888 59.9199675, 10.7697489 59.9199011, 10.766224 59.9176964))
            Sofienbergparken;POLYGON((10.7613465 59.9243752, 10.7613733 59.9235259, 10.7613774 59.9234289, 10.7613995 59.9224367, 10.7614079 59.9223633, 10.7634505 59.9221179, 10.763663 59.92209, 10.7650472 59.9219201, 10.7651623 59.9219513, 10.765805 59.9218721, 10.7658948 59.921861, 10.7659364 59.9218723, 10.7668695 59.9227148, 10.766972 59.9228074, 10.7671428 59.9229616, 10.7671233 59.9230016, 10.7667299 59.9230958, 10.7656892 59.9233449, 10.7644246 59.9236632, 10.7630894 59.9239932, 10.7614162 59.9244064, 10.7613465 59.9243752))
            Tøyenparken;POLYGON ((10.7805718 59.9243463, 10.7781525 59.9243479, 10.7780073 59.923397, 10.7776675 59.9234017, 10.7773356 59.9234189, 10.7766288 59.9234592, 10.7765678 59.9234146, 10.7758663 59.9234356, 10.7758114 59.9233497, 10.7757427 59.9233488, 10.7757055 59.9229754, 10.7756573 59.9227035, 10.7756822 59.9227024, 10.7764398 59.9226702, 10.777184 59.9226488, 10.7772131 59.9226479, 10.7772773 59.9224836, 10.7773295 59.9222487, 10.7772565 59.9217039, 10.7773 59.9216052, 10.7772254 59.9208286, 10.7770789 59.9207617, 10.7769971 59.9203083, 10.7767624 59.9201799, 10.7766595 59.9201303, 10.7765179 59.9200662, 10.7764377 59.9200392, 10.7763995 59.920029, 10.7763326 59.9200181, 10.7762572 59.9200087, 10.7761901 59.9200036, 10.7758612 59.9200039, 10.7758234 59.9200046, 10.7752503 59.9200275, 10.7751184 59.9200182, 10.7750905 59.9197791, 10.775064 59.919594, 10.7750236 59.9192891, 10.7750165 59.9192011, 10.7750178 59.9191171, 10.7750278 59.9189484, 10.7750434 59.9188375, 10.7750639 59.9187349, 10.7750891 59.9186291, 10.7751313 59.9184971, 10.7751943 59.9183416, 10.7752645 59.9182011, 10.7753564 59.9180442, 10.7755014 59.9178057, 10.7757945 59.9173272, 10.7758397 59.9173331, 10.776031 59.9170191, 10.7761772 59.9166108, 10.7765494 59.9160071, 10.77685 59.9156114, 10.7770533 59.9155783, 10.7774467 59.9156349, 10.7774363 59.9156539, 10.7778763 59.9157056, 10.7778852 59.9156862, 10.7781157 59.9157076, 10.7783817 59.9157282, 10.7786283 59.915742, 10.7788725 59.9157541, 10.7790144 59.9157694, 10.7791417 59.9157941, 10.7792908 59.9158268, 10.7794424 59.9158725, 10.7795584 59.9159201, 10.7796624 59.9159666, 10.7797728 59.9160385, 10.7798454 59.9160959, 10.7799106 59.9161561, 10.7799614 59.9162244, 10.7800031 59.9163151, 10.7798832 59.9169958, 10.7800145 59.9172356, 10.7796971 59.9172113, 10.7795722 59.9176903, 10.7797352 59.9176969, 10.7798948 59.9177033, 10.7805017 59.9186839, 10.7805283 59.9187962, 10.7802535 59.9199225, 10.7803154 59.9199667, 10.7806946 59.919986, 10.7805698 59.9204548, 10.7804721 59.9208087, 10.7804339 59.9208068, 10.7804086 59.9207987, 10.7802795 59.9207244, 10.7801896 59.9206982, 10.780144 59.9209274, 10.780254 59.9209381, 10.7803603 59.9210433, 10.7804015 59.9210841, 10.7802436 59.9216271, 10.78025 59.9217272, 10.7802462 59.9218329, 10.7801044 59.9221927, 10.7801116 59.9225137, 10.780232699999999 59.9228073, 10.7804607 59.9230745, 10.7806543 59.9233425, 10.7809694 59.9236466, 10.7812365 59.9238643, 10.781436 59.9240507, 10.7815271 59.9241567, 10.78141 59.9241822, 10.7813679 59.924155, 10.7813185 59.9241248, 10.7808728 59.9240983, 10.7807709 59.9241103, 10.7806592 59.9242235, 10.7805718 59.9243463))
            Olaf Ryes plass;POLYGON((10.7574478 59.9233433, 10.7573548 59.9232943, 10.7573681 59.9224758, 10.7575154 59.9224135, 10.7590113 59.9223832, 10.7590711 59.9224524, 10.7590665 59.9232943, 10.7590641 59.9233023, 10.7590524 59.9233135, 10.7589737 59.923341, 10.7574478 59.9233433))
            Birkelunden;POLYGON((10.7592964 59.9269399, 10.759312 59.9267484, 10.7593303 59.9265113, 10.75935 59.9262549, 10.7592484 59.926119, 10.7592695 59.9256574, 10.7594123 59.9255668, 10.7609994 59.9255922, 10.7609727 59.92703, 10.7609593 59.9271125, 10.7605802 59.9270631, 10.7595845 59.9270516, 10.7594429 59.9270309, 10.759352 59.9269921, 10.7592964 59.9269399))";

        public ParkListModel()
        {
            // Parsing hard-coded example data in Constructor

            // TODO: Get data from more useful storage 
            // TODO: Improve brute force splitting

            var lines = exampleData.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);

            foreach (var line in lines)
            {
                if (string.IsNullOrWhiteSpace(line) == false &&
                    line.Contains(";") == true &&
                    line.Contains("POLYGON") == true)
                {

                    var cols = line.Replace("\t", ";").Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);

                    var wkt = cols[1];

                    LocationCollection l = new LocationCollection();
                    var bruteForceWktSplit = wkt.Replace("POLYGON", "").Split(new char[] { '(', ')', ',' }, StringSplitOptions.RemoveEmptyEntries);
                    foreach (var part in bruteForceWktSplit)
                    {
                        if (string.IsNullOrWhiteSpace(part) == false && part.Contains(" "))
                        {
                            var lonlat = part.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
                            var lat = Convert.ToDouble(lonlat[1], CultureInfo.InvariantCulture);
                            var lon = Convert.ToDouble(lonlat[0], CultureInfo.InvariantCulture);
                            l.Add(lat, lon);
                        }
                    }

                    Parks.Add(new ParkModel() { Name = cols[0], WKT = cols[1], Locations = l });
                }
            }
        }

        private ObservableCollection<ParkModel> _parks = new ObservableCollection<ParkModel>();
        public ObservableCollection<ParkModel> Parks
        {
            get { return _parks; }
            set {
                if (_parks != null && value != null)
                {
                    _parks.Clear();
                    foreach (var p in value)
                    {
                        _parks.Add(p);
                    }
                }
                else
                {
                    _parks = value;  
                }
                RaisePropertyChanged();
            }
        }
    }
}

That's it for the base structure.


Set up the window to show the map data

For a basic usage of this the following XAML should make up the "MainWindow.xaml":

<Window x:Class="XamlMapControl.ShowShapes.MainWindow"
        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:local="clr-namespace:XamlMapControl.ShowShapes" 
        xmlns:vm="clr-namespace:XamlMapControl.ShowShapes.ViewModels" 
        xmlns:map="clr-namespace:MapControl;assembly=MapControl.WPF"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>

	<Grid>
        <map:Map x:Name="MainMap" Center="59.92, 10.759" ZoomLevel="14">
            <map:Map.Resources>
                <DataTemplate x:Key="PolygonItemTemplate">
                    <map:MapPolygon 
                        x:Name="poly" 
                        Focusable="True"
                        Locations="{Binding Locations}" 
                        Stroke="DarkGreen" 
                        StrokeThickness="3"
                        Fill="Green" 
                        Opacity=".5" />
                    <DataTemplate.Triggers>
                        <Trigger SourceName="poly" Property="IsMouseOver" Value="True">
                            <Setter TargetName="poly" Property="Fill" Value="{DynamicResource {x:Static SystemColors.HotTrackBrushKey}}" />
                            <Setter Property="Cursor" Value="Hand" />
                        </Trigger>
                        <DataTrigger Binding="{Binding Path=IsSelected, RelativeSource={RelativeSource AncestorType={x:Type map:MapItem}}}" Value="True" >
                            <Setter TargetName="poly" Property="Fill" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}" />
                        </DataTrigger>
                    </DataTemplate.Triggers>
                </DataTemplate>

                
            </map:Map.Resources>

            <map:MapTileLayer TileSource="http://tile.stamen.com/toner-lite/{z}/{x}/{y}.png" />
            
            <map:MapItemsControl ItemsSource="{Binding Parks}" IsHitTestVisible="True"
                                 ItemTemplate="{StaticResource PolygonItemTemplate}"
                                 IsSynchronizedWithCurrentItem="True"
                                 SelectedItem="{Binding SelectedPark}"
                                 />

        </map:Map>

        <Border HorizontalAlignment="Right" VerticalAlignment="Bottom" Background="{DynamicResource {x:Static SystemColors.InfoBrushKey}}">
            <TextBlock Margin="4,2" FontSize="10" Foreground="{DynamicResource {x:Static SystemColors.InfoTextBrushKey}}" Text="Map tiles by [Stamen Design], under [CC BY 3.0], Data by [OpenStreetMap], under [ODbL]"/>
        </Border>
        
        <Border Margin="20" Width="200" Height="200" HorizontalAlignment="Left" VerticalAlignment="Top">
            <Border.Effect>
                <DropShadowEffect Opacity=".5" BlurRadius="20"/>
            </Border.Effect>
            <ListBox ItemsSource="{Binding Parks}" DisplayMemberPath="Name" SelectedItem="{Binding SelectedPark}" IsSynchronizedWithCurrentItem="True" />
        </Border>
    </Grid>
</Window>

Note the following important settings at the top, and those may need be adusted to your needs:

  • xmlns:vm="clr-namespace:XamlMapControl.ShowShapes.ViewModels"
  • xmlns:map="clr-namespace:MapControl;assembly=MapControl.WPF"
  • <Window.DataContext><vm:MainWindowViewModel/></Window.DataContext>

- And that's it!

Press F5 to run it and see if the result is like the image at the top.


Explanation and adjustments to the XAML.

The first change I made was to bind the properties "Center", "ZoomLevel" to the view-model.

Reading the code above will be dependant on your familarity on XAML. Bu here's the important structure:
  • The top attributes for the "Window" refer and link to the data
    • Point to the ViewModels:  xmlns:vm="clr-namespace:XamlMapControl.ShowShapes.ViewModels"
  • The first TAG under window point to the DataContext
    • <Window.DataContext>
      • <vm:MainWindowViewModel/>
    • </Window.DataContext>
  • Then in the MapControl:
    • <map:Map x:Name="MainMap">
  • The data is fetched from the ViewModel's property "Parks":
    • <map:MapItemsControl ItemsSource="{Binding Parks}" IsHitTestVisible="True"
                                       ItemTemplate="{StaticResource PolygonItemTemplate}"
                                       IsSynchronizedWithCurrentItem="True"
                                       SelectedItem="{Binding SelectedPark}"
                                       />
  • This points back up to the DataTemplate specified in ItemTemplate.
    • <DataTemplate x:Key="PolygonItemTemplate">
      • <map:MapPolygon 
        • Locations="{Binding Locations}" 
To enhance things from this the main work can be done in the XAML Template and in the optional Styles. 

Some enhancements can be:
  • Using styles instead of templates
  • Using Converters for both data transformations, Data display or the more used "LocationToViewConverter"
  • Adding text or other information
  • Handling overlapping polygons
  • Adding lines and points
And for the data provided:
  • Serving data from other sources
  • Limit data based on viewport
  • Get selected object in view/zoom
But that may be for another blog post


How to build off this template

For new data go into the models and retrieve data according to your needs. This can be an expansion of the simple structure - but also include Provider-models or based on other frameworks.

Then expand the viewModel to provide data tailored for the view's needs.

In the views add new bound controls that "subscribe" to the data. For instance: 
  • Add a new ItemsControl inside the Map for i.e. Roads 
    • And make a new ItemsTemplate using the MapControl PolyLine
  • Add a new control such as ListViews or input-boxes
When the need comes to shared data, you should consider other frameworks. But for this simple approach you could build your own application-level views, containers or data by switching App.xaml so it uses App.cs and the Application_Starup. 


Comments

Popular posts from this blog

Custom Pushpin in Bing Maps Control for WPF

Bing Maps Control for WPF with Essential Tools

Tip to enable key-events in WPF