Esri Mobile 10 SDK, WPF and MVVM

We are going to be building off the project we created from my previous blog on ‘Esri Mobile 10 SDK and WPF Beginner’s Tutorial’. In that tutorial we created a map, added basic navigation and set up syncing. In this blog we are going to add another common GIS tool, selecting features and displaying results but following the MVVM design pattern.

MVVM stands for the Model-View-ViewModel design pattern commonly used in WPF/Silverlight applications. Like MVP and MVC, it strives to separate the user interface (UI) or View from the code behind doing the heavy lifting (querying, updating data, so on). UI design is becoming its own discipline and often the View will be designed by someone else working in Expression Blend (EB). For more information on MVVM, check out http://en.wikipedia.org/wiki/Model_View_ViewModel.

In this blog I am going to dive in and give a brief introduction and examples using the Esri Mobile 10 SDK with the following concepts:

  • Model
  • View
  • ViewModel
  • Bindings
  • Commands
  • Storyboards
  • Event Triggers

In your project add a Model, View and ViewModel folder. There are, like everything, multiple ways to implement MVVM as well as debates on where to put certain things. The implementation of MVVM design pattern I will use is a slightly easier variation of this one by Josh Smith- http://joshsmithonwpf.wordpress.com/advanced-mvvm/.  At the end of this tutorial, you should have a class diagram that resembles the following:

First let’s set up which layer we are going to select. In my example here, I am going to use the Schools layer that you can pull off the ESRI Data & Maps DVD. I clipped the schools by the State of Texas and named this layer tx_schools. (You can use whatever layer you have in your cache or pull this layer out as well. If you use your own layer, you will have to also create your own Model and modify the ViewModels covered here accordingly.)

In the Settings.xml file, I add the tag <selectLayer>tx_schools</selectLayer>. We will come back later in this blog to parse this out. Let’s now switch to setting up the View. Add a UserControl to the View folder and name it ResultsControlView . Size as you would like. Add a DataGrid control to your UserControl and name it dataGridResults. For my example, I want three of those fields to be displayed in my datagrid so I add three columns like so:

<DataGrid AutoGenerateColumns="False" Height="225" HorizontalAlignment="Left" Margin="10" Name="dataGridResults" VerticalAlignment="Top" Width="475" ItemsSource="{Binding SchoolList}" SelectedItem="{Binding SelectedItem}"><DataGrid.Columns><DataGridTextColumn Binding="{Binding SchoolName}" Header="School Name" FontWeight="Bold"/><DataGridTextColumn Binding="{Binding LabelFlag}" Header="Label Flag"/><DataGridTextColumn Binding="{Binding StCtyFips}" Header="FIPS"/></DataGrid.Columns></DataGrid>

A couple thinks to point out in this snippet from our xaml is the {Binding } term. Like I mentioned early we are going to use xaml’s ability to bind values from our ViewModel and Model to our View. All the view needs to know is what these properties will be named. My DataGrid source will be bound to a collection called SchoolList. The DataGrid SelectedItem is bound to my property SelectedItem. Each of the columns are bound to three properties coming from my school Model. When we create our ViewModel and Model things will start to make more sense.

Switch back to MainWindow.xaml and let’s add our new control. First add reference to Window header tag to our View namespace xmlns:view=”clr-namespace:WpfMobileTuturialMVVM.View”.  Now we can add our control in the xaml like so with some minor styling (you can change as you feel fit):

<view:ResultsControlView x:Name="resultsView" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="10" Visibility="Hidden"/>

Next let’s create our Model. Before we get started I want to introduce INotifyPropertyChanged. This interface is very important for bound properties. This is the mechanism that lets the View bound properties know if changes have occurred back in Model or ViewModel automatically. Pretty sweet right? Before we create the Model for our schools Layer we will create a base class that sets up the INotifyPropertyChanged. Add a new class to Model folder and call it EntityBase. And insert following code:

public class EntityBase:INotifyPropertyChanged
    {
        #region INotifyPropertyChanged Members
        public event PropertyChangedEventHandler PropertyChanged;
        protected void FirePropertyChange(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
        #endregion
    }

Now right click Model folder and add new class called SchoolModel that implements our EntityBase. The reason for the separation is because more than likely in a real world solution you will have multiple models. And there is no point in re-implementing the same code across all of them. Add the following code snippet to SchoolModel:

public class SchoolModel:EntityBase
    {
        public SchoolModel() { }
        #region members
        private string _schoolName;
        private string _stCtyFips;
        private int _labelFlag;
        private Geometry _schoolGeometry;
        #endregion
        #region properties
        public string SchoolName
        {
            get { return _schoolName; }
            set
            {
                _schoolName = value;
                base.FirePropertyChange("SchoolName");
            }
        }
        public string StCtyFips
        {
            get { return _stCtyFips; }
            set
            {
                _stCtyFips = value;
                base.FirePropertyChange("StCtyFips");
            }
        }
        public int LabelFlag
        {
            get { return _labelFlag; }
            set
            {
                _labelFlag = value;
                base.FirePropertyChange("LabelFlag");
            }
        }
        public Geometry SchoolGeometry
        {
            get { return _schoolGeometry; }
            set
            {
                _schoolGeometry = value;
                base.FirePropertyChange("SchoolGeometry");
            }
        }
        #endregion
    }

Let’s walk through this now. If you have worked in other design patterns you will notice this looks very similar to what are termed Domains but with utilization of the INotifyPropertyChanged method we defined in EntityBase (If you are a stickler you can separate this class into Domain and Model classes). If you look at the layer in ArcCatalog you will notice that I did not include all the fields. Typically I have the Model reflect every field in the actual layer, but for this tutorial I am cutting some corners. But you see where those property names we used in View are now defined here in the Model.  But we need the middle piece to bind them together so time to move onto the ViewModel.

First off let’s set up a base class for all the ViewModels that will implement INotifyPropertyChanged. Add a new class to ViewModel folder and name it ViewModelBase. Add the following code snippet:

#region INotifyPropertyChanged Members
        public event PropertyChangedEventHandler PropertyChanged;
        #endregion
        #region Protected Methods
        /// <summary>
        /// Fires the PropertyChanged event.
        /// </summary>
        /// <param name="propertyName">The name of the changed property.</param>
        protected void RaisePropertyChangedEvent(string propertyName)
        {
            if (PropertyChanged != null)
            {
                var e = new PropertyChangedEventArgs(propertyName);
                PropertyChanged(this, e);
            }
        }
        #endregion

For each View, we will have its own ViewModel. The master View is MainWindow and contains the ResultControlView. We are going to follow this pattern and create a MainWindowViewModel class and ResultsViewModel class in the ViewModel folder. Both this classes implement ViewModelBase. MainWindowViewModel will contain the reference to the ResultsViewModel and will pass calls that require changes to ResultsControllView bound properties to ResultsViewModel. Let us start setting up MainWindowViewModel:

public MainWindowViewModel() 
        {
            _resultsViewModel = new ResultsViewModel();
        }
        #region members
        ESRI.ArcGIS.Mobile.WPF.NavigationMode m_lastNavigatioMode;
        private ESRI.ArcGIS.Mobile.WPF.Map _map;
        private MobileCache _mobileCache;
        GraphicLayer selectionLayer;
        EnvelopeSketchTool envelopeSketchTool;
        private ResultsViewModel _resultsViewModel;
        #endregion
        #region properties
        public string FeatureLayerName { get; set; }
        public ESRI.ArcGIS.Mobile.WPF.Map Map
        {
            get { return _map; }
            set
            {
                _map = value;            
            }
        }
        public MobileCache MobileCache
        {
            get { return _mobileCache; }
            set
            {
                _mobileCache = value;
            }
        }
        public ResultsViewModel MyResultsViewModel
        {
            get { return _resultsViewModel; }
            set
            {
                _resultsViewModel = value;
                base.RaisePropertyChangedEvent("MyResultsViewModel");
            }
        }
        #endregion

Before we set the binding between the Views and ViewModels let us set up ResultsViewModel with following code snippet:

public ResultsViewModel() { }

        #region members
        private ObservableCollection<SchoolModel> _schoolList;
        private SchoolModel _selectedItem;
        private MobileCache _mobileCache;
        private ESRI.ArcGIS.Mobile.WPF.Map _map;
        private string _featureLayerName;
        private GraphicLayer glSelected;
        private FeatureDataTable selectedDataTable;
        private FeatureLayer featureLayer = null;
        #endregion
        #region properties
        public ObservableCollection<SchoolModel> SchoolList
        {
            get { return _schoolList; }
            set
            {
                _schoolList = value;
                base.RaisePropertyChangedEvent("SchoolList");
            }
        }
        public SchoolModel SelectedItem
        {
            get { return _selectedItem; }
            set
            {
                _selectedItem = value;
                base.RaisePropertyChangedEvent("SelectedItem");                
            }
        }
        public string FeatureLayerName
        {
            get { return _featureLayerName; }
            set { _featureLayerName = value; }
        }
        public MobileCache MobileCache
        {
            get { return _mobileCache; }
            set { _mobileCache = value; }
        }
        public ESRI.ArcGIS.Mobile.WPF.Map Map
        {
            get { return _map; }
            set { _map = value; }
        }
        #endregion

For now, we are going to focus on SchoolList and SelectedItem. Notice how SelectedItem is an instance of our ShcoolModel and SchoolList is a collection of those Models. This collection will represent the selection set. And this collection, ObservableCollection, is especially cool because it has built in notification about items getting added, removed, or updated. So, we don’t have to do anything special to handle any of that. Pretty cool.

Let’s connect up the Views with the ViewModels by switching to MainWindow.xaml.cs and update with following code snippet:

public MainWindow()
        {
            InitializeComponent();
            _mainWindowVM = new MainWindowViewModel();
            this.DataContext = _mainWindowVM;
        }
        private MainWindowViewModel _mainWindowVM;

In the method, Windows_Loaded add the following to the very bottom of the method:

_mainWindowVM.Map = map1;
_mainWindowVM.MobileCache = mobileCache;

Switch to MainWindow.xaml and add DataContext=”{Binding MyResultsViewModel}” to our ResultsControlView control.

What this did was bind instance of MainWindowViewModel to MainWindow.xaml by setting the DataContect. Since MainWindowViewModel exposes instance of ResultsViewModel, we then bound that to our ResultsControlView by also setting the DataContext. If we dive back into our ResultsControlView, we have that DataGrid that we set the source to SchoolList. This is exposed now through the bound instance of ResultsViewModel. If you are getting lost that is OK. Binding and how it tickles down to the children controls was new to me when I started as well.

So, let’s switch back to ResultsViewModel and add the methods for performing the spatial selection and populating our SchoolList so it shows up in our DataGrid on ResultsControlView.

#region methods
        public void SpatialQuerySchools(Envelope queryEnvelope)
        {
            //reset eveything
            SchoolList = new ObservableCollection<SchoolModel>();
            SelectedItem = new SchoolModel();
            //set up spatial queryfilter
            QueryFilter queryFilter = new QueryFilter();
            queryFilter.Geometry = queryEnvelope;
            queryFilter.GeometricRelationship = GeometricRelationshipType.Intersect;
            LoadSchoolResult(queryFilter);           
        }
        private void LoadSchoolResult(QueryFilter queryFilter)
        {
            //get feature layer
            if (featureLayer == null) featureLayer = getFeatureLayer();
            //if still null
            if (featureLayer == null)
            {
                MessageBox.Show("Could not find feature layer " + FeatureLayerName + " in map cache", "Search Error", MessageBoxButton.OK, MessageBoxImage.Error);
            }
            selectedDataTable = featureLayer.GetDataTable(queryFilter);
            FeatureDataReader selectDataReader = featureLayer.GetDataReader(queryFilter);
            //set up graphic layer
            _map.MapGraphicLayers.Remove(glSelected);
            //create selected graphic layer
            glSelected = new GraphicLayer();
            //create brush for fill
            System.Windows.Media.SolidColorBrush myBrush = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.Aqua);
            // Create a Pen to add to the GeometryDrawing created above.
            System.Windows.Media.Pen myPen = new System.Windows.Media.Pen();
            myPen.Thickness = 3;
            myPen.Brush = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.Aqua);
            glSelected.Fill = myBrush;
            glSelected.DrawingPen = myPen;
            glSelected.SelectionFill = myBrush;
            glSelected.SelectionPen = myPen;
            glSelected.DrawVertexes = false;
            _map.MapGraphicLayers.Add(glSelected);
            ObservableCollection<SchoolModel> schools = new ObservableCollection<SchoolModel>();
            try
            {
                if (selectedDataTable != null && selectedDataTable.Rows.Count > 0)
                {
                    FeatureDataRow featureRow;
                    for (int i = 0; i < selectedDataTable.Rows.Count; i++)
                    {

                        featureRow = selectedDataTable[i];
                        // work with retrieved records
                        SchoolModel school = new SchoolModel();
                        //set values
                        school.SchoolName = featureRow["NAME"].ToString().Trim();
                        school.StCtyFips = featureRow["STCTYFIPS"].ToString().Trim();
                        if (featureRow["LABEL_FLAG"] != DBNull.Value)
                            school.LabelFlag = Convert.ToInt32(featureRow["LABEL_FLAG"]);
                        //set spatial geometry
                        Geometry m_geometry = featureRow.Geometry;
                        ESRI.ArcGIS.Mobile.Geometries.Multipoint mapPoint = m_geometry as ESRI.ArcGIS.Mobile.Geometries.Multipoint;
                        school.SchoolGeometry = m_geometry;
                        //add to graphic
                        glSelected.GeometryBag.Add(m_geometry);
                        //add to list
                        schools.Add(school);
                    }
                    SchoolList = schools;
                }
                selectedDataTable.Dispose();
            }
            catch (Exception e)
            {
                System.Windows.MessageBox.Show(e.Message, "Error in SignSupport Query", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error);
                //throw;
            }
        }
        private FeatureLayer getFeatureLayer()
        {
            //get access to map layer 
            FeatureLayer flayer = MobileCache.Layers[FeatureLayerName] as FeatureLayer;
            return flayer;
        }
        #endregion

