Information
The topic you requested is included in another documentation set. For convenience, it's displayed below. Choose Switch to see the topic in its original location.

Using Project Server DataSet Objects

Office 2007

This content is outdated and is no longer being maintained. It is provided as a courtesy for individuals who are still using these technologies. This page may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist.

Client applications interact with Microsoft Office Project Server 2007 through the Project Server Interface (PSI) using typed data such as DataSet objects. Microsoft ADO.NET 2.0 provides the DataSet class that represents a memory-resident database. Each Project Server business entity is represented as a typed DataSet. With the PSI you do not create Microsoft .NET Framework data provider objects (such as Connection, Command, or DataAdapter) to connect to Project Server databases, or DataReader objects to retrieve data. Instead, you access the Project business entity, for example, as a ProjectDataSet through the PSI Project Web service.

The DataSet object provides a consistent relational programming model regardless of the source of the data it contains. A DataSet object can represent a complete set of data including the tables that contain, order, and constrain the data, as well as the relationships between the tables. As with a Microsoft SQL Server database, a DataSet object can include multiple DataTable and DataRelation objects.

This article includes the following sections.

There is no shortage of information available about ADO.NET 2.0 and programming with datasets. This section focuses on some simple examples that can help you get started programming with datasets in Project Server 2007. (For information about ADO.NET and datasets, see the links in the See Also section at the end of this article.)

A DataSet type—such as ProjectDataSet—is a class that derives from the DataSet class. It inherits all methods, events, and properties of the DataSet class. You can usually maintain DataSet objects within application memory during processing. If you want to serialize a ProjectDataSet to a file or stream to use on a disconnected mobile device, or to use with a script or COM-based application, you can use the WriteXml method. For example, the following code creates a Project object, gets information about a specified project in the Published database, and then writes the data to a file.

// Using a reference to the Project Web service named WebSvcProject, 
// for example, initialize a Project object that contains the 
// Web methods.
public static WebSvcProject.Project proj = 
    new WebSvcProject.Project();

// Not shown here: Set user credentials, logon cookie, 
// and URL for the Project object.

public SaveProjectInfoToXml(Guid projectUid, string filePathName)
{
    WebSvcProject.ProjectDataSet dsProject = 
        proj.ReadProject(projectUid, WebSvcProject.DataStoreEnum.PublishedStore);
    dsProject.WriteXml(filePathName);
}

For a published project that contains only the project name Test Project 1, the output file in the previous example contains the following content.

<?xml version="1.0" standalone="yes"?>
<ProjectDataSet xmlns="http://lucernepublishing.com/ProjectDataSet.xsd">
  <Project>
    <PROJ_UID>3b91f68b-de5a-4dcf-8a42-78bfea542e52</PROJ_UID>
    <PROJ_NAME>Test Project 1</PROJ_NAME>
    <PROJ_TYPE>0</PROJ_TYPE>
  </Project>
</ProjectDataSet>

You can also use the debugger in Microsoft Visual Studio 2005 to see the contents of a DataSet. For an example of how to use the DataSet Visualizer, see PSI Example 2: Adding Properties.

For a project that contains tasks, assignments, resources, custom fields, and other information, the output file would contain all of the project data. When you serialize a DataSet, the result includes only the data and does not show the empty properties. Using the PSI Reference, you can see that a ProjectDataSet object can include the following DataTable objects (along with change events, change event handlers, and other tables):

Many projects include a large amount of data. Instead of ReadProject, you can use the ReadProjectEntities method to get specific kinds of data. Some PSI methods use a filter or xmlFilter parameter to limit the amount of returned data, such as ReadResourceAssignments. For more information, see How to: Use a Filter Parameter with PSI Methods.

Limiting the Data Sent in DataSets

In some circumstances, you can limit the amount of information sent back to the server to just the updated or just the added information. This is true for the ProjectDataSet used in QueueUpdateProject and QueueAddToProject calls.

         // Following is an excerpt
         // Save enterprise assignment
         jobId = Guid.NewGuid();
         // Get only the added rows.
         projectDs = (ProjectWebSvc.ProjectDataSet) projectDs.GetChanges(DataRowState.Added);
         projectSvc.QueueAddToProject(jobId, sessionUid, projectDs, false);
         WaitForQueue(q,jobId);
         

