Select to view content in your preferred language

LocationDataSource.LocationChanged not firing

933
6
Jump to solution
09-16-2022 06:51 AM
ViktorSafar
Frequent Contributor

Hi

I have a WPF Prism app with a MapView which is getting a Map from the viewmodel. 

MainMapUC.xaml

    <esri:MapView        
        x:Name="MainMapView"            
        Map="{Binding Map}">

        <ei:Interaction.Behaviors>            
            <bh:MapViewLocationDataChangedBehavior Command="{Binding UpdateLocationCommand}" />
        </ei:Interaction.Behaviors>

    </esri:MapView>

 

I created a custom LocationDataSource by extending the `LocationDataSource` abstract class, exposed it via `IGeoLocationService` , and attached this location source to the MapView (later, I also used the `SimulatedLocationDataSource`).

MainMapUC.xaml.cs

		public MainMapUC(IGeoLocationService geoLocationService, IEventAggregator eventAggregator)
		{
			InitializeComponent();

			MainMapView.LocationDisplay.DataSource = geoLocationService.GetLocationDataSource();
			MainMapView.LocationDisplay.AutoPanMode = Esri.ArcGISRuntime.UI.LocationDisplayAutoPanMode.Recenter;	

			eventAggregator.GetEvent<GeoLocationEnabledEvent>().Subscribe(enable => MainMapView.LocationDisplay.IsEnabled = enable);
		}

 

I would like to get the Location object when it changes so I thought to hook up to the `LocationDataSource.LocationChanged` event but it's never fired.

 

I found this solution that is exposing MapView properties via Behavior which is how I am hooking up to the event.

https://github.com/marceloctorres/GeonetPost.WPF/blob/master/GeonetPost.WPF/

	public class MapViewLocationDataChangedBehavior : Behavior<MapView>
	{        
        public static readonly DependencyProperty CommandProperty = 
                DependencyProperty.Register(
                    nameof(Command),
                    typeof(ICommand),
                    typeof(MapViewLocationDataChangedBehavior)
                );

        public ICommand Command
        {
            get { return (ICommand)GetValue(CommandProperty); }
            set { SetValue(CommandProperty, value); }
        }

        protected override void OnAttached()
        {
			this.AssociatedObject.LocationDisplay.DataSource.LocationChanged += DataSource_LocationChanged;
        }

		
		protected override void OnDetaching()
        {
			this.AssociatedObject.LocationDisplay.DataSource.LocationChanged -= DataSource_LocationChanged;
        }

		private void DataSource_LocationChanged(object sender, Esri.ArcGISRuntime.Location.Location e)
		{
            if (this.Command != null)
            {
                if (this.Command.CanExecute(e))
                {
                    this.Command.Execute(e);
                }
            }
        }
	}

 

MainMenuVM.cs

	internal class MainMapVM : BindableBase
	{
		private IEventAggregator _eventAggregator;
		public MainMapVM(IEventAggregator eventAggregator)
		{
			Map = new Map()
			{
				Basemap = new Basemap(BasemapStyle.ArcGISStreets),
				InitialViewpoint = new Viewpoint(59, 6, 1000000),
			};

			UpdateLocationCommand = new DelegateCommand<Location>(UpdateLocationAction, CanUpdateLocation);
			_eventAggregator = eventAggregator;			
		}

		private void UpdateLocationAction(Location newLocation)
		{
			_eventAggregator.GetEvent<GeoLocationChangedEvent>().Publish(newLocation);
		}
		private bool CanUpdateLocation(Location newLocation)
		{
			return true;
		}

		public ICommand UpdateLocationCommand { get; private set; }

		private Map _map;
		public Map Map
		{
			get { return _map; }
			set { SetProperty(ref _map, value); }
		}
	}

 

No matter what I do, I couldn't get the LocationDataSource events to fire.

0 Kudos
1 Solution

Accepted Solutions
JoeHershman
MVP Regular Contributor

Trying to be clear what you are trying to do.  You are having the behavior trigger the command on every location changed if I am reading correctly.

I have taken a different approach and I inject the interface that exposes my LocationDataSource into the code behind and wire up there.  You are already injecting your IGeoLocationService so not much different.  I do have some differing ideas on how I did things.  One thing is I use a control I made up to share my MapView among different Views in the application, but I think things will work the same either way.  You don't show IGeoLocationService but have an idea what you are doing.

I've got ILocationProvider which would seem to do the same as your IGeoLocationService 

public interface ILocationProvider
{
	event EventHandler<ConnectionStatusChangedEventArgs> ConnectionStatusChanged;

