Here I am, talking about
mg-desktop and how it's the next best thing since sliced bread, but I haven't even explained how you can go about using it in your own .net applications! This post aims to rectify this problem.
This post will aim to show you how to create a simple .net WinForms application that displays a map from the Sheboygan sample data set with a basic selection handler. The final application will look like so:
Before we get started, make sure you have the following:
- Visual Studio 2008 or newer (I'm using the express edition for this post)
- The latest binary release of mg-desktop
Knowledge of the official MapGuide .net API is also assumed here because most of what you know about the official API is equally applicable to mg-desktop.
So fire up Visual Studio and let's get started!
1. Download mg-desktop
Download the
latest binary release of mg-desktop and extract this archive to a directory of your choice. We will be referring to files in this location for the rest of this post.
2. Set up the Visual Studio Toolbox
In order to facilitate drag-and-drop of the map viewer component, we need to register the mg-desktop viewer component into the Visual Studio Toolbox. To do this, right click the toolbox and select Choose Items
This will bring up the Choose Toolbox Items dialog, click the browse button
Browse to the directory you extracted mg-desktop into and select the OSGeo.MapGuide.Viewer.dll assembly. This will add our viewer components to the list of available components.
The components for reference, are:
- MgMapViewer - This is the map viewer component
- MgLegend - This is the legend component which can control the display and visibility of layers in the map viewer. This component is optional
- MgPropertyPane - This is the component for displaying attributes of selected features on the map viewer. This component is optional
- MgDefaultToolbar - This is a component containing a common list of functions for interacting with the map viewer (zoom, pan, select, etc). This component is optional. You can roll your own map viewer toolbar, but that requires a lot of boilerplate to set up. This component is provided for convenience.
Ensure these components are ticked and click OK to add these components to the Visual Studio Toolbox.
3. Create a new WinForms project
NOTE: The mg-desktop map viewer is a WinForms component. You can technically use this component in a WPF application using the WPF-WinForms interop libraries, but that is beyond the scope of this tutorial.
Now we create our WinForms application. Select File - New Project and select the Windows Forms Application template.
4. Build our main form
If you look at the Visual Studio Toolbox, your components should now be visible whenever the WinForms designer is active
Drag and drop the MgDefaultToolbar component into the main form
Now drag and drop a StatusStrip component into the main form.
Add 4 labels to this status strip. These labels will be used to show the following:
- The current mouse coordinates
- Any status messages sent by the viewer
- The current scale
- The size of the map
Now add a SplitContainer to the main part of the form
Add a second SplitContainer to the left side of this form with horizontal orientation
Now we can drag and drop the remaining components. Set all components to Dock = Fill to occupy the full space of its container
- Drag and Drop the MgLegend to the top-left panel
- Drag and Drop the MgPropertyPane to the bottom-left panel
- Finally, drag and drop the MgMapViewer to the main panel
Modify the properties of the lblMessage label as such:
- Spring = true
- TextAlign = MiddleLeft
This will ensure this label takes the maximum space in the status bar
Now we need to write some code.
5. Wire-up the viewer components
Now switch to the code view for Form1. Start by adding importing the OSGeo.MapGuide.Viewer namespace
1: using OSGeo.MapGuide.Viewer;
To show map viewer status messages, we need this form to implement the IMapStatusBar interface. This adds the following methods to our form
1: public void SetCursorPositionMessage(string message)
2: {
3:
4: }
5:
6: public void SetFeatureSelectedMessage(string message)
7: {
8:
9: }
10:
11: public void SetMapScaleMessage(string message)
12: {
13:
14: }
15:
16: public void SetMapSizeMessage(string message)
17: {
18:
19: }
These methods should be self explanatory. Simply connect the message parameter to its respective label
1: public void SetCursorPositionMessage(string message)
2: {
3: lblCoordinates.Text = message;
4: }
5:
6: public void SetFeatureSelectedMessage(string message)
7: {
8: lblMessage.Text = message;
9: }
10:
11: public void SetMapScaleMessage(string message)
12: {
13: lblScale.Text = message;
14: }
15:
16: public void SetMapSizeMessage(string message)
17: {
18: lblSize.Text = message;
19: }
Now how do we tie all of these components (viewer, toolbar, legend, property pane) together? We use a
MapViewerController to do this. Override the
OnLoad method like so:
1: protected override void OnLoad(EventArgs e)
2: {
3: new MapViewerController(mgMapViewer1, //The MgMapViewer
4: mgLegend1, //The MgLegend
5: this, //The IMapStatusBar
6: mgPropertyPane1, //The MgPropertyPane
7: mgDefaultToolbar1); //The MgDefaultToolbar
8: }
That one line (5 if you want to be pedantic), magically ties all our viewer components together. The MapViewerController basically handles all the plumbing so that your viewer components will properly communicate with each other. Some examples, include:
- Selecting an object in the MgMapViewer will populate the MgPropertyPane with attributes of the selected feature
- Ticking a layer on/off in the MgLegend will trigger a refresh of the MgMapViewer
The MapViewerController automagically sets up all of this for you.
Now we have a viewer that's all set up, now to load some data into it.
6. Code - Initialization
Before we show you how to do this, let's take a segway for a moment. Because we need to cover an important aspect of the mg-desktop API.
Like the official API, the mg-desktop API is driven by service classes. In mg-desktop, the following services are provided:
- MgdResourceService (inherits from MgResourceService)
- MgdFeatureService (inherits from MgFeatureService)
- MgRenderingService
- MgDrawingService
- MgTileService
In the official API, you would access these services via a MgSiteConnection object. For mg-desktop we use the MgServiceFactory class to create instances of these services. For example, here's how you would create an instance of MgdFeatureService
1: MgServiceFactory factory = new MgServiceFactory();
2: MgdFeatureService featureService = (MgdFeatureService)factory.CreateService(MgServiceType.FeatureService);
Other service classes are created in a similar fashion. You would then use these service classes in the same fashion as you would with the official API.
Also like the official API, we need to initialize the whole thing through a config file first before we can use any of the classes in the API. In our case, the file is
Platform.ini, and we initialize like so:
1: MgPlatform.Initialize("Platform.ini");
With that out of the way, we can start writing some code. First we need to add some references to the project. Add the following references from your mg-desktop directory
- OSGeo.MapGuide.Foundation.dll
- OSGeo.MapGuide.Geometry.dll
- OSGeo.MapGuide.PlatformBase.dll
- OSGeo.MapGuide.Desktop.dll
- OSGeo.MapGuide.Viewer.Desktop.dll
Be sure to set these references (and OSGeo.MapGuide.Viewer) to (Copy Local = false)
Now in our application's entry point, insert our call to initialize the API. Also hook the application's exit event to MgPlatform.Terminate(), which does some library cleanup. Program.cs should look like this
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Windows.Forms;
5: using OSGeo.MapGuide;
6:
7: namespace MgDesktopSample
8: {
9: static class Program
10: {
11: /// <summary>
12: /// The main entry point for the application.
13: /// </summary>
14: [STAThread]
15: static void Main()
16: {
17: MgPlatform.Initialize("Platform.ini");
18: Application.ApplicationExit += new EventHandler(OnApplicationExit);
19: Application.EnableVisualStyles();
20: Application.SetCompatibleTextRenderingDefault(false);
21: Application.Run(new Form1());
22: }
23:
24: static void OnApplicationExit(object sender, EventArgs e)
25: {
26: MgPlatform.Terminate();
27: }
28: }
29: }
7. Code - Load a package and map
Now back in our main form, we modify the overridden OnLoad to do the following:
- Check the existence of the map we want to load - Library://Samples/Sheboygan/Maps/Sheboygan.MapDefinition
- If it doesn't exist, prompt the user for the Sheboygan.mgp package and load this package
1: MgServiceFactory factory = new MgServiceFactory();
2: MgdResourceService resSvc = (MgdResourceService)factory.CreateService(MgServiceType.ResourceService);
3: MgResourceIdentifier mapDefId = new MgResourceIdentifier("Library://Samples/Sheboygan/Maps/Sheboygan.MapDefinition");
4: //If this map definition doesn't exist, we ask the user to
5: //load the Sheboygan package
6: if (!resSvc.ResourceExists(mapDefId))
7: {
8: using (OpenFileDialog diag = new OpenFileDialog())
9: {
10: diag.Filter = "MapGuide Packages (*.mgp)|*.mgp";
11: if (diag.ShowDialog() == DialogResult.OK)
12: {
13: MgByteSource source = new MgByteSource(diag.FileName);
14: MgByteReader reader = source.GetReader();
15: resSvc.ApplyResourcePackage(reader);
16: }
17: else
18: {
19: //No map, nothing to do here
20: Application.Exit();
21: }
22: }
23: }
At this point, the map definition exists. So we can create a runtime map and load it into the viewer like so:
1: //Create our runtime map
2: MgdMap map = new MgdMap(mapDefId);
3: //We need a rendering service instance
4: MgRenderingService renderSvc = (MgRenderingService)factory.CreateService(MgServiceType.RenderingService);
5: //Create our viewer provider
6: MgMapViewerProvider provider = new MgDesktopMapViewerProvider(map, resSvc, renderSvc);
7: //Initialize our viewer with this provider
8: mgMapViewer1.Init(provider);
The final
OnLoad method for our form looks like so:
1: protected override void OnLoad(EventArgs e)
2: {
3: new MapViewerController(mgMapViewer1, //The MgMapViewer
4: mgLegend1, //The MgLegend
5: this, //The IMapStatusBar
6: mgPropertyPane1, //The MgPropertyPane
7: mgDefaultToolbar1); //The MgDefaultToolbar
8:
9: MgServiceFactory factory = new MgServiceFactory();
10: MgdResourceService resSvc = (MgdResourceService)factory.CreateService(MgServiceType.ResourceService);
11: MgResourceIdentifier mapDefId = new MgResourceIdentifier("Library://Samples/Sheboygan/Maps/Sheboygan.MapDefinition");
12: //If this map definition doesn't exist, we ask the user to
13: //load the Sheboygan package
14: if (!resSvc.ResourceExists(mapDefId))
15: {
16: using (OpenFileDialog diag = new OpenFileDialog())
17: {
18: diag.Filter = "MapGuide Packages (*.mgp)|*.mgp";
19: if (diag.ShowDialog() == DialogResult.OK)
20: {
21: MgByteSource source = new MgByteSource(diag.FileName);
22: MgByteReader reader = source.GetReader();
23: resSvc.ApplyResourcePackage(reader);
24: }
25: else
26: {
27: //No map, nothing to do here
28: Application.Exit();
29: }
30: }
31: }
32:
33: //Create our runtime map
34: MgdMap map = new MgdMap(mapDefId);
35: //We need a rendering service instance
36: MgRenderingService renderSvc = (MgRenderingService)factory.CreateService(MgServiceType.RenderingService);
37: //Create our viewer provider
38: MgMapViewerProvider provider = new MgDesktopMapViewerProvider(map, resSvc, renderSvc);
39: //Initialize our viewer with this provider
40: mgMapViewer1.Init(provider);
41: }
7. Set up post-build and other loose ends
Now if you've worked with the official MapGuide .net API (and this whole post assumes you do), you know that referencing the MapGuide .net assemblies does not instantly give you a working MapGuide application. That's because those .net assemblies are managed wrappers around unmanaged dlls, so you need them as well. So for the official API, you would copy all the dlls from mapviewernet into your application's output directory so that all dependencies are met.
For mg-desktop, we pretty much do the same thing, we copy everything from our mg-desktop directory to our application's output directory. Or to automate this, include an xcopy command as part of your project's post build event. Assuming you extracted the mg-desktop binaries to
C:\mg-desktop, and example post build command would be like so:
This will copy all mg-desktop files (dlls, FDO, CS-Map dictionaries, etc, etc) to your application's output directory with the source directory structure intact, which is important because the default paths in Platform.ini are all relative.
If you are on a 64-bit machine, you will also need to explicitly set the CPU type of the application to
x86 instead of
Any CPU. If you don't do this, you will get a
BadImageFormatException thrown at your face as your executable will default to 64-bit and will attempt to load a 32-bit assembly. Actually, you should do this anyway to ensure the application works on both 32-bit and 64-bit windows.
Once this is all set up, you can compile and run your application!
Go on. Have a play around. It is now a fully functional map viewer application!
8. Custom selection handling
One of the things you would probably want to do in your application is to listen for selection changes and run code in response to such changes. The MgMapViewer component exposes a SelectionChanged event for this very purpose.
So to display the address of a selected parcel, the event handler code would look like this:
1: private void mgMapViewer1_SelectionChanged(object sender, EventArgs e)
2: {
3: MgSelectionBase selection = mgMapViewer1.GetSelection();
4: MgReadOnlyLayerCollection layers = selection.GetLayers();
5: if (layers != null)
6: {
7: for (int i = 0; i < layers.GetCount(); i++)
8: {
9: MgLayerBase layer = layers.GetItem(i);
10: if (layer.Name == "Parcels") //The selected layer is parcels
11: {
12: //Check that we only have one selected object
13: int count = selection.GetSelectedFeaturesCount(layer, layer.FeatureClassName);
14: if (count == 1)
15: {
16: MgFeatureReader reader = null;
17: try
18: {
19: reader = selection.GetSelectedFeatures(layer, layer.FeatureClassName, false);
20: if (reader.ReadNext())
21: {
22: //Address is in the RPROPAD property
23: if (reader.IsNull("RPROPAD"))
24: MessageBox.Show("Selected parcel has no address");
25: else
26: MessageBox.Show("Address: " + reader.GetString("RPROPAD"));
27: }
28: }
29: finally //You must always close all readers, otherwise connections will leak
30: {
31: reader.Close();
32: }
33: }
34: else
35: {
36: MessageBox.Show("Please select only one parcel");
37: }
38: break;
39: }
40: }
41: }
42: }
Which would result in this behaviour when selecting a parcel
Selecting multiple parcels gives you the following:
Wrapping up
Hopefully this should give you a comfortable introduction to mg-desktop and its viewer component. Where you go from here is completely up to you.
The source code for this example is available for download
here