Saving the Map State to IsolatedStorage with the ArcGIS Silverlight API

A common scenario when using an application is to have some means of saving the state. In this sample I’ll be taking the Esri Map control and saving the last extent and visible layers so that the next time the application starts the settings are remembered.

Ideally we would be able to bind all the properties using the TwoWay binding built into Silverlight though this is not always practical or possible. Map extent is a good example of this as firstly it is not a DependencyProperty and secondly, the value that we save or set for the extent may not be the actual extent value e.g. the browser window size has changed. With a little bit of code we can achieve what we want.

To start we’ll define our state model base (I would say view model as it represents the same thing but we aren’t using binding, either way you get the picture). 

public abstract class StateModelBase 
{
    public void Save(IPersistable persistance)
    {
        persistance.Save(this);
    }
}

Now we can define the map state class, this will handle the state management for the Map control and extends our StateModelBase. This could contain anything you want to store such as child layer definitions etc.

[KnownType(typeof(MapState))]
public class MapState : StateModelBase
{
    private Map _map;

    public MapState()
    {
        LayersVisible = new Dictionary<string, bool>();
    }

    /// <summary>
    /// Extent of the <c>Map</c>
    /// </summary>
    [DataMember]
    public Envelope Extent { get; set; }

    /// <summary>
    /// The visible state of each <c>Layer</c> in the <c>Map</c> <c>LayersCollection</c>
    /// </summary>
    [DataMember]
    public IDictionary<string, bool> LayersVisible { get; set; }

    /// <summary>
    /// Called to set the <c>Map</c> control to persist settings for 
    /// </summary>
    /// <param name="map"></param>
    public void Initialise(Map map)
    {
        _map = map;

        if (Extent == null)
            return;

        if (Extent != _map.Extent)
            _map.ZoomTo(Extent);

        foreach (var item in LayersVisible.Where(item => _map.Layers[item.Key] != null))
        {
            _map.Layers[item.Key].Visible = item.Value;
        }
    }

    /// <summary>
    /// Update the state values.
    /// </summary>
    public void Update()
    {
        Extent = _map.Extent;
        LayersVisible = _map.Layers.Where(layer => !string.IsNullOrWhiteSpace(layer.ID)).ToDictionary(layer => layer.ID, layer => layer.Visible);
    }
}

Now we have what we want to persist we need the mechanism to do it. For interacting with the IsolatedStorage we can use the following code.

public interface IPersistable
{
    T Load<T>() where T : StateModelBase;
    void Save<T>(T viewModel) where T : StateModelBase;
}
/// <summary>
/// Implementation of <c>IPersistable</c> that persists to and from Isolated Storage
/// </summary>
public class IsolatedStoragePersitence : IPersistable
{
    private readonly string _applicationName;

    public IsolatedStoragePersitence(string applicationName)
    {
        _applicationName = applicationName;
    }

    private string GetIsFile(Type t)
    {
        var assemblyName = new AssemblyName(t.Assembly.FullName);
        return string.Format("{0}_{1}_{2}.dat", _applicationName, t.Name, assemblyName.Version);
    }
        
    /// <summary>
    /// Load the <c>StateModelBase</c> from Isolated Storage 
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    public T Load<T>() where T : StateModelBase
    {
        //return null;
        string data = string.Empty;

        if (IsolatedStorageFile.IsEnabled)
        {
            using (var userAppStore = IsolatedStorageFile.GetUserStoreForApplication())
            {
                var dataFileName = GetIsFile(typeof(T));
                if (userAppStore.FileExists(dataFileName))
                {
                    using (var iss = userAppStore.OpenFile(dataFileName, FileMode.Open))
                    {
                        using (var sr = new StreamReader(iss))
                        {
                            data = sr.ReadToEnd();
                        }
                    }
                }
            }
        }

        if (string.IsNullOrWhiteSpace(data))
            return null;

        var serializer = new DataContractJsonSerializer(typeof(T));

        var stream = new MemoryStream(Encoding.UTF8.GetBytes(data));

        var viewModel = (T)serializer.ReadObject(stream);

        return viewModel;
    }

    /// <summary>
    /// Save the <c>StateModelBase</c> to Isolated Storage 
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="viewModel"></param>
    public void Save<T>(T viewModel) where T : StateModelBase
    {
        if (!IsolatedStorageFile.IsEnabled)
            return;
        var dataFileName = GetIsFile((viewModel.GetType()));
        using (var userAppStore = IsolatedStorageFile.GetUserStoreForApplication())
        {
            if (userAppStore.FileExists(dataFileName))
                userAppStore.DeleteFile(dataFileName);

            using (var iss = userAppStore.CreateFile(dataFileName))
            {
                using (var sw = new StreamWriter(iss))
                {
                    string dataAsString;

                    using (var ms = new MemoryStream())
                    {
                        var serializer = new DataContractJsonSerializer(typeof(T));
                        serializer.WriteObject(ms, viewModel);
                        ms.Position = 0;
                        using (var sr = new StreamReader(ms))
                        {
                            dataAsString = sr.ReadToEnd();
                        }
                    }

                    sw.Write(dataAsString);
                    sw.Close();
                }
            }
        }
    }
}