Several examples of this technique are provided in the Using the ProjTool Test Application sample application.

Creating Utility Classes with DataSets

The PSI and DataSet classes provide many methods, but you will likely need to add some methods in utility classes to do common jobs. For example, the Project class provides a GetProjectNameFromProjectUid method, but does not have a corresponding method to get the project GUID from a project name. However, developing a routine to do that is straightforward. Because the columns and rows in a DataSet object are typed objects, it is easy to search for data and access specific items.

Along with late bound access to values through weakly typed variables, the DataSet provides access to data through a strongly typed metaphor. You can access tables and columns that are part of the DataSet object with user-friendly names and typed variables. You access tables and columns by name rather than by collection methods. For example, in dsProjectList.Project[row].PROJ_NAME, the DataSet table is Project and the column is PROJ_NAME.

using System;
. . .
        public Guid GetProjectUidFromProjectName(WebSvcProject.Project proj, string projectName)
        {
            Guid result = Guid.Empty;
                WebSvcProject.ProjectDataSet dsProjectList = new LoginDemo.WebSvcProject.ProjectDataSet();
                dsProjectList = proj.ReadProjectList();

                for (int row = 0; row < dsProjectList.Project.Count; row++)
                {
                    if (projectName == dsProjectList.Project[row].PROJ_NAME.ToString())
                    {
                        result = dsProjectList.Project[row].PROJ_UID;
                        break;
                    }
                }
            return result;
        }

You can put the GetProjectUidFromProjectName method in a utility class to use with different development projects that use the Project Web service. The proj parameter receives a Project object, where the Project Web service reference is named WebSvcProject.

The proj object contains the user credentials, Project Server logon cookie, URL of the Project Web service, and all of the Project methods. ReadProjectList returns a ProjectDataSet object to dsProjectList.

NoteNote

ReadProjectList returns a list of all projects to which the user has access, and requires the ManageQueue and ManageSecurity global Project Server permissions.

In the following example, the application namespace is MyApp and the utilities class name is ProjectUtilties. The application initializes a Project object named project, creates an instance of the ProjectUtilties class, and calls GetProjectUidFromProjectName to get the GUID value (PROJ_UID) for the project Test Project 1.

using System;
using System.Net;
using System.Web.Services.Protocols;
. . .
// Class variable
public static WebSvcProject.Project project = 
            new MyApp.WebSvcProject.Project();
. . .
// Not shown here: Add user credentials, logon cookie, 
// and Web service URL to the project object.
. . .
    try
    {
        MyApp.ProjectUtilities utils = new ProjectUtilities();
        Guid tGuid = utils.GetProjectUidFromProjectName(project, 
        "Test Project 1");
    }
    catch(SoapException ex)
    {. . .}
    catch(WebException ex)
    {. . .}

PSI methods use DataSet classes to transport data with XML Web services. The PSI methods are the API for the Project Server business objects, which in turn manipulate data in the Project Server databases through the data access layer (DAL). For example, the QueueCreateProject method accepts project data in a parameter of type ProjectDataSet, and you can access lookup tables for enterprise outline codes in a LookupTableDataSet object returned from the PSI method ReadLookupTables. Visual Studio checks types at compile time and uses IntelliSense to provide code completion of data elements.

With ADO.NET 2.0, you can create a DataTable independently from a DataSet object, add data and manipulate the DataTable, and then add it to a DataSet object. Similarly, you can create a disconnected DataRow and then add it to the DataTable.

PSI Example 1: Creating a Project

PSI Example 1 describes the basic steps to create and publish a project programmatically, where you create a ProjectRow object, add it to a ProjectDataTable in a ProjectDataSet object, and then save and publish the project. PSI Example 2: Adding Properties shows an alternative way to set properties on a project, where the ProjectDataSet already exists.

To create a project with the PSI:

  1. In Solution Explorer in Visual Studio 2005, set a Web reference to the Project Web service of the PSI. For example, for a default installation of Project Server 2007, add a Web reference to http://ServerName/ProjectServer/_vti_bin/psi/Project.asmx and name the reference WebSvcProject.

  2. Initialize a variable for the WebSvcProject.Project object, for example project. That is, create an instance of the Project class named project.

  3. Create a ProjectDataSet object, for example dsProject. Create a row with the NewProjectRow method, for example, projectRow. This row is disconnected until you add it to the data table.

  4. Set the projectRow properties.

  5. Using the AddProjectRow method, add the row to the ProjectDataTable object.

  6. To save the new project to the Draft database for the Project Server instance specified in the project object, call QueueCreateProject.

  7. To publish the saved project to the Published database, call QueuePublish on the project object.

    NoteNote

    Project Server 2007 uses four databases: Draft, Published, Reporting, and Archive. For more information, see Project Server Architecture.