	LocationDataSource LocationDataSource { get; }
	bool IsConnected { get; }
	bool ShowLocation { get; set; }
	string Receiver { get; }
}

This implementation would then expose any type of LocationDataSource I want.  I have recently switched to using the NmeaLocationDataSource but all this worked with a custom LocationDataSource prior to NmeaLocationDataSource being added to the API.  Below shows different implementations that I have and can swap out (SxBlueLocationProvider was the previous custom one used before NMEA was added to API)

//containerRegistry.RegisterSingleton<ILocationProvider, SxBlueLocationProvider>();
containerRegistry.RegisterSingleton<ILocationProvider, SystemLocationProvider>();
//containerRegistry.RegisterSingleton<ILocationProvider, NmeaLocationProvider>();

This part would be different because I am doing as a control, and so instead of injecting anything they are setup as BindableProperties

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

public ILocationProvider LocationProvider
{
	get => (ILocationProvider) GetValue(LocationProviderProperty);
	set => SetValue(LocationProviderProperty, value);
}

public static readonly BindableProperty EventAggregatorProperty = ...

public static readonly BindableProperty LocationProviderProperty = ...

You should be able to just inject into code behind like you do.  I think I have the relevant pieces included if you are setup with the MapView in a view

private readonly ILocationProvider _provider;
private readonly IEventAggregator _eventAggregator;
		
public MainMapUC(ILocationProvider provider, IEventAggregator eventAggregator)
{
        _provide = provide;
        _eventAggregator = eventAggregator;
	InitializeComponent();
	
	mapView.PropertyChanged += (s, e) =>
	{
		//LocationDisplay is not set when MapView created
		if ( e.PropertyName == nameof(mapView.LocationDisplay) )
		{
			OnPropertyChanged(nameof(LocationProvider));
		}
	};
}


protected override void OnPropertyChanged(string propertyName = null)
{
	base.OnPropertyChanged(propertyName);

	switch (propertyName)
	{
		case nameof(LocationProvider):
			SetupLocationDataSource();
			break;
	}
}

private async void SetupLocationDataSource()
{
	try
	{
		MainThread.BeginInvokeOnMainThread(() =>
			mapView.LocationDisplay.DataSource = _provider.LocationDataSource);

		if ( mapView.LocationDisplay.DataSource?.Status == LocationDataSourceStatus.Stopped )
		{
			await mapView.LocationDisplay.DataSource?.StartAsync()!;
		}
	}
	catch (Exception e)
	{
		Log?.Error(e, e.Message);
		
	}
}

In the code behind we are now tying the LocationDataSource to our map view.  At this point we should be using the custom LocationDataSource

Now you can setup listening for LocationChanged in any ViewModel you want.

public EditControlViewModel(IEventAggregator eventAggregator, ILocationProvider locationProvider, IFeatureAttributes featureAttributes) : base(eventAggregator)
{
	_locationProvider = locationProvider;
	_featureAttributes = featureAttributes;

	try
	{
		if ( _locationProvider.LocationDataSource != null )
		{
			if ( _locationProvider.LocationDataSource.Status == LocationDataSourceStatus.Started )
			{
				_locationProvider.LocationDataSource.LocationChanged += OnLocationChanged;
			}
			else
			{
				void OnLocationDataSourceStatusChanged(object s, LocationDataSourceStatus status)
				{
					if ( status == LocationDataSourceStatus.Started )
					{
						_locationProvider.LocationDataSource.LocationChanged += OnLocationChanged;

						//Remove Status Changed listener
						_locationProvider.LocationDataSource.StatusChanged -= OnLocationDataSourceStatusChanged;
					}
				}

				//If LocationDataSource has not yet started listen for Status Changed
				_locationProvider.LocationDataSource.StatusChanged += OnLocationDataSourceStatusChanged;
			}
		}
	}
	catch (Exception e)
	{
		Log?.Error(e, e.Message);
	}
}

private void OnLocationChanged(object sender, Esri.ArcGISRuntime.Location.Location location)
{
	_currentLocation = location;
}

 I did a little extra stuff in there to make sure the LocationDataSource is up and running before tying the event to it.  Also my personal feelin is to try and avoid sending off Prism events from LocationChanged handler because that event is fired so frequently.

Thanks,
-Joe

View solution in original post

6 Replies
ViktorSafar
Frequent Contributor

