Accessing MapView methods like IdentifyGraphicsOverlaysAsync from viewmodel.

2606
6
04-03-2018 10:13 AM
BikeshMaharjan1
New Contributor III

I could bind map and graphic overlays but I want to identify graphics of certain layer and get it in viewmodel. Could someone advice me on how to do that? In code behind, I could use, "await mapview.IdentifyGraphicsOverlaysAsync".

Any help is appreciated.

Thank you!

0 Kudos
6 Replies
RichZwaap
Occasional Contributor III

I've attached a sample that demonstrates one way of doing this. It includes an IdentifyController utility class that I adapted from an Example App that Mara Stoica‌ is currently working on.  With it, you can listen for identify operations in your view-model by instantiating an IdentifyController and hooking to the IdentifyCompleted event like so:

// Initialize the identify controller, specifying the graphics overlay as the target of the identify operation
IdentifyController = new IdentifyController()
{
  Target = graphicsOverlay
};

// Listen for identify operations
IdentifyController.IdentifyCompleted += IdentifyController_IdentifyCompleted;‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

Then just add the logic for handling the identify results in the IdentifyCompleted event handler.

As far as wiring up an IdentifyController to a given MapView, the sample includes a MapViewExtensions.IdentifyController attached property.  So, with the view-model's IdentifyController instance exposed as a property, that the association can be made in XAML like so:

<esri:MapView Map="{Binding Map, Source={StaticResource MapViewModel}}"
              GraphicsOverlays="{Binding GraphicsOverlays, Source={StaticResource MapViewModel}}"
              samples:MapViewExtensions.IdentifyController="{Binding IdentifyController, Source={StaticResource MapViewModel}}"/>‍‍‍‍‍‍

Hope this helps.

0 Kudos
JoeHershman
MVP Regular Contributor

I've taken a different approach that I think works pretty clean.  I have an interface with an accessor of the MapView

    public interface IRuntimeControls
    {
        MapView MapView { get; }
    }‍‍‍‍

I implement this interface in the code behind of the class with the MapView and assign the MapView to the accessor.  Maybe this breaks some true purism of MVVM having code in the code behind.  But from here I can use dependency injection and share this class anywhere in the application.

    [Export(typeof(MobileAppMapView))]
    [Export(typeof(IRuntimeControls))]
    [PartCreationPolicy(CreationPolicy.Shared)]
    public partial class MobileAppMapView : IRuntimeControls
    {
        public MobileAppMapView()
        {
            InitializeComponent();

            MapView = EsriMapView;
            SharedGraphicsOverlay = new GraphicsOverlay();
            MapView.GraphicsOverlays.Add(SharedGraphicsOverlay);
        }

        public MapView MapView { get; }
        public GraphicsOverlay SharedGraphicsOverlay { get; }
    }‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

This is using MEF, but you could do same approach with Unity.   

The first line:

[Export(typeof(MobileAppMapView))]

 Is actually a necessity of using Prism regions for the UI composition, not with sharing the IRuntimeControls implementation across the project.

Now all I need to do is import (MEF construct) the IRuntimeControls in any ViewModel the MapView object is needed.

Thanks,
-Joe
0 Kudos
RichZwaap
Occasional Contributor III

You can certainly do that, and that abstraction gets around the view-model needing to know about specific screens within your app, but that's definitely not an MVVM-compliant approach.  I assume that you're passing IRuntimeControls to your view-model, and from there the view-model accesses the MapView?  If that's the case, then your view-model is directly accessing a component of the view.  That not only breaks the guideline that the view-model should not have specific knowledge of the view, but if you're actually making the interface be required by the view-model via dependency injection, then the design is a further step removed from MVVM by imposing requirements on the view in order to use the view-model.  That's the sort of coupling that employment of MVVM is intended to avoid.


On the other hand, the controller approach in the example allows the view-model to own and expose an object that will report identify operations to it - should the view happen to use it.  That last bit is key in that it means the view-model is not imposing any requirements on the view layer.

Maybe this breaks some true purism of MVVM having code in the code behind. 