The following Microsoft Visual C# code shows the same steps in detail.

using System;
using System.Data;
using System.Web;
using System.Web.Services.Protocols;
using System.Threading;
using PSLibrary = Microsoft.Office.Project.Server.Library;

namespace AppNamespace {
    partial class Demo {
        public static void CreateProject() {
. . .
/* Step 1: Set a Web reference in Solution Explorer.
   Step 2:  Create an instance of the Project class. Keep the project 
            object in memory for multiple calls. */
public static WebSvcProject.Project project = 
   new WebSvcProject.Project();
/* NOTE: The logon to Project Server is not shown here. You can use 
 *       the LoginWindows or LoginForms Web services. Then set user 
 *       credentials, logon cookie, and URL for the proj object. */

/* Step 3: Create a ProjectDataSet and a new row. */
WebSvcProject.ProjectDataSet dsProject = new 
   WebSvcProject.ProjectDataSet();
WebSvcProject.ProjectDataSet.ProjectRow projectRow = 
    dsProject.ProjectDataTable.NewProjectRow();

/* Step 4: Set the project row properties. */
Guid projectGuid = Guid.NewGuid();
    // Alternately, specify a GUID such as {FA8EA0AA-BFEB-43a0-BC17-6604F5C13D93}.
    // The constant must be on one line:
    // Guid projectGuid = new Guid("{ 0xfa8ea0aa, 0xbfeb, 0x43a0, 
    //    { 0xbc, 0x17, 0x66, 0x4, 0xf5, 0xc1, 0x3d, 0x93 }}");
projectRow.PROJ_UID = projectGuid;
    // ProjectType.Project is the default enumeration.
projectRow.PROJ_TYPE = (int)PSLibrary.Project.ProjectType.Project; 
projectRow.PROJ_NAME = "Test Project 1";

/* Step 5: Add the row to the data table. Here, Project is a property 
 *         of dsProject, of type ProjectDataSet.ProjectDataTable. 
 *         AddProjectRow is a method of ProjectDataTable. */
dsProject.Project.AddProjectRow(projectRow);

/* Step 6: Call QueueCreateProject and pass the ProjectDataSet object. 
 *         Set the validateOnly parameter to false to save the project 
 *         to the Draft database */
Guid jobGuid = Guid.NewGuid();
project.QueueCreateProject(jobGuid, dsProject, false);

// Wait three seconds for Queue job to complete. 
// Alternatively, create a utility that uses the QueueSystem Web service
// to determine when the Queue job is complete.
System.Threading.Thread.Sleep(3000);

/* Step 7: Save to the Published database and create the default 
 *         Windows SharePoint Services project workspace. 
 *         Set fullPublish to true, and publish with a new Queue jobUid. */
string wssUrl = "";
bool fullPublish = true;
Guid jobGuid = Guid.NewGuid();
WebSvcProject.ProjectRelationsDataSet dsProjectRelations = new 
   WebSvcProject.ProjectRelationsDataSet();

dsProjectRelations = project.QueuePublish(
    jobGuid, projectGuid, fullPublish, wssUrl);
} } }

The projectRow variable represents a disconnected object of type ProjectDataRow. When you add the row to the ProjectDataTable object, the row becomes one project in the collection. Figure 1 shows the main steps to create a row and then add it to the data table.

Figure 1. Adding a ProjectRow to the ProjectDataTable

Adding a ProjectRow to the ProjectDataTable
NoteNote

The ProjectDataSet includes the collection of data tables described in Programming with DataSets, and each DataTable has a DataRow object with the properties and methods for that type of object. For more information, see the DataTable and DataRow classes in the WebSvcProject namespace of the PSI Reference.

The QueuePublish method adds a publish message to the Project Server queue to publish the data that is saved in the Draft database. That is, Project Server has a queuing service that maintains a Publish queue. If you follow one Queue job by another that depends on the first, such as QueueCreateProject and QueuePublish, you must wait for the first job to finish before calling the second.