The last thing to take care of is wiring up the parts. As we want to load our state when the application runs and save the state when it exits we can hook into the Application events. In our App.xaml.cs add

internal static new App Current { get { return (App)Application.Current; } }

internal IPersistable Persistance { get; private set; }

private void Application_Startup(object sender, StartupEventArgs e)
{
    Persistance = new IsolatedStoragePersitence("SamplePersistence");
    RootVisual = new MainPage();
}

private void Application_Exit(object sender, EventArgs e)
{
    ((MainPage)RootVisual).MapState.Update();
    ((MainPage)RootVisual).MapState.Save(Persistance);
}

and in our MainPage.xaml.cs class we can add

public partial class MainPage
{
    public MapState MapState { get; set; }

    public MainPage()
    {
        InitializeComponent();

        // Load our viewmodels
        MapState = App.Current.Persistance.Load<MapState>() ?? new MapState();
        MapState.Initialise(MyMap);
    }
}

Finally we need a Map and some layers in the UI. I’ve taken some markup from the Esri samples but feel free to add anything you like and play around with the code.

<UserControl x:Class="SilverlightPersistence.MainPage"
    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:esri="http://schemas.esri.com/arcgis/client/2009" 
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">

    <Grid x:Name="LayoutRoot" Background="White">
        <esri:Map x:Name="MyMap" WrapAround="True" Extent="-15000000,3600000,-11000000,5300000" Background="White">
            <esri:ArcGISTiledMapServiceLayer ID="World"
                Url="http://services.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer" />
            <esri:ArcGISTiledMapServiceLayer ID="ShadedRelief" 
                    Url="http://services.arcgisonline.com/ArcGIS/rest/services/World_Shaded_Relief/MapServer" Visible="False"/>
            <esri:ArcGISDynamicMapServiceLayer ID="California" VisibleLayers="8,10"
                    Url="http://maverick.arcgis.com/ArcGIS/rest/services/California/MapServer" />
            <esri:FeatureLayer ID="Earthquakes" Mode="OnDemand"
                    Url="http://sampleserver3.arcgisonline.com/ArcGIS/rest/services/Earthquakes/EarthquakesFromLastSevenDays/MapServer/0" />
        </esri:Map>

        <Border Background="#77919191" BorderThickness="1" CornerRadius="5"
            HorizontalAlignment="Right"  VerticalAlignment="Top"
            Margin="20" Padding="5" BorderBrush="Black" >
            <ListBox x:Name="MyList" ItemsSource="{Binding ElementName=MyMap, Path=Layers}">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <StackPanel Orientation="Horizontal">
                            <!--Layer visibility checkbox-->
                            <CheckBox IsChecked="{Binding Visible, Mode=TwoWay}" />                            
                            <!--Layer name-->
                            <TextBlock Text="{Binding ID, Mode=OneWay}" Margin="5,0,0,0" > 
                            <!-- Tooltip on hover-->
                                <ToolTipService.ToolTip>
                                    <StackPanel MaxWidth="400">
                                        <TextBlock FontWeight="Bold" Text="{Binding CopyrightText}" TextWrapping="Wrap" />
                                        <TextBlock Text="{Binding Description}" TextWrapping="Wrap" />
                                    </StackPanel>
                                </ToolTipService.ToolTip>
                            </TextBlock>
                        </StackPanel>
                    </DataTemplate>
                </ListBox.ItemTemplate>
            </ListBox>
        </Border>

    </Grid>
</UserControl>

Build and run your app and next time you run it the state will be remembered. Hopefully you can use this example as a starting point for your coding.

Advertisements

6 comments

  1. I’m trying to get this to work as a tool I can plug into ESRI’s Silverlight Viewer to accomplish the same thing; do you have any suggestions? Also, do you perhaps have a sample I can download on Github or somewhere? I seem to be missing some assemblies; for instance, it has a problem with IDictionary, even though mscorlib is referenced.
    Thanks!

    1. Hi Karen,

      I’d suggest looking at implementing this as a behaviour. Sorry I don’t have the source anymore (it was a while ago that I wrote this) but the code above should work as is. If I get time I’ll get it running again and post a link to it.

      1. Dave,
        Thanks! Yeah, I’ve been approaching it as a behavior, but for some reason when I use this code in Visual Studio 10, (using Silverlight 4, Esri’s Silverlight API 2.4) it has a lot of problems with not recognizing assemblies, even if they’re already referenced.

  2. Hi Dave,
    Just wanted to say thanks for your post, this has been very useful, and was very well explained.
    Karen: I initially had problems with finding some assemblies, but after searching for and adding unreferenced ones it all worked fine for me. I am also using VS2010, SL4 and ESRIs SL API 2.4.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s