Skip To Content

Create an offline app

In this topic

When in a connected environment, ArcGIS Runtime SDK apps can use online services hosted by ArcGIS for Server or ArcGIS for Portal (ArcGIS Online, for example) to provide data. These services can provide map tiles, features, or even functionality like geocoding or routing for your app. An advantage of using online services is that you do not need to store large datasets locally. Instead, you can request the maps, features, or functionality you need from the service. You also don't need to maintain numerous copies of the data since online services allow you to maintain a consistent centralized view of your data. Apps that consume your services can be assured that they have the most current (or at least the same) version of the data.

There are several use cases, however, where working with data locally may provide a good strategy. You can design your app to work exclusively with local data (completely disconnected, in other words) or as a hybrid solution that leverages both online and local data sources.

Here are some common motivating factors for using local data in an ArcGIS Runtime app.

  • Mobile apps with unpredictable connectivity—You can design your app to automatically respond to loss of connectivity by switching to local datasets.
  • Disconnected apps—Due to hardware or security limitations, you may need an app that works completely offline and only uses local data.
  • Mobile editing—Edits can be made by field workers while connected or disconnected. When a connection becomes available, offline edits can be synchronized with the service.
  • Increased performance—Data and functionality accessed locally does not have the network communication overhead involved when accessing online services.

The primary concerns when working with local data are creating copies of the datasets you need and getting them onto the device. There are two patterns you can use for working with offline data, characterized by how the local versions of the data are created and used by the app: the desktop pattern and the services pattern.

  • Desktop pattern—Data packages are created using ArcGIS for Desktop, then provisioned for your app. Such packages are deployed with the installation of your app and make things like features, tiles, network datasets, and locators (geocoding functionality) available on the client. In the desktop pattern, the local datasets are read-only.
    Note:

    Shapefiles can also be copied to a device and read by your ArcGIS Runtime app. They are read-only and do not support projection on-the-fly.

  • Services pattern—Datasets (features or tiles) are downloaded from configured services to generate local versions of the data. This pattern lets multiple offline users edit the same data layers and synchronize their edits back to the service later (when an Internet connection is available, for example). When more than one user is editing the same feature and the edits conflict, the last edit committed overrides the others.

The desktop pattern allows you to deliver the data with your app, which works well when you know exactly what data the app needs. Feature data provisioned using the desktop pattern is read-only, so if editing is required in your app, you must use the services pattern to create the offline features. The services pattern provides local copies of the data directly from the service, and allows you to filter the features you need according to layers and map extent. Most importantly, local datasets copied from the service allow editing of features. Those edits can be synchronized with the service when needed. Be aware that the services pattern may require a lot of time and bandwidth to produce local copies of large datasets, especially a local map tile cache.

A hybrid solution for working with local data is often a good approach. Local feature data can be obtained using the services pattern to ensure the data are current, editable, and can be synchronized with the service. Local tile caches can be provisioned using the desktop pattern to ensure the large datasets are available when needed, without requiring the user to wait for them to generate and download.

License:

An app licensed at the Basic level can include viewing offline basemaps and feature data from a runtime geodatabase. You must license your app at the Standard level if it includes any of the following functionality: offline editing, syncing offline edits with an upload, offline routing, offline geocoding, and any use of the Local Server. See License your app for more information on license levels.

This tutorial introduces you to techniques that can give your user a seamless experience when working with an app in connected and offline modes. Specifically, you will learn how to:

  • Create a local database of features using the services pattern.
  • Create a local tile package.
  • Access local datasets for display.
  • Switch between online and local data sources in the map.

Prerequisites

This tutorial requires a supported version of Microsoft Visual Studio and ArcGIS Runtime SDK for .NET. See Install the SDK and System requirements for more information.

Familiarity with Visual Studio and a basic understanding of XAML, and C# or Visual Basic .NET (VB .NET) is recommended.

Create a WPF app