You can specify whether to publish all data (full publish) or just the changed data (incremental publish). The jobGuid parameter for QueueCreateProject method and QueuePublish method is a GUID associated with each job in the queue. Project Server has job handlers that asynchronously publish jobs in the queue in the order of requests. The job handlers send events that allow you to find out the job status.

You could use dsProjectRelations to create a new ProjectRelationsRow object and set a relationship of your published project to another project. For more information, see the ProjectRelationsDataSet.ProjectRelationsRow properties in the PSI Reference.

PSI Example 2: Adding Properties

Because the PSI is built upon the .NET Framework 2.0, there are many advantages in development. It is easy to add properties to a DataRow object. For example, you can process dates in different languages and avoid potential problems with the correct date format for the installed Project Server system.

To add other properties, such as a project start date:

  1. Create a ProjectRow object and add it to the ProjectDataTable, as described in PSI Example 1: Creating a Project.

  2. Set additional properties on the existing ProjectRow.

The following code shows how to add a project property to an existing project, which is a different way that you can work with datasets. The code in bold text is an example that also shows how to create a language-independent project start date.

. . .
using System.Globalization;
. . .
WebSvcProject.ProjectDataSet.ProjectRow projectRow = 
    dsProject.ProjectDataTable.NewProjectRow();
/* Step 4: Set the project row properties. */
Guid projectGuid = Guid.NewGuid();
projectRow.PROJ_UID = projectGuid;
projectRow.PROJ_TYPE = (int)PSLibrary.Project.ProjectType.Project; 
projectRow.PROJ_NAME = "Test Project 2"

/* Step 5: Add the row to the data table. Here, Project is a property 
 *         of dsProject, of type ProjectDataSet.ProjectDataTable. 
 *         AddProjectRow is a method of ProjectDataTable. */
dsProject.Project.AddProjectRow(projectRow);

/* Step 5a: Now add a language-independent start date. */
int rowIndex = dsProject.Project.Count - 1;
CultureInfo germanCulture = new CultureInfo("de-DE");dsProject.Project[rowIndex].PROJ_INFO_START_DATE =     DateTime.Parse("31 Mai 2006", germanCulture);
. . .
} } }

The .NET Framework handles conversion of the German-language string "24 Oktober 2005" to an internal date that displays long-format dates as "October 24, 2005" on an English-language system, "le 24 Octobre, 2005" on a French-language system, and so on. This is just one example of the many advantages of having the PSI built on the .NET Framework.

If you serialize the dsProject object to a file, you can see the internal value of the start date.

<?xml version="1.0" standalone="yes"?>
<ProjectDataSet xmlns="http://lucernepublishing.com/ProjectDataSet.xsd">
  <Project>
    <PROJ_UID>e2359c97-f060-4162-917c-e8b17bfdc7ee</PROJ_UID>
    <PROJ_NAME>Test Project 2</PROJ_NAME>
    <PROJ_TYPE>0</PROJ_TYPE>
    <PROJ_INFO_START_DATE>2006-05-31T00:00:00-07:00</PROJ_INFO_START_DATE>
  </Project>
</ProjectDataSet>

You can easily see DataSet values in a debug session (Figure 2). For example, set a breakpoint after the dsProject.Project[rowIndex].PROJ_INFO_START_DATE line in the previous code, click the magnifying glass icon that appears when you pause the mouse pointer over dsProject, and then click DataSet Visualizer.

Figure 2. Using the DataSet Visualizer in Visual Studio 2005

Using the DataSet Visualizer in Visual Studio 2005

Scroll to the PROJ_INFO_START_DATE column in the DataSet Visualizer dialog box to see the value 5/31/2006 12:00:00 AM.

NoteNote

When you create or update a project, the PSI can process up to 1000 rows of data at a time. If the total number of rows of new or updated data in all tables of a ProjectDataSet object exceeds 1000, the PSI returns the ProjectExceededItemsLimit error.

You can use several different procedures to update Project Server data by using the PSI. Some methods, such as, update only entities in the ProjectDataSet parameter that already exist in Project Server. QueueUpdateProject does not create or delete entities, but can delete custom field values in a project. For an example that shows how to delete a task custom field value by deleting a TaskCustomFieldsRow in the TaskCustomFieldsDataTable of a ProjectDataSet, see QueueUpdateProject.

