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.
Solved! Go to Solution.
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.
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);
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)
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().
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.
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!
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.