 |
|
| Edit in Browser | /_layouts/images/icxddoc.gif | /_layouts/formserver.aspx?XsnLocation={ItemUrl}&OpenIn=Browser | 0x0 | 0x1 | FileType | xsn | 255 | | Edit in Browser | /_layouts/images/icxddoc.gif | /_layouts/formserver.aspx?XmlLocation={ItemUrl}&OpenIn=Browser | 0x0 | 0x1 | ProgId | InfoPath.Document | 255 | | Edit in Browser | /_layouts/images/icxddoc.gif | /_layouts/formserver.aspx?XmlLocation={ItemUrl}&OpenIn=Browser | 0x0 | 0x1 | ProgId | InfoPath.Document.2 | 255 | | Edit in Browser | /_layouts/images/icxddoc.gif | /_layouts/formserver.aspx?XmlLocation={ItemUrl}&OpenIn=Browser | 0x0 | 0x1 | ProgId | InfoPath.Document.3 | 255 | | Edit in Browser | /_layouts/images/icxddoc.gif | /_layouts/formserver.aspx?XmlLocation={ItemUrl}&OpenIn=Browser | 0x0 | 0x1 | ProgId | InfoPath.Document.4 | 255 | | View in Web Browser | /_layouts/images/ichtmxls.gif | /_layouts/xlviewer.aspx?listguid={ListId}&itemid={ItemId}&DefaultItemOpen=1 | 0x0 | 0x1 | FileType | xlsx | 255 | | View in Web Browser | /_layouts/images/ichtmxls.gif | /_layouts/xlviewer.aspx?listguid={ListId}&itemid={ItemId}&DefaultItemOpen=1 | 0x0 | 0x1 | FileType | xlsb | 255 | | Snapshot in Excel | /_layouts/images/ewr134.gif | /_layouts/xlviewer.aspx?listguid={ListId}&itemid={ItemId}&Snapshot=1 | 0x0 | 0x1 | FileType | xlsx | 256 | | Snapshot in Excel | /_layouts/images/ewr134.gif | /_layouts/xlviewer.aspx?listguid={ListId}&itemid={ItemId}&Snapshot=1 | 0x0 | 0x1 | FileType | xlsb | 256 |
|
|
| Edit in Browser | /_layouts/images/icxddoc.gif | /_layouts/formserver.aspx?XsnLocation={ItemUrl}&OpenIn=Browser | 0x0 | 0x1 | FileType | xsn | 255 | | Edit in Browser | /_layouts/images/icxddoc.gif | /_layouts/formserver.aspx?XmlLocation={ItemUrl}&OpenIn=Browser | 0x0 | 0x1 | ProgId | InfoPath.Document | 255 | | Edit in Browser | /_layouts/images/icxddoc.gif | /_layouts/formserver.aspx?XmlLocation={ItemUrl}&OpenIn=Browser | 0x0 | 0x1 | ProgId | InfoPath.Document.2 | 255 | | Edit in Browser | /_layouts/images/icxddoc.gif | /_layouts/formserver.aspx?XmlLocation={ItemUrl}&OpenIn=Browser | 0x0 | 0x1 | ProgId | InfoPath.Document.3 | 255 | | Edit in Browser | /_layouts/images/icxddoc.gif | /_layouts/formserver.aspx?XmlLocation={ItemUrl}&OpenIn=Browser | 0x0 | 0x1 | ProgId | InfoPath.Document.4 | 255 | | View in Web Browser | /_layouts/images/ichtmxls.gif | /_layouts/xlviewer.aspx?listguid={ListId}&itemid={ItemId}&DefaultItemOpen=1 | 0x0 | 0x1 | FileType | xlsx | 255 | | View in Web Browser | /_layouts/images/ichtmxls.gif | /_layouts/xlviewer.aspx?listguid={ListId}&itemid={ItemId}&DefaultItemOpen=1 | 0x0 | 0x1 | FileType | xlsb | 255 | | Snapshot in Excel | /_layouts/images/ewr134.gif | /_layouts/xlviewer.aspx?listguid={ListId}&itemid={ItemId}&Snapshot=1 | 0x0 | 0x1 | FileType | xlsx | 256 | | Snapshot in Excel | /_layouts/images/ewr134.gif | /_layouts/xlviewer.aspx?listguid={ListId}&itemid={ItemId}&Snapshot=1 | 0x0 | 0x1 | FileType | xlsb | 256 |
|
|
| Edit in Browser | /_layouts/images/icxddoc.gif | /_layouts/formserver.aspx?XsnLocation={ItemUrl}&OpenIn=Browser | 0x0 | 0x1 | FileType | xsn | 255 | | Edit in Browser | /_layouts/images/icxddoc.gif | /_layouts/formserver.aspx?XmlLocation={ItemUrl}&OpenIn=Browser | 0x0 | 0x1 | ProgId | InfoPath.Document | 255 | | Edit in Browser | /_layouts/images/icxddoc.gif | /_layouts/formserver.aspx?XmlLocation={ItemUrl}&OpenIn=Browser | 0x0 | 0x1 | ProgId | InfoPath.Document.2 | 255 | | Edit in Browser | /_layouts/images/icxddoc.gif | /_layouts/formserver.aspx?XmlLocation={ItemUrl}&OpenIn=Browser | 0x0 | 0x1 | ProgId | InfoPath.Document.3 | 255 | | Edit in Browser | /_layouts/images/icxddoc.gif | /_layouts/formserver.aspx?XmlLocation={ItemUrl}&OpenIn=Browser | 0x0 | 0x1 | ProgId | InfoPath.Document.4 | 255 | | View in Web Browser | /_layouts/images/ichtmxls.gif | /_layouts/xlviewer.aspx?listguid={ListId}&itemid={ItemId}&DefaultItemOpen=1 | 0x0 | 0x1 | FileType | xlsx | 255 | | View in Web Browser | /_layouts/images/ichtmxls.gif | /_layouts/xlviewer.aspx?listguid={ListId}&itemid={ItemId}&DefaultItemOpen=1 | 0x0 | 0x1 | FileType | xlsb | 255 | | Snapshot in Excel | /_layouts/images/ewr134.gif | /_layouts/xlviewer.aspx?listguid={ListId}&itemid={ItemId}&Snapshot=1 | 0x0 | 0x1 | FileType | xlsx | 256 | | Snapshot in Excel | /_layouts/images/ewr134.gif | /_layouts/xlviewer.aspx?listguid={ListId}&itemid={ItemId}&Snapshot=1 | 0x0 | 0x1 | FileType | xlsb | 256 |
|
|
|
 |
