ArcGIS Blog

Developers

ArcGIS Location Platform

Using ArcGIS Maps SDK for .NET in an MVVM pattern

By Hamish Duff

.NET applications commonly use the Model-View-ViewModel (MVVM) design pattern, in which code can be broken up into classes with a small number of well defined responsibilities. This can help improve code understandability, maintenance and ease of modification in larger code bases.

What is MVVM?

Model-View-ViewModel (MVVM) is a pattern that separates application business logic from the user interface (UI). In short, the view describes the user interface (UI) and interacts with the view model through data bindings and commands, and it updates the model state. This helps keep the model separated from the view, which means changes do not need to be reflected in the view and vice versa.

MVVM in an ArcGIS Maps SDK for .NET application

To demonstrate how to implement the MVVM pattern in a .NET Maps SDK app this blog post looks at the following use cases:

  1. Create a .NET application
  2. Add NuGet packages
  3. Setup dependency injection
  4. Display data on a MapView
  5. Add a GeoViewController to a MapView
  6. Implement layer identification
  7. Change viewpoint from the view model

If you haven’t installed the .NET Maps SDK already, you can do so by visiting the install and set up pages on the Esri Developer’s website.

In the following sections I describe how to build an ArcGIS Maps SDK for .NET app that follows an MVVM pattern. As I am focusing on MVVM in this blog post I will highlight the areas of the application that are relevant to MVVM, where this is not the case I will provide a brief overview and link to more information on the topic.

Create a .NET application

You can easily get started building an MVVM app by downloading the ArcGIS Maps SDK for .NET template for .NET from the Microsoft Visual Studio marketplace. This template references the appropriate NuGet packages for each platform, using the MVVM design pattern. In this blog I will be using .NET MAUI but the same patterns and practices apply to WPF, WinUI and UWP.

The template provides boiler-plate code with a view model called MapViewModel.cs which is bound to the view in MainPage.xaml.cs.

public partial class MainPage : ContentPage
{
    public MainPage()
    {
	InitializeComponent();

	this.BindingContext = new MapViewModel();
    }
}

Note that there are multiple ways to bind the view model to the view. In the template, BindingContext is defined in MainPage.xaml.cs code-behind but it could also be set in the MainPage.xaml file via xaml, as shown below.

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

ArcGIS location services, data hosting services, and content management services require authentication with an API key or by authenticating with ArcGIS Online. You can learn more about API keys and how to generate them from the Create an API key tutorial. Once you have it, this API key can be added in the MauiProgram.cs file, and if you’re using a project from the template, you can replace the placeholder text with the key for your app.

.UseApiKey("YOUR_API_KEY")

Add NuGet packages

To simplify the code and enhance the view model’s capabilities, add the following package references to the project file.

<PackageReference Include="CommunityToolkit.Maui" Version="8.0.1" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<PackageReference Include="Esri.ArcGISRuntime.Toolkit.Maui" Version="200.5.0" />

Include the UseArcGISToolkit() and UseMauiCommunityToolkit() statements in the host builder in MauiProgram.cs.

var builder = MauiApp.CreateBuilder();
builder
    .UseMauiApp<App>()
    .ConfigureFonts(fonts =>
    {
        fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
        fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
    })
    .UseArcGISRuntime(config => config
       .UseApiKey("YOUR_API_KEY")
       .ConfigureAuthentication(auth => auth
           .UseDefaultChallengeHandler())
      )
    .UseArcGISToolkit()
    .UseMauiCommunityToolkit();

Let’s review the components used in the workflow described in this blog post.

ArcGIS Maps SDK for .NET Toolkit (Esri.ArcGISRuntime.Toolkit.Maui) | NuGet

The ArcGIS Maps SDK for .NET Toolkit is an open-source library of controls and components that can be used to improve your ease of development with the .NET Maps SDK. The toolkit repo can be found on GitHub.

The GeoViewController in the toolkit provides a helper class which enables performing view operations such as layer identification and setting viewpoints on the MapView from the view model.

.NET MAUI Community Toolkit (CommunityToolkit.Maui) | NuGet

EventToCommandBehavior is used to invoke a command on the bound view model using an event. Using this behavior, the view model can react to tap events on the MapView, change events on pickers, and loaded events on UI elements. This is used in this example application to handle tap events on clusters and features. Note that no toolkit is available to replicate this EventToCommandBehavior on WPF and WinUI but other solutions are possible.

MVVM Toolkit (CommunityToolkit.Mvvm) | NuGet

