Writing a PDS Extender

Summary

This article describes how Project Data Service (PDS) Extenders allow you to easily develop solutions with Microsoft Office Project Server 2003. It describes in detail the programming interface and improvements for Project Server 2003, and the procedure for registering PDS Extenders.

Download The Project Server 2003 PDS Reference (PDSReference.exe) is available from the Microsoft Download Center. The file includes the complete PDS reference, with the Visual Basic 6 sample code that can act as a template for PDS Extender implementations for Project Server 2003.

For the article Building Managed Code PDS Extensions for Project Server 2003, see the Project 2003 Technical Articles section on MSDN. The article includes a download and instructions for using a base class to easily build PDS extensions in Visual C# with Microsoft Visual Studio .NET.

An example of a PDS Extender for Microsoft Project Server 2002 is available with the Project Managers PDS Extender article in the Project 2002 SDK.

Contents

Project Data Service Extenders

PDS Extender Interface

Registering a PDS Extender

PDS Extender Template Functions

Project Data Service Extenders

A PDS Extender allows you to customize 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 from 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 Project Server security, which ensures that project data stored in the database remains consistent. The PDS Extender in Project Server 2003 improves access to the Project Server security object, and enables checking for individual user permissions.

When the PDS receives a request that isn't part of its standard API, it checks to see if any PDS Extenders are registered. The PDS forwards the unrecognized request to the first PDS Extender it finds. 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 PDS passes the request to the next registered PDS Extender, if one exists. There can be a maximum of 200 registered PDS Extenders: 100 can be the Microsoft Project Server 2002 style of extender and 100 can be the new Project Server 2003 style. 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.

The PDS forwards unrecognized requests to PDS Extenders

Figure 1. The PDS forwards unrecognized requests to PDS Extenders

PDS Extender Requirements

The following are the requirements of a PDS Extender:

  • It must be a registered COM object (or have a registered COM wrapper).

  • It must support at least one specific public interface.

  • It must be registered with the PDS.

PDS Extender Interface

A PDS Extender is implemented as COM DLL that acts as an extension to the core PDS methods.

Improvements for Project Server 2003

The PDS Extender interface is improved for Project Server 2003. It allows the PDS to pass into each extension the information necessary to use the Project Server Security object, and to make calls back into the PDS. The improved interface also makes it easier for future expansion of the data provided to an extension, and makes the PDS even more valuable as an extensibility platform for Project Server.

The PDS still supports extensions written for Microsoft Project Server 2002; those extensions can also be easily transformed to use the new PDS extension parameters.

A PDS Extender DLL exposes only one public method:

  • Project Server 2003 extensions use the public function XMLRequestEx. It provides more functionality for PDS extensions in Project Server 2003. Use XMLRequestEx for all new Project Server 2003 PDS extensions. In the template provided, XMLRequestEx in turn calls a private implementation of XMLRequest, as described in the section XMLRequestEx Function.

  • Project Server 2002 extensions use the public function XMLRequest Project Server 2003 continues to support extensions written for Project Server 2002 that directly call XMLRequest. This method is private in implementations of new PDS extensions for Project Server 2003.

XMLRequestEX Syntax

XMLRequestEX has the following signature in Microsoft Visual Basic:

Public Function XMLRequestEx( _
    ByVal sXML As String, _
    ByVal sPDSInfoEx As String, _
    ByRef nHandled As Integer) _
    As String

XMLRequestEx Parameters

The PDS provides the sXML and sPDSInfoEx parameters. The PDS Extender sets nHandled on return to the PDS.

sXML

Required. sXML contains the XML request string, just like sXML in XMLRequest. The request must be formatted the same as other PDS API methods and enclosed in <Request> tags, as in the following:

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

sPDSInfoEx

Required. The PDS provides the parameters within sPDSInfoEx, in the form of an XML string that includes all of the additional information needed for the Project Server 2003 extension. The sPDSInfoEx syntax is as follows:

<PDSExtensionXML>
    <UserName></UserName>
    <UserGUID></UserGUID>
    <UserID></UserID>
    <PDSConnect></PDSConnect>
    <BasePath></BasePath>
    <DBType></DBType>
    <SOAPRequestCookie></SOAPRequestCookie>
    <HTTPRequestCreate></HTTPRequestCreate>
</PDSExtensionXML>

Parameters in sPDSInfoEx:

UserName   Required. Name of the currently logged-on user.

UserGUID   Required. Global unique identifier of the currently logged-on user.

UserID   Required. ID of the currently logged-on user.

PDSConnect   Required. Connection string used by the PDS for the Project Server database.

BasePath   Required. Name of the Project Server virtual directory; for example:

    http://MyServer/ProjectServer

DBType   Required. The database type is always set to 1, which is for Microsoft SQL Server™.

