Wednesday, August 10, 2011

CREATION OF COMMANDS AND TOOLS ADVANCED - PART IV

Introduction

Before we will move to more specific GIS functionality as geoprocessing and feature editing, I will first introduce some more complex commands and tools. In the real world there is a much richer set of tools required. In this part we will add some more advanced tools.

The first tool I will illustrate is the use of an inspect tool. Clicking on the map, we want to display the information of the feature.

 A second pattern I demonstrate is the use of the right click on the map to trigger some action. This pattern can be used to display a context menu or perform certain GIS action at the clicked location. This will illustrate the use of attached properties in combination with event triggers on the map control.

Inspect  feature action


At the toolbar, we add a new button ‘Info’ that will start a drawing operation for a point. At the end of the drawing action we display a new window with the attribute information of the feature.

The main issue here is how we will initiate a new window displaying once the information is found. Using the MVVM pattern there is little information about this kind of design patterns. Using the rules of MVVM, you cannot create a child window from within the ViewModel because it is stated that the ViewModel must not having acknowledge about the view.

The solution I created was the use of a new service ‘DialogManager’ that will initiate a child window displaying the information required by our tool. Communication between de ViewModel and the dialog service happens through the EventAggregator service. Later I will illustrate another technique for calling a dialog window from a ViewModel.

First we create a MVVM command that will initiate the spatial query. At the clicked event, the drawing is initiated.

private void OnInfoCommandClicked(object arg)
{
  currentCommand = "Info";
  gisOperations.SetCompleteDrawEvent(DrawComplete);
  gisOperations.SetDrawMode(DrawMode.Point);
}

When the drawing is complete, we start a spatial query in the same way we already executed an attribute query, but this time only geometry is passed to the GIS service. The return is a list of features.

