Tuesday, August 2, 2011

CREATION OF COMMANDS AND TOOLS - PART III

Creation of a Toolbar

To be able to illustrate the handling of ArcGis commands and tools, we will start from a toolbar. We create a new module that contain our new toolbar. We create a new module ToolbarModule with some buttons, the module is created in the same way as the TocModule.

The new visual layout of the application will be:

Region 3 contains the toolbar, region 4 is a spare region that we will fill with a view when needed.



GIS SERVICE


To make the ArcGis plumbing simple, we create a GIS operation class that will handle all low-level ArcGis functionality. Doing this, in most cases we do not need a Model for accessing data.

Because it is defined as a service, all use of the service is done by means of a shared interface IGisOperations. To make it available for all projects, it is put in our common Helper projects. The service is available through dependency injection in the ViewModel.

The interface could looks like this:

public delegate void MyResultsHandler(object sender, ResultsEventArgs e);
public delegate void MyDrawCompleteHandeler(object sender,ESRI.ArcGIS.Client.DrawEventArgs args);
public interface IGisOperations
{
  Map GetMa p();
  string GetBaseMapUrl();
  void SetMap(Map map,string baseMap);
  void AttributeQueryTask_Async(string whereValue, string whereField, string url,
  int layerID, string fieldType);
  void AttributeQueryTask_ExecuteCompleted(object sender,
  ESRI.ArcGIS.Client.Tasks.QueryEventArgs args);
  void AttributeQueryTask_Failed(object sender, TaskFailedEventArgs args);
  void SetFinishedEvent(MyResultsHandler finishedOperation);
  void ResetFinishedEvent(MyResultsHandler finishedOperation);
  void SetCompleteDrawEvent(MyDrawCompleteHandeler drawComplete);
  void ResetCompleteDrawEvent(MyDrawCompleteHandeler drawComplete);
  void ZoomTo(ESRI.ArcGIS.Client.Geometry.Geometry geometry);
  MarkerSymbol GetSelectionMarkerSymbol();
  LineSymbol GetSelectionLineSymbol();
  FillSymbol GetSelectionFillSymbol();
  void SetSelectionMarkerSymbol(MarkerSymbol symbol);
  void SetSelectionLineSymbol(LineSymbol symbol);
  void SetSelectionFillSymbol(FillSymbol symbol);
  void SetDrawMode(DrawMode drawMode);
  void MapZoom(string action, Geometry geometry);
}


Because all server Gis operations are asynchroon, you must always implement an event handling for the callback functionality.
All these methods have to be implemented in the class GisOperation. An instance of this class will be created in the bootstrapper. By means of the dependency injection, the interface will be made available to all classes that need some form of GIS processing. Almost always this will be done in the ViewModel of the modules.

In some cases I could be wise to split this class in different parts as not always all GIS functionality is required in an application. In this manner you could reduce the size of the xap file.

protected override void ConfigureContainer()
{
 
base.ConfigureContainer();  
  this.Container.ComposeExportedValue<IConfiguration>

   (this.configuration);
  
this.Container.ComposeExportedValue<IGisOperations>
   (this.gisOperations);
 
this.Container.ComposeExportedValue<CompositionContainer>
   (this.Container);
}


As you can see, the GisOperation service  works exactly as the configuration service.

CREATION OF A COMMAND


Use of ICommand


As for now there was little code behind the views. We must keep it like that, even when buttons and actions on these buttons must be handled. To make this possible, the MVVM architecture makes use of command classes that implement the ICommand interface.

To simplify the creation of commands, the PRISM framework adds a factory to simplify the creation of commands by means of the Delegate Command class.

The general structure of the creation of a command becomes:

ICommand MyCommand =
  new DelegateCommand<object>(Execute, CanExecute);

The delegates has the signature:
void Execute(object arg)
bool CanExecute(object arg)

Some controls derived from ButtonBase like Button has a command as property, enabling the use of the Command property and CommandArgument directly in xaml.

<Button Margin="0,0,0,0" Name="btnQuery"
  Command="{Binding QueryCommand}">
  <Image Source ="../Resources/i_search.png" />
</Button>


So to make actions happen we simply bind the command property to a property of the ViewModel. Together with the creation of the commands in the constructor of the ModelView we can bind an action to a button without putting an event handling in the code behind of our view.

