Can I show callouts using MVVM ?

1582
4
07-24-2019 01:16 PM
by Anonymous User
Not applicable

I would like to use MVVM to show callouts.

I'm able to use code that  Thad Tilton‌ posted here in the Show Callout sample in the viewer.

It's not very pretty, but it works:

It doesn't use MVVM.  Here's the code:

private void Initialize()
{
    Basemap myBasemap = Basemap.CreateStreets();
    Map myMap = new Map(myBasemap);
    MyMapView.Map = myMap;
    MyMapView.GeoViewTapped += MyMapView_GeoViewTapped;
    //this.DataContext = _vm;
}

private void MyMapView_GeoViewTapped(object sender, esriCtrls.GeoViewInputEventArgs e)
{
    MapPoint mapLocation = e.Location;
    Geometry myGeometry = GeometryEngine.Project(mapLocation, SpatialReferences.Wgs84);
    MapPoint projectedLocation = (MapPoint)myGeometry;
    esriCtrls.Callout c = new esriCtrls.Callout()
    {
        Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.CornflowerBlue),
        BorderBrush = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.PaleVioletRed),
        BorderThickness = new System.Windows.Thickness(5, 5, 5, 3),
        Content = $"Lat: {projectedLocation.Y:F3} Long:{projectedLocation.X:F3}"
    };
    GeoView.SetViewOverlayAnchor(c, e.Location);
    MyMapView.Overlays.Items.Add(c);
    //_vm.MyCallouts.Add(c);
}

This is where I get lost, this xaml compiles:

<Grid>
    <esri:MapView x:Name="MyMapView">
        <esri:MapView.Overlays>
            <esri:OverlayItemsControl ItemsSource="{Binding MyOverlays}"/>
        </esri:MapView.Overlays>
    </esri:MapView>
    <Border Style="{StaticResource BorderStyle}">
        <TextBlock Text="Tap to show a callout."
                FontWeight="SemiBold"
                TextAlignment="Center" />
    </Border>
</Grid>

Here's my viewmodel (PropertyChangedBase implements INotifyPropertyChanged):

public class MyVM: PropertyChangedBase
{
    public ObservableCollection<esriCtrls.Callout> MyCallouts { get; private set; } =
        new ObservableCollection<esriCtrls.Callout>();
        
    public MyVM()
    {
    }
}

Uncommented/commented code to use vm:

private void Initialize()
{
    Basemap myBasemap = Basemap.CreateStreets();
    Map myMap = new Map(myBasemap);
    MyMapView.Map = myMap;
    MyMapView.GeoViewTapped += MyMapView_GeoViewTapped;
    this.DataContext = _vm;
}

private void MyMapView_GeoViewTapped(object sender, esriCtrls.GeoViewInputEventArgs e)
{
    MapPoint mapLocation = e.Location;
    Geometry myGeometry = GeometryEngine.Project(mapLocation, SpatialReferences.Wgs84);
    MapPoint projectedLocation = (MapPoint)myGeometry;
    esriCtrls.Callout c = new esriCtrls.Callout()
    {
        Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.CornflowerBlue),
        BorderBrush = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.PaleVioletRed),
        BorderThickness = new System.Windows.Thickness(5, 5, 5, 3),
        Content = $"Lat: {projectedLocation.Y:F3} Long:{projectedLocation.X:F3}"
    };
    GeoView.SetViewOverlayAnchor(c, e.Location);
    //MyMapView.Overlays.Items.Add(c);
    _vm.MyCallouts.Add(c);
}

I get a run-time error before I even tap on the screen:

System.Windows.Data Error: 2 : 
Cannot find governing FrameworkElement or FrameworkContentElement for target element. 
BindingExpression:Path=MyOverlays; DataItem=null; 
target element is 'OverlayItemsControl' (HashCode=19513753); 
target property is 'ItemsSource' (type 'IEnumerable')

With this xaml also fails:

<esri:MapView x:Name="MyMapView">
    <esri:MapView.Overlays>
        <esri:OverlayItemsControl 
        ItemsSource="{Binding Path=DataContext.MyOverlays, RelativeSource={RelativeSource AncestorType={x:Type esri:MapView}}}"/>
    </esri:MapView.Overlays>
