ProConcepts Parcel Fabric - kataya/arcgis-pro-sdk GitHub Wiki

The parcel fabric is a comprehensive framework of functionality in ArcGIS for modeling parcels in organizations ranging from national cadastral agencies to local governments. This topic provides an introduction to the parcel fabric API. It details the classes and methods that query and edit the parcel fabric. The parcel fabric API is commonly used in conjunction with the geodatabase and editing APIs.

Language:      C#
Subject:       Parcel Fabric
Contributor:   ArcGIS Pro SDK Team <[email protected]>
Organization:  Esri, http://www.esri.com
Date:          10/01/2020
ArcGIS Pro:    2.7
Visual Studio: 2017, 2019  

In this topic

Introduction

Overview

  • The parcel fabric API is a Data Manipulation Language (DML)-only API. This means that all schema creation and modification operations such as creating parcel types or adding and deleting rules need to use the geoprocessing API.

  • Almost all of the methods in the parcel fabric API should be called on the Main CIM Thread (MCT). The API reference documentation on the methods that need to run on the MCT are specified as such. These method calls should be wrapped inside the QueuedTask.Run call. Failure to do so will result in ConstructedOnWrongThreadException being thrown. See Working with multi-threading in ArcGIS Pro to learn more.

  • In a multi-user environment a parcel fabric is accessed via services and not client-server. As such the parcel fabric API is designed to support a service-oriented architecture. The parcel fabric also has a single-use model that works directly on a file geodatabase.

Note: This topic assumes a basic understanding of the parcel fabric information model. See the online help for more information. See: What is the parcel fabric?

Namespaces

The items of the parcel fabric are included in the ArcGIS.Desktop.Editing and ArcGIS.Desktop.Mapping assembly namespaces.
Add using ArcGIS.Desktop.Editing; and using ArcGIS.Desktop.Mapping; to the top of your source files.

License level

The parcel fabric functionality is available at the Standard and Advanced license levels. Code that makes function calls to the Parcel Fabric API should check license level on the client application to avoid an Insufficient License exception. Check license level prior to making these function calls. In general, the try{} catch{} pattern should also be used to handle exceptions such as these. However, an explicit license check can be used as an early confirmation.

This code returns the current license level of the application and returns a message if the license is at the Basic level:

var lic = ArcGIS.Core.Licensing.LicenseInformation.Level;
if (lic < ArcGIS.Core.Licensing.LicenseLevels.Standard)
{
  MessageBox.Show("Insufficient License Level.");
  return;
}

Parcel data model

The parcel fabric is a controller dataset that handles a set of simple feature classes, a single geodatabase topology, and a set of attribute rules. Multiple parcel types can be added to a fabric. Each parcel type is represented by a polygon-polyline featureclass-pair. Each parcel type’s featureclass-pair has its own schema and can be extended with additional fields, domains and subtypes. Validating a parcel fabric’s topology rules and evaluating attribute rules may result in the creation of error features that report on rules that are outside their defined limits. To learn more see the help topic Parcel fabric data model.

Parcel layer

Only one parcel layer can be added to a map view. Attempting to add a second parcel layer to a map view through the user interface or through code is prevented by the system, and an error is returned. Hence there can be a reliable expectation that there will only be one fabric layer in the map view. Consequently the following line of code is reliable for accessing the parcel layer.

var myParcelFabricLayer = MapView.Active.Map.GetLayersAsFlattenedList().OfType<ParcelLayer>().FirstOrDefault();

The parcel layer represents the controller for all the corresponding layers for points, connections, records, and the parcel types.

You can test a layer to check if it is controlled by a fabric as follows:

// Check if a layer has a parcel fabric source
var myFeatureLayer = MapView.Active.GetSelectedLayers().FirstOrDefault();
bool bIsControlledByFabric = await myFeatureLayer.IsControlledByParcelFabric(ParcelFabricType.ParcelFabric);

NOTE: There are two different types of parcel fabrics, the type created by ArcMap, and the new type created when using ArcGIS Pro. In ArcGIS Pro the parcel fabrics from ArcMap are read-only. You can test if a layer's source is pointing to a legacy ArcMap parcel fabric by using the following code:

// Check if a layer has a source that is a parcel fabric for ArcMap 
var myLayer = MapView.Active.GetSelectedLayers().FirstOrDefault();
bool bIsControlledByFabric = await myLayer.IsControlledByParcelFabric(ParcelFabricType.ParcelFabricForArcMap);

To learn more about upgrading an ArcMap parcel fabric to work in ArcGIS Pro see Upgrade an ArcMap parcel fabric.

Extension methods

The fabric's parcel layer has extension methods to get and set the active record. Note that nearly all the methods in the parcel fabric API should be called on the Main CIM Thread (MCT). The API reference documentation on the methods indicate those that need to run on the MCT. These method calls should be wrapped inside the QueuedTask.Run call. Failure to do so will result in ConstructedOnWrongThreadException being thrown. See Working with multi-threading in ArcGIS Pro to learn more.

Parcel editing concepts

When editing a parcel fabric there are two high-level conceptual workflow modes:

  1. Record driven workflows
  2. Quality driven workflows

Record driven workflows and the Active Record

Record driven workflows are those that update the legal parcel information and the parcel lineage; the parent parcel is retired when its child parcel(s) are created. This happens when entering data from a legal land record document such as a Deed, Subdivision Plan, Record of Survey, and so on. For example when an existing parcel gets subdivided into two portions the legal document that defines this land transaction is called the legal record. The original parent parcel is retired by that record and the two new parcels are created by that record. From the user workflow perspective this requires that the first step is to create the record in the fabric and then to set that new record as active. Here is code to do that:

string errorMessage = await QueuedTask.Run( async () =>
{
  Dictionary<string, object> RecordAttributes = new Dictionary<string, object>();
  string sNewRecord = "MyRecordName";

  var myParcelFabricLayer = MapView.Active.Map.GetLayersAsFlattenedList().OfType<ParcelLayer>().FirstOrDefault();
  //if there is no fabric in the map then bail
  if (myParcelFabricLayer == null)
    return "Please add a parcel fabric to the map.";
  try
  {
    var recordsLayer = await myParcelFabricLayer.GetRecordsLayerAsync();
    var editOper = new EditOperation()
    {
      Name = "Create Parcel Fabric Record",
      ProgressMessage = "Create Parcel Fabric Record...",
      ShowModalMessageAfterFailure = true,
      SelectNewFeatures = false,
      SelectModifiedFeatures = false
    };
    RecordAttributes.Add("Name", sNewRecord);
    editOper.CreateEx(recordsLayer.FirstOrDefault(), RecordAttributes);
    if (!editOper.Execute())
      return editOper.ErrorMessage;
    await myParcelFabricLayer.SetActiveRecordAsync(sNewRecord);
  }
  catch (Exception ex)
  {
    return ex.Message;
  }
  return "";
});
if (!string.IsNullOrEmpty(errorMessage))
  MessageBox.Show(errorMessage);

The active record’s globally unique identifier (guid) is used to populate the fields called RetiredByRecord and CreatedByRecord. These fields are populated when new parcel features are created or when existing parcels are retired and are being replaced by new parcels.

The concept of a record being active is client specific. The server does not persist any information related to a record being active. The client has a state whereas the server is stateless.

var pRec = myParcelFabricLayer.GetActiveRecord();

When the active record is set the edits that create new parcel features are tagged with this active record’s guid and edits like Merge, Divide, Build will apply the correct parcel edits and parcel lineage edits. In the following code note that there are no Parcel API function calls. However when this code is applied to features controlled by a fabric these enhanced parcel edits are applied if there is an active record. When the active record is not set the edits are standard edits and from a parcel editing perspective are considered to be quality driven workflows. This code will Merge selected features:

string errorMessage = await QueuedTask.Run( async () =>
{
  // check for selected layer
  if (MapView.Active.GetSelectedLayers().Count == 0)
    return "Please select a feature layer in the table of contents.";
  //get the feature layer that's selected in the table of contents
  var featSrcLyr= MapView.Active.GetSelectedLayers().OfType<FeatureLayer>().FirstOrDefault();
  if (featSrcLyr.SelectionCount == 0)
    return "There is no selection on the source layer.";
  var myParcelFabricLayer = MapView.Active.Map.GetLayersAsFlattenedList().OfType<ParcelLayer>().FirstOrDefault();
  string sTargetParcelType = "Tax";
  if (sTargetParcelType.Trim().Length == 0)
    return "";
  try
  {
    var typeNamesEnum = await myParcelFabricLayer.GetParcelPolygonLayerByTypeName(sTargetParcelType);
    if (typeNamesEnum.Count() == 0)
      return "Target parcel type " + sTargetParcelType + " not found. Please try again.";
    
    var featTargetLyr = typeNamesEnum.FirstOrDefault();
    var pRec = myParcelFabricLayer.GetActiveRecord();
    if (pRec == null)
      return "There is no Active Record. Please set the active record and try again.";
    
    var opMerge2 = new EditOperation()
    {
      Name = "Merge2",
      ProgressMessage = "Merging parcels...",
      ShowModalMessageAfterFailure = true,
      SelectNewFeatures = true,
      SelectModifiedFeatures = false
    };
    opMerge2.Merge(featTargetLyr, featSrcLyr, featSrcLyr.GetSelection().GetObjectIDs());
    opMerge2.Execute();
  }
  catch (Exception ex)
  {
    return ex.Message;  
  }
  return "";
  //When active record is set, and merging into the same parcel type, the original parcels are set historic with RetiredByRecord GUID field
  //When active record is set, the new merged parcel will be tagged with the Active record.
});
if (!string.IsNullOrEmpty(errorMessage))
  MessageBox.Show(errorMessage);

For another example and further information on parcel behavior triggered by the active record see the section below Integration with the Editing API.

You can write your code to check if there is an active record and if necessary prompt for it to be set. Here is code that does that:

// Check if there is an active record. If no active record then prompt user to set it or create it. 
// Otherwise, warn user that new features will not have an active record.
  var myParcelFabricLayer = MapView.Active.Map.GetLayersAsFlattenedList().OfType<ParcelLayer>().FirstOrDefault();
  var pRec = myParcelFabricLayer.GetActiveRecord();
  if (pRec == null)
  {
    System.Windows.MessageBox.Show("There is no Active Record. Please set the active record and try again.", "Merge Parcels");
    return;
  }

Quality driven workflows

The quality driven workflows are used for things like attribute edits such as correcting a parcel name, its stated area, or the attributed distance on a parcel’s boundary line. These quality driven workflows can also be geometry based such as re-aligning parcel boundaries. Quality driven workflows do not require an active record to be set and if an active record is set for the map, it will have no impact on the quality driven workflows.

Parcel types