This package simplifies the code in the view model by taking care of notify property changed boilerplate code through use of the provided ObservableObject type and simplified command binding using [RelayCommand]. Other MVVM libraries are available, and this is not a required package but I find it simplifies the code.

Setup dependency injection

The MVVM example app I am sharing in this blog post will utilize a single view model instance shared across one page that occurs in a single instance and another page that opens to display data based on current view model state.

Add the following lines to MauiProgram.cs to register the single view model and page instances.

builder.Services.AddSingleton<MainPage>();
builder.Services.AddSingleton<MapViewModel>();

The MainPage.xaml.cs constructor can then be updated as a dependency injection container will identify the dependency upon MapViewModel.cs when the object is created and handle the injection of the service.

public partial class MainPage : ContentPage
{
    public MainPage(MapViewModel mapViewModel)
    {
	InitializeComponent();

	this.BindingContext = mapViewModel;
    }
}

Using dependency injection provides some benefits. It removes the need for a class to locate its dependencies. Furthermore, dependencies can be mocked to improve testability.

Display data on a MapView

Start by updating MapViewModel.cs to be a partial class inheriting from ObservableObject in the MVVM toolkit package. This gives access to [RelayCommand] and [ObservableProperty] attributes which are used to reduce code length and complexity. In this case you don’t need to directly implement INotifyPropertyChanged.

public partial class MapViewModel : ObservableObject

Then, remove the existing property changed code and update the Map property to use the [ObservableProperty] attribute.

[ObservableProperty]
private Map? _map;

Display a feature service using a feature layer

In the example app I use a feature service to display places of worship in India. To do this, instantiate the Map object in MapViewModel.cs with a basemap then create a feature layer using the feature service URL and add it to the maps operational layers. For more information on displaying a feature service see the Feature layer (feature service) sample.

Set up clustering data

To ensure the data from the feature layer displays as clusters when the map loads, clone the UniqueValueRenderer used by the feature layer to create a ClusteringFeatureReduction object. Then create a SimpleLabelExpression to display the total number of points in each cluster as a label. To see more in depth clustering content have a look at the Display clusters and Configure clusters samples

Picture of clustered data
Clustered place of worship data

Add a GeoViewController to a MapView

To make use of layer identification and setting viewpoints, add a GeoViewController to the view-model and a GeoViewTappedCommand to respond to tap events.

public GeoViewController Controller { get; } = new GeoViewController();

[RelayCommand]
public async Task GeoViewTapped(GeoViewInputEventArgs eventArgs) { }

Note that using the MVVM Toolkit commands with the attribute [RelayCommand] are exposed with the word “Command” appended to the end of the method signature. So, in this case the view will bind to GeoViewTappedCommand.

Add the following namespaces to the view:

xmlns:mauitoolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
xmlns:toolkit="clr-namespace:Esri.ArcGISRuntime.Toolkit.Maui;assembly=Esri.ArcGISRuntime.Toolkit.Maui"

Update the MapView to include the GeoViewController and an EventToCommandBehavior which allows commands to be invoked on the view model when a GeoViewTapped event occurs.

<esri:MapView x:Name="MyMapView"
              Grid.ColumnSpan="2"
              toolkit:GeoViewController.GeoViewController="{Binding Controller}"
              Map="{Binding Map}">
    <esri:MapView.Behaviors>
        <mauitoolkit:EventToCommandBehavior x:TypeArguments="esri:GeoViewInputEventArgs"
                                            Command="{Binding GeoViewTappedCommand}"
                                            EventName="GeoViewTapped" />
    </esri:MapView.Behaviors>
</esri:MapView>

In this case, the Map is bound to a Map object on the view model, the GeoViewController is bound to the GeoViewController and the GeoViewTapped event invokes the GeoViewTappedCommand, passing GeoViewInputEventArgs as a parameter.

Implement layer identification

The GeoViewController is set up to invoke a GeoViewTappedCommand which means the view model can respond to tap events from the view.

The main concept to understand when performing layer identification from the view model is to use the GeoViewController. Once you have a GeoViewController configured to react to a GeoViewTapped event you can update your code in MapViewModel as follows.

[RelayCommand]
public async Task GeoViewTapped(GeoViewInputEventArgs eventArgs) => await Identify(eventArgs.Position, eventArgs.Location);

public async Task Identify(Point location, MapPoint? mapLocation)
{
        // Identify the tapped observation.
        IdentifyLayerResult? results = await Controller.IdentifyLayerAsync(_featureLayer, location, 10, false);
}

 