The SpatialQuerySchools method will accept the selection envelope. The LoadSchoolResults gets a reference to our selected layer and sets up the graphic layer that will show the highlight features on map. It then runs the query and loops through results to first add them to a new instance of a SchoolModel which are then added to the ObservableCollection SchoolList as well as to the graphic layer.

Switching back to MainWindow.xaml, add two buttons like so for starting and clearing map selection:

<Button Content="Select Schools" Height="24" HorizontalAlignment="Left" Margin="10,52,0,0" Name="btnSelect" Opacity="0.6" VerticalAlignment="Top" Width="100" Command="{Binding SelectingSchoolsCommand}"/>
        <Button Content="Clear Selection" Height="24" HorizontalAlignment="Left" Margin="10,82,0,0" Name="btnClear" Opacity="0.6" VerticalAlignment="Top" Width="100" />

Notice in btnSelect instead of using an event (like Click or MouseDown) we are using a bound Command. This is another cool feature of WPF that sets the MainWindowViewModel to handle the event. Now let’s switch to MainWindowViewModel and set up our SelectingShoolsCommand with the following private class create inside the MainWindowViewModel:

private class SelectSchoolsCommand : ICommand
        {
            #region ICommand Members
            readonly MainWindowViewModel _mv;
            public SelectSchoolsCommand(MainWindowViewModel mv)
            {
                _mv = mv;
            }
            public bool CanExecute(object paramenter)
            {
                return true;
            }
            public event EventHandler CanExecuteChanged;
            public void Execute(object paramenter)
            {
                _mv.setupSelectTool();
            }
            #endregion
        }

