Visually Located

XAML and GIS

Enabling tilt on “non-selectable” items

While working an a recent app I wanted to enable tilt functionality onto a Grid that contained a few items. Unfortunetly I was unable to get the control to tilt. I tried setting all sorts of properties, but nothing worked. I downloaded the source and took a look at it, and the same time I asked the twiiter universe whether non selectable items could be “tiltable” Morten Neilson replied with exactly what I was seeing.  His suggestion was to change the source which is easy enough to do. Just add FrameworkElement to the list.

static TiltEffect()
 {
     // The tiltable items list.
     TiltableItems = new List<Type>() 
     { 
         typeof(ButtonBase), 
         typeof(ListBoxItem), 
         typeof(MenuItem),
     };
 }

But I wanted to find a way to make this work without having to change the source for the WP7 toolkit. I love open source projects, I have two myself. But it’s a lot nicer to use the out of the box code rather than having to maintain your own.

My thought was if the toolkit limits to those items, what if I wrapped my Grid in one of them? I didn’t want to use a button because then I’d have to style it, so I tried a ListBoxItem.

<ListBoxItem toolkit:TiltEffect.IsTiltEnabled="True">
    <Grid>
        <TextBlock Text="I'm now tiltable!"/>
    </Grid>
</ListBoxItem>

Of course there is no need for the Grid in the above, but you get the point. This is being used outside of a ListBox and works fantastic!

Using extension methods to iterate ArcGIS COM collections

I’m a big fan of .Net and all the glory that it offers. So when I’m developing against ArcGIS COM objects I cry a little inside. Working with COM objects in .Net is not fun. C# 4.0 made this a little better by improving the code generated by interop.

Example code to get an indexed property within .Net from a COM object with VS2008

int fieldIndex = 2;
IRow row = cursor.NextRow();
object value = row.get_Value(fieldIndex);

Example code to get an indexed property within .Net from a COM object with VS2010

int fieldIndex = 2;
IRow row = cursor.NextRow();
object value = row.Value[fieldIndex];

Notice the difference? It’s in the last line where we get the value for the field. Not only do you not need the get_XXX prefix, but you use square brackets. It’s actually an indexed property now! Small things like this make working with ArcObjects a little better, but there is still a lot more that can be done. One area that always just plain sucks is iterating over COM collections. Here is a simple example of iterating over an ICursor

ICursor cursor = table.Search(null, true);
IRow row = cursor.NextRow();
while(row != null)
{
    // Do some stuff
    row = cursor.NextRow();
}

This is one of the easier COM collections to work with. This one at least does not have a Reset() method. Without a doubt, you are bound to forget that setter at the end of the while loop and your be looking at the same row for a very long time. This is very error prone.

Luckily we can add new functionality to any object by using extension methods. I hope you’re familiar with extension methods. I’m always surprised when I find active developers that have never heard of them. By creating an extension method, I can change the code above to:

ICursor cursor = table.Search(null, true);
foreach(var row in cursor.ToEnumerable())
{
    // do some stuff
}

Ahhh… So much better! No need to worry about setting the row object again at the end of the while loop. I can work with this a lot easier than before, and it just looks a ton better!

Let’s take a look at what it would take to write this extension method.

public static class Extensions
{
    public static IEnumerable<IRow> ToEnumerable(this ICursor source)
    {
        if (source != null)
        {
            IRow row = source.NextRow();
            while (row != null)
            {
                yield return row;
                row = source.NextRow();
            }
        }
    }
}

I have this code written once and I can now forget all about it! From now on it’s .Net goodness baby!

But wait! There’s more! I went ahead an created a NuGet package that contains many of these extension methods!

PM> Install-Package ArcGISExtensions

This package contains 15 ToEnumerable methods to help out with iterating over the ArcGIS COM collections. I picked the ones that are used the most. I plan to add some more Enumerable extensions as well as others to help out with your ArcGIS development.

Enjoy!

Basic styling of the Silverlight 5 PivotViewer

Normally when people ask me how to create a custom style for a control in Silverlight or WPF I quickly reply

Use Expression Blend!

If they do not have Expression Blend, then the next answer is

Get the default style from MSDN and change as needed.

So, when someone asked how to go about styling the new PivotViewer control, my immediate response was going to be “Use Expression Blend”. But, we’re in a odd place right now. There is now RTM of Express Blend. There is only the Expression Blend Preview for Silverlight 5. As this is not a RTM release, and PivotViewer is new, I thought I would test it out first. So, open a Silverlight 5 project in Blend Preview and add the PivotViewer Control. Right click on the PivotViewer –> Edit Template –> Edit a Copy…

image

To my surprise, the following xaml was generated

<UserControl.Resources>
    <ControlTemplate x:Key="PivotViewerControlTemplate1" TargetType="sdk:PivotViewer"/>
</UserControl.Resources>

WHAT?!?!? An empty template! I must have done that wrong. I must have done PivotViewer –> Edit Template –> Create empty… Yup, that must be it! Try again, and again an empty template. Blend, you have failed me! How could you? I used to think that Blend was the all powerful Oz when you need to style a control. But it failed!

OK, I’ll just go to step two. Microsoft is always great when it comes to documentation. Go to MSDN, and… No templates?!?! Ok, Silverlight 5 had the longest Beta program! Was it a year? I don’t recall when the first beta came out, maybe around MIX? Yes I think that’s it. Granted, the PivotViewer control was not released with the first Beta, but it was with the second. So this is one time when the team dropped the ball when it comes to the docs.

