Select to view content in your preferred language

MapView Events handled in ViewModel

2562
10
Jump to solution
11-01-2023 01:32 PM
HS_PolkGIS
Occasional Contributor

Does ESRI have a single example of a MapView event being handled from a ViewModel using the best MVVM approach possible?

I am using the basic Display a Map example from ESRI and added Prism for commands and the event aggregator. How would you handle the GeoViewTapped event in the MapViewModel?

 

Edit

Thank you for the responses already. I should have included this in the original post.

Using WPF and .NET 6 and starting with this tutorial.   https://developers.arcgis.com/net/maps-2d/tutorials/display-a-map/

0 Kudos
1 Solution

Accepted Solutions
JoeHershman
MVP Alum

So if you are using Prism, then you should be using the Prism container.  I think Prism 7 had an Autofac version, but they depreciated that. starting at Prism 8.  I would suggest Prism.DryIoc as that is the direction they are moving with Prism 9.  You want to add one of the Prism packages with a container either Prism.Unity or Prism.DryIoc and then let Prism do the rest

Prism provides a method in App.Xaml.cs to override with registrations. 

protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
	containerRegistry.Register<IComboBoxItem, ComboBoxItem>();
	containerRegistry.Register<IToolbarCommand, ToolbarCommand>();
	containerRegistry.RegisterSingleton<IConnectionCheck, PortalConnectionCheck>();
	containerRegistry.RegisterSingleton<ILog4NetFacade, Log4NetLogger>();
       
    //If you did not have Modules or Regions you could register a view for Navigation here
   containerRegistry.RegisterForNavigation<MobileAppMapView, MobileAppMapViewModel>();
}

 If you have Modules the initialization is done in the IModule implementation and would be along these lines

/// <summary>
/// Map Module is primary application module responsible for inital loading
/// of the map and all interations with the MapView
/// </summary>

public class MapModule : ModuleBase
// I have a ModuleBase class that implements IModule so that common initialization code is put there
{
	public override void RegisterTypes(IContainerRegistry containerRegistry)
	{
		base.RegisterTypes(containerRegistry);
		containerRegistry.Register<IMapLoadService, MapLoadService>();
	}

	public override void OnInitialized(IContainerProvider containerProvider)
	{
		base.OnInitialized(containerProvider);

		//RegionManager comes from base class this is is how one gets RegionManager in Module
		// RegionManager = containerProvider.Resolve<IRegionManager>();
		RegionManager.RegisterViewWithRegion(RegionNames.MapRegion, typeof(MobileAppMapView));
	}
}

There is no need to be setting a DataContext, Prism does that for you

As an aside:  I would avoid naming my View MapView, I find this confuses things sometimes because it is the same name as the esri class

Thanks,
-Joe

View solution in original post

10 Replies
BjørnarSundsbø1
Frequent Contributor

Hi,

This post covered some of this, but unfortunately it appears the links are broken. https://community.esri.com/t5/arcgis-maps-sdks-native-blog/blogpost-navigating-the-mapview-from-a-vi...

I don't have the opportunity to search or access my version of this code, but look for MapViewController in some of Morten's git repositories. While not a full answer, at least it is something to point you in the right direction.

I would implement the GeoViewTapped through custom WeakEventManager implementations to avoid memory leaks.

dotMorten_esri
Esri Notable Contributor

You don't really need a controller for reacting to events (see my other response). For performing operations on the view, this PR in the works might be of interest: https://github.com/Esri/arcgis-maps-sdk-dotnet-toolkit/pull/528 (you could just pull the controller code and have a play with it until this is in the toolkit)

dotMorten_esri
Esri Notable Contributor

The approach is the same you use with any view events - the Maps SDK isn't special in this regard. Depending on which UI framework you use, it's slightly different though. For instance MAUI using the MAUI Community Toolkit you can execute a command from an event:

 

<esri:MapView Map="{Binding Map, Source={StaticResource VM}}">
    <esri:MapView.Behaviors>
        <mauitoolkit:EventToCommandBehavior EventName="GeoViewTapped"
            x:TypeArguments="esri:GeoViewInputEventArgs"
            Command="{Binding GeoViewTappedCommand, Source={StaticResource VM}}" />
     </esri:MapView.Behaviors>
</esri:MapView>

 

In WPF you can use the Behaviors SDK to do the same thing:

 

<esri:MapView Map="{Binding Map, Source={StaticResource VM}}">
	<Behaviors:Interaction.Triggers>
		<Behaviors:EventTrigger EventName="GeoViewTapped" >
			<Behaviors:InvokeCommandAction Command="{Binding GeoViewTappedCommand, Source={StaticResource VM}}" PassEventArgsToCommand="True" />
		</Behaviors:EventTrigger>
	</Behaviors:Interaction.Triggers>
</esri:MapView>

 

In WinUI and UWP, you can bind straight to a method in your VM using x:Bind:

 

 GeoViewTapped="{x:Bind VM.OnGeoViewTapped}"

 

 
Prism also offers some documentation on this:
 - https://prismlibrary.com/docs/wpf/interactivity/event-to-command.html
 - https://prismlibrary.com/docs/maui/behaviors/eventtocommandbehavior.html

JoeHershman
MVP Alum

You don't actually say what you are developing in.  If you have Prism setup, I would use the CommandToEvent from Prism instead of the MAUI toolkit like Morten shows below.  But basically same principle.   As a rule if using Prism stick with all of their implementations, don't mix and match MAUI toolkit and Prism implementations