You'll use Visual Studio to create a WPF app.

  1. Open a supported version of Microsoft Visual Studio.
  2. Click File > New > Project (or click New Project on the Start page) to create a project.
  3. Click Windows Desktop > WPF Application on the New Project dialog box (you can create your project in either C# or VB .NET).
    Note:

    Visual Studio 2015 organizes project types slightly differently. You'll find WPF Application projects under Windows > Classic Desktop

    Tip:

    ArcGIS Runtime SDK for .NET provides a project template for creating your mapping app, called ArcGIS Runtime 10.2.7 for .NET App. Creating your project from the template will add the appropriate references and a page with a map view containing a single base layer. In this tutorial, you'll build your app from a blank template.

  4. Choose a folder location for your new project and name it OfflineApp.

    Visual Studio New Project dialog box

  5. Click OK to create the project.

    Your project opens in Visual Studio and contains a single WPF window called MainWindow.xaml.

  6. Next, add a reference to the ArcGIS Runtime SDK for .NET API assembly.
  7. Right-click the References node under the OfflineApp project listing in the Visual Studio Solution Explorer window, and click Add Reference in the context menu.
  8. Check the listing for the Esri.ArcGISRuntime assembly under Assemblies > Extensions.

    Visual Studio Reference Manager dialog box

  9. Click OK to add the reference to ArcGIS Runtime for .NET.

    Visual Studio project references

  10. The Esri.ArcGISRuntime library contains the Map control and all core API components and classes you need.

Add an empty map control

To start with, your app contains a MapView control with an empty Map. At runtime, your app attempts to display content from ArcGIS Online. If it is unable to use the online datasets, it reports an error.

Begin by creating a basic UI for your app. To use the classes in the ArcGIS Runtime API, define XML namespace references in MainWindow.xaml.

  1. Open the MainWindow.xaml page in XAML view.
  2. Add the following XAML in the Window element of MainWindow.xaml to define an XML namespace reference for ArcGIS Runtime API.

    xmlns:esri="http://schemas.esri.com/arcgis/runtime/2013"

  3. Inside the Grid element on the page, add the XAML below to create an empty MapView control. Assign the name MyMapView to the control so you can reference it in your code behind.

    <esri:MapView x:Name="MyMapView">
        <esri:Map>
        </esri:Map>
    </esri:MapView>

Depending on connectivity, at runtime your MapView displays maps and layers from online or local sources.

Create the UI

The UI for your app have controls for creating local versions of the online data sources (map tiles and features), and for switching between online and local data sources. The XAML to define the app's UI has been created for you.

  1. Copy the following XAML and paste it into your MainWindow.xaml page to create your app's UI. Paste it immediately after the XAML that defines your map view.

    <Canvas
    	Width="370" Height="197"
    	HorizontalAlignment="Left" VerticalAlignment="Bottom"
    	Margin="20,0,0,143" >
        <Border x:Name="ToolsPanel" 
    		CornerRadius="10" 
    		Background="DarkGray" Opacity="0.85"
    		Width="370" 
    		Canvas.Top="0">
            <Grid x:Name="Controls"                           
    			Opacity="1"
    			Margin="10">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition/>
                    <ColumnDefinition/>
                    <ColumnDefinition/>
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition/>
                    <RowDefinition/>
                    <RowDefinition/>
                    <RowDefinition/>
                    <RowDefinition/>
                    <RowDefinition/>
                </Grid.RowDefinitions>
                <RadioButton x:Name="UseOnlineDataOption" 
    					Grid.Row="0" Grid.Column="0"
    					Click="DataOptionChecked"
    					Content="Online Data" 
    					HorizontalAlignment="Center" VerticalAlignment="Center" 
    					IsChecked="True"/>
                <RadioButton x:Name="UseLocalDataOption" 
    					Grid.Row="0" Grid.Column="1"
    					Click="DataOptionChecked"
    					Content="Local Data" 
    					HorizontalAlignment="Center" VerticalAlignment="Center" />
                <StackPanel Orientation="Horizontal"
    					Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3"
    					Margin="0,10,0,5">
                    <Button x:Name="GenerateLocalGeodatabaseButton" 
    						Click="GetFeatures"
    						ToolTip="Generate local geodatabase"
    						Margin="0,0,3,0"  Width="20" Height="20">
                        <Image Source="http://static.arcgis.com/images/Symbols/Cartographic/esriCartographyMarker_60_Blue.png" >
                            <Image.RenderTransform>
                                <RotateTransform Angle="180" CenterX="8" CenterY="8"/>
                            </Image.RenderTransform>
                        </Image>
                    </Button>
                    <TextBlock Text="Local features: " />
                    <TextBlock x:Name="LocalDataPathTextBlock" 
    						Text="&lt; none &gt;" />
                </StackPanel>
                <StackPanel Orientation="Horizontal"
    					Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3"
    					Margin="0,5,0,10">
                    <Button x:Name="GenerateLocalTilesButton" 
    						Click="GetTiles"
    						ToolTip="Generate local tiles"
    						Margin="0,0,3,0"  Width="20" Height="20">
                        <Image Source="http://static.arcgis.com/images/Symbols/Cartographic/esriCartographyMarker_60_Blue.png" >
                            <Image.RenderTransform>
                                <RotateTransform Angle="180" CenterX="8" CenterY="8"/>
                            </Image.RenderTransform>
                        </Image>
                    </Button>
                    <TextBlock Text="Local tiles: " />
                    <TextBlock x:Name="LocalTilesPathTextBlock" 
    						Text="&lt; none &gt;" 
    						Margin="10,0"/>
                </StackPanel>
                <StackPanel x:Name="StatusPanel"
    					Grid.Row="5" Grid.Column="0" Grid.ColumnSpan="3"
    					Orientation="Vertical"
    					Visibility="Collapsed"							
    					Margin="0,10">
                    <ListBox x:Name="StatusMessagesList"
    						Width="350" Height="80"
    						ScrollViewer.VerticalScrollBarVisibility="Visible"/>
                    <ProgressBar x:Name="StatusProgressBar" 
    						Width="350" Height="5"/>
                </StackPanel>
            </Grid>
        </Border>
    </Canvas>
    Many of the controls in the preceding XAML refer to event handlers that do not exist in your code behind.

    UI controls for your offline app
  2. Create stubs in your code behind for the event handlers referenced in your UI's XAML by copying the following code into your MainWindow.xaml.cs file. Some of the handlers require the async keyword.

    private void DataOptionChecked(object sender, RoutedEventArgs e)
    {
    
    }
    
    private async void GetTiles(object sender, RoutedEventArgs e)
    { 
            
    }
    
    private async void GetFeatures(object sender, RoutedEventArgs e)
    {
            
    }

    Tip:

    You can create event handlers for your XAML controls by right-clicking the name of a handler and clicking Go to definition in the context menu. If the handler does not exist, Visual Studio creates it for you.

Load online layers

When the empty map in your app loads, call a function to load layers from online services (ArcGIS Online). If the online sources cannot be loaded, an error message displays to the user. If local versions of the data are available, the user can load local layers that display in the map.

Start by adding some required using statements, variables, and an event handler for the map loaded event.

  1. Open the MainWindow.xaml.cs code behind.
  2. Add the following using statements at the top of the code module for additional namespaces you'll require.

    using Esri.ArcGISRuntime.Controls;
    using Esri.ArcGISRuntime.Data;
    using Esri.ArcGISRuntime.Geometry;
    using Esri.ArcGISRuntime.Http;
    using Esri.ArcGISRuntime.Layers;
    using Esri.ArcGISRuntime.Tasks.Offline;
    
    using System.Threading;
    using System.Threading.Tasks;

  3. Inside the class definition, add the following variable declarations. These variables contain the information you need to load layers from online or local sources.

    private const string basemapUrl = "http://sampleserver6.arcgisonline.com/arcgis/rest/services/World_Street_Map/MapServer";
    private const string operationalUrl = "http://sampleserver6.arcgisonline.com/arcgis/rest/services/Sync/SaveTheBaySync/FeatureServer/0";
    
    private string localTileCachePath;
    private string localGeodatabasePath;

  4. In the class constructor, create a handler for the MyMapView.Loaded event. When prompted, click the Tab key to have Visual Studio generate the stub for the handler. Your code should look like the following.

    public MainWindow()
    {
        InitializeComponent();
       
        
    
        MyMapView.Loaded += MyMapView_Loaded; 
    }
    
    private void MyMapView_Loaded(object sender, RoutedEventArgs e)
    {
       
    }
    The MyMapView_Loaded handler calls a function that attempts to load online layers.

  5. Create a new async function called TryLoadOnlineLayers. Inside the function, add a try block that catches three types of exception: Esri.ArcGISRuntime.Http.ArcGISWebException, System.Net.Http.HttpRequestException, and System.Exception as shown in the following code.

    private async void TryLoadOnlineLayers()
    {
        try
        {
            
    
        // handle a variety of possible exceptions
        }                
        catch (ArcGISWebException arcGISExp)
        {
            // token required?
            MessageBox.Show("Unable to load online layers: credentials may be required", "Load Error");
            
            
        }
        catch (System.Net.Http.HttpRequestException httpExp)
        {
            // not connected? server down? wrong URI?
            MessageBox.Show("Unable to load online layers: check your connection and verify service URLs", "Load Error");
            
            
        }
        catch (Exception exp)
        {
            // other problems ...
            MessageBox.Show("Unable to load online layers: " + exp.Message, "Load Error");
            
            
        }
    }
    By handling exceptions in your try block from specific to general, you can better identify problems encountered when accessing online services. An ArcGISWebException is commonly thrown when access to a secured online service is made without the proper credentials. An HttpRequestException may be thrown for a variety of network connectivity problems, such as an incorrect URI or an unavailable server resource. Other problems are handled by catching the generic System.Exception.

  6. Inside the try block, add code to create two new layers: an ArcGISTiledMapServiceLayer to display a streets basemap, and a FeatureLayer to display marine wildlife sightings from an online feature service. Give the FeatureLayer an ID so you can find it in the map's layer collection later.

    // create an online tiled map service layer, an online feature layer
    var basemapLayer = new ArcGISTiledMapServiceLayer(new Uri(basemapUrl));
    var operationalLayer = new FeatureLayer(new Uri(operationalUrl));
                    
    // give the feature layer an ID so it can be found later
    operationalLayer.ID = "Sightings";

  7. Initialize the layers by calling InitializeAsync. Since this is an asynchronous call, you must use the await keyword to await the result. After initializing, check each layer's InitializationException property to see if initialization was successful.

    // initialize the layers
    await basemapLayer.InitializeAsync();
    await operationalLayer.InitializeAsync();
    
    // see if there was an exception when initializing the layers, if so throw an exception
    if (basemapLayer.InitializationException != null || 
        operationalLayer.InitializationException != null)
    {
        // unable to load one or more of the layers, throw an exception
        throw new Exception("Could not initialize layers");
    }

  8. If the layers were initialized without an exception, add them to the map.

    // add layers
    MyMapView.Map.Layers.Add(basemapLayer);
    MyMapView.Map.Layers.Add(operationalLayer);

  9. Call the TryLoadOnlineLayers function from your MyMapView_Loaded event handler.

    private void MyMapView_Loaded(object sender, RoutedEventArgs e)
    {
        // try to load the online layers
        TryLoadOnlineLayers();
    }

  10. Run the app. If you have an Internet connection, you should see the online layers load.

    App displaying online layers

Create a local tile cache

A set of map images (tiles) can be generated from a tiled map service if its ExportTiles capability is enabled. Since the world street map layer in your map supports this capability, you can request tiles for a specified map extent and scale levels. Tiles can be generated as compact cache or tile package format. Once they've been generated by the server, you can download them to the local device.

A tiled map service must support the Export Tiles operation

Before you can create a local version of the map, create local datasets for the basemap (a local tile cache) and for the operational layer (a local geodatabase table). To control the size of the datasets generated, only enable the generate tiles button when the user zooms in to a smaller portion of the map.

  1. Add the following code in your page's constructor to handle the MyMapView.NavigationCompleted event. When a navigation operation completes (the user pans or zooms in or out, in other words), the GenerateLocalTilesButton control is only enabled if the user has zoomed to a reasonable extent for creating the tile cache.

    public MainWindow()
    {
        InitializeComponent();
    
    
        MyMapView.NavigationCompleted += (s, e) => { GenerateLocalTilesButton.IsEnabled = MyMapView.Scale < 6000000; };
        MyMapView.Loaded += MyMapView_Loaded; 
    }

    Tip:

    You can use ExportTileCacheTask.EstimateTileCacheSizeAsync to estimate the physical size required for a desired tile cache. This can help you determine if the creation of a particular cache is practical for your app or not.

  2. Declare a private CancellationTokenSource variable at the top of your class to manage user requests to cancel cache generation.

    private CancellationTokenSource cancellationTokenSource;

  3. The cache generation code goes in the GetTiles function, which is the event handler for the GenerateLocalTilesButton.Click event. Start the code with a Try block. In the catch portion of the block, show the exception in the messages list. Use a finally statement to reset the progress bar to zero.

    private async void GetTiles(object sender, RoutedEventArgs e)
     {
        try
        {
    
        }
        catch (Exception exp)
        {
            StatusMessagesList.Items.Clear();
            StatusMessagesList.Items.Add("Unable to get local tiles: " + exp.Message);
        }
        finally
        {
            // reset the progress indicator
            StatusProgressBar.Value = 0;  
        }
    }

  4. Start the code in the try block by showing the controls for reporting the status of the task. Add a message that reports the cache is being requested.

    // show the status controls
    StatusPanel.Visibility = Visibility.Visible;
    StatusMessagesList.Items.Add("Requesting tile cache ...");

  5. Cancel any previously submitted tasks that might still be executing (in case the user clicked the button before an earlier request was completed, for example). Create the CancellationTokenSource and CancellationToken for the current request.

    // cancel if an earlier call was made
    if (cancellationTokenSource != null)
    {
        cancellationTokenSource.Cancel();
    }
    
    // get a cancellation token for this task
    cancellationTokenSource = new CancellationTokenSource();
    var cancelToken = cancellationTokenSource.Token;

  6. Create a new ExportTileCacheTask that uses the URL for the world street map service in your map.

    // create a new ExportTileCacheTask to generate the tiles
    var exportTilesTask = new ExportTileCacheTask(new Uri(basemapUrl));

    There are two broad steps involved for creating a local tile cache from a service: 1) generate the tiles on the server, and 2) download the tiles to the local device. The ExportTileCache class has methods for performing each of these tasks individually (GenerateTileCacheAsync and DownloadTileCacheAsync). In the following steps, however, call a single method, GenerateTileCacheAndDownloadAsync, that performs both steps.

    ExportTileCachTask.GenerateTileCacheAndDownloadAsync requires the following parameters.

    • Generation options (GenerateTileCacheParameters)—Defines the output format, map extent, and scale levels to include in the cache.
    • Download options (DownloadTileCacheParameters)—Specifies the location on the local disk, and whether or not to overwrite an existing cache.
    • Check interval (TimeSpan)—The interval at which to check the status of the cache generation.
    • Cancellation token (System.Threading.CancellationToken)—Allows the task to be cancelled.
    • Progress callbacks (IProgress<T>)—Callbacks to report progress for both cache generation and download (optional).

  7. Create a new GenerateTileCacheParameters object and define some options for the cache, such as the extent and scale levels to create.

    // define options for the new tiles (extent, scale levels, format)
    var generateOptions = new GenerateTileCacheParameters();
    generateOptions.Format = ExportTileCacheFormat.CompactCache; 
    generateOptions.GeometryFilter = MyMapView.Extent;
    generateOptions.MinScale = 6000000.0;
    generateOptions.MaxScale = 1.0;
    Note:

    Since the MinScale and MaxScale values for the cache parameters are specified with the scale denominator, the value of MinScale is always larger than the MaxScale value (a scale numerator value of 1.0 is implied). In the previous code, the minimum scale is 1.0 / 6000000.0 = 0.00000017 and the maximum is 1.0 / 1.0 = 1.0.

  8. Create a new DownloadTileCacheParameters object and define the location for the output file.

    // download the tile package to the app's local folder
    var outFolder = System.AppDomain.CurrentDomain.BaseDirectory;
    var downloadOptions = new DownloadTileCacheParameters(outFolder);
    
    // overwrite the file if it already exists
    downloadOptions.OverwriteExistingFiles = true;

  9. Define a two-second interval for checking the status of the cache creation.

    // check generation progress every two seconds
    var checkInterval = TimeSpan.FromSeconds(2);

  10. The optional IProgress<T> arguments report status while the task is running. Create one for the cache generation progress (Progress<ExportTileCacheJob>) using the following code. In the callback handler, parse out the percentage complete and update the progress bar. Other progress messages are added to the StatusMessageList for the user to see.

    var creationProgress = new Progress<ExportTileCacheJob>(p =>
    {
        StatusMessagesList.Items.Clear();
        foreach (var m in p.Messages)
        {
            // find messages with percent complete
            // "Finished:: 9 percent", e.g.
            if (m.Description.Contains("Finished::"))
            {
                // parse out the percentage complete and update the progress bar
                var numString = m.Description.Substring(m.Description.IndexOf("::") + 2, 3).Trim();
                var pct = 0.0;
                if (double.TryParse(numString, out pct))
                {
                    StatusProgressBar.Value = pct;
                }
            }
            else
            {
                // show other status messages in the list
                StatusMessagesList.Items.Add(m.Description);
            }
        }
    });

  11. Create another IProgress<T> variable to report the download progress (Progress<ExportTileCacheDownloadProgress>). Use the ProgressPercentage property to update the progress bar.

    // show download progress 
    var downloadProgress = new Progress<ExportTileCacheDownloadProgress>(p =>
    {
        StatusProgressBar.Value = p.ProgressPercentage;                     
    });

  12. Call ExportTileCacheTask.GenerateTileCacheAndDownloadAsync to create the local cache. Pass in the required arguments created previously. Use the await keyword to await the result.

    // generate the tiles and download them 
    var result = await exportTilesTask.GenerateTileCacheAndDownloadAsync(generateOptions, 
                                                                         downloadOptions, 
                                                                         checkInterval, 
                                                                         cancelToken, 
                                                                         creationProgress,
                                                                         downloadProgress);

  13. When the task completes, store the path to the local cache and report success to the user.

    // when complete, store the path to the new local tile cache
    this.localTileCachePath = result.OutputPath;
    LocalTilesPathTextBlock.Text = this.localTileCachePath;
    LocalTilesPathTextBlock.ToolTip = this.localTileCachePath;
    
    
    // clear the working messages, report success
    StatusProgressBar.Value = 100;
    StatusMessagesList.Items.Clear();
    StatusMessagesList.Items.Add("Local tiles created at " + this.localTileCachePath);

  14. Run your app, zoom into an area, and test creating a local tile cache.

    Generating local tiles for the map extent