So, nothing from the all powerful Blend, nothing from MSDN, now what? Like any .Net developer I keep a copy of Reflector close by. Open up the PivotViewer assembly and check out the resources. Sure enough, there it is! If you’ve played with PivotViewer before, you know that this control is not just a simple control. This thing is a beast! It is an entire application disguised as a control. Because this thing is so huge, the styles used are contained in a 6000+ line xaml file! Maybe this is why it is not on MSDN, but the basic style of the actual PivotViewer control is only 170 lines of xaml. That’s nothing! Here is the default style for PivotViewer

<!-- Default style for the PivotViewer -->
<Style TargetType="sdk:PivotViewer">
    <Style.Setters>
        <Setter Property="Foreground" Value="{StaticResource ForegroundBrush}" />
        <Setter Property="Background" Value="{StaticResource BackgroundBrush}" />
        <Setter Property="BorderBrush" Value="{StaticResource BorderBrush}" />
        <Setter Property="AccentColor" Value="{StaticResource AccentColor}" />
        <Setter Property="SecondaryBackground" Value="{StaticResource SecondaryBackgroundBrush}" />
        <Setter Property="ControlBackground" Value="{StaticResource ControlBackgroundBrush}" />
        <Setter Property="SecondaryForeground" Value="{StaticResource SecondaryForegroundBrush}" />
        <Setter Property="PrimaryItemValueBackgroundColor" Value="{StaticResource PrimaryItemValueBackgroundColor}" />
        <Setter Property="SecondaryItemValueBackgroundColor" Value="{StaticResource SecondaryItemValueBackgroundColor}" />
        <Setter Property="IsTabStop" Value="False" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="sdk:PivotViewer">
                    <Grid x:Name="PART_Container" Background="{TemplateBinding Background}">
                        <Grid.RowDefinitions>
                            <RowDefinition Height="30"/>
                            <RowDefinition Height="*"/>
                        </Grid.RowDefinitions>
 
                        <!-- PivotViewer border -->
                        <Rectangle Grid.RowSpan="2" Stroke="{TemplateBinding BorderBrush}"/>
 
                        <!-- ControlBar -->
                        <Grid Grid.Row="0" x:Name="PART_ControlBar">
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="*"/>
                                <ColumnDefinition Width="Auto"/>
                                <ColumnDefinition Width="Auto"/>
                                <ColumnDefinition Width="Auto"/>
                                <ColumnDefinition Width="Auto"/>
                            </Grid.ColumnDefinitions>
 
                            <!-- ControlBar background -->
                            <Rectangle Grid.ColumnSpan="5" Fill="{TemplateBinding Background}" Stroke="{StaticResource BorderBrush}"/>
 
                            <!-- Container for AppliedFiltersView -->
                            <ItemsControl x:Name="PART_AppliedFiltersBreadcrumb" 
                                  Grid.Column="0" Margin="5,0,5,0"
                                  Style="{StaticResource AppliedFiltersBreadcrumbStyle}">
                                <ToolTipService.ToolTip>
                                    <ToolTip x:Name="PART_AppliedFiltersToolTip" ContentTemplate="{StaticResource AppliedFiltersToolTipTemplate}" />
                                </ToolTipService.ToolTip>
                            </ItemsControl>
 
                            <!-- Container for SortPropertySelector -->
                            <ComboBox x:Name="PART_SortPropertySelector" Style="{StaticResource SortCategoryDropdownStyle}"
                                      AutomationProperties.Name="SortPropertySelector"
                                      Grid.Column="1" VerticalAlignment="Center" 
                                      Height="22"
                                      Margin="0,0,5,0" />
 
                            <!--Container for ViewSelectionView -->
                            <ListBox x:Name="PART_ViewSelector" Style="{StaticResource ViewSelectorStyle}"
                                     Grid.Column="2" VerticalAlignment="Center" 
                                     Width="60" Height="22"
                                     Margin="0,0,5,0" />
                            
                            <!-- Container for ZoomSliderView -->
                            <Grid x:Name="PART_ZoomSliderView" 
                                  Grid.Column="3" VerticalAlignment="Center" Margin="0,0,5,0"
                                  Width="146" Height="22">
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="18"/>
                                    <ColumnDefinition />
                                    <ColumnDefinition Width="18"/>
                                </Grid.ColumnDefinitions>
 
                                <Rectangle Stroke="{TemplateBinding BorderBrush}" Margin="0" Grid.ColumnSpan="3" Fill="{TemplateBinding Background}"/>
                                <Button x:Name="PART_ZoomOutButton" Grid.Column="0"
                                        Style="{StaticResource Style_Button_noBorder}">
                                    <Path x:Name="path" Stretch="Fill" HorizontalAlignment="Center" VerticalAlignment="Center" Width="8" Height="2.5" Data="M16,6 L16,10 0,10 0,6 z" Fill="{Binding (con:FrameworkElementExtensions.Brushes)[ControlForeground], RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=sdk:PivotViewer}}" Margin="0" />
                                </Button>
                                <con:ClickDragSlider x:Name="PART_ZoomSlider"
                                                     Width="100"
                                                     SmallChange="0"
                                                     Grid.Column="1" 
                                                     Style="{StaticResource Style_ZoomSlider}" 
                                                     BorderBrush="{Binding ControlBackground, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=sdk:PivotViewer}}" />
                                <Button x:Name="PART_ZoomInButton" Grid.Column="2"
                                        Style="{StaticResource Style_Button_noBorder}">
                                    <Path Stretch="Fill" HorizontalAlignment="Center" VerticalAlignment="Center" Width="8" Height="8" Data="M6,0 L10,0 10,6 16,6 16,10 10,10 10,16 6,16 6,10 0,10 0,6 6,6 z" Fill="{Binding (con:FrameworkElementExtensions.Brushes)[ControlForeground], RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=sdk:PivotViewer}}" Margin="0" />
                                </Button>
                            </Grid>
                        </Grid>
 
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
        <Setter Property="ItemAdornerStyle">
            <Setter.Value>
                <Style TargetType="sdk:PivotViewerItemAdorner">
                    <Setter Property="Template">
                        <Setter.Value>
                            <ControlTemplate TargetType="sdk:PivotViewerItemAdorner">
                                <Grid Background="Transparent">
                                    <VisualStateManager.VisualStateGroups>
                                        <VisualStateGroup x:Name="CommonStates">
                                            <VisualState x:Name="Normal"/>
                                            <VisualState x:Name="MouseOver">
                                                <Storyboard>
                                                    <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="ButtonBorder">
                                                        <EasingDoubleKeyFrame KeyTime="0" Value="1"/>
                                                    </DoubleAnimationUsingKeyFrames>
                                                </Storyboard>
                                            </VisualState>
                                            <VisualState x:Name="Disabled"/>
                                        </VisualStateGroup>
                                        <VisualStateGroup x:Name="ItemStates">
                                            <VisualState x:Name="Default"/>
                                            <VisualState x:Name="IsSelected">
                                                <Storyboard>
                                                    <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="FocusedItemBorder">
                                                        <EasingDoubleKeyFrame KeyTime="0" Value="1"/>
                                                    </DoubleAnimationUsingKeyFrames>
                                                </Storyboard>
                                            </VisualState>
                                        </VisualStateGroup>
                                    </VisualStateManager.VisualStateGroups>
 
                                    <Border x:Name="ButtonBorder"
                                            Margin="-1"
                                            BorderThickness="4"
                                            BorderBrush="{Binding (con:FrameworkElementExtensions.Brushes)[ControlHoverBackground], RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=sdk:PivotViewer}}"
                                            Opacity="1"/>
                                    <Border x:Name="FocusedItemBorder"
                                            Margin="-1"
                                            BorderThickness="4"
                                            BorderBrush="{Binding (con:FrameworkElementExtensions.Brushes)[FocusBorderBrush], RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=sdk:PivotViewer}}"
                                            Opacity="1"/>
                                </Grid>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </Style>
            </Setter.Value>
        </Setter>
        <Setter Property="DetailPaneStyle">
            <Setter.Value>
                <Style TargetType="sdk:PivotViewerDetailPane">
                    <Setter Property="Foreground" Value="{Binding Foreground, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=sdk:PivotViewer}}" />
                    <Setter Property="Background" Value="{Binding Background, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=sdk:PivotViewer}}" />
                    <Setter Property="BorderBrush" Value="{Binding BorderBrush, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=sdk:PivotViewer}}" />
                    <Setter Property="AccentColor" Value="{Binding AccentColor, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=sdk:PivotViewer}}" />
                    <Setter Property="Background" Value="{Binding SecondaryBackground, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=sdk:PivotViewer}}" />
                    <Setter Property="ControlBackground" Value="{Binding ControlBackground, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=sdk:PivotViewer}}" />
                    <Setter Property="SecondaryForeground" Value="{Binding SecondaryForeground, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=sdk:PivotViewer}}" />
                    <Setter Property="PrimaryItemValueBackgroundColor" Value="{Binding PrimaryItemValueBackgroundColor, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=sdk:PivotViewer}}" />
                    <Setter Property="SecondaryItemValueBackgroundColor" Value="{Binding SecondaryItemValueBackgroundColor, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=sdk:PivotViewer}}" />
                </Style>
            </Setter.Value>
        </Setter>
        <Setter Property="FilterPaneStyle">
            <Setter.Value>
                <Style TargetType="sdk:PivotViewerFilterPane">
                    <Setter Property="Foreground" Value="{Binding Foreground, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=sdk:PivotViewer}}" />
                    <Setter Property="Background" Value="{Binding Background, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=sdk:PivotViewer}}" />
                    <Setter Property="BorderBrush" Value="{Binding BorderBrush, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=sdk:PivotViewer}}" />
                    <Setter Property="AccentColor" Value="{Binding AccentColor, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=sdk:PivotViewer}}" />
                    <Setter Property="SecondaryBackground" Value="{Binding SecondaryBackground, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=sdk:PivotViewer}}" />
                    <Setter Property="ControlBackground" Value="{Binding ControlBackground, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=sdk:PivotViewer}}" />
                    <Setter Property="ClearButtonBackground" Value="{StaticResource ClearButtonCircleBackground}" />
                    <Setter Property="SecondaryForeground" Value="{Binding SecondaryForeground, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=sdk:PivotViewer}}" />
                </Style>
            </Setter.Value>
        </Setter>
    </Style.Setters>