The following sections give an overview of the code used in the example app. To view the context of these code snippets, see the full code and try the app out yourself or clone the repo on GitHub.

I’ve added an additional view model to wrap the GeoElement objects. This keeps the GeoElement data model separate from the view and allows for the data to be validated or formatted prior to presentation.

public PlaceOfWorshipViewModel(int index, ArcGISFeature feature)
{
    // Set the default property values.
    Name = Religion = Fid = "null";

    // Set the display index and feature.
    ArcGISFeature = feature;
    DisplayIndex = index;

    // Get attribute values from the attributes dictionary. 
    if (feature.Attributes.TryGetValue("fid", out object? fidValue) && fidValue != null && fidValue is long fid) Fid = fid.ToString();
    if (feature.Attributes.TryGetValue("religion", out object? religionValue) && religionValue != null) Religion = (string)religionValue;
    if (feature.Attributes.TryGetValue("name", out object? nameValue) && nameValue != null) Name = (string)nameValue; 

    // Given the feature geometry get a formatted coordinate string.
    FormattedCoordinates = CoordinateFormatter.ToLatitudeLongitude((MapPoint)ArcGISFeature.Geometry!, LatitudeLongitudeFormat.DegreesDecimalMinutes, 2);
}

I then create PlaceOfWorshipViewModel objects by passing in an ArcGISFeature and populating properties used in the view. None of the properties on this view model change once presented so they do not need to be observable. Each feature’s geometry is formatted using the CoordinateFormatter to present the information in a way that is easier to read in the view. The DisplayIndex field is added to the view model to display the item index in the view and improve readability when scrolling through large numbers of items.

I have created another .NET MAUI view called GeoElementSelectionPage, this is used to display the data contained in a tapped cluster.

A collection view on the GeoElementSelectionPage is used to display an ObservableCollection of PlaceOfWorshipViewModel objects in the MapViewModel.

As clusters in this data set can contain thousands of GeoElement objects, the number of items in the collection is incrementally increased as you scroll. This is achieved by adding a LoadMore() method in the MapViewModel which is called when the number of items in the collection hits a specified threshold.

The core concept is to use the GeoViewController to perform identification in the view model – see the example app repo for the full code that deals with the more complex workflow of identifying clustered data on a layer.

public async Task Identify(Point location, MapPoint? mapLocation)
{
    // Identify the tapped observation.
    IdentifyLayerResult? results = await Controller.IdentifyLayerAsync(_featureLayer, location, 10, false); 
}

 

Flesh out the view and view model

Add more commands and properties with [RelayCommand] and [ObservableProperty] attributes to perform actions like navigating away from the GeoElementSelectionPage and hold the displayed GeoElement objects etc. Using properties in the PlaceOfWorshipViewModel and commands in the MapViewModel you can create a UI to display all of the elements within a tapped cluster and format their data for presentation.

Picture of data contained within a cluster
GeoElements contained within a tapped cluster

Change viewpoint from the view model

This section will outline how to update the viewpoint of the MapView from the MapViewModel.

First lets update the viewpoint when a picker selection change event occurs.

Add a picker to MainPage update the current viewpoint of the MapView.

<Picker x:Name="RegionPicker"
        Title="Scope to region"
        SelectedItem="{Binding SelectedRegion}">
    <Picker.Behaviors>
        <mauitoolkit:EventToCommandBehavior Command="{Binding SelectedRegionChangedCommand}"
                                            CommandParameter="{Binding Source={x:Reference RegionPicker}, Path=SelectedIndex}"
                                            EventName="SelectedIndexChanged" />
    </Picker.Behaviors>
    <Picker.Items>
        <x:String>North</x:String>
        <x:String>South</x:String>
        <x:String>East</x:String>
        <x:String>West</x:String>
        <x:String>Central</x:String>
    </Picker.Items>
</Picker>

 

Again, use the EventToCommandBehavior to invoke a command on the view model when the picker’s selection changes.

In MapViewModel, implement a command to respond to the index changed event.

