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
{
public static readonly DependencyProperty MapViewControllerProperty = DependencyProperty.RegisterAttached(
nameof(MapViewController),
typeof(MapViewController),
typeof(MapViewExtensions),
new PropertyMetadata(null, OnMapViewControllerChanged));
private static void OnMapViewControllerChanged(DependencyObject dependency, DependencyPropertyChangedEventArgs args)
{
if (args.NewValue is MapViewController)
{
((MapViewController)args.NewValue).MapView = dependency as MapView;
}
}
public static MapViewController GetMapViewController(DependencyObject d)
{
return (d as MapView)?.GetValue(MapViewControllerProperty) as MapViewController;
}
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.
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)
{
}
}
protected override void OnAttached()
{
base.OnAttached();
if (!(AssociatedObject is MapView mapView)) return;
mapView.GeoViewDoubleTapped += (s, e) => { _doubleTapped = true; };
}
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();
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