</Style>

As you can see, the default style is nothing much. It’s mainly just the title bar. The DetailPaneStyle helps apply some color based style like Background, Foreground, etc. to the panel that appears when you drill into a trading card. This panel shows up on the right. The FilterPaneStyle does the same but for the panel on the left that shows the different properties that you can on. Being able to set the color is nice, but come on I want to do more. Surely I can use the ability to set this styles in Expression Blend with the Edit Additional Styles feature. Oh, that’s disabled with the right click menu, but it’s not disabled in the Object menu! Oh, that doesn’t work at all. Crap! Blend you suck! I want a divorce!

Luckily the styles for these two controls are also in that 6000+ line file. So, if you do want to style the left pane (also called the Facets) or the right details pane, you have everything needed.

If you are looking to get down and dirty with the Facets, you’ll want to pay attention to the Accordion styles. The PivotViewer uses a CustomAccordion control, and it uses a lot of styles that are supplied in the file. You’ll want to alter the AccordionItemStyle, FacetCategoryHeaderTemplate, and the FacotCategoryTemplate styles. 

As it’s currently impossible to find these styles, I’ve supplied them here. This file comes complete with all of the comments from the team that developed it! WooHoo! I also went ahead and fixed some of the styling. For some reason this team REALLY loved ancestor binding and used it everywhere. They even used it where TemplateBinding should be used.

