Monday, July 25, 2011

USE OF PRISM AND MVVM IN ARCGIS SILVERLIGHT APPLICATIONS. PART I

START A SILVERLIGHT APPLICATION

Introduction

With the disappearance of the Web ADF for ArcGis in the long term, the company I worked for was looking for a new architecture that can be used to replace the current Web ADF development. The ESRI Silverlight API is one of the possible solutions. However what most development teams are looking for is a flexible architecture for building application like Web ADF and in the mean time a solid architecture.  Not often I found a certain lack of software quality in ArcGis development. In many developments focus was on the ArcGis development framework, which in itself is huge and not in maintainable development.
In the coming publications I will explain a roadmap how we can achieve the flexible development goals and even create a more modern approach to develop applications.
After some research I came to the PRISM framework for Silverlight development.  It is well known that for Silverlight there is a design & practice pattern MVVM to create applications. However today there is a strong push for the use of containers to create more loosely coupled components.
Some time ago a Microsoft Team developed a framework PRISM for Silverlight/WCF using container technology to make the life easer for developers of Silverlight applications. In the coming sections I will describe how we can use this framework to develop ArcGis Silverlight applications. The benefits of using this kind of framework are:
·         Modular concept. Applications are split into independent components (modules).
·         Separation of UI and business logic. Application development can be done by two separate teams: UI Designers and Developers. In Microsoft terms we speak of ‘Blendable’ development, pointing to the use of Microsoft Blend.
By using PRISM some assumptions are needed to be made, in this study I used the folowing architecture:
  • Container library: We will use MEF as the container library. You can use other libraries, but we will use MEF here because it is embedded in .NET 4.0 and use attributes to pass information to the framework, eliminating to do the coding for interfacing with the PRISM framework.
  • Log library, custom implementation of ILogFacade.

Prerequisites

You must have installed Microsoft .NET 4.0 framework in order to use PRISM 4.0 together with Silverlight 4.0.
You also need Visual Studio 2010 professional or higher in order to use the code; I am not sure all can be done with Visual Studio 2010 Express.
For Silverlight applications the Microsoft Silverlight 4 Tools for Visual Studio 2010 are needed.
You need to install the PRISM 4.0 library that you can find at the location http://www.microsoft.com/download/en/details.aspx?displaylang=en&id=4922

Additional it is useful to install the Nuget Package Manager plug-in. You can use this to get the references imported of the PRISM assembly’s when they are required during design.
  

Create PRISM Application


To start our first PRISM driven application, we will create a simple Silverlight application based on the template you will find in Silverlight in Visual Studio 2010.

For use later, check the Enable WCF RIA Services.  Make sure Silverlight 4 is selected.


We now have a blank Silverlight application that we will adapt for PRISM 4.0 and add ArcGis functionality to it.

Rename the page MainPage.xaml to Shell.xaml as this is the standard name used for the applications or you can delete MainPage.xaml and create an new Silverlight UserControl named Shell.xaml

Using PRISM requires no longer using a XAML file as startup but rather a bootstrapper. The bootstrapper will contain all the pluming needed for the PRISM framework to operate within out Silverlight application.

The final step in the bootstrapper will be the launch of the startup XAML, in our case the Shell.xaml.

Add PRISM assembly’s references to the project. You can use Nuget to import the references from the PRISM package and the MEF extensions.

Create a new Class MyBootstrapper derived from MefBootstrapper.

We need to implement the following methods:
protected override void ConfigureAggregateCatalog()
{
  base.ConfigureAggregateCatalog();
  this.AggregateCatalog.Catalogs.Add(new AssemblyCatalog(typeof(MyBootstrapper).Assembly));
}

protected override DependencyObject CreateShell()
{
  return this.Container.GetExportedValue<Shell>();
}

protected override void InitializeShell()
{
  base.InitializeShell();
  Application.Current.RootVisual = (UIElement)this.Shell;
}

private const string ModuleCatalogUri = “/Demoapplication;component/ModulesCatalog.xaml”

protected override IModuleCatalog CreateModuleCatalog()
{
  Uri uri = new Uri(ModuleCatalogUri, UriKind.Relative);
  Microsoft.Practices.Prism.Modularity.ModuleCatalog.CreateFromXaml(
          uri);
  return;
}
Here we will use a resource xaml file ModulesCatalog.xaml that contains all the modules we will use. Doing so, we can easy add or remove components to the application. Let’s say we have an overview module we can add or remove this from our application depending if it is needed or not.

