Adding custom feature attributes

Complexity: Beginner Data Requirement: Installed with software

A common customization to provide an enhanced user experience is to augment and/or replace the existing attributes for a feature displayed to the user. For example, a feature layer may contain two columns, FirstName and LastName, that you want to present as a single attribute, FullName. To do this, you can write an extension that replaces the FirstName and LastName attributes with a custom attribute called FullName. This FullNameAttribute class will encapsulate the editing of the two underlying attributes into a single entity or attribute.

Steps:
  1. Create a new FeatureAttribute-derived class, FullNameFeatureAttribute.

    FeatureAttribute class

    The key properties and methods that need to be overridden by your class are the Value property (get and set), the Refresh() method, and the RestoreOriginalValue() method. The Value property is just the string value of the attribute, and, in this example, will be the combined attribute values. The Refresh() method needs to update the attribute's Value (and DisplayString) property to reflect the current state of the attribute's associated feature. The RestoreOriginalValue() method needs to update the attribute's Value (and DisplayString) property to the value when the attribute was initially created (in other words, undo any edits).

    Name the class FullNameFeatureAttribute. This class will combine the two string attributes into a single string. The actual value of the attribute will be formatted as <Last>, <First>, but the user-friendly display string will be formatted <First> <Last>.

    The constructor for FullNameFeatureAttribute will take a Feature (required by the FeatureAttribute base class) and two FeatureAttribute objects corresponding to the first and last name attributes. These two FeatureAttribute objects will be used to generate the Value property and will be updated when the Value property is modified.

    Create a new class file for this code or add into the extension class file.

    public class FullNameFeatureAttribute : FeatureAttribute
      {
        FeatureAttribute _firstName;
        FeatureAttribute _lastName;
        string _fullName;
    
        public FullNameFeatureAttribute(Feature feature, FeatureAttribute firstName, FeatureAttribute lastName) : 
          base(feature)
        {
          this.DisplayCaption = "Full Name";
          this.EditCaption = "Full Name";
    
          _firstName = firstName;
          _lastName = lastName;
          UpdateValue();
        }
    
        public override object Value
        {
          get { return _fullName; }
          set
          {
            string tempString = value as string;
    
            int index = -1;
            if (!String.IsNullOrEmpty(tempString))
              index = tempString.IndexOf(',');
    
            if (index < 0)
            {
              _firstName.Value = null;
              _lastName.Value = null;
            }
            else
            {
              _firstName.Value = tempString.Substring(index + 1);
              _lastName.Value = tempString.Substring(0, index);
            }
    
            UpdateValue();
          }
        }
    
        public override void Refresh()
        {
          _firstName.Refresh();
          _lastName.Refresh();
          UpdateValue();
        }
    
        public override void RestoreOriginalValue()
        {
          _firstName.RestoreOriginalValue();
          _lastName.RestoreOriginalValue();
          UpdateValue();
        }
    
        private void UpdateValue()
        {
          _fullName = String.Format("{0}, {1}", _lastName.DisplayString, _firstName.DisplayString);
          OnPropertyChanged("Value");
    
          this.DisplayString = String.Format("{0} {1}", _firstName.DisplayString, _lastName.DisplayString);
        }
      }
    

  2. Create a data template for editing the FullNameFeatureAttribute.

    Data template will determine how to display the FullNameFeatureAttribute object in EditFeatureAttributesPage. First, create an unnamed data template for the FullNameFeatureAttribute data type for editing attributes. Use an unnamed data template because there is no way for the edit controls to specify the name of or select a template. After creating EditAttributeResources.xaml as the data template for editing purpose, it will be loaded and added directly to the resources for the edit controls.

    The data template for editing FullNameFeatureAttribute might look something like this:

    <ResourceDictionary 
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:System="clr-namespace:System;assembly=mscorlib" 
      xmlns:local="clr-namespace:CustomizationSamples;assembly=AttributeCustomizationExtensions"
      >
      <!--
      Template for editing FullNameFeatureAttribute...
      -->
      <DataTemplate DataType="{x:Type local:FullNameFeatureAttribute}">
        <Button 
            Margin="10"
            HorizontalContentAlignment="Stretch"
            Style="{DynamicResource DefaultButtonStyle}" 
            Background="HotPink"
            Command="{x:Static local:Commands.EditFullName}"
            CommandParameter="{Binding}"
            >
          <Grid Margin="10">
            <Grid.ColumnDefinitions>
              <ColumnDefinition SharedSizeGroup="AttributeNameColumn" Width="Auto" />
              <ColumnDefinition />
              <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>
            <TextBlock 
                Grid.Column="0" 
                VerticalAlignment="Center"
                Margin="0,0,10,0"
                FontWeight="Bold"
                Style="{DynamicResource NormalTextStyle}"
                Text="{Binding EditCaption}"
                />
            <TextBlock 
                Grid.Column="1" 
                VerticalAlignment="Center"
                Text="{Binding DisplayString}"
                Style="{DynamicResource NormalTextStyle}"
                />
            <Viewbox 
                Grid.Column="2" 
                VerticalAlignment="Center"
                Width="30" 
                Height="30"
                >
              <Grid HorizontalAlignment="Left" VerticalAlignment="Top" Width="100" Height="100">
                <Ellipse Margin="0,0,0,0" Fill="#FF7F8293" Stroke="#FF000000"/>
                <Path RenderTransformOrigin="0.5,0.5" HorizontalAlignment="Left" VerticalAlignment="Top" Width="50" Height="50" Fill="#FFFFFFFF" Stretch="Fill" Stroke="#FF707070" Data="M0.5,0.5 L14.5,0.5 39.5,0.5 39.5,14.5 14.5,14.5 14.5,39.5 0.5,39.5 0.5,14.5 0.5,0.5 z" Margin="20,25,0,0">
                  <Path.RenderTransform>
                    <TransformGroup>
                      <ScaleTransform ScaleX="1" ScaleY="1"/>
                      <SkewTransform AngleX="0" AngleY="0"/>
                      <RotateTransform Angle="135"/>
                      <TranslateTransform X="0" Y="0"/>
                    </TransformGroup>
                  </Path.RenderTransform>
                </Path>
              </Grid>
            </Viewbox>
          </Grid>
        </Button>
      </DataTemplate>
    </ResourceDictionary>
    

    The button is using the Commands.EditFullName command, which you created, and the CommandParameter attribute, which passes FullNameFeatureAttribute as a parameter to the command.

  3. Create a project extension that replaces the appropriate attributes with FullNameFeatureAttribute.

    The project extension injects the custom attribute and adds the data template to the edit controls.

    1. Listen to the ControlCreatingFeatureAttributes events.
    2. Load the data template and add it to the edit controls.
    3. Create a command binding and add it to the edit controls.
    4. Add a new instance of FullNameFeatureAttribute to the edit controls.
    5. Respond to the command binding.

    The project extension will attach to the ControlCreatingFeatureAttributes events on AttributeEditControl class from the OnOwnerInitialized() method:

    _collectFeaturesTask = MobileApplication.Current.Project.Tasks.GetFirstExtensionOfType(typeof(CollectFeaturesTask)) as CollectFeaturesTask;
    _collectFeaturesTask.CollectionStarted += new EventHandler(_collectFeaturesTask_CollectionStarted);
    AttributeEditControl.ControlCreatingFeatureAttributes += new 
        EventHandler<AttributeEditControlEventArgs>(AttributeEditControl_ControlCreatingFeatureAttributes);
    

    Load the resource dictionaries containing the data templates to the OnOwnerIntialized() method:

    // Load the data templates for our FullNameFeatureAttribute
    _editAttributesResources = new ResourceDictionary();
    _editAttributesResources.Source = new 
        Uri("/AttributeCustomizationExtensions;component/EditAttributesResources.xaml", 
        UriKind.RelativeOrAbsolute);
    

    Create a command binding:

    // Create a command binding to the EditFullName command
    _commandBinding = new CommandBinding(Commands.EditFullName);
    _commandBinding.Executed += new ExecutedRoutedEventHandler(_commandBinding_Executed);
    

    Add the follow routine.

    public static class Commands
    {
     /// <summary>
     /// Represents the command to edit a full name feature attribute.
     /// </summary>
     public static readonly RoutedCommand EditFullName = new RoutedCommand();
    }
    

    Add the following declarations to the class:

    ResourceDictionary _editAttributesResources;
    CommandBinding _commandBinding;
    

    Add the following code to start and stop the customization properly:

    In the OnOwnerInitialized()

    // Make sure we create our resource dictionary on the UI thread
    if (!MobileApplication.Current.Dispatcher.CheckAccess())
    {
      MobileApplication.Current.Dispatcher.BeginInvoke((ThreadStart)delegate()
      {
        OnOwnerInitialized();
      });
      return;
    }
    

    In the Uninitialize()

    protected override void Uninitialize()
    {
      // Stop listening to events
      AttributeEditControl.ControlCreatingFeatureAttributes -= new EventHandler<AttributeEditControlEventArgs>(AttributeEditControl_ControlCreatingFeatureAttributes);
    
      if (_editFeatureAttributesPage != null)
      {
        _editFeatureAttributesPage = null;
      }
    
      if (_commandBinding != null)
      {
        _commandBinding.Executed -= new ExecutedRoutedEventHandler(_commandBinding_Executed);
        _commandBinding = null;
      }
    }
    

    Add the following assemblies:

    using System;
    using System.Collections.ObjectModel;
    using System.Threading;
    using System.Windows;
    using System.Windows.Input;
    using ESRI.ArcGIS.Mobile.Client;
    using ESRI.ArcGIS.Mobile.Client.Controls;
    using ESRI.ArcGIS.Mobile.Client.Pages;
    using ESRI.ArcGIS.Mobile.Client.Tasks.Synchronization;
    using ESRI.ArcGIS.Mobile.Client.Tasks.CollectFeatures;
    

    All the work happens in response to the ControlCreatingFeatureAttributes event. For edit controls, the event handlers will remove the feature attributes for the FirstName and LastName attributes and create and add a new instance of the FullNameFeatureAttribute class:

    void AttributeEditControl_ControlCreatingFeatureAttributes(object sender, AttributeEditControlEventArgs e)
    {
      // Add our data template for our FullNameFeatureAttribute to the 
      // control's resources.
      if (_editAttributesResources != null && !_editFeatureAttributesPage.Resources.MergedDictionaries.Contains(_editAttributesResources))
      {
        e.Control.Resources.MergedDictionaries.Add(_editAttributesResources);
      }
    
      // Add our command binding to the page
      if (!_editFeatureAttributesPage.CommandBindings.Contains(_commandBinding))
      {
        e.Control.CommandBindings.Add(_commandBinding);
      }
    
      FindAndReplaceFirstAndLastName(e.Feature, e.Attributes);
    }
    
    private void FindAndReplaceFirstAndLastName(Feature feature, ObservableCollection<FeatureAttribute> attributes)
    {
      // Look for FirstName and LastName attributes.
      FeatureAttribute firstName = null;
      FeatureAttribute lastName = null;
      foreach (FeatureAttribute attribute in attributes)
      {
        DataColumnFeatureAttribute columnAttribute = attribute as DataColumnFeatureAttribute;
        if (columnAttribute == null)
          continue;
    
        if (columnAttribute.ColumnName.ToLower() == Column1.ToLower())
          firstName = attribute;
        else if (columnAttribute.ColumnName.ToLower() == Column2.ToLower())
          lastName = attribute;
      }
    
      // Replace the FirstName/LastName attributes with our custom attribute
      if (firstName != null && lastName != null)
      {
        FullNameFeatureAttribute fullName = new FullNameFeatureAttribute(feature, firstName, lastName);
        attributes.Remove(firstName);
        attributes.Remove(lastName);
        attributes.Add(fullName);
      }
    }
    

    The final thing your extension needs to do is edit FullNameFeatureAttribute when the EditFullName command is executed (which is established when you created the command binding). Since the FullNameFeatureAttribute class is really nothing more than a string value, you can reuse the existing EditTextAttributePage class to edit the value of FullNameFeatureAttribute:

    void _commandBinding_Executed(object sender, ExecutedRoutedEventArgs e)
    {
      //EditCustomFeatureAttributePage page = new EditCustomFeatureAttributePage();
      EditTextAttributePage page = new EditTextAttributePage();
      page.Attribute = e.Parameter as FeatureAttribute;
      MobileApplication.Current.Transition(page);
    }
    

    You could have created a custom page to edit the value of FullNameFeatureAttribute, in which case it would derive from the EditAttributePage class.

For the complete source code, see the AttributeCustomization extension sample, which is available on ArcGIS.com.

1/7/2015