Monday, 23 January 2012

How to: Use mg-desktop in your own .net applications

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:
  1. The current mouse coordinates
  2. Any status messages sent by the viewer
  3. The current scale
  4. 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:  {
   4:  }
   6:  public void SetFeatureSelectedMessage(string message)
   7:  {
   9:  }
  11:  public void SetMapScaleMessage(string message)
  12:  {
  14:  }
  16:  public void SetMapSizeMessage(string message)
  17:  {
  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:  }
   6:  public void SetFeatureSelectedMessage(string message)
   7:  {
   8:      lblMessage.Text = message;
   9:  }
  11:  public void SetMapScaleMessage(string message)
  12:  {
  13:      lblScale.Text = message;
  14:  }
  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;
   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:          }
  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
   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:      }
  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


hah said...

Thank you Jackie for the post.

But I was blocked at :

MgMapViewerProvider provider = new MgDesktopMapViewerProvider(map, resSvc, renderSvc);

I notice that the constructor of MgMapViewerProvider in marked as protected, so it does not compile...

Should it be public?

Jackie Ng said...

Nope. The MgMapViewerProvider is protected for subclasses (like MgDesktopMapViewerProvider) to chain into from their (public) constructors

hah said...

Thanks, it was my mistake :)

EndlessLove said...

thanks you for the post.
But i was error:
Could not load file or assembly 'OSGeo.MapGuide.Desktop, Version=, Culture=neutral, PublicKeyToken=e75f9fd7cf82dc3f' or one of its dependencies. The system cannot find the file specified.

can you show me to solve it.

EndlessLove said...

hi, in main class i init: MgPlatform.Initialize("Platform.ini");
but i don't understand about Platform.ini; can you explain about it for me. My error's reason can by it.

Jackie Ng said...

Does your output directory contain all the files from the mg-desktop zip distribution with directory structures intact?

This is what the post-build setup in this post is supposed to do for you.

EndlessLove said...

thanks you for reply.
i understand what you said.
but it don't solve my proplem.
i have done everything that you have written on the blog.
1. All code
and i have added references(all files OSGeo.MapGuide.*) and set properties for them (Copy Local = false),edited Post-build event: xcopy /S /Y /I "C:\Users\namnd\Documents\Visual Studio 2010\Projects\demoMap\demoMap\bin\release\*.*". or xcopy /S /Y /I "C:\Users\namnd\Documents\Visual Studio 2010\Projects\demoMap\demoMap\bin\debug\*.*".
but when i'm building the project so it threw the follow exception(FileNotFoundExecption was unhandled): Could not load file or assembly 'OSGeo.MapGuide.Desktop, Version=, Culture=neutral, PublicKeyToken=e75f9fd7cf82dc3f' or one of its dependencies. The system cannot find the file specified.
i don't understand by why. please show me how can i fix it.

Jackie Ng said...

If the post-build doesn't set up the files correctly, you can always extract the mg-desktop zip file contents into your project's output directory

EndlessLove said...

no, because i tried again many time but it don't right so i copied them to there.
thanks you.
i could wrong anywhere that i don't just know. i'm going to again.

EndlessLove said...

i made again but it still wrong.
same the error.
i extract mg-desktop to C:\mgdesktop
and i edit post-build event:
xcopy /S /Y /I "C:\mgdesktop\*.*".
indeed i don't know that i need to care the file "Platform.ini" because you don't tell about how do create file Platform.ini and in my project haven't had it.

Jackie Ng said...

At the end of the day, all we want is to have all the files in the mg-desktop zip file into the project's output directory. Platform.ini and all the dlls your application are in this zip file.

So if the post-build doesn't copy these files, then just remove the post-build event and unzip the files manually to your project output directory.

firexoulz said...

Hello sir,Thanks for the post you had made! I am trying to develop the mg-desktop but I am a VB.Net programmer and I am having a hard time translating your C# codes into mine because I really don't know what is the equivalent code for could you help me. I am looking forward to your response. Thank You!

billtenbroeck said...

@firexoulz, you might want to run The posted code through the following:

That should get you close enough to understand what is going on.

Bauer50 said...

Thanks for the post! I got everything to work just fine using a basic setup and basic map.

My question is how would you suggest going about using a SQL DB with this? What I am wanting to do is have a program where you manage the SQL DB via the program interface and also be able to view the map that uses that same DB. One of the DBs I am thinking will be SHP files converted to SQL Spatial and the other would be just the data that would be linked. I have another situation that I would be able to have both in the same DB, but for other reasons, this one has to be separate. The spatial one could be "local" but the other would need to be on a server.

I didn't know if you would have to create the feature source and that with Maestro and then have another link in the program itself or if they could be on in the same.

Any thoughts would be greatly appreciated.

Jackie Ng said...

Just make a SQL Server Feature Source and package that up with your other data and transfer the mgp over to mg-desktop (it supports mgp packages).

Bauer50 said...

Thanks for the reply.

So ok, I will just have a "link" to the SQL DB in 2 places (mgp and the program itself). I have a "hair-brain" idea that might make just one that I might try IF I have time. If it works, I will let you know.

Thanks again! Your blog is EXTREMELY helpful on several issues.

Yuval Goder said...

Dear Jackie,
I still have a problem with running the app:
Could not load file or assembly 'OSGeo.MapGuide.Desktop, Version=, Culture=neutral, PublicKeyToken=e75f9fd7cf82dc3f' or one of its dependencies. An attempt was made to load a program with an incorrect format.

any suggestion?

Jackie Ng said...

You application must be built so that its CPU platform matches the bitness of your mg-desktop binaries. You should strongly avoid using "Any CPU"