MapView Events handled in ViewModel

1080
10
Jump to solution
11-01-2023 01:32 PM
Labels (3)
HS_PolkGIS
New Contributor II

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 Regular Contributor

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
Occasional Contributor II

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 Regular Contributor

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
New Contributor II

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 Regular Contributor

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
New Contributor II

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 Regular Contributor

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 Regular Contributor

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