In the next section, you will follow similar steps to create a local geodatabase containing the features in the sightings layer.

Add code to create a local runtime geodatabase

A geodatabase containing datasets (tables) can be generated from a feature service if it is sync enabled (the CreateReplica capability is enabled, in other words). Since the SaveTheBaySync feature service containing the marine sightings layer in your map supports this capability, you can request features for specified layers within a defined extent. Features are generated as a runtime geodatabase containing a table for each layer requested. Once generated by the server, you can download the geodatabase to the local device.

A sync enabled feature service supports the Create Replica operation
Tip:

You can sync enable your ArcGIS Online hosted feature services by checking Enable Sync in the item properties.

The user will be able to create a copy of the features in the feature service and store them in a geodatabase locally. In the following steps, you write code to create a local geodatabase for a single layer in the service and at the current map extent.

  1. The geodatabase generation code goes in the GetFeatures function, which is the event handler for the GenerateLocalGeodatabaseButton.Click event. Start the code with a try block as shown in the following example.

    private async void GetFeatures(object sender, RoutedEventArgs e)
    {
        try
        {
    
        }
        catch (Exception ex)
        {
     
        }
    }

  2. Inside the try portion of the block, show the status controls, add a message that the generate geodatabase job has been submitted, and show the progress bar.

    // show the status controls
    StatusPanel.Visibility = Visibility.Visible;
    StatusMessagesList.Items.Add("Submitting generate geodatabase job ...");
    StatusProgressBar.IsIndeterminate = true;
    StatusProgressBar.IsEnabled = true;

    Note:

    The IsIndeterminate setting for the ProgressBar control means that it continually scrolls to show that an operation is working, as opposed to showing the percent complete.

  3. In the catch portion of the block, show the exception in the messages list and reset the IsIndeterminate property of the progress bar false.

    catch (Exception ex)
    {
        StatusMessagesList.Items.Add("Unable to create offline database: " + ex.Message);
        StatusProgressBar.IsIndeterminate = false;
    }

  4. Return to the try block. After the code to show the status controls, add code to cancel any previously submitted jobs that may be running and to create a new CancellationToken for the current job.

    // cancel if an earlier call was made
    if (cancellationTokenSource != null)
    {
        cancellationTokenSource.Cancel();
    }
    
    // get a cancellation token
    cancellationTokenSource = new CancellationTokenSource();
    var cancelToken = cancellationTokenSource.Token;

  5. A GeodatabaseSyncTask is created by passing in the URI for the feature service you want to work with. The feature service must be sync enabled to create a local geodatabase.
  6. Use the URL to the sightings layer to get the URL of the feature service by stripping off the layer index at the end ("/0").

    // create a new GeodatabaseSyncTask with the uri of the feature service to pull from
    var serverUrl = operationalUrl.Substring(0, operationalUrl.LastIndexOf('/'));
    var uri = new Uri(serverUrl);
    var getFeaturesTask = new GeodatabaseSyncTask(uri);

    The GeodatabaseSyncTask.GenerateGeodatabaseAsync method creates the desired geodatabase on the server and requires the following parameters.

    • Generation options (GenerateGeodatabaseParameters)—Defines the output spatial reference, map extent, and layers to include in the geodatabase.
    • Generate complete callback—A callback that executes when the geodatabase generation is complete. Use the callback to see if generation was successful and to download the data locally.
    • Check interval (TimeSpan)—The interval at which to check the status of the geodatabase generation.
    • Progress callback (IProgress<T>)—A callback to report progress for geodatabase creation (optional).
    • Cancellation token (System.Threading.CancellationToken)—Allows the task to be cancelled.

  7. Create a GenerateGeodatabaseParameters and set the layers, extent, and spatial reference for the output geodatabase.

    // create parameters for the task: layers and extent to include, out spatial reference, and sync model
    var layers = new List<int>(new int[1] { 0 }); // just get the first layer
    var extent = MyMapView.Extent;
    var getFeaturesParams = new GenerateGeodatabaseParameters(layers, extent)
    {
        OutSpatialReference = MyMapView.SpatialReference,
        SyncModel = SyncModel.PerLayer
    };
    Note:

    The SyncModel property has two possible values: PerGeodatabase or PerLayer. This setting controls the granularity of synchronization of edits with the service, managed either on a layer-by-layer basis or for the geodatabase as a whole.

  8. Create a two-second interval for checking the status of the job. Use a Progress<GeodatabaseStatusInfo> to show the current status.

    // check progress every two seconds
    var checkInterval = TimeSpan.FromSeconds(2);
    var creationProgress = new Progress<GeodatabaseStatusInfo>(p =>
    {
        this.StatusMessagesList.Items.Add(DateTime.Now.ToShortTimeString() + ": " + p.Status);
    });

    The last thing required before generating the geodatabase is a callback handler to execute when the server job is complete. The callback sees if the database was created successfully on the server, then downloads it locally.

  9. Create a new function to act as the callback handler for the GenerateGeodatabaseAsync method. The function will be asynchronous and take two arguments: a GeodatabaseStatusInfo and an Exception.

    // provide a callback to execute when the GeodatabaseSyncTask completes (successfully or with an exception)
    private async void GenerateFeaturesCompleteCallback(GeodatabaseStatusInfo statusInfo, Exception ex)
    {
    
    }

  10. Start the callback function by checking for an exception. If an exception has occurred, report it to the user and return.

    // if unsuccessful, report the exception and return
    if (ex != null)
    {
        this.Dispatcher.Invoke(() => StatusMessagesList.Items.Add("An exception occurred: " + ex.Message));
        
        return;
    }

    Caution:

    Because the callback may be executing on a thread other than the UI thread, you must use Dispatcher.Invoke to update elements in the UI, as shown in the preceding code. See Threading considerations for more information.

  11. If the geodatabase was generated without exceptions, download it using ArcGISHttpClient. The GeodatabaseStatusInfo object passed into the callback provides the URI for the database.

    // if successful, download the generated geodatabase from the server
    var client = new ArcGISHttpClient();
    var geodatabaseStream = client.GetOrPostAsync(statusInfo.ResultUri, null);

  12. Create an output path for the geodatabase. Call it Wildlife.geodatabase and store it in the app's directory.

    // create a path for the local geodatabase
    var outFolder = System.AppDomain.CurrentDomain.BaseDirectory;
    var geodatabasePath = System.IO.Path.Combine(outFolder, "Wildlife.geodatabase");

  13. Use a TaskFactory to create a new task to copy the database stream to the output file. Use Dispatcher.Invoke to update UI elements with the local geodatabase path when the copy completes.

    // download the geodatabase to the local machine
    await Task.Factory.StartNew(async delegate
    {
        using (var stream = System.IO.File.Create(geodatabasePath))
        {
            await geodatabaseStream.Result.Content.CopyToAsync(stream);
        }
    
        this.localGeodatabasePath = geodatabasePath;
        this.Dispatcher.Invoke(() => LocalDataPathTextBlock.Text = geodatabasePath);
        this.Dispatcher.Invoke(() => LocalDataPathTextBlock.ToolTip = geodatabasePath);
        this.Dispatcher.Invoke(() => StatusMessagesList.Items.Add("Features downloaded to " + geodatabasePath));
        this.Dispatcher.Invoke(() => StatusProgressBar.IsIndeterminate = false);
    });

    Your callback handler for generating the geodatabase is complete.

  14. Return to the GetFeatures function and add a call to GenerateGeodatabaseAsync. Pass in the required parameters and the callback to execute when it completes.

    // call GenerateGeodatabaseAsync, the GenerateFeaturesCompleteCallback callback will execute when it's complete
    var gdbResult = await getFeaturesTask.GenerateGeodatabaseAsync(getFeaturesParams,
                                                                   GenerateFeaturesCompleteCallback,
                                                                   checkInterval,
                                                                   creationProgress,
                                                                   cancelToken);

  15. Run your app, zoom into an area, and test creating a local geodatabase.

    Creating a local version of the feature data in the current map extent