Managing Extents within the ArcGIS WPF/Silverlight/etc. APIs

Managing extents within the ArcGIS .Net client API’s is pretty simple. Esri has an example on the resources page. I implemented one for the ArcFM Silverlight SDK sample. Oddly enough, they are quite similar. I don’t remember copying theirs, but you never know. Like most things we as developers do, after a couple of years you look back and wonder “What was I thinking?” I look back at my original code and wonder why did I implement the Extents class the way that I did. I originally used one List to hold all of the Extents. This made some of the code pretty ugly. I’ve created a different implementation that utilizes two stacks. I have one class that manages all of the extents:

   1: public class Extents
   2: {
   3:     private readonly Stack<Envelope> _backStack = new Stack<Envelope>();
   4:     private readonly Stack<Envelope> _forwardStack = new Stack<Envelope>();
   5:  
   6:     public bool HasPreviousExtent
   7:     {
   8:         get { return _backStack.Count > 0; }
   9:     }
  10:  
  11:     public bool HasNextExtent
  12:     {
  13:         get { return _forwardStack.Count > 0; }
  14:     }
  15:  
  16:     public Envelope CurrentExtent { get; set; }
  17:  
  18:     public Envelope PreviousExtent
  19:     {
  20:         get
  21:         {
  22:             if (HasPreviousExtent)
  23:             {
  24:                 _forwardStack.Push(CurrentExtent);
  25:                 CurrentExtent = _backStack.Pop();
  26:                 return CurrentExtent;
  27:             }
  28:             return null;
  29:         }
  30:     }
  31:  
  32:     public Envelope NextExtent
  33:     {
  34:         get
  35:         {
  36:             if (HasNextExtent)
  37:             {
  38:                 _backStack.Push(CurrentExtent);
  39:                 CurrentExtent = _forwardStack.Pop();
  40:                 return CurrentExtent;
  41:             }
  42:             return null;
  43:         }
  44:     }
  45:  
  46:     public void Add(Envelope extent)
  47:     {
  48:         if (extent == null) return;
  49:  
  50:         if (CurrentExtent != null)
  51:         {
  52:             _backStack.Push(CurrentExtent);
  53:         }
  54:         CurrentExtent = extent;
  55:  
  56:         // If a new extent is added, then anything in our forward stack needs to be removed
  57:         _forwardStack.Clear();
  58:     }
  59: }

I’m a big fan of MVVM, so I’ll use a ViewModel to access the Extents. I’ll also be using the DelegateCommand from Microsoft.

   1: public class ViewModel
   2: {
   3:     public ViewModel()
   4:     {
   5:         Extents = new Extents();
   6:         PreviousExtent = new DelegateCommand(MovePreviousExtent, extent => Extents.HasPreviousExtent);
   7:         NextExtent = new DelegateCommand(MoveNextExtent, extent => Extents.HasNextExtent);
   8:         IsNewExtent = true;
   9:     }
  10:  
  11:     private Map _map;
  12:  
  13:     public Map Map
  14:     {
  15:         get { return _map; }
  16:         set
  17:         {
  18:             UnsubscribeMapEvents(_map);
  19:             SubscribeMapEvents(value);
  20:             _map = value; 
  21:         }
  22:     }
  23:  
  24:     public DelegateCommand PreviousExtent { get; private set; }
  25:     public DelegateCommand NextExtent { get; private set; }
  26:  
  27:     private bool IsNewExtent { get; set; }
  28:     private Extents Extents { get; set; }
  29:  
  30:     private void MovePreviousExtent(object obj)
  31:     {
  32:         // This extent should not be put onto the Extents stack.
  33:         IsNewExtent = false;
  34:         Map.ZoomTo(Extents.PreviousExtent);
  35:     }
  36:     
  37:     private void MoveNextExtent(object obj)
  38:     {
  39:         // This extent should not be put onto the Extents stack.
  40:         IsNewExtent = false;
  41:         Map.ZoomTo(Extents.NextExtent);
  42:     }
  43:  
  44:     private void UnsubscribeMapEvents(Map map)
  45:     {
  46:         if (map == null) return;
  47:         map.ExtentChanged -= Map_ExtentChanged;
  48:     }
  49:  
  50:     private void SubscribeMapEvents(Map map)
  51:     {
  52:         if (map == null) return;
  53:         map.ExtentChanged += Map_ExtentChanged;
  54:     }
  55:  
  56:     private void Map_ExtentChanged(object sender, ExtentEventArgs e)
  57:     {
  58:         // Only add the extent if it is "new" ie: Not a Previous or Next extent
  59:         if (IsNewExtent)
  60:         {
  61:             Extents.Add(e.NewExtent);
  62:         }
  63:         IsNewExtent = true;
  64:         PreviousExtent.RaiseCanExecuteChanged();
  65:         NextExtent.RaiseCanExecuteChanged();
  66:     }
  67: }