An example of a resource file is

<Modularity:ModuleCatalog xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:Modularity="clr-namespace:Microsoft.Practices.Prism.Modularity;assembly=Microsoft.Practices.Prism">
  <Modularity:ModuleInfoGroup Ref="MyMap.xap" InitializationMode="WhenAvailable">
     <Modularity:ModuleInfo ModuleName="MapModule"/>
  </Modularity:ModuleInfoGroup>
</Modularity:ModuleCatalog>

Modify the startup of the application in the App.cs to point to our bootstrapper:

public App()
{
   MyBootstrapper bootstrapper = new MyBootstrapper();
   bootstrapper.Run();
}

Create Modules


Before we can run our application, let’s add a module. As almost all ArcGis application will need a map, we will start with the implementation of a Map module.


To create a module, we will add a new project to the solution. We will choose a Silverlight Application from the project templates of Silverlight in Visual Studio 2010.

Make sure to uncheck ‘Add a test page that references the application’ as we do not need another startup page.


A blank Silverlight application is now generated. Delete the App.xaml and MainPage.xaml as we will do not need these files.

Create the MVVM project structure as we will use the MVVM design & practice pattern.

And finally we will create a class that will be the module.

The project should contain folders Model , ModelView and View.

Add the different PRISM references; make sure to mark these references ‘Copy Local’ as false.

Create a module MapModule that implements the IModule interface of the PRISM framework. You need to implement the Initialize method.

Add also the ModuleExport attribute to make it available in the catalog of modules. The MefContainer use attributes to do the registration, in this way you don’t need to registration by code.

Below is an example of the module.

[ModuleExport(typeof(MapModule))]
public class MapModule : IModule
{
  [Import]
  public IRegionManager RegionManager { private get; set; }

  public void Initialize()
  {
    this.RegionManager.RegisterViewWithRegion("Region1", typeof(MapView));
  }
}

By using Dependency injection we can access the RegionManager component to register the view in a region defined in the Shell.xaml

In the folder View we create a Silverlight control MapView that will embed the ArcGis Map component.

Create an empty UserControl.

In the code behind add an export attribute to make it available in the MapModule for the registration of the region.

[Export(typeof(MapView))]

Add in the Shell.xaml the region namespace and the definition.

xmlns:Regions="clr-namespace:Microsoft.Practices.Prism.Regions;assembly=Microsoft.Practices.Prism"

<ItemsControl x:Name="RegionMap" Regions:RegionManager.RegionName="Region1" />

You can now compile and run the application. The result should be an empty page as for now we do not have initialized the ArcGis map component with an ArcGis MapService.

Add a ArcGis Map to the Application


We will now modify the previous application so that we can add a MapService to the application in the predefined region. Implementation will respect the MVVM design & practice pattern.

First, start with the implementation of the ViewModel that we will need. The ViewModel must be derived from NotificationObject of the PRISM assembly.  This replace the INotifyPropertyChanged interface of the MVVM architecture resulting in less code needed to raise the changed event.

Using a Lambda expression eliminates the use of hardcoded property names.

Simply add some properties needed for the initialization of the Map.

[Export(typeof(MapViewModel))]
public class MapViewModel : NotificationObject
{
  public Map MapView { get; set; }


  private Envelope _initialExtent;
  private Envelope _initialExtent;

  public Envelope InitialExtent
  {
    get { return _initialExtent; }
    set {
    _initialExtent = value;
    }
  }

  private LayerCollection _layers;
  public LayerCollection Layers
  {
   get { return _layers; }
   set
   {
    _layers = value;
    this.RaisePropertyChanged(() => this.Layers);
   }
  }
  public ObservableCollection<string> LayerIds;
}


To make the ViewModel available in the View we can export the class so that it is available in the container. Using Dependency Injection this will be available in the view during the initialization process of the container.

Add export attribute to the ViewModel.

[Export(typeof(MapViewModel))]
public class MapViewModel : NotificationObject


Import the ViewModel in the MapModule :

[Export(typeof(MapView))]
public partial class MapView : UserControl
{
  public MapView()
  {
   InitializeComponent();
  }
  [Import]
  public MapViewModel mapViewModel
  {
   set { this.DataContext = value; }
  }
}


