Add Operational Layer to MapView Using MVVM

2180
6
Jump to solution
12-02-2020 12:41 PM
thomasbales
Occasional Contributor

I am unable to add a layer to the mapview from a button click using the .net MVVM pattern in WPF. I have rigged the button using the ICommand interface and can activate the function in my MapViewModel but I have no clue how to add a featurelayer because I can't use my x:Name="mapView". What I want is to click my button and activate the AddAnotherLayer method, which I can do but I have no idea how to access the mapview to do this.

Does anyone have a simple example on how to do this without using galasoft or prism, just regular MVVM.

help....

 

 

MapViewModel.cs

public class MapViewModel : INotifyPropertyChanged
{
    private Map _map = new Map(Basemap.CreateImageryWithLabels());
    public RadarCommand RadarCommand { get; set; }

    public Map Map
   {
        get { return _map; }
       set { _map = value; OnPropertyChanged(); }
   }

 

     public MapViewModel()
     {
           this.RadarCommand = new RadarCommand(this);
           CreateNewMap();
     }


     protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
     {
          var propertyChangedHandler = PropertyChanged;
          if (propertyChangedHandler != null)
          propertyChangedHandler(this, new PropertyChangedEventArgs(propertyName));
    }


     public async void CreateNewMap()

     {
         
         FeatureLayer trailHeadsLayer = new FeatureLayer(new          Uri("https://services3.arcgis.com/GVgbJbqm8hXASVYi/arcgis/rest/services/Trailheads/FeatureServer/0"));
         await trailHeadsLayer.LoadAsync();

        Map.OperationalLayers.Add(trailHeadsLayer);       

       Console.WriteLine("all done loading");

    }

    public async void AddAnotherLayer() {

         FeatureLayer featureLayer = new FeatureLayer(new          Uri("https://services3.arcgis.com/GVgbJbqm8hXASVYi/arcgis/rest/services/Trails/FeatureServer/0"));
        await featureLayer.LoadAsync();
        Map.OperationalLayers.Add(featureLayer);
        Console.WriteLine("all trail lines done loading");
     }

     public event PropertyChangedEventHandler PropertyChanged;
}

MainWindow.xaml

<Window.Resources>
<local:MapViewModel x:Key="MapViewModel" />
</Window.Resources>
<Grid>
<esri:MapView Map="{Binding Map, Source={StaticResource MapViewModel}}" x:Name="mapView" />
<uc:TopToolbar/>
</Grid>

 

 

 

Tags (3)
0 Kudos
2 Solutions

Accepted Solutions
dotMorten_esri
Esri Notable Contributor

 I am still having a problem with my ICommand not adding AnotherLayer()

Can you clarify what problem? Is the code not executing, can you step through and see how far you come? Are you getting an exception? Is the layer getting added to the Map's layer collection, but it's just not showing on the map, or? If you step through, how far are you getting?
I also don't see in your code where you execute RadarCommand. Do you have something like

`<Button Command="{Binding RadarCommand, Source={StaticResource MapViewModel}}" />` anywhere

On a side-note: Be careful with this line: `await featureLayer.LoadAsync();`
If that throws (for instance if the server is down or there is a network glitch), your entire app will crash. Any

time you have a method that says "async void" instead of 'async Task', make sure you try/catch from before the first await call until the last line of code.

View solution in original post

0 Kudos
JoeHershman
MVP Regular Contributor

I tend to always wonder why someone doesn't want to use an external MVVM library, but that is a conversation for another day.

Without an external library generally, one would want to roll their own delegate command ICommand implementation which is pretty straight forward

public class DelegateCommand : ICommand
{
	private readonly Predicate<object> _canExecute;
	private readonly Action<object> _execute;

	public DelegateCommand(Action<object> execute) : this(execute, null)
	{
	}

	public DelegateCommand(Action<object> execute, Predicate<object> canExecute)
	{
		_execute = execute;
		_canExecute = canExecute;
	}

	#region ICommand Members

	public event EventHandler CanExecuteChanged;

	public bool CanExecute(object parameter)
	{
		return _canExecute == null || _canExecute(parameter);
	}

	public void Execute(object parameter)
	{
		_execute(parameter);
	}

	#endregion

	public void RaiseCanExecuteChanged()
	{
		CanExecuteChanged?.Invoke(this, EventArgs.Empty);
	}
}

With this one can just create an ICommand and pass the action into this DelegateCommand.

To simplify the MainWindow Xaml, instead of making the view model a static resource, bind it as the DataContext.  By doing this one does not need to set the source of the binding, it will be the defined DatContext

<Window x:Class="WpfApp2.MainWindow"
        ...
        xmlns:local="clr-namespace:WpfApp2"
        xmlns:esri="http://schemas.esri.com/arcgis/runtime/2013"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800" >
    <Window.DataContext>
        <local:MapViewModel />
    </Window.DataContext>
    <Grid>
        <esri:MapView Map="{Binding Map}" x:Name="mapView" />
        <!-- Command is bound to ICommand on ViewModel -->
        <Button Content="Add Layer" Command="{Binding AddLayerCommand}"
                Height="40" Width="80" Margin="10,10,0,0" HorizontalAlignment="Left" VerticalAlignment="Top"/>
    </Grid>