SOAPRequestCookie   Optional. Only included if the request is made via SOAP.

HTTPRequestCreate   Optional. Only included if the request is made via HTTP POST (the request uses PDSRequest.asp).

nHandled

Required. The Extender sets nHandled 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 XMLRequestEx.

XMLRequestEX Return Value

The return value is the XML response of the method. It is recommended that the return value is in the same format as a PDS API method, and includes <HRESULT> and <STATUS> tags. 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, PDS extenders should return status code values that are greater than 10000. The return value for XMLRequestEx should take the following form:

<Reply>
    <HRESULT></HRESULT>
    <STATUS></STATUS>
    <NameOfExtenderMethod>
        -- method-specific output --
        -- optional PDS call back output --
    </NameOfExtenderMethod>
        -- optional PDS call back output --
</Reply>

XMLRequest Syntax

XMLRequestEx calls the XMLRequest private function. The primary task for XMLRequest is to parse the XML request and return an XML response. XMLRequest has the following Visual Basic signature:

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

NoteXMLRequest is the public interface in the PDS Extender implementation for Project Server 2002. It is private in the PDS Extender implementation for Project Server 2003.

XMLRequest Parameters

The PDS provides the sXML, sUser, sConnect, and lDBType parameters. The PDS Extender sets nHandled on return to the PDS.

sXML

Required. sXML contains the XML request string. The request must be formatted the same as other PDS API methods and enclosed in <Request> tags, as in the following:

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

sUser

Required. sUser is the authenticated Project Server logon ID making the request.

sConnect

Required. sConnect contains the connection string for connecting to the Project Server database on behalf of the logged-on user.

lDBType

Required. lDBType indicates that the type of the Project Server database lDBType is always set to 1, which means Microsoft SQL Server™.

nHandled

Required. The extender sets nHandled 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.

XMLRequest Return Value

The return value is the XML response of the method. The return value is also in the same format as a PDS API method, and includes <HRESULT> and <STATUS> tags. 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, PDS extenders should return status code values that are greater than 10000. The return value for XMLRequest takes the following form:

<Reply>
    <HRESULT></HRESULT>
    <Status></Status>
    <NameOfExtenderMethod>
        -- method-specific output –-
        -- optional PDS call back output in Project Server 2003 --
    </NameOfExtenderMethod>
        -- optional PDS call back output in Project Server 2003 --
</Reply>

Registering a PDS Extender

You may register up to 200 PDS Extenders for one Project Server virtual directory, 100 legacy extenders and 100 of the Project Server 2003 style. A PDS Extender is a COM Dynamic Linked Library (DLL). There are two steps to register a PDS Extender.

  1. Register the COM DLL that implements the PDS Extender using Regsvr32.exe. For example, if you placed the PDS Extender myPDSextender.dll in the subdirectory MySolution of the Project Server virtual root, type the following in a Command window:

    Regsvr32 [Program Files]\Microsoft Office Project Server 2003
             \MySolution\myPDSextenderEx.dll
    

    The PDS Extender DLL can reside in any directory on the Project Server computer.

  2. Register the PDS Extender with the PDS, using the Registry Editor (regedit.exe). The registry key for Project Server 2003 is:

    HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Office\11.0\MS Project _
         \WebClient Server\<IIS Virtual Directory Name>
    

    A default Project Server installation has the following key for the virtual directory:

    [HKLM\...]\MS Project\WebClient Server\ProjectServer
    

    Each PDS Extender contains a string value which is the ProgID of the COM DLL or proxy. The ProgID is usually in the form of DLLName.MainClassName. For legacy PDS extensions implemented with XMLRequest, use the following string value – data pair:

    "PDSExtensionN"="PDSExtenderDllName.PublicInterfaceClass"
    

    For Project Server 2003 PDS extensions implemented with XMLRequestEx, use the following string value – data pair:

    "PDSExtensionExN"="PDSExtenderExDllName.PublicInterfaceClass"
    

    In either case, N represents an integer from 1 to 100.

For example, to register a Project Server 2003 PDS extender:

  1. Create a new string value in the virtual directory key. In the Registry Editor, select the key for the Project Server virtual directory and then click New – String Value on the File menu.

  2. Type the name of the string (for example, PDSExtensionEx1).

  3. Double-click the string name; in the Edit String dialog type the ProgID (for example, myPDSextenderEx.CMain).

The PDS Extender is now registered.

PDS Extender Template Functions

PDSExtensionEx.vbp is a Visual Basic project that serves as a template for creating PDS Extenders that use the XMLRequestEx method. It implements a simple method named Ping, which in turn executes a call to the PDS method ProjectsStatus. A client application calls Ping using the following XML:

<Request>
    <Ping/>
</Request>