[RelayCommand]
public async Task SelectedRegionChanged(int selectedIndex)
{
    // Get the full extent of the feature layer.
    Viewpoint viewpoint = new Viewpoint(_featureLayer!.FullExtent!);

    // Get the spatial reference of the feature layer.
    SpatialReference spatialReference = SpatialReference.Create(3857);

    double scale = 10.5e6;

    switch (selectedIndex)
    {
        case 0:
            viewpoint = new Viewpoint(new MapPoint(8597888, 3622342, spatialReference), scale);
            break;
        case 1:
            viewpoint = new Viewpoint(new MapPoint(8617655, 1603703, spatialReference), scale);
            break;
        case 2:
            viewpoint = new Viewpoint(new MapPoint(9947837, 2859542, spatialReference), scale);
            break;
        case 3:
            viewpoint = new Viewpoint(new MapPoint(7966035, 2803998, spatialReference), scale);
            break;
        default:
            break;
    }

    // Set the viewpoint based on the desired selection.
    await Controller.SetViewpointAsync(viewpoint);

    // Reset the selected region for the next navigation.
    SelectedRegion = null;
}

 

Use the GeoViewController to update the viewpoint with SetViewpointAsync(), this will update the view from the view model.

Picture of picker to change viewpoint
Change viewpoint with picker

Now, lets respond to a changed selection in a collection view. We will update the viewpoint and show a callout using the GeoViewController.

In the GeoElementSelectionPage the GeoElementCollectionView.SelectionChangedCommand can be bound to the MapViewModel.

Add a new SelectedGeoElementChanged() method with the [RelayCommand] attribute. This will take the selected item as a parameter to be used to update the viewpoint.

[RelayCommand]
public async Task SelectedGeoElementChanged(PlaceOfWorshipViewModel selectedItem)
{
    if (selectedItem == null) return;
    
    // Clear any currently selected features.
    _featureLayer?.ClearSelection();

    // Get the map point for the selected view model.
    MapPoint mapPoint = (MapPoint)selectedItem.ArcGISFeature.Geometry!;

    // Close the modal page.
    await Shell.Current.GoToAsync("..");

    // Select the feature associated with the selected view model.
    _featureLayer?.SelectFeature(selectedItem.ArcGISFeature);

    // Navigate to the selected feature and show a callout.
    // Scale is set to a value smaller than the layers feature reduction max scale so that clusters do not render when the navigation completes.
    await Controller.SetViewpointAsync(new Viewpoint(mapPoint, 1e4), TimeSpan.FromSeconds(1));

    // Use an arcade expression to fallback to a default value if the properties used in the callout are null.
    var calloutDefn = new Esri.ArcGISRuntime.UI.CalloutDefinition(selectedItem.ArcGISFeature, "DefaultValue($feature.name, 'null')", "DefaultValue($feature.religion, 'null')");
    Controller.ShowCalloutForGeoElement(selectedItem.ArcGISFeature, default, calloutDefn);
}

 

When a GeoElement is selected in the CollectionView the GeoElementSelectionPage closes and any selected items on the feature layer are cleared. The GeoElement is then selected on the feature layer and the viewpoint is updated to center on its geometry. A callout is displayed for the GeoElement to display its name using GeoViewController.ShowCalloutForGeoElement().

The point to highlight here is the use of the GeoViewController.SetViewpointAsync() as this is the key to performing viewpoint change operations on the view from the view model.

Finishing up

In the completed code in the example app I have included a toggle to enable and disable clustering and a legend to display the predominant religion in each cluster. These have been added following the processes outlined earlier in this blog post.

GIF of full application features
Completed application full feature demonstration

Summary

Model-View-View-Model (MVVM) is widely used pattern in .NET applications. In this blog post I’ve demonstrated how to get started with a simple MVVM application using a mixture of helpful external packages.

As an example, I’ve shown how to bind a Map object in view model to a MapView in a view. I gave an overview of the usage of the GeoViewController and walked through implementing identification functionality in the view-model to respond to tap events in the view. I discussed how to configure the view model to update the MapView viewpoint and display callouts using the ArcGIS Maps SDK for .NET Toolkit GeoViewController.

Let us know how you’re getting on with your MVVM apps in the Esri Community forums, and if you haven’t already, check out our developer documentation for more tips and guides on how to use the ArcGIS Maps SDK for .NET.

Useful resources

The ArcGIS Maps SDK for .NET samples applications showcase capabilities and functionality offered by ArcGIS Maps SDKs for Native Apps via simple workflows. These focused samples enable you to easily understand the concepts and allow you to copy code snippets directly into your application. To maintain that simplicity, the applications implement sample logic in the code-behind for each sample. This also makes it easier to copy the sample code into another application without additional steps. This blog post can be used in conjunction with the .NET samples applications to implement the broad range of functionality offered by the ArcGIS Maps SDK for .NET.

Share this article