The joy of these classes is that they can be used within all of the .Net Client API’s! This can even be used within the new ArcGIS Runtime (currently in Beta). Using these within our application is now a breeze! Here is the xaml for a WPF Window

   1: <Window x:Class="ExtentNavigationApp.MainWindow"
   2:         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   3:         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
   4:         xmlns:esri="http://schemas.esri.com/arcgis/client/2009"
   5:         Title="MainWindow" Height="350" Width="525">
   6:     <Grid x:Name="LayoutRoot" Background="White">
   7:         <esri:Map x:Name="Map" >
   8:             <esri:ArcGISTiledMapServiceLayer Url="http://server.arcfmsolution.com/ArcGIS/rest/services/Landbase/MapServer"/>
   9:             <esri:ArcGISTiledMapServiceLayer Url="http://server.arcfmsolution.com/ArcGIS/rest/services/Electric/MapServer"/>
  10:         </esri:Map>
  11:         <StackPanel Orientation="Horizontal" VerticalAlignment="Top">
  12:             <Button Content="Previous Extent" Command="{Binding PreviousExtent}" Height="25" Width="100"/>
  13:             <Button Content="Next Extent" Command="{Binding NextExtent}" Height="25" Width="100"/>
  14:         </StackPanel>
  15:     </Grid>
  16: </Window>

And the code behind:

   1: public partial class MainWindow : Window
   2: {
   3:     public MainWindow()
   4:     {
   5:         InitializeComponent();
   6:         DataContext = new ViewModel() { Map = Map };
   7:     }
   8: }

Creating a Custom MessageBox for Windows Phone Applications

UPDATE: See a new post to get the latest code for the custom MessageBox.


UPDATE: After posting this blog I found out about the message box within the XNA framework. This does allow for custom button text which is what I was trying to accomplish. However, the user experience is different than what you get from the message boxes within the native phone applications (eg: deleting a text). With the native message boxes, the application bar disappears, but with the XNA message box, it gets greyed out. It’s the little things that matter. Also within the XNA framework you cannot add additional components to the message box. For example, you might want to add a “Do not show me this again” option within the message box.

While using a Windows Phone you get prompted every once in awhile by a message box. Custom Applications have them, even apps native to the phone has them. When deleting a text message or a contact you get a nice prompt asking you if you want to delete it. But there is something unique about these message boxes that separates them from the one that you and I get to have. The standard message box only allows for an “OK” and a “Cancel” button. The message boxes that are native to the phone have custom text. When you delete a text, you are prompted with buttons “delete” and “cancel”. Seeing as there is not a way to do this, you need to create your own. I’ve created a very simple sample that can be used.

The CustomMessageBox sample is based on the assumption that message boxes are “binary”. What I mean is that you get binary options, Yes/No, OK/Cancel, etc. So I’ve limited what is allowed to be a valid result.

 1: public enum CustomMessageBoxResult
 2: {
 3:     Yes, 
 4:     No,
 5:     // Not using this yet, but you could wire up to the back button of the phone to be a cancel
 6:     Cancel
 7: }

I don’t have the ability to have a Show method return the CustomMessageBoxResult so I’ll need an EventArg that will be used within an event.

 1: public class MessageBoxEventArgs : EventArgs
 2: {
 3:     public CustomMessageBoxResult Result { get; set; }
 4: }

The xaml is pretty straight forward. We need to "”block” the page the CustomMessageBox is for. To do this I made the control a grid so it will fill up everything. Then give it a background with an opacity that will block all clicks.

<Grid x:Class="Visual.Controls.MessageBox"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:toolkit="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone.Controls.Toolkit"             
             Margin="0">
    <Grid.Background>
        <SolidColorBrush Color="{StaticResource PhoneBackgroundColor}" Opacity=".5"/>
    </Grid.Background>
    <Grid x:Name="MessagePanel" Background="{StaticResource PhoneChromeBrush}"
          VerticalAlignment="Top" HorizontalAlignment="Stretch"
          toolkit:TiltEffect.IsTiltEnabled="True">
        <StackPanel Margin="12,12,12,18">
            <TextBlock x:Name="HeaderTextBlock" TextWrapping="Wrap"
                       Style="{StaticResource PhoneTextLargeStyle}"
                       FontFamily="{StaticResource PhoneFontFamilySemiBold}"
                       HorizontalAlignment="Left"/>
            <TextBlock x:Name="MessageTextBlock" TextWrapping="Wrap"
                       Style="{StaticResource PhoneTextNormalStyle}"
                       FontSize="{StaticResource PhoneFontSizeMedium}"
                       Margin="12,24,12,24"
                       HorizontalAlignment="Left"/>
            <Grid HorizontalAlignment="Stretch" Margin="0,6,0,0">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>
                <Button x:Name="YesButton" Click="YesButton_Click"/>
                <Button x:Name="NoButton" Grid.Column="1" Click="NoButton_Click"/>
            </Grid>
        </StackPanel>
    </Grid>
</Grid>