Some methods, such as UpdateStatus, use a changeXml parameter, which is an XML string of the change delta. Other methods, such as UpdateLookupTables, create, modify, or delete data within a lookup table.

For example, a lookup table value is one LookupTableDataSet.LookupTableTreesRow object in a LookupTableTreesDataTable object. To delete a lookup table value, use the following steps.

  1. To get a complete LookupTableDataSet, use the ReadLookupTablesByUids method.

  2. Marks the row for deletion by using the LookupTableDataSet.LookupTableTrees.Rows[index].Delete method.

  3. Get the lookup table GUID, and then check out the lookup table by using CheckOutLookupTables.

  4. Use UpdateLookupTables with the modified LookupTableDataSet.

  5. Check in the lookup table using CheckInLookupTables.

NoteNote

The RemoveLookupTableTreesRow method removes the row from the LookupTableTreesDataTable in the LookupTableDataSet, but does not mark the row for deletion in the Project Server database.

The following example reads a specified lookup table and deletes a specified row in the table. The lookupTable parameter in the DeleteLookupTableRow sample method is a LookupTable object with valid Url and Credentials properties. The language parameter is an integer for the LCID; for example, 1033 is U.S. English. LookupTableWebSvc is an arbitrary reference name to the LookupTable.asmx Web service. For more information about creating lookup tables, see Walkthrough: Creating a Hierarchical Lookup Table.

public string DeleteLookupTableRow(LookupTableWebSvc.LookupTable lookupTable,
    Guid lutGuid, int row, int language)
{
    string result = "";
    string fmtResult = "Lookup table: {0}. ";
    string tableName = "[unknown]";
    string rowName = "";

    LookupTableWebSvc.LookupTableDataSet lutDs = 
        new LookupTableWebSvc.LookupTableDataSet();

    Guid[] lutList = new Guid[] { lutGuid };
    bool autoCheckOut = false;

    lutDs = lookupTable.ReadLookupTablesByUids(lutList, autoCheckOutIn, language);
    int numRows = lutDs.LookupTableTrees.Rows.Count;

    foreach (DataRow lutRow in lutDs.LookupTables)
    {
        if ((Guid)lutRow[lutDs.LookupTables.LT_UIDColumn] == lutGuid)
        {
            tableName = (string)lutRow[lutDs.LookupTables.LT_NAMEColumn];
            break;
        }
    }

    if (row < numRows)
    {
        rowName = lutDs.LookupTableTrees[row].LT_VALUE_TEXT;
        lutDs.LookupTableTrees.Rows[row].Delete();

        lookupTable.CheckOutLookupTables(lutList);
        bool validateOnly = false;
        lookupTable.UpdateLookupTables(lutDs, validateOnly, autoCheckOut, language);
        bool forceCheckIn = false;
        lookupTable.CheckInLookupTables(lutList, forceCheckIn);

        fmtResult += "Row {1} deleted: {2}";
        result = string.Format(fmtResult, tableName, row, rowName);
    }
    else
    {
        fmtResult += "Requested row {1} greater than number of rows {2}.";
        result = string.Format(fmtResult, tableName, row, numRows);
    }
    return result;
}

DataSet types are bound to a specific XML schema definition at design time. A DataSet derived type provides typed methods, events, and properties. That means you can access tables and columns by name instead of using collection-based methods. In addition to improved readability of the code, a DataSet type also supports IntelliSense in Visual Studio, where the Code Editor helps you complete code lines as you write them. Furthermore, the DataSet provides access to values of the correct type at compile time. Visual Studio catches type mismatch errors when you compile the code rather than at run time.

The DataSet type definition is available as an XML schema (.xsd) file in the Web service description. To access the XML schema, append the Web Service Description Language (WSDL) Schema query string parameter and the name of the dataset to the URL of the .asmx page. For example, the following URL returns the schema for the Web service including the list of five datasets that the Project Web service uses, including ProjectDataSet.

http://ServerName/ProjectServer/_vti_bin/psi/project.asmx?wsdl

However, the detailed schema of the ProjectDataSet, for example, is not available until run time. The following schema fragment defines the ReadProject method parameters and response in the Project Web service.