// Info request (spatial query)
gisOperations.SetFinishedEvent(HandleResultQuery);
gisOperations.AttributeQueryTask_Async("""", gisOperations.GetBaseMapUrl(), 9, "", args.Geometry);

After obtaining the result objects, the results are published to the dialog service for displaying.

FeatureSet results = e.Results;
if (e.Results.Features != null && e.Results.Features.Count > 0)
  {
    List<SearchResult> searchResults = new List<SearchResult>();
    foreach (var item in results)
    {  
      SearchResult searchResult = new SearchResult()
             { AttributeValues = item.Attributes, 
               LayerName = "", 
               Geometry = item.Geometry };
      searchResults.Add(searchResult);
     }
     resultEvent.Publish(searchResults);

Now the dialog service comes in action. During the creation of the DialogManager in the bootstrapper, the subscription has been activated during the constructor of the service.

public DialogManager()
{
  this.MyEventAggregator = ServiceLocator.Current.GetInstance<IEventAggregator>();
  if (this.MyEventAggregator != null)
    this.MyEventAggregator.GetEvent<CompositePresentationEvent<List<SearchResult>>>()
    .Subscribe(ShowResultsDialog);
}

When the information arrives at the subscriber service, a new view is initiated for displaying the results. Doing this, the action in the ViewModel does not create directly another view and we can use a ViewModel concept for handling the display of the view.

public void ShowResultsDialog(System.Collections.Generic.List<SearchResult> results)
{
  ResultsView resultsView = new ResultsView();
  ResultsViewModel resultsViewModel = new ResultsViewModel();
  resultsViewModel.SetResults(results);
  resultsView.DataContext = resultsViewModel;
  resultsView.OverlayOpacity = 0;
  resultsView.Show();
}

The use of a dialog manager allows the handling of different requests for displaying information coming from a GIS operation using the MVVM pattern. For large projects a split of the dialog manager in different parts for dynamic loading could be useful.

This pattern is only useful when there is no post processing required at the OK button of the window dialog.  In some later part I will illustrate how some additional processing can be done when the OK button of the dialog window is pressed.

Zoom at a location through right click.


I will now add a new functionality to the map control. Here I  use MVVM attached properties to the map control and using an event trigger to capture the right mouse click event. To implement the right click I have to take into account the following situation:

·         The map control does not have an event defined for the right click.

·         Microsoft adds in Silverlight applications a standard right click event handler for allowing the configuration of the Silverlight.

The ESRI Silverlight API already delivers an out of the box behavior for the map control that keeps the extent when changing the size of the browser window.

To  have our own right click event on the map control, we need to make our right click event triggered  before Microsoft could take control for displaying the Silverlight configuration button. 

To handle the right click I add a trigger to the map control. This trigger is acting on the right mouse click down event. This event happens before the right mouse click up event and occurs before the right mouse click up event which is handled by Microsoft for the configuration of the Silverlight plug-in on the desktop.

<i:Interaction.Triggers>
  <i:EventTrigger EventName="MouseRightButtonDown" >
    <maphelper:RightClickMenu Command="{Binding HandleRightClick}" CommandParameter=""/>
  </i:EventTrigger>
</i:Interaction.Triggers>

First I create a MVVM command in the constructor of the ViewModel of the map control that will handle the processing for the right click event.

HandleRightClick = new DelegateCommand<object>(this.OnRightMouseClicked, this.CanRightMouseClicked);

In this command the final processing happens based on the location clicked. I then simply create an extent based on the point selected by adding an offset and perform a zoom operation.

private void OnRightMouseClicked(object arg)
{
  MapPoint point = arg as MapPoint;
  Envelope extent = new Envelope(point.X - 10000, point.Y - 10000, point.X + 10000, point.Y + 10000);
  gisOperations.ZoomTo(extent);
  return;
}

To finish the job, I implement a class derived from the TriggerAction class. When implementing the trigger action you need to override the following methods

  • Invoke
  • Onattached
I only use here the Invoke method. This method does the real work when the mouse right click is done.

In case of the map, I attached properties that contain the command and optional the command parameter and invoke parameter. In the example the parameters are not used.

public class RightClickMenu : TriggerAction<FrameworkElement>
{
  // Definition of dependency properties that could be usefull
  public static readonly DependencyProperty CommandParameterProperty =
    DependencyProperty.Register("CommandParameter", typeof(object), typeof(RightClickMenu), null);

  public static readonly DependencyProperty CommandProperty =
    DependencyProperty.Register("Command", typeof(ICommand), typeof(RightClickMenu), null);

  public static readonly DependencyProperty InvokeParameterProperty = DependencyProperty.Register(
   "InvokeParameter", typeof(object), typeof(RightClickMenu), null);

The invoke method is implemented as follow:

protected override void Invoke(object parameter)
{
  // Verify if it is a map
  MouseButtonEventArgs eventArgs = parameter as MouseButtonEventArgs;
  eventArgs.Handled = true;
  Map map = this.AssociatedObject as Map;
  if (map != null)
  {
    MapPoint mapPoint = map.ScreenToMap(eventArgs.GetPosition(map));
    ICommand command = this.ResolveCommand();
    if ((command != null) && command.CanExecute(this.CommandParameter))
    {
      command.Execute(mapPoint);
    }
  }
  return;
}

To eliminate the display of the Silverlight notice you must set the handling of the right mouse click event to ‘handled’. This stops the Microsoft handling of the right mouse up event.

  eventArgs.Handled = true;

A more general approach was done here to retrieve the MVVM command linked to the right mouse click. This approach allows the triggering of a command defined on another control, ex a button. This logic can be found in the ResolveCommand method.

private ICommand ResolveCommand()
{
  ICommand command = null;
  if (this.Command != null)
  {
    return this.Command;
  }
  // In the case the command is not defined within the triggger and you need to activate it, 
  // search is done for the context containing the command.
  var frameworkElement = this.AssociatedObject as FrameworkElement;
  if (frameworkElement != null)
  {
    object dataContext = frameworkElement.DataContext;
    if (dataContext != null)
    {
      PropertyInfo commandPropertyInfo = dataContext
         .GetType()
         .GetProperties(BindingFlags.Public | BindingFlags.Instance)
         .FirstOrDefault(
            p =>
            typeof(ICommand).IsAssignableFrom(p.PropertyType) &&
            string.Equals(p.Name, this.CommandName, StringComparison.Ordinal)
         );
      if (commandPropertyInfo != null)
      {
        command = (ICommand)commandPropertyInfo.GetValue(dataContext, null);
      }
    }
  }
  return command;
}

The above pattern can also be used to display a context menu instead of a direct action. Using the pattern of a dialog manager, you can display a menu with the needed action on the location clicked.


No comments:

Post a Comment