Notice how this class is initiated with a reference to the instance of MainWindowViewModel. Next, add in this code snippet to create instance of this command that is exposed and hence bindable:

#region commands
        private ICommand _selectingSchoolsCommand;
        public ICommand SelectingSchoolsCommand
        {
            get
            {
                if (_selectingSchoolsCommand == null)
                    _selectingSchoolsCommand = new SelectSchoolsCommand(this);
                return _selectingSchoolsCommand;
            }
        }
        #endregion

Before I show the code for the setupSelectTool, let’s move IsDesignMode and getAppPath from MainWindow.xaml.cs to MainWindowViewModel because we will be making calls to it from our ViewModel. You will have to update any references to getAppPath in MainWindow.xaml.cs to _mainWindowVM.getAppPath() or you will get compile errors. Now we can move on by adding following code snippet to MainWindowViewModel:

private void setupSelectTool()
        {                        
            if (selectionLayer != null)
                Map.MapGraphicLayers.Remove(selectionLayer);
            //setup selection layer
            selectionLayer = new GraphicLayer();
            selectionLayer.DrawVertexes = false;
            Map.MapGraphicLayers.Add(selectionLayer);
            selectionLayer.GeometryBag.Add(new ESRI.ArcGIS.Mobile.Geometries.Polygon());
            selectionLayer.SelectedGeometry = selectionLayer.GeometryBag[0];
            m_lastNavigatioMode = Map.CurrentNavigationMode;
            Map.CurrentNavigationMode = ESRI.ArcGIS.Mobile.WPF.NavigationMode.None;
            envelopeSketchTool = new EnvelopeSketchTool();
            envelopeSketchTool.AttachToSketchGraphicLayer(selectionLayer);
            envelopeSketchTool.SketchCompleted += doneSketch;
        }
        private void doneSketch(object sender, EventArgs e)
        {
            Envelope env = envelopeSketchTool.Envelope;
            if (env.XMax == env.XMin && env.YMax == env.YMax)
            {
                env.XMax += 25;
                env.XMin -= 25;
                env.YMax += 25;
                env.YMin -= 25;
            }
            //leave on until collapse or pan turn on
            if (envelopeSketchTool != null)
            {
                Map.CurrentNavigationMode = ESRI.ArcGIS.Mobile.WPF.NavigationMode.Pan;
                if (envelopeSketchTool != null)
                    envelopeSketchTool.DetachFromSketchGraphicLayer();
            }
            //check if need to setup ResultsViewModel
            if (MyResultsViewModel.Map == null)
                MyResultsViewModel.Map = _map;
            if (MyResultsViewModel.MobileCache == null)
                MyResultsViewModel.MobileCache = MobileCache;
            if (MyResultsViewModel.FeatureLayerName == null || MyResultsViewModel.FeatureLayerName.Length < 1)
            {
                string xmlPath = getAppPath() + "Settings.xml";
                XmlDocument xmlDoc = new XmlDocument();
                xmlDoc.Load(xmlPath);
                XmlNodeList elemList;
                elemList = xmlDoc.GetElementsByTagName("selectLayer");
                MyResultsViewModel.FeatureLayerName = elemList[0].InnerXml;
            }
            MyResultsViewModel.SpatialQuerySchools(env);
        }