My own implementation of LocationDataSource:

	internal class MyLocationDataSource : LocationDataSource, ILocationSource
	{

		public LocationDataSource LocationDataSource => this;

		public async Task GetLocation()
		{
			
			var result = ... // location obtained from a source

			var newLocation = new Location(
				new Esri.ArcGISRuntime.Geometry.MapPoint(result.longitude, result.latitude, SpatialReferences.Wgs84),
				50,
				0,
				0,
				false
			);

			base.UpdateLocation(newLocation);	// <<<<< I thought this would fire the LocationChanged event		
		}

		protected override async Task OnStartAsync()
		{
			await GetLocation();
		}

		protected override Task OnStopAsync()
		{
			return Task.CompletedTask;
		}

 

I can see the dot on the MapView but the LocationChanged event is never fired.

 

And as mentioned, I also tried the SimulationLocationDataSource. With this, I can see the dot moving around the map, but the LocationChanged event is never fired

			var simulatedLocations = (Polyline)Geometry.FromJson("{\"paths\":[[[-13185646.046666779,4037971.5966668758],[-13185586.780000051,4037827.6633333955], [-13185514.813333312,4037709.1299999417],
...
[-13182618.624747934,4034679.7416197238]]], \"spatialReference\":{\"wkid\":102100,\"latestWkid\":3857}}");
			var parameters = new SimulationParameters(DateTimeOffset.Now, 150);
			var simulatedSource = new SimulatedLocationDataSource();
			simulatedSource.SetLocationsWithPolyline(simulatedLocations, parameters);

 

0 Kudos
dotMorten_esri
Esri Notable Contributor

Regarding this:

protected override async Task OnStartAsync()
{
    await GetLocation();
}

This method shouldn't get the location. It should start monitoring for locations (ie start listening to an event source, or launch a thread that starts monitoring in a loop), and when locations gets updated, you call base.UpdateLocation. Stop async would then stop that thread (or unsubscribe depending on what you're getting locations from)

0 Kudos
ViktorSafar
Frequent Contributor

Thanks, Morten. I agree this is not the best show of how it should work. In this case the location is ever obtained just a single time as it was IP based - so I thought it was OK to trigger it from OnStartAsync().

0 Kudos
JoeHershman
MVP Regular Contributor

Trying to be clear what you are trying to do.  You are having the behavior trigger the command on every location changed if I am reading correctly.

I have taken a different approach and I inject the interface that exposes my LocationDataSource into the code behind and wire up there.  You are already injecting your IGeoLocationService so not much different.  I do have some differing ideas on how I did things.  One thing is I use a control I made up to share my MapView among different Views in the application, but I think things will work the same either way.  You don't show IGeoLocationService but have an idea what you are doing.

I've got ILocationProvider which would seem to do the same as your IGeoLocationService 

public interface ILocationProvider
{
	event EventHandler<ConnectionStatusChangedEventArgs> ConnectionStatusChanged;

	LocationDataSource LocationDataSource { get; }
	bool IsConnected { get; }
	bool ShowLocation { get; set; }
	string Receiver { get; }
}

This implementation would then expose any type of LocationDataSource I want.  I have recently switched to using the NmeaLocationDataSource but all this worked with a custom LocationDataSource prior to NmeaLocationDataSource being added to the API.  Below shows different implementations that I have and can swap out (SxBlueLocationProvider was the previous custom one used before NMEA was added to API)

//containerRegistry.RegisterSingleton<ILocationProvider, SxBlueLocationProvider>();
containerRegistry.RegisterSingleton<ILocationProvider, SystemLocationProvider>();
//containerRegistry.RegisterSingleton<ILocationProvider, NmeaLocationProvider>();

This part would be different because I am doing as a control, and so instead of injecting anything they are setup as BindableProperties

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

public ILocationProvider LocationProvider
{
	get => (ILocationProvider) GetValue(LocationProviderProperty);
	set => SetValue(LocationProviderProperty, value);
}

public static readonly BindableProperty EventAggregatorProperty = ...

public static readonly BindableProperty LocationProviderProperty = ...

You should be able to just inject into code behind like you do.  I think I have the relevant pieces included if you are setup with the MapView in a view

private readonly ILocationProvider _provider;
private readonly IEventAggregator _eventAggregator;
		
public MainMapUC(ILocationProvider provider, IEventAggregator eventAggregator)
{
        _provide = provide;
        _eventAggregator = eventAggregator;
	InitializeComponent();
	
	mapView.PropertyChanged += (s, e) =>
	{
		//LocationDisplay is not set when MapView created
		if ( e.PropertyName == nameof(mapView.LocationDisplay) )
		{
			OnPropertyChanged(nameof(LocationProvider));
		}
	};
}


