Click here to get the sample associated with this walkthrough.
Tree feature example
Object Model diagram: click here.
Description: This feature is a subclass of a standard geodatabase Feature. It adds an interface with functions specific to trees.
Design: COM aggregation of an esriGeodatabase Feature.
Categories: ESRI GeoObjects
Interfaces: ITreeFeature
How to use
-
Open and build the project Tree.dsp to register the DLL and register to component categories.
-
Open ArcMap and add the Trees feature class from the personal geodatabase in the Extending ArcObjects sample data. This class has been preconfigured to store custom tree features.
-
In the ArcMap VBA environment, click Tools, then click References, and browse to the example's DLL.
-
Run the 'TreeFeatureTest' VBA macro from the .bas file that accompanies the example.
Implementing your own interface
You can use custom features to implement your own interface to provide functionality that is specific to your data. Take for example a point feature class of trees. You might have a requirement to calculate the age of a tree based on its recorded planting date. If the trees were implemented as custom features you could define a new interface, ITreeFeature perhaps, with an Age property. The alternative is to provide a function located elsewhere that client developers can call; a good place would be on a feature class extension.
In this example, a tree is a feature with an extra property to return the tree's age.
In the case of a custom feature, its use from a client would look something like this:
In the case of a class extension, the client would be more like the following:
Although the custom feature solution results in more elegant coding, there is no clear benefit apart from the fact that the custom feature can be developed against in the same way as a standard feature.
Consider the esriCarto DimensionFeature. The functionality of dimensions could probably be produced with class extensions, but they fit better into the ArcGIS object model as kinds of features, and accordingly, developers can use them more simply.
Considering the extra development complexity in general, the custom feature approach for adding interfaces is only recommended when you strongly prefer to have developers use the extra functionality directly on the feature.
Handling aggregation
You may find the ESRI CASE tools useful when designing and implementing custom features. In particular, the Code Generation Wizard will create an ATL-based Visual C++ project with stubbed out methods for your custom feature.
For more details of the CASE tools, see Building a Geodatabase, and also Geodatabase Modeling with UML .
To implement a custom feature, you must aggregate the existing Feature coclass. Of course, you could implement a custom nonspatial object in the same way by aggregating the existing Object coclass.
The object to be aggregated is known as the inner object. When your object is created, you cocreate a new instance of the inner object and keep a reference to its IUnknown interface; this is referred to as the inner unknown, since of course your object, the outer, also has an IUnknown interface.
[VCPP]
HRESULT CTreeFeature::FinalConstruct()
{
HRESULT hr;
IUnknown *pOuter = GetControllingUnknown();
// Aggregate in ESRI's simple Feature object
hr = CoCreateInstance(CLSID_Feature, pOuter, CLSCTX_INPROC_SERVER,
IID_IUnknown, (void **) &m_pInnerUnk)))if (FAILED(hr))return E_FAIL;
The clever part of how aggregation works is in the handling of QueryInterface calls. The outer object, on encountering a request for an interface that it doesn't implement directly, will forward the request to the inner object.
When subsequently another call to QueryInterface is made, the inner object will forward the request to the outer object (note that a reference to the outer unknown is given to the inner object when it is created). In this way it appears to the client as though there is only one object that correctly implements a set of interfaces.
The interfaces are defined as usual in the ATL category map, except for those interfaces that are exposed directly from the inner object. There is a special macro to handle these interfaces as seen below.
[VCPP]
BEGIN_COM_MAP(CTreeFeature)COM_INTERFACE_ENTRY(ITreeFeature)COM_INTERFACE_ENTRY
(ISupportErrorInfo)COM_INTERFACE_ENTRY_AGGREGATE_BLIND(m_pInnerUnk)
END_COM_MAP()
In the macro above, the word blind indicates that the outer object is giving control to the inner object over which interfaces are exposed to the client. This means that if the esriGeodatabase Feature coclass implements extra interfaces in the future, your custom feature will also expose those extra interfaces.
Also, in the header file of your custom feature class, note the following line.
[VCPP]
DECLARE_GET_CONTROLLING_UNKNOWN()
This macro provides the GetControllingUnknown function that is used in the previously described FinalConstruct code. GetControllingUnknown guarantees to return the outermost unknown in a situation where there is nested aggregation. It is possible that another developer may want to aggregate your object. If you want to allow your object to be aggregated, it must be written with that in mind.
It is possible that your custom feature may be aggregated by other developers.
Fortunately, ATL makes this easy—you just choose to support aggregation on the ATL Object Wizard when creating your custom feature.
When developing a custom feature, you should be aware of an issue related to the inner and outer unknowns. Note that in the example, for convenience, a reference is kept to the IFeature interface on the inner object (since the example functionality is so simple, it isn't really necessary to keep this pointer, except for demonstrating this issue).
[VCPP]
hr = m_pInnerUnk->QueryInterface(IID_IFeature, (void **) &m_pFeature)if (FAILED
(hr))
return E_FAIL;
pOuter->Release();
Why is Release called, and moreover, why is it called on the outer unknown? You will note that m_pFeature has been declared as a normal pointer rather than a smart pointer.
[VCPP]
IFeature *m_pFeature;
This is to simplify the code for FinalRelease. There is no real need to have the reference count go above one, since the code is handling the lifetime events of the object being implemented. When m_pFeature is set up, a call to AddRef is automatically made on the object in question, in this case the inner object. Therefore, a call to Release is required to decrement the reference count. However the inner object is delegating all its IUnknown calls to the outer object, so the AddRef actually gets called on the outer unknown. This is why you must make the Release on the outer object.
For more about COM aggregation, refer to the bibliography .
Making your code efficient
Many of the implementation recommendations for class extensions also apply to custom features. For example, avoid references to applications such as ArcMap. Also, do not use user interface functions.
You should also strive to make your code as efficient as possible, particularly since users might deal with thousands of your custom features at a time. Note the implementation of get_Age in the example.
[VCPP]
IFieldsPtr ipFields;
hr = m_pFeature->get_Fields(&ipFields);
if (FAILED(hr))
return E_FAIL;
long lPlantedYearField;
hr = ipFields->FindField(L "YEAR_PLANTED", &lPlantedYearField);
if (FAILED(hr))
return E_FAIL;
if (lPlantedYearField == - 1)
{
AtlReportError(CLSID_TreeFeature, _T("Required YEAR_PLANTED field notfound "),
IID_ITreeFeature, E_FAIL);return E_FAIL;
}
The code to find the field will be executed for every feature. In a production environment, it would be better to additionally implement a class extension that cached the field position, and which the custom feature could call to avoid extra work.
With custom features, it is important to make your code as efficient as possible—users may deal with thousands of your features at a time.
See Also:
The example codeAbout custom features
Custom features versus other solutions
Making a class extension with your custom feature
Managing custom features
To use the code in this topic, reference the following assemblies in your Visual Studio project. In the code files, you will need using (C#) or Imports (VB .NET) directives for the corresponding namespaces (given in parenthesis below if different from the assembly name):
ESRI.ArcGIS.Geometry ESRI.ArcGIS.Geodatabase ESRI.ArcGIS.System (ESRI.ArcGIS.esriSystem)
Development licensing | Deployment licensing |
---|---|
ArcGIS for Desktop Standard | ArcGIS for Desktop Standard |
ArcGIS for Desktop Advanced | ArcGIS for Desktop Advanced |