Creating Server solutions using .NET—Scenarios  

Creating a Server Object Extension

 


This walkthrough is for developers who need to build and deploy a server object extension for use in server applications. It describes the process of extending the MapServer to provide methods for performing a specific type of spatial analysis on the features in one layer of the map.  The scenario consists of four parts:


This scenario is also included as a developer sample, ArcGIS_Spatial_Query_Server_Object_Extension.  

The sample code is located in:<install location>\DeveloperKit\SamplesNET\Server\Web_Applications

The purpose of this scenario is to create a server object extension using C# to extend a MapServer server object. The server object extension exposes methods to clip the geometries of polygons in one of the layers in the map to a buffer around a user-defined point. Summary statistics are then provided using the area of the clipped polygons based on the unique values of a specified field. Both the layer to perform the analysis on, and the field to summarize statistics on will be properties of the server object extension.

As part of this scenario, you'll create custom administration property pages - one for ArcCatalog and one for ArcGIS Server Manager - that will allow you to set the layer and field properties of the server object extension, based on the MapServer object's map document. Finally, you'll create a client Web application that consumes your server object extension. This application will utilize the Web ADF's controls and capabilities.

Concepts

Both coarse-grained calls to remote ArcObjects, such as the methods on the MapServer and GeocodeServer, as well as fine-grained calls to remote ArcObjects, such as looping through all the vertices of a polygon, are exposed through the ArcGIS Server ArcObjects API and can be used in your application. However, it’s important to note that when making a call against an object running in the server from your Web application, you are making that call across processes. The Web application is running in one process, while the object is running in another process. 

Calls to objects across processes are significantly slower than calls to objects in the same process. It’s also likely that your Web application is running on a Web server that is actually a different machine from the one the object is running on, so the calls are not only cross process but also cross machine.

If your application requires making a large number of fine-grained ArcObjects calls, there are two strategies you can employ to extend the GIS server with your own object, both of which expose coarse-grained interfaces: create utility COM objects or create server object extensions.

Extending a server object has the following advantages over creating a generic COM object:

However, unlike utility COM objects, server object extensions are registered and configured with specific server objects and are not for ad-hoc use or use with an empty server context. See the sample ArcGIS_Spatial_Query COM Utility for an example of extending the GIS server with utility COM object.

The server object extension satisfies application requirements for functionality that requires making a large number of fine-grained ArcObjects calls within the ArcGIS Server container process (ArcSOC.exe).  In this scenario, these calls include looping through features, getting their geometry, clipping the geometry, summarizing the areas of the geometries based on an attribute, creating a graphic for each feature, and so on.  Since the caller of the server object extension is free to specify a buffer distance that may include a large number of features, the number of features that would be analyzed is indeterminate, which could easily result in thousands of fine-grained ArcObjects calls.

Design

The server object extension in this scenario extends the MapServer with specialized functionality exposed as a stateless method on a custom interface.  As a result, you should configure the server object extension with a pooled MapServer.  The server object extension is also designed to store two custom properties with the MapServer configuration for which the extension is enabled.  These properties will be used during execution and will be made settable through custom property pages for ArcCatalog and ArcGIS Server Manager:

The Web application is designed to make stateless use of the GIS server.  It uses events on the Web ADF Map control to get a point from the user, gains access to server context using the MapResource for an ArcGIS Server Local data source, then uses the point as input to the server object extension to perform the analysis. To support this application, you need to add a pooled map server object to your ArcGIS Server and use ArcCatalog or ArcGIS Server Manager to enable the custom server object extension on the server object.

The Web application will use the Web ADF to manage the connection to the GIS server, and the Web ADF's controls will provide the basic mapping functionality required for this application. You will add a new tool to the Toolbar control that allows the user to click the map to provide the input to the analysis.  The results are displayed on the map as a set of graphics and summarized in a table on the Web page.

Requirements

The requirements for working through this scenario are that you have ArcGIS Server and ArcGIS Desktop installed and running.  The machine on which you develop the Web application must, at a minimum have the Web ADF, .NET SDK, and IIS installed.

The following ArcObjects .NET assemblies will be used to build the server object extension and property pages:


The following .NET framework assembly will also be required:


The following ArcObjects and Web ADF .NET assemblies will be used to build the Web application:

The development environment does not require any ArcGIS licensing; however, connecting to a server and using a map server object does require that the GIS server is licensed to run ArcObjects in the server.  None of the assemblies or object libraries used require an extension license.

The IDE used in this example is Visual Studio .NET 2008, and all IDE specific steps will assume that it is the IDE you are using.

Implementation

The code for this scenario is divided into four parts; all are written in C#.  The first is the implementation of the server object extension, which will run within the GIS server, and exposes the methods that extend the MapServer server object.  The second is the server object extension's property pages, one of which will be registered with the ArcCatalog desktop application and the other of which will be registered with the ArcGIS Server Manager web application.

The third is a utility application that will register the server object extension with the GIS Server.  The final part illustrates how to create a Web application that makes use of the server object extension and the Web ADF controls and APIs.

The instructions below will be easiest to follow as a narrative description of the sample code for this scenario. You may wish to open the sample in Visual Studio and read the rest of this section as a walk through of the sample.

Part 1: Developing the server object extension

Create the server object extension solution and interfaces project

The first step is to create a new project that will contain interfaces implemented by server object extension classes and utilized by clients.

  1. Start Visual Studio.
  2. Click File, click New, then click Project.
  3. In the New Project dialog box, under Project Types, expand the Other Project Types category.  Then click Visual Studio Solutions.  Under Visual Studio installed templates, Blank Solution.
  4. For the solution name, type "ArcGIS_Spatial_Query_SOE_CSharp2008."
  5. In the Location textbox, type or browse to the path where you want to put this solution.
  6. Click OK.
  7. In the Solution Explorer, right-click the solution and select Add --> New Solution Folder. Specify "Implementation" as the folder name. This is where you will create the projects storing the server object extension's implementation.
  8. Right-click the Implementation folder and select Add --> New Project... from the menu that appears.
  9. In the New Project dialog box, under Project Types, click the Visual C# Projects category.  Under Visual Studio installed Templates, click Class Library.
  10. Type a path to where you want to put this project.
  11. For the project name, type "SpatialQuerySOE.Interfaces_CSharp".
  12. Click OK. Remove the Class1.cs file which was autogenerated when the project was created.
  13. Open the project's properties. To do this, right-click the project in Solution Explorer and select Properties.
  14. On the Application tab of the Properties page, specify an assembly name and default namespace of "SpatialQuerySOE.Interfaces."

You will create a C# class named Interfaces that contains the two interfaces to implement within the server object extension classes in another project. Why are the interfaces for the server object extension stored in a separate assembly? Client applications only need the interfaces, registered as COM types, to interact with the server object extension. The business logic for the server object extension only needs to reside on the GIS Server and does not need to be provided to the client. As a result, the assembly provided to a client application only contains interfaces - no business logic. Much like working with ArcObjects remotely via ArcGIS Server, a client will use a custom server object extension interface to work with server object extension classes (objects) remotely via ArcGIS Server.

Create the interfaces class

Add the new class to the SpatialQuerySOE.Interfaces_CSharp project.

  1. In the Solution Explorer, right-click the SpatialQuerySOE.Interfaces_CSharp project, click Add, then click New Item...
  2. In the Add New Item dialog box, under Templates, click Class.
  3. For the name, type "Interfaces.cs".
  4. Click Add. This will add a new class to your project and will open the code for the class with some autogenerated code. Remove the autogenerated code.

Implementing the SOE Interfaces class

You'll need to add references to assemblies that contain the ArcObjects required for the interface definitions in this class file. Because the class contains interfaces that will be exposed as COM types, it will need to reference some additional assemblies to manage interoperability.

  1. Add references to the following assemblies:
    • ESRI.ArcGIS.Carto
    • ESRI.ArcGIS.Display
    • ESRI.ArcGIS.Geodatabase
    • ESRI.ArcGIS.Geometry
    • ESRI.ArcGIS.Server
    • ESRI.ArcGIS.System
    • System.EnterpriseServices
  2. Add the namespace SpatialQuerySOE.Interfaces. Within the namespace definition, add two interfaces: IExtension and IResults.

    The IExtension interface will define the basic framework to be implemented by the SpatialQuerySOE.Extension class, namely a method to accepts a user provided ArcObjects object of type IPoint and a double value to define the distance around the point to generate a buffer. An ArcGIS Server client can work with a reference to the SpatialQuerySOE.Extension object running within the server object container via the QueryPoint method on the IExtension interface. This pattern follows standard programming techniques for working with ArcObjects (COM objects) remotely via ArcGIS Server. The QueryPoint method will create a SpatialQuerySOE.Results object in the server container process, update it's properties and return to the client a reference to the remote SpatialQuerySOE.Results COM object via the IResults interface.

    The IResults interface will define the basic framework to be implemented by the SpatialQuerySOE.Results class, namely two properties to store a reference to an array of graphic elements and a set of records.

    Since both interfaces will be registered (added to the registry) with COM, they should define a unique GUID using the COM attribute GuidAttribute. The GUID will be used to uniquely identify an application, component, class, interface, etc. within the Windows registry. There are many options for generating a unique GUID. You can create a unique GUID using the .NET System.Guid struct. The call to System.Guid.NewGuid().ToString() will return a usable GUID string. In addition, a number of Web sites and services provide GUID generating capabilities.  Below is the code for the interface:

    namespace SpatialQuerySOE.Interfaces
    {
        [GuidAttribute("9D47D51C-F2B8-4381-8FA7-A6E1E1E9792F")]
        public interface IExtension
        {
            IResults QueryPoint(ESRI.ArcGIS.Geometry.IPoint point, double distance);
        }
    
        [GuidAttribute("19A98B11-1EDE-4470-88FD-007F4B7BD05F")]
        public interface IResults
        {
            ESRI.ArcGIS.Carto.IGraphicElements ResultsGraphics { get; set; }
    
            ESRI.ArcGIS.Geodatabase.IRecordSet SummaryStatistics { get; set; }
        }
    }
    

Set assembly properties for the SpatialQuerySOE.Interfaces_CSharp project

In the SpatialQuerySOE.Interfaces_CSharp project, open the AssemblyInfo.cs file and change it to reflect the following settings. It is important to set the ComVisibleAttribute to true and specify a unique GUID for the GuidAttribute.  Since the assembly will be used to generate a type library, these attributes will enable the interfaces to be registered with COM and will define a unique identifier.

using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

[assembly: AssemblyTitle("SpatialQuerySOE.Interfaces")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("ESRI")]
[assembly: AssemblyProduct("SpatialQuerySOE.Interfaces")]
[assembly: AssemblyCopyright("Copyright © ESRI 2009")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]