</Window>

Now we just wire up our ViewModel

public class MapViewModel : INotifyPropertyChanged
{

	public MapViewModel()
	{
		CreateNewMap();
	}

	private void CreateNewMap()
	{
		//For sample using the viewpoint of the layer in code, 
		// personally I would not set an initial extent from a feature layer that needed to load at startup, 
		// but that's also a conversation for another day
		var targetGeometry =
			Geometry.FromJson(
				"{\"xmin\":-13240129.679701095,\"ymin\":3994281.9887753138,\"xmax\":-13106722.92583799,\"ymax\":4101417.5063218847,\"spatialReference\":{\"wkid\":102100,\"latestWkid\":3857}}");

		Map = new Map(Basemap.CreateImageryWithLabels())
		{
			InitialViewpoint = new Viewpoint(targetGeometry!)
		};
		Map.Loaded += (sender, args) => { Console.WriteLine("all done loading"); };
	}


	private Map _map;

	public Map Map
	{
		get => _map;
		set
		{
			_map = value;
			OnPropertyChanged(nameof(Map));
		}
	}
	
	//So here just create a DelegateCommand passing in the delegate, it will execute when invoked
	public ICommand AddLayerCommand => new DelegateCommand(ExecuteAddLayer);

	//As pointed out by Morten, generally an async should return a Task, 
	// In this situation void is proper because it is a Delegate command
	private async void ExecuteAddLayer(object obj)
	{
		try
		{
			var trailHeadsLayer = new FeatureLayer(new Uri("https://services3.arcgis.com/GVgbJbqm8hXASVYi/arcgis/rest/services/Trailheads/FeatureServer/0"));
			await trailHeadsLayer.LoadAsync();

			Map.OperationalLayers.Add(trailHeadsLayer);
			await Map.LoadAsync();
		}
		catch (Exception e)
		{
			Console.WriteLine(e);
		}
	}
	//remove INotifyPropertyChanged for sample purposes
}
Thanks,
-Joe

View solution in original post

6 Replies
JoeHershman
MVP Regular Contributor

You're pretty much there.  Layers are added to a Map, not to a MapView.  You already have the Map which is bound to your MapView's Map property

 

public async void AddAnotherLayer() 
{
    //Do something to create a feature layer
    FeatureLayer featureLayer = new FeatureLayer(featureServiceUri);
    await featureLayer.LoadAsync();
    Map.OperationalLayers.Add(featureLayer);
}

 

 

Thanks,
-Joe
0 Kudos
thomasbales
Occasional Contributor

Thank for your help I am getting closer and made some edits above but I am still having a problem with my ICommand not adding AnotherLayer()

Do I need to add anything in my MainWindow.cs? Do I need to modify my Command class?

MainWindow.cs

public partial class MainWindow : Window
{

        public MainWindow()
        {
              InitializeComponent();
        }
   }
}

 

RadarCommand.cs

public class RadarCommand : ICommand
{
    public event EventHandler CanExecuteChanged;
    public MapViewModel MapViewModel { get; set; }

    public RadarCommand(MapViewModel mapViewModel)
    {
           this.MapViewModel = mapViewModel;
    }

    public bool CanExecute(object parameter)
    {
       return true;
     }

    public void Execute(object parameter)
    {
        MapViewModel.AddAnotherLayer();
    }
}

0 Kudos
dotMorten_esri
Esri Notable Contributor

 I am still having a problem with my ICommand not adding AnotherLayer()

Can you clarify what problem? Is the code not executing, can you step through and see how far you come? Are you getting an exception? Is the layer getting added to the Map's layer collection, but it's just not showing on the map, or? If you step through, how far are you getting?
I also don't see in your code where you execute RadarCommand. Do you have something like

`<Button Command="{Binding RadarCommand, Source={StaticResource MapViewModel}}" />` anywhere

On a side-note: Be careful with this line: `await featureLayer.LoadAsync();`
If that throws (for instance if the server is down or there is a network glitch), your entire app will crash. Any

time you have a method that says "async void" instead of 'async Task', make sure you try/catch from before the first await call until the last line of code.

0 Kudos
JoeHershman
MVP Regular Contributor

I tend to always wonder why someone doesn't want to use an external MVVM library, but that is a conversation for another day.

Without an external library generally, one would want to roll their own delegate command ICommand implementation which is pretty straight forward

public class DelegateCommand : ICommand
{
	private readonly Predicate<object> _canExecute;
	private readonly Action<object> _execute;

	public DelegateCommand(Action<object> execute) : this(execute, null)
	{
	}

	public DelegateCommand(Action<object> execute, Predicate<object> canExecute)
	{
		_execute = execute;
		_canExecute = canExecute;
	}

	#region ICommand Members

	public event EventHandler CanExecuteChanged;

	public bool CanExecute(object parameter)
	{
		return _canExecute == null || _canExecute(parameter);
	}

	public void Execute(object parameter)
	{
		_execute(parameter);
	}