The message box has really one basic function, to ask the user a question. To accomplish that one function, we need to do three things.

  1. The message box must be put into the application. (Show method)
  2. When the user answers the question, the message box needs to be removed from the application. (Remove method)
  3. The message box needs to tell the application what the user picked. (Closed event)
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
 
using Microsoft.Phone.Controls;
 
namespace Visual.Controls
{
    public partial class MessageBox : Grid
    {
        private PhoneApplicationPage _page;
 
        private MessageBox()
        {
            InitializeComponent();
        }
 
        public event EventHandler<MessageBoxEventArgs> Closed;
 
        protected virtual void OnClosed(MessageBoxResult result)
        {
            // need to unsubscribe from the backkeypress
            _page.BackKeyPress -= Page_BackKeyPress;
 
            var handler = this.Closed;
            if (handler != null)
            {
                handler(this, new MessageBoxEventArgs { Result = result });
            }
            Remove();
        }
 
        public static MessageBox Show(string message, string caption, string yesButtonText, string noButtonText = null)
        {
            MessageBox msgBox = new MessageBox();
            msgBox.HeaderTextBlock.Text = caption;
            msgBox.MessageTextBlock.Text = message;
            msgBox.YesButton.Content = yesButtonText;
            if (string.IsNullOrWhiteSpace(noButtonText))
            {
                msgBox.NoButton.Visibility = Visibility.Collapsed;
            }
            else
            {
                msgBox.NoButton.Content = noButtonText;
            }
            msgBox.Insert();
            return msgBox;
        }
 
        private void Insert()
        {
            // Make an assumption that this is within a phone application that is developed "normally"
            var frame = Application.Current.RootVisual as Microsoft.Phone.Controls.PhoneApplicationFrame;
            _page = frame.Content as PhoneApplicationPage;
            _page.BackKeyPress += Page_BackKeyPress;
 
            // assume the child is a Grid, span all of the rows
            var grid = System.Windows.Media.VisualTreeHelper.GetChild(_page, 0) as Grid;
            if (grid.RowDefinitions.Count > 0)
            {
                Grid.SetRowSpan(this, grid.RowDefinitions.Count);
            }
            grid.Children.Add(this);
 
            // Create a transition like the regular MessageBox
            SwivelTransition transitionIn = new SwivelTransition();
            transitionIn.Mode = SwivelTransitionMode.BackwardIn;
 
            // Transition only the MessagePanel
            ITransition transition = transitionIn.GetTransition(MessagePanel);
            transition.Completed += (s, e) => transition.Stop();
            transition.Begin();
 
            if (_page.ApplicationBar != null)
            {
                // Hide the app bar so they cannot open more message boxes
                _page.ApplicationBar.IsVisible = false;
            }
        }
 
        private void Remove()
        {
            var frame = Application.Current.RootVisual as Microsoft.Phone.Controls.PhoneApplicationFrame;
            var page = frame.Content as PhoneApplicationPage;
            var grid = System.Windows.Media.VisualTreeHelper.GetChild(page, 0) as Grid;
 
            // Create a transition like the regular MessageBox
            SwivelTransition transitionOut = new SwivelTransition();
            transitionOut.Mode = SwivelTransitionMode.BackwardOut;
 
            ITransition transition = transitionOut.GetTransition(MessagePanel);
            transition.Completed += (s, e) =>
                {
                    transition.Stop();
                    grid.Children.Remove(this);
                    if (page.ApplicationBar != null)
                    {
                        page.ApplicationBar.IsVisible = true;
                    }
                };
            transition.Begin();
        }
 
        private void Page_BackKeyPress(object sender, CancelEventArgs e)
        {
            OnClosed(MessageBoxResult.Cancel);
            e.Cancel = true;
        }
 
        private void YesButton_Click(object sender, RoutedEventArgs e)
        {
            OnClosed(MessageBoxResult.Yes);
        }
 
        private void NoButton_Click(object sender, RoutedEventArgs e)
        {
            OnClosed(MessageBoxResult.No);
        }
    }
 
}

Now you can show your message box like such:

 1: var msgBox = CustomMessageBox.Show("Do you like Windows Phone.", "Custom Prompt", "I <3 WP", "No Thanks");

And you get the following:

image

Improving the User Experience (with Silverlight 5)

In case you were unable to see my lighting talk I gave at this years Esri Dev Meetup, here’s your chance!

It’s hard to see the slides in the video of my presentation, so you’ll have to sync up the video with my slides.

Devmeetup Colorado Shawn Kendrot from glenn letham on Vimeo.

The talk was a lot of fun and I look forward to doing another. Maybe at the next Ignite Spatial?

Using the Silverlight 5 PivotViewer with ArcGIS Silverlight

Pivot was originally released as a demonstration project that was a separate download from Silverlight itself. At version 5, Pivot becomes part of the Silverlight family. Pivot allows users the ability to visualize their data. Puts the power of filtering and grouping their data without the need to learn complex SQL statements. The original version of Pivot required you to have an XML representation of the data, and images that it would display. This required extra work for the developer, or web administrator to create this data from their data store. With Silverlight 5, you now have the ability to bind to any property that your class has. It also allows you the ability to create what’s known as trading cards with XAML. These cards replace the images you previously needed. You can even define at what stage you want a trading card to display. By defining different trading cards at different levels, you can give the user more information the more they filter down their data.

When I first started looking into whether I’d be able to get PivotViewer results from ArcGIS Server, I figured I would have to create a Silverlight class with properties for all of the fields that I wanted to use within Pivot. It turns out that Pivot works great with the data you get straight from the ArcGIS or ArcFM Silverlight SDK. It easily binds to the Attributes Dictionary that is on the Graphics objects.

