Adding custom feature attributes

Complexity: Beginner Data Requirement: Installed with software

Another customization use case is to augment and/or replace the existing attributes for a feature. For example, a feature layer may contain two columns, FirstName and LastName, that you would like to present as a single attribute, FullName. To do this, you will write an extension that replaces the FirstName and LastName attributes with a custom attribute called FullName. This FullNameAttribute class will encapsulate the display and editing of the two underlying attributes into a single entity/attribute.

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

    Need alt text

    The key properties/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 self-explanatory. 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.

    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 = "Edit 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 data templates for viewing and editing the FullNameFeatureAttribute.

    Data templates will determine how to display the FullNameFeatureAttribute object in ViewFeatureAttributesControl and EditFeatureAttributesPage. First, create two unnamed data templates for the FullNameFeatureAttribute data type, one for viewing attributes and one for editing attributes. Use unnamed data templates because there is no way for the view/edit controls to specify the name of or select a template. Since you'll need two data templates for the same data type, create these templates in two separate resource dictionaries, ViewAttributeResources.xaml and EditAttributeResources.xaml, so they can be loaded independently and added directly to the resources for the view/edit controls.

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

    <!--
      Template for viewing FullNameFeatureAttribute...
      -->
      <DataTemplate DataType="{x:Type local:FullNameFeatureAttribute}" >
        <Border 
            BorderThickness="0,0,0,1"
            Background="HotPink"
            BorderBrush="Plum"        
            Margin="0"
            Padding="10"
            >
          <Grid>
            <Grid.ColumnDefinitions>
              <ColumnDefinition SharedSizeGroup="AttributeNameColumn" Width="Auto" />
              <ColumnDefinition />
            </Grid.ColumnDefinitions>
            <TextBlock 
                Grid.Column="0"
                Margin="0,0,10,0"
                VerticalAlignment="Center"
                FontWeight="Bold"
                Style="{DynamicResource NormalTextStyle}" 
                Text="{Binding DisplayCaption}" 
                Foreground="DarkRed"
                />
            <TextBlock 
                Grid.Column="1"
                VerticalAlignment="Center"
                Style="{DynamicResource NormalTextStyle}" 
                Text="{Binding DisplayString}"
                />
          </Grid>
        </Border>
      </DataTemplate>

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

    <!--
      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 Value}"
                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>

    Note that 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 templates to the view/edit controls.

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

    As in the first example, your project extension will attach to the ControlCreatingFeatureAttributes events on both the ViewFeatureAttributesControl and EditFeatureAttributesPage classes:

    ViewFeatureAttributesControl.ControlCreatingFeatureAttributes += new  EventHandler<ViewFeatureAttributesControlEventArgs>(ViewFeatureAttributesControl_ControlCreatingFeatureAttributes);
    EditFeatureAttributesPage.ControlCreatingFeatureAttributes += new EventHandler<EditFeatureAttributesPageEventArgs>(EditFeatureAttributesPage_ControlCreatingFeatureAttributes);

    Next, load the resource dictionaries containing the data templates:

    __viewAttributesResources = new  ResourceDictionary();
    _viewAttributesResources.Source = new Uri("/AttributeCustomization;component/ViewAttributesResources.xaml", UriKind.RelativeOrAbsolute);
     
    _editAttributesResources = new ResourceDictionary();
    _editAttributesResources.Source = new Uri("/AttributeCustomization;component/EditAttributesResources.xaml", UriKind.RelativeOrAbsolute);

    Create a command binding:

    _commandBinding = new CommandBinding(Commands.EditFullName);
    _commandBinding.Executed += new ExecutedRoutedEventHandler(_commandBinding_Executed);

    All the magic happens in response to the ControlCreatingFeatureAttributes event. For both 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:

    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 == "FirstName")
            firstName = attribute;
        else if (columnAttribute.ColumnName == "LastName")
              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 event handler for ViewFeatureAttributesControl

    void  ViewFeatureAttributesControl_ControlCreatingFeatureAttributes(object  sender, ViewFeatureAttributesControlEventArgs  e)
    {
      // Add our data template for our FullNameFeatureAttribute to the
      // control's resources.
      if (_viewAttributesResources != null && !e.Control.Resources.MergedDictionaries.Contains(_viewAttributesResources))
      {
        e.Control.Resources.MergedDictionaries.Add(_viewAttributesResources);
      }
    
      FindAndReplaceFirstAndLastName(e.Control.Feature, e.Control.Attributes);
    }

    The event handler for EditFeatureAttributesPage has one additional step, which is to add a command binding for the EditFullName command to EditFeatureAttributesPage:

    void  EditFeatureAttributesPage_ControlCreatingFeatureAttributes(object  sender, EditFeatureAttributesPageEventArgs  e)
    {
      // Add our data template for our FullNameFeatureAttribute to the
      // control's resources.
      if (_editAttributesResources != null && !e.Page.Resources.MergedDictionaries.Contains(_editAttributesResources))
      {
        e.Page.Resources.MergedDictionaries.Add(_editAttributesResources);
      }
    
      // Add our command binding to the page
      if (!e.Page.CommandBindings.Contains(_commandBinding))
      {
        e.Page.CommandBindings.Add(_commandBinding);
      }
    
      FindAndReplaceFirstAndLastName(e.Page.Feature, e.Page.Attributes);
    }

    The last 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)
    {
      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, refer to the AttributeCustomization extension sample, which is available for download.


9/20/2011