Writing a Project Data Service Extender

Writing a Project Data Service Extender

This content is no longer actively maintained. It is provided as is, for anyone who may still be using these technologies, with no warranties or claims of accuracy with regard to the most recent product version or service release.

Summary

This article describes how Project Data Service (PDS) Extenders allow you to easily develop solutions with Microsoft® Project Server.

Introduction

This article describes how Project Data Service (PDS) Extenders allow you to easily develop solutions with Microsoft Project Server. The programming interface and procedure for registering PDS Extenders are described in detail. You can also download the file PDSxtndr.exe in the MSDN Downloads; the file contains sample code that can act as a template for PDS Extender implementations. A more detailed example of a PDS Extender can be found in the Project Managers PDS Extender article in the Microsoft Project Software Development Kit (SDK).

Project Data Service Extenders

A PDS Extender allows you to customize Microsoft Project Server by using the standard PDS interface. Because you can customize within the standard PDS interface, making calls to your PDS Extender is no different than making method calls to the PDS itself, and method calls take the form of XML requests. By using the same interface you also get the additional benefit of Microsoft Project Server security, which ensures that project data stored in the database remains consistent. For more information on Microsoft Project Server security, see The Project Data Service and Microsoft Project Server Security Architecture article in the Microsoft Project SDK.

When the PDS receives a request that isn't part of its standard API, it checks to see if any PDS Extenders are registered. If a PDS Extender is found, the request is forwarded to the PDS Extender. If the PDS Extender can handle the request, it returns a response to the PDS, which, in turn, returns that response to the client. From the client's perspective, it appears that the PDS itself services the method call. If the PDS Extender does not recognize the request, the request is passed to another PDS Extender, if one exists. There can be a maximum of 100 registered PDS Extenders. If no PDS Extender can handle the request, the PDS returns an error to the client indicating that the method is not recognized. Figure 1 summarizes how the PDS handles requests to a PDS Extender.

Aa164954.PDR_pdsxtndr01(en-us,office.10).gif

Figure 1. The PDS forwards unrecognized requests to PDS Extenders

PDS Extender Interface

A PDS Extender is implemented as COM DLL. This DLL must expose one public method, named XMLRequest, which has the following signature in Microsoft Visual Basic®:

  Public Function XMLRequest( _
      ByVal sXML As String, _
      ByVal sUser As String, _
      ByVal sConnect As String, _
      ByVal lDBType As Long, _
      ByRef nHandled As Integer) _
      As String

The sXML, sUser, sConnect, and lDBType parameters are provided by the PDS. The PDS Extender sets the nHandled parameter on return to the PDS. The return value of XMLRequest is the XML response of the method.

The sXML parameter contains the XML request. For consistency, it is required that the request be formatted in the same way as other PDS API methods and enclosed in <REQUEST> tags, such as the following:

  <Request>
      <NameOfExtenderMethod>
          <ExtenderParameter1>Value1</ExtenderParameter1>
          <ExtenderParameter2>Value2</ExtenderParameter2>
          ...   
      </NameOfExtenderMethod>
  </Request>

The primary task for an XMLRequest implementation is to parse the XML request and return an XML response. Additional information passed by the PDS may be useful. The sUser parameter is the authenticated Microsoft Project Server logon ID making the request. The sConnect parameter contains the connection string for connecting to the Microsoft Project Server database on behalf of the logged-on user. The lDBType parameter indicates the installation type of the Microsoft Project Server database and is always equal to 1, which means Microsoft SQL Server™.

The return value should also be of the same format as a PDS API method, which includes an HRESULT and Status tag. A non-zero HRESULT indicates a communication error. A non-zero Status indicates a logical error. The PDS reserves Status code values up to 10000. Therefore, it is recommended that extenders return values that are greater than 10000. The return value for XMLRequest takes the following form:

  <Reply>
      <HRESULT></HRESULT>
      <Status></Status>
      <NameOfExtenderMethod>
          method-specific output
      </NameOfExtenderMethod>
  </Reply>

In addition to the XML response, the extender sets the nHandled parameter to 1 if it successfully handles the request. If the extender cannot handle the request, it sets nHandled to 0. This notifies the PDS that another extender may be able to handle the request. In this case, the PDS ignores the return value of XMLRequest.

Registering a PDS Extender

In addition to registering the COM DLL that implements the extender, a PDS Extender must be registered with the PDS. The location of this key is:

  [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Office\10.0\MS Project _
       \WebClient Server\<Virtual Directory>]

Each PDS Extender contains a string value indicating the ProgID of the COM DLL:

  "PDSExtensionN"="PDSExtenderDllName.PublicInterfaceClass"

Where N represents an integer from 1 to 100.

PDS Extender Template

PDSExtension.vbp is a Visual Basic project containing code that acts as a template for creating PDS Extenders. It implements a simple method named Ping, which a client application calls using the following XML:

  <Request>
      <Ping/>
  </Request>