The fabric has user-defined parcel types. Each parcel type is represented by a polygon feature class and a polyline feature class. A parcel type is created with a given name such as 'Tax' or 'Lot', for example, and the parcel type is represented in the fabric as a polygon feature class and polyline feature class.

If polygons in a parcel type exist without corresponding lines around its perimeter, then you use a Build function to create the lines for the parcels. Similarly, if there are lines in a parcel type that form loops but that do not enclose a parcel, then you can build parcel polygons from the parcel lines. The fabric also has points. The points are shared between parcel types so a parcel’s points do not have a parcel type. Points are also assigned to the active record when they are created.

The list of parcel types controlled by a parcel fabric can be returned through the following code:

var myParcelFabricLayer = MapView.Active.Map.GetLayersAsFlattenedList().OfType<ParcelLayer>().FirstOrDefault();
IEnumerable<string> parcelTypeNames = await myParcelFabricLayer.GetParcelTypeNames();

In the map view a parcel type is represented by a line and polygon feature layer. By default these are presented in a group layer in the table of contents. There is also a sub-group layer called Historic that holds the historic lines and historic parcels for the same type. The historic layers are used to show the parcel features that have been retired by a later record in the parcel lineage.

In addition to being able to check if a given feature layer is controlled by a fabric, as described in the Parcel layer section above, you can also get the feature layer for a particular parcel type. For example, the following code returns the polygon feature layer for a parcel type called 'Lot' :

var myParcelFabricLayer = MapView.Active.Map.GetLayersAsFlattenedList().OfType<ParcelLayer>().FirstOrDefault();
var polygonLyrEnum = await myParcelFabricLayer.GetParcelPolygonLayerByTypeName("Lot");
if (polygonLyrEnum.Count() == 0)
  return;
var myLotParcelPolygonLyr = polygonLyrEnum.FirstOrDefault();

Note that the code above returns an IEnumerable for the polygon layer. The reason for this is to account for the possibility that the map view has multiple instances of feature layers for the same parcel type. This is not expected to be a common situation; the polygon and the line feature layers are most typically represented as a single instance for each within their respective type groups.

What are parcel seeds?

Conceptually, seeds are the preliminary geometries for parcels. The seeds are used to help the user work with the parcel by maintaining the parcel's attributes in a compact geometry that is easy to move and place while in its un-built state. A seed is usually a small circular polygon geometry that is inside a closed loop of lines.

A seed is a row in the feature class table of a parcel type’s polygon feature class.

You can modify the attributes of the parcel seed or move it to a different closed loop lines. During the build operation the geometry of the seed is modified to become a topologically complete parcel that fills the space enclosed by the parcel lines.

The following code reduces an existing parcel back into a seed and, as part of the same edit operation, alters the original parcel name:

var MyParcelEditToken = editOper.ShrinkParcelsToSeeds(myParcelFabricLayer, sourceParcelFeatures);
editOper.Execute();
var editOperation2 = editOper.CreateChainedOperation();
Dictionary<string, object> ParcelAttributes = new Dictionary<string, object>();
var FeatSetModified = MyParcelEditToken.ModifiedFeatures;
string sReportResult = "";
if (FeatSetModified != null)
{
  foreach (KeyValuePair<MapMember, List<long>> kvp in FeatSetModified)
  {
    foreach (long oid in kvp.Value)
    {
      var dictInspParcelFeat = kvp.Key.Inspect(oid).ToDictionary(a => a.FieldName, a => a.CurrentValue);
      ParcelAttributes.Add("Name", "Seed for: " + dictInspParcelFeat["Name"]);
      editOperation2.Modify(kvp.Key, oid, ParcelAttributes);
      ParcelAttributes.Clear();
    }
    sReportResult += "There were " + kvp.Value.Count().ToString() + " " + kvp.Key.Name + " features modified." +
        Environment.NewLine;
  }
}
if (editOperation2.Execute())
  return sReportResult;

Note that the ParcelEditToken is used in the code above. More information about the ParcelEditToken follows in the section Using a parcel edit token.

Order of operations

There is an order dependency for parcel data creation when using record driven workflows. In general there should be a record created for the edit, and then that record should be set active before creating parcel seeds, or before creating other parcel lines or points. Here is an example workflow:

  1. Create Record – this is making a new Record feature in the Records feature class, and setting it as the active record.
  2. Copy lines from an existing selection of non-fabric features into a parcel type called “Lot” in a parcel fabric. Parcel seeds are created automatically.
  3. Build the active record – this fills out the parcel type’s polygons by "growing" the seeds, and also creates fabric points at the shared nodes.
  4. Create another new record, and set it active.
  5. Copy lines from an existing selection of “Lot” parcels into a parcel type called “Tax” in the same parcel fabric. Parcel seeds are created automatically.
  6. Build the active record.

Get the Active Record

The current active record can be accessed as follows:

protected async override void OnClick()
{
  string errorMessage = await QueuedTask.Run( () =>
  {
    var myParcelFabricLayer = MapView.Active.Map.GetLayersAsFlattenedList().OfType<ParcelLayer>().FirstOrDefault();
    var theActiveRecord = myParcelFabricLayer.GetActiveRecord();
    if (theActiveRecord == null)
      return "There is no Active Record. Please set the active record and try again.";
    return "";
  });
  if (!string.IsNullOrEmpty(errorMessage))
    MessageBox.Show(errorMessage);
}

Set the Active Record

A record with a given name can be found and set as the active record as follows:

string errorMessage = await QueuedTask.Run( async () =>
{
  var myParcelFabricLayer = MapView.Active.Map.GetLayersAsFlattenedList().OfType<ParcelLayer>().FirstOrDefault();
  //if there is no fabric in the map then bail
  if (myParcelFabricLayer == null)
    return "Please add a parcel fabric to the map.";
  string sExistingRecord = "MyRecordName";
  if (!await myParcelFabricLayer.SetActiveRecordAsync(sExistingRecord))
  {
    myParcelFabricLayer.ClearActiveRecord();
    return "Record with that name not found.";
  }
  return "";
});
if (!string.IsNullOrEmpty(errorMessage))
  MessageBox.Show(errorMessage);

Creating a new Record

A new parcel fabric record can be created and set as the active record as follows:

string errorMessage = await QueuedTask.Run( async () =>
{
  Dictionary<string, object> RecordAttributes = new Dictionary<string, object>();
  string sNewRecord = "MyRecordName";
  var myParcelFabricLayer = MapView.Active.Map.GetLayersAsFlattenedList().OfType<ParcelLayer>().FirstOrDefault();
  //if there is no fabric in the map then bail
  if (myParcelFabricLayer == null)
    return "Please add a parcel fabric to the map.";
  try
  {
    var recordsLayer = await myParcelFabricLayer.GetRecordsLayerAsync();
    var editOper = new EditOperation()
    {
      Name = "Create Parcel Fabric Record",
      ProgressMessage = "Create Parcel Fabric Record...",
      ShowModalMessageAfterFailure = true,
      SelectNewFeatures = false,
      SelectModifiedFeatures = false
    };
    RecordAttributes.Add("Name", sNewRecord);
    editOper.CreateEx(recordsLayer.FirstOrDefault(), RecordAttributes);
    if (!editOper.Execute())
      return editOper.ErrorMessage;
    await myParcelFabricLayer.SetActiveRecordAsync(sNewRecord);
  }
  catch (Exception ex)
  {
    return ex.Message;
  }
  return "";
});
if (!string.IsNullOrEmpty(errorMessage))
  MessageBox.Show(errorMessage);

Integration with the editing API

Adding new features to a parcel type's polygon feature class or to a parcel type's line feature class has specific behavior when a record is active. New parcel type features that are created will have the guid that is associated with the record stored in the parcel feature's CreatedByRecord field. Similarly, parcel point features and connection line features will have the active record guid assigned when they are created by the standard editing API. This is done automatically and does not require additional code. The code below adds a parcel point but does not proceed with the edit until the user has specified an active record. This ensures that the parcel fabric behavior is in effect. The code snippet below could be enhanced to prompt the user to create or choose a record using code similar to that from the previous section, "Creating a new Record". A new parcel point can be added to a fabric as follows:

string errorMessage = await QueuedTask.Run( async () =>
{
  var lic = ArcGIS.Core.Licensing.LicenseInformation.Level;
  if (lic < ArcGIS.Core.Licensing.LicenseLevels.Standard)
    return "Insufficient License Level.";
  //make sure there is a fabric and an active record
  var myParcelFabricLayer = MapView.Active.Map.GetLayersAsFlattenedList().OfType<ParcelLayer>().FirstOrDefault();
  if (myParcelFabricLayer == null)
    return "Please select a parcel fabric layer in the table of contents.";
  try
  {
    var pRec = myParcelFabricLayer.GetActiveRecord();
    if (pRec == null)
      return "There is no Active Record. Please set the active record and try again.";
    var pointLyrEnum = await myParcelFabricLayer.GetPointsLayerAsync();
    if (pointLyrEnum.Count() == 0)
      return "No point layer found.";
    var pointLyr = pointLyrEnum.FirstOrDefault();
    Dictionary<string, object> PointAttributes = new Dictionary<string, object>();
    var newPoint = MapView.Active.Extent.Center;
    if (newPoint == null)
      return "";
    PointAttributes.Add("Name", "MyTestPoint");
    PointAttributes.Add("IsFixed", 1);
    var editOper = new EditOperation()
    {
      Name = "Create a test parcel fabric point",
      ProgressMessage = "Create a test parcel fabric point...",
      ShowModalMessageAfterFailure = true,
      SelectNewFeatures = true,
      SelectModifiedFeatures = false
    };
    editOper.CreateEx(pointLyr, newPoint, PointAttributes);
    if (!editOper.Execute())
      return editOper.ErrorMessage;
  }
  catch (Exception ex)
  {
    errorMessage=ex.Message;
  }
  return "";
});
if (!string.IsNullOrEmpty(errorMessage))
  MessageBox.Show(errorMessage, "Add Control Point");

Other editing functions will also apply parcel behavior when an active record has been set. For example the following code will merge selected features from a source layer into a target layer. This code is the same regardless of whether standard polygon features are being merged or if the features are a parcel type's polygon features. However, if the features are parcels and the active record is set, then parcel fabric behavior is applied. Note that the following code block does not have any parcel specific API functions. The parcel behavior that results is described further after the code block:

var opMerge = new EditOperation()
{
  Name = "Merge",
  ProgressMessage = "Merging parcels...",
  ShowModalMessageAfterFailure = true,
  SelectNewFeatures = true,
  SelectModifiedFeatures = false
};
opMerge.Merge(featLyrTarget, featLyrSource, featLyrSource.GetSelection().GetObjectIDs());
opMerge.Execute();