This result in the following code in the ViewModel:
private ICommand _queryCommand;
public ICommand QueryCommand
{
  get { return _queryCommand; }
  set
  {
   _queryCommand = value;
   this.RaisePropertyChanged(() => this.QueryCommand);
  }
}
private ICommand _zoomOutCommand;
public ICommand ZoomOutCommand
{
  get { return _zoomOutCommand; }
  set
  {
   _zoomOutCommand = value;
   this.RaisePropertyChanged(() => this.ZoomOutCommand);
  }
}
private ICommand _zoomInCommand;
public ICommand ZoomInCommand
{
  get { return _zoomInCommand; }
  set
  {
_  zoomInCommand = value;
   this.RaisePropertyChanged(() => this.ZoomInCommand);
  }
}
[ImportingConstructor]
public ToolbarViewModel(IRegionManager regionManager,CompositionContainer container,ILoggerFacade loggerFacade)
{
  this.QueryCommand = new DelegateCommand<object>(
   this.OnQueryCommandClicked, this.CanQueryCommandClicked);
  this.ZoomInCommand = new DelegateCommand<object>(
   this.OnZoomInCommandClicked, this.CanZoomInCommandClicked);
  this.ZoomOutCommand = new DelegateCommand<object>(
   this.OnZoomOutCommandClicked, this.CanZoomOutCommandClicked);
  this._regionManager = regionManager;
  this._container = container;
}

Create command with input form


Implement an attribute query.

Here we will illustrate the creation of a command that initiate an input form where we can enter some data. When the OK button is pressed, an attribute query is done using methods coming from the IGisOperations interface.

In our views folder we add a new UserControl ‘LocationInput’ that contains two textboxes:
       Country and city

And two buttons:

      Ok and Cancel.

This view is dynamically displayed in a reserved region using code below.
private void OnQueryCommandClicked(object arg)
{
  IRegion region = _regionManager.Regions["Region4"];
  var inputView = _container.GetExportedValue<LocationInput>();
  region.Add(inputView, "LocationInputView");
  region.Activate(inputView);
}


After we create the commands for the Ok and Cancel button, the processing can be put in the ViewModel.

As we already saw before with the configuration, doing GIS operation results in an asynchronously process. Therefore in our GisOperation class we added an event handler that has to be initialized by the calling method so control will be returned after the GIS operation is finished.

First step:       

  • Set the callback when operation is finished.
  • Call the requested operation.
private void OnOKClicked(object arg)
{
  string where = string.Empty;
  if (this.Country.Length > 0)
   where = "COUNTRY='" + this.Country + "'";
  if (this.City.Length > 0)
  {
   if (where.Length > 0)
    where += " and ";
   where = "NAME='" + this.City + "'";
  }
  StartAttributeQuery(where, "", 9, "C");
}
private void StartAttributeQuery(string whereValue, string whereField,
int layerID, string fieldType)
{
  gisOperations.SetFinishedEvent(new MyResultsHandler(HandleResultQuery));
  gisOperations.AttributeQueryTask_Async(whereValue, whereField,
  gisOperations.GetBaseMapUrl(), layerID, fieldType);
}


Second step:

  • Clear the callback handler setting.
  • Process the result.
private void HandleResultQuery(object sender, ResultsEventArgs e)
{
  gisOperations.ResetFinishedEvent(new MyResultsHandler(HandleResultQuery));
  if (e != null)
  {
   FeatureSet results = e.Results;
   if (e.Results.Features != null && e.Results.Features.Count > 0)
   {
     // Zoom to result
     gisOperations.ZoomTo(e.Results.Features[0].Geometry);
     // Display graphics
     GraphicsLayer graphicsLayer =
     gisOperations.GetMap().Layers["Selections"] as GraphicsLayer;
     graphicsLayer.ClearGraphics();
     foreach (Graphic feature in results.Features)
     {
       if (feature.Geometry.GetType() == typeof(MapPoint))
       {                                         
         feature.Symbol =
         gisOperations.GetSelectionMarkerSymbol();
       }
       else if (feature.Geometry.GetType() ==
          typeof(ESRI.ArcGIS.Client.Geometry.Polyline))
       {
         feature.Symbol = gisOperations.GetSelectionLineSymbol();
       }
       else if (feature.Geometry.GetType() ==
         typeof(ESRI.ArcGIS.Client.Geometry.Polygon))
       {
         feature.Symbol = gisOperations.GetSelectionFillSymbol();
       }
       graphicsLayer.Graphics.Insert(0, feature);
    }
   }
  }
}

To simplify the job of rendering, in the map view I defined a resource containing the different rendering symbols. When the map gets initialized, I add code so that the different symbols are saved in the GisOperation class and can be used later.

This initialization can be achieved with this code:

// Initialise symbols
_gisOperations.SetSelectionLineSymbol(
   mapView.LayoutRoot.Resources["SelectLineSymbol"] as LineSymbol);
_gisOperations.SetSelectionMarkerSymbol(
   mapView.LayoutRoot.Resources["SelectMarkerSymbol"] as MarkerSymbol);
