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.
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.
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.
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:
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.
Type the name of the string (for example,
PDSExtensionEx1).
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