The PDS Extender processes the request and returns the following XML response:

  <Reply>
      <HRESULT>0</HRESULT>
      <STATUS>0</STATUS>
      <Ping/>
  </Reply>

For more information on making PDS API calls, see the PDS Reference for Microsoft Project Server in the Microsoft Project SDK.

Build the PDSExtension.vbp project and register the DLL on the Microsoft Project Server computer (for example, by using Regsvr32.exe). Within the following registry key:

  [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Office\10.0\MS Project\WebClient Server\ProjectServer]

Create the following string-value key:

  "PDSExtension1"="PDSExtension.CMain"

You should first examine the registry for an existing extender named PDSExtension1 so that you don't accidentally unregister it. If the extender PDSExtension1 exists, rename the key by changing the 1 to a 2 or to any other integer value up to 100.

Note that if you modify and replace an existing PDS Extender on the Microsoft Project Server computer, you will need to restart Internet Services to overwrite the DLL.

Examining the PDS Extender code reveals one function, which is XMLRequest:

  Public Function XMLRequest( _
      ByVal sXML As String, _
      ByVal sUser As String, _
      ByVal sConnect As String, _
      ByVal lDBType As Long, _
      ByRef nHandled As Integer) _
      As String

Most of the work of XMLRequest is to parse the request. Parsing XML is easily done with the Microsoft XML Parser (MSXML). The function first checks to see if the XML request is valid:

  'Checking if sXML variable is empty.
  'If yes then error message is created...
  If Len(sXML) = 0 Then
      sXMLReply = "<Error>Request string is empty</Error>"
      m_lStatus = STATUS_INVALID_REQUEST
      GoTo PrepareReply
  End If
          
  'Checking if sXML variable is valid XML.
  'If not, then error message is created...
  Dim oXMLDocument As MSXML2.DOMDocument
  Set oXMLDocument = New MSXML2.DOMDocument
  oXMLDocument.loadXML sXML
  If oXMLDocument.parseError.errorCode <> 0 Then
      sXMLReply = "<Error>XML Parse error</Error>"
      m_lStatus = STATUS_INVALID_REQUEST
      GoTo PrepareReply
  End If

Note that the constants for status codes are arbitrary values defined by the function implementer. If the XML request is valid, XMLRequest looks for the <Request> node:

  'Loading up the root node which should be <Request> node
  Dim oXMLRoot As MSXML2.IXMLDOMElement
  Set oXMLRoot = oXMLDocument.firstChild
  If oXMLRoot Is Nothing Then
      sXMLReply = "<Error>Root node not found</Error>"
      m_lStatus = STATUS_INVALID_REQUEST
      GoTo PrepareReply
  End If
      
  'Looking for the <Request> node to proceed with further
  'operations on the XML method request
  Dim sNodeName As String
  sNodeName = oXMLRoot.nodeName
  Select Case sNodeName
      'Found the <Request> node
      Case "Request"
      ... look for recognized methods ...
      Case Else
          m_lStatus = STATUS_UNKNOWN_REQUEST
  End Select

After XMLRequest has determined that this is a valid request, it parses for recognized methods. In this implementation, there is one valid method named Ping. Although the Ping method doesn't contain any parameters, this is also where you parse method parameters.

  Dim oNode As MSXML2.IXMLDOMNode
  Dim oChildNode As MSXML2.IXMLDOMNode
  For Each oNode In oXMLRoot.childNodes
      sNodeName = oNode.nodeName
      result = oNode.hasChildNodes()
      'There is a child node, now it has to see if it recognizes
      'it. This loop is the essence of the PDS extender -
      'add code to recognize the method(s) here...
      Select Case sNodeName
          'Recognizing child node <Ping>
          Case "Ping"
              sXMLReply = "<Ping/>"
              nHandled = REQUEST_HANDLED
          'If STATUS_UNKNOWN_REQUEST then assumption is that
          'another registered extender can handle the request
          Case Else
              m_lStatus = STATUS_UNKNOWN_REQUEST
      End Select
      'If successful in recognizing first xml command then it tries
      'to recognize all commands
  Next oNode

If we got something other than a Ping request, the extender passes it on to the next extender:

  If m_lStatus = STATUS_UNKNOWN_REQUEST Then
      nHandled = REQUEST_NOT_HANDLED
      sXMLReply = "<Error>Unknown request " & sNodeName & "</Error>"
      GoTo PrepareReply
  End If

Otherwise, XMLRequest returns a successful reply:

  'Returns a reply with an HRESULT, a STATUS code
  'and any other data that needs to be returned
  PrepareReply:
      XMLRequest = "<Reply><HRESULT>0</HRESULT><STATUS>" & _
          Format$(m_lStatus, "0") & "</STATUS>" & sXMLReply & "</Reply>"
  End Function