To get started using Pivot, you need to download the latest Silverlight 5 Tools. Create a new Silverlight Application. Make sure to pick Silverlight 5 as the version

image

You’ll need to add a reference to ESRI.ArcGIS.Client and System.Windows.Controls.Pivot. To add the ArcGIS Silverlight SDK, open the Add Reference dialog, and click the Browse tab. Browse to the location of ESRI.ArcGIS.Client assembly. The default location is C:\Program Files (x86)\ESRI SDKs\Silverlight.

Open MainPage.xaml.cs. We’re going to need to create a query to the ArcGIS Server. For my example, I’m going to use the Telvent ArcGIS Server at http://server.arcfmsolution.com/arcgis/rest/services. First, we need to query for the electrical switches that we’ll use within Pivot

public MainPage()
{
    InitializeComponent();

    QueryForSwitches();
}

private void QueryForSwitches()
{
    // Query for switches. Switches have LayerID of 3 
QueryTask task = new QueryTask("http://server.arcfmsolution.com/ArcGIS/rest/services/Electric/MapServer/3"); Query query = new Query(); // Return all of the fields, we want use them all, but good to have 'em anyways! query.OutFields.Add("*"); // Get as many of the switches as we can
query.Where = "ObjectID > 0"; task.ExecuteCompleted += QueryTask_ExecuteCompleted; task.ExecuteAsync(query); }

Add a call to this method within the constructor of the MainPage. When the task completes, all we need to do is set the DataContext to the Features that are returned.

void QueryTask_ExecuteCompleted(object sender, QueryEventArgs e)
{
    DataContext = e.FeatureSet.Features;
}

The XAML is where all of the magic happens. We are able to pick which fields the user will be able to filter/sort by, and define the trading cards. I want to give the user the ability to sort and filter switches by Installation Date, the Phase Designation, which Feeder it belongs to, and the Operating Voltage. So the fields I need from the Attributes dictionary are INSTALLATIONDATE, PHASEDESIGNATION, FEEDERID, and OPERATINGVOLTAGE. To do this, you add PivotViewerProperties.

<UserControl x:Class="PivotViewer.MainPage"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk"
             xmlns:ViewModels="clr-namespace:PivotViewer.ValueConverters">
    <Grid x:Name="LayoutRoot" Background="White">
        <Grid.Resources>
            <ViewModels:PhaseDesignationValueConverter x:Key="PhaseConverter"/>
            <ViewModels:VoltageToStringValueConverter x:Key="VoltageConverter"/>            
        </Grid.Resources>
        <sdk:PivotViewer x:Name="MyPivot" ItemsSource="{Binding}">
            <!-- Setting PivotProperties -->
            <sdk:PivotViewer.PivotProperties>
                <sdk:PivotViewerDateTimeProperty Id="InstallDate" Options="CanFilter" DisplayName="Date Installed" 
Binding="{Binding Attributes[INSTALLATIONDATE], StringFormat=\{0:MM/dd/yyyy\}}" /> <sdk:PivotViewerStringProperty Id="Phase" Options="CanFilter" DisplayName="Phase"
Binding="{Binding Attributes[PHASEDESIGNATION], Converter={StaticResource PhaseConverter}}" /> <sdk:PivotViewerStringProperty Id="Feeder" Options="CanFilter" DisplayName="Feeder"
Binding="{Binding Attributes[FEEDERID]}" /> <sdk:PivotViewerStringProperty Id="OpVoltage" Options="CanFilter" DisplayName="Operating Voltage"
Binding="{Binding Attributes[OPERATINGVOLTAGE], Converter={StaticResource VoltageConverter}}" /> </sdk:PivotViewer.PivotProperties> </sdk:PivotViewer> </Grid> </UserControl>

I’m using fields that have domain values, so I need a few value converters, I won’t bore you with the conversion of the domains here. If you need to sort/filter by date values, like the Installation Date, use a PivotViewerDateTimeProperty This gives you cool date filtering capabilities. I don’t have any integer values that I’m sorting by, but if I did I would use the PivotViewerNumericProperty. To create a trading card, add a PivotViewerItemTemplate to the ItemTemplates collection.

<sdk:PivotViewer.ItemTemplates>
    <sdk:PivotViewerItemTemplate>
        <Border Width="200" Height="200" Background="Gray" BorderBrush="Black" BorderThickness="1">
            <StackPanel Orientation="Vertical">
                <TextBlock Text="{Binding Attributes[FACILITYID]}" FontSize="24" Foreground="White" />
                <StackPanel Margin="10,0">
                    <TextBlock Text="Feeder" Foreground="White" FontSize="18" Margin="0,10,0,0"/>
                    <TextBlock Text="{Binding Attributes[FEEDERID]}" FontSize="14" Foreground="White" />

                    <TextBlock Text="Voltage" Foreground="White" FontSize="18" Margin="0,10,0,0"/>
                    <TextBlock Text="{Binding Attributes[OPERATINGVOLTAGE], 
Converter={StaticResource VoltageConverter}}"
FontSize="14" Foreground="White" /> <TextBlock Text="Installed" Foreground="White" FontSize="18" Margin="0,10,0,0"/> <TextBlock Text="{Binding Attributes[INSTALLATIONDATE], StringFormat=\{0:MM/dd/yyyy\}}"
FontSize="14" Foreground="White" /> </StackPanel> </StackPanel> </Border> </sdk:PivotViewerItemTemplate> </sdk:PivotViewer.ItemTemplates>