The PDS Extender processes the request and returns the following XML response. Note that the ProjectsStatus response is embedded within the Ping response.

<Reply>
    <HRESULT>0</HRESULT>
    <STATUS>0</STATUS>
    < PDSExtensionExPing/>
        -- Reply from ProjectsStatus callback --
    <Reply>
        <HRESULT>0</HRESULT>
        <STATUS>0</STATUS>
        <UserName>Administrator</UserName>
        <ProjectsStatus>
            . . .
        </ProjectsStatus>
    </Reply>
</Reply>

Build the PDSExtensionEx.vbp project and register the DLL on the Project Server computer (using Regsvr32.exe). Within the following registry key:

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

create the following string value and data pair within the ProjectServer key:

"PDSExtensionEx1"="PDSExtensionEx.CMain"

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

"PDSExtensionEx2"="PDSExtensionEx.CMain"

Note If you modify or replace an existing PDS Extender on the Project Server computer, you will need to restart Internet Information Services (IIS) to overwrite the DLL registration with Project Server.

XMLRequestEx Function

The class CMain.cls in the project PDSExtensionEx.vbp has one public function, XMLRequestEx, as described previously:

Public Function XMLRequestEx( _
    ByVal sXML As String, _
    ByVal sPDSInfoEx As String, _
    ByRef nHandled As Integer) _
    As String

XMLRequestEx parses the PDS information string sPDSInfoEx to extract the user name, security cookie or HTTP request, and other data necessary to make a request. Parsing XML is easily done with the Microsoft XML Parser. The project requires a reference to Microsoft XML, v3.0 or above (msxml3.dll, msxml4.dll, or msxml5.dll) to access the type library MSXML2.

Dim oXMLDocument As MSXML2.DOMDocument
Dim oXMLRoot As MSXML2.IXMLDOMElement

'Load and parse the sPDSInfoEx XML document
Set oXMLDocument = New MSXML2.DOMDocument
oXMLDocument.loadXML sPDSInfoEx
Set oXMLRoot = oXMLDocument.firstChild

Dim sValue As String
Dim sNodeName As String
sNodeName = oXMLRoot.nodeName
Select Case sNodeName
    Case "PDSExtensionXML"
        Dim oNode1 As MSXML2.IXMLDOMNode
        For Each oNode1 In oXMLRoot.childNodes
            sValue = oNode1.Text
            Select Case oNode1.nodeName
                Case "UserName"
                    m_sUserName = sValue
                Case "UserGUID"
                    m_sUserGUID = sValue
                Case "UserID"
                    m_lUserID = Val(sValue)
                Case "PDSConnect"
                    m_sPDSConnect = sValue
                Case "BasePath"
                    m_sBasePath = sValue
                Case "DBType"
                    m_eDatabaseType = Val(sValue)
                Case "SOAPRequestCookie"
                    m_sSOAPRequestCookie = sValue
                Case "HTTPRequestCreate"
                    m_sHTTPRequestCreate = sValue
            End Select
        Next oNode1
End Select

XMLRequestEx then sends the XML request string sXML and the parsed PDS information to the private function XMLRequest, which assembles and returns the reply string.

XMLRequestEx = XMLRequest( _
    sXML, _
    m_sUserName, _
    m_sPDSConnect, _
    m_eDatabaseType, _
    nHandled)

XMLRequest Function

XMLRequest is a private function since it requires XMLRequestEx to parse the PDS data and initialize the class member variables. Most of the work of XMLRequest is to parse the request, make calls back to the PDS, and assemble the reply.

The status code constants are arbitrary values defined by the function implementer, typically in a separate module such as Constants.bas. The general status codes follow the definitions listed in Error Codes; for example:

Public Const STATUS_INVALID_REQUEST = 2
Public Const STATUS_EXCEPTION_SECURITY = 9006

Extension-specific status codes should be 10000 or above to avoid conflicts with standard PDS status codes; for example:

Public Const STATUS_EXT_SPECIFIC = 10000

XMLRequest first checks to see if the XML request is valid:

Dim lStatus As Long
Dim sXMLReply As String
Dim oXMLDocument As MSXML2.DOMDocument
Dim oXMLRoot As MSXML2.IXMLDOMElement

'Check if sXML variable is empty
lStatus = STATUS_OK
If Len(sXML) = 0 Then
    sXMLReply = "<Error>Request string is empty</Error>"
    lStatus = STATUS_INVALID_REQUEST
    GoTo PrepareReply
End If

'Check if sXML variable contains valid XML
Set oXMLDocument = New MSXML2.DOMDocument
oXMLDocument.loadXML sXML
If oXMLDocument.parseError.errorCode <> 0 Then
    sXMLReply = "<Error>XML Parse error</Error>"
    lStatus = STATUS_INVALID_REQUEST
    GoTo PrepareReply