When the code above is executed on parcel polygon features and the source and target layers are different parcel types, and there is an active record, then the following attribute edits are applied automatically, without any additional code required:

  • the newly created merged parcel has its CreatedByRecordfield value updated with the active record's guid.
  • the newly created merged parcel has its StatedArea field value updated with the sum of the stated area values of the original selected parcels.

When the code above is executed on parcel polygon features and the source and target layers are the same parcel type, then parcel lineage is also captured with the following additional attribute edit:

  • the existing source parcels have their RetiredByRecordfield value updated with the active record's guid.

Creating new parcels

Parcels can be created in the fabric through code such as the approach in the example above for point creation. The parcel polygon feature and the parcel line features can be created directly in this way. The parcel fabric API also has additional methods for creating parcels. The following sections provide some code examples of these.

Copy standard line features into a parcel type

In addition to copying the source lines into the parcel type's line, the following code will also create parcel seeds within the detected closed-line loops:

editOper.CopyLineFeaturesToParcelType(srcStandardLineFeatureLayer, StandardLineObjectIds, 
              destParcelTypeLineLayer, destParcelTypePolygonLayer);
editOper.Execute();

Copy parcel lines into a parcel type

In addition to copying the source parcel's lines into the target parcel type's line layer, the following code will also create parcel seeds at the centroid locations of the original source parcel polygons:

var ids = new List<long>(srcParcelTypePolygonFeatLyr.GetSelection().GetObjectIDs());
var kvp = new KeyValuePair<MapMember, List<long>>(srcParcelTypePolygonFeatLyr, ids);
var sourceParcelPolygonFeatures = new List<KeyValuePair<MapMember, List<long>>> { kvp };
editOper.CopyParcelLinesToParcelType(myParcelFabricLayer, sourceParcelPolygonFeatures, 
             destParcelTypeLineLayer, destParcelTypePolygonLayer, true, false, true);
editOper.Execute();

The last two parameters, useSourceLineAttributes and useSourcePolygonAttributes specify whether or not to copy across the values of extended attributes from the source features to the target features. The field mapping is done automtically based on name matching. The regular parcel fabric schema field values for attributes like direction and distance are always carried across regardless of how these last two parameters are set.

Create parcel seeds for closed loops of parcel lines

var myParcelFabricLayer = 
  MapView.Active.Map.GetLayersAsFlattenedList().OfType<ParcelLayer>().FirstOrDefault();
if (myParcelFabricLayer == null)
{
  MessageBox.Show("Please add a parcel fabric to the map.", "Create Parcel Seeds");
  return;
}
string errorMessage = await QueuedTask.Run( async () =>
{
  try
  {
      var pRec = myParcelFabricLayer.GetActiveRecord();
      if (pRec == null)
        return "There is no Active Record. Please set the active record and try again.";
      var guid = pRec.Guid;
      var editOper = new EditOperation()
      {
        Name = "Create Parcel Seeds",
        ProgressMessage = "Create Parcel Seeds...",
        ShowModalMessageAfterFailure = true,
        SelectNewFeatures = true,
        SelectModifiedFeatures = false
      };
    List<FeatureLayer> parcelLayers = new List<FeatureLayer>();
    List<string> sParcelTypes = new List<string> { "Tax", "Lot" };
    foreach (string sParcTyp in sParcelTypes)
    {
      IEnumerable<FeatureLayer> lyrs = await myParcelFabricLayer.GetParcelPolygonLayerByTypeName(sParcTyp);
      parcelLayers.Add(lyrs.FirstOrDefault());
      lyrs = await myParcelFabricLayer.GetParcelLineLayerByTypeName(sParcTyp);
      parcelLayers.Add(lyrs.FirstOrDefault());
    }
    editOper.CreateParcelSeedsByRecord(myParcelFabricLayer, guid, MapView.Active.Extent, parcelLayers);
    if (!editOper.Execute())
      return editOper.ErrorMessage;
  }
  catch (Exception ex)
  {
    return ex.Message;
  }
  return "";
});
if (!string.IsNullOrEmpty(errorMessage))
  MessageBox.Show(errorMessage, "Create Parcel Seeds");

Build parcels from parcel seeds

await QueuedTask.Run( () =>
{
  var myParcelFabricLayer = MapView.Active.Map.GetLayersAsFlattenedList().OfType<ParcelLayer>().FirstOrDefault();
  if (myParcelFabricLayer == null)
    return;
  try
  {
    var theActiveRecord = myParcelFabricLayer.GetActiveRecord();
    var guid = theActiveRecord.Guid;
    var editOper = new EditOperation()
    {
      Name = "Build Parcels",
      ProgressMessage = "Build Parcels...",
      ShowModalMessageAfterFailure = true,
      SelectNewFeatures = true,
      SelectModifiedFeatures = true
    };
    editOper.BuildParcelsByRecord(myParcelFabricLayer, guid);
    editOper.Execute();
  }
  catch
  {
    return;
  }
});

Also related to this code, note that it is possible to reduce parcels back to seeds. For more on this see topic below, Shrink parcels to seeds.

Duplicate parcels

You can duplicate parcels from multiple source types into a single target parcel type. The following code example uses just a single parcel type as a source in the List of KeyValuePair. Additional KeyValuePair items can be added to the List for duplicating from additional source parcel types.

var myParcelFabricLayer = 
  MapView.Active.Map.GetLayersAsFlattenedList().OfType<ParcelLayer>().FirstOrDefault();