In MainWindow.xaml.cs Windows_Loaded, we set the Map and MobileCache properties so in setupSelectTool and doneSketch you see we are making changes to the map that will be reflected when you interact with the application. setupSelectTool sets up the graphic that will handle the select draw rectangle affect on the map. It also sets the SketchCompleted event to doneSketch. doneSketch first checks if this a single click selection and makes a default envelope. It then checks if ResultsViewModel map, mobilecache and FeatureLayerName have been set- if not- sets them. FeatureLayerName is the value we set in the selectedLayer tag in Settings.xml. The final thing is it calls the SpatialQuerySchools method from the instance of ResultsViewModel.

Ok, I know your head must be spinning with all this code. Go ahead and compile, but do not run- you just want to see if you have any errors. The last thing we need to add in is how the ResultsViewControl opens and closes.  And I am going to show you through using Storyboards and Event Triggers in MainWindow.xaml. Add the following between the Window header tag and your Grid tag in xaml:

<Window.Resources>
        <Storyboard x:Key="OpenResults">
            <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="resultsView">
                <DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Visible}"/>
            </ObjectAnimationUsingKeyFrames>
        </Storyboard>
        <Storyboard x:Key="CloseResults">
            <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="resultsView">
                <DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Hidden}"/>
            </ObjectAnimationUsingKeyFrames>
        </Storyboard>
    </Window.Resources>
    <Window.Triggers>
        <EventTrigger RoutedEvent="ButtonBase.Click" SourceName="btnSelect">
            <BeginStoryboard x:Name="OpenResults_BeginStoryboard1" Storyboard="{StaticResource OpenResults}"/>
        </EventTrigger>
        <EventTrigger RoutedEvent="ButtonBase.Click" SourceName="btnClear">
            <BeginStoryboard x:Name="CloseResults_BeginStoryboard" Storyboard="{StaticResource CloseResults}"/>
        </EventTrigger>
    </Window.Triggers>

If we look at the Storyboards, OpenResults and CloseResults just set the visibility value on our instance of ResultsControlView we named resultsView. The triggers wait for a click event and depending on from which button (btnSelect or btnClear) it will tell which Storyboard to run. This demonstrates how you can control simple UI/View behavior right in xaml and not in xaml.cs. And this is a very simple example. You can have very elaborate Storyboards or use style triggers with multi data triggers.

