Display point graphic from mouse click

4343
11
Jump to solution
06-21-2021 03:20 PM
johnmarker
New Contributor III

Hello,

I want to be able to register a mouse click event and then plot a point graphic at that location. Right now I get the mouse click, get the location of that mouse click and then use those coordinates to create a graphic. After doing all this the point still does not display onto the map..

 

//MainWindow.xaml.cs

        private void MainMapView_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            MapViewModel mvm = new MapViewModel();
            Point p = e.GetPosition(MainMapView);
            MapPoint mp = MainMapView.ScreenToLocation(e.GetPosition(MainMapView));
            mvm.createPoint(mp);
        }


//MapViewModel.cs

        public void createPoint(MapPoint point)
        {
            // Create a new graphics overlay to contain a variety of graphics.
            var graphicsOverlay = new GraphicsOverlay();

            // Add the overlay to a graphics overlay collection.
            GraphicsOverlayCollection overlays = new GraphicsOverlayCollection
            {
                graphicsOverlay
            };

            // Set the view model's "GraphicsOverlays" property (will be consumed by the map view).
            this.GraphicsOverlays = overlays;

            var mapPoint = new MapPoint(point.X, point.Y, SpatialReferences.Wgs84);

            // Create a symbol to define how the point is displayed.
            var pointSymbol = new SimpleMarkerSymbol
            {
                Style = SimpleMarkerSymbolStyle.X,
                Color = System.Drawing.Color.Red,
                Size = 10.0
            };

            // Add an outline to the symbol.
            pointSymbol.Outline = new SimpleLineSymbol
            {
                Style = SimpleLineSymbolStyle.Solid,
                Color = System.Drawing.Color.Red,
                Width = 2.0
            };

            // Create a point graphic with the geometry and symbol.
            var pointGraphic = new Graphic(mapPoint, pointSymbol);

            // Add the point graphic to graphics overlay.
            graphicsOverlay.Graphics.Add(pointGraphic);
        }
0 Kudos
2 Solutions

Accepted Solutions
NathanCastle1
Esri Contributor

Hi,

It looks like there are a few problems in the code. If you are using the default basemaps, the map's spatial reference will be WebMercator. Currently, the code is creating a new point with WebMercator coordinates while having a WGS84 spatial reference.

 

var mapPoint = new MapPoint(point.X, point.Y, SpatialReferences.Wgs84);

 

should change to 

 

var mapPoint = new MapPoint(point.X, point.Y, point.SpatialReference);

 

But note that this step shouldn't be necessary at all. You could reference `point` directly without creating `mapPoint`.

I also recommend a few other changes:

  • Consider using `GeoViewTapped` instead of `MouseLeftButtonUp`, as that will save you from needing to call `ScreenToLocation` https://developers.arcgis.com/net/wpf/api-reference/html/E_Esri_ArcGISRuntime_UI_Controls_GeoView_Ge... GeoViewInputEventArgs has a Location property.

  • You should be able to add an overlay to the overlay collection rather than re-creating the collection each time. You can also add multiple graphics to a single overlay. If you just want to be able to draw graphics on the map, you should only need to set up the overlay once at startup.
  • The ViewModel is being re-created every time the user clicks, and it isn't clear that the view is ever being re-configured to reference that ViewModel. The ViewModel would typically be created for the view only once. As written, I don't think you would ever see points, because a new ViewModel is created without the view being updated.
  • Creating the symbol for the graphics each time is perfectly valid code, but there is a simpler way. You can define a renderer on the overlay and any graphics you add will be drawn with the defined symbol. There is an example here: https://github.com/Esri/arcgis-runtime-samples-dotnet/tree/main/src/WPF/ArcGISRuntime.WPF.Viewer/Sam... 

I hope that helps. Let me know if you have any other questions.

View solution in original post

NathanCastle1
Esri Contributor