protected override void OnPropertyChanged(string propertyName = null)
{
	base.OnPropertyChanged(propertyName);

	switch (propertyName)
	{
		case nameof(LocationProvider):
			SetupLocationDataSource();
			break;
	}
}

private async void SetupLocationDataSource()
{
	try
	{
		MainThread.BeginInvokeOnMainThread(() =>
			mapView.LocationDisplay.DataSource = _provider.LocationDataSource);

		if ( mapView.LocationDisplay.DataSource?.Status == LocationDataSourceStatus.Stopped )
		{
			await mapView.LocationDisplay.DataSource?.StartAsync()!;
		}
	}
	catch (Exception e)
	{
		Log?.Error(e, e.Message);
		
	}
}

In the code behind we are now tying the LocationDataSource to our map view.  At this point we should be using the custom LocationDataSource

Now you can setup listening for LocationChanged in any ViewModel you want.

public EditControlViewModel(IEventAggregator eventAggregator, ILocationProvider locationProvider, IFeatureAttributes featureAttributes) : base(eventAggregator)
{
	_locationProvider = locationProvider;
	_featureAttributes = featureAttributes;

	try
	{
		if ( _locationProvider.LocationDataSource != null )
		{
			if ( _locationProvider.LocationDataSource.Status == LocationDataSourceStatus.Started )
			{
				_locationProvider.LocationDataSource.LocationChanged += OnLocationChanged;
			}
			else
			{
				void OnLocationDataSourceStatusChanged(object s, LocationDataSourceStatus status)
				{
					if ( status == LocationDataSourceStatus.Started )
					{
						_locationProvider.LocationDataSource.LocationChanged += OnLocationChanged;

						//Remove Status Changed listener
						_locationProvider.LocationDataSource.StatusChanged -= OnLocationDataSourceStatusChanged;
					}
				}

				//If LocationDataSource has not yet started listen for Status Changed
				_locationProvider.LocationDataSource.StatusChanged += OnLocationDataSourceStatusChanged;
			}
		}
	}
	catch (Exception e)
	{
		Log?.Error(e, e.Message);
	}
}

private void OnLocationChanged(object sender, Esri.ArcGISRuntime.Location.Location location)
{
	_currentLocation = location;
}

 I did a little extra stuff in there to make sure the LocationDataSource is up and running before tying the event to it.  Also my personal feelin is to try and avoid sending off Prism events from LocationChanged handler because that event is fired so frequently.

Thanks,
-Joe
ViktorSafar
Frequent Contributor

Ah, beautiful. I totally overcomplicated it.

I simplified to IGeoLocationService to ILocationProvider (registers as singleton):

	public interface ILocationProvider
	{
		public LocationDataSource LocationDataSource { get; }
	}

and removed all of the Behavior/Command stuff

	public partial class MainMapUC : UserControl
	{
		public MainMapUC(ILocationProvider locationProvider, IEventAggregator eventAggregator)
		{
			InitializeComponent();

			MainMapView.LocationDisplay.DataSource = locationProvider.LocationDataSource;
			MainMapView.LocationDisplay.AutoPanMode = Esri.ArcGISRuntime.UI.LocationDisplayAutoPanMode.Recenter;	

			eventAggregator.GetEvent<GeoLocationEnabledEvent>().Subscribe(enable => MainMapView.LocationDisplay.IsEnabled = enable);			
		}
	}
	internal class MainMapVM : BindableBase
	{
		private readonly IEventAggregator _eventAggregator;
		private readonly ILocationProvider _locationProvider;

		public MainMapVM(IEventAggregator eventAggregator, ILocationProvider locationProvider)
		{
			_eventAggregator = eventAggregator;
			_locationProvider = locationProvider;

			Map = new Map()
			{
				Basemap = new Basemap(BasemapStyle.ArcGISStreets),
				InitialViewpoint = new Viewpoint(0, 0, 1000000),
			};
			
			_locationProvider.LocationDataSource.LocationChanged += (sender, e) => _eventAggregator.GetEvent<GeoLocationChangedEvent>().Publish(e);
		}

		private Map _map;
		public Map Map
		{
			get { return _map; }
			set { SetProperty(ref _map, value); }
		}
	}

 

and all is working perfectly.

 

Thanks for setting me on the right path!

0 Kudos
dotMorten_esri
Esri Notable Contributor

My guess would be that when you set up the `event this.AssociatedObject.LocationDisplay.DataSource.LocationChanged` you're doing it to the datasource, before you're reassigning the datasource, but your code never checks is the datasource changes, where you'd then unsubscribe from the old one and subscribe to the new one. At least double check that this event handler is hooked up _after_ you assign the new datasource.

0 Kudos