2

I am currently writing a set of batch processing jobs using ArcObjects in C#.NET. I noticed that a lot of ESRI examples pile the code into one static class with each member declared inside each method scope.

I am considering using either a factory pattern or a singleton pattern for processing features. The intent is to wrap the unmanaged interfaces in instance classes to compartmentalize the code a bit and help with re-usability. These classes implement the IDisposable interface to help make sure everything is properly cleaned up after use.

Considering what you know about COM objects, could you please let me know if the following class looks effective? Is my expectation of reusability reasonable considering how COM works? I.e., If I encapsulate a COM member, will it provide the same benefit as a manged type, or is this adding unnecessary overhead? As far as I know, the only managed type I am using is the Geoprocessor class, IFeatureClass is unmanaged.

public class FeatureProcessing : ProcessBase, IDisposable
{
    #region Fields

    private IFeatureClass _FeatureClassObj;

    private ITrackCancel2 _GPTrackCancel;

    private Geoprocessor _GeoProcessor;

    private bool _IsRunning = false;

    #endregion Fields

    #region Properties

    public IFeatureClass FeatureClassObj
    {
        get
        {
            return _FeatureClassObj;
        }
        private set
        {
            _FeatureClassObj = value;
        }
    }

    public bool IsRunning
    {
        get
        {
            return _IsRunning;
        }
    }

    #endregion Properties

    #region Constructor

    public FeatureProcessing(IFeatureClass feature)
    {
        _FeatureClassObj = feature;
    }

    #endregion Constructor

    #region Methods

    public void Simplify(int footTollerance = 5, SimplifyAlgorithmTypes algorythmType = SimplifyAlgorithmTypes.POINT_REMOVE)
    {
        //FeatureClass tmpFeatureClass = new FeatureClass(null);
        ESRI.ArcGIS.CartographyTools.SimplifyLine lineSimplify = new ESRI.ArcGIS.CartographyTools.SimplifyLine();
        lineSimplify.in_features = _FeatureClassObj;
        lineSimplify.out_feature_class = $"{_FeatureClassObj.AliasName}_Simplified";
        lineSimplify.tolerance = footTollerance;
        lineSimplify.algorithm = algorythmType.ToString();
        RunGeoprocessingOperation(lineSimplify);
    }


    private void RunGeoprocessingOperation(IGPProcess gpProc)
    {
        _GeoProcessor = new Geoprocessor();
        _GeoProcessor.OverwriteOutput = true;
        _GeoProcessor.ToolExecuting += Gp_ToolExecuting;
        _GeoProcessor.ToolExecuted += Gp_ToolExecuted;
        _GeoProcessor.ProgressChanged += Gp_ProgressChanged;
        IGeoProcessorResult2 gpResult = _GeoProcessor.Execute(gpProc, _GPTrackCancel) as
            IGeoProcessorResult2;
    }

    private void Gp_ToolExecuting(object sender, ToolExecutingEventArgs e)
    {
        if (e.GPResult == null) return;
        _IsRunning = true;
        IGeoProcessorResult2 result = e.GPResult as IGeoProcessorResult2;
        base.RaiseProcessEvent(ProcessEventTypes.INFO, $"GPTool: {result.Process.ToolName} started");
    }

    private void Gp_ToolExecuted(object sender, ToolExecutedEventArgs e)
    {
        _IsRunning = false;
    }

    private void Gp_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        base.RaiseProcessEvent(ProcessEventTypes.INFO, e.Message);
    }

    public void CancelRunningTask()
    {
        if (_IsRunning != false && _GPTrackCancel != null)
        {
            _GPTrackCancel.Cancel();
            _IsRunning = false;
        }
    }

    public void Dispose()
    {
        CancelRunningTask();
        ReleaseCOMObj();
    }

    private void ReleaseCOMObj()
    {
        int refsLeft = 0;
        if (_FeatureClassObj != null)
        {
            do { refsLeft = Marshal.ReleaseComObject(_FeatureClassObj); }
            while (refsLeft > 0);
        }
    }
    #endregion Methods
}
4

There are very few objects that ESRI suggests you need to explicitly release. See: How to release COM references

