Getting Started with the PSI
Many people have asked, “How do I get started working with the PSI?” So I figured I would blog about creating a very simple application that interacts with the PSI. For this example, I will create a simple Windows Application that connects to Project Server and retrieves a list of resources for a given project.
Before we begin, it is important to realize that the PSI is made up of a number of Web Services. You can find a list of all the Project Server Web Service here: https://msdn2.microsoft.com/en-us/library/ms488627.aspx These Web Services are logically separated by business objects. For this example, we will be using both the Project and Resource Web Service.
To get started, open visual studio and create a Windows Application. The first step will be to add web references to the Project and Resource Web Services:
1. In the Solution Explorer, right click on References
2. Click on Add Web Reference.
3. Type in the URL to the Project Web Service.
The URL for the web service is:
https://SERVER_NAME/PWA_INSTANCE/_vti_bin/psi/project.asmx
Where SERVER_NAME is the name of the server Project Server is hosted on and PWA_INSTANCE is the name of the Project Web Access instance you want to connect to. _vti_bin/psi is where all the Project Server PSI Web Services reside. project.asmx is specific to the Project Web Service.
4. Give the Web Reference a name, such as WSProject
5. Click Add Reference
This will add a reference to the Project Web Service. Repeat the same steps again, except this time, on step 3 specify resource.asmx instead of project.asmx and in step for name the Web Reference WSResource.
Now that the Web References are step up, we can start to program! When I develop against the PSI, I always create a connection object to handle the various connections to the PSI. This allows me to reuse the connection class in a number of applications. Below is the source code of my connection class:
using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Net;
using System.Resources;
using System.Globalization;
using System.Web.Services.Protocols;
using System.Reflection;
namespace PSIDemo
{
public class Connection
{
public const string Resource = "Resource";
public const string Project = "Project";
private static Dictionary<string, SoapHttpClientProtocol> WSDictionary;
private string ms_ProjServURL;
public Connection(string as_ProjServURL)
{
ms_ProjServURL = as_ProjServURL + "/_vti_bin/psi/";
WSDictionary = new Dictionary<string, SoapHttpClientProtocol>();
}
public SoapHttpClientProtocol GetWebService(string as_WSName)
{
SoapHttpClientProtocol lo_WS;
if (WSDictionary.TryGetValue(as_WSName.ToString(), out lo_WS) == false)
{
switch(as_WSName)
{
case Resource:
Auth(Resource, new WSResource.Resource());
break;
case Project:
Auth(Project, new WSProject.Project());
break;
}
lo_WS = WSDictionary[as_WSName];
}
return lo_WS;
}
public static void Reset()
{
WSDictionary.Clear();
}
private void Auth(string as_WSName, SoapHttpClientProtocol as_WS)
{
try
{
object [] parameters = new object [1];
parameters[0] = ms_ProjServURL + as_WSName + ".asmx";
MethodInfo setUrlMethod = as_WS.GetType().GetProperty("Url").GetSetMethod();
setUrlMethod.Invoke(as_WS, parameters);
parameters[0] = CredentialCache.DefaultCredentials;
MethodInfo setCredentialsMethod = as_WS.GetType().GetProperty("Credentials").GetSetMethod();
setCredentialsMethod.Invoke(as_WS, parameters);
WSDictionary.Add(as_WSName, as_WS);
}
catch (Exception ex)
{
throw ex;
}
}
}
}
This connection class has a dictionary of all the available PSI Web Services. With this implementation we are only concerned with the Project and Resource Web Service, so I have not included any other web services, but it would not be difficult to add additional PSI Web Services. It would only require a couple more lines of code for each Web Service. I will save that for another post.
The method that handles the setup to the Web Service is Auth. For each Web Service, we need to set the URL for the server that we want to connect to at run time. The URL was passed in with the constructor and this can be a different URL then the one used for the Web Reference. The second step is to set the credentials. For this example, we will only do NT authentication, but if there is interest, I can post an extension for Forms Authentication. Once that is done, we add the Web Service to the dictionary and it is ready to be used.
Next, I am going to add three controls, plus a few labels to the Windows form. The first control is a text box for the URL to the Project Server (txtURL), the second control is a drop down which will be populated with all the Projects the user has access to (cboProjects) and the third control will be a list box which will contain the names of resources that belong to the selected project (lstResources). Below is a screen shot of the form:
This form has two methods that contain all the calls to the Web Services. The first method is the Leave event for the URL textbox:
private void txtURL_Leave(object sender, EventArgs e)
{
cboProjects.Items.Clear();
conn = new Connection(txtURL.Text);
projWS = (WSProject.Project)conn.GetWebService(Connection.Project);
DataTable projList = projWS.ReadProjectList().Tables[0];
foreach (DataRow dr in projList.Rows)
{
cboProjects.Items.Add(new ProjListItem(dr["Proj_Name"].ToString(), new Guid(dr[0].ToString())));
}
if (cboProjects.Items.Count > 0)
{
cboProjects.SelectedItem = cboProjects.Items[0];
}
}
In this method, we instantiate the Connection object and pass in the URL for the Project Server. This is the URL that will be used at run time. Next, we get the Project and Resource Web Services from the connection object. This allows us to read the projects and populate the drop down with all the project names. I have created a basic object, ProjListItem, which contains the GUID and name of the project so that we can easily retrieve the GUID later on to get the list of resources. It is important to note here, that we are working with datasets. The majority of our Web Services have datasets that can be manipulated and sent back to the server to update the data.
The second method is the select index changed for the drop down list of project names:
private void cboProjects_SelectedIndexChanged(object sender, EventArgs e)
{
lstResources.Items.Clear();
WSProject.ProjectTeamDataSet pds;
ProjListItem projItem = (ProjListItem)cboProjects.SelectedItem;
pds = projWS.ReadProjectTeam(projItem.getGuid());
DataTable dt = pds.Tables["ProjectTeam"];
foreach (DataRow dr in dt.Rows)
{
lstResources.Items.Add(dr["Res_Name"].ToString());
}
}
Here we retrieve the GUID for the selected project from the ProjListItem object that we populated the drop down list in the first method and we get the resources on the team by calling ReadProjectTeam method and passing the selected project GUID. ReadProjectTeam returns a dataset that contains a data table “ProjectTeam” that lists all the resources that are team members on the project.
So, now we have a little application that is able to connect to the server and retrieve data,
Chris Boyd
Comments
Anonymous
November 12, 2006
Hi, I’m currently assigned to a task to do some web parts for the new project server 2007. Unfortunately as I haven’t even started yet while trying out calling the web services through the PSI I got stuck with what I can only guess are authentification problems. I’ve been going through every single post and sources I could find but so far it seems as no one had have this kind of a problem. The problem itself starts with the simple set of a web reference to the (for example) Project web service (none of the rest work either, except for the loginForms and loginWindows). When trying to make a reference the studio can’t find the services unless not written like (http://server/_vti_bin_project.asmx?wsdl) But then the call for wsdl in runtime results in SOAP ex. “Possible SOAP version mismatch:….”. If I try getting to the web service following the local path (http://server:56737/SharedServices1/PSI/project.asmx) the studio has no problems there but at runtime I get the 401 Unauthorized (even if the test class I did is run on the server machine where the user must have all the rights needed). One more try I did using the “LoginDemo” example which comes with the SDK(where hoping that as it works fine can just add an extra button with simple call to some other web service which will prove that there is something I haven’t done in my previous tries). After I added the button and a call to the resource.asmx on_click : System.Diagnostics.Trace.WriteLine(res.GetCurrentUserUid()); Where public static LoginDemo.WebSvcResource.Resource res = new LoginDemo.WebSvcResource.Resource(); when I did try it out the login demo still worked fine but as for my call it threw a (System.Net.WebException' occurred in System.Web.Services.dll 401 unathorized ex. Again when the web reference was the local path for the service) and the (Possible SOAP version mismatch: Envelope namespace http://schemas.xmlsoap.org/wsdl/ was unexpected. Expecting http://schemas.xmlsoap.org/soap/envelope/) when trying out the virtual path. I guessed the trick was hidden somewhere in the cookies or the credentials but after made sure they were alright nothing changed at all. As I already said I’m a beginner and perhaps I have missed something quite simple and my problem might seem a silly one but I’ve spend over a week looking for a solution and a way to make a call to the web services that would work with no success so far. I’d appreciate any help or full sample codes that work fine to use as an example. Just wondering has anyone had any problems of the sort? HELP mates…getting desperate in here! Thanks in advance Cheers HristiyanAnonymous
November 12, 2006
Hi, Once again after trying so hard and at the final gave up and started whining for help the problem got a name and was solved but not before I made a big fuss about it. Sorry for bothering with a obvious mistake of mine:) HristiyanAnonymous
November 15, 2006
I am happy that you got it working! Can you please post how you solved the issue in case someone runs into the same problem? Thanks, Chris BoydAnonymous
November 30, 2006
Hi, Well in my case, for my great shame, the whole problem was from that I had missed the name of the Project Web Access instance I was connecting to. In other words I used “http://server/_vti_bin_project.asmx?wsdl” where the correct one has the name “pwa” which refers to the project web access – “http://server/pwa/_vti_bin_project.asmx”. Funny thing was that I could call the “loginForms” and “loginWindows” services without any problems whatsoever from where on I guessed I should not have any problems with the rest either but the fact is - it wasn’t exactly true and I found it the hard way. Anyways I didn’t have the nerves to continue digging in the case “How can I call the services anyway without going through the pwa” but if I do it one day and find a "go around the authentication" way I will post it for sure. Cheers HristiyanAnonymous
December 02, 2006
The comment has been removedAnonymous
October 19, 2007
Hi Chris: I've been trying your code on a Project Server 2007 enterprise installation, and I'm getting what I think is an authentication error (WebException with text "Unable to connect to the remote server") when calling the ReadProjectList method. The Web Service properly connects (I think) because I get the "projWS" variable properly instantiated. My machine is on the domain, and my user is a PS Server Admin. Could it be that the CredentialCache.DefaultCredentials is not providing the proper credentials to the server? Any thoughts? Thanks in advance!! NelsonAnonymous
October 22, 2007
Hi Chris: I've found the cause for the previous error! As it seems Mcafee got a little too restrictive with my app and was not allowing it to connect to the server. I disabled Access Protection and is all working now. Thanks anyway!! Regards NelsonAnonymous
December 17, 2007
Hi Chris, I'm concerned with potential issues around scalability and performance of ReadProjectList and ReadProjectStatus with PSI customizations that need to process a small subset of projects against a PWA instance with a good amount of projects. Your example here shows retrieval of all projects into a dataset: DataTable projList = projWS.ReadProjectList().Tables[0]; foreach (DataRow dr in projList.Rows) { .... ///////////////////// //Retrieving Projects ///////////////////// I have noticed the filter xml capability (found in Calendars, Custom Fields, Lookup Tables, Resources, Assignments, etc.) is not a capability of retrieving Projects. Neither GetProjectList nor GetProjectStatus has the filter parameter and I do not see how one would, for example, request a list of projects without pulling every project over and looping through the resulting dataset. So if I had 1500 projects and only wanted those that belong to a particular department (maybe 15 projects), I need to pull over a list of 1500 projects to find the 15? This seems pretty expensive. /////////////////////// // 1000 row limitation? /////////////////////// Also, I've seen mention in blogs of a limitation of a maximum of 1000 rows in the resulting Project dataset on these calls. This does not seem believable but is this true? I have not yet tested this but it concerns me as we have clients with 1000+ projects and I don't want my customizations failing the first time ReadProjectList() is encountered. I hope neither case is true but if so, is there a workaround? Maybe a paging or filter mechanism in the SDK I'm not seeing? If this is true might it be wise for me to ponder the construction of a PSI extension to hit the working/published databases directly (read-only with no locks) to retrieve a filtered list of Projects UIDs that I could later call the single project methods on? Thanks, Paul CongdonAnonymous
December 30, 2007
Chris, Can I get you to sent me some code or direct me in the path of doing this with forms authentication? thanks FreddieAnonymous
June 09, 2010
I'm transitioning from VB.net to C# and am having a problem with the above code. I get several errors during build with a common theme. Here is the first: Error 1 The name 'conn' does not exist in the current context C:InetpubMSPS_PSI_CDefault.aspx.cs 98 9 C:InetpubMSPS_PSI_C Any ideas on why I can't set the variable conn? thanks, Alex Entire code: using System; using System.Configuration; using System.Data; using System.Linq; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.HtmlControls; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Xml.Linq; using System.Collections.Generic; using System.Text; using System.Net; using System.Resources; using System.Globalization; using System.Web.Services.Protocols; using System.Reflection; namespace PSIDemo { public class Connection { public const string Resource = "Resource"; public const string Project = "Project"; private static Dictionary<string, SoapHttpClientProtocol> WSDictionary; private string ms_ProjServURL; public Connection(string as_ProjServURL) { ms_ProjServURL = as_ProjServURL + "/_vti_bin/psi/"; WSDictionary = new Dictionary<string, SoapHttpClientProtocol>(); } public SoapHttpClientProtocol GetWebService(string as_WSName) { SoapHttpClientProtocol lo_WS; if (WSDictionary.TryGetValue(as_WSName.ToString(), out lo_WS) == false) { switch (as_WSName) { case Resource: Auth(Resource, new WSResource.Resource()); break; case Project: Auth(Project, new WSProject.Project()); break; } lo_WS = WSDictionary[as_WSName]; } return lo_WS; } public static void Reset() { WSDictionary.Clear(); } private void Auth(string as_WSName, SoapHttpClientProtocol as_WS) { try { object[] parameters = new object[1]; parameters[0] = ms_ProjServURL + as_WSName + ".asmx"; MethodInfo setUrlMethod = as_WS.GetType().GetProperty("Url").GetSetMethod(); setUrlMethod.Invoke(as_WS, parameters); parameters[0] = CredentialCache.DefaultCredentials; MethodInfo setCredentialsMethod = as_WS.GetType().GetProperty("Credentials").GetSetMethod(); setCredentialsMethod.Invoke(as_WS, parameters); WSDictionary.Add(as_WSName, as_WS); } catch (Exception ex) { throw ex; } } } } public partial class _Default : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { } private void txtURL_Leave(object sender, EventArgs e) { ddlProjects.Items.Clear(); conn = new PSIDemo.Connection(txtURL.Text); projWS = (WSProject.Project)conn.GetWebService(PSIDemo.Connection.Project); DataTable projList = projWS.ReadProjectList().Tables[0]; foreach (DataRow dr in projList.Rows) { ddlProjects.Items.Add(new ProjListItem(dr["Proj_Name"].ToString(), new Guid(dr[0].ToString()))); } if (ddlProjects.Items.Count > 0) { ddlProjects.SelectedItem = ddlProjects.Items[0]; } } private void ddlProjects_SelectedIndexChanged(object sender, EventArgs e) { lstResources.Items.Clear(); WSProject.ProjectTeamDataSet pds; ProjListItem projItem = (ProjListItem)ddlProjects.SelectedItem; pds = projWS.ReadProjectTeam(projItem.getGuid()); DataTable dt = pds.Tables["ProjectTeam"]; foreach (DataRow dr in dt.Rows) { lstResources.Items.Add(dr["Res_Name"].ToString()); } } }