When a graphic is added to a graphics overlay that is already in a mapview or sceneview, the graphic will be shown automatically, assuming there is a renderer or the graphic is configured with a symbol.

Based on the code you shared, it looks like you're following the pattern set in the template projects and the developer tutorials. If that is the case, you will see something like the following in your MainWindow.xaml file:

<Window.Resources>
    <local:MapViewModel x:Key="MapViewModel" />
</Window.Resources>

That code will create an instance of your viewmodel when InitializeComponent is called.

The MapView's Map and GraphicsOverlays properties are set via binding:

<esri:MapView x:Name="MainMapView"
              Map="{Binding Map, Source={StaticResource MapViewModel}}"
              GraphicsOverlays="{Binding GraphicsOverlays, Source={StaticResource MapViewModel}}" />

The view is bound to the specific instance of the viewmodel that is added to the `Window.Resources` dictionary. Your code is creating another instance of viewmodel, but that instance is never connected to the view in any way, so the view won't update to reflect changes in viewmodel state. You could fix this by re-setting up bindings when you create the viewmodel, or by using the existing viewmodel instance created in XAML.

I recommend updating your code to reference the copy of the ViewModel created in XAML:

You could use something like the following to get a reference to the viewmodel:

mvm = FindResource("MapViewModel") as MapViewModel;

 

View solution in original post

11 Replies
NathanCastle1
Esri Contributor

Hi,

It looks like there are a few problems in the code. If you are using the default basemaps, the map's spatial reference will be WebMercator. Currently, the code is creating a new point with WebMercator coordinates while having a WGS84 spatial reference.

 

var mapPoint = new MapPoint(point.X, point.Y, SpatialReferences.Wgs84);

 

should change to 

 

var mapPoint = new MapPoint(point.X, point.Y, point.SpatialReference);

 

But note that this step shouldn't be necessary at all. You could reference `point` directly without creating `mapPoint`.

I also recommend a few other changes:

  • Consider using `GeoViewTapped` instead of `MouseLeftButtonUp`, as that will save you from needing to call `ScreenToLocation` https://developers.arcgis.com/net/wpf/api-reference/html/E_Esri_ArcGISRuntime_UI_Controls_GeoView_Ge... GeoViewInputEventArgs has a Location property.

  • You should be able to add an overlay to the overlay collection rather than re-creating the collection each time. You can also add multiple graphics to a single overlay. If you just want to be able to draw graphics on the map, you should only need to set up the overlay once at startup.
  • The ViewModel is being re-created every time the user clicks, and it isn't clear that the view is ever being re-configured to reference that ViewModel. The ViewModel would typically be created for the view only once. As written, I don't think you would ever see points, because a new ViewModel is created without the view being updated.
  • Creating the symbol for the graphics each time is perfectly valid code, but there is a simpler way. You can define a renderer on the overlay and any graphics you add will be drawn with the defined symbol. There is an example here: https://github.com/Esri/arcgis-runtime-samples-dotnet/tree/main/src/WPF/ArcGISRuntime.WPF.Viewer/Sam... 

I hope that helps. Let me know if you have any other questions.

johnmarker
New Contributor III

@NathanCastle1 Thank you for those suggestions. Most of it is clear and makes sense as to why they would be better practice/necessary changes to be made.

Is there anything I need to do to call update to the view after creating this point? Or once I add it to the graphics overlay with  graphicsOverlay.Graphics.Add(pointGraphic); will it automatically update the view?

Another thing, I have the two classes MainWindow.xaml/MW.xaml.cs and MapViewModel.cs. MainWindow in it's constructor calls  InitializeComponent(); which in turns eventually calls the constructor of MapViewModel as part of setup. And then when I create an object of MapViewModel so that I can access the createPoint() method I call the constructor of MapViewModel again which will call setup() which just assigns the basemap type again. Would that mess this up?

I can include code snippets of this if needed.

0 Kudos
JoeHershman
MVP Regular Contributor