if (myParcelFabricLayer == null)
{
  MessageBox.Show("Please add a parcel fabric to the map.", "Duplicate Parcels");
  return;
}
string sTargetParcelType = "Tax";
string errorMessage = await QueuedTask.Run( async () =>
{
  try
  {
    //get the feature layer that's selected in the table of contents
    var sourceParcelTypePolygonLayer = 
      MapView.Active.GetSelectedLayers().OfType<FeatureLayer>().FirstOrDefault();
    string parcelPolygonType = sourceParcelTypePolygonLayer.Name; //assumes layer name matches parcel type name 
    var ids = new List<long>(sourceParcelTypePolygonLayer.GetSelection().GetObjectIDs());
    if (ids.Count == 0)
      return "No selected " + parcelPolygonType + " parcels found. Please select parcels and try again.";
    var targetFeatLyrEnum = await myParcelFabricLayer.GetParcelPolygonLayerByTypeName(sTargetParcelType);
    if (targetFeatLyrEnum.Count()== 0)
      return "No parcel type " + sTargetParcelType + " found.";
    var targetFeatLyr = targetFeatLyrEnum.FirstOrDefault();
    if (targetFeatLyr == null)
      return "";
    var kvp00 = new KeyValuePair<MapMember, List<long>>(sourceParcelTypePolygonLayer, ids);
    var sourceFeatures = new List<KeyValuePair<MapMember, List<long>>> { kvp00 };
    var pRec = myParcelFabricLayer.GetActiveRecord();
    if (pRec == null)
      return "There is no Active Record. Please set the active record and try again.";
    var editOper = new EditOperation()
    {
      Name = "Duplicate Parcels",
      ProgressMessage = "Duplicate Parcels...",
      ShowModalMessageAfterFailure = true,
      SelectNewFeatures = true,
      SelectModifiedFeatures = false
    };
    editOper.DuplicateParcels(myParcelFabricLayer, sourceFeatures, pRec, targetFeatLyr);
    if (!editOper.Execute())
      return editOper.ErrorMessage;
  }
  catch (Exception ex)
  {
    return ex.Message;
  }
  return "";
});
if (!string.IsNullOrEmpty(errorMessage))
  MessageBox.Show(errorMessage, "Duplicate Parcels");

Updating parcels

Parcels can be directly updated in the fabric through code using the standard editing API. The parcel fabric API also has additional methods for updating parcels. The following sections and code snippets provide some examples of these.

Assign selected parcel features to the active record

var myParcelFabricLayer =
  MapView.Active.Map.GetLayersAsFlattenedList().OfType<ParcelLayer>().FirstOrDefault();
if (myParcelFabricLayer == null)
  MessageBox.Show("Please select a parcel fabric layer in the table of contents.","Assign Features To Record");
string errorMessage = await QueuedTask.Run( async () =>
{
  //check for selected layer
  if (MapView.Active.GetSelectedLayers().Count == 0)
    return "Please select a source feature layer in the table of contents.";
  //get the feature layer that's selected in the table of contents
  var srcFeatLyr = MapView.Active.GetSelectedLayers().OfType<FeatureLayer>().FirstOrDefault();
  bool bIsControlledByFabric = await srcFeatLyr.IsControlledByParcelFabric(ParcelFabricType.ParcelFabric);
  if (!bIsControlledByFabric)
    return "Please select a parcel fabric layer in the table of contents.";
  try
  {
    var pRec = myParcelFabricLayer.GetActiveRecord();
    if (pRec == null)
      return "There is no Active Record. Please set the active record and try again.";
    var ids = new List<long>(srcFeatLyr.GetSelection().GetObjectIDs());
    var kvp00 = new KeyValuePair<MapMember, List<long>>(srcFeatLyr, ids);
    var sourceFeatures = new List<KeyValuePair<MapMember, List<long>>> { kvp00 };
    var editOper = new EditOperation()
    {
      Name = "Assign Features to Record",
      ProgressMessage = "Assign Features to Record...",
      ShowModalMessageAfterFailure = true,
      SelectNewFeatures = true,
      SelectModifiedFeatures = false
    };
    editOper.AssignFeaturesToRecord(myParcelFabricLayer, sourceFeatures, pRec);
    if (!editOper.Execute())
      return editOper.ErrorMessage;
  }
  catch (Exception ex)
  {
    errorMessage= ex.Message;
  }
  return "";
});
if (!string.IsNullOrEmpty(errorMessage))
  MessageBox.Show(errorMessage, "Assign Features To Record");

History: setting parcels historic or current

var myParcelFabricLayer = 
  MapView.Active.Map.GetLayersAsFlattenedList().OfType<ParcelLayer>().FirstOrDefault();