This gives our trading card a Metro feel. But there isn’t much data here, as the user drills further into their data, I want to show them more. We can add more trading cards if we want to add more detail when users start filtering their data, or using the zoom capability of Pivot. To do this you set the MaxWidth property of the PivotViewerItemTemplate, and add another template.

<sdk:PivotViewerItemTemplate MaxWidth="250">

To see more, download the complete sample here. Give your users the power to SEE their data.

Using the Repository Pattern with ArcObjects

The Repository Pattern is well known to everyone that needs to access a database. That is unless you access a database through ArcObjects. A repository is easy to use, easy to inject, and easy to unit test. Doing anything with ArcObjects is very complicated. Especially unit test. Wrapping ArcObjects in a layer of abstraction usually helps simplify things. By wrapping a few simple operations within a repository, your code no longer needs to start or stop an edit operation, no longer needs to worry about creating a QueryFilter (something that cannot be done within unit tests), and no longer needs to worry about all of the little details of accessing ArcObjects.

Let’s start with a simple repository using ArcObjects. We’ll need the normal CRUD operations.

interface ITableRepository {
    IRow GetById(int id);
    IEnumerable<IRow> GetAll();
    IEnumerable<IRow> GetByWhere(string whereClause);
    void Delete(IRow entity);
    int Save(IRow entity);
    void BeginTransaction();
    void EndTransaction(string name);
    void RollbackTransaction();
}

If this interface is injected into your object, you can now easily unit test all of the basic CRUD operations, something that you cannot do if you rely on ArcObjects.

The basic implementation of this interface will need to do all of the dirty ArcObjects access that your code used to do. It will need an instance of the Editor object so that it can manipulate the operations. It will have to create the QueryFilters for doing queries.. You get the idea.

class TableRepository : ITableRepository {
    private readonly IName _tableName;
    private readonly IEditor _editor;

    public TableRepository(ITable table)
    {
        if (table == null) throw new ArgumentNullException("table");

        // Store the IName, it's cheaper than olding onto the actual table 
        IDataset dataset = (IDataset)table;
        _tableName = dataset.FullName;

        _editor = GetEditor();
    }

    public ITable Table { get { return _tableName.Open() as ITable; } }

    #region ITableRepository Members

    public IRow GetById(int id)
    {
        ITable table = Table;

        return table.GetRow(id);
    }

    public IEnumerable<IRow> GetAll()
    {
        return GetByWhere(null);
    }

    public IEnumerable<IRow> GetByWhere(string whereClause)
    {
        ITable table = Table;

        // Some great magic happens here, throw this code into reflector and see 
        // what comes out 
        using (var releaser = new ComReleaser())
        {
            IQueryFilter filter = new QueryFilterClass();
            filter.WhereClause = whereClause;

            // non-recycling cursor, we are holding on to the row returned 
           ICursor cursor = table.Search(filter, false);
            releaser.ManageLifetime(cursor);

            IRow row = cursor.NextRow();
            while (row != null)
            {
                yield return row;

                row = cursor.NextRow();
            }
        }
    }

    public void Delete(IRow entity)
    {
        if (entity == null) throw new ArgumentNullException("entity");

        entity.Delete();
    }

    public int Save(IRow entity)
    {
        if (entity == null) throw new ArgumentNullException("entity");

        int id = entity.OID;
        if (entity.HasOID && (entity.OID < 0))
        {
            // It's a new row, we need to add it to the table 
            ICursor cursor = Table.Insert(true);
            id = Convert.ToInt32(cursor.InsertRow(entity));
            cursor.Flush();
        }
        else {
            // existing row 
            entity.Store();
        }
        return id;
    }

    public void BeginTransaction()
    {
        if (_editor.EditState == esriEditState.esriStateEditing)
        {
            _editor.StartOperation();
        }
    }

    public void EndTransaction(string name)
    {
        if (_editor.EditState == esriEditState.esriStateEditing)
        {
            _editor.StopOperation(name);
        }
    }

    public void RollbackTransaction()
    {
        if (_editor.EditState == esriEditState.esriStateEditing)
        {
            _editor.AbortOperation();
        }
    }

    #endregion private IEditor GetEditor()
    {
        IApplication app = GetApplication();
        UID editorUid = new UIDClass();
        editorUid.Value = "esriEditor.Editor";
        return app.FindExtensionByCLSID(editorUid) as IEditor;
    }

    private IApplication GetApplication()
    {
        Type t = Type.GetTypeFromProgID("esriFramework.AppRef");
        System.Object obj = Activator.CreateInstance(t);
        return obj as IApplication;
    }
}

If you’re developing with ArcFM, you can make the Transaction methods a little cleaner and you no longer  need to hold onto an IEditor reference.

public void BeginTransaction()
{
    if(Editor.EditState == EditState.StateEditing)
    {
        Editor.StartOperation();                
    }
}

public void EndTransaction(string name)
{
    if(Editor.IsOperationInProgress())
    {
        Editor.StopOperation(name);
    }
}

public void RollbackTransaction()
{
    if (Editor.IsOperationInProgress())
    {
        Editor.AbortOperation();
    }
}

If you like to keep things more generic, remove the ITable and IRow from the methods and properties and make them generic type parameters. This allows you the ability to implement the interface and get back your own custom objects.

interface ITableRepository<TEntity>