</esri:MapView>

But with a different error:

System.Windows.Data Error: 4 : Cannot find source for binding with reference

'RelativeSource FindAncestor, AncestorType='Esri.ArcGISRuntime.UI.Controls.MapView', AncestorLevel='1''.

BindingExpression:Path=DataContext.MyOverlays; DataItem=null;

target element is 'OverlayItemsControl' (HashCode=16581154);

target property is 'ItemsSource' (type 'IEnumerable')

Looking at the live visual tree on the version that works (with no mvvm), I see the callout textbox under the mapview:

Any clues or suggestions are greatly appreciated!

Tags (2)
0 Kudos
4 Replies
ThadTilton
Esri Contributor

Hi Kirk,

I remember running into a similar issue when I tried implementing this via MVVM. Unfortunately, it was about 5 years ago, and I can't find how (or if!) I worked around it. 

From Google, I see that people have fixed similar binding issues (same error message at least) by using a proxy (such as a resource or hidden control) to provide the data context. For example: https://stackoverflow.com/questions/7660967/wpf-error-cannot-find-governing-frameworkelement-for-tar...

I see that Antti has an example of using popups that looks pretty good. He only binds to the text shown in the popup and the geometry for the anchor. He's showing info for a clicked feature, so he sets the binding context in the geoview_tapped event. https://community.esri.com/thread/190047-popups-in-arcgis-runtime-sdk-for-net#comment-666876

You might also want to check out callouts. Maybe they'll work better for what you need to do: https://github.com/Esri/arcgis-runtime-samples-dotnet/tree/master/src/WPF/ArcGISRuntime.WPF.Viewer/S...

I hope that helps, Thad

JoeHershman
MVP Regular Contributor

It's a fun exercise in architecture, and took me a few iterations to find something I was completely happy with in the limitations of the MapView needing to display the callout.  The MapView is a control so while it is possible to pass this to other parts of the application, it would break some MVVM principle (imo - although I have gone back and forth on this).

The approach I have used does require eventing, and so some outside framework is needed.  I use Prism, but MVVMLight would work.  Because the MapView is a control, it is not against MVVM to have code in the code behind of the View that contains the MapView control.  All the methods that actually display the popup are in in the MapView's code behind.

The code to identify is implemented as a custom TriggerAction which is attached to the MapView.  I have attached the IdentifiyAction code.

In Xaml of the MapView

<esri:MapView x:Name="MapView" Map="{Binding Map}" GraphicsOverlays="{Binding GraphicsOverlays}"
			  SketchEditor="{Binding SketchEditor}"
			  framework:MapViewExtensions.MapViewController="{Binding MapViewController}">
	<i:Interaction.Triggers>
		<i:EventTrigger EventName="GeoViewTapped">
			<identify:IndentifyAction Map="{Binding Map}" EventAggregator="{Binding EventAggregator}"/>
		</i:EventTrigger>
	</i:Interaction.Triggers>
</esri:MapView>‍‍‍‍‍‍‍‍‍

The EventAggregator is passed into the Action so it can fire off when the identify is completed and send the results.

In the code behind of the View that contain MapView the callout is displayed.  The Action passed in a custom arguments parameter which can really be defined however is needed

private void OnShowCallout(ShowCalloutEventArgs obj)
{
	try
	{
		MapView.ShowCalloutAt(obj.Location, obj.Callout);
	}
	catch (Exception e)
	{
		_log.Error(e.Message, e);
	}
}‍‍‍‍‍‍‍‍‍‍‍
Thanks,
-Joe
0 Kudos
by Anonymous User
Not applicable

Hi Joe -

I don't see IdentifiyAction code, did you forget to attach?

Thanks for replying!

0 Kudos
JoeHershman
MVP Regular Contributor

At the bottom under attachments is the IdentifyAction.cs.zip.  i. Implement a graphics identify in a identical pattern

Thanks,
-Joe
0 Kudos