End If

If the XML request is valid, XMLRequest loads the root node:

Set oXMLRoot = oXMLDocument.firstChild
If oXMLRoot Is Nothing Then
    sXMLReply = "<Error>Root node not found</Error>"
    lStatus = STATUS_INVALID_REQUEST
    GoTo PrepareReply
End If

If there is a root node, XMLRequest then calls InitPjSvrSecurity to initialize the Project Server security object:

If InitPjSvrSecurity = 1 Then
    lStatus = STATUS_EXCEPTION_SECURITY
End If

XMLRequest looks for the <Request> node, and then processes recognized methods within the request and builds the reply string sXMLReply. Within a recognized method, you can check for specific user permissions. The Visual Basic module Constants.bas includes permission constants such as PERMISSION_ADMIN_ENTERPRISE.

XMLRequest can optionally make other PDS method calls while processing the PDS extension methods. Other PDS calls can use either SOAP or HTTP POST: XMLRequestEx received the strings m_sSOAPRequestCookie, m_sHTTPRequestCreate, and m_sUserGUID from the PDS. If the request is handled, XMLRequest sets nHandled to 1.

In this implementation, there is one valid method named Ping. If Ping contained any parameters, this is where you would parse the method parameters.

Dim sNodeName As String
sNodeName = oXMLRoot.nodeName
Select Case sNodeName
    Case "Request"
        Dim oNode As MSXML2.IXMLDOMNode
        For Each oNode In oXMLRoot.childNodes
            sNodeName = oNode.nodeName
            'This loop is the essence of the PDS extender -
            'add code to recognize the method(s) here...
            Select Case sNodeName
                Case "Ping"
                    'Check appropriate global user permissions
                    If m_oPjSvrSecurity.CheckSPGlobalPermission( _
                        m_sUserGUID, PERMISSION_ADMIN_ENTERPRISE) _
                        <> 0 Then
                        sXMLReply = "<PDSExtensionExPing/>"

                        'The following illustrates making calls
                        'back into the PDS
                        Dim oPDS As Object
                        Set oPDS = CreateObject("PDS.CMain")
                        If m_sSOAPRequestCookie <> "" Then
                            sXMLReply = sXMLReply & _
                                oPDS.SoapXMLRequest( _
                                m_sSOAPRequestCookie, _
                                "<Request><ProjectsStatus/></Request>")
                            nHandled = 1
                            Set oPDS = Nothing
                        ElseIf m_sHTTPRequestCreate <> "" Then
                            If oPDS.Create(m_sHTTPRequestCreate) = 0 Then
                                sXMLReply = sXMLReply & _
                                   oPDS.XMLRequest( _
                                   "<Request><ProjectsStatus/></Request>")
                                nHandled = 1
                                Call oPDS.Destroy
                                Set oPDS = Nothing
                            End If
                        End If
                        'Not needed here, if nHandled set previously
                        'nHandled = 1
                    Else
                        'Return Access Denied if the security check fails
                        lStatus = STATUS_ACCESS_DENIED
                    End If
                Case Else
                    'If STATUS_UNKNOWN_REQUEST then assume another
                    'registered extender can handle the request
                    lStatus = STATUS_UNKNOWN_REQUEST
            End Select
        Next oNode
    Case Else
        lStatus = STATUS_UNKNOWN_REQUEST
End Select

If the PDS Extender receives something other than a Ping request, XMLRequest sets nHandled to 0, to pass the request on to the next extender.

PrepareReply:
    If lStatus = STATUS_UNKNOWN_REQUEST Then
        nHandled = 0
    Else
        nHandled = 1
    End If

Finally, XMLRequest assembles a reply that it returns to XMLRequestEx. The reply includes an HRESULT and STATUS code, and any other data that is necessary.

    XMLRequest = "<Reply><HRESULT>0</HRESULT><STATUS>" & _
        Format$(lStatus, "0") & "</STATUS>" & sXMLReply & "</Reply>"
End Function

InitPjSvrSecurity Function

XMLRequest calls InitPjSvrSecurity in the CMain class to initialize the Project Server security object. The Project Server security object must be initialized before further requests can be made to it. The original call to XMLRequestEx sets the string m_sBasePath.

Private Function InitPjSvrSecurity() As Integer
    On Error GoTo Exception_Error
    If m_oPjSvrSecurity Is Nothing Then
        Set m_oPjSvrSecurity = _
            CreateObject("PjSvrSecurity.PjSvrSecurity.1")
        Call m_oPjSvrSecurity.SetDBConnection(m_sBasePath)
    End If
    InitPjSvrSecurity = 0
    Exit Function
Exception_Error:
    InitPjSvrSecurity = 1
End Function