For building the list of layers we use a Model class LayerModel to retrieve the map information needed in the ViewModel.

public static class LayerModel
{
  public static List<string> BaseServiceNames()
  {
   List<string> names = new List<string>();
   names.Add  ("http://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer");
   return names;
  }
  public static Envelope BaseExtent()
  {
   SpatialReference sref = new SpatialReference(3857);
   Envelope extent = new Envelope(444000, 6636000, 446000, 6638999);
   extent.SpatialReference = sref;
   return extent;
  }
}

This way of working is purely for demonstration purpose. In reality the layers must come from some kind of configuration data. This will be illustrated later.

In the constructor of the ViewModel of MapViewModel we call the Model to retrieve the map information.

_layers = new LayerCollection();
List<string> layerNames = Model.LayerModel.BaseServiceNames();
for (int i = 0; i < layerNames.Count; i++)
{
  ArcGISDynamicMapServiceLayer mapLayer = new ArcGISDynamicMapServiceLayer();
  mapLayer.Url = layerNames[i];
  mapLayer.DisableClientCaching = true;
  _layers.Add(mapLayer);
  Envelope baseExtent = Model.LayerModel.BaseExtent();
  _extent = baseExtent;
}

this.RaisePropertyChanged(() => this.Layers);
this.RaisePropertyChanged(() => this.Extent);

Use a Lambda expression in the RaisePropertyChanged method of the NotificationObject to notify the View of the modification.

In the ESRI Silverlight API in the map control not all properties can be bind to the ViewModel. This is because they are not derived from the dependency property component.

One of these properties is the Map.Extent of the Map control.

To solve this issue, you can add an attached property to replace the extent. This attached property will then be responsible to change the extent of the map.

Create a Helper folder to group all utility classes.

In the helper folder we can implement a MapExtent property:

public class MapViewHelper
{
  public static readonly DependencyProperty MapExtentProperty =
   DependencyProperty.RegisterAttached("MapExtent",typeof(Envelope),
   typeof(MapViewHelper),new PropertyMetadata(new PropertyChangedCallback

   (OnMapExtentChanged)));

  public static Envelope GetMapExtent(DependencyObject depObject)
  {
   return (Envelope)depObject.GetValue(MapExtentProperty);
  }

  public static void SetMapExtent(DependencyObject depObject,Envelope value )
  {
   depObject.SetValue(MapExtentProperty, value);
  }

  private static void OnMapExtentChanged(DependencyObject depObject,
   DependencyPropertyChangedEventArgs e)
  {
   Map map = depObject as Map;
   if (map == null)
   {
     throw new ArgumentException(
      "DependencyObject must be of type ESRI.ArcGis.client.Map");
   }
   Envelope newExtent = GetMapExtent(map);
   if (map.Extent == null)
    map.Extent = newExtent;
   else
    map.ZoomTo(newExtent);
  }
}


In the xaml view add the property to the ESRI Map component:

xmlns:extra="clr-namespace:MyMap.Helper"

<esri:Map Background="White" HorizontalAlignment="Stretch"
x:Name="map" VerticalAlignment="Stretch" Margin="0,0,0,12"
Width="800" Height="500" Layers="{Binding Path=Layers}"
extra:MapViewHelper.MapExtent="{Binding Path=InitialExtent}">

You now have a two way binding for the extent of the Map control.

Compile and run the application. You now have a map displayed at a certain location.

Add Configuration Service


As for now we have used fixed values to initialize the Map control. In the real world almost all applications have a configuration file to make the application customizable. In Silverlight you cannot use configuration files like .ini files or .config files due to security restrictions.

A possible configuration service can be implemented in the following way:

  • Implement a WCF service that retrieves a configuration file based on the application. You can use a database to make it flexible.
  • Create a class configuration, implementing a configuration interface.
  • In the bootstrapper create an instance of the configuration class.
  • Expose the configuration interface through dependency injection.
In the example below we will retrieve the MapService layer from the configuration data that will be retrieved from the web service.

To consume the configuration information we can use the interface that has been injected in the ViewModel.