It's a misconception that MVVM precludes having code in code-behind.  While MVVM would rule out many things that developers would otherwise commonly do in code-behind, the pattern really has nothing to do with whether something is done in XAML or code.  MVVM can still be employed in environments that don't even support XAML like native Xamarin  iOS or Android, and still offers the same benefits.  From the perspective of adhering to the pattern, it's fine, for instance, to consume something coming from your view-model layer in a view's code-behind, to instantiate a view-model, or to manage some sort of interaction between view-layer components.

Your approach is certainly valid and presumably very practically useful; there are many different ways of tackling this sort of problem, and solutions need not adhere to MVVM.  But I wanted to clarify a little bit about MVVM and how the approaches here do or don't fit the pattern.

0 Kudos
JoeHershman
MVP Regular Contributor

I know this discussion is old and maybe has been beaten like the proverbial dead horse, however, as we are making some large scale changes to our application I thought I would revisit the architecture of how identify is being done.

First to explain our application architecture, we are using a module application built on top of the prism framework.  Unity is used as a DI framework.  Modules are discovered at run time so different application configurations can be deployed based on what modules are present on a system.  While it is not required to use this approach, some MVVM framework that provides robust event aggregation is required.  Something like MVVM Light would also fit the bill.  I don't really understand trying to build a large scale application using MVVM without a good framework to support better command and event patterns.  

I will also continue to argue that the MapView does need to be exposed (and also that the provided sample is in actuality doing just that).  One place dependent on this is the Table of Contents control.  Because this is binds to the MapView and is in its own module there has to be a way to expose the MapView to the rest of the application or at minimum outside of the UserControl containing the MapView. 

All that said...As was pointed out and shown in the example the Controller approach is a really nice clean way to expose the MapView.  This basically uses the same approach as in the example except it just is used to expose the MapView.

So as in the example, but with a little renaming.

public class MapViewExtensions : DependencyObject
{
     /// <summary>
     /// Creates a MapViewController property
     /// </summary>
     public static readonly DependencyProperty MapViewControllerProperty = DependencyProperty.RegisterAttached(
          nameof(MapViewController),
          typeof(MapViewController),
          typeof(MapViewExtensions),
          new PropertyMetadata(null, OnMapViewControllerChanged));

     /// <summary>
     /// Invoked when the MapViewController's value has changed
     /// </summary>
     private static void OnMapViewControllerChanged(DependencyObject dependency, DependencyPropertyChangedEventArgs args)
     {
          if (args.NewValue is MapViewController)
          {
               ((MapViewController)args.NewValue).MapView = dependency as MapView;
          }
     }

     /// <summary>
     /// MapViewController getter method
     /// </summary>
     public static MapViewController GetMapViewController(DependencyObject d)
     {
          return (d as MapView)?.GetValue(MapViewControllerProperty) as MapViewController;
     }

     /// <summary>
     /// MapViewController setter method
     /// </summary>
     public static void SetMapViewController(DependencyObject d, MapViewController value)
     {
          (d as MapView)?.SetValue(MapViewControllerProperty, value);
     }
}

And the simplified controller

public class MapViewController : DependencyObject
{
     /// <summary>
     /// Gets or sets the MapView on which to perform identify operations
     /// </summary>
     public virtual MapView MapView { get; set; }
}

This gets to the fun part.  Identify is done using a TriggerAction which can then be attached to the MapView.  Also something to note is that our application identifies all the layers in the map, not just a single or multiple pre-defined layer.  Here is where the EventAggregator comes in, this is set as a DependencyProperty so it can be bound from the ViewModel.  The Map is also bound to the TriggerAction. 

/// <summary>
/// Custom action fired on GeoviewTappedEvent.  Either bind a Layer collection
/// of Layers to be identified or bind Map if want to identify all visible layers
/// </summary>
public class IndentifyAction : TriggerAction<MapView>
{
     private bool _doubleTapped = false;