How are you setting the data context?  You should not need to have the code where you new up the ViewModel

Thanks,
-Joe
0 Kudos
johnmarker
New Contributor III

@JoeHershman 

//MainWindow.xaml.cs

    public partial class MainWindow : Window
    {
        private MapViewModel mvm;
        public MainWindow()
        {
            InitializeComponent();
            mvm = new MapViewModel();
            MapPoint mapCenterPoint = new MapPoint(-117.6709, 35.6225, SpatialReferences.Wgs84);
            MainMapView.SetViewpoint(new Viewpoint(mapCenterPoint, 100000));
        }

        private void MainMapView_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            Point p = e.GetPosition(MainMapView);
            MapPoint mp = MainMapView.ScreenToLocation(e.GetPosition(MainMapView));
            mvm.createPoint(mp);
        }
    }


//MapViewModel.cs

    class MapViewModel : INotifyPropertyChanged
    {

        public MapViewModel()
        {
            SetupMap();
        }

        private void SetupMap()
        {
            // Create a new map with a 'topographic vector' basemap.
            Map = new Map(BasemapStyle.ArcGISTopographic);

        }
0 Kudos
JoeHershman
MVP Regular Contributor

What is in your Xaml?  Somewhere you must be setting the data context of the view.  This would be why it runs the constructor.  Are you using any MVVM libraries or just core WPF?

Thanks,
-Joe
0 Kudos
NathanCastle1
Esri Contributor

When a graphic is added to a graphics overlay that is already in a mapview or sceneview, the graphic will be shown automatically, assuming there is a renderer or the graphic is configured with a symbol.

Based on the code you shared, it looks like you're following the pattern set in the template projects and the developer tutorials. If that is the case, you will see something like the following in your MainWindow.xaml file:

<Window.Resources>
    <local:MapViewModel x:Key="MapViewModel" />
</Window.Resources>

That code will create an instance of your viewmodel when InitializeComponent is called.

The MapView's Map and GraphicsOverlays properties are set via binding:

<esri:MapView x:Name="MainMapView"
              Map="{Binding Map, Source={StaticResource MapViewModel}}"
              GraphicsOverlays="{Binding GraphicsOverlays, Source={StaticResource MapViewModel}}" />

The view is bound to the specific instance of the viewmodel that is added to the `Window.Resources` dictionary. Your code is creating another instance of viewmodel, but that instance is never connected to the view in any way, so the view won't update to reflect changes in viewmodel state. You could fix this by re-setting up bindings when you create the viewmodel, or by using the existing viewmodel instance created in XAML.

I recommend updating your code to reference the copy of the ViewModel created in XAML:

You could use something like the following to get a reference to the viewmodel:

mvm = FindResource("MapViewModel") as MapViewModel;

 

johnmarker
New Contributor III

@NathanCastle1 Thank you for your explanation. It was very clear and helpful. I was following the template project while trying to look things up that it did not explain (the InitializeComponent method being one of those things). 

0 Kudos
JoeHershman
MVP Regular Contributor

Assuming @NathanCastle1 is correct, which is what I also assume and why I asked about what is in the Xaml.  I'm going to go out on a limb and assume the DataContext is being set in the Xaml also.  However, without seeing the Xaml I cannot be sure exactly what is being done but I am going with this assumption.

One nice MVVM approach to adding functionality, imo, is using TriggerActions.  Especially if you are not using an external library to handle Commanding and Eventing

Based on the code you have above we could create an Action that handles the GeoViewTapped and then creates the point.

public class CreatePointAction : TriggerAction<MapView>
{
	private static bool _doubleTapped;

	protected override void OnAttached()
	{
		base.OnAttached();
		if (!(AssociatedObject is MapView mapView)) return;

		//This seems silly to me, but a GeoViewDoubleTapped fires both a tapped and a double tapped.
		//This indicate was double tapped
		mapView.GeoViewDoubleTapped += (s, e) => { _doubleTapped = true; };
	}


	protected override async void Invoke(object parameter)
	{
		await Task.Delay(250);
		if (_doubleTapped)
		{
			_doubleTapped = false;
			return;
		}

		if (!(AssociatedObject is MapView mapView)) return;
		if (!(parameter is GeoViewInputEventArgs args)) return;

		//I'm just going to assume only the one layer
		var graphicsOverlay = mapView.GraphicsOverlays.FirstOrDefault();
		if ( graphicsOverlay == null ) return;

		AddPoint(graphicsOverlay, args.Location);
	}

	private void AddPoint(GraphicsOverlay graphicsOverlay, MapPoint mapPoint)
	{
		var pointSymbol = new SimpleMarkerSymbol
		{
			Style = SimpleMarkerSymbolStyle.X,
			Color = System.Drawing.Color.Red,
			Size = 10.0,
			Outline = new SimpleLineSymbol
			{
				Style = SimpleLineSymbolStyle.Solid, Color = System.Drawing.Color.Red, Width = 2.0
			}
		};

		// Create a point graphic with the geometry and symbol.
		var pointGraphic = new Graphic(mapPoint, pointSymbol);

		// Add the point graphic to graphics overlay.
		graphicsOverlay.Graphics.Add(pointGraphic);
	}
}

To wire this in we add the Action as an EventTrigger in our MapView definition

<esri:MapView x:Name="MapView" Map="{Binding Map}" GraphicsOverlays="{Binding GraphicsOverlays}">
	<i:Interaction.Triggers>
		<i:EventTrigger EventName="GeoViewTapped">
			<local:CreatePointAction></local:CreatePointAction>
		</i:EventTrigger>
	</i:Interaction.Triggers>
</esri:MapView>

Similar to what you are already doing you can setup the view point in the View constructor.  But we do not need to instantiate the VM, this was done and we are not using the VM in any code in the code behind

public partial class MainWindow : Window
{
	public MainWindow()
	{
		InitializeComponent();
		MapPoint mapCenterPoint = new MapPoint(-117.6709, 35.6225, SpatialReferences.Wgs84);
		MainMapView.SetViewpoint(new Viewpoint(mapCenterPoint, 100000));
	}
}

And in the ViewModel the map initialization can be done as before

public class MapViewModel
{
	protected GraphicsOverlay EditGraphicsOverlay { get; } = new GraphicsOverlay { Id = nameof(EditGraphicsOverlay) };
	
	public MapViewModel()
	{
		 SetupMap();
		 AddGraphicsOverlays();
	}
	
	private void SetupMap()
	{
		// Create a new map with a 'topographic vector' basemap.
		Map = new Map(BasemapStyle.ArcGISTopographic);
	}
	
	private void AddGraphicsOverlays()
	{
		if (!GraphicsOverlays.Contains(EditGraphicsOverlay))
		{
			GraphicsOverlays.Add(EditGraphicsOverlay);
		}
	}
}

With that we should have everything wired up.  There is no functional code in code behind all logic is in the Action and ViewModel.  The Action could be reused in another project without need to change anything, just include the definition in the MapView xaml

Thanks,
-Joe
0 Kudos
johnmarker
New Contributor III

@JoeHershman 

That is what my MainWindow.xaml looks like.

<Window x:Class="map_display.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:map_display"
        xmlns:esri="http://schemas.esri.com/arcgis/runtime/2013"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <local:MapViewModel x:Key="MapViewModel" />
    </Window.Resources>
    <Grid>
        <esri:MapView x:Name="MainMapView"
                      Map="{Binding Map, Source={StaticResource MapViewModel}}" 
                      GraphicsOverlays="{Binding GraphicsOverlays, Source={StaticResource MapViewModel}}" 
                      MouseLeftButtonUp ="MainMapView_MouseLeftButtonUp" />

    </Grid>
</Window>

 

0 Kudos