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.
No comments:
Post a Comment