In general, aside from those objects ESRI explicitly says should be released, you don't really need to call Marshal.ReleaseComObject, and in fact doing so can cause serious issues.

The objects that people generally release, in the way you are asking about, are cursors and any enumerator that may be backed by a cursor. This is done since cursors are:

  • Generally a finite resource, and if created in a loop the GC might not be able to release out of scope objects and their cursors as fast as the loop creates them. Eventually the loop may not be able to create new cursors and an exception will be thrown. Once the code continues the GC will run and the out of scope cursors will be released, but from a UX pov you've already pissed off the user.
  • Well understood to never be cached internally by ArcObjects. The function that creates the cursor knows it has full ownership of the cursor.

The risk in just calling Marshal.FinalReleaseComObject, or looping on Marshal.ReleaseComObject, is that other code may still have a reference to the wrapped COM object. That code could be yours, other 3rd party dll's, or internal (cached) in ArcObjects.

So, general rules:

  1. Don't use Marshal.*ReleaseComObject unless you need to.
  2. If creating reusable classes (like you want to do) then start by only creating them for cursors and some enumerators.
  3. Never call Marshal.FinalReleaseComObject or loop on Marshal.ReleaseComObject. If you create once then release once. If you want to go nuts and create release wrappers for everything then at least follow this rule.

In your example there is a (good ?) chance the IFeatureClass you are forcably releasing is cached/referenced somewhere else. Your code would nuke it and result in "The RCW is separated from its underlying object" type errors when the other code tried to use its reference to the IFeatureClass.

Also read: Marshal.ReleaseComObject Considered Dangerous

  • 1
    Note: Since you explicitly state your use is for batch processing, you're probably ok doing what you suggest, but I still wouldn't unless testing showed I needed to. – Chris Kushnir Feb 9 '17 at 21:58
  • If I scrap the release logic in the dispose method, then am I still practicing good form here by assigning the IFeatureClass interface to a public property? If I replace this variable using the setter of FeatureClassObj, should I set the private field _FeatureClassObj to null before setting a new value? Basically I want the property to always point to the new output of each geoprocessing operation. – Andrew T Feb 10 '17 at 1:17
  • 1
    Depends on how FeatureProcessing is being created/used. set_FeatureClassObj is private, so it can only be set by the constructor. Begs the question why is get_FeatureClassObj public, is it ever used, or for completeness ? If you want to do something in the Dispose then instead of calling ReleaseCOMObj() you could just set FeatureClassObj, _GPTrackCancel, _GeoProcessor = null, however it isn't required. It _may help indicate to the GC that those objects are ready for collection a fraction of a second earlier, it can also help debugging. – Chris Kushnir Feb 10 '17 at 1:50
0

At a cursory glance this looks like it will work well.

I would change your releaseCOMObj and make it available more program wide:

public static void ReleaseCOMObj(object o)
{
    if (o != null && System.Runtime.InteropServices.Marshal.IsComObject(o))
        Marshal.FinalReleaseComObject(o);
        //or Marshal.ReleaseComObject(o) 
}

Also get Simplify() out of there.

  • What's wrong with including the simplify() method in the processing class? – Andrew T Feb 10 '17 at 1:19
  • basically I want the class to perform geoprocessing operations on the IFeatureClass object it holds, then assign the output of each operation back to the public FeatureClassObj property. – Andrew T Feb 10 '17 at 1:25
  • @AndrewT I would remove Simplify because it is out of scope. Your class performs a generic geoprocessing operation and assigns the result back. Simplify is a specific geoprocessing operation. You mentioned using a factory for geoprocessing operations, so that is where this should be moved to. – danielm Feb 10 '17 at 15:10
  • @AndrewT Also, how are you dealing with operations that return multiple feature classes? – danielm Feb 10 '17 at 15:11
  • that makes more sense to me now. I also realize now that Simplify is indeed one example of an operation that outputs multiple datasets. I could potentially change the IFeatureClass layer to be a IFeatureClassContainer or the more generic IDatasetContainer type for more than one return, which would start scope creeping the purpose of the class a bit. It is bothersome that the COM objects leave very little in the way of compile-time metadata. – Andrew T Feb 10 '17 at 20:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.