if (myParcelFabricLayer == null)
{
  System.Windows.MessageBox.Show("There is no parcel layer in the map.", "Set Parcels Historic");
  return;
}
string errorMessage = await QueuedTask.Run( async () =>
{
  try
  {
    FeatureLayer destPolygonL = null;
    //test to make sure the layer is a parcel type, is non-historic, and has a selection
    bool bFound = false;
    var ParcelTypesEnum = await myParcelFabricLayer.GetParcelTypeNames();
    foreach (FeatureLayer mapFeatLyr in 
      MapView.Active.Map.GetLayersAsFlattenedList().OfType<FeatureLayer>())
    { 
      foreach (string ParcelType in ParcelTypesEnum)
      {
        var layerEnum = await myParcelFabricLayer.GetParcelPolygonLayerByTypeName(ParcelType);
        foreach (FeatureLayer flyr in layerEnum)
        {
          if (flyr == mapFeatLyr)
          {
            bFound = mapFeatLyr.SelectionCount > 0;
            destPolygonL = mapFeatLyr;
            break;
          }
        }
        if (bFound) break;
      }
      if (bFound) break;
    }
    if (!bFound)
      return "Please select parcels to set as historic.";
    var theActiveRecord = myParcelFabricLayer.GetActiveRecord();
    if (theActiveRecord == null)
      return "There is no Active Record. Please set the active record and try again.";

    var ids = new List<long>(destPolygonL.GetSelection().GetObjectIDs()); 
    var kvp00 = new KeyValuePair<MapMember, List<long>>(destPolygonL, ids);
    var sourceFeatures = new List<KeyValuePair<MapMember, List<long>>> { kvp00 };
    var editOper = new EditOperation()
    {
      Name = "Set Parcels Historic",
      ProgressMessage = "Set Parcels Historic...",
      ShowModalMessageAfterFailure = true,
      SelectNewFeatures = true,
      SelectModifiedFeatures = false
    };
    editOper.SetParcelHistoryRetired(myParcelFabricLayer, sourceFeatures, theActiveRecord);
    if (!editOper.Execute())
      return editOper.ErrorMessage;
  }
  catch (Exception ex)
  {
    return ex.Message;
  }
  return "";
});
if (!string.IsNullOrEmpty(errorMessage))
  MessageBox.Show(errorMessage, "Set Parcels Historic");

Change parcel types

var myParcelFabricLayer = 
  MapView.Active.Map.GetLayersAsFlattenedList().OfType<ParcelLayer>().FirstOrDefault();
if (myParcelFabricLayer == null)
{
  MessageBox.Show("Please add a parcel fabric to the map.", "Change Parcel Type");
  return;
}
string sSourceParcelType = "Tax";
string sTargetParcelType = "Lot";
string errorMessage = await QueuedTask.Run( async () =>
{
  try
  {
    var sourcePolygonLotTypeEnum = 
      await myParcelFabricLayer.GetParcelPolygonLayerByTypeName(sSourceParcelType);
    var sourcePolygonLotTypeLayer = sourcePolygonLotTypeEnum.FirstOrDefault();
    Dictionary<string, object> RecordAttributes = new Dictionary<string, object>();
    var recordsLayerEnum = 
      await myParcelFabricLayer.GetRecordsLayerAsync();
    var recordsLayer = recordsLayerEnum.FirstOrDefault();
    var targetParcelTypeFeatLyrEnum = 
      await myParcelFabricLayer.GetParcelPolygonLayerByTypeName(sTargetParcelType);
    var targetParcelTypeFeatLyr = targetParcelTypeFeatLyrEnum.FirstOrDefault();
    if (myParcelFabricLayer == null || sourcePolygonLotTypeLayer == null)
      return "";
    var ids = new List<long>(sourcePolygonLotTypeLayer.GetSelection().GetObjectIDs());
    var kvp00 = new KeyValuePair<MapMember, List<long>>(sourcePolygonLotTypeLayer, ids);
    var sourceFeatures = new List<KeyValuePair<MapMember, List<long>>> { kvp00 };
    var opCpToPT = new EditOperation()
    {
      Name = "Change Parcel Type",
      ProgressMessage = "Change Parcel Type...",
      ShowModalMessageAfterFailure = true,
      SelectNewFeatures = true,
      SelectModifiedFeatures = false
    };
    opCpToPT.ChangeParcelType(myParcelFabricLayer, sourceFeatures, targetParcelTypeFeatLyr);
    if (!opCpToPT.Execute())
      return opCpToPT.ErrorMessage;
  }
  catch (Exception ex)
  {
    return ex.Message;
  }
  return "";
});
if (!string.IsNullOrEmpty(errorMessage))
  MessageBox.Show(errorMessage, "Change Parcel Type");

Shrink parcels to seeds

As part of a quality driven workflow, it is useful to be able to reduce existing built parcels back down to seeds so that geometry edits can be made on the bounding lines without requiring topology maintenance on the geometry if the polygon as well. The parcel seeds are then re-built after the line geometry edits are done. The Shrink To Seeds command is used to convert existing parcels into seeds. The API for this function is demonstrated here:

string errorMessage = await QueuedTask.Run( async () =>
{
  var myParcelFabricLayer = 
    MapView.Active.Map.GetLayersAsFlattenedList().OfType<ParcelLayer>().FirstOrDefault();
  if (myParcelFabricLayer == null)
    return "Please add a parcel fabric to the map.";
  try
  {
    FeatureLayer parcelPolygonLyr = null;
    //find the first layer that is a polygon parcel type, is non-historic, and has a selection
    bool bFound = false;
    var ParcelTypesEnum = await myParcelFabricLayer.GetParcelTypeNames();
    foreach (FeatureLayer mapFeatLyr in 
      MapView.Active.Map.GetLayersAsFlattenedList().OfType<FeatureLayer>())
    {
      foreach (string ParcelType in ParcelTypesEnum)
      {
        var layerEnum = await myParcelFabricLayer.GetParcelPolygonLayerByTypeName(ParcelType);
        foreach (FeatureLayer flyr in layerEnum)
        {
          if (flyr == mapFeatLyr)
          {
            bFound = mapFeatLyr.SelectionCount > 0;
            parcelPolygonLyr = mapFeatLyr;
            break;
          }
        }
        if (bFound) break;
      }
      if (bFound) break;
    }
    if (!bFound)
      return "Please select parcels to shrink to seeds.";
    var editOper = new EditOperation()
    {
      Name = "Shrink Parcels To Seeds",
      ProgressMessage = "Shrink Parcels To Seeds...",
      ShowModalMessageAfterFailure = true,
      SelectNewFeatures = true,
      SelectModifiedFeatures = false
    };
    var ids = new List<long>(parcelPolygonLyr.GetSelection().GetObjectIDs());
    var kvp00 = new KeyValuePair<MapMember, List<long>>(parcelPolygonLyr, ids);
    var sourceParcelFeatures = new List<KeyValuePair<MapMember, List<long>>> { kvp00 };
    editOper.ShrinkParcelsToSeeds(myParcelFabricLayer, sourceParcelFeatures);
    if (!editOper.Execute())
      return editOper.ErrorMessage;
  }
  catch (Exception ex)
  {
    return ex.Message;
  }
  return "";
});
if (!string.IsNullOrEmpty(errorMessage))
  MessageBox.Show(errorMessage, "Shrink Parcels To Seeds");