_gisOperations.SetSelectionFillSymbol(
   mapView.LayoutRoot.Resources["SelectFillSymbol"] as FillSymbol

CREATION OF A TOOL


In our toolbar I add two buttons ‘zoom in’ and ‘zoom out’. These two buttons illustrate a typical ESRI tool. A tool consists of a draw action and at the end of the draw, a processing.

In our case we will draw a rectangle and at the end of the drawing we will take the requested zoom action.

As in the case of the query tool, the two buttons will use an implementation of ICommand to expose the action to the button.

The main difference with the query command is that we must activate the draw functionality once the button is pressed.

In out GIS service we will add the following methods defined in the interface that is exposed:
void SetCompleteDrawEvent(MyDrawCompleteHandeler drawComplete);void ResetCompleteDrawEvent(MyDrawCompleteHandeler drawComplete);
void SetDrawMode(DrawMode drawMode);


The first two will allow us to handle the end processing of a draw operation. The last method set the kind of draw operation that is required. In case of the zoom action this will be a rectangle.
Because the draw object is associated with the map, it is already initialized during the setting of the map object.

Setting the draw mode active, looks like that:
public void SetDrawMode(DrawMode drawMode){
  if (mapDraw != null)
  {
   mapDraw.DrawMode = drawMode;
   mapDraw.IsEnabled = (mapDraw.DrawMode != DrawMode.None);
   mapDraw.DrawComplete += MapDrawSurface_DrawComplete;
  }
}


When the drawing is complete, the action that is triggered will be responsible to call the handler provided by the calling tool.  Here this is the eventhandler OnDrawComplete that has been set by the calling tool.

private void MapDrawSurface_DrawComplete(object sender, ESRI.ArcGIS.Client.DrawEventArgs args)
{
  mapDraw.DrawMode = DrawMode.None;
  mapDraw.IsEnabled = false;
  OnDrawComplete(args);
}



With this in place, all is now ready to start the drawing process by the tool. The actual action of the tool on the GIS must also be implemented in the GIS operation service. In our case this will be a zoom in and zoom out action on the map.

A possible implementation is highlighted below :
public void MapZoom(string action, ESRI.ArcGIS.Client.Geometry.Geometry geometry)
{
  if (action.Equals("ZoomIn"))
  {
    _mapView.ZoomTo(geometry as Envelope);
  }
  else if (action.Equals("ZoomOut"))
  {
    Envelope currentExtent = _mapView.Extent;
    Envelope zoomBoxExtent = geometry as Envelope;
    MapPoint zoomBoxCenter = zoomBoxExtent.GetCenter();
    double whRatioCurrent = currentExtent.Width / currentExtent.Height;
    double whRatioZoomBox = zoomBoxExtent.Width / zoomBoxExtent.Height;
    Envelope newEnv = null;
    if (whRatioZoomBox > whRatioCurrent)
    // use width
    {
      double mapWidthPixels = _mapView.Width;
      double multiplier = currentExtent.Width / zoomBoxExtent.Width;
      double newWidthMapUnits = currentExtent.Width * multiplier;
      newEnv = new Envelope(
          new MapPoint(zoomBoxCenter.X - (newWidthMapUnits / 2), zoomBoxCenter.Y),
          new MapPoint(zoomBoxCenter.X + (newWidthMapUnits / 2), zoomBoxCenter.Y));
    }
    else
    // use height
    {
      double mapHeightPixels = _mapView.Height;
      double multiplier = currentExtent.Height / zoomBoxExtent.Height;
      double newHeightMapUnits = currentExtent.Height * multiplier;
      newEnv = new Envelope(
         new MapPoint(zoomBoxCenter.X, zoomBoxCenter.Y - (newHeightMapUnits / 2)),
         new MapPoint(zoomBoxCenter.X, zoomBoxCenter.Y + (newHeightMapUnits / 2)));
    }
    if (newEnv != null)
      _mapView.ZoomTo(newEnv);
   }
  }
}

Now we can add the necessary code in the command event of the ViewModel to trigger the action and to do execute the GIS operation at the end of the drawing.

For the zoom in button when clicked, set the draw complete handler and start drawing a rectangle.
private void OnZoomInCommandClicked(object arg)
{
  currentCommand = "ZoomIn";
  gisOperations.SetCompleteDrawEvent(new MyDrawCompleteHandeler(DrawComplete));
  gisOperations.SetDrawMode(DrawMode.Rectangle) ;
}


When the draw is complete, the following we do the job:

private void DrawComplete(object sender, ESRI.ArcGIS.Client.DrawEventArgs args)
{
  gisOperations.MapZoom(currentCommand, args.Geometry);
  currentCommand = string.Empty;
}


As you can see, all the processing is done in the ViewModel, leaving a clean View with almost no code behind even if the view has a lot of functionality.
The ESRI map control will handle changes to the view by itself, there is no MVVM functionality involved in this.

No comments:

Post a Comment