[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

[assembly: ComVisibleAttribute(true)]
[assembly: GuidAttribute("8B929DF3-7D8A-4119-BEC8-FA8986CA10E3")]

Create the server object extension project

  1. In the same Visual Studio session used to create the SpatialQuerySOE.Interfaces_CSharp project, right-click the Implementation folder, click Add, then click New Project.
  2. In the New Project dialog box, under Project Types, click the Visual C# Projects category.  Under Templates, click Class Library.
  3. Type a path to where you want to put this project.
  4. For the project name, type "SpatialQuerySOE_CSharp".
  5. Click OK. Remove the Class1.cs file which was autogenerated when the project was created.
  6. Open the project's properties. To do this, right-click the project in Solution Explorer and select Properties.
  7. On the Application tab of the Properties page, specify an assembly name and default namespace of "SpatialQuerySOE."

You will create a C# class named Extension that represents the server object extension. This server object extension will provide a method that performs the spatial query and returns an object of type Results that contains an array of graphic elements and a recordset. Before creating the server object extension itself, you'll create the Results (spatial query results) class. This class is merely used as a complex type to store the results of the server object extension in a single object for use by the consuming client (e.g. Web application).

Create the Results class

Add the new class to the SpatialQuerySOE_CSharp project.

  1. In the Solution Explorer, right-click the SpatialQuerySOE_CSharp project, click Add, then click New Item...
  2. In the Add New Item dialog box, under Templates, click Class.
  3. For the name, type "Results.cs".
  4. Click Add. This will add a new class to your project and will open the code for the class with some autogenerated code.  Remove the autogenerated code.

Implementing the Results class

You'll need to add references to assemblies that contain the ArcObjects required for this class. In addition, because this is a COM object for use in the GIS server, you'll need to add some additional references to support calling a managed .NET component from a COM client.  The .NET component is the custom server object extension assembly and the COM client is the container process, ArcSOC.exe.  The COM client uses a COM callable wrapper to work with the .NET component.  The technology that permits .NET components and COM clients to work together is known as COM Interop. 

  1. Add references to the following assemblies:
    • ESRI.ArcGIS.Carto
    • ESRI.ArcGIS.Display 
    • ESRI.ArcGIS.Geodatabase
    • ESRI.ArcGIS.Geometry
    • ESRI.ArcGIS.Server
    • ESRI.ArcGIS.System
    • System.EnterpriseServices
  2. Add a reference to the SpatialQuerySOE.Interfaces_CSharp project in the same solution. If the SpatialQuerySOE.Interfaces_CSharp project is not in the same solution, add a reference to the SpatialQuerySOE.Interfaces assembly.
  3. Define the public COM class to contain implementation of the Results object.  Since this class will be exposed to COM, a couple of COM attributes need to be added to the class definition; specifically AutomationProxy, ClassInterface and GuidAttribute.  AutomationProxy determines if an object should be marshaled using the automation marshaller (true) or custom marshaled (false).  We'll use the automation marshaller.  ClassInterface defines how interfaces to the COM class are generated in the type library and exposed to a COM client.  In this case, we want to support COM versioning and explicitly define the default interface by which a COM client will access the class.  Setting the ClassInterface attribute to ClassInterfaceType.None requires that we define an explicit interface (IResults) to work with our class (Results).  The GuidAttribute should reference a unique GUID to identify our class.  

    Our class should derive from the ServicedComponent class to be hosted by COM clients.  In addition, we will implement the IResults interface as a default interface to interact with our COM object.  Two properties will be defined to get\set the graphics elements and recordset, ResultsGraphics and SummaryStatistics, respectively.      

       

    namespace SpatialQuerySOE
    {
        [AutomationProxy(true), ClassInterface(ClassInterfaceType.None), GuidAttribute("C9FB0536-D1D6-455d-897D-8AD26849C79A")]
        public class Results : ServicedComponent, SpatialQuerySOE.Interfaces.IResults
        {
            private ESRI.ArcGIS.Carto.IGraphicElements m_resultsGraphics; 
            private ESRI.ArcGIS.Geodatabase.IRecordSet m_summaryStats;
    
            public ESRI.ArcGIS.Carto.IGraphicElements ResultsGraphics
            {
                get{ return m_resultsGraphics;	}
                set{ m_resultsGraphics = (ESRI.ArcGIS.Carto.IGraphicElements) value; }
            }
    
            public ESRI.ArcGIS.Geodatabase.IRecordSet SummaryStatistics
            {
                get{ return m_summaryStats; }
                set{ m_summaryStats = (ESRI.ArcGIS.Geodatabase.IRecordSet) value; }
            }
        }
    }
    

    Create the Server Object Extension class

    The first step is to add the new class to the project.

    1. In the Solution Explorer, right-click the SpatialQuerySOE_CSharp project, click Add, then click New Item...
    2. In the Add New Item dialog box, under Templates, click Class.
    3. For the name, type "Extension.cs".
    4. Click Add. This will add a new class to your project and will open the code for the class with some autogenerated code.  Remove the autogenerated code.

    Implementing the Server Object Extension class

    Now that you have implemented the results class, you can implement your server object extension class. A server object extension extends a server object with additional interfaces to provide more specialized functionality. In this example, the server object extension implements the custom interface IExtension.

    1. Define the public COM class to contain the implementation of the Extension object. Since this class will be exposed to COM, a couple of COM attributes need to be added to the class definition; specifically AutomationProxy, ClassInterface and GuidAttribute. AutomationProxy determines if an object should be marshaled using the automation marshaller (true) or custom marshaled (false). We'll use the automation marshaller. ClassInterface defines how interfaces to the COM class are generated in the type library and exposed to a COM client. In this case, we want to support COM versioning and explicitly define the default interface by which a COM client will access the class. Setting the ClassInterface attribute to ClassInterfaceType.None requires that we define an explicit interface (IExtension) to work with our class (Extension). The GuidAttribute should reference a unique GUID to identify our class.

      A set of member variables should be created to store a reference to an IServerObjectHelper (IServerObjectExtension implementation), an ILayer, two strings to account for layer name and field name (IExtension implementation), and an ILog (ILogSupport implementation). Populating these variables will be discussed in the next section.

      namespace SpatialQuerySOE
      {
          [AutomationProxy(true), ClassInterface(ClassInterfaceType.None), GuidAttribute("1932805D-7266-41a2-9428-0421A5617436")]
          public class Extension : ServicedComponent, SpatialQuerySOE.Interfaces.IExtension, ESRI.ArcGIS.Server.IServerObjectExtension,
              ESRI.ArcGIS.esriSystem.IObjectConstruct, ESRI.ArcGIS.esriSystem.ILogSupport, ESRI.ArcGIS.esriSystem.IObjectActivate
          {
              private ESRI.ArcGIS.Server.IServerObjectHelper m_ServerObjectHelper;
              private ESRI.ArcGIS.Carto.IFeatureLayer m_featureLayer;
              private string m_layerName;
              private string m_fieldName;
              private ESRI.ArcGIS.esriSystem.ILog m_log;
      
      
    2. The following class and interfaces need to be implemented:

      • ServicedComponent (class) 
      • ILogSupport
      • IServerObjectExtension
      • IObjectConstruct
      • IObjectActivate
      • IExtension

      The following diagram provides the order in which the methods of interfaces implemented by a server object extension are called during initialization and use:

      A discussion and appropriate implementation code is provided below:

      • ServicedComponent

        The Extension class should derive from the ServicedComponent class to be hosted by COM clients (e.g. ArcSOC.exe). No additional code is necessary.

      • ILogSupport

        If you want your server object extension to log messages to the GIS server's log file, your server object extension should implement ILogSupport. ILogSupport is an optional interface for server object extensions that has a single InitLogging method. InitLogging is called when the server object extension is created and hands back a reference to the GIS server's log object via the log argument. Once you have a reference to the server log, you will often call a single method, AddMessage(), to add information to the log. The AddMessage() method has three parameters: level, code, and message.

        The level is the level of detail of the message in relation to other messages. Levels are classified from 1 to 5 and termed, in order, Error, Warning, Normal, Detailed, and Debug. ArcGIS Server log file settings determine which messages are included in the server log.

        The code is the result code associated with the message. The code is an arbitrary integer value to uniquely define the source of the message. Codes 0 - 5999 are utilized by the SOM. Codes 6000 and above can be generated by any serviced component (e.g. MapServer, GeocodeServer, custom component, etc.).

        The message is the custom string inserted into the GIS server log file.

                public void InitLogging(ESRI.ArcGIS.esriSystem.ILog log)
                {
                    m_log = log;
                }
        
      • IServerObjectExtension

        A mandatory interface that must be supported by all server object extensions and includes two methods - Init and Shutdown. This interface is used by the server object to manage the lifetime of the server object extension. The server object cocreates the server object extension and calls the Init method, handing it a reference to the server object via the server object helper argument. The server object helper implements a weak reference on the server object. The extension can keep a strong reference on the server object helper (for example, in a member variable) but should not keep a strong reference on the server object. Extensions should get the server object from the server object helper in order to make any method calls on the server object and release the reference after making the method calls. Init is called once, when the instance of the server object extension is created. The Shutdown method is called once and informs the server object extension that the server object's context is being shut down and is about to go away. In response the server object extension should release its reference on the server object helper. The log entries are merely informative and completely optional.

                public void Init(ESRI.ArcGIS.Server.IServerObjectHelper pSOH)
                {
                    m_ServerObjectHelper = pSOH;
                    m_log.AddMessage(3,8000,"SpatialQuerySOE custom message. Init called");
                }
        
                public void Shutdown()
                {
                    m_log.AddMessage(3, 8000, "SpatialQuerySOE custom message. Shutdown called");
                    m_ServerObjectHelper = null;
                    m_featureLayer = null;
                    m_log = null;
                }
        
      • IObjectConstruct

        If your server object extension includes configuration properties or requires any additional initialization logic, you need to implement IObjectConstruct. IObjectConstruct is an optional interface for server object extensions. The interface includes a single method called Construct. Construct is called only once, when the server object extension is created, after IServerObjectExtension::Init is called. You should include any expensive initialization logic within your implementation of Construct.

        Construct hands back the configuration properties for the server object extension as a property set. The configuration properties are stored in the server object configuration file. Configuration files are named <service name>.<server object type>.cfg and stored on the SOM machine in the <ArcGIS Install>\server\user\cfg directory. For example, a map service named Yellowstone has a configuration file named Yellowstone.MapServer.cfg. Properties of server object extensions configured for use with a server object are also stored in the cfg file. In this scenario, a set of properties are being read from the service's cfg file. The property values, a feature layer name and field name, are validated to confirm they exist in the default map frame associated with the map server object. If an error occurs, the appropriate log entries are added.

                public void Construct(ESRI.ArcGIS.esriSystem.IPropertySet props)
                {
                    try
                    {
                        m_layerName = props.GetProperty("LayerName") as string;
                        m_fieldName = props.GetProperty("FieldName") as string;
                    }
                    catch (Exception ex)
                    {
                        m_log.AddMessage(1, 8000, "SpatialQuerySOE custom error. Error reading properties: " + ex.Message + " " + props.Count.ToString());
                        return;
                    }
        
                    try
                    {
                        // Get the map underlying the map service and the IGeoFeatureLayers contained in the map
                        ESRI.ArcGIS.Carto.IMapServer mapServer = (ESRI.ArcGIS.Carto.IMapServer) m_ServerObjectHelper.ServerObject;
                        ESRI.ArcGIS.Carto.IMapServerObjects mapServerObjects = (ESRI.ArcGIS.Carto.IMapServerObjects) mapServer;
                        ESRI.ArcGIS.Carto.IMap map = mapServerObjects.get_Map(mapServer.DefaultMapName);
                        ESRI.ArcGIS.esriSystem.UID layerTypeID = new ESRI.ArcGIS.esriSystem.UIDClass();
                        layerTypeID.Value = "{E156D7E5-22AF-11D3-9F99-00C04F6BC78E}";
                        ESRI.ArcGIS.Carto.IEnumLayer enumLayer = map.get_Layers(layerTypeID, true);
                        enumLayer.Reset();
        
                        // Get the layer specified as the SOE layer
                        while ((m_featureLayer = enumLayer.Next() as ESRI.ArcGIS.Carto.IFeatureLayer) != null)
                        {
                            if (m_featureLayer.Name == m_layerName)
                                break;
                        }
        
                        if (m_featureLayer == null)
                        {
                            m_log.AddMessage(1, 8000, "SpatialQuerySOE custom error: Layer " + m_layerName + " not found.");
                            return;
                        }
        
                        // Make sure the layer contains the field specified by the SOE's configuration
                        if (m_featureLayer.FeatureClass.FindField(m_fieldName) == -1)
                            m_log.AddMessage(1, 8000, "SpatialQuerySOE custom error: Field " + m_fieldName + " not found in layer " + m_layerName);
                        else
                            m_log.AddMessage(3, 8000, "SpatialQuerySOE successfully initialized.");
        
                    }
                    catch (Exception ex)
                    {
                        m_log.AddMessage(1, 8000, "SpatialQuerySOE custom error: Failed to initialize extension: " + ex.Message + "::" + ex.StackTrace.Length.ToString());
                    }
                }
        
      • IObjectActivate

        While IServerObjectExtension::Init and IObjectConstruct::Construct are called once when the instance of the server object extension is created, if your server object extension requires logic to run each time its server context is acquired and released (each time a client calls CreateServerContext and ReleaseContext), you need to implement IObjectActivate. IObjectActivate is an optional interface for server object extensions that includes two methods: Activate and Deactivate. Activate is called each time a client calls CreateServerContext on the server object extension's server object's context, and Deactivate is called each time a client releases the context (via ReleaseContext). Because Activate and Deactivate are called each time a client gets and releases the server object's context, any logic you implement in these methods should not be expensive. In this scenario, we merely add some instructive information to the log file to indicate the method was called.

                void ESRI.ArcGIS.esriSystem.IObjectActivate.Activate()
                {
                    m_log.AddMessage(3, 8000, "SpatialQuerySOE custom message. Activate called");
                }
        
                void ESRI.ArcGIS.esriSystem.IObjectActivate.Deactivate()
                {
                    m_log.AddMessage(3, 8000, "SpatialQuerySOE custom message. Deactivate called");
                }
        
      • IExtension

        IExtension is a custom interface implemented by the custom SOE to expose a method called by a client application. Implementing the QueryPoint method defined by the IExtension interface is the last step to complete the server object extension. The QueryPoint method takes as arguments a point and a distance. In the method, you'll add code that buffers the point to the specified distance, then queries the layer's feature class for all the polygons that intersect that buffer. For each polygon, the method clips it to the buffer, creates a graphic of the clipped polygon, and adds its area to a Dictionary object based on the value of the specified field.

                public SpatialQuerySOE.Interfaces.IResults QueryPoint(ESRI.ArcGIS.Geometry.IPoint point, double distance)
                {
                    if (m_featureLayer == null)
                    {
                        m_log.AddMessage(1, 8000, "SpatialQuerySOE custom error: layer not found");
                        return null;
                    }
        
                    ESRI.ArcGIS.Geodatabase.IFeatureClass featureClass = m_featureLayer.FeatureClass;
        
                    // buffer the point
                    ESRI.ArcGIS.Geometry.ITopologicalOperator topologicalOperator = (ESRI.ArcGIS.Geometry.ITopologicalOperator)point;
                    ESRI.ArcGIS.Geometry.IGeometry queryGeometry = topologicalOperator.Buffer(distance);
        
                    // query the feature class
                    ESRI.ArcGIS.Geodatabase.ISpatialFilter spatialFilter = new ESRI.ArcGIS.Geodatabase.SpatialFilter();
                    spatialFilter.Geometry = queryGeometry;
                    spatialFilter.SpatialRel = ESRI.ArcGIS.Geodatabase.esriSpatialRelEnum.esriSpatialRelIntersects;
                    spatialFilter.GeometryField = featureClass.ShapeFieldName;
        
                    ESRI.ArcGIS.Geodatabase.IFeatureCursor resultsFeatureCursor = featureClass.Search(spatialFilter, true);
        

        Before looping through the features, you need to create a GraphicElements collection to hold the graphics, a simple fill symbol to apply to each graphic element, a dictionary object that you will use to categorize the different feature types, and some other needed variables. Note that the fill symbol is created using a helper method called createFillSymbol. You will define this method later.

                    topologicalOperator = (ESRI.ArcGIS.Geometry.ITopologicalOperator)queryGeometry;
                   int classFieldIndex = featureClass.FindField(m_fieldName);
        
                   System.Collections.Specialized.ListDictionary summaryStatsDictionary = new System.Collections.Specialized.ListDictionary();
        
                   // create the symbol and graphic elements collection for the graphics
                   ESRI.ArcGIS.Display.ISimpleFillSymbol simpleFillSymbol = createFillSymbol();
                   ESRI.ArcGIS.Carto.IGraphicElements resultsGraphics = new ESRI.ArcGIS.Carto.GraphicElements();
        

        The next step is to loop through the features in the feature class that intersect the buffer geometry and clip each feature's polygon to the buffer. The resulting clipped geometry is then used to create a graphic that is added to the graphics collection. The area of the clipped geometry is added to the total area of the feature's type (as defined by the field specified in the server object extension properties) in the dictionary object.

                    ESRI.ArcGIS.Geodatabase.IFeature resultsFeature;
                   while ((resultsFeature = resultsFeatureCursor.NextFeature()) != null)
                   {
                        // create the graphic
                        ESRI.ArcGIS.Carto.IFillShapeElement fillShapeElement = new ESRI.ArcGIS.Carto.PolygonElement() as
                            ESRI.ArcGIS.Carto.IFillShapeElement;
                        ESRI.ArcGIS.Carto.IElement element = fillShapeElement as ESRI.ArcGIS.Carto.IElement;
        
                        // clip the geometry
                        ESRI.ArcGIS.Geometry.IGeometry clippedResultsGeometry = topologicalOperator.Intersect(resultsFeature.Shape, 
                            ESRI.ArcGIS.Geometry.esriGeometryDimension.esriGeometry2Dimension);
                        element.Geometry = clippedResultsGeometry;
                        fillShapeElement.Symbol = simpleFillSymbol;
                        ESRI.ArcGIS.Carto.IGraphicElement resultsGraphicElement = fillShapeElement as
                            ESRI.ArcGIS.Carto.IGraphicElement;
                        resultsGraphics.Add(resultsGraphicElement);
        
                        // get statistics and add to dictionary
                        ESRI.ArcGIS.Geometry.IArea area = clippedResultsGeometry as ESRI.ArcGIS.Geometry.IArea;
                        string resultsClass = resultsFeature.get_Value(classFieldIndex) as string;
        
                        // If the class is already in the dictionary, add the current feature's area to the existing entry
                        if (summaryStatsDictionary.Contains(resultsClass))
                            summaryStatsDictionary[resultsClass] = (double)summaryStatsDictionary[resultsClass] + area.Area;
                        else
                            summaryStatsDictionary[resultsClass] = area.Area;
                   }
        

        At this point, the summary statistics dictionary object will have a key for each unique value of the specified field and a value that is the total area within the buffer of features that share the unique value. The next step is to create a record set object and copy the keys and values from the dictionary into rows and fields in the record set. This is accomplished using the createSummaryRecordSet helper method, which will be implemented later.

                    ESRI.ArcGIS.Geodatabase.IRecordSet summaryStatsRecordSet = createSummaryRecordSet(summaryStatsDictionary);
        

        Finally, since the QueryPoint function returns a SpatialQuerySOE.Results object, the last part of the function creates a new Results object, sets the graphics collection and summary record set in the object, and returns the object to the caller. Note that the new Results object is referenced using the SpatialQuerySOE.Interfaces.IResults interface. The client application will work with the interface to process the results.

                    SpatialQuerySOE.Interfaces.IResults results = new Results();
                   results.ResultsGraphics = resultsGraphics;
                   results.SummaryStatistics = summaryStatsRecordSet;
        
                   return results;
        
    3. As described in the previous step, the QueryPoint method makes use of two helper methods to create a fill symbol (createFillSymbol) and to copy the contents of a dictionary object to a record set (createSummaryRecordSet). You will now implement these helper methods in your server object extension class.

      The createSummaryRecordSet method takes a dictionary object as an argument and returns a record set. The function creates a new record set with a field for the type of feature (key) and a field for the total area (value). It then loops through the keys and values in the dictionary and creates a row in the record set for each key/value pair.

              private ESRI.ArcGIS.Geodatabase.IRecordSet createSummaryRecordSet(
                  System.Collections.Specialized.ListDictionary summaryStatsDictionary)
              {
                  // initialize the summary statistics record set
                  ESRI.ArcGIS.Geodatabase.IRecordSet summaryStatsRecordSet = new ESRI.ArcGIS.Geodatabase.RecordSet();
                  ESRI.ArcGIS.Geodatabase.IRecordSetInit recordSetInit = summaryStatsRecordSet as ESRI.ArcGIS.Geodatabase.IRecordSetInit;
      
                  ESRI.ArcGIS.Geodatabase.IFields summaryFields = new ESRI.ArcGIS.Geodatabase.Fields();
                  ESRI.ArcGIS.Geodatabase.IFieldsEdit summaryFieldsEdit = summaryFields as ESRI.ArcGIS.Geodatabase.IFieldsEdit;
                  summaryFieldsEdit.FieldCount_2 = 2;
      
                  ESRI.ArcGIS.Geodatabase.IField field = new ESRI.ArcGIS.Geodatabase.Field();
                  ESRI.ArcGIS.Geodatabase.IFieldEdit fieldEdit = field as ESRI.ArcGIS.Geodatabase.IFieldEdit;
                  fieldEdit.Name_2 = "Type";
                  fieldEdit.Type_2 = ESRI.ArcGIS.Geodatabase.esriFieldType.esriFieldTypeString;
                  fieldEdit.Length_2 = 50;
                  summaryFieldsEdit.set_Field(0, field);
      
                  field = new ESRI.ArcGIS.Geodatabase.Field();
                  fieldEdit = field as ESRI.ArcGIS.Geodatabase.IFieldEdit;
      
                  fieldEdit.Name_2 = "Area";
                  fieldEdit.Type_2 = ESRI.ArcGIS.Geodatabase.esriFieldType.esriFieldTypeDouble;
                  summaryFieldsEdit.set_Field(1, field);
      
                  recordSetInit.CreateTable(summaryFields);
      
                  ESRI.ArcGIS.Geodatabase.ICursor cursor = recordSetInit.Insert();
                  ESRI.ArcGIS.Geodatabase.IRowBuffer rowBuffer = recordSetInit.CreateRowBuffer();
      
                  // Copy the summary stats to the record set
                  System.Collections.IDictionaryEnumerator summaryStatsEnumerator = summaryStatsDictionary.GetEnumerator();
                  while (summaryStatsEnumerator.MoveNext())
                  {
                      rowBuffer.set_Value(0, summaryStatsEnumerator.Key);
                      rowBuffer.set_Value(1, summaryStatsEnumerator.Value);
                      cursor.InsertRow(rowBuffer);
                  }
      
                  return summaryStatsRecordSet;
              }
              

      The createFillSymbol method creates and returns a new SimpleFillSymbol object. This fill symbol is a hollow fill symbol with a green outline. The client application can choose to use this rendering scheme when working with the server object extension results.

              private ESRI.ArcGIS.Display.ISimpleFillSymbol createFillSymbol()
              {
                  ESRI.ArcGIS.Display.ISimpleLineSymbol simpleLineSymbol = new ESRI.ArcGIS.Display.SimpleLineSymbol();
                  ESRI.ArcGIS.Display.IRgbColor rgbColor = new ESRI.ArcGIS.Display.RgbColor();
                  rgbColor.Red = 0;
                  rgbColor.Green = 255;
                  rgbColor.Blue = 0;
                  simpleLineSymbol.Color = rgbColor;
                  simpleLineSymbol.Style = ESRI.ArcGIS.Display.esriSimpleLineStyle.esriSLSSolid;
                  simpleLineSymbol.Width = 2;
      
                  ESRI.ArcGIS.Display.ISimpleFillSymbol simpleFillSymbol = new ESRI.ArcGIS.Display.SimpleFillSymbol();
                  simpleFillSymbol.Outline = simpleLineSymbol;
                  simpleFillSymbol.Style = ESRI.ArcGIS.Display.esriSimpleFillStyle.esriSFSHollow;
      
                  return simpleFillSymbol;
              }
      

Set assembly properties for the SpatialQuerySOE_CSharp project

In the SpatialQuerySOE_CSharp project, open the AssemblyInfo.cs file and change it to reflect the following settings. It is important that the ComVisibleAttribute be set to true (or removed since the default is true).  Setting this on the assembly will make all public types visible to COM clients.  The ComVisibleAttribute can also be applied to individual types in the assembly.

using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

[assembly: AssemblyTitle("SpatialQuerySOE")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("ESRI")]
[assembly: AssemblyProduct("SpatialQuerySOE")]
[assembly: AssemblyCopyright("Copyright © ESRI 2009")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]

[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

[assembly: ComVisibleAttribute(true)]
[assembly: GuidAttribute("C1E4CA92-0380-4ed1-ABF4-9F45C9914D6D")]

Build the server object extension projects

Build the SpatialQuerySOE.Interfaces_CSharp and SpatialQuerySOE_CSharp projects. Each project will create an assembly, SpatialQuerySOE.Interfaces.dll and SpatialQuerySOE.dll. The deployment section below discusses the distribution and registration of these assemblies.

Part 2: Creating the server object extension's property pages

In this part of the implementation, you'll create the necessary objects to implement two custom property pages - one for ArcCatalog and one for ArcGIS Server Manager.  Each of these pages allows you to configure your server object extension's properties. The properties for this server object extension are:

Both of these properties will be settable using either property page. Each property page will display all of the feature layers in the default map and the fields for the selected feature layer. The property pages will only be used by the GIS server administrator to specify the properties for the custom server object extension when the extension is enabled on a map service (MapServer server object).

Create a Solution Folder for the property pages

The solution for the custom server object extension will contain projects that deal with several different implementation aspects (e.g. implementation, registration, administration). To keep the extension logic organized, place the property page classes in a new Solution Folder.

  1. In Visual Studio's Solution Explorer, right-click the ArcGIS_Spatial_Query_SOE_CSharp2008 solution and select Add --> New Solution Folder.
  2. Specify "ArcCatalog and Manager Integration" as the folder name. This is where you will create the projects that define the custom property pages for ArcCatalog and ArcGIS Server Manager.

Implementing the ArcCatalog property page

Custom pages for defining the properties of custom server object extensions can be integrated with the ArcCatalog desktop application. Such custom pages are made available from within ArcCatalog when editing the capabilities of an ArcGIS Server service. This part of the walkthrough will demonstrate how to implement such a property page for the SpatialQuerySOE extension.

Create the server object extension ArcCatalog property page project

The first step is to create a new project that will contain the implementation of an ArcCatalog property page.

  1. In Solution Explorer, right-click the ArcCatalog and Manager Integration folder, click Add, then click New Project...
  2. In the Add New Project dialog, under Project Types, click Visual C# Projects.
  3. Under Templates, click Class Library. For the project name, specify "SpatialQuerySOE.ArcCatalog_CSharp."
  4. Click OK. Remove the Class1.cs file which was autogenerated when the project was created.
  5. Right-click the project in Solution Explorer and select Properties.
  6. On the Application tab of the Properties page, specify an assembly name and default namespace of "SpatialQuerySOE.ArcCatalog."

This project will contain two class files and a Windows form. The Windows form will provide the visual interface of the property page in ArcCatalog. The class files will provide the logic for registering the property page, showing\hiding the property page, and retrieving and setting server object extension properties in the server object configuration file.

Create the ArcCatalog property page classes and form

  1. In the Solution Explorer, right-click the SpatialQuerySOE.ArcCatalog_CSharp project, click Add, then click New Item...
  2. In the Add New Item dialog box, under Templates, click Class.
  3. For the name, type "SOEPropertyPage.cs".
  4. Click Add. This will add a new class to your project and will open the code for the class with some autogenerated code. Remove the autogenerated code.
  5. Again, in the Solution Explorer, right-click the SpatialQuerySOE.ArcCatalog_CSharp project, click Add, then click New Item...
  6. In the Add New Item dialog box, under Templates, click Class.
  7. For the name, type "PropertyPage.cs".
  8. Click Add. This will add a new class to your project and will open the code for the class with some autogenerated code. Remove the autogenerated code.
  9. Again, in the Solution Explorer, right-click the SpatialQuerySOE.ArcCatalog_CSharp project, click Add, then click New Item...
  10. In the Add New Item dialog box, under Templates, click Windows Form.
  11. For the name, type "PropertyForm.cs".
  12. Click Add.  This will add a new Windows form to the project.

Implementing the form for the ArcCatalog property page

You'll need to add references to assemblies that contain the ArcObjects required for implementation code in both the property page classes and form.

  1. Add references to the following assemblies:
    • ESRI.ArcGIS.Carto
    • ESRI.ArcGIS.Catalog
    • ESRI.ArcGIS.CatalogUI
    • ESRI.ArcGIS.Framework
    • ESRI.ArcGIS.Geodatabase
    • ESRI.ArcGIS.Geometry
    • ESRI.ArcGIS.Server
    • ESRI.ArcGIS.System
  2. Add some controls to the form in design view. These controls will work with the Map document to return feature layer names and fields.
    1. In the Solution Explorer, double-click the PropertyForm.cs to open the form in design mode.
    2. In the properties for the form, click the FormBorderStyle dropdown and click None.
    3. Open the Visual Studio toolbox, click the Windows Forms tab, click Label and drag a label control on your form.
    4. In the properties dialog for the label, type the following string for the Text property:
      Select the layer and summary field for the extension:
    5. Drag two more label controls onto the form and set their text properties to be "Layer" and "Field".
    6. Drag a ComboBox control onto the form, and in its properties, type ComboLayers for the (Name).
    7. Drag another combo box onto the form and type ComboFields for the (Name).

      The form should appear as follows:

  3. Add code to the form.
    1. In Solution Explorer, right-click the form and click View Code to open the code window for the form.
    2. The code already contains a constructor and the class is defined within the SpatialQuerySOE.ArcCatalog namespace.  Add logic to the constructor to determine whether the service being configured is stopped.  If it is not, then it cannot be modified, so disable the layers and fields combo boxes.
      namespace SpatialQuerySOE.ArcCatalog
      {
          public partial class PropertyForm : Form
          {
              public PropertyForm()
              {
                  InitializeComponent();
      
                  // Get ArcCatalog's representation of the map service being modified
                  System.Type type = System.Type.GetTypeFromCLSID(typeof(ESRI.ArcGIS.Framework.AppRefClass).GUID);
                  ESRI.ArcGIS.CatalogUI.IGxApplication gxApp = Activator.CreateInstance(type) as ESRI.ArcGIS.CatalogUI.IGxApplication;
                  ESRI.ArcGIS.Catalog.IGxAGSObject gxAgsObj = gxApp.SelectedObject as ESRI.ArcGIS.Catalog.IGxAGSObject;
      
                  // If the service is not stopped, disable the layers and fields drop-down lists so that the SOE cannot be configured
                  if (gxAgsObj.Status != "Stopped")
                  {
                      ComboLayers.Enabled = false;
                      ComboFields.Enabled = false;
                  }
              }
      
    3. The form needs to expose properties to get and set the layer and field names the user chooses in the form and to store the names of the layers and fields contained by the map document underlying the MapServer server object on which the server object extension is enabled.  The form also needs to track whether initialization logic or user interaction is the source of the selected layer being changed.  Add the following member variables to the form to hold these properties:
              private string m_layer;
             private string m_field;
             private bool m_init = false;
             private System.Collections.Generic.Dictionary m_fieldsDictionary = 
                  new System.Collections.Generic.Dictionary();
      
    4. Add a method to return the handle of the form. This is needed for the property page form to display its contents in ArcCatalog.
              public int getHWnd()
             {
                  return this.Handle.ToInt32();
             }
      
    5. Add a property to store the ComPropertyPageSite of the class implementing the property page.  ComPropertyPageSite is passed by ArcCatalog to the class implementing the property page and, by invoking the PageChanged method, can be used to notify ArcCatalog that the property page's contents have changed.  The form's PageSite property will be initialized by the class that implements the interfaces needed to integrate property page with ArcCatalog.
              internal ESRI.ArcGIS.Framework.IComPropertyPageSite PageSite { private get; set; }
      
    6. Add the properties to get and set the selected layer and field name from the form. The get will be accessed when the ArcCatalog is used to create a new server object, and when changes are made to a server object via its property page. The set is needed to display the properties for an existing server object when the form is displayed in the server object's property page.
              internal string Layer
             {  
                 get { return m_layer; }
                 set { m_layer = value; }
             }
      
              internal string Field
              {
                  get { return m_field; }
                  set { m_field = value; }
              }
      
    7. Define the method to set the map document the server object extension will use. The server object extension will use the same map document associated with the MapServer server object on which the custom server object extension is enabled. In the method, you'll add code to open the map document, get all of the simple polygon feature layers, add their names to the ComboLayers combo box, and store the names of the fields contained in these layers. The code will also set the layer combo box's selected layer to that currently specified by the server object extension or, if the extension has not yet been configured, to the first layer in the list.
              internal void SetMap(string filePath)
              {
                  // Open the map document
                  ESRI.ArcGIS.Carto.IMapDocument mapDocument = new ESRI.ArcGIS.Carto.MapDocumentClass();
                  mapDocument.Open(filePath, null);
      
                  // The call to get_Map() will create a lock (ldb) on a personal gdb.  Make sure ArcCatalog user and 
                  // ArcGIS Server container account has read\write access to data location.  If not,
                  // feature classes in the personal gdb may not be accessible (or visible in the map).
                  ESRI.ArcGIS.Carto.IMap map = mapDocument.get_Map(0);
      
                  // Get IGeoFeatureLayers from the map
                  ESRI.ArcGIS.esriSystem.UID id = new ESRI.ArcGIS.esriSystem.UIDClass();
                  id.Value = "{E156D7E5-22AF-11D3-9F99-00C04F6BC78E}";
                  ESRI.ArcGIS.Carto.IEnumLayer enumLayer = map.get_Layers(id, true);
      
                  int selectedIndex = 0;
                  string[] fieldNames = null;
                  m_fieldsDictionary.Clear();
                  // Add the names of simple polygon feature layers to the layers drop-down and store the names
                  // of each layer's fields in a dictionary
                  ESRI.ArcGIS.Carto.IFeatureLayer featureLayer = null;
                  while ((featureLayer = enumLayer.Next() as ESRI.ArcGIS.Carto.IFeatureLayer) != null)
                  {
                      if (featureLayer.FeatureClass.ShapeType == ESRI.ArcGIS.Geometry.esriGeometryType.esriGeometryPolygon &&
                      featureLayer.FeatureClass.FeatureType == ESRI.ArcGIS.Geodatabase.esriFeatureType.esriFTSimple)
                          ComboLayers.Items.Add(featureLayer.Name);
      
                      if (featureLayer.Name == m_layer)
                          selectedIndex = ComboLayers.Items.Count - 1;
      
                      fieldNames = new string[featureLayer.FeatureClass.Fields.FieldCount];
                      for (int i = 0; i < fieldNames.Length; i++)
                          fieldNames[i] = featureLayer.FeatureClass.Fields.get_Field(i).Name;
      
                      m_fieldsDictionary.Add(featureLayer.Name, fieldNames);
                  }
      
                  mapDocument.Close();
                  mapDocument = null;
                  map = null;
      
                  // Toggle the init flag and initialize the layers drop-down to show the current SOE layer.  Init is used
                  // to prevent the drop-down's SelectedIndexChanged logic from setting m_field during initialization.
                  m_init = true;
                  ComboLayers.SelectedIndex = selectedIndex;
              }
      
    8. Add code to ComboLayers' SelectedIndexChanged event to update the m_layer member variable with the name of the selected layer and to get the field names for the selected layer and display them in the ComboFields combo box.
      1. Open the form in design mode.
      2. Select the ComboLayers combo box.
      3. In the properties window, click the Events button.
      4. Double click the SelectedIndexChanged event. This will add an event and open the code window to the implementation of the event.

        When the user selects a layer from the ComboLayers drop down other than the one currently selected, the code in the SelectedIndexChanged event will be executed. Add code to this event to update the m_layer member variable with the name of the selected layer and to get the names of the selected layer's fields from the m_fieldsDictionary member variable and add them to the ComboFields combo box:
              private void ComboLayers_SelectedIndexChanged(object sender, EventArgs e)
              {
                  m_layer = ComboLayers.Text;
      
                  ComboFields.Items.Clear();
      
                  // Add the fields for the currently selected layer to the fields drop-down
                  int selectedIndex = 0;
                  string[] fieldNames = m_fieldsDictionary[m_layer];
                  for (int i = 0; i < fieldNames.Length; i++)
                  {
                      ComboFields.Items.Add(fieldNames[i]);
      
                      // Get the index of the current field if the method is executing during initialization and
                      // the current field match's the SOE field
                      if (m_init && fieldNames[i] == m_field)
                          selectedIndex = ComboFields.Items.Count - 1;
                  }
      
                  // Update the field and layer properties if executing during initialization.  Otherwise, toggle the
                  // init boolean to indicate initialization is complete.
                  if (m_init)
                      m_init = false;
      
                  // Set the fields drop-down to show the current SOE field.
                  ComboFields.SelectedIndex = selectedIndex;
              }
      
    9. Add code to the ComboFields combo box's SelectedIndexChanged event to update the m_field member variable with the name of the selected field and fire the form's PageSite's PageChanged method.  Firing the PageChanged method notifies ArcCatalog that the page's contents have changed and enables the Apply button.
      1. Open the form in design mode.
      2. Select the ComboFields combo box.
      3. In the properties, click the Events button.
      4. Double click the SelectedIndexChanged event. This will add an event and open the code window to the implementation of the event.

        When the user clicks the ComboFields drop down to choose a field, the code in the SelectedIndexChanged event will be executed. Add code to this event to set the m_field member variable to the name of the selected field:
              private void ComboFields_SelectedIndexChanged(object sender, EventArgs e)
              {
                  // Update the current SOE field
                  m_field = ComboFields.Text;
      
                  // Notify ArcCatalog that the properties have changed
                  this.PageSite.PageChanged();
              }
      

Implementing the ArcCatalog property page classes

When the SpatialQuerySOE.ArcCatalog_CSharp project was created, two class files, SOEPropertyPage.cs and PropertyPage.cs, were created to house the logic for the classes that implement the ArcCatalog property page. SOEPropertyPage will be used to create an abstract class that implements the necessary interfaces and performs COM registration, while PropertyPage will store logic specific to the custom SpatialQuerySOE property page. Since many of the required interface members are not used in this scenario, it makes sense to abstract these away and thus simplify the implementation of the custom property page's logic. For similar simplification of other custom server object extension property page implementations, the SOEPropertyPage class can be used as a base class for those property pages as well.

References to the assemblies needed by the property page classes were added when implementing the property page's form.


Add code to the abstract property page base class:
  1. In Solution Explorer, open the SOEPropertyPage.cs class file.
  2. Since this class will be consumed by a COM client (ArcCatalog) it will need to be registered as a COM object type. The GuidAttribute attribute will define a unique GUID to identify the property page class via COM. For custom ArcCatalog server object exstension property pages, two interfaces must be implemented - IComPropertyPage and IAGSSOEParameterPage.
    namespace SOEUtilities
    {
        [System.Runtime.InteropServices.GuidAttribute("5514323A-02F8-48d3-B7BA-5BF07AD36F49")]
        public abstract class SOEPropertyPage : ESRI.ArcGIS.Framework.IComPropertyPage, ESRI.ArcGIS.CatalogUI.IAGSSOEParameterPage
        {
    
  3. For the property page to be accessible from ArcCatalog as an extension to ArcGIS Server server object capabilities, it must be registered in the "AGS Extension Parameter Pages" component category. This registration takes place when the property page class is registered with COM. A set of COM specific methods to register this property page in the appropriate category are provided below. The string parameter passed to both methods is the Guid defined for the custom property page. When registering the property page, the Guid associated with AGS Extension Parameter Page component category (A585A585-B58B-4560-80E3-87A411859379) is added to the "Implemented Categories" key in the registry. Once this is done, ArcCatalog will recognize that the custom page is associated with ArcGIS Server extension parameter pages. When our property page is unregistered, the custom property page's Guid is removed from the registry.  Note that, since SOEPropertyPage is an abstract class, classes that inherit from SOEPropertyPage are registered, rather than SOEPropertyPage itself.

            // Automatically register with ArcCatalog as a component, use categories.exe -> AGS Extension Parameter Pages to view 
            [System.Runtime.InteropServices.ComRegisterFunction()]
            static void RegisterFunction(String regKey)
            {
                Microsoft.Win32.Registry.ClassesRoot.CreateSubKey(regKey.Substring(18) + "\\Implemented Categories\\" + "{A585A585-B58B-4560-80E3-87A411859379}");
            }
    
            [System.Runtime.InteropServices.ComUnregisterFunction()]
            static void UnregisterFunction(String regKey)
            {
                Microsoft.Win32.Registry.ClassesRoot.DeleteSubKeyTree(regKey.Substring(18));
            }
    
    To view property pages registered with ArcGIS component categories, run the categories.exe in the <ArcGIS Install>\bin directory and select the category you are interested in.


  4. Two interfaces need to be implemented: IComPropertyPage and IAGSSOEParameterPage.

    • The IComPropertyPage interfaces defines the framework for ArcCatalog to work with property pages in general, such as show and hide. Custom server object extension property pages must implement logic for four IComPropertyPage members - PageSite, Activate, Show, and Hide.  Implement these as abstract members so that sub-classes of SOEPropertyPage are forced to implement them.

              public abstract ESRI.ArcGIS.Framework.IComPropertyPageSite PageSite { set; }
      
             public abstract int Activate();
      
             public abstract void Show();
      
             public abstract void Hide();
    • Two IComPropertyPage members, Height and Deactivate, can be implemented by custom server object extension property pages, but do not have to be. Implement these as virtual members so that sub-classes of SOEPropertyPage can implement these members, but are not required to.

              public virtual int Height { get { return 0; } }
      
             public virtual void Deactivate() { }
    • The remaining IComPropertyPage members are not used in custom server object extension property page implementations. Explicitly implement these members without declaring them virtual so that SOEPropertyPage sub-classes cannot implement them.

              public bool IsPageDirty { get { return false; } }
      
             public string Title
             {
                 get { return null; }
                 set { }
             }
      
             public int Priority
             {
                 get { return 0; }
                 set { }
             }
      
             public string HelpFile { get { return null; } }
      
             public int Width { get { return 0; } }
      
             public void Apply() { }
      
             public void Cancel() { }
      
             public int get_HelpContextID(int controlID) { return 0; }
      
             public void SetObjects(ESRI.ArcGIS.esriSystem.ISet objects) { }
      
             public bool Applies(ESRI.ArcGIS.esriSystem.ISet objects) { return false; }
    • The IAGSSOEParameterPage interface defines a set of properties that need to be explicitly implemented by a custom ArcGIS Server server object extension. This interface works with server object and extension properties defined in this class and the map server object's configuration file to update both the property page and the configuration file. The configuration properties for a server object are stored in the server object configuration file. Configuration files are named <service name>.<server object type>.cfg and stored on the SOM machine in the <ArcGIS Install>\server\user\cfg directory. For example, a map service named Yellowstone has a configuration file named Yellowstone.MapServer.cfg. Properties of server object extensions configured for use with a server object are also stored in the cfg file. In this scenario, a set of properties associated with our custom server object extension are being written to the configuration file ("LayerName" and "FieldName"). In addition, three properties are being read from the configuration file, two from our custom server object extension ("LayerName" and "FieldName") and one from the server object itself ("FilePath").

      The ServerObjectProperties property stores the properties of the server object only, not the extensions. If the server object extension has already been configured, the LayerName and FieldName properties will be available. If not, only the path to the server object's map document will be used. The setter portion of the ServerObjectProperties property will be called when the property page form opens.

      The ExtensionProperties property manages access to the properties of the server object extension in the server object configuration file. In this scenario, if the server object extension is enabled, the following entries are added to the server object configuration file:
      <Extension>
          <TypeName>SpatialQuerySOE</TypeName>
          <Enabled>true</Enabled>
          <Properties>
      	    <LayerName>Vegetation</LayerName>
      	    <FieldName>PRIMARY_</FieldName>
          </Properties>
          <Info>
      	    <WebEnabled>true</WebEnabled>
      	    <WebCapabilities></WebCapabilities>
          </Info>
      </Extension>
      Since all members of the IAGSSOEParameterPage interface must be implemented by custom server object extension property pages, implement them as abstract members to require sub-classes of SOEPropertyPage to implement them.
          public abstract ESRI.ArcGIS.esriSystem.IPropertySet ServerObjectProperties { get; set; }
      
         public abstract ESRI.ArcGIS.esriSystem.IPropertySet ExtensionProperties { get; set; }
      
         public abstract string ServerObjectExtensionType { get; }
      
         public abstract string ServerObjectType { get; }

Implement the property page:

  1. From the Solution Explorer window, open the PropertyPage.cs file.
  2. Since the interfaces required for custom server object extension property pages have been implemented by the abstract class SOEPropertyPage, the property page class only needs to inherit from that class.
    namespace SpatialQuerySOE.ArcCatalog
    {
        [System.Runtime.InteropServices.Guid("2EAD4A98-BB8C-4b88-A323-48F53653ACBF")]
        public class PropertyPage : SOEUtilities.SOEPropertyPage
        {
  3. The constructor of the Property page class will be called by ArcCatalog when the property page needs to be created and displayed. The constructor will assign member variables as follows:

    • propertyPage - new instance of the PropertyForm Windows form, which defines the interface of the property page
    • m_serverObjectType - the type of server object that the server object extension is registered for use with, which is, in this case, "MapServer." Used as the return value for the ServerObjectType property.
    • m_extensionType - The name of the server object extension's type, which is, in this case, "SpatialQuerySOE." Note that this value must match the name of the server object extension when it is registered with ArcGIS Server (discussed in the next section). Used as the return value for the ServerObjectExtensionType property.

    The destructor is explicitly defined to dispose of the property page.

         public PropertyPage()
        {
            propertyPage = new PropertyForm();
            m_serverObjectType = "MapServer";
            // Must be the same name used when registering the SOE
            m_extensionType = "SpatialQuerySOE";
        }
    
        ~PropertyPage()
        {
            propertyPage.Dispose();
            propertyPage = null;
        }
  4. Properties of the MapServer server object on which the server object extension is enabled are passed to the property page through the setter of ServerObjectProperties.  Since the property page form implemenation retrieves layers and fields from the map document underlying the server object, the path to that document must be retrieved from the server object's properties and passed to the form.
            public override ESRI.ArcGIS.esriSystem.IPropertySet ServerObjectProperties
            {
                get { return m_serverObjectProperties; }
                set
                {
                    // Pass the location of the map document underlying the current server object to the property form
                    m_serverObjectProperties = value;
                    propertyPage.SetMap(m_serverObjectProperties.GetProperty("FilePath").ToString());
                }
            }
  5. The LayerName and FieldName properties will differ depending on changes made via the property page form. When the custom server object extension's properties are applied in ArcCatalog (i.e. the custom server object extension is enabled and OK or Apply is clicked on the properties dialog), these properties are retrieved from the custom server object extension property page through the getter of the ExtensionProperties property. The retrieved properties are then either added to or updated in the map server object's configuration file. So changes to the LayerName and FieldName properties can be saved as shown in the code below.

    The saved server object extension properties are passed to the property page implementation through the setter of the ExtensionProperties property. So the corresponding properties on the property page form should be initialized there.
            public override ESRI.ArcGIS.esriSystem.IPropertySet ExtensionProperties
            {
                get
                {
                    m_extensionProperties.SetProperty("LayerName", propertyPage.Layer);
                    m_extensionProperties.SetProperty("FieldName", propertyPage.Field);
                    return m_extensionProperties;
                }
                set
                {
                    m_extensionProperties = value;
                    propertyPage.Layer = m_extensionProperties.GetProperty("LayerName").ToString();
                    propertyPage.Field = m_extensionProperties.GetProperty("FieldName").ToString();
                }
            }
    
  6. The member variables storing the type of the server object extension and the server object type to which the extension applies are set when the property page class is instantiated, as shown above. The implementations of the ServerObjectExtensionType and ServerObjectType properties simply return these member variables.
            public override string ServerObjectExtensionType { get { return m_extensionType; } }
           public override string ServerObjectType { get { return m_serverObjectType; } }

Set assembly properties for the SpatialQuerySOE.ArcCatalog_CSharp project

In the SpatialQuerySOE.ArcCatalog_CSharp project, open the AssemblyInfo.cs file and change it to reflect the following settings.

using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

[assembly: AssemblyTitle("SpatialQuerySOE.ArcCatalog")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("ESRI")]
[assembly: AssemblyProduct("SpatialQuerySOE.ArcCatalog")]
[assembly: AssemblyCopyright("Copyright © ESRI 2009")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]

[assembly: ComVisible(true)]
[assembly: GuidAttribute("2597052D-C832-40db-9DFF-39D49BE60357")]

[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

Build the server object extension ArcCatalog property page project

Build the SpatialQuerySOE.ArcCatalog_CSharp project and create the assembly SpatialQuerySOE.ArcCatalog.dll. The deployment section below will discuss the distribution and registration of this assembly.

Integrating the server object extension with ArcGIS Server Manager

Pages to define the properties of custom server object extensions can also be created for the ArcGIS Server Manager web administration application. This part of the walkthrough will demonstrate how to do this with the SpatialQuerySOE custom extension. As with the property page for ArcCatalog, the property page for Manager will allow interactive specification of the LayerName and FieldName properties.

Create the ArcGIS Server Manager server object extension property page project

First, you must create a new project that will hold the implementation of the custom property panel.
  1. In the Solution Explorer pane, right-click the ArcCatalog and Manager Integration solution folder, click Add, then select New Project…
  2. In the Add New Project dialog, under Project Types, select Visual C# Projects.
  3. Under Templates, click Class Library. Specify "SpatialQuerySOE.Manager_CSharp" as the project name
  4. Click OK.
  5. Remove the Class1.cs file.
  6. Right-click the project in Solution Explorer and select Properties.
  7. On the Application tab of the Properties page, specify an assembly name and default namespace of "SpatialQuerySOE.Manager."

When you are finished, this project will contain a class file that defines a property page for configuring SpatialQuerySOE in Manager. The class will contain the logic for defining the property page’s UI, retrieving the server object extension’s properties and populating the UI with them, and updating the server object extension’s properties from the UI.

Create the ArcGIS Server Manager property page class

  1. In the Solution Explorer pane, right click the SpatialQuerySOE.Manager_CSharp project, click Add, then click New Item...
  2. In the Add New Item dialog box, under Templates, click Class.
  3. Type “Configurator.cs” for the class’s name.

Implementing the ArcGIS Server Manager property page class

  1. The implementation code will require you to add references to several assemblies. Some of these contain the ArcObjects required for interacting with a service’s map document and the server object:
    • ESRI.ArcGIS.Carto
    • ESRI.ArcGIS.Geodatabase
    • ESRI.ArcGIS.Server
    • ESRI.ArcGIS.ServerManager
    • ESRI.ArcGIS.System
    Others define web controls and objects that will be used in building the page’s UI:
    • System.Web
    • System.Web.Extensions
    Add references to all of these assemblies to the SpatialQuerySOE.Manager_CSharp project.
  2. Open the Configurator.cs file.
  3. For a class to be integrated within Manager, it must implement the IServerObjectExtensionConfigurator interface. Change the class declaration to read as follows:
    namespace SpatialQuerySOE.Manager
    {
        public class Configurator : ESRI.ArcGIS.ServerManager.IServerObjectExtensionConfigurator
        {
        }
    }
  4. Declare a set of member variables to store references to the DropDownList that defines the extension’s layer (m_layersDropDown), the DropDownList that defines the extenion’s field (m_fieldsDropDown), the name of the currently selected layer (m_layer), the name of the currently selected field (m_field), and a JSON string that contains the names of the current service’s layers and the fields contained by each (m_jsonServiceLayersAndFields):
            private System.Web.UI.WebControls.DropDownList m_layersDropDown = new System.Web.UI.WebControls.DropDownList();
           private System.Web.UI.WebControls.DropDownList m_fieldsDropDown = new System.Web.UI.WebControls.DropDownList();
           private string m_layer;
           private string m_field;
           private string m_jsonServiceLayersAndFields = "{}";
  5. In implementing IServerObjectExtensionConfigurator, one of the methods you must define is LoadConfigurator. This method is called by Manager when the property page is loaded and is responsible for returning a string that contains the HTML defining the property page UI. The arguments passed to this method are as follows:
    • serverContext – server context for the service
    • ServerObjectProperties – properties of the server object (i.e. service)
    • ExtensionProperties – server object extension properties for the current server object
    • InfoProperties – global (i.e. server-level) server object extension properties
    • isEnabled – whether the extension is enabled on the current server object
    • servicesEndPoint – end point for the server’s web services
    • serviceName – name of the current service
    Stub out the method as follows:
    public string LoadConfigurator(ESRI.ArcGIS.Server.IServerContext serverContext System.Collections.Specialized.NameValueCollection ServerObjectProperties, 
    System.Collections.Specialized.NameValueCollection ExtensionProperties, System.Collections.Specialized.NameValueCollection InfoProperties, bool isEnabled, 
    string servicesEndPoint, string serviceName)
    {
    }
    First, you should check whether the extension is enabled and, if not, return an appropriate message:
    if (!isEnabled)
        return ("<span>No Properties to configure</span>");
    Next, initialize the member variables holding the extension’s layer and field names based on the passed-in extension properties:
    if (!string.IsNullOrEmpty(ExtensionProperties["LayerName"]))
        m_layer = ExtensionProperties["LayerName"];
    if (!string.IsNullOrEmpty(ExtensionProperties["FieldName"]))
        m_field = ExtensionProperties["FieldName"];
    Now declare and initialize the controls that will comprise the property configuration UI:
    System.Web.UI.HtmlControls.HtmlGenericControl propertiesDiv =
    new System.Web.UI.HtmlControls.HtmlGenericControl("propertiesDiv");
    propertiesDiv.Style[System.Web.UI.HtmlTextWriterStyle.Padding] = "10px";
    
    System.Web.UI.HtmlControls.HtmlTable table = new System.Web.UI.HtmlControls.HtmlTable();
    table.CellPadding = table.CellSpacing = 4;
    propertiesDiv.Controls.Add(table);
    
    System.Web.UI.HtmlControls.HtmlTableRow row = new System.Web.UI.HtmlControls.HtmlTableRow();
    table.Rows.Add(row);
    
    System.Web.UI.HtmlControls.HtmlTableCell cell = new System.Web.UI.HtmlControls.HtmlTableCell();
    row.Cells.Add(cell);
    cell.ColSpan = 2;
    
    System.Web.UI.WebControls.Label lbl = new System.Web.UI.WebControls.Label();
    lbl.Text = "Choose the layer and field.";
    cell.Controls.Add(lbl);
    
    row = new System.Web.UI.HtmlControls.HtmlTableRow();
    table.Rows.Add(row);
    
    cell = new System.Web.UI.HtmlControls.HtmlTableCell();
    row.Cells.Add(cell);
    
    lbl = new System.Web.UI.WebControls.Label();
    cell.Controls.Add(lbl);
    lbl.Text = "Layer:";
    
    cell = new System.Web.UI.HtmlControls.HtmlTableCell();
    row.Cells.Add(cell);
    cell.Controls.Add(m_layersDropDown);
    m_layersDropDown.ID = "layersDropDown";
    
    m_layersDropDown.Attributes["onchange"] = "ExtensionConfigurator.OnLayerChanged(this);";
    
    row = new System.Web.UI.HtmlControls.HtmlTableRow();
    table.Rows.Add(row);
    
    cell = new System.Web.UI.HtmlControls.HtmlTableCell();
    row.Cells.Add(cell);
    
    lbl = new System.Web.UI.WebControls.Label();
    cell.Controls.Add(lbl);
    lbl.Text = "Fields:";
    
    cell = new System.Web.UI.HtmlControls.HtmlTableCell();
    row.Cells.Add(cell);
    cell.Controls.Add(m_fieldsDropDown);
    m_fieldsDropDown.ID = "fieldsDropDown";
    Notice that the onchange attribute for the layers DropDownList (m_layerDropDown) has been set to invoke ExtensionConfigurator.OnLayerChanged. You will define this function when implementing the SupportingJavaScript method. To initialize the layers and field DropDownLists, add the code below. For clarity, you will define the logic to populate the property page’s UI with the current service’s layers and fields separately, in a method called populateDropDowns:
    string fileName = ServerObjectProperties["FilePath"];
    populateDropDowns(serverContext, fileName);
    Now that the UI’s components are created and initialized, they must be converted to an HTML string and returned. To do so, insert the following:
    System.IO.StringWriter stringWriter = new System.IO.StringWriter();
    System.Web.UI.HtmlTextWriter htmlWriter = new System.Web.UI.HtmlTextWriter(stringWriter);
    propertiesDiv.RenderControl(htmlWriter);
    string html = stringWriter.ToString();
    stringWriter.Close();
    return html;
  6. Implement the populateDropDowns method. First, access the map document underlying the current service and get the all the layers contained in the document that implement IGeoFeatureLayer. At the end of the method, in a finally block, add code to close the map document to avoid locking conflicts:
    private void populateDropDowns(ESRI.ArcGIS.Server.IServerContext serverContext, string mapDocPath)
    {
        ESRI.ArcGIS.Carto.IMapDocument mapDoc = null;
        ESRI.ArcGIS.Carto.IMap map = null;
    	
        try
        {
            mapDoc = serverContext.CreateObject("esriCarto.MapDocument") as ESRI.ArcGIS.Carto.IMapDocument;
            mapDoc.Open(mapDocPath, null);
            map = mapDoc.get_Map(0);
    
            ESRI.ArcGIS.esriSystem.UID id = serverContext.CreateObject("esriSystem.UID") as ESRI.ArcGIS.esriSystem.UID;
            id.Value = "{E156D7E5-22AF-11D3-9F99-00C04F6BC78E}";
            ESRI.ArcGIS.Carto.IEnumLayer enumLayer = map.get_Layers(id, true);
        }
        catch { }
        finally 
        { 
            if (mapDoc != null)
                mapDoc.close();
            mapDoc = null;
        }
    }
    Since the server object extension works with simple polygon layers, you will add logic to find these layers. The names of each of these layers will then be added to the layers drop-down list, and, if the layer is either the current extension layer or the first layer to be added, the names of the layer’s fields will be added to the fields drop-down list. You will also add the name of each layer and its fields to a Dictionary object for serialization to JSON. This JSON layer-fields mapping will be used in client-tier logic. Add the following code to the try block:
    ESRI.ArcGIS.Carto.IFeatureLayer featureLayer = (ESRI.ArcGIS.Carto.IFeatureLayer)enumLayer.Next();
    Dictionary<string, List<string>> layersAndFieldsDictionary = new Dictionary<string, List<string>>();
    bool addFields = false;
    while (featureLayer != null)
    {
        List<string> fieldsList = new List<string>();
    
        if (featureLayer.FeatureClass.ShapeType == ESRI.ArcGIS.Geometry.esriGeometryType.esriGeometryPolygon
            && featureLayer.FeatureClass.FeatureType == ESRI.ArcGIS.Geodatabase.esriFeatureType.esriFTSimple)
        {
            m_layersDropDown.Items.Add(featureLayer.Name);
    
            if (featureLayer.Name == m_layer || (m_layer == null && m_layersDropDown.Items.Count == 1)
                addFields = true;
    
            ESRI.ArcGIS.Geodatabase.IFields fields = featureLayer.FeatureClass.Fields;
            for (int i = 0; i < fields.FieldCount; i++)
            {
                ESRI.ArcGIS.Geodatabase.IField field = fields.get_Field(i);
                fieldsList.Add(field.Name);
    
                if (addFields) 
                    m_fieldsDropDown.Items.Add(field.Name);
            }
    
            addFields = false;
    
            layersAndFieldsDictionary.Add(featureLayer.Name, fieldsList);
        }
    
        featureLayer = (ESRI.ArcGIS.Carto.IFeatureLayer)enumLayer.Next();
    }
    
    System.Web.Script.Serialization.JavaScriptSerializer serializer = new System.Web.Script.Serialization.JavaScriptSerializer();
    m_jsonServiceLayersAndFields = serializer.Serialize(layersAndFieldsDictionary);
    The last step in populating the drop-down lists is setting the layer and field in the layers and fields drop-downs, if these have already been defined on the current server object:
    if (m_layer != null)
        m_layersDropDown.SelectedValue = m_layer;
    
    if (m_field != null)
        m_fieldsDropDown.SelectedValue = m_field;
  7. The next member of IServerObjectExtensionConfigurator that you must implement is a read-only property called SupportingJavaScript. This method can be used to return a string of JavaScript to execute when the property page is loaded. In this implementation, you will use this method to define the ExtensionConfigurator.OnLayerChanged function. In LoadConfigurator, you specified that this method be invoked when a layer is selected from the layers drop-down list. Note that ExtensionConfigurator is a global JavaScript object defined by the Manager application and reliably available to custom server object extension property pages. Also notice that this implementation of SupportingJavaScript makes use of the JSON-formatted layer-fields mapping:
    public string SupportingJavaScript
    {
        get
        {
            return string.Format(@"
                var layerFieldsMapping = {0};
    
                ExtensionConfigurator.OnLayerChanged = function(layersDropDown) {{
                    var layerName = layersDropDown.options[layersDropDown.selectedIndex].value;
                    var fieldsDropDown = document.getElementById('fieldsDropDown');  
                    var len = fieldsDropDown.options ? fieldsDropDown.options.length : 0;
    
                    for(var i=0; i < len; i++)
                        fieldsDropDown.remove(0);
    
                    var fieldsArray = layerFieldsMapping[layerName];
                    if(fieldsArray)
                    {{  
                        for(i=0; i < fieldsArray.length; i++)
                            fieldsDropDown.options.add(new Option(fieldsArray[i], fieldsArray[i]));
    
                        fieldsDropDown.options[0].selected = true;
                    }}
                }}", 
            m_jsonServiceLayersAndFields);
        }
    }
  8. Next you need to implement another read-only property, HtmlElementIds. This property returns a list of strings, where each string contains the DOM Element ID of a control that defines one of the server object extension’s properties. The IDs returned here are used in saving the extension’s properties. Specifically, they are used to retrieve the values that are passed in the Request parameter of the SaveProperties method. In this implementation, the elements defining the extension’s properties are the layers and fields drop-down lists, so you should implement HtmlElementIds to return a list containing the IDs of those controls:
    public List<string> HtmlElementIds { get { return new List<string>(new string[] { "layersDropDown", "fieldsDropDown" }); } }
  9. The last IServerObjectExtensionConfigurator method to be implemented is the SaveProperties method. The implementation of this method should update the extension’s properties according to the information specified on the property page. The method has no return value and defines the following arguments:
    • serverContext – server context for the service
    • Request – the values contained by the DOM elements with IDs matching those defined in HtmlElementIds. This ordinarily defines the values to be saved.
    • isEnabled – whether the extension is enabled on the current server object
    • ExtensionProperties – server object extension properties for the current server object. This must be instantiated in implementations of SaveProperties.
    • InfoProperties – global (i.e. server-level) server object extension properties. This must be instantiated in implementations of SaveProperties.
    Here there are only two properties that need to be saved – the layer and field. These are passed in the Request parameter and can be saved by defining them on the ExtensionProperties parameter. To do this, implement the method as follows:
    public void SaveProperties(ESRI.ArcGIS.Server.IServerContext serverContext, System.Collections.Specialized.NameValueCollection Request, bool isEnabled,
        out System.Collections.Specialized.NameValueCollection ExtensionProperties, out System.Collections.Specialized.NameValueCollection InfoProperties)
    {
        ExtensionProperties = new System.Collections.Specialized.NameValueCollection();
    
        string layerName = Request["layersDropDown"];
        if (!string.IsNullOrEmpty(layerName))
            ExtensionProperties.Add("LayerName", layerName);
    
        string fieldName = Request["fieldsDropDown"];
        if (!string.IsNullOrEmpty(fieldName))
            ExtensionProperties.Add("FieldName", fieldName);
    
        InfoProperties = new System.Collections.Specialized.NameValueCollection();
    }

Part 3: Registering the custom server object extension

Components of a custom server object extension must be registered in two ways.  One, the assemblies must be registered with .NET\COM on the machine they will be used.  And two, the server object extension must be registered with ArcGIS Server.

Thus far, four .NET assemblies have been created:

Since these assemblies are designed to expose .NET types as COM types, the .NET Framework is required on all machines where this Microsoft .NET-based server object extension will be registered. Utilities included with the .NET Framework will enable the appropriate configuration and registration of the .NET assemblies.

The following diagram illustrates which applications or processes must have access to which libraries.  Note that the machine on which the SOM is running does not require the availability of any of the libraries created in this scenario.  The SOM does, however, require that the custom server object extension be registered with it.

SpatialQuerySOE.dll

The SpatialQuerySOE assembly must be registered on all Server Object Container machines (ArcSOC.exe) where a server object may enable and use the server object extension. As a COM client, the ArcSOC.exe process can work with other COM objects and types. To expose the .NET types in the SpatialQuerySOE assembly to COM, the .NET Assembly Registration tool (regasm.exe) will be used. To register the SpatialQuerySOE.dll, do the following:

  1. Open a Visual Studio command prompt and navigate to the location of the the SpatialQuerySOE.dll. Use the following command:
    regasm SpatialQuerySOE.dll /codebase
    

    The regasm tool reads the metadata within an assembly and adds the necessary entries to the registry. The /codebase option registers the explicit location of the assembly. The codebase option will return a warning indicating that the assembly should be signed. If you choose to sign the assembly, you can also place it in the Global Assembly Cache (GAC) and remove the "/codebase" option when using regasm. To sign the assembly and add it to the GAC, do the following:

    1. Create a strongly named key:
      sn -k MyKeyPair.snk
      
    2. Add or uncomment the following entries to the AssemblyInfo.cs file in the SpatialQuerySOE_CSharp project. Change the path for the AssemblyKeyFile attribute to the key created in the previous step.
      [assembly: AssemblyDelaySign(false)]
      [assembly: AssemblyKeyFile("C:/temp/MyKeyPair.snk")]
      
    3. Rebuild the SpatialQuerySOE_CSharp project.
    4. Run the following command:
      regasm SpatialQuerySOE.dll
      
    5. Add the assembly to the GAC:
      gacutil -i SpatialQuerySOE.dll
      						
  2. Important:  Make sure that the ArcGIS Server Object Container user account has read/execute access on the directory that contains the registered assembly.
  3. To unregister the assembly and type library, use the following command:
    regasm /unregister SpatialQuerySOE.dll


SpatialQuerySOE.Interfaces.dll

The SpatialQuerySOE.Interfaces assembly stores interface definitions implemented by the server object extension, therefore it must be registered on all Server Object Container machines (ArcSOC.exe) where a server object may enable and use the server object extension.  As a COM client, the ArcSOC.exe process can work with the server object extension classes and interfaces as COM objects and types. 

In addition, the SpatialQuerySOE.Interfaces.dll must be registered on any application client machine that will consume the server object extension.  As a result, the Microsoft .NET Framework used to build the assembly must be installed on the client machine.  The SpatialQuerySOE.Interfaces.dll contains the interfaces a client application will use to access server object extension objects remotely (just like working with ArcObjects remotely via ArcGIS Server).  No business logic is distributed with the SpatialQuerySOE.Interfaces.dll. 

To expose the .NET interface types in the SpatialQuerySOE.Interfaces assembly to COM, the .NET Assembly Registration tool (regasm.exe) will be used. To register the SpatialQuerySOE.Interfaces.dll, do the following:

  1. Open a Visual Studio command prompt and navigate to the location of the the SpatialQuerySOE.Interfaces.dll. Use the following command:
    regasm SpatialQuerySOE.Interfaces.dll /tlb:SpatialQuerySOE.Interfaces.tlb
    

    The regasm tool reads the metadata within an assembly.  Since the assembly only contains interface types, a type library must be generated using the /tlb option.  The location of the tlb is stored in the registry.          

  2. Important:  Make sure that the ArcGIS Server Object Container user account has read/execute access on the registered type library.  On the client application machine, make sure the user account that application is running as has read/execute access to the registered type library.   To work with the types, add a reference to the SpatialQuerySOE.Interfaces.dll in the client application (discussed in part 4 of this scenario).     
  3. To unregister the assembly and type library, use the following command:
    regasm /unregister SpatialQuerySOE.Interfaces.dll /tlb:SpatialQuerySOE.Interfaces.tlb


SpatialQuerySOE.ArcCatalog.dll

The SpatialQuerySOE.ArcCatalog assembly must be registered on all machines where ArcCatalog will be used to administer GIS Servers where the server object extension can be enabled.    Since the assembly is a .NET component, the Microsoft .NET Framework used to build the assembly must also be installed.  As a COM client, ArcCatalog can work with other COM objects and types.  To expose the .NET types and Windows form in the SpatialQuerySOE.ArcCatalog assembly to COM so they can be used by ArcCatalog, the .NET Assembly Registration tool (regasm.exe) will be used. To register the SpatialQuerySOE.ArcCatalog.dll, do the following:

  1. Open a Visual Studio command prompt and navigate to the location of the the SpatialQuerySOE.ArcCatalog.dll. Use the following command:
    regasm SpatialQuerySOE.ArcCatalog.dll /codebase
    

    The regasm tool reads the metadata within an assembly and adds the necessary entries to the registry. The /codebase option registers the explicit location of the assembly. The codebase option will return a warning indicating that the assembly should be signed. If you choose to sign the assembly, you can also place it in the Global Assembly Cache (GAC) and remove the "/codebase" option when using regasm. To sign the assembly and add it to the GAC, do the following:

    1. Create a strongly named key:
      sn -k MyKeyPair.snk
      
    2. Add or uncomment the following entries to the AssemblyInfo.cs file in the SpatialQuerySOE.ArcCatalog_CSharp project. Change the path for the AssemblyKeyFile attribute to the key created in the previous step.
      [assembly: AssemblyDelaySign(false)]
      [assembly: AssemblyKeyFile("C:/temp/MyKeyPair.snk")]
      
    3. Rebuild the SpatialQuerySOE.ArcCatalog_CSharp project.
    4. Run the following command:
      regasm SpatialQuerySOE.ArcCatalog.dll
      
    5. Add the assembly to the GAC:
      gacutil -i SpatialQuerySOE.ArcCatalog.dll
      						
  2. Important:  Make sure that the user account running ArcCatalog has read/execute access on the directory that contains the registered assembly.
  3. To confirm that the property page registered with ArcGIS component categories correctly, run the categories.exe in <ArcGIS Install>\bin directory and open the AGS Extension Parameter Pages folder.  The SpatialQuerySOE.ArcCatalog property page should be listed.
  4. To unregister the assembly and type library, use the following command:
    regasm /unregister SpatialQuerySOE.ArcCatalog.dll

SpatialQuerySOE.Manager.dll

The SpatialQuerySOE.Manager assembly must be available on any machine that hosts the ArcGIS Server Manager web administration application and is to allow administrators to configure the custom server object extension. To make the assembly available to Manager, it should be installed in the Global Assembly Cache (GAC) and Manager’s web.config file must be modified slightly. This can be done as follows:

  1. In Visual Studio, right-click the SpatialQuerySOE.Manager_CSharp project and select Properties.
  2. On the Properties page, select the Signing tab.
  3. Check the Sign the Assembly checkbox and select <New…> from the drop-down list below the checkbox.
  4. On the dialog that appears, choose a name for the key file and, if desired, add a password to protect it.
  5. Rebuild the project.
  6. Open a Visual Studio command prompt and navigate to the location of the SpatialQuerySOE.Manager.dll file.
  7. Input the following command to install the assembly into the GAC:
    gacutil –i SpatialQuerySOE.Manager.dll
  8. For updating Manager’s web.config file, you will need to know the PublicKeyToken of the assembly. To retrieve this, use the following command:
    sn –T SpatialQuerySOE.Manager.dll
  9. In Visual Studio, open Manager’s web.config file. The default location of the file is <Install Drive Letter>:\Inetpub\wwwroot\ArcGIS\Manager\web.config.
  10. In the Extensions section, add an Extension element to make Manager aware of the extension. The element will appear as follows, though the PublicKeyToken will differ:
    <Extension name="SpatialQuerySOE" DisplayName="Spatial Query" Capabilities="" web="true" ConfiguratorType="SpatialQuerySOE.Manager.Configurator, SpatialQuerySOE.Manager, 
            Version=1.0.0.0, Culture=neutral, PublicKeyToken=a640acaea98a8392"/>
  11. In the ServerObjectTypes section, find the MapServer ServerObjectType. Since SpatialQuerySOE extends MapServer, you need to add it to the Extensions attribute. The modified element will look similar to the following. Note the extensions listed may differ from those below, as this depends on which extensions you have installed:
    <ServerObjectType name="MapServer" Extensions="MapServer,WCSServer,WMSServer,WFSServer,MobileServer,KmlServer,NAServer,SpatialQuerySOE" FileExtensions=".mxd;.pmf;.msd"/>
  12. At a command prompt, restart IIS by entering the following command:
    iisreset


Register the server object extension with ArcGIS Server

The server object extension needs to be registered with ArcGIS Server - more specifically, it needs to be registered with the Server Object Manager (SOM).  Registration will involve using ArcObjects in the ESRI.ArcGIS.Server library to add information about the availability and properties of the server object extension to ArcGIS Server configuration files.   Using .NET, the easiest way to accomplish this is to create a console application in Visual Studio and add the necessary code.  The following steps will cover creating the console application project, adding the code, and running the application.  
  1. Create the server object extension registration project.
    1. In Visual Studio, open the ArcGIS_Spatial_Query_SOE_CSharp2008 solution, if it is not already.
    2. In the Solution Explorer pane, right-click the solution, then select Add --> New Solution Folder.  Name the folder "Registration."
    3. In Solution Explorer, right-click the Registration folder, click Add, then click New Project...
    4. In the New Project dialog box, under Project Types, click the Visual C# Projects category.  Under Templates, click Console Application.
    5. Type a path to where you want to put this project.
    6. For the project name, type "RegisterSOE_CSharp" and click OK. 
  2. Add a reference to the following assemblies:
    • ESRI.ArcGIS.ADF.Connection
    • ESRI.ArcGIS.Server
  3. Open the Program.cs file.  In the autogenerated Main method, add code to initiate a connection to the SOM: check whether the server object extension is registered, and either register or unregister the server object extension, depending on the command line arguments.  To check the registration status of the extension, use a method called ExtensionRegistered, which you will define below.  After the main action has executed, output a message to inform the user of the command's status.
        // Must run as an user in the agsadmin group on the SOM
        ESRI.ArcGIS.ADF.Connection.AGS.AGSServerConnection agsServerConnection = new ESRI.ArcGIS.ADF.Connection.AGS.AGSServerConnection();
        agsServerConnection.Host = "localhost";
        agsServerConnection.Connect();
        ESRI.ArcGIS.Server.IServerObjectAdmin2 serverObjectAdmin = agsServerConnection.ServerObjectAdmin as ESRI.ArcGIS.Server.IServerObjectAdmin2;
    Initialize the name of the server object extension.
    Note: this name must match the server object extension name as defined in the property pages.
       string extensionName = "SpatialQuerySOE";
    Check whether a command line argument of "/unregister" was specified. If so, unregister the extension if it is currently registered. Then output a message informing the user of the command's result.
        // Check command line arguments to see if SOE is to be unregistered
        if (args.Length == 1 && args[0] == "/unregister")
        {
            // Check whether the SOE is registered
            if (ExtensionRegistered(serverObjectAdmin, extensionName))
            {
                // Delete the SOE
                serverObjectAdmin.DeleteExtensionType("MapServer", extensionName);
                Console.WriteLine(extensionName + " successfully unregistered");
            }
            else
                Console.WriteLine(extensionName + " is not registered with ArcGIS Server");
        }
    If the "/unregister" argument was not specified, then register the extension. First, check whether the extension is already registered. If not, instantiate and initalize a ServerObjectExtensionType object, which defines the server object extension's registration information. Note that this is where the server object extension's name is specified, which, as explained above, must match the name specified in the extension's property pages. Then add the extension to the server.
        else
        {
            // Check whether the SOE is registered
            if (!ExtensionRegistered(serverObjectAdmin, extensionName))
            {
                ESRI.ArcGIS.Server.IServerObjectExtensionType2 serverObjectExtensionType = 
                    serverObjectAdmin.CreateExtensionType() as ESRI.ArcGIS.Server.IServerObjectExtensionType2;
    
                // Must match the namespace and class name of the class implementing IServerObjectExtension
                serverObjectExtensionType.CLSID = "SpatialQuerySOE.Extension";
                serverObjectExtensionType.Description = "Spatial Query Server Object Extension";
                serverObjectExtensionType.Name = extensionName;
                        
                // Name that will be shown in the capabilities list on property pages
                serverObjectExtensionType.DisplayName = "Spatial Query";
    
                // Register the SOE with the server
                serverObjectAdmin.AddExtensionType("MapServer", serverObjectExtensionType);
    
                Console.WriteLine(extensionName + " successfully registered with ArcGIS Server");
            }
            else
                Console.WriteLine(extensionName + " is already registered with ArcGIS Server");
        }
  4. Implement the ExtensionRegistered method, which will check whether a server object extension with the specified name has already been registered with the ArcGIS Server instance.
    static private bool ExtensionRegistered(ESRI.ArcGIS.Server.IServerObjectAdmin2 serverObjectAdmin, string extensionName)
    {
        // Get the extensions that extend MapServer server objects
        ESRI.ArcGIS.Server.IEnumServerObjectExtensionType extensionTypes = serverObjectAdmin.GetExtensionTypes("MapServer");
        extensionTypes.Reset();
    
        // If an extension with a name matching that passed-in is found, return true
        ESRI.ArcGIS.Server.IServerObjectExtensionType extensionType = extensionTypes.Next();
        while (extensionType != null)
        {
            if (extensionType.Name == extensionName)
                return true;
    
            extensionType = extensionTypes.Next();
        }
    
        // No matching extension was found, so return false
        return false;
    }
  5. Build the RegisterSOE_CSharp project and run it.  The application must be run with a user account that has administrative privledges on the ArcGIS Server - thus the user must be part of the agsadmin group on the SOM machine.  If registration was successful, the line "SpatialQuerySOE successfully registered with ArcGIS Server" will appear in the console window. 
  6. On the SOM machine, the following entry will be added to the ServerTypesExt.dat file in the <ArcGIS Install>\server\system directory:
    <types>
        <ServerObjectType>
            <Name>MapServer</Name>
            <ExtensionTypes>
                <ExtensionType>
                    <Name>SpatialQuerySOE</Name>
                    <DisplayName>SpatialQuery</DisplayName>
                    <CLSID>SpatialQuerySOE.Extension</CLSID>
                    <Description>Spatial Query Server Object Extension</Description>
                </ExtensionType>
            </ExtensionTypes>
        </ServerObjectType>
    </types>

Configuring the server object extension on a map service

Since you have created custom server object extension property pages for both ArcCatalog and ArcGIS Server Manager, enabling and configuring the server object extension can be done using either of these programs. A set of sample data is provided for this scenario in <ArcGIS Install\DeveloperKit\SamplesNET\Server\data\Yellowstone.  The Yellowstone.mxd map document contains two layers: a polygon feature class and a grid.  The polygon feature class is stored in the personal geodatabase veg.mdb.  It contains polygons referencing the geographic areas of different vegetation types in Yellowstone National Park.  The grid is a DEM in Arc\Info GRID format and merely provides a topographic reference for the area.   In this section, you will add the Yellowstone map document as an ArcGIS Server map service and enable the SpatialQuerySOE extension using a field in the vegetation layer. Using either ArcCatalog or ArcGIS Server Manager, publish the Yellowstone.mxd map document as a new map service. For information on how to publish services, refer to the document Adding a New Service in the ArcGIS Server Help.

Configuring the server object extension in ArcCatalog

  1. Open ArcCatalog and connect to ArcGIS Server as an administrator.
  2. Right-click the Yellowstone service and click Service Properties from the context menu.
  3. Click the Capabilities tab.
  4. In the list of available capabilities (in the upper left corner of the dialog), check the box next to the "Spatial Query" extension. Checking this box enables your custom server object extension.
  5. In the Properties section of the Capabilities tab, you will see the form you implemented in the SpatialQuerySOE.ArcCatalog_CSharp project. Set the Layer property to "Vegetation" and Field property to "PRIMARY_".   The "PRIMARY_" field stores the vegetation type name and will be used by the SpatialQuerySOE extension to aggregate the area totals for the selected region.

  6. Click OK.

Configuring the server object extension in ArcGIS Server Manager

  1. Open a web browser, navigate to the ArcGIS Server Manager login page, and login as an administrator.
  2. In the left-hand menu, click Services. 
  3. From the list of services, select the service you created from the Yellowstone.mxd map document.
  4. Click the Capabilities tab.
  5. In the list of capabilities (in the upper left corner of the tab), check the box next to the the "Spatial Query" extension.  This will enable the custom server object extension you have created.
  6. In the properties section of the tab, the configuration interface you implemented in the SpatialQuerySOE.Manager_CSharp project will appear.  Set the Layer property to "Vegetation" and Field property to "PRIMARY_".   The "PRIMARY_" field stores the vegetation type name and will be used by the SpatialQuerySOE extension to aggregate the area totals for the selected region.


  7. Click Save.

The next section will cover the creation of a Web ADF application to utilize the SpatialQuerySOE extension via the Yellowstone map service. 
  

Part 4: Creating the client web application

Now that you have developed your server object extension and deployed it with an ArcGIS Server map service, you can build a web application to make use of it.  In this example, we will create a web application from scratch using the Web ADF's components.  The Web ADF and ASP.NET will provide the basic framework for mapping, capturing user events, and displaying results.  We will create a custom tool to interact with the server object extension.

Register the COM types in the SpatialQuerySOE.Interfaces assembly

As mentioned in part 3, the SpatialQuerySOE.Interfaces.dll must be registered on any client machine that will consume the server object extension.  The assembly contains COM interface types that will be used by the web application to work with the custom server object extension's COM objects remotely.  The SpatialQuerySOE.Interfaces.dll was built at the conclusion of part 1 of this scenario.

  1. Open a Visual Studio command prompt and navigate to the location of the the SpatialQuerySOE.Interfaces.dll.
  2. Use the following command:
    regasm SpatialQuerySOE.Interfaces.dll /tlb:SpatialQuerySOE.Interfaces.tlb
    

    The regasm tool reads the metadata within an assembly and adds the necessary entries to the registry.  Since the assembly only contains interface types, a type library must be generated using the /tlb option.  The location of the tlb is stored in the registry. 


Create the web application project
  1. Start Visual Studio.
  2. Click File, click New, then click Web Site.
  3. In the New Web Site dialog box, under Templates, click ASP.NET Web Site.
  4. For the Web application name and url enter "http://localhost/ArcGIS_Spatial_Query_SOE_CSharp/SpatialQuerySOE_WebSite_CSharp"

Add and configure Web ADF and ASP.NET controls

  1. Add the following controls in the locations specified in the screenshot below.  The following steps will configure each control: MapResourceManager, Map, Toolbar, Toc, Label, Textbox, Checkbox, and div

  2. Configure the MapResourceManager control
    1. Set the ResourceItems property on the MapResourceManager control.  Click the "Edit Map Resources" smart tag on the control in the design view -or- in the MapResourceManager property page click the ellipsis next to the ResourceItems property to display the MapResourceInfo Collection Editor dialog.
        
    2. In the MapResourceItem Collection Editor dialog, add a new MapResourceItem (click the Add button).  Change its name to "Vegetation Layers."
    3. Set the MapResourceItem's Definition property by clicking the ellipsis in the property page to display the Map Resource Definition Editor.  Select an "ArcGIS Server Local" data source type and specify the name of the SOM machine on which the Yellowstone map service is running with the custom server object extension enabled.

    4. Once the data source is defined, clicking the ellipsis next to the Resource property opens the ArcGIS Resource Definition Editor dialog, which provides a list of available map services and data frames. Select the Yellowstone map service and the default data frame.


    5. Note that the Identity property is grayed out and cannot be set from the Map Resource Definition Editor dialog.  At design-time, the identity of the user running Visual Studio is used to connect to an ArcGIS Server local data source.  At runtime, that identity is established by the Web application.  Only one identity can be used to define access to all ArcGIS Server local data sources in a single Web application .  You can define this identity by right-clicking the Web project in Solution Explorer and selecting the Add ArcGIS Identity option.  Enter the identity credentials which will be used to access ArcGIS Server local resources at runtime.  This information will be added to the web.config file within a standard ASP.NET identity tag.  If the "Encrypt identity in web.config" checkbox is checked, the identity tag will be encrypted; otherwise, the username and password will be stored as clear text.    



  3. Configure the Map control by setting the MapResourceManager property.  Select the Map control in design view or in the Properties window, then click the dropdown list for the MapResourceManager property.  Select the name of the MapResourceManager configured in the previous step (by default "MapResourceManager1").
  4. Configure the Toolbar control by setting its BuddyControls and ToolbarItems properties.

    Toolbar when first added to page
                                         down arrow
    Toolbar with properties set
    1. Toolbar control propertiesSpecify the map that the toolbar will work with through the toolbar control’s BuddyControls property. Set the BuddyControlType property to "Map" and click the ellipsis button in the BuddyControls property value to display the BuddyControls Collection Editor dialog. On the dialog, add an item and associate it with the map control (Map1). 
    2. To add an item to your toolbar, follow the steps below:

      1. Click the Toolbar control, then, in the Properties window, click the … button next to the ToolbarItems property. You will see a form with a list of available toolbar items and current toolbar contents.
      2. In the Toolbar Items column, select and add the Zoom In, Zoom Out, and Pan tools individually.  The custom tool will be added in a later step.
  5. Configure the Toc control by setting the BuddyControl property.  Select the Toc control in design view or in the Properties window, then click the dropdown list for the BuddyControl property.   Select the name of the Map control configured in the previous step (by default "Map1").
  6. Configure the Label, Textbox, and Checkbox control using the screenshot above.  Confirm that the id for each control is Label1, Textbox1, and Checkbox1, respectively.  Add the appropriate text for the Label and Checkbox control as shown in the screenshot in the first step.  Set the default value in Textbox1 to 10000. 
  7. Configure the div and GridView controls.  Select the HTML div control and set its Id value to "summaryStatsDiv" (either in the property page or HTML source).  Insert (drag and drop) the GridView control into the div.   Resize the GridView control to a height and width of approximately 200 by 250. 


Add JavaScript to capture user input and show tool progress

The custom server object extension will be utilized via a custom tool.   The web application user interface contains a textbox and checkbox to store information that the custom tool will use to calculate buffer distance and update a GridView with the custom tool output.  In this section, you will add JavaScript to the web application to capture textbox and checkbox values.  You will also add code to display a progress indicator while the custom tool is executing.

  1. Add JavaScript to the Default.aspx page to declare page variables and execute some initialization logic. The initialization logic, which is hooked to the Microsoft AJAX init event, wires event handlers for the map's onServerRequest and the toolbar's onToolSelected events and checks whether a ScriptManager is present in the application. Add the following code just inside the closing tag of the page's form element:
    <script language="javascript" type="text/javascript">
    
            // Tracks whether the vegetation summary tool is currently selected
            var vegToolSelected = false;
            
            // Tracks whether a scriptManager is on the page
            var hasScriptManager = false;
            
            // Add the initialize function to the page's AJAX initialization routine
            Sys.Application.add_init(initialize);
               
            // Called during AJAX initialization
            function initialize()
            {   
                // Get a reference to the client-side Web ADF map object     
                var map = $find('Map1'); 
                // Add an event that fires whenever the map sends a request to the server.
                map.add_onServerRequest(mapServerRequest);
                
                // Get a reference to the client-side Web ADF toolbar object
                var toolbar = $find('Toolbar1');
                // Add an event handler that fires whenever a tool on the toolbar is selected.
                toolbar.add_onToolSelected(toolbarToolSelected);
                
                // Check to see whether a ScriptManager is on the page by seeing whether its client-tier representation exists
                if (Sys && Sys.WebForms && Sys.WebForms.PageRequestManager) {
                    // There is a ScriptManager on the page.  
                    hasScriptManager = true;
                } 
                
            }        
      </script>
  2. Inside the script element, define the event handler for the toolbar's onToolSelected event. The handler will simply check to see whether the custom tool was selected and update the appropriate page variable accordingly:
  3.         // Fires when a tool on Toolbar1 is selected
            function toolbarToolSelected(toolbarObject, activeToolInfo)
            {
                // Check whether the clicked tool is one of our custom tools and set the tracking boolean accordingly
                if (activeToolInfo.tool.name == "VegetationSummary")
                    vegToolSelected = true;
                else
                    vegToolSelected = false;        
            }
  4. Now define the handler for the onMapServerRequest event. This event fires whenever the map issues a request, which includes when a user clicks the map with the custom tool activated. The handler will check whether the custom tool is activated and, if so, display the progress indicator. Also, if callbacks rather than partial postbacks are being used for asynchronous functionality (i.e. a ScriptManager is not present on the page), add the values of the input textbox and checkbox to the request via the helper method addCustomArguments, which you will define in the next step. The values of these controls need to be included in the request so that they can be retrieved in the custom tool implementation.

    If partial postbacks are being used, the values of all ASP.NET server controls are automatically included in the request.
  5.         // Fires when the map sends a request to the server
            function mapServerRequest(mapObject, inputArgument)
            {
                // Check whether the vegetation summary tool is selected and a ScriptManager is on the page.  If the tool is selected, but there is no ScriptManager, then the values of the distance
                // textbox and show summary results checkbox must be explicitly added to the page request arguments.  Also, display the busy indicator.
                if (vegToolSelected) {
                    if (!hasScriptManager)
                        inputArgument.argument = addCustomArguments(inputArgument.argument);
    
                    var busyIndicator = $get('busyIndicator');
                    busyIndicator.style.display = '';
                }
            }
  6. Define the addCustomArguments method. This will retrieve the values of the input textbox and checkbox and add them to the passed-in request:
            function addCustomArguments(args)
            {            
                // Get the search distance and the value of the show summary results checkbox
                var searchDistance = $get('SearchDistanceTextBox').value;
                var showSummaryResults = $get('ShowResultsCheckBox').checked;
                
                // Add the control values to the passed-in arguments with the control IDs as keys.
                args += String.format('&SearchDistanceTextBox={0}&ShowResultsCheckBox={1}',  searchDistance, showSummaryResults);            
                return args;
            }

Add a custom tool to work with the server object extension
 

  1. In Solution Explorer, right-click the web project and select "Add New Item...".  In the Add New Item dialog, under the Visual Studio Installed Templates section, select the "Class" item.  Set the class file name to "VegTool.cs" and make sure the language is "Visual C#".  Visual Studio will prompt you to create an "App_Code" folder and place the new class file inside.  Click Yes.  The VegTool.cs file should open for you to start adding content.  This file will contain the executable code associated with the custom tool.

  2. The server object extension exposes two interfaces: SpatialQuerySOE.IExtension and SpatialQuerySOE.IResults.  As a client, the web application needs a reference to the type definition for both interfaces to remotely utilize the COM objects they represent.  The COM interface types are provided in the SpatialQuerySOE.Interfaces.dll. 

    In Solution Explorer, right-click the Web project and select "Add Reference...".  In the dialog, browse to the location of the SpatialQuerySOE.Interfaces.dll and add it to the project.

  3. Additional components are also required to work with ArcGIS Server and the Web ADF.  In Solution Explorer, right-click the Web project and select "Add ArcGIS Reference...".  In the dialog, select the following components, click Add, then Finish:

    • ESRI.ArcGIS.ADF.ArcGISServer
    • ESRI.ArcGIS.ADF.Connection
    • ESRI.ArcGIS.ADF.Web.DataSources.ArcGISServer
    • ESRI.ArcGIS.Carto
    • ESRI.ArcGIS.Display
    • ESRI.ArcGIS.Geodatabase
    • ESRI.ArcGIS.Geometry
    • ESRI.ArcGIS.Server
  4. Remove the VegTool constructor and implement the IMapServerToolAction interface on the VegTool class.  The code for the class should begin as follows: 
    public class VegTool : ESRI.ArcGIS.ADF.Web.UI.WebControls.Tools.IMapServerToolAction 
    {
        public void ServerAction(ESRI.ArcGIS.ADF.Web.UI.WebControls.ToolEventArgs toolEventArgs)
        {
    
  5. Next, get the user input from the client.  To do this, first retrieve a reference to the buddied Map control and then a reference to the Page containing then Map.  The value in SearchDistanceTextBox is the buffer distance and ShowResultsCheckBox determines if a table of data will be returned to the browser.  If the tool initiates a callback.  If the tool initiates a partial postback (i.e. a ScriptManager is on the page), the argument/value pairs for the input controls will be included directly in the HTTP request.  Otherwise, the tool initiates a callback, and the __CALLBACKPARAM argument in the HTTP request will contain the argument/value pairs.     
            // Get the map control that was clicked
            ESRI.ArcGIS.ADF.Web.UI.WebControls.Map adfMap = toolEventArgs.Control as ESRI.ArcGIS.ADF.Web.UI.WebControls.Map;
            
            // Get the page containing the map control
            System.Web.UI.Page page = adfMap.Page;
    
            // Get the request parameters
            System.Collections.Specialized.NameValueCollection requestParameters = null;       
            if (System.Web.UI.ScriptManager.GetCurrent(page) == null)
                requestParameters = ESRI.ArcGIS.ADF.Web.UI.WebControls.CallbackUtility.ParseStringIntoNameValueCollection(page.Request.Params["__CALLBACKPARAM"]);
            else
                requestParameters = page.Request.Params;
    
            // Get the search distance
            double searchDistance;
            if (!System.Double.TryParse(requestParameters["SearchDistanceTextBox"], out searchDistance))
                searchDistance = 10000;
    
            // Get the value of the Show Results checkbox
            bool showSummaryStats;
            if (!bool.TryParse(requestParameters["ShowResultsCheckBox"], out showSummaryStats))
                showSummaryStats = false;
  6. The ArcGIS Server implementation of the Web ADF Common API will be used to get the map resource for an ArcGIS Server local data source (MapResourceLocal).  Local data sources provide access to the ArcObjects API for a server object.  So in this case, you have access to server context and the stateless interface for map services, IMapServer.  The user click in the browser will provide the center point for a buffer to select and aggregate features.  Server context is used to create an ArcObjects Point for use with the custom server object extension.  Buffer distance is set with the value provided by the user in SearchDistanceTextBox.
            // Get MapFunctionality and MapResource for the Vegetation Layers resource item
            ESRI.ArcGIS.ADF.Web.DataSources.ArcGISServer.MapFunctionality agsMapFunctionality = 
                adfMap.GetFunctionality("Vegetation Layers") as ESRI.ArcGIS.ADF.Web.DataSources.ArcGISServer.MapFunctionality;
            ESRI.ArcGIS.ADF.Web.DataSources.ArcGISServer.MapResourceLocal agsMapResourceLocal = 
                (ESRI.ArcGIS.ADF.Web.DataSources.ArcGISServer.MapResourceLocal) agsMapFunctionality.MapResource;
    
            // Get the server context
            ESRI.ArcGIS.Server.IServerContext serverContext = agsMapResourceLocal.ServerContextInfo.ServerContext;
    
            // Get the click point as an ArcObjects point
            ESRI.ArcGIS.ADF.Web.UI.WebControls.MapPointEventArgs mapPointEventArgs = toolEventArgs as ESRI.ArcGIS.ADF.Web.UI.WebControls.MapPointEventArgs;
            ESRI.ArcGIS.Geometry.IPoint clickPoint = (ESRI.ArcGIS.Geometry.IPoint)serverContext.CreateObject("esriGeometry.Point");
            clickPoint.X = mapPointEventArgs.MapPoint.X;
            clickPoint.Y = mapPointEventArgs.MapPoint.Y;
    
  7. Every server object type implements the ArcObjects IServerObjectExtensionManager interface to support the discovery of server object extensions.  If a server object extension is enabled on the server object, the FindExtensionByTypeName method can be used to return a reference to the server object extension with its registered name.  In this scenario, the custom server object extension class SpatialQuerySOE.Extension implements the SpatialQuerySOE.IExtension interface, which exposes a single method, QueryPoint.  The method requires two parameters: an ArcObjects Point and a double value to store distance.  The method uses the point and distance to construct a buffer.   When the server object extension was enabled (in ArcCatalog or ArcGIS Server Manager) a feature layer and field was selected.  The buffer will select features in the feature layer and the results will be summarized using the field.  In this case, the feature layer contains polygons defining vegetation regions and the field is the proper name of the vegetation type classification.   The QueryPoint method returns a reference to a SpatialQuerySOE.Results object via the SpatialQuerySOE.IResults interface.  The next few steps will cover working with the properties of this results object.
            // Get the Spatial Query server object extension
            ESRI.ArcGIS.Carto.IMapServer mapServer = agsMapResourceLocal.MapServer;
            ESRI.ArcGIS.Server.IServerObjectExtensionManager serverObjectExtensionManager = mapServer as ESRI.ArcGIS.Server.IServerObjectExtensionManager;
            ESRI.ArcGIS.Server.IServerObjectExtension serverObjectExtension = serverObjectExtensionManager.FindExtensionByTypeName("SpatialQuerySOE");
            SpatialQuerySOE.Interfaces.IExtension spatialQuerySOE = serverObjectExtension as SpatialQuerySOE.Interfaces.IExtension;
    
            // Execute the SOE's logic
            SpatialQuerySOE.Interfaces.IResults spatialQueryResults = spatialQuerySOE.QueryPoint(clickPoint, searchDistance);
    
  8. The SpatialQuerySOE.IResults.ResultsGraphics property references a set of graphics elements via the IGraphicElements interface.  To render these graphics in the map requested by the Web ADF and generated by ArcGIS Server, the graphics elements need to be converted from a COM object to a value object.  Value objects are used by the ArcGIS Server SOAP API to store values.  The Web ADF uses the ArcGIS Server SOAP API to work with ArcGIS Server data sources, both Internet and Local.  When the Web ADF requests a map image from an ArcGIS Server map service, it uses the MapFunctionality's MapDescription value object.  By default, changes to the MapDescription are maintained for the duration of the user session.  In this section of code, the properties of the graphics representing the summarized features are explicitly defined. The color of the solid outline is dark red.  Since we're working with the ArcGIS Server SOAP API, value objects are used to set and store these properties.  The symbol for each element in the graphic element array is set, then the graphic element array is associated with the MapDescription via the CustomGraphics property.    The next time time a map is requested from this map resource, the custom graphics will be rendered in the map.   
            // Get the results graphics
            ESRI.ArcGIS.ADF.ArcGISServer.GraphicElement[] resultsGraphics = ESRI.ArcGIS.ADF.ArcGISServer.Converter.ComObjectToValueObject(spatialQueryResults.ResultsGraphics, serverContext, typeof(ESRI.ArcGIS.ADF.ArcGISServer.GraphicElement[])) as
                ESRI.ArcGIS.ADF.ArcGISServer.GraphicElement[];
    
            // Create a symbol for the graphics' outlines
            ESRI.ArcGIS.ADF.ArcGISServer.RgbColor rgbColor = new ESRI.ArcGIS.ADF.ArcGISServer.RgbColor();
            rgbColor.Red = 155;
            rgbColor.Green = 0;
            rgbColor.Blue = 0;
            rgbColor.AlphaValue = 255;
            
            ESRI.ArcGIS.ADF.ArcGISServer.SimpleLineSymbol simpleLineSymbol = new ESRI.ArcGIS.ADF.ArcGISServer.SimpleLineSymbol();
            simpleLineSymbol.Style = ESRI.ArcGIS.ADF.ArcGISServer.esriSimpleLineStyle.esriSLSSolid;
            simpleLineSymbol.Color = rgbColor;
            simpleLineSymbol.Width = 0.2;
    
            // Apply the symbol to the graphics
            foreach (ESRI.ArcGIS.ADF.ArcGISServer.PolygonElement polygonElement in resultsGraphics)
            {
                ESRI.ArcGIS.ADF.ArcGISServer.SimpleFillSymbol simpleFillSymbol = polygonElement.Symbol as
                    ESRI.ArcGIS.ADF.ArcGISServer.SimpleFillSymbol;
                simpleFillSymbol.Outline = simpleLineSymbol;
            }
    
            // Add the graphics to the map
            agsMapFunctionality.MapDescription.CustomGraphics = resultsGraphics;
  9. The web page contains a div element that contains a GridView control that will be used to display summarized statistics returned from the server object extension.  If the GridView should be displayed, the IRecordSet is returned from the SpatialQuerySOE.IResults.SummaryStatistics property.  The IRecordSet is converted into a ArcGIS Server SOAP API RecordSet which can easily be converted into an ADO.NET DataTable using the convenient Converter method ToDataTable.  The GridView is rendered as HTML, which is returned to the browser and inserted into the div "summaryStatsDiv".   Since the Map generates the request when it is clicked with the custom tool, the Map will also receive the response (i.e. callback results).  You will create a custom CallbackResult to contain the raw HTML GridView content and append it to the Map's callback results collection.      
            // Get the grid view for displaying summary stats
            System.Web.UI.WebControls.GridView summaryStatsGridView = adfMap.Page.FindControl("GridView1") as System.Web.UI.WebControls.GridView;
            
            // Update the summary stats grid view if the stats are to be shown
            if (showSummaryStats)
            {
                // Get the summary statistics as a value object record set
                ESRI.ArcGIS.ADF.ArcGISServer.RecordSet summaryStatsRecordSet = ESRI.ArcGIS.ADF.Web.DataSources.ArcGISServer.Converter.ComObjectToValueObject(spatialQueryResults.SummaryStatistics, serverContext, typeof(ESRI.ArcGIS.ADF.ArcGISServer.RecordSet)) as
                    ESRI.ArcGIS.ADF.ArcGISServer.RecordSet;
    
                // Convert the record set to a DataTable
                System.Data.DataTable summaryStatsDataTable = ESRI.ArcGIS.ADF.Web.DataSources.ArcGISServer.Converter.ToDataTable(summaryStatsRecordSet);
    
                if (summaryStatsDataTable.Rows.Count > 0)
                {
                    // Bind the summary stats to the grid view
                    summaryStatsGridView.DataSource = summaryStatsDataTable;
                    summaryStatsGridView.DataBind();
    
                    // Get the grid view's HTML
                    System.IO.StringWriter stringWriter = new System.IO.StringWriter();
                    System.Web.UI.HtmlTextWriter htmlTextWriter = new System.Web.UI.HtmlTextWriter(stringWriter);
                    summaryStatsGridView.RenderControl(htmlTextWriter);
                    htmlTextWriter.Flush();
                    string gridViewHtml = stringWriter.ToString();
    
                    // Create a callback result that will update the summary stats grid view
                    ESRI.ArcGIS.ADF.Web.UI.WebControls.CallbackResult updateStatsTableCallbackResult = ESRI.ArcGIS.ADF.Web.UI.WebControls.CallbackResult.CreateSetInnerContent("summaryStatsDiv", gridViewHtml);
                    adfMap.CallbackResults.Add(updateStatsTableCallbackResult);
                }
                else
                {
                    showSummaryStats = false;
                }
            }
  10. If the DataTable is not empty, the content of the summaryStatsDiv element needs to be made visible.  The following custom CallbackResult includes JavaScript to set the visibility of the div, and thus the GridView content, in the browser.  The progress indicator also needs to be hidden, so the JavaScript for that is included here as well.
            // Create a callback result that will update the visibility of the summary stats div and busy indicator via JavaScript
            string statsDivVisibility = showSummaryStats ? "" : "none";
            string setElementVisibilityScript = @"
                var summaryStatsDiv = $get('summaryStatsDiv');
                summaryStatsDiv.style.display = '{0}';
    
                var busyIndicator = $get('busyIndicator');
                busyIndicator.style.display = 'none';";
            setElementVisibilityScript = string.Format(setElementVisibilityScript, statsDivVisibility);
            ESRI.ArcGIS.ADF.Web.UI.WebControls.CallbackResult setStatsDivVisibilityCallbackResult =
                ESRI.ArcGIS.ADF.Web.UI.WebControls.CallbackResult.CreateJavaScript(setElementVisibilityScript);
            adfMap.CallbackResults.Add(setStatsDivVisibilityCallbackResult);
    
  11. The final step in the custom tool is to refresh the ArcGIS Server resource that contains the custom graphics which were added earlier.
            adfMap.RefreshResource(agsMapResourceLocal.Name);
  12. Now that the implementation code for our custom tool is finished, we need to add a tool item to the Toolbar control to trigger the action that executes the custom tool.  Select the Toolbar control in design view or in the Properties window, then click the ellipsis for the ToolbarItems property.  In the ToolbarItems Collection Editor dialog, add a new "Tool" item.  In the Toolbar Items section, select the Tool item and click the Add button.  The new Tool should appear under the Current Toolbar Items section.
  13. Select the new Tool item and click the Show Properties button.  A properties dialog for the new Tool should be displayed.   

     

    Set the following properties:

    Property Value Description
     Text

     Vegetation Summary Proximity Search Tool

     Label for the tool in the Toolbar
     ClientAction

     Point

     Client event passed to the server
     Name  VegetationSummary  Object name of the tool, if used in code
     ServerActionAssembly

     App_Code

     Class libraries associated with a Web site are compiled into an assembly named App_Code
     ServerActionClass

     VegTool

     The name of the custom class which implements IMapServerToolAction and will be executed when this tool is used in the map

Using the Web application at run-time


  1. Open a browser and navigate to the Web application URL (for example, http://localhost/ArcGIS_Spatial_Query_SOE_CSharp/SpatialQuerySOE_WebSite_CSharp) -or- load the Web application solution within Visual Studio.
  2. Enter a distance in meters to search vegetation regions within the Vegetation layer. The default is 10000.
  3. To display the summary statistics for the selected regions, check the box next to "Show Summary Results in a Table".
  4. In the toolbar, click on the tool "Vegetation Summary Proximity Search Tool".
  5. Click on the map. 
  6. When the application is finished processing, a circular region in the map should be highlighted along the boundary of the features in the Vegetation layer. The summary statistics should be rendered in a table below the Toc.


Additional Resources

This scenario includes functionality and programming techniques covering a number of different aspects of ArcObjects, the ArcGIS Server ArcObjects API, ArcGIS Server SOAP API, Web ADF Common API and Web controls.

You are encouraged to read the overview to Working with the ArcObjects API to get a better understanding of core ArcGIS Server programming concepts such as stateful versus stateless server application development. This section also covers concepts and programming guidelines for working with server contexts and ArcObjects running within those contexts, as well as discussion on extending server objects as demonstrated in this scenario.

This scenario makes use of the Web ADF to provide the majority of the user interface for this Web application. To learn more about the Web ADF read Developing Web Applications using the Web ADF - which includes detailed descriptions and examples of using the Web controls, including the Map and Toolbar Web controls that you made use of while programming this Web application. If you are unfamiliar with ASP.NET Web development, it's also recommended that you refer to your .NET developer documentation to become more familiar with Web application development.

ArcGIS Server applications exploit the rich GIS functionality of ArcObjects. This application is no exception. It includes the use of ArcObjects to work with the components of a MapServer, buffer and clip geometries, query a geodatabase, and create graphics. To learn more about these aspects of ArcObjects, refer to the online developer documentation on the Carto, Display, GeoDatabase, and Geometry object libraries.