Wednesday, July 27, 2011

USE OF PRISM IN ARCGIS SILVERLIGHT APPLICATIONS - PART II

Adding Modules to the application

Part I - Starting a new application with PRISM

In this part, we will add a second module to the application. As example module we will add an ArcGis Legend component included in the TocModule. The main pattern we will have to solve is the synchronization between the MapModule and the TocModule concerning the ArcGis Map control. There is no longer the possibility to link different ArcGis controls within the same view with xaml properties. The map control and legend control are located in different xap files.
 In Prism there are different ways to communicate between modules. The method I choose was the use of the EventAggregate method, it is based on the principle of publish and subscribe. In our case the MapModule will be the publisher and the TocModule the subscriber. In this way all modules that need a reference to the Map control can use this communication pattern. As the Map control is central to all ArcGis processing, a lot of modules will need to subscribe to map changes.
Using EventAggregate methods we will respect the loosely coupling of modules. The use of filtering is not used here. When needed, the subscriber can write a method to limit the number of published events to be processed.
Start a new project similar as we did for the MapModule and name it TocModule. Mark the application as host by the existing web configuration. Uncheck ‘Add a test page that references the application” as we do not need this to be a startup xap file.



Delete the app.xaml and mainpage.xaml and create a new view base on the Silverlight UserControl template. Add the ArcGis Legend control to the page as outlined below.

<UserControl x:Class="MyToc.Views.TocView"
   xmlns:esri="http://schemas.esri.com/arcgis/client/2009"
   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:bind="clr-namespace:Helper.RelativeBinding;assembly=Helper"
   mc:Ignorable="d"
   d:DesignHeight="300" d:DesignWidth="250">
   <Grid x:Name="LayoutRoot" Background="White">
     <StackPanel VerticalAlignment="Top">
     <esri:Legend x:Name="LayerLegend" LayerItemsMode="Tree" ShowOnlyVisibleLayers="False"
        Map="{Binding MapView}" Margin="0" Height="400">
     <esri:Legend.MapLayerTemplate>
       <DataTemplate>
        <StackPanel>
          <CheckBox Content="{Binding Label}"
                    IsChecked="{Binding IsEnabled,Mode=TwoWay}"
                    IsEnabled="{Binding IsInScaleRange}">
           </CheckBox>
         </StackPanel>
         </DataTemplate>
       </esri:Legend.MapLayerTemplate>
       <esri:Legend.LayerTemplate>
       <DataTemplate>
          <CheckBox Content="{Binding Label}"
              IsChecked="{Binding IsEnabled,Mode=TwoWay}"
              IsEnabled="{Binding IsInScaleRange}">
          </CheckBox>
       </DataTemplate>
     </esri:Legend.LayerTemplate>
   </esri:Legend>
  </StackPanel>
</Grid>
</UserControl>

Create the module class TocModule implementing the IModule interface. Don’t forget to export the module to the MEF container by adding the needed attribute to the class.

Create a ViewModel class with at least the MapView defined that will expose the map control.

[Export(typeof(TocViewModel))]
public class TocViewModel : NotificationObject
{
    private Map _map;
    public Map MapView
   {
      get { return _map; }
      set
      {
         _map = value;
         this.RaisePropertyChanged(() => this.MapView);
      }
    }
}
 To have the new module to be incorporated in the application we still need to do two things:
  • Add the module to our xaml catalog of modules.
<Modularity:ModuleInfoGroup InitializationMode="WhenAvailable">
   <Modularity:ModuleInfo Ref="MyMap.xap" ModuleName="MapModule"/>
</Modularity:ModuleInfoGroup>
<Modularity:ModuleInfoGroup InitializationMode="WhenAvailable">
   <Modularity:ModuleInfo Ref="MyToc.xap" ModuleName="TocModule"/>
</Modularity:ModuleInfoGroup>

  • Define a region where we will put this legend. Use the Initialize method from the IModule for the region registration.
public void Initialize()
{
     this._regionManager.RegisterViewWithRegion("Region2", typeof(TocView));
}

This results is the layout below:


Region 2 contains the legend module.

First we will add to the MapModule the possibility to publish the changes in the Map control.

To be able to communicate information to a subscriber, we will add a new class to our common Helper project. This class will be used to exchange information to all subscribers.

Add a class MapInfo:

public class MapData
{
   public string BaseMap { get; set; }
   public Map MapView { get; set; }
}
With this class, you now can publish this information. In the ViewModel of the MapModule we will add a reference to the EventAggregator of the container.

[ImportingConstructor]
public MapViewModel(IEventAggregator eventAggregator, IGisOperations gisOperations)
{
   mapDataEvent = eventAggregator.GetEvent<CompositePresentationEvent<MapData>>();
   this._gisOperations = gisOperations;
}

You can call the publish method at different location. Here I opt to do it when the initial extent has been set after the map has been configured.

public void SetInitialExtent(double xMin, double yMin, double xMax, double yMax, int wkid)
{
   SpatialReference sref = new SpatialReference(wkid);
   _initialExtent = new Envelope(xMin, yMin, xMax, yMax);
   _initialExtent.SpatialReference = sref;
   this.wkid = wkid;
   this.RaisePropertyChanged(() => this.Layers);
   this.RaisePropertyChanged(() => this.InitialExtent);
   PublishMapChange();
}

private void PublishMapChange()
{
   MapData mapData = new MapData();
   mapData.MapView = mapView.map;
   _gisOperations.SetMap(mapView.map,baseMap);
   // Broadcast changes
   mapDataEvent.Publish(mapData);
}

Next we will need to handle the subscription to the changes exposed by means of the EventAggregator in the TocModule.

As with the MapModule, we will initialize the subscription in the ViewModel of the TocModule. The most logical place to do it is in the constructor of the module. Using dependency injection we will make the EventAggregator available.

[ImportingConstructor]public TocViewModel(IEventAggregator eventAggregator)
{
   if (eventAggregator != null)
   eventAggregator.GetEvent<CompositePresentationEvent<MapData>>().Subscribe
    (OnMapChanged);
}
The real processing has to done on the handling of the event when something has been changed in. This will be called every time a publishing of the above event happens.
public void OnMapChanged(MapData mapData)
{
   if (mapData != null)
      this.MapView = mapData.MapView;
}

By setting a new value to MapView, a property changed event will occur modifying the map property of the legend control and forcing a refresh of the legend.

Often you will have more to do than simply set a property to set the map control. In the case of the legend control, it could be useful to collapse the legend except for one or several layers or hide the graphic layer in the legend.

This is all we have to do create a communication between two modules that respects the loosely coupling between modules.

The same pattern can be used to create modules based on other ArcGis controls as scalebar, overview map or in case you create your own custom controls.

2 comments:

  1. Hey Johnny - Thanks for posting this! Do you have a source code for the completed project? If not, could you describe what the implementation of IGisOperations does?

    ReplyDelete
  2. IGisOperations is the interface used to access my implementation of ArcGis methods from within the Prism framework. IGisOperations is a service consumed within the framework. This in no way an ESRI interface, but my own interpretation of using ArcGis Silverlight API methods.

    ReplyDelete