     protected override async void Invoke(object parameter)
     {
          await Task.Delay(250);
          if ( _doubleTapped )
          {
               _doubleTapped = false;
               return;
          }

          if (!(AssociatedObject is MapView mapView)) return;
          if (!(parameter is GeoViewInputEventArgs args)) return;

          double tolerance = 10d;

          if ( Map != null )
          {
               var identifyLayerResults = new List<IdentifyLayerResult>();

               Mouse.OverrideCursor = Cursors.Wait;

               for (int i = Map.OperationalLayers.Count - 1; i >= 0; i--)
               {
                    FeatureLayer mapLayer = Map.OperationalLayers[i] as FeatureLayer;
                    if (mapLayer?.PopupDefinition == null || mapLayer.IsVisible == false ) continue;
                    if ( mapLayer.FeatureTable.TableName == null ) continue;

                    var result = await mapView.IdentifyLayerAsync(mapLayer, args.Position, tolerance, false, 10);
                    identifyLayerResults.Add(result);
               }

               Mouse.OverrideCursor = Cursors.Arrow;

               EventAggregator.GetEvent<IdentifyEvent>().Publish(new IdentifyEventArgs(args.Location, identifyLayerResults));

               return;
          }


          if (Layers != null)
          {
               //TODO
          }
     }

     protected override void OnAttached()
     {
          base.OnAttached();
          if (!(AssociatedObject is MapView mapView)) return;

          //This seems silly to me, but a GeoViewDoubleTapped fires both a tapped and a double tapped.
          //This indicates was double tapped
          mapView.GeoViewDoubleTapped += (s, e) => { _doubleTapped = true; };
     }

     //Bound Layers to be identified.
     public static readonly DependencyProperty LayersProperty = DependencyProperty.Register(
          "Layers", typeof(IEnumerable<Layer>), typeof(IndentifyAction), new PropertyMetadata(default(IEnumerable<Layer>)));

     public IEnumerable<Layer> Layers
     {
          get => (IEnumerable<Layer>) GetValue(LayersProperty);
          set => SetValue(LayersProperty, value);
     }   

     public static readonly DependencyProperty MapProperty = DependencyProperty.Register(
          "Map", typeof(Map), typeof(IndentifyAction), new PropertyMetadata(default(Map)));

     public Map Map
     {
          get => (Map) GetValue(MapProperty);
          set => SetValue(MapProperty, value);
     }

     public static readonly DependencyProperty EventAggregatorProperty = DependencyProperty.Register(
          "EventAggregator", typeof(IEventAggregator), typeof(IndentifyAction), new PropertyMetadata(default(IEventAggregator)));

     public IEventAggregator EventAggregator
     {
          get => (IEventAggregator) GetValue(EventAggregatorProperty);
          set => SetValue(EventAggregatorProperty, value);
     }    
}‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

The Xaml for the View containing the MapView looks like this.

<Grid>

     <esri:MapView x:Name="MapView" Map="{Binding Map}"
                      framework:MapViewExtensions.MapViewController="{Binding MapViewController}">
          <esri:MapView.Resources>
               <Style TargetType="esri:Callout">
                    <Setter Property="BorderBrush" Value="Transparent" />
                    <Setter Property="BorderThickness" Value="0" />
                    <Setter Property="Background" Value="{DynamicResource ApplicationBackgroundColorLight}"/>
                    <Setter Property="Padding" Value="0"/>
               </Style>
          </esri:MapView.Resources>

          <i:Interaction.Triggers>
               <i:EventTrigger EventName="GeoViewTapped">
                    <identify:IndentifyAction Map="{Binding Map}" EventAggregator="{Binding EventAggregator}"/>
               </i:EventTrigger>
          </i:Interaction.Triggers>
     </esri:MapView>
</Grid>

That pretty much does the trick.  At this point all that is needed is someone to listen for the event and show a popup if desired.  There is one kind of annoying thing I did do and that is use the wait cursor.  This is not needed and because this is async method if there is not a listener the identify just happens on a background thread and the user is none the wiser.  That does need some reconsideration.

There is now a module specific to identify.  It has the Controller injected through the constructor

public PopupViewModel(MapViewController controller)
{ 
     PopupPages = new ObservableCollection<PopupPage>();
     _controller = controller;

     Height = 200;
     Width = 370;
}

Subscribe to the event

EventAggregator.GetEvent<IdentifyEvent>().Subscribe(OnIdentify);

And respond to the event