Now go ahead and compile and run. Play with the select tools and notice how bindings trickle down to populating the datagrid with your selection.

Congratulations- you survived your first MVVM project. I throw a lot of new terms/concepts out there. Googling on any one of them will return a wealth of information for you to explore further. If you are really feeling ambitious, go back and refractor the rest of the code into MVVM. Happy coding.

Esri Mobile 10 SDK and WPF Beginner’s Tutorial

WPF stands for Windows Presentation Framework and we are going to see how quick it is to utilize Esri’s Mobile 10 SDK’s new ability to work in WPF. In this blog I am not going to go into depth about WPF except to say it is Microsoft’s (MS) equivalent to Adobe AIR. Say goodbye to WinForms and welcome to a whole range of slick animations, affects and all that other jazzy stuff you now expect from any Silverlight or Flex web applications. But with this blog we are just going to focus on the nuts and bolts of quickly standing up a mobile application designed for a tablet that runs a Windows operating system. Plus I will point out some gotchas I stumbled across along the way.

Prerequisites:

  • MS Visual Studio 2008 sp1 or higher (I personally prefer 2010- more built in features I make use of)
  • Esri Mobile 10 SDK (make sure and run the uninstaller application first to identify any conflicts with prior installed versions of ArcGIS products)
  • ArcGIS 10 (optional- but need if you want to use geoprocessing tools to create mobile caches)
  • Created mobile caches
  • Running mobile map service (unless you want to skip the sync part)

Map Tutorial Application

Once the prerequisites are installed, open Visual Studio and create a new project.  In the new project dialog, select the WPF Windows Application template. For this tutorial, I chose to go down the path of C# (sorry you VB.NET programmers- but just Google a translator. None of the code snippets will be too difficult to translate). Name your new project WpfMobileTutorial (or whatever you want) and click OK. When your new project solution opens you will see two files- App.xaml and MainWindow.xaml. Expanding them and you see their corresponding cs files. For this tutorial we will ignore the App.xaml file. The only thing I will point out is in App.xaml you can set what xaml file in your project is the start up window-  StartupUri=”MainWindow.xaml”

Now let’s pull in our Mobile SDK. Right click References folder and select to Add References. Add ESRI.ArcGIS.Mobile. If you had used Mobile 9.3.1 SDK or earlier you are probably used to going to the Toolbox window and seeing your Esri controls like map. There is a bug in the current release of this SDK and you will not see them in Toolbox but don’t despair- there are there. We just need to add them the old fashion way before we had fancy dandy IDEs that did all that work for us. First add the reference to the SDK to the Window header tag like so:

<Window x:Class="WpfMobileTuturial.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:esrimap="clr-namespace:ESRI.ArcGIS.Mobile.WPF;assembly=ESRI.ArcGIS.Mobile"  
        Title="MainWindow" Height="450" Width="650">

Add the following line inside your Grid container.

<Grid>
<esrimap:Map x:Name="map1"  />
</Grid>

You could compile and launch but nothing fun will happen- we need to add the cache to the map. Now, I have done Flex development and I love the config files they use so things can be changed without recompiling. So, I mimic that by adding an xml file to my project that I call Settings.xml. You can setup how you want but this is the format I typically use to store my cache information:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <cacheLocations>
    <operationalCacheLocation>C:\Caches\OperationalCache\MobileCache</operationalCacheLocation>
    <baseMapCacheLocation>C:\Caches\BaseMapCache\Layers</baseMapCacheLocation>
  </cacheLocations>
  <mobileServiceInfo>
    <mobileServiceURL>http://serverTutorial/ArcGIS/services/Panhandle/tutorial/MapServer/MobileServer</mobileServiceURL>    
  </mobileServiceInfo>
</configuration>

For now let’s focus on what is between the cacheLocations tags. Most mobile applications will have separate caches. One for the stuff they plan to edit and sync- operationCacheLocation tag. And the base stuff used for reference and not typically synced- baseMapCacheLocation tag.  OK- back in MainWindow.xaml add the following event to the Window header tag Loaded=”Window_Loaded”. Switch over to MainWindow.xaml.cs and add the following code:

MobileCache mobileCache;
        private void Window_Loaded(object sender, RoutedEventArgs e)
        {            
                       string xmlPath = getAppPath() + "Settings.xml";
            XmlDocument xmlDoc = new XmlDocument();
            xmlDoc.Load(xmlPath);
            XmlNodeList elemList;
        }
        public static bool IsDesignMode()
        {
            return (System.Diagnostics.Debugger.IsAttached);
        }
        public static string getAppPath()//make sure return with trailing \
        {
            bool check = IsDesignMode();
            if (check)
            {
                return @"C:\Projects\WpfMobileTuturial\WpfMobileTuturial\";
            }
            else
            {
                return System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location) + @"\";
            }
        }
      private MobileCache getMobileCache(String cachePath)
        {
            MobileCache m_mobileCache = new MobileCache();
            m_mobileCache.StoragePath = cachePath;
            m_mobileCache.Open();
            return m_mobileCache;
        }

Let’s go over what is going on here. First, if you had done mobile development prior to 10 MobileServices was replaced with MobileCache. But be warned- it is not an exact switch out between them. So, I would try and forgot how MobileServices worked in 9.3.1 and treat MobileCache as something new you are learning. The helper methods getAppPath() and IsDesignMode() just do some of the leg work of finding where Settings.xml is- if debugging I did go ahead and hardcode the path but if it is installed I just pull out it of Assembly.  I will discuss getMobilCache later. Let’s now add the code to our Window_Loaded method for adding the caches after the line XmlNodeList elemList;

elemList = xmlDoc.GetElementsByTagName("baseMapCacheLocation");
            string streetCache = elemList[0].InnerXml;
            if (streetCache != null && streetCache.Length > 0)
            {
                //rasterdata- local Tile Map Cache
                TileCacheMapLayer mpLayer = new TileCacheMapLayer(streetCache);
                mpLayer.Name = "Base Tile Cache";
                mpLayer.Open();
                map1.MapLayers.Add(mpLayer);
            }
            elemList = xmlDoc.GetElementsByTagName("operationalCacheLocation");
            string localCache = elemList[0].InnerXml;
            if (localCache != null && localCache.Length > 0)
            {
                mobileCache = getMobileCache(localCache);
                //update the cache
                elemList = xmlDoc.GetElementsByTagName("mobileServiceURL");
                string mapurl = elemList[0].InnerXml;
                int index = 0;
                //map1.MapLayers.AddRange(mobileCache);
                //this method AddRange doesn't always behave as expected so here is my work around- make a renderable mobilcachemaplayer for each layer
                //and then add to WPF map
                ReadOnlyLayerCollection clc = mobileCache.Layers;
                int iCnt = mobileCache.Layers.Count;
                for (int i = 0; i < iCnt; i++)
                {
                    Layer layer = mobileCache.Layers[i];
                    MobileCacheMapLayer mobileCacheMapLayer = new MobileCacheMapLayer((MobileCacheLayer)layer);
                    map1.MapLayers.Insert(index, mobileCacheMapLayer);
                    index++;
                }               
            }

So, all this code snippet does is first get the value from our tag baseMapCacheLocation and uses one of the new features of 10;using Tile Caches created from a Tile Map Service. You just need to copy _alllayers folder, conf.cdi and conf.xml locally and just have your Settings.xml store where they  are saved locally.

The next part adds the operational layers to the MobileCache. And now you see us utilize the getMobileCache helper method. Just pass in the path location grabbed out of the Settings.xml and all this method does is initialize the mobile cache. Next we add these layers to the map. Here you will notice some comments in the code. I have had very inconsistent luck with using the map’s method AddRange. To be safe I always do the long way of casting the layers into MobileCacheMapLayer and add that to the map. Feel free to do whichever.

At this point you can compile and run and see a new window open showing your cache data..

Cool. Now let’s add some navigation. In MainWindow, drag three button controls onto your window and position where you would like them. Here you could get real fancy and add in some wicked style/animation affects. What I will do is just change text to Pan, Zoom In, and Zoom Out respectfully. I know, lame, but you can stop here and play with styling and if you have Expression Blend it is even easier. Here is a cool one to try- http://www.codeproject.com/KB/WPF/glassbuttons.aspx.

But back to the boring stuff let’s add the tiny bit of code snippets for adding in basic navigation:

private void btnPan_Click(object sender, RoutedEventArgs e)
        {
            map1.CurrentNavigationMode = ESRI.ArcGIS.Mobile.WPF.NavigationMode.Pan;
        }
        private void btnZoomIn_Click(object sender, RoutedEventArgs e)
        {
            map1.CurrentNavigationMode = ESRI.ArcGIS.Mobile.WPF.NavigationMode.ZoomIn;
        }
        private void btnZoomOut_Click(object sender, RoutedEventArgs e)
        {
            map1.CurrentNavigationMode = ESRI.ArcGIS.Mobile.WPF.NavigationMode.ZoomOut;
        }

And hooking our WPF button controls to our methods add Click events to each of your buttons and point to corresponding method (e.g. Click=”btnPan_Click”). Now compile and run and you can navigate around your map by panning and zooming in and out.

Since this is a mobile application the last thing I will cover here is synchronization- especially since the process changed in 10. Let’s add a fourth button to our application. Again, I am not going to utilize any wicked cool affects and just change the text to ‘Sync’.  And put the following method in MainWindow.xaml.cs:

private void btnSync_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                string xmlPath = getAppPath() + "Settings.xml";

                XmlDocument xmlDoc = new XmlDocument();
                xmlDoc.Load(xmlPath);

                XmlNodeList elemList;

                elemList = xmlDoc.GetElementsByTagName("mobileServiceURL");
                string url = elemList[0].InnerXml;

                MobileServiceConnection mobileServiceConnection1 = new MobileServiceConnection();
                //link the mobile classes to put the data into the display 

                mobileServiceConnection1.Url = url;

                ReadOnlyLayerCollection clc = mobileCache.Layers;
                int iCnt = mobileCache.Layers.Count;
                for (int i = 0; i < iCnt; i++)
                {
                    //synch just be feature test
                    FeatureLayer featureLayer = mobileCache.Layers[i] as FeatureLayer;
                    FeatureLayerSyncAgent featLayerSync = new FeatureLayerSyncAgent(featureLayer, mobileServiceConnection1);
                    FeatureLayerSyncResults featLayerResults = new FeatureLayerSyncResults();
                    featLayerResults = featLayerSync.Synchronize() as FeatureLayerSyncResults;
                    IDictionary<int, string> synErrors = featLayerResults.UploadedFeaturesErrors;
                    if (synErrors.Count > 0)
                    {
                        foreach (KeyValuePair<int, string> kvp in synErrors)
                        {
                            int v1 = kvp.Key;
                            string v2 = kvp.Value;
                            System.Windows.MessageBox.Show("Error uploading " + featureLayer.Name + ". Contact administrator. " + "Key: " + v1.ToString() + " Value: " + v2);
                        }
                    }
                    if (featLayerResults.Exception != null)
                    {
                        Exception e = featLayerResults.Exception;
                        if (e.Message != null && e.Message.Length > 0)
                        {
                            System.Windows.MessageBox.Show("Error uploading " + featureLayer.Name + ". Contact administrator. " + "Message: " + e.Message);
                        }
                    }
                }
                return;
            }
            catch (Exception e)
            {
                System.Windows.MessageBox.Show(e.Message, "Error in OpenCacheConnection", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error);
                return;
            }

Looking at the code you now see the mobileServiceURL tag come into play. The URL to the mobile map service I also like to put in the Settings.xml. This, makes moving things to another environment much easier. Once we pull out the URL we assign to MobileServiceConnection. Now there was something I noticed about the sync process that annoyed me- it will run and not return any errors automatically. You actually have to dive into either FeatureLayerSyncResults or MobileCacheResults. In the above code snippet I actually show how to loop through cache and sync each layer. Then pull out if there were any errors in UploadedFeaturesErrors or in Exception. Makes debugging sync issues so much easier. So, go ahead and compile again and if you have your mobile service set up go ahead and sync. If there are any errors you will get an alert stating the problem.

So, that ends the tutorial for getting up and running with Mobile 10 and WPF. I will be following this with a blog extending this tutorial to start incorporating aspects of the design framework MVVM and utilizing WPF binding capabilities. Also, if you would like to see how cool WPF can look check out our project showcase (http://showcase.gisinc.com/) and look for the Panhandle Energy Pilot Patrol Application or City of Appleton Sign Retro-reflectivity Solution projects.

Follow

Get every new post delivered to your Inbox.

Join 31 other followers