Utility COM objects


In this topic


About utility COM objects

A geographic information system (GIS) server can  be extended to use application-specific utility Component Object Model (COM) objects that can be written in Visual Basic (VB), C++, or .NET. COM objects allow you to share your components and customizations across ArcGIS Server, ArcGIS Desktop, and ArcGIS Engine. Utility COM objects do not need to be tied to any particular server object configuration or type, and can be used in an empty server context. See the following illustration:



However, this strategy does have limitations. When extending the GIS server with a utility COM object, the object must be created at each request made on a particular server object or server context. This means that for pooled server objects, each time a request is made on a server object, the utility COM object must be created. If the COM object has a high initialization cost, this continual creation of the object can be prohibitive. Because the COM object is created for each request, you cannot use it to cache information as it is used. If these COM components are installed on the server object container (SOC) machines on which your server objects are hosted, they can be used with your application. For example, if you have a custom network tracing function that you want to use in your server application, you can write the tracing code as a COM object and install it on the server as shown in the following code example:
[C#]
mylib.TraceUtilities tracer = pServerContext.CreateObject("mylib.TraceUtilities")as
    mylib.TraceUtilities;
result = tracer.DoIsolationTrace(...)
[VB.NET]
Dim tracer As mylib.TraceUtilities = TryCast(pServerContext.CreateObject("mylib.TraceUtilities"), mylib.TraceUtilities)
result = tracer.DoIsolationTrace(...)
In this example, the tracing function can initiate thousands of ArcObjects calls; however, those calls occur in the server where the TraceUtilities COM object is running. The coarse-grained DoIsolationTrace method is the only method that the Web application needs to call, which means there is only a single remote object call to the server.
To illustrate this in more detail, the following code example shows an ASP.NET example in which a Web application includes a button (btnTotalAreas) that reports the total area of polygon features in one of the map layers that intersects the map extent and displays the result on a label on the Web form (lblResult).
In this example, the Web application includes a Map control (Map1) from which it gets the MapServer and its server context, and includes a TOC control (Toc1 ) that the user of the Web application uses to identify the layer to query.
To complete this process, a spatial query is made on the selected layer's feature class using the current map extent to return a geodatabase cursor. The application then loops through the cursor, gets each feature's geometry, and adds its area to the total.
[C#]
protected void btnTotalAreas_Click(object sender, EventArgs e)
{
    // Get a reference to the MapServer and its context from the Map control.
    MapFunctionality func = Map1.MapFunctionalities[0] as MapFunctionality;
    MapResource mr = func.Resource as MapResource;
    IMapServer map = mr.MapServer;
    MapInformation mi = mr.MapInformation as MapInformation;
    GISDataSource gdsrc = func.Resource.DataSource as GISDataSource;
    IServerContext ctx = gdsrc.ServerContext;
    IMapServerObjects mapobj = map as IMapServerObjects;
    IMap fgmap = mapobj.get_Map(mi.DataFrame);

    // Find the selected layer.
    IEnumLayer maplayers = fgmap.get_Layers(null, true);
    ILayer lyr;
    string sLayername = Toc1.SelectedNode.Text;

    while ((lyr = maplayers.Next()) != null)
    {
        if (lyr.Name == sLayername)
            break;
    }
    if (lyr == null)
    {
        lblResult.Text = "Layer not found";
        return ;
    }

    // Get the feature class and ensure its geometry type is polygon. 
    IFeatureLayer flyr = lyr as IFeatureLayer;
    IFeatureClass fc = flyr.FeatureClass;
    if (fc.ShapeType != esriGeometryType.esriGeometryPolygon)
    {
        lblResult.Text = "Select a polygon layer";
        return ;
    }

    // Create the query using the current map extent.
    ISpatialFilter sf = ctx.CreateObject("esriGeoDatabase.SpatialFilter")as
        ISpatialFilter;
    IMapArea ma = func.MapDescription.MapArea;
    sf.SpatialRel = esriSpatialRelEnum.esriSpatialRelIntersects;
    sf.GeometryField = fc.ShapeFieldName;
    sf.Geometry = ma.Extent as IGeometry;

    // Execute the query and loop through the results. 
    IFeature f;
    IArea area;
    double dTotalArea = 0.0;
    IFeatureCursor fcursor = fc.Search(sf, true);

    while ((f = fcursor.NextFeature()) != null)
    {
        area = f.Shape as IArea;
        dTotalArea += area.Area;
    }

    lblResult.Text = dTotalArea.ToString();

}
[VB.NET]
Protected Sub btnTotalAreas_Click(ByVal sender As Object, ByVal e As EventArgs)
' Get a reference to the MapServer and its context from the Map control.
Dim func As MapFunctionality = TryCast(Map1.MapFunctionalities(0), MapFunctionality)
Dim mr As MapResource = TryCast(func.Resource, MapResource)
Dim map As IMapServer = mr.MapServer
Dim mi As MapInformation = TryCast(mr.MapInformation, MapInformation)
Dim gdsrc As GISDataSource = TryCast(func.Resource.DataSource, GISDataSource)
Dim ctx As IServerContext = gdsrc.ServerContext
Dim mapobj As IMapServerObjects = TryCast(map, IMapServerObjects)
Dim fgmap As IMap = mapobj.get_Map(mi.DataFrame)
' Find the selected layer.
Dim maplayers As IEnumLayer = fgmap.get_Layers(Nothing, True)
Dim lyr As ILayer
Dim sLayername As String = Toc1.SelectedNode.Text
lyr = maplayers.Next()
Do While lyr IsNot Nothing
    If lyr.Name = sLayername Then
        Exit Do
    End If
    lyr = maplayers.Next()
Loop
If lyr Is Nothing Then
    lblResult.Text = "Layer not found"
    Return
End If
' Get the feature class and ensure its geometry type is polygon.
Dim flyr As IFeatureLayer = TryCast(lyr, IFeatureLayer)
Dim fc As IFeatureClass = flyr.FeatureClass
If fc.ShapeType <> esriGeometryType.esriGeometryPolygon Then
    lblResult.Text = "Select a polygon layer"
    Return
End If
' Create the query using the current map extent.
Dim sf As ISpatialFilter = TryCast(ctx.CreateObject("esriGeoDatabase.SpatialFilter"), ISpatialFilter)
Dim ma As IMapArea = func.MapDescription.MapArea
sf.SpatialRel = esriSpatialRelEnum.esriSpatialRelIntersects
sf.GeometryField = fc.ShapeFieldName
sf.Geometry = TryCast(ma.Extent, IGeometry)
' Execute the query and loop through the results.
Dim f As IFeature
Dim area As IArea
Dim dTotalArea As Double = 0.0
Dim fcursor As IFeatureCursor = fc.Search(sf, True)
f = fcursor.NextFeature()
Do While f IsNot Nothing
    area = TryCast(f.Shape, IArea)
    dTotalArea + = area.Area
    f = fcursor.NextFeature()
Loop
lblResult.Text = dTotalArea.ToString()
End Sub
This code makes approximately eight remote calls on ArcObjects running on the server to get the map from the MapServer, get the layers from the map, find the selected layer, verify that it is a polygon layer, then create the QueryFilter and set its properties. These fine-grained calls to the server do not take a significant amount of time. However, once the query is executed, the following calls are made to the server per feature:
  • One to get the feature
  • One to get the feature's geometry
  • One to get the area of the geometry
Because of the way this application is written, the number of features that can result from the query is indeterminate. The application could potentially loop through thousands of features. If there are 1,000 features intersecting the map extent, there will be 3,000 fine-grained calls to the server.
The cost of this number of fine-grained calls adds up and can cause the performance of your application to suffer. To minimize or eliminate the large number of remote calls, you can create a simple COM object that has a method that loops through the features and totals their areas. This aspect of the Web application can then be rewritten to call this single method, reducing the large number of remote calls to a single call to the method on the COM object.
The following code example shows how to write this COM object. In this example, the method of totaling the areas takes the feature class and the query filter as arguments. Notice the interface definition, the use of class attributes, and the fact that the COM class inherits from ServicedComponent.
[C#]
public interface IAreaSum
{
    double sumArea(ref IFeatureClass pFClass, ref IQueryFilter pQFilter);
}

namespace ServerUtilCS
{
    [AutomationProxy(true), ClassInterface(ClassInterfaceType.AutoDual)]
    public class ServerUtil: ServicedComponent, IAreaSum
    {
        public ServerUtil(){}

        public double sumArea(ref IFeatureClass pFClass, ref IQueryFilter pQFilter)
        {
            double dArea = 0;

            // Ensure these are polygon features.
            if (pFClass.ShapeType != esriGeometryType.esriGeometryPolygon)
                return dArea;

            // Loop through the features and total their areas.
            IFeature pFeature = null;
            IArea pArea = null;
            IFeatureCursor pFeatureCursor = pFClass.Search(pQFilter, true);
            while ((pFeature = pFeatureCursor.NextFeature()) != null)
            {
                pArea = pFeature.Shape as IArea;
                dArea += pArea.Area;
            }
            return dArea;
        }
    }
}
[VB.NET]
Public Interface IAreaSum

Function sumArea(ByRef pFClass As IFeatureClass, ByRef pQFilter As IQueryFilter) As Double
    End Interface
    
    <AutomationProxy(True), ClassInterface(ClassInterfaceType.AutoDual)> _
                     Public Class ServerUtil
        Inherits ServicedComponent
        Implements IAreaSum
        
        Public Sub New()
            MyBase.New()
        End Sub


        Public Function sumArea(ByRef pFClass As IFeatureClass, ByRef pQFilter As IQueryFilter) As Double Implements ServerUtilVBNET.IAreaSum.sumArea
            Dim dResult As Double = 0.0#
            
            ' Ensure these are polygon features.
            If pFClass.ShapeType <> esriGeometryType.esriGeometryPolygon Then sumArea = dResult
            
            ' Loop through the features and total their areas.
            Dim pFeature As IFeature = Nothing
            Dim pArea As IArea = Nothing
            Dim pFeatureCursor As IFeatureCursor = pFClass.Search(pQFilter, True)
            
            pFeature = pFeatureCursor.NextFeature
            Do Until pFeature Is Nothing
                pArea = pFeature.Shape
                dResult = dResult + pArea.Area
                pFeature = pFeatureCursor.NextFeature
            Loop
            
            sumArea = dResult
            
        End Function

    End Class
The method returns the total area of the features that satisfy the query. The return value is tailored to what the calling application (the Web application in this case) requires as an answer (the total area).

Distributing a utility COM object

After building the COM object, register the COM object's dynamic-link library (DLL) that generates an associated type library (.tlb) file. These two files must also be registered in a location on each SOC machine (or in a shared directory) that has read and write privileges for the ArcSOC container account user. For example, if the COM object is named MyObject, use the following syntax at the Visual Studio .NET command prompt:
regasm /tlb:MyObject.tlb /codebase MyObject.dll
In addition, the .NET application that consumes the COM object also needs a reference to the COM DLL to access type information for early binding. The benefits of early binding include strong type checking at compile time and auto-completion capabilities at design time. 

You can also move the interfaces defined and implemented in the utility COM object into another library and reference the interface library in the COM DLL. The interface library can be distributed to clients without including the business logic. The business logic included in the COM DLL always executes on the server (SOC machine).     
Once the COM object is installed on the SOC machine and on the Web server, you can use its functionality. The following code example shows how to create an instance of the COM object in the GIS server and call its methods:
[C#]
// Create a utility COM object on the server.
IAreaSum totarea = ctx.CreateObject("ServerUtil.ServerUtil")as IAreaSum;

IQueryFilter qf = sf as IQueryFilter;
double dTotalArea = totarea.sumArea(ref fc, ref qf);
...
[VB.NET]
' Create a utility COM object on the server.
Dim totarea As IAreaSum = ctx.CreateObject("ServerUtil.ServerUtil")

Dim qf As IQueryFilter = sf
Dim dTotalArea As Double = totarea.sumArea(fc, qf)
The following code example is what executes when the button is clicked:
[C#]
protected void btnTotalAreas_Click(object sender, EventArgs e)
{
    // Get a reference to the MapServer and its context from the Map control.
    MapFunctionality func = Map1.MapFunctionalities[0] as MapFunctionality;
    MapResource mr = func.Resource as MapResource;
    IMapServer map = mr.MapServer;
    MapInformation mi = mr.MapInformation as MapInformation;
    GISDataSource gdsrc = func.Resource.DataSource as GISDataSource;
    IServerContext ctx = gdsrc.ServerContext;
    IMapServerObjects mapobj = map as IMapServerObjects;
    IMap fgmap = mapobj.get_Map(mi.DataFrame);

    // Find the selected layer.
    IEnumLayer maplayers = fgmap.get_Layers(null, true);
    ILayer lyr;
    string sLayername = Toc1.SelectedNode.Text;

    while ((lyr = maplayers.Next()) != null)
    {
        if (lyr.Name == sLayername)
            break;
    }
    if (lyr == null)
    {
        lblResult.Text = "Layer not found";
        return ;
    }

    // Get the feature class and ensure its geometry type is polygon. 
    IFeatureLayer flyr = lyr as IFeatureLayer;
    IFeatureClass fc = flyr.FeatureClass;
    if (fc.ShapeType != esriGeometryType.esriGeometryPolygon)
    {
        lblResult.Text = "Select a polygon layer";
        return ;
    }

    // Create the query using the current map extent.
    ISpatialFilter sf = ctx.CreateObject("esriGeoDatabase.SpatialFilter")as
        ISpatialFilter;
    IMapArea ma = func.MapDescription.MapArea;
    sf.SpatialRel = esriSpatialRelEnum.esriSpatialRelIntersects;
    sf.GeometryField = fc.ShapeFieldName;
    sf.Geometry = ma.Extent as IGeometry;

    // Create a utility COM object on the server.
    IAreaSum totarea = ctx.CreateObject("ServerUtil.ServerUtil")as IAreaSum;
    IQueryFilter qf = sf as IQueryFilter;
    double dTotalArea = totarea.sumArea(ref fc, ref qf);
    lblResult.Text = dTotalArea.ToString();

}
[VB.NET]
Protected Sub btnTotalAreas_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnTotalAreas.Click
' Get a reference to the MapServer and its context from the Map control.
Dim func As MapFunctionality = Map1.MapFunctionalities(0)
Dim mr As MapResource = func.Resource
Dim map As IMapServer = mr.MapServer
Dim mi As MapInformation = mr.MapInformation
Dim gdsrc As GISDataSource = func.Resource.DataSource
Dim ctx As IServerContext = gdsrc.ServerContext
Dim mapobj As IMapServerObjects = map
Dim fgmap As IMap = mapobj.Map(mi.DataFrame)

' Find the selected layer.
Dim maplayers As IEnumLayer = fgmap.Layers(Nothing, True)
Dim sLayername As String = Toc1.SelectedNode.Text
Dim lyr As ILayer = maplayers.Next()
While (Not lyr Is Nothing)
    If lyr.Name = sLayername Then
        Exit While
    End If
    lyr = maplayers.Next()
End While

If lyr Is Nothing Then
    lblResult.Text = "Layer not found"
    Exit Sub
End If

' Get the feature class and ensure its geometry type is polygon.
Dim flyr As IFeatureLayer = lyr
Dim fc As IFeatureClass = flyr.FeatureClass
If fc.ShapeType <> esriGeometryType.esriGeometryPolygon Then
    lblResult.Text = "Select a polygon layer"
    Exit Sub
End If

' Create the query using the current map extent.
Dim sf As ISpatialFilter = ctx.CreateObject("esriGeoDatabase.SpatialFilter")
Dim ma As IMapArea = func.MapDescription.MapArea
sf.SpatialRel = esriSpatialRelEnum.esriSpatialRelIntersects
sf.GeometryField = fc.ShapeFieldName
sf.Geometry = ma.Extent

' Create a utility COM object on the server.
Dim totarea As IAreaSum = ctx.CreateObject("ServerUtil.ServerUtil")
Dim dTotalArea As Double = totarea.sumArea(fc, sf)
lblResult.Text = dTotalArea.ToString()
End Sub
When looping through features, the number of remote calls can be in the thousands. With this version of the code, the number has been reduced to a single call. This method of pushing fine-grained ArcObjects calls to the server can result in a significant increase in the performance of an operation that requires a large number of fine-grained ArcObjects calls.
The client machine (that is, your Web server) must also install the COM object or a proxy to the COM object so the application has access to its interfaces and methods when it is created on the server. You can create utility COM objects using VB, C++, or .NET.
When extending the GIS server with a utility COM object, the object must be created at each request made on a particular server object or server context. This means that for pooled server objects, each time a request is made on a server object, the utility COM object must be created. If the COM object has a high initialization cost, this continual creation of the object can be prohibitive. Because the COM object is created for each request, you cannot use it to cache information as it is used.


See Also:

Server object extensions (SOE)
Extending ArcGIS Server