Or you could just fire your own events, which is what I have done.  You can inject IEventAggregator into the code behind that has the MapView .  I create an Event for each of the MapView interactions and wire them up

 

private void WireUpMapViewEventHandlers(IEventAggregator eventAggregator)
{
	MapView.GeoViewTapped += (s, e) => { eventAggregator.GetEvent<GeoViewTappedEvent>().Publish(e); };
	MapView.GeoViewDoubleTapped += (s, e) => { eventAggregator.GetEvent<GeoViewDoubleTappedEvent>().Publish(e); };
	MapView.GeoViewHolding += (s, e) => { eventAggregator.GetEvent<GeoViewHoldingEvent>().Publish(e); };
	MapView.ViewpointChanged += (s, e) => { eventAggregator.GetEvent<ViewpointChangedEvent>().Publish(MapView.VisibleArea.Extent); };
	MapView.NavigationCompleted += (s, e) => { eventAggregator.GetEvent<NavigationCompletedEvent>().Publish(MapView.VisibleArea.Extent); };
}

 

This way everything if just published and I can hook into in any Module

Thanks,
-Joe
HS_PolkGIS
Occasional Contributor

Joe, thank you for the response on this and your snippet is exactly what I am looking for. However, I am a bit confused on how to achieve this.

How are you injecting the IEventAggregator into the View?

For ViewModels, I would pass the IEventAggregator as a parameter in the constructor.

0 Kudos
JoeHershman
MVP Alum

You still haven't mentioned what framework you are using. 

You should be able to inject the IEventAggregator into the View code behind.  Prism will handle this.  I have WPF and Xamarin Forms code that use this approach for events associated to View updates or actions

public MobileAppMapView(IEventAggregator eventAggregator)
{
	_eventAggregator = eventAggregator;
	InitializeComponent();
	
	WireUpMapViewEventHandlers(eventAggregator);
	SetupEventListeners(eventAggregator);
}

I have not done this with a View using regions in Xamarin, because when I originally built out my Xamarin architecture Prism hadn't introduced Regions in Xamarin yet.  But it should still work

Thanks,
-Joe
HS_PolkGIS
Occasional Contributor

My framework is WPF and I have tried what you suggested but I run into an error when adding a parameter to the View Constructor.

This is where I register (autofac) the View in the Application_Startup, the XAML where the view is used, and the View's code behind. The XAML shows a warning and throws a runtime error due to lacking a parameter-less constructor.

 

builder.RegisterType<MapView>().AsSelf();
<!--This has a warning for no accessible constructors-->
<mViews:MapView DataContext="{Binding MapViewModel}" />
private IEventAggregator _eventAggregator;
public MapView(IEventAggregator eventAggregator)
{
    InitializeComponent();

    _eventAggregator = eventAggregator;
    RegisterMapViewEvents(_eventAggregator);

}

private void RegisterMapViewEvents(IEventAggregator eventAggregator)
{
    MyMapView.GeoViewTapped += (s, e) => { eventAggregator.GetEvent<OnClickMapEvent>().Publish(e); };
}

 

 

 

 

0 Kudos
JoeHershman
MVP Alum

So if you are using Prism, then you should be using the Prism container.  I think Prism 7 had an Autofac version, but they depreciated that. starting at Prism 8.  I would suggest Prism.DryIoc as that is the direction they are moving with Prism 9.  You want to add one of the Prism packages with a container either Prism.Unity or Prism.DryIoc and then let Prism do the rest

Prism provides a method in App.Xaml.cs to override with registrations. 

protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
	containerRegistry.Register<IComboBoxItem, ComboBoxItem>();
	containerRegistry.Register<IToolbarCommand, ToolbarCommand>();
	containerRegistry.RegisterSingleton<IConnectionCheck, PortalConnectionCheck>();
	containerRegistry.RegisterSingleton<ILog4NetFacade, Log4NetLogger>();
       
    //If you did not have Modules or Regions you could register a view for Navigation here
   containerRegistry.RegisterForNavigation<MobileAppMapView, MobileAppMapViewModel>();
}

 If you have Modules the initialization is done in the IModule implementation and would be along these lines

/// <summary>
/// Map Module is primary application module responsible for inital loading
/// of the map and all interations with the MapView
/// </summary>

public class MapModule : ModuleBase
// I have a ModuleBase class that implements IModule so that common initialization code is put there
{
	public override void RegisterTypes(IContainerRegistry containerRegistry)
	{
		base.RegisterTypes(containerRegistry);
		containerRegistry.Register<IMapLoadService, MapLoadService>();
	}

	public override void OnInitialized(IContainerProvider containerProvider)
	{
		base.OnInitialized(containerProvider);

		//RegionManager comes from base class this is is how one gets RegionManager in Module
		// RegionManager = containerProvider.Resolve<IRegionManager>();
		RegionManager.RegisterViewWithRegion(RegionNames.MapRegion, typeof(MobileAppMapView));
	}
}

There is no need to be setting a DataContext, Prism does that for you

As an aside:  I would avoid naming my View MapView, I find this confuses things sometimes because it is the same name as the esri class

Thanks,
-Joe
JoeHershman
MVP Alum

Attached is a sample of Prism setup using the sample app shown in help.  Even with the APIKey I am getting a token error, but the general setup of Prism with injecting IEventAggregator all works

 

Thanks,
-Joe