|
|
|
|
|
Project Server Help Blog > Categories
|
5/12/2010
Implementing and Administering Microsoft Project Server 2010 sets the deployment Gold Standard. Do not start your Microsoft Project Server 2010 implementation without it!
New York, NY, May 13, 2010 –msProjectExperts essential reference for Microsoft Project Server 2010 begins shipping June 14, in step with the general availability launch of the Microsoft Office 2010 suite. "We are pleased to announce the pre-sale availability of our most popular book in our series for Microsoft Project Server 2010 and Microsoft Project 2010," commented Gary Chefetz, Microsoft Project MVP and the book's primary author. "My co-authors Dale Howard, Tony Zink and I are very excited to be first to market with Project Server 2010 guidance, especially considering the importance of this release," he continued.
Project Server 2010 is a landmark release of Microsoft's market-leading project and portfolio management software. "This is the one I've been waiting for," remarked Chefetz. "This release fulfills the potential I saw in this software when I first started using it as a PMO Director in 2000, when Microsoft first released Project Central. Project Server 2010 connects enterprise project management, enterprise portfolio management, resource management and business intelligence in an unprecedented package through the marriage of Microsoft's project management and SharePoint 2010 technologies."
Implementing and Administering Microsoft Project Server 2010 covers all aspects of Project Server deployment, from installation through configuration, and includes non-technical topics such as preparing an organization for an enterprise project management deployment and providing techniques for organizational change management. "Time and again we see organizations master the technical challenges of deploying an enterprise project management tool, but fail to plan for the organizational challenges, which very often prove to be the most difficult," Chefetz said. "We tackle this right up front in Implementing and Administering Microsoft Project Server 2010, and follow this guidance with best-practice considerations throughout the book."
At 912 pages, Implementing and Administering Microsoft Project Server 2010 includes extensive SharePoint Server 2010 management guidance and a strong introduction to configuring and managing Microsoft's business intelligence stack, including Excel Services and PerformancePoint Services. "We show you how to get started with SharePoint Server 2010 workflows, how to build and deliver Excel reports and how to build dashboards for your Project Server 2010 implementation," commented Chefetz.
Implementing and Administering Microsoft Project Server 2010 is available directly from msProjectExperts or your favorite bookseller. For a limited time only, you can pre-order your copy from msProjectExperts at special pre-order pricing.
Contact: info@msprojectexperts.com 12/17/2009Recently on the microsoft.public.project.developer newsgroup, a user posted the following question: I want to consume the PSI Calendar webservice for reporting purposes. I created a shared datasource in Visual Studio 2008 (installed by SQL2008) to access the PSI ListCalendars webservice and it works fine (finally). Now I'm trying to consume the Calendar.ReadCalendars PSI webservice via the same datasource but it keeps on giving me the following error: *** The remote server returned an error: (500) Internal Server Error. <snip> <class name="Value cannot be null. Parameter name: s"><error id="13043" name="CalendarFilterInvalid" uid="7ae0376c-f8c9-48ed-8184-c9e0fd6c3694" /></class> <snip> *** Obviously it complains about 'CalendarFilterInvalid', but the filter parameter is set (see below) and even when put a value in it, it keeps on giving the same error. I am using this query... 1: <Query> 2: <SoapAction>http://schemas.microsoft.com/office/project/server/webservices/Calendar/ReadCalendars</SoapAction> 3: <Method Name="ReadCalendars" Namespace="http://schemas.microsoft.com/office/project/server/webservices/Calendar"> 4: <Parameters> 5: <Parameter Name="filter"> 6: <DefaultValue>String.Empty</DefaultValue> 7: </Parameter> 8: <Parameter Name="autoCheckOut"> 9: <DefaultValue>false</DefaultValue> 10: </Parameter> 11: </Parameters> 12: </Method> 13: <ElementPath IgnoreNamespaces="true">*</ElementPath> 14: </Query>
I was able to reliably reproduce the reported error in using a console application, and examination of the SoapException’s Detail.InnerXml property yielded the following additional information:
1: <errinfo> 2: <general> 3: <class name="The filter parameter cannot be empty. You must at least specify the table and fields to return."> 4: <error id="13043" name="CalendarFilterInvalid" uid="8f74dedb-4491-4b36-81ee-295d7ac5a988" /> 5: </class> 6: </general> 7: </errinfo>
Although the SDK article on the ReadCalendars() method states that passing String.Empty for the filter parameter will return a list of all enterprise calendars, from the post quoted above and my own testing it would appear that this is not the case. When I reported this inconsistency between the SDK and the PSI’s behavior to Microsoft, their response was that they do not plan to fix this issue in the near future. However, they do plan to update the SDK to reflect the actual behavior of the PSI.
In order to get ReadCalendars() to correctly return a list of all enterprise calendars, you need to use code similar to the following:
1: CalendarSvc.Calendar _calPsi = new CalendarSvc.Calendar(); 2: _calPsi.Url = "http://epmdemo/pwa/_vti_bin/psi/calendar.asmx"; 3: _calPsi.Credentials = CredentialCache.DefaultCredentials; 4: 5: Filter _calFilter = new Filter(); 6: using (CalendarSvc.CalendarDataSet _calData = new CalendarSvc.CalendarDataSet()) 7: { 8: _calFilter.FilterTableName = _calData.Calendars.TableName; 9: _calFilter.Fields.Add(new Filter.Field(_calData.Calendars.CAL_CHECKOUTBYColumn.ColumnName)); 10: _calFilter.Fields.Add(new Filter.Field(_calData.Calendars.CAL_CHECKOUTDATEColumn.ColumnName)); 11: _calFilter.Fields.Add(new Filter.Field(_calData.Calendars.CAL_IS_STANDARD_CALColumn.ColumnName)); 12: _calFilter.Fields.Add(new Filter.Field(_calData.Calendars.CAL_NAMEColumn.ColumnName)); 13: _calFilter.Fields.Add(new Filter.Field(_calData.Calendars.CAL_UIDColumn.ColumnName)); 14: } 15: 16: CalendarSvc.CalendarDataSet _calDs = _calPsi.ReadCalendars(_calFilter.GetXml(), false);
If you are trying to consume the ReadCalendars() method in SSRS, things get a little more complicated.
First, you need to create an XML DataSource and set its connection string to the Calendar Web service’s URL (e.g. http://epmdemo/pwa/_vti_bin/psi/calendar.asmx).
Next, create a new dataset that uses the XML data source, as shown below.
Set the query for this dataset to:
1: <Query> 2: <Method Namespace="http://schemas.microsoft.com/office/project/server/webservices/Calendar/" Name="ReadCalendars" /> 3: <SoapAction>http://schemas.microsoft.com/office/project/server/webservices/Calendar/ReadCalendars</SoapAction> 4: <Parameters> 5: <Parameter Name="filter" Type="xml" /> 6: <Parameter Name="autoCheckOut" Type="boolean" /> 7: </Parameters> 8: <ElementPath IgnoreNamespaces="true">ReadCalendarsResponse/ReadCalendarsResult/diffgram/CalendarDataSet/Calendars</ElementPath> 9: </Query>
Make sure to leave the parameter elements blank!
Next, create two parameters in the dataset. The names of these parameters need to match the names of the parameters for the ReadCalendars() method, so you should name the first parameter autoCheckOut and name the second filter. If there are any extraneous pa
Set the parameter value for autoCheckOut to false, and populate the parameter value for filter with:
1: <?xml version="1.0" encoding="utf-16"?> 2: <Filter xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" filterTableName="Calendars" xmlns="http://microsoft.com/ProjectServer/FilterSchema.xsd"> 3: <Fields> 4: <Field tableName="Calendars" fieldName="CAL_CHECKOUTBY" /> 5: <Field tableName="Calendars" fieldName="CAL_CHECKOUTDATE" /> 6: <Field tableName="Calendars" fieldName="CAL_IS_STANDARD_CAL" /> 7: <Field tableName="Calendars" fieldName="CAL_NAME" /> 8: <Field tableName="Calendars" fieldName="CAL_UID" /> 9: </Fields> 10: <Criteria /> 11: </Filter>
We’re almost done. Now you need to delete any fields the Report Designer added into the dataset and replace them with your desired fields from the CalendarDataSet.Calendars table. In this example, I’m only using CAL_UID and CAL_NAME. Note that the field name and field source for each field need to match.
Finally, create a new table in the report that uses the Dataset you created and add your desired columns.
You should now have a functioning report.
If you specify the value for the filter parameter in the dataset query, the report will not work properly – for some reason it gets mangled during query processing. Placing the filter XML into a dataset parameter avoids this issue. 10/16/2009
When it comes to implementing Project Server, many would agree that installing and configuring the tool is the easy part; the hard part comes when you try to 'motivate' everyone in the organization to use the system properly and consistently. Although forcing some people to log their task progress or view the online reports that you spent hours building can be an exercise in futility, at least we can monitor usage patterns to get some insight into who IS using the system and which features or reports / views they are using on a regular basis.
This technique is for PWA only and will not allow you to see Project Professional usage, and it will not allow you to see ALL areas of PWA that people are accessing. Although this technique is limited to monitoring usage of any PWA web part page that is editable via the web browser, it does not require any direct server-level access to implement. Here's how it works:
- Create a somewhat hidden custom SharePoint list ('traffic log') within the PWA web site to track page visits.
- Switch one or more PWA pages into edit mode and drop some javascript into a Content Editor Web Part (CEWP); the javascript will grab the page's address whenever the page is visited and create a new entry in the 'traffic log' list.
That's it... nothing to it! Let's get started...
Create the Traffic Log
1. Visit PWA and select Site Actions > Create:
2. On the Create page, select Custom List:
3. On the New page, enter a Name for the list (such as trafficlog), select No for Display this list on the Quick Launch, then click the Create button:
4. On the trafficlog page, select Settings > List Settings:
5. On the Customize trafficlog page, in the Columns section, select the Title column:
6. On the Change Column page, change the Column name to page, then click the OK button:
7. On the Customize trafficlog page, scroll down to the Views section and click the All Items view:
8. On the Edit View page, select the Display check box next to the Created and Created By columns, then click the OK button:
Add Javascript to Pages for Tracking (Using PWA Home Page as Example)
1. Visit the desired PWA page (the PWA Home page, in this example) and select Site Actions > Edit Page:
2. With the page in edit mode, click the orange Add a Web Part bar that spans the entire width of the Header web part zone above the Reminders web part:
3. In the Add Web Parts to Header dialog, scroll down to the Miscellaneous section, select the Content Editor Web Part, then click the Add button:
4. With the CEWP on the page, open the edit menu (located in the upper right corner of the new web part) and select Modify Shared Web Part:
5. In the newly-opened Content Editor Web Part panel on the right side of the page, click the Source Editor... button:
6. In the Text Entry dialog box, paste the following javascript, then click the Save button:
//********** START COPY JAVASCRIPT BELOW THIS LINE **********
<script language="javascript">
function PostToTrafficLog()
{
var siteurl = "http://yourprojectserver/pwa";
var trafficlogname = "trafficlog";
try
{
wsURL = siteurl + "/_vti_bin/Lists.asmx";
//SOAP Action and XML
var wsSoapAction = "http://schemas.microsoft.com/sharepoint/soap/UpdateListItems";
var wsXML = '<?xml version="1.0" encoding="utf-8"?>';
wsXML += '<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ';
wsXML += 'xmlns:xsd="http://www.w3.org/2001/XMLSchema" ';
wsXML += 'xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">';
wsXML += '<soap:Body>';
wsXML += '<UpdateListItems xmlns="http://schemas.microsoft.com/sharepoint/soap/">';
wsXML += '<listName>' + trafficlogname + '</listName>';
wsXML += '<updates>';
wsXML += '<Batch OnError="Continue" ListVersion="1">';
wsXML += '<Method ID="1" Cmd="New">';
wsXML += '<Field Name="ID">New</Field>';
wsXML += '<Field Name="Title">'+ window.location.href + '</Field>';
wsXML += '</Method>';
wsXML += '</Batch>';
wsXML += '</updates>';
wsXML += '</UpdateListItems>';
wsXML += '</soap:Body>';
wsXML += '</soap:Envelope>';
//Create XML Document and get HTTP response using XMLHTTP object
var xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
var httpResponse = getServiceResults(wsURL, wsSoapAction, wsXML);
}
catch(Exception)
{
// Suppress any excess script errors to avoid user confusion.
}
}
function getServiceResults(url, soap, xml) {
//Send XML packet to web service and return HTTP response text
try {
if (xml.length > 0) {
xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
xmlHttp.open("POST", url, false);
xmlHttp.setRequestHeader("SOAPAction", soap);
xmlHttp.setRequestHeader("Content-Type", "text/xml");
xmlHttp.send(xml);
if (parseInt(xmlHttp.status) == 404) {
return 404;
}
else {
return xmlHttp.responseText;
}
}
}
catch(e) {
alert(e.message);
}
}
PostToTrafficLog();
</script>
//********** END COPY JAVASCRIPT ABOVE THIS LINE **********
NOTE: You will need to change the following two values in the javascript, located on lines 4 and 5:
var siteurl = "http://yourprojectserver/pwa";
var trafficlogname = "trafficlog";
siteurl is the URL of your PWA site, and trafficlogname is the name of the custom traffic log list that you created previously. Update the values between the quotation marks to reflect your system.
7. In the Content Editor Web Part panel, expand the Appearance section, then enter a Title for the CEWP (such as Traffic Logger) and select None for Chrome Type (you may need to scroll the panel downward) and click the OK button:
8. Select Exit Edit Mode in the upper right corner of the page:
Repeat steps 1-8 in this section for every PWA page that you would like to monitor.
View the Traffic Log
If you chose to hide the trafficlog list from the Quick Launch menu, there is no direct way to access it; here is one option:
1. Click on the Documents heading in the PWA Quick Launch menu:
2. On the All Site Content page, click View All Site Content at the top of the Quick Launch menu:
3. On the All Site Content page, click trafficlog, located below the Lists heading:
Each time someone visits a PWA with the javascript tracking code, a new entry will be added to the trafficlog list:
Pages That Will Accept the Tracking Code Easily
Not all PWA pages are editable within the web browser; here are a few pages that are:
- Home
- My Work (tabbed Tasks, Timesheet, and Schedule page)
- My Tasks
- My Timesheet (not My Timesheets!)
- Project Center
- Resource Center
- Resource Assignments
- Data Analysis
- Task Updates Approvals
- Administrative Time Approvals
- My Queued Jobs
Extra Credit
You may also notice that the Project Details page (ProjectDrillDown.aspx) is not editable out of the box; however, if you are careful and if you have direct access to the Project Server, you can make the page editable by using the information posted in Brian Smith's blog post here:
http://blogs.msdn.com/brismith/archive/2008/11/24/project-server-2007-where-did-my-web-part-go.aspx
If you DO choose to add the tracking code to the Project Details page, I would advise you to immediately remove the ability to edit the page after you are done.
Happy hacking! 10/6/2009
This issue comes up from time to time in the Project Server news group. The default structure of the Project Server 2007 reporting database attributes timesheet data to the Timehseet Creator rather than Timesheet Owner. This causes actual work to be distributed correctly in all cases where the Timesheet Owner and Timesheet Creator are one and the same, but this does not correctly support the distribution of actual work to the Timesheet Owner in the OLAP cube data for surrogate timesheets.
The following solution was provided in the Microsoft Project Server news group by frequent poster Pawel. It is also posted in our FAQ Pages. Please note that that altering the structure of the reporting database views is not supported by Microsoft, although extending the database is supported. Please take all necessary steps to recover to your starting state before attempting this solution. Make a backup of the original MSP_TimesheetActual_OlapView definition before using the following SQL to alter the structure:
Query to modify the ProjectServer_Reporting DB view (MSP_TimesheetActual_OlapView ) to provide the Timesheet Owner
USE [ProjectServer_Reporting] GO SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO ALTER VIEW [dbo].[MSP_TimesheetActual_OlapView] AS SELECT ta.TimesheetLineUID as TimesheetLineUID, ta.TimeByDay as TimeByDay, t.OwnerResourceNameUID as LastChangedResourceNameUID, --This is important ta.CreatedDate as CreatedDate, ta.TimeByDay_DayOfWeek as TimeByDay_DayOfWeek, ta.TimeByDay_DayOfMonth as TimeByDay_DayOfMonth, ta.AdjustmentUID as AdjustmentUID, ta.ActualWorkBillable as ActualWorkBillable, ta.ActualWorkNonBillable as ActualWorkNonBillable, ta.ActualOvertimeWorkBillable as ActualOvertimeWorkBillable, ta.ActualOvertimeWorkNonBillable as ActualOvertimeWorkNonBillable, ta.PlannedWork as PlannedWork, ta.Comment as Comment FROM MSP_TimesheetActual AS ta FULL OUTER JOIN dbo.MSP_TimesheetLine AS tl ON ta.TimesheetLineUID = tl.TimesheetLineUID FULL OUTER JOIN dbo.MSP_Timesheet AS t ON tl.TimesheetUID = t.TimesheetUID 8/27/2009Recently, someone in the Project Developer newsgroup (microsoft.public.project.developer) asked how to go about retrieving the names of the Project Server 2007 databases through code. Unfortunately, there’s no way to do this through the PSI – retrieving this information requires the use of Reflection. The example GetPsDbNames() method below illustrates one method of retrieving this information. It accepts the GUID of the PWA Site as a parameter and returns a Dictionary object containing the Project Server instance’s Database Names. 1: using System; 2: using System.Collections.Generic; 3: using System.Data.SqlClient; 4: using System.Reflection; 5: 6: namespace PsReflectionSample 7: { 8: public static class PsReflectionUtil 9: { 10: public static Dictionary<String, String> GetPsDbNames(Guid pwaSiteId) 11: { 12: // Return object 13: Dictionary<String, String> _psDbNames = 14: new Dictionary<String, String>(); 15: 16: // The PlatformContext object allows retrieval of the 17: // a ProjectSite object for a specific Project 18: // Server instance. 19: Type _platformContextType = Type.GetType( 20: "Microsoft.Office.Project.Server.Base.PlatformContext, " + 21: "Microsoft.Office.Project.Server, " + 22: "Version=12.0.0.0, Culture=neutral, " + 23: "PublicKeyToken=71e9bce111e9429c"); 24: 25: // The ProjectSite object stores information about the 26: // instance of Project Server, including DB Names. 27: Type _projectSiteType = Type.GetType( 28: "Microsoft.Office.Project.Server.Administration.ProjectSite, " + 29: "Microsoft.Office.Project.Server.Administration, " + 30: "Version=12.0.0.0, Culture=neutral, " + 31: "PublicKeyToken=71e9bce111e9429c"); 32: 33: // The ProjectDatabase object stores information about 34: // a Project Server database, such as Connection String. 35: Type _projectDatabaseType = Type.GetType( 36: "Microsoft.Office.Project.Server.Administration.ProjectDatabase, " + 37: "Microsoft.Office.Project.Server.Administration, " + 38: "Version=12.0.0.0, Culture=neutral, " + 39: "PublicKeyToken=71e9bce111e9429c"); 40: 41: // Retrieve information on the GetProjectSite method. 42: MethodInfo _getProjectSiteMethodInfo = 43: _platformContextType.GetMethod("GetProjectSite", 44: new Type[] { typeof(Guid) }); 45: 46: // Call GetProjectSite, which returns a ProjectSite object. 47: object _projectSiteObject = 48: _getProjectSiteMethodInfo.Invoke( 49: null, new object[] {pwaSiteId}); 50: 51: // Retrieve information about the ProjectSite object's 52: // <DbName>Database properties. 53: PropertyInfo _workingDatabaseProperty = 54: _projectSiteType.GetProperty("WorkingDatabase"); 55: PropertyInfo _publishedDatabaseProperty = 56: _projectSiteType.GetProperty("PublishedDatabase"); 57: PropertyInfo _versionsDatabaseProperty = 58: _projectSiteType.GetProperty("VersionsDatabase"); 59: PropertyInfo _reportingDatabaseProperty = 60: _projectSiteType.GetProperty("ReportingDatabase"); 61: 62: // Retrieve information about the ProjectDatabase object's 63: // DatabaseConnectionString property. 64: PropertyInfo _dbConnectionStringProperty = 65: _projectDatabaseType.GetProperty("DatabaseConnectionString"); 66: 67: // Retrieve the value of the ProjectSite object's 68: // <DbName>Database properties, which return a 69: // ProjectDatabase object. 70: object _workingDatabaseObject = 71: _workingDatabaseProperty.GetValue(_projectSiteObject, null); 72: object _publishedDatabaseObject = 73: _publishedDatabaseProperty.GetValue(_projectSiteObject, null); 74: object _versionsDatabaseObject = 75: _versionsDatabaseProperty.GetValue(_projectSiteObject, null); 76: object _reportingDatabaseObject = 77: _reportingDatabaseProperty.GetValue(_projectSiteObject, null); 78: 79: // Create a new ConnectionStringBuilder using the 80: // value of the WorkingDatabase object's 81: // DatabaseConnectionString property. 82: SqlConnectionStringBuilder _connectionStringBuilder = 83: new SqlConnectionStringBuilder( 84: _dbConnectionStringProperty.GetValue( 85: _workingDatabaseObject, null) as string); 86: 87: // Add the Working (Draft) Database Name to the Dictionary 88: _psDbNames.Add("Working", _connectionStringBuilder.InitialCatalog); 89: 90: // Recreate ConnectionStringBuilder using the 91: // value of the PublishedDatabase object's 92: // DatabaseConnectionString property. 93: _connectionStringBuilder = 94: new SqlConnectionStringBuilder( 95: _dbConnectionStringProperty.GetValue( 96: _publishedDatabaseObject, null) as string); 97: 98: // Add the Published Database Name to the Dictionary 99: _psDbNames.Add("Published", _connectionStringBuilder.InitialCatalog); 100: 101: // Recreate ConnectionStringBuilder using the 102: // value of the VersionsDatabase object's 103: // DatabaseConnectionString property. 104: _connectionStringBuilder = 105: new SqlConnectionStringBuilder 106: (_dbConnectionStringProperty.GetValue( 107: _versionsDatabaseObject, null) as string); 108: 109: // Add the Versions (Archive) Database Name to the Dictionary 110: _psDbNames.Add("Versions", _connectionStringBuilder.InitialCatalog); 111: 112: // Recreate ConnectionStringBuilder using the 113: // value of the ReportingDatabase object's 114: // DatabaseConnectionString property. 115: _connectionStringBuilder = 116: new SqlConnectionStringBuilder( 117: _dbConnectionStringProperty.GetValue( 118: _reportingDatabaseObject, null) as string); 119: 120: // Add the Reporting Database Name to the Dictionary 121: _psDbNames.Add("Reportng", _connectionStringBuilder.InitialCatalog); 122: 123: return _psDbNames; 124: } 125: } 126: }
Note that this method is somewhat slow due to the overhead incurred through the use of Reflection.
5/27/2009
Task and Time Tracking: The Challenge
One of the most challenging aspects of a Microsoft EPM implementation can be motivating project Team Members to log in to PWA on a regular basis and submit their task updates or record their time spent on various activities. For those of us who are implementers, we realize the importance of timely, consistent, and accurate updates; without this, we lose the very foundation of accurate historical tracking in Project Server. It is therefore very important that we make the Team Member's job of tracking their work as simple and effortless as possible.
One school of thought suggests that the more often the Team Member stops, thinks about the work that they have performed, and makes record of it, the easier it will be for them to track their work accurately. In contrast, if a Team Member procrastinates (we have all done this) and sits down in front of PWA once a week (or more!), it is more difficult for them to remember the details of the work that they performed. I would argue that the toughest part of time tracking isn't the process of clicking through PWA and typing the keystrokes, but rather it is the process of trying to remember what I did and how much time I spent doing it.
If I can easily remember how much time I spent working on each task for each project, however, entering those updates into PWA is a piece of cake!
Enter Twitter
I must admit that until very recently, I was a grumpy old man when it came to Twitter. I thought that it was yet another silly social networking toy and that I was not going to jump onto the bandwagon this time. However, after receiving recommendations from a couple of friends and using the tool for a couple of weeks, I am not only a believer, but I am a huge fan. For those who are not familiar, Twitter (http://www.twitter.com) is an extremely lightweight statusing or micro-blogging tool. Many people use it in many different ways, but generally, people post short messages (up to 140 characters long) that they think others may be interested in reading. Perhaps you are working on something interesting, or perhaps the woman in line behind you at Starbucks is talking too loudly on her phone, so you decide to post a short message about it.
When it comes to Twitter, its simplicity is its power.
Twitter + Mobile = Easy Tracking
Question: If you are a Team Member, when is the best time to record the work that you have performed?
Answer: While you are doing the work... or immediately afterward.
This might not be a problem if you happen to work at your desk all day and can easily open a web browser, visit the PWA web site, and update your work. However, what if you are down the hall, in a separate building, or even in a completely different country? What if you do not have easy access to the internet?
You can post Twitter updates by logging in to their web site, or if you use a mobile phone with text messaging enabled, you can post updates right from your phone. This is where things start to get interesting for us...
Mixed in with your other Twitter posts (entered conveniently on your mobile phone) in which you update your friends about that funny New York Times article and how nasty the break room coffee is today, perhaps you sneak in a couple of updates about the work that you have been doing:
- Check nyt.com! Palin did it again!
- MOSS Implementation: Install SQL 100%
- MOSS Implementation: Build app server 25%
- Once again break room coffee smells like feet
Twitter has just become your mobile notepad for tracking task and time progress, and the next time that you sit down to update the status of your work in PWA, you can simply open up your Twitter page in a web browser to review your progress. In addition, if you have told your Project Manager how to monitor (or "follow", in Twitter-speak) your updates online, they can get some quick updates about your work even before you have updated PWA.
Useful Twitter Work Tracking Conventions
If Team Members choose to use Twitter to help keep track of their work, it may be useful for them to follow a standard convention. Following are a few examples:
The 'Install SQL' task in the 'MOSS Implementation' project is 100% complete:
MOSS Implementation: Install SQL 100%
The 'Install SQL' task in the 'MOSS Implementation' project has an 'Actual Start' date of 5/18 and is 50% complete:
MOSS Implementation: Install SQL as 5/18 50%
The 'Install SQL' task in the 'MOSS Implementation' project has an 'Actual Start' date of 5/18 and an 'Actual Finish' date of 5/25:
MOSS Implementation: Install SQL as 5/18/2009 af 5/25/2009
I spent 4 hrs working on the 'Install SQL' task in the 'MOSS Implementation' project:
spent 4h on MOSS Implementation: Install SQL
This is not necessary, but if it helps them to perform more timely and accurate updates, then it is worth it. Also, this type of standardization will become extremely important if you choose to customize your Project Server implementation to automatically read Team Members' updates from the Twitter web site (Twitter has a programming API available... but one step at a time).
Taking It a Step Further: Displaying Twitter Updates in PWA
Rather than opening two web browser sessions (one for PWA and the other for Twitter) and copying information from one web page into another, there is an easy way for Team Members to display their Twitter updates within PWA. In order to enable this, you will need administrative privileges for the PWA web site, since it will involve editing a PWA page, adding a Content Editor Web Part (CEWP), and adding a bit of HTML and Javascript to the CEWP.
If you are unfamiliar with manipulating PWA pages and adding the CEWP, here are a few articles that provide detailed information and examples:
Hacking PWA with Web Parts:
http://www.projectserverhelp.com/Lists/Posts/Post.aspx?ID=7
Hacking Project Server 2007 Timesheets -- Hiding Planned Work Rows:
http://www.projectserverhelp.com/Lists/Posts/Post.aspx?ID=8
YAPH -- Disabling Start and Finish Editing on the My Tasks Page
http://www.projectserverhelp.com/Lists/Posts/Post.aspx?ID=11
To enable a Team Member to view their latest Twitter posts on the 'My Tasks' page, follow these steps:
1. Log in to the PWA web site with an account that has administrative privileges.
2. Visit the 'My Tasks' page and switch it into 'editing' mode.
3. Add the Content Editor Web Part (CEWP) to the page.
4. Copy the following code into the 'Source Editor' for the CEWP:
<br/>
<span id=twitterform>
Twitter id:<br/>
<input type="text" name="twitterid" id="twitterid"><br/>
Number of Tweets:<br/>
<input type="text" name="tweets" id="tweets"><br/>
<button OnClick="javascript:fetch();">Fetch!</button><br/>
</span>
<div id="twitter_div">
<ul id="twitter_update_list"></ul>
</div>
<script type="text/javascript" src="http://twitter.com/javascripts/blogger.js"></script>
<script type="text/javascript">
function fetch()
{
var twitterid = document.getElementById('twitterid');
var tweets = document.getElementById('tweets');
var apiurl = "http://twitter.com/statuses/user_timeline/" + twitterid.value + ".json?callback=twitterCallback2&count=" + tweets.value;
var headTag = document.getElementsByTagName('head').item(0);
var js = document.createElement('script');
js.setAttribute('language', 'javascript');
js.setAttribute('type', 'text/javascript');
js.setAttribute('src', apiurl);
headTag.appendChild(js);
}
</script>
5. Tweak the CEWP position, title, etc. as desired.
6. Switch the 'My Tasks' page out of 'editing' mode.
Any Team Member who chooses to use Twitter to track their work may now enter their Twitter user id as well as the number of updates ("tweets") they wish to fetch, and the Twitter viewer will display those updates within the PWA page for quick review:
If your organization has chosen to perform detailed time tracking via the Project Server Timesheets feature, then you can follow the same procedure to add the Twitter viewer to the 'My Timesheet' page within PWA:
In Summary
Millions of people use Twitter, and more are starting every day. Although people commonly use it as a social networking tool, it is a very simple yet powerful application that you can use to supplement business systems such as Project Server 2007. It allows people to not only easily keep track of their work while they are doing it, but it also allows those same people to track their work from anywhere using a mobile phone with text messaging or web service.
This is one very simple example of how you can use Twitter and Project Server 2007 together. If you want to take this concept several steps further, the folks at Twitter have published a programming API (Application Programming Interface) which describes how you might build a custom application which automatically fetches task or time updates and automatically feeds task status and timesheets.
Happy hacking! 5/7/2009
Although many Project Managers have grown accustomed to accepting Task Updates via PWA, some companies choose to adopt a process in which the PMs only accept those updates from within Project Professional 2007. They may have chosen to follow this process because they prefer to use the publishing engine within Project Pro. They may also prefer to accept the updates and immediately proceed to reschedule work, analyze variances, and perform other adjustments within the schedule prior to publishing.
Whatever the reason, the following process describes how to hide the 'Accept' option on the 'Task Updates' page within PWA, while still displaying the option when processing the same updates within Project Pro.
If you are not familiar with manipulating SharePoint pages with web parts, see my primer on the topic ('Hacking PWA with Web Parts') here:
http://www.projectserverhelp.com/Lists/Posts/Post.aspx?ID=7
Note: this procedure will hide the 'Accept' option for ALL Project Managers.
Hiding the 'Accept' Option on the PWA 'Task Updates' Page
1. Visit the 'Task Updates' page in PWA, pull open the 'Site Actions' menu near the upper right corner of the page, then select the 'Edit Page' option:
2. With the 'Task Updates' page in edit mode, click the 'Add a Web Part' button, which spans the entire width of the 'Main' web part zone:
3. In the 'Add Web Parts to Main' dialog box, select the checkbox next to the 'Content Editor Web Part' item, then click the 'OK' button:
4. After the 'Content Editor Web Part' has been added to the page, pull open the 'edit' menu near the upper right corner of the newly-added web part, then select the 'Modify Shared Web Part' option:
5. In the 'Content Editor Web Part' editing panel which has opened on the right side of the page, click the 'Source Editor...' button:
6. In the source editor dialog box, enter the following text, then click the 'Save' button:
<script language="JavaScript">
if ((window.location.search.match('SimpleUI')==null) && (window.location.search.match('ProjectUID')==null))
{
var menuitems = document.getElementsByTagName('ie:menuitem');
for (var i=0; i<menuitems.length; i++)
{
itm = menuitems(i)
if ((itm.id.match('Accept')!=null) && (itm.id.match('Preview')==null))
{
itm.hidden = "true";
}
}
var tbls = document.getElementsByTagName('table');
for (idx in tbls)
{
if (tbls[idx].className == 'pwa-toolbar2')
{
var cells = tbls[idx].getElementsByTagName('td');
for (var j=0; j<cells.length; j++)
{
if ((cells(j).id.match('Accept')!=null) && (cells(j).id.match('Preview')==null))
{
cells(j).style.display = "none";
}
}
}
}
}
</script>
7. In the 'Content Editor Web Part' editing panel which should still be open on the right side of the page, expand the 'Appearance' heading by clicking [+], then enter 'Hide Accept' as the 'Title' of the web part:
8. In the 'Content Editor Web Part' editing panel which should still be open on the right side of the page, select 'None' for the 'Chrome Type' of the web part, then click the 'OK' button at the bottom of the panel:
9. After the 'Content Editor Web Part' editing panel is closed, hover the mouse cursor over the header of the 'Task Update Requests' web part until the 4-way cursor is displayed, click and drag the header until it is positioned above the new 'Hide Accept' web part header (as indicated by the colored 'I-beam'), then drop the web part into the upper position in the web part zone:
The 'Hide Accept' web part will now appear below the 'Task Update Requests' web part on the page:
10. Exit 'edit mode' by clicking the 'Exit Edit Mode' link near the upper right corner of the page:
As you can now see, the 'Accept' feature no longer appears on the toolbar above the data grid (normally located between the 'Preview' and 'Reject' buttons):
Additionally, the 'Accept' feature no longer appears on the 'Actions' menu (normally located between the 'Preview' and 'Reject' options):
However, the option is still available when reviewing and accepting task updates via Project Pro (Collaborate > Update Project Progress):
In Summary...
No Server Access Required, and No Custom Coding Required
As long as you have the permission to edit PWA pages, you can perform this hack, regardless of whether you have direct access to the server. This will work in hosted environments (such as ProjectHosts) as well!
Be Careful!
Remember... this hack affects all Project Managers, so be sure that there isn't anyone who will miss the 'Accept' feature on the page.
To unhide the 'Accept' option, simply delete the 'Hide Accept' web part from the page. To learn how this is done, review my PWA web parts primer ('Hacking PWA with Web Parts') here:
http://www.projectserverhelp.com/Lists/Posts/Post.aspx?ID=7
Happy hacking! 4/2/2009
SharePoint Designer 2007, Microsoft's design and editing tool for IT professionals, can now be downloaded for free:
http://www.microsoft.com/spd
What is SharePoint Designer?
Perhaps many of you have heard of SharePoint Designer (SPD), but haven't had the opportunity to take it for a test drive. So what exactly is it?
SPD is a desktop / client tool which can be used to create, design, modify, and manage SharePoint web sites. In previous versions of SharePoint (WSS 2.0, SharePoint Portal Server 2003...), Microsoft FrontPage could be used for this function. SPD has been made more powerful, more robust, and has been rebranded to emphasize its new focus on SharePoint (although it can certainly still be used to create non-SharePoint pages).
Here are some highlights:
- Create SharePoint sites, lists, and libraries
- Manage the security within a SharePoint site
- Change the design ("look and feel") of a SharePoint web site via Master Pages and Page Layouts
- Create no-code Workflows to automate manual business processes
- Create no-code data views and data forms
Generally speaking, any administrative functions for a SharePoint site which you would normally perform through the web interface you can do with SPD... and then some.
One important aspect of SPD which makes it so powerful is the fact that you can do many things to manipulate the design of a SharePoint site or build in additional features... without having to install a copy of Visual Studio or write a single line of code. IT Professionals are now empowered to create their own SharePoint-based business applications without the need to pile more work onto a developer. Also, there is little risk of an IT professional "breaking" a SharePoint web site with SPD, since it's relatively easy to reverse changes that were made to the site.
Why Is This Important for Project Server 2007 Implementers & Administrators?
Project Web Access (PWA) and the associated Project Workspaces in Project Server 2007 are all SharePoint web sites. Although the Microsoft folks have locked us out of editing the PWA web site with SPD (unless you know the hack)...
...we can still crack open a Project Workspace with SPD and start editing:
Be Careful!
If you do decide to grab a free copy of SharePoint Designer and start kicking the tires, be sure to take the time to learn how to use it properly, and don't start by modifying sites in a production environment. There are some great online resources, including how-to videos, describing many different ways that you can use SPD to modify and manage SharePoint sites.
To Be Continued...
Check back here for future articles describing how you can use SharePoint Designer 2007 to manage, design, and add some powerful functionality to Project Workspaces, including no-code workflows. In the meantime, run out and download a copy of SPD and take it for a spin.
Good luck! 3/19/2009Unlike other methods in the PSI (e.g. ReadResources()), the ReadProject() and ReadProjectEntities() methods do not accept an xmlFilter parameter to pre-filter the data returned by the PSI. Normally, your only option would be to use a foreach or while loop. However, the DataTable class used by the PSI offers the Select() method an alternative solution. Another option is to use LINQ, which Microsoft introduced in the .NET Framework 3.5. In order to implement the LINQ code in this post, you will need a copy of Visual Studio 2008. If you are implementing this code on a server, you will need to download and install a copy of the .NET Framework 3.5. If you only have Visual Studio 2003/2005 or your organization will not deploy the .NET Framework 3.5 to your servers, you will be limited to using the DataTable.Select() sample. To start, I’ve created a test project consisting of six tasks. You will notice that two of these tasks are named Test Task. My objective is to retrieve the project from the PSI and return only these two tasks first using Select() and then using LINQ. Note that for this code I assume that you have added a Web Reference to the Project web service called ProjectPSI. Remember that this is only one way of doing things, and that this code was created for demonstration purposes. As such, it’s not really fully developed with everything it ought to have for a production environment (e.g. error checking and more intelligent filters). Here’s the complete code for the TaskFilter class: using System; using System.Collections.Generic; using System.Data; using System.Linq; using System.Net; using System.Text; namespace PSH { public class TaskFilter { public static List<string> SelectFilterTaskNames(Guid projectGuid, string fieldName, string filter) { List<string> _filteredNames = new List<string>(); // Set up PSI Reference ProjectPSI.Project _projectPsi = CreatePsiReference(); // Retrieve Project Data ProjectPSI.ProjectDataSet _projectDs = _projectPsi.ReadProject(projectGuid, ProjectPSI.DataStoreEnum.PublishedStore); // Filter Project Data DataRow[] _returnedRows = _projectDs.Task.Select(filter); // Read filtered data into return collection foreach (DataRow _row in _returnedRows) { _filteredNames.Add(_row[fieldName].ToString()); } // Remember to dispose of the PSI objects! _projectPsi.Dispose(); _projectDs.Dispose(); return _filteredNames; } public static List<string> LinqFilterTaskNames(Guid projectGuid, string fieldName, string filterValue) { List<string> _filteredNames = new List<string>(); // Set up PSI Reference ProjectPSI.Project _projectPsi = CreatePsiReference(); // Retrieve Project Data ProjectPSI.ProjectDataSet _projectDs = _projectPsi.ReadProjectEntities(projectGuid, 2, ProjectPSI.DataStoreEnum.PublishedStore); // Filter using LINQ var _query = from r in _projectDs.Task.AsEnumerable() where r.Field<string>(fieldName) == filterValue select r.Field<string>(fieldName); // Read filtered data into return collection foreach (var _row in _query) { _filteredNames.Add(_row); } // Remember to dispose of the PSI objects! _projectPsi.Dispose(); _projectDs.Dispose(); return _filteredNames; } private static ProjectPSI.Project CreatePsiReference() { ProjectPSI.Project _projPsi = new ProjectPSI.Project(); _projPsi.Credentials = CredentialCache.DefaultCredentials; _projPsi.Url = "http://epm2007demo/Litware/_vti_bin/PSI/Project.asmx"; return _projPsi; } } }
Using DataTable.Select() to Filter Task Data
The SelectFilterTaskNames() method (lines 12-36) is actually pretty simple. My implementation takes three parameters:
- a Guid named projectGuid, which represents the projectUid of the individual project you wish to retrieve data for and is used by the ReadProject() method
- a string named fieldName, which represents the ProjectDataSet.TaskRow column you wish to use for filtering and is used in several places
- a string named filter, which is used for the filterExpression parameter of the DataTable.Select() method
Now, let’s get into the code.
First, on line 14, I create a generic List of type string called _filteredNames, which I use to hold the data that this method will return to its caller. Since I’m only planning on using the methods in this class to filter and retrieve task names, this is sufficient. However, if you are going to use this code to do other things, you really should either develop a more intelligent filter parser or be prepared to write a lot of method overloads.
List<string> _filteredNames = new List<string>();
Next, on lines 16-20, I call a private method named CreatePsiReference() to create my instance of the ProjectPSI.Project() proxy class. I then retrieve the project data from the Published store by using ReadProject(). This code will also work with the ReadProjectEntities() method, as demonstrated in the LINQ sample.
// Set up PSI Reference ProjectPSI.Project _projectPsi = CreatePsiReference(); // Retrieve Project Data ProjectPSI.ProjectDataSet _projectDs = _projectPsi.ReadProject(projectGuid, ProjectPSI.DataStoreEnum.PublishedStore);
The code for the CreatePsiReference() method is pretty simple – all it does is instantiate the proxy class, set up the credentials, and set its URL. You can get into more complicated things here like impersonation and so forth, but I’m not including that in this sample. However, if you would like to see a sample that does include impersonation, please leave a comment!
private static ProjectPSI.Project CreatePsiReference() { ProjectPSI.Project _projPsi = new ProjectPSI.Project(); _projPsi.Credentials = CredentialCache.DefaultCredentials; _projPsi.Url = "http://epm2007demo/Litware/_vti_bin/PSI/Project.asmx"; return _projPsi; }
Lines 22-29 are the meat of this sample. On line 23, I take the filter parameter that’s being passed into this method and use it for the filterExpression parameter of the DataTable.Select() method. Then, the foreach loop iterates through the returned array of DataRow objects, removes the data contained in the column denoted by the fieldName parameter, and inserts it into the previously created _filteredNames generic list collection.
// Filter Project Data DataRow[] _returnedRows = _projectDs.Task.Select(filter); // Read filtered data into return collection foreach (DataRow _row in _returnedRows) { _filteredNames.Add(_row[fieldName].ToString()); }
The crucial takeaway from this sample is that the DataTable.Select() method returns an array of regular DataRow objects. If you plan to take these rows and add them into another ProjectDataSet.TaskDataTable object, you will need to cast them back into ProjectDataSet.TaskRow objects.
To end the method, I clean up my PSI objects and return the List<string> object.
// Remember to dispose of the PSI objects! _projectPsi.Dispose(); _projectDs.Dispose(); return _filteredNames;
That’s it! Pretty simple, right? Now, I’ll show you how to use LINQ to do the same thing.
Using LINQ to Filter Task Data
The LinqFilterTaskNames() method is very similar to the SelectFilterTaskNames() method that we previously examined. However, there are some very important differences. The first one comes on line 46, where I use the ReadProjectEntities() method instead of the ReadProject() method. There’s no particular reason for this other than my desire to show you the syntax of both methods. Notice the addition of the ProjectEntityType parameter, which is explained in the documentation for the ReadProjectEntities() method.
// Retrieve Project Data ProjectPSI.ProjectDataSet _projectDs = _projectPsi.ReadProjectEntities(projectGuid, 2, ProjectPSI.DataStoreEnum.PublishedStore);
The other major difference is that this time around we are going to use LINQ instead of DataTable.Select(). This change comes on lines 46-51.
// Filter using LINQ var _query = from r in _projectDs.Task.AsEnumerable() where r.Field<string>(fieldName) == filterValue select r.Field<string>(fieldName);
If you’ve never used LINQ before, you’re in for a treat – it really does make life a lot easier. Unfortunately, LINQ uses many new features of C# 3.0 and VB.NET 9.0 that make it rather complicated to explain in brief. However, Microsoft has an excellent LINQ primer on MSDN. The ADO.NET team also has an excellent introductory post on querying DataSet objects using LINQ, which is what I am doing in this sample.
Here’s the console app I used to test the application – it’s pretty straightforward, so I won’t go through it.
1: using System; 2: using System.Collections.Generic; 3: using System.IO; 4: using System.Linq; 5: using System.Text; 6: 7: namespace PSH 8: { 9: class Program 10: { 11: static void Main(string[] args) 12: { 13: 14: Guid _projectGuid = new Guid("7ACE8127-CD09-494B-B3AD-42CE776D2E72"); 15: 16: Console.WriteLine("Filtering DataSet using Select()..."); 17: Console.WriteLine(); 18: 19: List<string> _selectResults = TaskFilter.SelectFilterTaskNames(_projectGuid, "TASK_NAME", "TASK_NAME = 'Test Task'"); 20: 21: foreach (string _selectResult in _selectResults) 22: { 23: Console.WriteLine(_selectResult); 24: } 25: 26: Console.WriteLine(); 27: Console.WriteLine(); 28: Console.WriteLine(); 29: Console.WriteLine(); 30: 31: Console.WriteLine("Filtering DataSet using LINQ..."); 32: Console.WriteLine(); 33: 34: _selectResults = TaskFilter.LinqFilterTaskNames(_projectGuid, "TASK_NAME", "Test Task"); 35: 36: foreach (string _selectResult in _selectResults) 37: { 38: Console.WriteLine(_selectResult); 39: } 40: 41: Console.WriteLine(); 42: Console.WriteLine(); 43: Console.WriteLine("Press any key to continue..."); 44: Console.ReadKey(); 45: } 46: } 47: }
That’s all there is to it! Although Microsoft chose to not include filtering on these two methods, it’s a pretty simple matter for you to simply do it yourself. The techniques that I’ve demonstrated in this post can also be used in many other areas of the PSI and Project Server Development in general.
Happy coding! | Edit in Browser | /_layouts/images/icxddoc.gif | /_layouts/formserver.aspx?XsnLocation={ItemUrl}&OpenIn=Browser | 0x0 | 0x1 | FileType | xsn | 255 | | Edit in Browser | /_layouts/images/icxddoc.gif | /_layouts/formserver.aspx?XmlLocation={ItemUrl}&OpenIn=Browser | 0x0 | 0x1 | ProgId | InfoPath.Document | 255 | | Edit in Browser | /_layouts/images/icxddoc.gif | /_layouts/formserver.aspx?XmlLocation={ItemUrl}&OpenIn=Browser | 0x0 | 0x1 | ProgId | InfoPath.Document.2 | 255 | | Edit in Browser | /_layouts/images/icxddoc.gif | /_layouts/formserver.aspx?XmlLocation={ItemUrl}&OpenIn=Browser | 0x0 | 0x1 | ProgId | InfoPath.Document.3 | 255 | | Edit in Browser | /_layouts/images/icxddoc.gif | /_layouts/formserver.aspx?XmlLocation={ItemUrl}&OpenIn=Browser | 0x0 | 0x1 | ProgId | InfoPath.Document.4 | 255 | | View in Web Browser | /_layouts/images/ichtmxls.gif | /_layouts/xlviewer.aspx?listguid={ListId}&itemid={ItemId}&DefaultItemOpen=1 | 0x0 | 0x1 | FileType | xlsx | 255 | | View in Web Browser | /_layouts/images/ichtmxls.gif | /_layouts/xlviewer.aspx?listguid={ListId}&itemid={ItemId}&DefaultItemOpen=1 | 0x0 | 0x1 | FileType | xlsb | 255 | | Snapshot in Excel | /_layouts/images/ewr134.gif | /_layouts/xlviewer.aspx?listguid={ListId}&itemid={ItemId}&Snapshot=1 | 0x0 | 0x1 | FileType | xlsx | 256 | | Snapshot in Excel | /_layouts/images/ewr134.gif | /_layouts/xlviewer.aspx?listguid={ListId}&itemid={ItemId}&Snapshot=1 | 0x0 | 0x1 | FileType | xlsb | 256 |
|
|
|
|
|
|
|
|