The major problem that has to been tackled is the fact that WCF services in Silverlight is asynchrony and that you can only handle the map control initialization after the WCF services has terminated. So we have to find some way to know when the call to the WCF service has terminated, and then initialize the map control with the configuration content.

To handle this issue, the following solution I used:

  • Create a separate project with the IConfiguration interface having the methods we need. In particular I need the method for reading the full configuration. I use a Helper project that contains interfaces and classes available for the whole solution. 
public interface IConfiguration
{
  void GetConfiguration();
  object Setting(string key);
  void SetFinishedEvent(EventHandler finishedFillConfiguration);
  void ResetFinishedEvent(EventHandler finishedFillConfiguration);
}

  • Create a configuration class in our main Silverlight project (containing the bootstrapper) that will implement the IConfiguration interface.
[Export(typeof(IConfiguration))]
public class Configuration :IConfiguration
{
  private ObservableCollection<string> baselayers =
   new ObservableCollection<string>();
  private int wkid;
  private Extent extent;
  private ConfigurationServiceClient configService =
    new ConfigurationServiceClient();

  public object Setting(string key)
  {
    string value = string.Empty;
    if (key.Equals("BASE"))
    {
     if (baselayers.Count > 0)
     {
      value = baselayers[0];
     }
     return value;
    }
    if (key.Equals("EXTENT"))
    {
     MyExtent myExtent = new MyExtent();
     myExtent.XMin = extent.XMin;
     myExtent.XMax = extent.XMax;
     myExtent.YMin = extent.YMin;
     myExtent.YMax = extent.YMax;
     myExtent.WKID = wkid;
     return myExtent;
    }
    return null;
  }

  public void GetConfiguration()
  {

    baselayers = new ObservableCollection<string>();
    configService.GetSettingCompleted += (s, ea) =>
    {
      baselayers = ea.Result.BaseLayers;
      extent = ea.Result.BaseExtent;
      wkid = ea.Result.WKID;
      EventArgs eventArgs = new EventArgs();
      OnFinishedFill(eventArgs);
    };
    configService.GetSettingAsync("Demo", "Init");
  }
  private event EventHandler FinishedFillConfiguration;

  public void SetFinishedEvent(EventHandler finishedFillConfiguration)
  {
   this.FinishedFillConfiguration += finishedFillConfiguration;
  }

  public void ResetFinishedEvent(EventHandler finishedFillConfiguration)
  {
   this.FinishedFillConfiguration -= finishedFillConfiguration;
  }

  protected virtual void OnFinishedFill(EventArgs e)
  {
   if (FinishedFillConfiguration != null)
   {
    FinishedFillConfiguration(this, e);
   }
  }
}


  • Add an event to the class that will be raised at the end of the WCF call for the configuration data.
  • Finally, use the MapModule to hook to this event. Because almost all ArcGis applications use a map, we can hook to this event. The call to get the configuration data can be done during the Initialize method of the module. In the Initialize we hook to the end of the configuration service.

When the call to get the configuration data is finished, we can update the ViewModel of the MapModule to make the map displayed.

To initialize the configuration at this level is not conform the loosely coupling of modules but in the case of ArcGis Silverlight application this is an acceptable method.

public void Initialize()
{
   this._regionManager.RegisterViewWithRegion("Region1", typeof(MapView));
   configuration.SetFinishedEvent(new EventHandler(ConfigurationInitialised));
   configuration.GetConfiguration();
}
private void ConfigurationInitialised(object sender,EventArgs e)
{
   string baseMap = string.Empty;
   baseMap = (string) configuration.Setting("BASE");
   configuration.ResetFinishedEvent(ConfigurationInitialised);
   MyExtent myExtent = (MyExtent)configuration.Setting("EXTENT");
   mapViewModel.SetBaseMap(baseMap);
   mapViewModel.SetInitialExtent( myExtent.XMin, myExtent.YMin, myExtent.XMax,    

     myExtent.YMax, myExtent.WKID);
}


In the example I also handle the zoom to an initial extent. The extent property in the map control is not a dependency property; therefore you cannot bind it to the ViewModel. To bypass this issue I use a custom attached property derived from the dependency object. In this way you can set the extent from the map from the ViewModel.

1 comment:

  1. The MapView property of MapViewModel is not setted. So we can not publish this to other modules. How can you set MapView property? it only declared like this,
    public Map MapView { get; set; }

    ReplyDelete