<s:element name="ReadProject">
  <s:complexType>
    <s:sequence>
      <s:element minOccurs="1" maxOccurs="1" name="projectUid"type="s1:guid" />
      <s:element minOccurs="1" maxOccurs="1" name="dataStore"type="tns:DataStoreEnum" />
    </s:sequence>
  </s:complexType>
</s:element>
<s:simpleType name="DataStoreEnum">
  <s:restriction base="s:string">
    <s:enumeration value="WorkingStore" />
    <s:enumeration value="PublishedStore" />
    <s:enumeration value="VersionsStore" />
    <s:enumeration value="ReportingStore" />
  </s:restriction>
</s:simpleType>
<s:element name="ReadProjectResponse">
  <s:complexType>
    <s:sequence>
      <s:element minOccurs="0" maxOccurs="1" name="ReadProjectResult">
        <s:complexType>
          <s:sequence>
            <s:any namespace="http://lucernepublishing.com/ProjectDataSet.xsd" />
          </s:sequence>
        </s:complexType>
      </s:element>
    </s:sequence>
  </s:complexType>
</s:element>
NoteNote

The DataStoreEnum values correspond to the Draft, Published, Archive, and Reporting databases.

In the compile-time schema definition, the code lines in bold text show that ReadProject requires the parameters projectUid of type guid and dataStore of type DataStoreEnum, and returns a ProjectDataSet object. The run-time SOAP reply contains the full schema definition of the ProjectDataSet type along with the data values and a Microsoft–specific attribute that tells the .NET Framework the ProjectDataSet element is a DataSet.

The purpose of using DataSet types in the PSI is to conveniently model the business entities in the business object layer for development of applications using the .NET Framework. The PSI and DataSet types provide a logical abstraction of the Project Server business objects, not of the underlying Project Server databases. For more information about business objects and the DAL, see Project Server Architecture.

The use of datasets in the PSI Web services requires the .NET Framework 2.0 or later for easy development. You can use development tools and client applications that are not based on the .NET Framework, for example Microsoft Visual Basic 6.0, Visual C++ 7.0 or earlier, or Java 2 Enterprise Edition (J2EE) from Sun Microsystems. However, using a system that is not based on the .NET Framework requires you to parse the DataSet schema to create SOAP calls and also parse the run-time schema and returned values in the XML reply.

The formal WSDL output of a PSI Web service shows DataSet and schema locations in each Web service proxy. For example, the output from project.asmx?wsdl includes the following.

<s:import namespace="http://lucernepublishing.com/ProjectDataSet.xsd" />  
. . . 
<s:import schemaLocation=  
"http://ServerName:56737/DefaultSSP/PSI/project.asmx?schema=
   ProjectDataSet" 
   namespace="http://lucernepublishing.com/ProjectDataSet.xsd" />

The schema URL option provides a way to get the full schema of each PSI Web service. For example, instead of using the port and address of the PSI from Microsoft Internet Information Services (IIS), you can also use the following URL for the ProjectDataSet schema in the Project Web service:

http://ServerName/ProjectServer/_vti_bin/psi/project.asmx?schema=ProjectDataSet

Figure 3 shows the view of the ProjectDataSet schema in Internet Explorer. You can expand the "+" elements to show, or collapse the "-" elements to hide, all of the child elements and fields. For example, if you click AssignmentCustomFields, you see the fields CUSTOM_FIELD_UID of type System.Guid, FIELD_TYPE_ENUM, TEXT_VALUE, and so forth.

Figure 3. Schema of the ProjectDataSet in Internet Explorer

Schema of the ProjectDataSet in Internet Explorer

Writing code to parse DataSet schemas and return values is beyond the scope of the Project 2007 SDK. Use of the .NET Framework and Visual Studio 2005 makes development much easier.

Creating and using datasets is a key part of development with the PSI in Project Server 2007. You can use built-in DataSet methods to serialize and deserialize data for datasets, and you can quickly see the data during a debug session in Visual Studio with the DataSet Visualizer.

The sample code in PSI Example 1: Creating a Project shows only the minimum code you need to create, save, and publish a project to the Draft and Published databases. The code in PSI Example 2: Adding Properties is a very brief sample that shows how to add properties to an existing DataSet object and uses globalization features of the .NET Framework 2.0. For more complete code examples, see the how-to and walkthrough topics in Project Server Programming Tasks.

Show:
© 2014 Microsoft