private void OnIdentify(IdentifyEventArgs args)
{
     PopupPages.Clear();
     PopupPageCount = 0;
     CurrentFeatures.Clear();

     //How to be MVVM and still have a custom popup?  Not sure this fits the bill...
     //Stuff my popup view in a hidden region
     UIElement popupView = (UIElement)RegionManager.Regions[RegionNames.PopupRegion].Views.FirstOrDefault();
     Location = args.Location;

     if ( popupView == null ) return;

     foreach (var featureLayerResult in args.IdentifyLayerLayerResults)
     {
          foreach (var feature in featureLayerResult.GeoElements.OfType<Feature>())
          {
               if ( featureLayerResult.LayerContent is FeatureLayer featureLayer )
               {
                    AddPopupPage(feature, featureLayer);
               }
          }
     }

     _controller.MapView.ShowCalloutAt(Location, popupView);

}

And here one could say the MVVM is blown-up.  A custom control is used for the popup and this view does come into the ViewModel in order to be shown.  If a standard CalloutDefinition was used this would not be required.  In the back recesses of my mind I am trying to come up with a way you could do this with Navigation but that will take a lot more to figure out.

Thanks,
-Joe
JoeHershman
MVP Regular Contributor

As it seems someone actually read my early diatribe I have been able to come to a solution that does not require the MapViewController and is able to do everything with eventing and the above described Action.  This has made Identify and the MapView completely decoupled.  Display of Callouts is done within our 'MapModule' and just requires a MapPoint and a UIElement be passed as Event arguments

Setting Viewpoints/Zooming and Export of a MapView image are also done with eventing so these are now decoupled and do not require the MapView in any other part of the application.  I still have a hangup with MapViewInteractionOption because I need a way to know the existing setting and the LocationDisplay.  For these I created an IMapViewSettings which is initialized in my MapView and injected into modules were required. 

The LocationDisplay issue is that the API no longer allows you to set this in Xaml, in 10.x you could.  If that was possible I am pretty sure I could do what I need with data binding.  We have a custom LocationDataSource and so this needs to be set.  I am still looking at a way to do this with eventing, but the InteractionOptions issue seems unsolvable to me without the MapViewSettings class at this point.

Thanks,
-Joe
0 Kudos
JoeHershman
MVP Regular Contributor

I assume that you're passing IRuntimeControls to your view-model, and from there the view-model accesses the MapView?  If that's the case, then your view-model is directly accessing a component of the view.

I would argue this picks nits.  The MapView is an object implemented on a class.  No where in the application is there any knowledge of the implementation of IRuntimeControls, only that this interface exposes the MapView.  A class is implemented that is added to a container, the rest of the application has no knowledge of how it is implemented. 

In the example you posted the Controller exposes the MapView.  This is really no different and in the same way we don't now how that object was exposed.  Just so happens in the example the MapView is tied to the controller using a Dependency Property instead of exposed from the UserControl that contains the MapView control. 

In my application I could do the same with the IRuntimeControl implementation and nothing in the rest of the application changes.  Just for yucks, I did this.  In an application that is over 100,000 lines of code based on some quick testing of the places I know the MapView is used in the application nothing broke.

but if you're actually making the interface be required by the view-model via dependency injection, then the design is a further step removed from MVVM by imposing requirements on the view in order to use the view-model

I do not know what that means, " imposing requirements on the view in order to use the view-model."  There is no requirement on the view, there is a class that implements an interface that exposes the MapView class

The interface is not required by the view model, however, when the MapView is needed the interface can be injected (I'll avoid the conversation on whether MEF is truly DI).  We have a modular application, not only do none of the ViewModels know anything about the implementation of the IRuntimeControls interface, they don't even know where this is implementation lives, just that there is an interface and that interface provides access to a class of type MapView and if they ask, it will be provided.

To me the issue partially comes down to the GeoView/MapView providing so much functionality that there is almost no way to not share it across a large application.  The example uses a lot of code to perform something that was done in about 5 lines in my application (sharing MapView).  Also using event aggregation instead of using the approach of an event implemented on IdentifyController class all MapView events can be tied into and again, no view model has any knowledge or coupling to how these events are fired.

Just my thinking

Thanks,
-Joe
0 Kudos