Deleting parcels

The standard delete method from the editing assembly is one valid approach for removing features from the parcel fabric. The DeleteParcels method does additional work that is specific to the parcel information model. Using this method, the parcels’ non-shared lines and points are also deleted.

await QueuedTask.Run( () =>
{
  // check for selected layer
  if (MapView.Active.GetSelectedLayers().Count == 0)
  {
    MessageBox.Show("Please select a source feature layer in the table of contents", "Delete Parcel");
    return;
  }
  //first get the feature layer that's selected in the table of contents
  var theParcelFeatLyr = MapView.Active.GetSelectedLayers().OfType<FeatureLayer>().First();
  if (theParcelFeatLyr == null)
    return;
  var ids = new List<long>(theParcelFeatLyr.GetSelection().GetObjectIDs());
  if (ids.Count == 0)
    return;
  try
  {
    var editOper = new EditOperation()
    {
      Name = "Delete Parcels",
      ProgressMessage = "Delete Parcels...",
      ShowModalMessageAfterFailure = true,
      SelectNewFeatures = false,
      SelectModifiedFeatures = false
    };
    editOper.DeleteParcels(theParcelFeatLyr, ids);
    editOper.Execute();
  }
  catch
  {
    return;
  }
});

Using a parcel edit token

When using the parcel API functions to create or update parcel features, it is often useful to be able to access those features for further action. The ParcelEditToken can be used for this purpose. If this action includes edits of the features within the same edit operation then a chained edit operation must be used. For more information about chaining edit operations see the Editing API topic, Chaining Edit Operations.

The two patterns of using the function are:

  1. var peToken = editOper.BuildParcelsByRecord(myParcelFabricLayer, guid, parcelLayers);
  2. editOper.BuildParcelsByRecord(myParcelFabricLayer, guid);

If you don’t specify anything for the third parameter, then you receive an empty ParcelEditToken, with Count =0 for both modified and created features.

The parcelLayers that you pass in are the ones that you are interested in working with thereafter.

In the following code snippets the parcel edit token is used to do some additional work on the parcel features that are created or modified after executing BuildParcelByRecord.

The token is acquired by declaring a return variable for the EditOperation and it also requires that the IEnumerable parcel feature layers be provided on the (otherwise optional) third parameter:

var MyParcelEditToken = editOper.BuildParcelsByRecord(myParcelFabricLayer, guid, parcelLayers)

The edit operation is then executed and a variable declared for the set of modified features and another is declared for the created features.

editOper.Execute();
var FeatSetCreated = MyParcelEditToken.CreatedFeatures;
var FeatSetModified = MyParcelEditToken.ModifiedFeatures;

The chained edit operation is created and the additional work is done as follows:

var editOperation2 = editOper.CreateChainedOperation();
string sReportResult="";
Dictionary<string, object> ParcelAttributes = new Dictionary<string, object>();
foreach (KeyValuePair<MapMember, List<long>> kvp in FeatSetModified)
{
  foreach (long oid in kvp.Value)
  {
    ParcelAttributes.Add("Name", "My" + kvp.Key.Name + " " + oid.ToString());
    editOperation2.Modify(kvp.Key, oid, ParcelAttributes);
    ParcelAttributes.Clear();
  }
  sReportResult += "There were " + kvp.Value.Count().ToString() + " " + kvp.Key.Name + " features modified." +
      Environment.NewLine;
}
foreach (KeyValuePair<MapMember, List<long>> kvp in FeatSetCreated)
{
  foreach (long oid in kvp.Value)
  {
    //Do things with the created features
  }
  sReportResult += "There were " + kvp.Value.Count().ToString() + " new " + kvp.Key.Name + " features created." +
      Environment.NewLine;
}
if ( editOperation2.Execute())
  return sReportResult;

Note that not all the of the Parcel API functions will return both Modified as well as Created features in the returned ParcelEditToken. The following table lists each of the functions, and if it is possible for its parcel edit token to carry modified features, created features, or both.

Parcel API Function Name Supports created features? Supports modified features?
AssignFeaturesToRecord
BuildParcelsByExtent
BuildParcelsByRecord
ChangeParcelType
CopyParcelLinesToParcelType
CopyLineFeaturesToParcelType
CreateParcelSeedsByRecord
DeleteParcels -- --
DuplicateParcels
RetireFeaturesToRecord
SetParcelHistoryRetired
SetParcelHistoryCurrent
ShrinkParcelsToSeeds
⚠️ **GitHub.com Fallback** ⚠️