Switch between offline and online modes

Many ArcGIS Runtime SDK for .NET classes have online and local versions. In your current map, you're using an ArcGISTiledMapServiceLayer to display a tiled service from an online source. The equivalent layer for displaying tiles from a local cache is ArcGISLocalTiledLayer. Similarly, you can display features from an online source using a FeatureLayer with a ServiceFeatureTable. For features stored in a local runtime geodatabase, you use the same FeatureLayer class, but with a GeodatabaseFeatureTable.

In the following steps, add code to switch the map between using online and local sources. When the app is in online mode, the map displays the online data sources (services). When offline, display data from the local tile cache and geodatabase (if they've been created).

    Previously, you created a function to load the online layers in your map (TryLoadOnlineLayers). You will now create the equivalent code for loading local (offline) layers.
  1. Create a new asynchronous function in your code behind (MainWindow.xaml.cs) called TryLoadLocalLayers.
  2. Add a try block and handle exceptions with a single catch block that displays a message to the user.

    private async void TryLoadLocalLayers()
    {
        try
        {    
    
        }
        catch (Exception exp)
        {
            MessageBox.Show("Unable to load local layers: " + exp.Message, "Load Layers");
            
        }
    }

  3. Inside the try block, check to see that the local tiles and geodatabase exist. If they don't, throw an exception with the appropriate message to the user.

    if (string.IsNullOrEmpty(this.localGeodatabasePath))
    {
        throw new Exception("Local features do not yet exist. Please generate them first.");
    }
    
    if (string.IsNullOrEmpty(this.localTileCachePath))
    {
        throw new Exception("Local tiles do not yet exist. Please generate them first.");
    }

  4. Create a new ArcGISLocalTiledLayer. Pass the path to the local cache in the constructor.

    // create a local tiled layer
    var basemapLayer = new ArcGISLocalTiledLayer(this.localTileCachePath);

  5. A local runtime geodatabase can be accessed using the static OpenAsync method on the Geodatabase class. Once opened, you can access the tables inside. A geodatabase table that has geometry can be added to the map and displayed with a FeatureLayer.
  6. Open the local geodatabase and get a reference to the first table (it should contain only the marine wildlife sightings table). Create a new FeatureLayer to display the table.

    // open the local geodatabase, get the first (only) table, create a FeatureLayer to display it
    var localGdb = await Geodatabase.OpenAsync(this.localGeodatabasePath);
    var gdbTable = localGdb.FeatureTables.FirstOrDefault();
    var operationalLayer = new FeatureLayer(gdbTable);
    // give the feature layer an ID so it can be found later
    operationalLayer.ID = "Sightings";

  7. Initialize the local layers and await completion. If either layer was not initialized successfully, throw an exception with a message for the user.

    await basemapLayer.InitializeAsync();
    await operationalLayer.InitializeAsync();
    
    // see if there was an exception when initializing the layers
    if (basemapLayer.InitializationException != null || 
        operationalLayer.InitializationException != null)
    {
        // unable to load one or more of the layers, throw an exception
        throw new Exception("Could not initialize local layers");
    }

  8. Add the local layers to the map.

    MyMapView.Map.Layers.Add(basemapLayer);
    MyMapView.Map.Layers.Add(operationalLayer);

  9. Add code to call the functions to load online (TryLoadOnlineLayers) or local data (TryLoadLocalLayers) when the corresponding radio buttons in the UI are clicked.
  10. Navigate to the DataOptionChecked function in your code behind. This is the event handler for the local and online radio buttons.
  11. Before calling code to load layers, clear all current layers in the map.

    private void DataOptionChecked(object sender, RoutedEventArgs e)
    {
        MyMapView.Map.Layers.Clear();
    
    
    }

  12. Use a branching statement to load either online or local layers depending on what the user selected.

    if (UseOnlineDataOption.IsChecked == true)
    {
        TryLoadOnlineLayers();
    }
    else // offline
    {
        TryLoadLocalLayers();
    }

  13. Test your code: run the app, zoom into an area of interest, generate local features and tiles, then toggle between online and local data for the map.

As you zoom out of the map when using your local data, you'll notice that the map tiles and features only exist within the original extent at which they were generated.

Using local features and tiles in the map

You've completed the tutorial, nice work. You now have a better understanding of how to generate local datasets from online services and how to display them in your map. For more information about editing with offline feature data and synchronizing edits with the original service, see the Editing, Edit features, and Sync offline edits topics.