	#endregion

	public void RaiseCanExecuteChanged()
	{
		CanExecuteChanged?.Invoke(this, EventArgs.Empty);
	}
}

With this one can just create an ICommand and pass the action into this DelegateCommand.

To simplify the MainWindow Xaml, instead of making the view model a static resource, bind it as the DataContext.  By doing this one does not need to set the source of the binding, it will be the defined DatContext

<Window x:Class="WpfApp2.MainWindow"
        ...
        xmlns:local="clr-namespace:WpfApp2"
        xmlns:esri="http://schemas.esri.com/arcgis/runtime/2013"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800" >
    <Window.DataContext>
        <local:MapViewModel />
    </Window.DataContext>
    <Grid>
        <esri:MapView Map="{Binding Map}" x:Name="mapView" />
        <!-- Command is bound to ICommand on ViewModel -->
        <Button Content="Add Layer" Command="{Binding AddLayerCommand}"
                Height="40" Width="80" Margin="10,10,0,0" HorizontalAlignment="Left" VerticalAlignment="Top"/>
    </Grid>
</Window>

Now we just wire up our ViewModel

public class MapViewModel : INotifyPropertyChanged
{

	public MapViewModel()
	{
		CreateNewMap();
	}

	private void CreateNewMap()
	{
		//For sample using the viewpoint of the layer in code, 
		// personally I would not set an initial extent from a feature layer that needed to load at startup, 
		// but that's also a conversation for another day
		var targetGeometry =
			Geometry.FromJson(
				"{\"xmin\":-13240129.679701095,\"ymin\":3994281.9887753138,\"xmax\":-13106722.92583799,\"ymax\":4101417.5063218847,\"spatialReference\":{\"wkid\":102100,\"latestWkid\":3857}}");

		Map = new Map(Basemap.CreateImageryWithLabels())
		{
			InitialViewpoint = new Viewpoint(targetGeometry!)
		};
		Map.Loaded += (sender, args) => { Console.WriteLine("all done loading"); };
	}


	private Map _map;

	public Map Map
	{
		get => _map;
		set
		{
			_map = value;
			OnPropertyChanged(nameof(Map));
		}
	}
	
	//So here just create a DelegateCommand passing in the delegate, it will execute when invoked
	public ICommand AddLayerCommand => new DelegateCommand(ExecuteAddLayer);

	//As pointed out by Morten, generally an async should return a Task, 
	// In this situation void is proper because it is a Delegate command
	private async void ExecuteAddLayer(object obj)
	{
		try
		{
			var trailHeadsLayer = new FeatureLayer(new Uri("https://services3.arcgis.com/GVgbJbqm8hXASVYi/arcgis/rest/services/Trailheads/FeatureServer/0"));
			await trailHeadsLayer.LoadAsync();

			Map.OperationalLayers.Add(trailHeadsLayer);
			await Map.LoadAsync();
		}
		catch (Exception e)
		{
			Console.WriteLine(e);
		}
	}
	//remove INotifyPropertyChanged for sample purposes
}
Thanks,
-Joe
thomasbales
Occasional Contributor

Joel and Morten, I am going to give this a shot today and I will let you know. As for my command implementations it is like this. I did add a try catch statement around the loadasync and the button did work so my design pattern must be okay. The symbology did fail though because the trails are supposed to be pink and the point are a a round red symbol. I am going to replace the trails with NOOA's radar feed and see what I get. Thanks for all your help

 

public class RadarCommand : ICommand
{
    public event EventHandler CanExecuteChanged;
    public MapViewModel MapViewModel { get; set; }

    public RadarCommand(MapViewModel mapViewModel)
    {
        this.MapViewModel = mapViewModel;
    }

    public bool CanExecute(object parameter)
    {
        return true;
    }

    public void Execute(object parameter)
    {
        MapViewModel.AddAnotherLayer();
    }
}

 

User Control

<UserControl x:Class="Nav_Pro_6._1.Controls.TopToolbar"
                         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
                        xmlns:local="clr-namespace:Nav_Pro_6._1.Controls"
                        xmlns:vm="clr-namespace:Nav_Pro_6._1"
                        mc:Ignorable="d"
                        d:DesignHeight="50" d:DesignWidth="800">
<UserControl.Resources>
                        <vm:MapViewModel x:Key="viewModel"/>
</UserControl.Resources>
<Grid>
<ToolBarPanel HorizontalAlignment="Left" Height="65" Margin="0,0,0,0" VerticalAlignment="Top" Width="800">
     <Button x:Name="button" Content="Radar" HorizontalAlignment="Left" Width="50" Height="48"            Command="{Binding RadarCommand, Source={StaticResource viewModel}}" />
     </ToolBarPanel>
</Grid>
</UserControl>

 

0 Kudos
JoeHershman
MVP Regular Contributor

In general terms by passing the view model into the ICommand implementation (RadarCommand), the ICommand implementation and the view model are now tightly coupled.  This defeats one of the driving principles of MVVM.  You would need an ICommand implementation for every bound command. 

Thanks,
-Joe
0 Kudos