Welcome to MSDN Blogs Sign in | Join | Help

Hi All,

By now you may have seen that Brian's responses have been non-existent since late June.  It is because he was involved in a motorcycle accident the third week in June on his way home from work.  He's recuperating well and has given me permission to update you and at least publish and update the blog.

I am not qualified technically to provide useful feedback to your inquiries but as Brian gets better I will be hopefully adding some new blob posts for him.

In the meantime please continue to support yourselves and the blog to keep it going strong.  If you wish to provide Brian with a support message, please email me at joycebileau@hotmail.com.

 Regards and Happy Developing and Troubleshooting.

 

The latest updates for this month can be found at the following links.  Great work guys!  If there are any other topics you think we are light on then please let me know, so either I can blog on them or we can get other content prepared. 

Demo Videos

Watch this: Use lag and lead time

This demo shows how to use lag and lead time to create gaps and overlaps between tasks in a project.

Watch this: Create a project

This demo shows how to create a project, set project properties, and set file properties.

Watch this: Set up a recurring task

This demo shows how to create a task that repeats on a set schedule throughout a project.

Watch this: Split a task

This demo shows how to interrupt a task, creating a gap between two portions of the task. It also shows how to move the entire split task, adjust the length of the gap created by the split, and rejoin the split portions of the task to remove the gap.

Watch this: Insert a task

This demo shows how to insert a new task between two existing tasks in a project.

Watch this: Group tasks or resources

This demo shows how to group tasks, remove the grouping, and create a new resource group using multiple criteria.

Watch this: Create a cross-project link

This demo shows how to create task dependencies across separate Project 2007 files.

Watch this: Link tasks in your project

This demo shows how to create task dependencies within a single project, and how to adjust the link type for the dependency.

TechNet Content

Best practices for managing the Microsoft Office Project Server Queue Service

This article describes best practices for managing and troubleshooting the Queue system in Office Project Server 2007.

View the ULS logs for queue job entries

This article describes how to view Microsoft Office Project Server 2007 queue job entries in the ULS logs. It can be used in conjunction with the Manage Queue page in Project Web Access Server settings to help troubleshoot queue job issues.

Configure maximum job processor threads for the Project Server Queue service

This article provides guidance about how to configure the maximum job processor threads for each queue type for the Project Server queue service.

WhitePaper: A phased approach to deploying Enterprise Project Management

This whitepaper by Chris Vandersluis provides business decision makers, network administrators, and Project Server administrators guidance about various challenges you can face when planning to deploy the Enterprise Project Management solution in your environment. It is the feature article of the From the Trenches – Deploying EPM in the Real World column available on the Project Server 2007 TechCenter.

Back up and restore the Shared Services Provider

This article gives you an overview of the task of backing up and restoring the Shared Services Provider for Project Server 2007, and includes task requirements and best practices.

Back up the Shared Services Provider

This article provides procedural steps for backing up the Shared Services Provider for Project Server 2007, and includes task requirements.

Restore the Shared Services Provider

This article provides procedural steps for restoring the Shared Services Provider for Project Server 2007, and includes task requirements.

During one of the sessions at TechEd Developer 2008 I raised this point, and as many in the audience were unaware of this behavior when updates are applied I (and the audience) though this worthy of some blog space.  The KB below relates to Project Server 2003 sp2a and later service packs – and as far as I am aware (but I am double checking…) this behavior is still the standard for Project Server 2007 too.  So if you have modified any files in your deployment you should return the original (that you saved before customizing – right?) to its rightful place before applying the update.  Once the update is applied then you will obviously need to re-apply whatever changes you needed to the new file (after making another secure copy of course).  Regardless of if we overwrite your customization or don’t update modified file the above is good practice so that you know you have the latest files in place and you have consciously reviewed and re-applied updates.  Who knows – we might even have fixed something so that you no longer need your mod!

http://support.microsoft.com/kb/906357

I’m guessing this problem may be more of an issue for us Project Server 2007 support guys at Microsoft than for most customers – but I can certainly see scenario’s where this one would bite you too, so worth an explanation.  I’ll throw in all the text from the logs etc. at the end as it makes better reading for the search indexers than for normal people and just concentrate first on what breaks and how to work around the issue.

When you are restoring a farm backup you get the option to update the accounts used for most things – then some of the other accounts you can set once you have things up and running.  However, if you have added any accounts into the “Process Account with Access to this SSP” section of the Edit Properties part of the SSP the accounts just get carried through to the new configuration database.  Unfortunately this can break some stuff – and even more unfortunate is that the first thing it breaks is the page where you would go to remove them!  Basically the page tries to validate the data it has and gives the following error:-

An unhandled exception occurred in the user interface.Exception Information: The specified account name is invalid.
Parameter name: account

The same problem occurs if an account added in Process Accounts is removed from AD – and this may well be the most common customer scenario if restoring across domains is not involved.  Verbose logging for Office Server General will also show the actual DOMAIN\User that is causing the problem.

You can also see the same error if you try to run stsadm –o enumssp.  And other SSP related jobs may post a very similar error in the Application Event Log as they fail to work.  Our workaround until now has been the usual; create a new SSP and change associations, and only today did I get to the bottom of this particular failure.  The only supportable workaround is to take a fresh backup after removing any accounts from the Process Accounts section.  The stsadm –o editssp also does not seem to give a way to remove an account that is not valid – giving an error:

A failure occurred during the processing of this command. Check diagnostic logs
for more information.

and logs the same kind of thing in the ULS logs as detailed below. 

So here is the less interesting stuff that Live and any other search engines I have forgotten can lap up…

ULS Log

=======

Verbose

Office Server

Office Server General

792o

Verbose

NTAccount 'DOMAIN\User could not be translated to a SID. Exception: System.Security.Principal.IdentityNotMappedException: Some or all identity references could not be translated. at System.Security.Principal.NTAccount.Translate(IdentityReferenceCollection sourceAccounts, Type targetType, Boolean forceSuccess) at System.Security.Principal.NTAccount.Translate(Type targetType) at Microsoft.Office.Server.Utilities.WindowsSecurity.ValidateAccount(NTAccount account, Boolean throwIfInvalid)

Exception

Office Server

Office Server General

837w

Exception

Unhandled page level exception. Path: /_admin/sspdetails.aspx, Error: The specified account name is invalid. Parameter name: account, Details: System.ArgumentException: The specified account name is invalid. Parameter name: account ---> System.Security.Principal.IdentityNotMappedException: Some or all identity references could not be translated. at System.Security.Principal.NTAccount.Translate(IdentityReferenceCollection sourceAccounts, Type targetType, Boolean forceSuccess) at System.Security.Principal.NTAccount.Translate(Type targetType) at Microsoft.Office.Server.Utilities.WindowsSecurity.ValidateAccount(NTAccount account, Boolean throwIfInvalid) --- End of inner exception stack trace --- at Microsoft.Office.Server.Utilities.WindowsSecurity.ValidateAccount(NTAccoun... Continues...

Application Event Log

==================

No specific error relating to the failed Edit Properties page - but due to the same root cause there would likely be errors like the one below, possibly every minute.

Event Type: Warning

Event Source: Office SharePoint Server

Event Category: Office Server Shared Services

Event ID: 5783

Date: 6/12/2008

Time: 10:01:52 AM

User: N/A

Computer: W2K3

Description: Synchronization for Shared Services Provider 'SSP1' has failed. The operation will be retried.

Reason: The specified account name is invalid.

Parameter name: account

Technical Support Details:

System.ArgumentException: The specified account name is invalid.

Parameter name: account ---> System.Security.Principal.IdentityNotMappedException: Some or all identity references could not be translated. at System.Security.Principal.NTAccount.Translate(IdentityReferenceCollection sourceAccounts, Type targetType, Boolean forceSuccess) at System.Security.Principal.NTAccount.Translate(Type targetType) at Microsoft.Office.Server.Utilities.WindowsSecurity.ValidateAccount(NTAccount account, Boolean throwIfInvalid)

--- End of inner exception stack trace ---

at Microsoft.Office.Server.Utilities.WindowsSecurity.ValidateAccount(NTAccount account, Boolean throwIfInvalid) at Microsoft.Office.Server.Administration.SharedAccessRule.Validate() at Microsoft.Office.Server.Administration.SharedComponentSecurity.SetAccessRule(SharedAccessRule accessRule) at Microsoft.Office.Server.Administration.SharedComponentSecurityFormatter.DeserializeFromXml(XmlReader xmlReader, SharedComponentSecurity& security) at Microsoft.Office.Server.Administration.SharedComponentSecurityFormatter.FromXmlString(String xml) at Microsoft.Office.Server.Administration.SharedResourceProvider.InternalGetAccessControl() at Microsoft.Office.Server.Administration.SharedResourceProvider.GetApplicationSecurity() at Microsoft.Office.Server.Administration.SharedResourceProvider.Microsoft.Office.Server.Administration.ISharedComponent.Synchronize() at Microsoft.Office.Server.Administration.SharedResourceProviderJob.Execute(Guid targetInstanceId)

One of the cool things Christophe and I got to do at TechEd Developer 2008 was an interview in the Fish Bowl with Richard Campbell about our “Line of Business Integration” session.  You can view the interview here, and see other great content from TechEd on the Developer Landing page and the Library page.  Enjoy!

Technorati Tags: ,

If you go to http://technet.microsoft.com/en-us/office/projectserver/default.aspx you will see a new EPM column – “From the Trenches”.  This features articles by Chris Vandersluis - president and founder of Montreal, Canada–based HMS Software, a Microsoft Gold Certified Partner.  Thanks Chris for this great content – and If there are other partners out there who want to share experience with the wider community then please send me e-mail by clicking the EMAIL link at the top of the page.

Technorati Tags:

I really enjoyed TechEd last week – and thanks to all who attended the sessions and came and said “Hi”.  I’m back in the office this week doing proper work – but Christophe is back down in the sunshine of Florida.

This week Christophe will be joined by Simon Floyd, Michael Jordan and Emmanuel Fadullon at TechEd IT Professionals in Orlando and they will be delivering the following EPM sessions:

OFC255

Best Practices: Enabling Innovation Process Management Using Microsoft Office Enterprise Project Management Solution and Microsoft Office SharePoint Server 2007

Tuesday, June 10 10:30 AM

In today’s fast-paced global economy, innovation is the key to the next big breakthrough in products, services, and processes. However, businesses are often challenged with facilitating innovation due to cultural, strategic, or logistical pitfalls. This session covers the best practices for fostering a culture of innovation by enabling an organization’s most valuable assets, its people, to actively and easily participate in the innovation process. A live demonstration shows how anyone can capture, investigate, formulate, and evaluate ideas to conclusion using the Microsoft Office Enterprise Project Management solution and SharePoint Server 2007.

OFC358

Deploying Microsoft Office Enterprise Project Management Solution 2007 into an Existing Microsoft Office SharePoint Server Environment

Tuesday, June 10 1:15 PM

Microsoft Office Enterprise Project Management Solution 2007 (EPM 2007) is a very unique form of Windows SharePoint Services V3. This sessions provides you with best practices, learned in Performance Labs in Redmond, to keep in mind when you want to deploy EPM 2007 into an existing MOSS farm.

OFC450

Microsoft Office Project Server 2007 Deployment for High Availability and Scalability

Wednesday, June 11 1:00 PM

This session covers the components of the EPM solution and the main considerations when planning for deployments that require high availability, when to scale up and out, points of failure and software/hardware boundaries.

OFC53-TLC

Microsoft Office Project Server 2007 Disaster Recovery

Thursday, June 12 8:30 AM

Learn how to recover from a catastrophic failure on your SharePoint/EPM farm. This session discusses the options to recover from a loss of any component of your SharePoint/EPM Farm: SQL server, application server, disk, etc. It covers SharePoint/EPM Farm or database restore options including pros and cons as well as a live demo. This session does not cover detailed backup/restore plans and all high availability options.

Beside attending all these Project Server sessions, you can also meet them at the Project Server booth, don’t be shy!

This is the last TechEd sample and is a very simple example of a PSI call from Project Pro (or Standard for that matter).  It shows how a user of the client not connected to the server can still make PSI calls.  Obviously they need an account in the PWA they are communicating with.  The sample shows a PSI call to check that the local project name (or any text you want to enter manually) is in use on the Project Server.  The response is shown on the form for the add-in and also put into the text1 field of the project – just to show how local data can also be updated with VSTO applications. 

The details for creating a VSTO add-in for project were very well covered in Jack Dahlgren's posting http://zo-d.com/blog/archives/programming/making-the-move-from-vba-to-vsto-in-microsoft-project.html posting recently – thanks Jack – so I will just give samples of my code and not repeat the “how to” section.  I also needed web references to the Project and LoginWindows web services.  My code is also in C# so some differences to Jack’s posting. 

The ThisAddIn.cs file looks like this and is adding a toolbar button and setting the for to show when the button is clicked:-

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using MSProject = Microsoft.Office.Interop.MSProject;
using Office = Microsoft.Office.Core;
using System.Windows.Forms;

namespace TechEdComAddIn
{
    public partial class ThisAddIn
    {
        private Office.CommandBar commandBar;
        private Office.CommandBarButton importButton;

        private void ThisAddIn_Startup(object sender, System.EventArgs e)
        {
            
            if (commandBar == null)
            {
                int barPosition = 1;
                bool isMenuBar = false;
                bool isTemporary = true;
                commandBar = Application.CommandBars.Add("ValidateBar", barPosition,
                                                         isMenuBar, isTemporary);
            }
            
            try
            {
                importButton = (Office.CommandBarButton)commandBar.Controls.Add(
                    Office.MsoControlType.msoControlButton, missing, missing, missing, missing);
                importButton.Style = Office.MsoButtonStyle.msoButtonCaption;
                importButton.Caption = "Validate Project Name";
                importButton.Tag = "Validate Project Name";
                importButton.TooltipText = "Validates a name for later use in Project Server.";
                importButton.Click +=
                    new Office._CommandBarButtonEvents_ClickEventHandler(ImportButtonClick);

                commandBar.Visible = true;
            }
            catch (ArgumentException ex)
            {
                MessageBox.Show(ex.Message, "Error adding toolbar button",
                    MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
            
        }

        private void ThisAddIn_Shutdown(object sender, System.EventArgs e)
        {
        }

        private void ImportButtonClick(Office.CommandBarButton ctrl, ref bool cancel)
        {
            //ImportDialogBox importDialog = new ImportDialogBox();
            //importDialog.Show();
            
            ProjNameValidate myForm = new ProjNameValidate(); ;
            myForm.Show();
        }
        
       
        #region VSTO generated code

        /// <summary>
        /// Required method for Designer support - do not modify
        /// the contents of this method with the code editor.
        /// </summary>
        private void InternalStartup()
        {
            this.Startup += new System.EventHandler(ThisAddIn_Startup);
            this.Shutdown += new System.EventHandler(ThisAddIn_Shutdown);
        }
        
        #endregion
    }
}
 
The form looks like this:-
 
image 
And the code behind:-
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Net;
using System.Text;
using System.Windows.Forms;

namespace TechEdComAddIn
{
    public partial class ProjNameValidate : Form
    {
        private const string URLPREFIX = "http://";
        private const string LOGINWINDOWSWEBSERVICE = "_vti_bin/PSI/LoginWindows.asmx";
        private const string PROJECTWEBSERVICE = "_vti_bin/PSI/Project.asmx";

        private string baseUrl = "http://brismithwfe/cal/";


  
        private static WebSvcLoginWindows.LoginWindows loginWindows =
            new WebSvcLoginWindows.LoginWindows();
        private static WebSvcProject.Project project =
            new WebSvcProject.Project();

        public ProjNameValidate()
        {
            InitializeComponent();
            string projName = Globals.ThisAddIn.Application.ActiveProject.Name.ToString();
            int extLocation = projName.LastIndexOf(".mpp");
            textBoxProjectName.Text = projName.Substring(0, extLocation);
        }

        private void btnValidateName_Click(object sender, EventArgs e)
        {
            try
            {
                loginWindows.Url = baseUrl + LOGINWINDOWSWEBSERVICE;
                loginWindows.Credentials = CredentialCache.DefaultCredentials;
                project.Url = baseUrl + PROJECTWEBSERVICE;
                project.Credentials = CredentialCache.DefaultCredentials;

               
                WebSvcProject.ProjectDataSet dsProject =
                        new WebSvcProject.ProjectDataSet();
                dsProject = project.ReadProjectList();
                foreach (WebSvcProject.ProjectDataSet.ProjectRow rowProject in dsProject.Project)
                    if (rowProject.PROJ_NAME == textBoxProjectName.Text.ToString())
                    {
                        lblResponse.Text = "Project name already in use";
                        lblResponse.Visible = true;
                        break;
                    }
                    else
                    {
                        lblResponse.Text = "Project name is OK to use.";
                    }
                Globals.ThisAddIn.Application.ActiveProject.Text1 = lblResponse.Text;
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }

        }
    }
}
I don’t think there is anything too complicated.  I trim the .mpp off the local filename and then see if it exists on the server.  Globals is the way to interact with the local application object.
Once you have that available most of the VBA developers out there should be on home territory.  Take a look at the VSTO stuff – it is very simple to deploy too – as Jack’s article outlines.
Technorati Tags: , ,
 
 
 
 
 
 
 

First day of TechEd today and despite the 3hr time difference from Seattle I still woke up early.  This posting will be part of the subject of the chalk talk I am delivering with Boris Scholl tomorrow (6/4) morning.  If any of you remember the Solution Accelerator for Six Sigma this is part inspired by some custom code that was in that solution – and is designed to allow a Project Manager to select a workspace type from a list in a custom project field – and then when she publishes that type of site will be provisioned from a pre-built template.

The first part of this is to create some templates for new workspaces.  This is covered in the SDK, but briefly you must base any new template on an unlinked site, then you save as a template, then from the template gallery save to a file (*.stp) and then you use stsadm –o addtemplate to bring the template back in so it can be used for provisioning (and a final IIS reset).  You will also need to use the command stsadm –o enumtemplates to get the internal name (something like _GLOBAL_#2) for each of your templates for later use.

The next step is creating a lookup table to hold the template details – and it should look something like this:-

image

 

I am using the description field to hold the internal name of the template as that is the one used when requesting a site to be provisioned.  You could resolve this in code somewhere – it just seemed easier to me to put it here. You also need to create a Project Level custom field that references this lookup table – and in my example I use the “NotYet” value to mean don’t create a site for me.  The names of the field/table aren’t important – but you will need to know the Guid associated with them.  Also you will need to turn off automatic site creation – as we will be controlling this via an event handler.  I am using Red/Green/Blue for the visual recognition for the demo – obviously ISO 9001, Six Sigma, Administration m- would be more useful types of templates to use.

Now the event handler.  I’ve thrown a few comments in and also it logs to the application event log the different actions it can take.  It could certainly do with more (read some) exception handling… but it does the job.  Based on the custom field requested it will get the template name from the lookup table description.  If a site already exists it will stop – and if the requested site is already in use it will stop.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Net;
using Microsoft.Office.Project.Server.Events;
using PSLibrary = Microsoft.Office.Project.Server.Library;


namespace TechEdEventHandler
{
    public class MyPublishedEventHandler : ProjectEventReceiver
    {
        private const string URLPREFIX = "http://";
        private const string LOGINWINDOWSWEBSERVICE = "_vti_bin/PSI/LoginWindows.asmx";
        private const string PROJECTWEBSERVICE = "_vti_bin/PSI/Project.asmx";
        private const string WSSINTEROPWEBSERVICE = "_vti_bin/PSI/WSSInterop.asmx";
        private const string LOOKUPTABLEWEBSERVICE = "_vti_bin/PSI/LookupTable.asmx";
        private string baseUrl = "http://brismithwfe/cal/";
        private const int EVENTID = 9191;
        
        private string webTemplateName;
        private Guid projectGuid;
        private Guid codeValue;
        const int PROJECT_CUSTOM_FIELDS_ENTITY_TYPE = 32;
        // We need the Guids for the lookup table and custom field - better practice would be to use a settings file
        private Guid workspaceTemplateLUTGuid = new Guid("b0fc9d01-b716-43d3-aa74-335a3297214b");
        private Guid workspaceTemplateCFGuid = new Guid("98c9444e-1fa2-4f12-ab2b-4cb7e1818738");

        private static WebSvcLoginWindows.LoginWindows loginWindows =
            new WebSvcLoginWindows.LoginWindows();
        private static WebSvcProject.Project project =
            new WebSvcProject.Project();
        private static WebSvcWssInterop.WssInterop wssInterop =
            new WebSvcWssInterop.WssInterop();
        private static WebSvcLookupTable.LookupTable lookupTable =
            new WebSvcLookupTable.LookupTable();

        public override void OnPublished(Microsoft.Office.Project.Server.Library.PSContextInfo contextInfo, ProjectPostPublishEventArgs e)
        {
            string eventName = "Auto Workspace Creation - OnPublished";
            
            loginWindows.Url = baseUrl + LOGINWINDOWSWEBSERVICE;
            loginWindows.Credentials = CredentialCache.DefaultCredentials;
            project.Url = baseUrl + PROJECTWEBSERVICE;
            project.Credentials = CredentialCache.DefaultCredentials;
            lookupTable.Url = baseUrl + LOOKUPTABLEWEBSERVICE;
            lookupTable.Credentials = CredentialCache.DefaultCredentials;
            wssInterop.Url = baseUrl + WSSINTEROPWEBSERVICE;
            wssInterop.Credentials = CredentialCache.DefaultCredentials;

            projectGuid = e.ProjectGuid;

            WebSvcWssInterop.WssServersDataSet dsWssServersDataSet = 
                new WebSvcWssInterop.WssServersDataSet();

            WebSvcWssInterop.WssSettingsDataSet dsWssSettingsDataSet =
                new WebSvcWssInterop.WssSettingsDataSet();

            WebSvcWssInterop.ProjectWSSInfoDataSet dsProjectWSSInfoDataSet =
                new WebSvcWssInterop.ProjectWSSInfoDataSet();

            // If rowcount is greater than zero then a site already exists for the project - so exit
            dsProjectWSSInfoDataSet = wssInterop.ReadWssData(projectGuid);
            int rowCount = dsProjectWSSInfoDataSet.ProjWssInfo.Rows.Count;
            if (rowCount == 0)
            {

                WebSvcProject.ProjectDataSet dsProject =
                    new WebSvcProject.ProjectDataSet();
                // We are getting the project dataset - but only for the entity type 32 - custom fields
                dsProject = project.ReadProjectEntities(projectGuid, PROJECT_CUSTOM_FIELDS_ENTITY_TYPE, WebSvcProject.DataStoreEnum.WorkingStore);
                // The we look for the workspace template field - and get the code value
                foreach (WebSvcProject.ProjectDataSet.ProjectCustomFieldsRow rowProjectCF in dsProject.ProjectCustomFields)
                    if (rowProjectCF.MD_PROP_UID == workspaceTemplateCFGuid)
                    {
                        codeValue = rowProjectCF.CODE_VALUE;
                    }
                // Using the Guid for the lookup table we search for the code value 
                Guid[] ltUidList = new Guid[] { workspaceTemplateLUTGuid };
                WebSvcLookupTable.LookupTableDataSet dsLookupTable =
                    new WebSvcLookupTable.LookupTableDataSet();
                dsLookupTable = lookupTable.ReadLookupTablesByUids(ltUidList, false, 1033);
                // the code value gets us to the description which we use as the webtemplatename
                foreach (WebSvcLookupTable.LookupTableDataSet.LookupTableTreesRow rowLookupTable in dsLookupTable.LookupTableTrees)
                    if (rowLookupTable.LT_STRUCT_UID == codeValue)
                    {
                        webTemplateName = rowLookupTable.LT_VALUE_DESC;
                    }
                dsWssServersDataSet = wssInterop.ReadWssServerInfo();
                dsWssSettingsDataSet = wssInterop.ReadWssSettings();

                Guid wssServerUid = dsWssSettingsDataSet.WssAdmin[0].WADMIN_CURRENT_STS_SERVER_UID;
                string wssWebFullUrl = dsWssServersDataSet.WssServers.FindByWSTS_SERVER_UID(wssServerUid).WSS_SERVER_URL
                    + "/" + dsWssSettingsDataSet.WssAdmin[0].WADMIN_DEFAULT_SITE_COLLECTION
                    + "/" + e.ProjectName.ToString();

                int webTemplateLcid = 1033;
                // If site exists then we cannot create it for this project...
                bool SiteExists = wssInterop.WSSWebExists(wssWebFullUrl);
                if (!SiteExists)
                {
                    //NotYet means we will create it later
                    if (webTemplateName != "NotYet")
                    {   
                        // This is where we create a site if we need one
                        wssInterop.CreateWssSite(projectGuid, wssServerUid, wssWebFullUrl, webTemplateLcid, webTemplateName);
                        
                        string msg = string.Format("{0}: Site {1} created for Project {2}", eventName, wssWebFullUrl, e.ProjectName);
                        WriteEvent(msg, EventLogEntryType.SuccessAudit , EVENTID);
                    }

                    else
                    {
                        string msg = string.Format("{0}: No site required at this time for project {1}", eventName, e.ProjectName);
                        WriteEvent(msg, EventLogEntryType.Information, EVENTID);
                    }


                }
                else
                {
                    string msg = string.Format("{0}: Site {1) already exists", eventName, wssWebFullUrl);
                    WriteEvent(msg, EventLogEntryType.Error, EVENTID);
                }

            }
            else
            {
                string msg = string.Format("{0}: Workspace already created", eventName);
                WriteEvent(msg, EventLogEntryType.Information, EVENTID);
            }
            
        }
        
    private void WriteEvent(string msg, EventLogEntryType logEntryType, int eventId)
        {
            EventLog myLog = new EventLog();
            myLog.Source = "Project Event Handler";

            string message = msg;
            myLog.WriteEntry(msg, logEntryType, eventId);
        }
    }
}

Enjoy!

Boris Scholl, Christophe Fiessinger and I will be attending TechEd Developers 2008 in Orlando next week.

We’ll be delivering the following sessions:

Customizing the Microsoft Office Project Web Access User Interface

Line-of-Business Integration Using Microsoft Office SharePoint Server 2007 and Microsoft Office Project Server 2007

Code Samples

Project Initiation Using Microsoft Office InfoPath and Windows Workflow Foundation

Code Samples

We’ll be around all week attending the Blogger Connect (next to TechEd online / Fishbowl areas) as well as the Office Developer booths.

Don’t be shy, come and meet us we want to hear your Project Server feedback from a developer perspective!

There may soon be pictures of Christophe and I showing up on the blog list for Project at http://technet.microsoft.com/en-us/office/projectserver/cc511254.aspx –  you have been warned!

In yesterday’s posting the BDC was used to present data from both Project Server and the AdventureWorks database with the join being the Vendor ID from the Purchasing.Vendors table.  To allow this to happen we need a custom field and lookup table in Project Server that has the values for the vendors so that we can make the association.  One way to do this (and there are many) is to use a CLR trigger – and that is another part of Christophe and my presentation next Thursday at TechEd.

SQL Server 2005 introduced the capability to use Common Language Runtime procedures within database entities.  CLR integration means that you can now write stored procedures, triggers, user-defined types, user-defined functions (scalar and table-valued), and user-defined aggregate functions using any .NET Framework language, including Microsoft Visual Basic .NET and Microsoft Visual C#.  This example uses C# within a trigger to make PSI calls and insert values to a Project Server lookup table.  As with any programming task, just because you can do something a particular way does not necessarily mean you should, and you certainly wouldn’t want to use the code here to make very frequent updates between systems – but for occasional updates – such as adding a new vendor in a small application it may fit the bill.

There are a number of hoops to jump through to get CLR integration running with SQL Server – and as it does open up some security concerns you will need to fully understand the consequences of these changes before making them.  They are full described, and some alternative options presented, in the SQL Server Books Online starting with the Introduction linked above and specifically dealing with security considerations here.  Please read these articles – then the following steps will make more sense.

The first step is to set the database instance to CLR enabled – and this uses sp_configure:-

sp_configure 'show advanced options', 1;

GO

RECONFIGURE;

GO

sp_configure 'clr enabled', 1;

GO

RECONFIGURE;

GO

Next is to give the dbo UNSAFE ASSEMBLY permissions:-

use master

GRANT UNSAFE ASSEMBLY To "domain\user"

go

and then set the database property TRUSTWORTHY to true

ALTER DATABASE AdventureWorks SET TRUSTWORTHY ON

an alternative approach here is to sign the assembly with an asymmetric key or cert that has a LOGIN with UNSAFE ASSEMBLY permission.

When using calls to Web Services, as we will be with the PSI, you also need to deploy the serialization assembly – so this involves setting the option to Generate serialization assembly and then configuring PostDeployScript.sql and PreDeployScript.sql to make the deployment.

 

clip_image002

 

PreDeployScript.sql

IF EXISTS (SELECT [name] FROM sys.assemblies WHERE [name] = N'AWTrigger.XmlSerializers') 
    DROP ASSEMBLY [AWTrigger.XmlSerializers]

PostDeployScript.sql

CREATE ASSEMBLY [AWTrigger.XmlSerializers] from
'C:\TechEd\LOB\Brian\AWTrigger\bin\Debug\AWTrigger.XmlSerializers.dll'
WITH PERMISSION_SET = SAFE

Finally piece of setup in this example is that I created a new table in AdventureWorks and then used just a normal SQL trigger to copy data from the Vendor table to my table.  This was a workaround for what I later found was a bug with VS 2008 in that it cannot deploy automatically to entities in schema other than dbo – so the Purchasing.Vendor table would take the trigger directly.  I could have manually deployed to work around this – but as I already had the workaround working I haven’t done this.  For completeness this is the SQL to create my extra table called AWTrigger, and is followed by the SQL to add the SQL trigger to the Purchasing.Vendor table.

USE [AdventureWorks]
GO
/****** Object:  Table [dbo].[AWTrigger]    Script Date: 05/20/2008 12:36:08 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
SET ANSI_PADDING ON
GO
CREATE TABLE [dbo].[AWTrigger](
    [VendorID] [int] NULL,
    [VendorName] [varchar](50) NULL
) ON [PRIMARY]

GO
SET ANSI_PADDING ON

USE [AdventureWorks]
GO
/****** Object:  Trigger [Purchasing].[copyToAWTrigger]    Script Date: 05/20/2008 12:36:54 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author:        <Author,,Name>
-- Create date: <Create Date,,>
-- Description:    <Description,,>
-- =============================================
CREATE TRIGGER [Purchasing].[copyToAWTrigger]
   ON  [Purchasing].[Vendor]
   AFTER INSERT
AS
BEGIN
    -- SET NOCOUNT ON added to prevent extra result sets from
    -- interfering with SELECT statements.
    SET NOCOUNT ON;

    -- Insert statements for trigger here
    declare @vendorid int;
    declare @vendorname varchar(50);

    select @vendorid=i.VendorID from inserted i;
    select @vendorname=i.Name from inserted i;

    insert into AWTrigger (VendorID, VendorName) values(@vendorid, @vendorname)

END

Now to the real code!  In VS 2008 you can create a project of type SQL-CLR

image

and you will set up a database connection.  In this solution I also have web references added for the LoginWindows and LookupTable web services.

using System;
using System.Data;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Net;
using System.Security.Principal;
using Microsoft.SqlServer.Server;

public partial class Triggers
{
    // Enter existing table or view for the target and uncomment the attribute line
    // Using another table rather than messing with the Vendor table to work around known bug with triggers in other schema
    // Could have deployed trigger manually.
    [Microsoft.SqlServer.Server.SqlTrigger(Name = "LUTTrigger", Target = "AWTrigger", Event = "FOR INSERT")]
    public static void LUTTrigger()
    {
        #region Setup

        const string PSI_URI = "http://W2K3/pwa/_vti_bin/psi/"; // <<--Change to match your project server and directory.
        const string LOBI_NAME = "AW - LUTTrigger";

        WindowsIdentity winID = SqlContext.WindowsIdentity;

        // Set up the services.
        AWTrigger.wsLoginWindows.LoginWindows loginWindows = new AWTrigger.wsLoginWindows.LoginWindows();
        AWTrigger.wsLookupTable.LookupTable lookupTable = new AWTrigger.wsLookupTable.LookupTable();
        loginWindows.Url = PSI_URI + "loginWindows.asmx";
        lookupTable.Url = PSI_URI + "lookuptable.asmx";
       
        loginWindows.UseDefaultCredentials = true;
        lookupTable.UseDefaultCredentials = true;

        CookieContainer cookies = new CookieContainer();
        loginWindows.CookieContainer = cookies;

        AWTrigger.wsLookupTable.LookupTableDataSet dsLookupTable;

        // Guid for AW-Vendor Lookup Table set and then put into array
        Guid lutGuid = new Guid("E6342609-6A5D-4D05-94D1-519D0901F566"); 
 
// You will need to change the UID used here – and good practice would be to get this from a .config file.
        
        Guid[] lutList = new Guid[] { lutGuid };
        bool autoCheckOut = false;
        string msg = string.Empty;

        #endregion

        try
        {
            // Attempt to log in using Windows Credentials
            if (!loginWindows.Login())
            {
                msg = string.Format("{0}: User {1} CANNOT login using Windows Credentials: {2}"
                    , LOBI_NAME, winID.Name, loginWindows.Url);
                WriteEvent(msg, EventLogEntryType.FailureAudit, 8989);
                return;
            }

            // Get the VendorID and Name from the inserted row
            int vendorID;
            string vendorName;
            SqlCommand command;
            SqlTriggerContext triggContext = SqlContext.TriggerContext;
            SqlPipe pipe = SqlContext.Pipe;
            SqlDataReader reader;

            using (SqlConnection connection
                = new SqlConnection(@"context connection=true"))
            {
                connection.Open();
                command = new SqlCommand(@"SELECT * FROM INSERTED;", connection);
                reader = command.ExecuteReader();
                reader.Read();
                vendorID = (int)reader[0];
                vendorName = (string)reader[1];
                reader.Close();
            }
            // Get a dataset for our vendor lookup table and add out row – then update
            dsLookupTable = lookupTable.ReadLookupTablesByUids(lutList, autoCheckOut, 1033);

            AWTrigger.wsLookupTable.LookupTableDataSet.LookupTableTreesRow rowLookupTableTree =
                dsLookupTable.LookupTableTrees.NewLookupTableTreesRow();

            Guid lt_struct_uid = Guid.NewGuid();
            rowLookupTableTree.LT_STRUCT_UID = lt_struct_uid;
            rowLookupTableTree.LT_UID = lutGuid;
            rowLookupTableTree.LT_VALUE_NUM = (decimal)vendorID;
            rowLookupTableTree.LT_VALUE_DESC = vendorName;
            dsLookupTable.LookupTableTrees.AddLookupTableTreesRow(rowLookupTableTree);

            lookupTable.CheckOutLookupTables(lutList);
            bool validateOnly = false;
            lookupTable.UpdateLookupTables(dsLookupTable, validateOnly, autoCheckOut, 1033);
            bool forceCheckIn = false;
            lookupTable.CheckInLookupTables(lutList, forceCheckIn);
        }
        catch(Exception ex)
        {
            msg = string.Format("{0}: EXCEPTION {1}", LOBI_NAME, ex.Message);
            WriteEvent(msg, EventLogEntryType.Error, 8989);
        }
    }

    #region Helper Methods

    private static void WriteEvent(string msg, EventLogEntryType logEntryType, int eventId)
    {
        EventLog myLog = new EventLog();
        myLog.Source = "Adventure Works LOBI";

        string message = msg;
        myLog.WriteEntry(msg, logEntryType, eventId);
    }

    #endregion

}

The code isn’t totally robust, and will fail if the lookup table is checked out – and the code that Christophe posted earlier this week on controlling lookup table updates helps with that.  It controls who can check out that particular lookup table.  In triggers it is also really important to handle exceptions as you always want the trigger to “work” even when it fails.  Try/Catch and writing errors out to the event log in this case ensures the trigger works and doesn’t undo any of the changes to the original vendor table.

The test.sql script is used for debug – by actually making an insert that will fire the trigger – thanks Christophe for adding this piece and also the better exception and error handling. 

DECLARE @AcctNumber nvarchar(10)
SET @AcctNumber=ROUND(RAND()*1000,0)

INSERT INTO Purchasing.Vendor (AccountNumber, Name, CreditRating, PreferredVendorStatus, ActiveFlag, ModifiedDate)
       VALUES (@AcctNumber, 'Vendor '+@AcctNumber, 1, 1, 1, GETDATE())

As I was working on this I made good use of Live Search – so thanks to Vineet on the SQL Server team for the very helpful blog posting as well as excellent content from the following KB and TechNet articles.

http://support.microsoft.com/kb/913668

CLR Triggers

http://technet.microsoft.com/en-us/library/ms131093.aspx

Building Database Objects with Common Language Runtime (CLR) Integration

http://technet.microsoft.com/en-us/library/ms131046.aspx

Getting Started with CLR Integration

http://technet.microsoft.com/en-us/library/ms131052.aspx

Enabling CLR Integration

http://technet.microsoft.com/en-us/library/ms131048.aspx

Part 3 will probably be posted early next week – and will include a server side event handler that uses the OnPublished event to query a project custom field for the type of Project Workspace you would like created for that project.  This will be part of my chalk talk session with Boris Scholl on Wednesday morning on customizing the UI in Project Server 2007.

This is the first of my pre-TechEd 2008 postings – and you may already have seen one that Christophe posted a few days ago.  This one is part of the same session I am doing with Christophe next Thursday afternoon (not Tuesday as I said earlier) in Orlando and the scenario here is accessing both AdventureWorks data (from the sample database) and Project Server 2007 data using the Business Data Catalog (BDC). 

To connect the two sets of data I am using a Project level custom field to hold the Vendor ID, which is the same code used in AdventureWorks to identify vendors.  More on this link in my next post where I have a database CLR Trigger in C# that updates Project Server with new vendors – and Christophe’s code mentioned above stops “unauthorized” users making changes to the specific lookup table.

A picture is certainly worth a thousand or more words – so here is a sample of the kind of this this scenario delivers.  This is a web part page with a Business Data List and a Business Data Item part added.  Selecting from the list allows display of more details of the specific project and associated vendor – as well as a few actions.

 image

We can see details of the actual work and cost (from Project Server) and the vendor name, city and location (from AdventureWorks).  The actions on the top web part allow us to search for the Project Owner on Live Search, Drill Down to Project on PWA, and finally to Map the Vendor Location using Live Maps – and here is what you get using that last option…

image

As you can see from the URL it is using data from the BDC as parameters in the search – the other actions use a similar process.  In this example as the AdventureWorks street addresses are fictitious I just use the zip code and city.

I have attached the XML of the definition file, and you will need to make a few edits to fit your server names and you will also need to have a Project level Number custom field to hold the VendorID.  I am pulling data from the reporting database views and cross joined to the AdventureWorks database as you will see from the XML.

When I started working with the BDC I found it easiest to look at the XML files from the AdventureWorks sample first, then also took a look at Christophe’s search example for project data and once I understood how it all worked it made it easier to work with the Catalog Definition Editor.  The cool thong about the definition editor is that it allows you to execute the “finders” to ensure everything is working as planned.

image

and even the actions…

image

In the next week or two I aim to post a video walking through creation of some of this – but I am sure with the XML and looking at the other samples already out there you should soon be able to get this kind of thing working.  As a final couple of ideas for you I also show a couple of list to which I have added a new column containing “Business Data” and then defined which elements I want to display.  This then adds data from Project and AdventureWorks to list data!

First is a calendar list item

image

where my project meeting item has the actions available

image

including profile information

image

The same kind of thing can be surfaced in more regular custom lists…

image

and again all the actions are available for each project.

One thing to remember is that in this example I am just pulling data based on the account running the web application so you will want to consider security in more depth than I have.  Using a web service instead of SQL as the datasource for the BDC may give you more control in this respect – although the standard PSI web services cannot be consumed directly.  Finally the BDC is just surfacing the data – it isn’t replicated in any way and is not stored in the content database.

I was working on a case today where a bug in one place makes something break elsewhere. And the added challenge here is the original problem probably goes un-noticed so isn’t obviously a problem.

If you have ever tried to import a timesheet on the My Tasks page, and the screen flashed but nothing happened – and the only way out was to press Cancel then you may have this problem.  The timesheet does not restrict the tasks that you can add in the same way they show in My Tasks.  Either through Add Lines, or using the automatic method from Server Settings, Timesheet Settings and Defaults page (By default, timesheets will be created by using:  Current task assignment) you could end up with lines on your timesheet where you are not the assignment owner.  It is this that breaks the import.  You will also notice that these tasks do not appear on your My Tasks page.

In many cases I’m guessing that you do expect to be the assignment owner, and some earlier (now fixed) bugs may have led to projects and templates having the incorrect assignment owner for your assignments.  The workaround is therefore to either correct the assignment owner and republish – or remove the tasks from the timesheet if the assignment owner is valid (and not you).

For those searching the following are the errors at various levels that you would find in the ULS logs as a result of this problem.  Not great reading – but the search engines appreciate it.

Exception Level Log
Exception occurred in method Statusing.ImportTimesheet System.ArgumentException: Statusing - Cannot create new Saved Task for resource's task modifications.  Parameter name: taskData     at Microsoft.Office.Project.Server.BusinessLayer.Statusing.CreateSavedTask(PlatformContext context, DalDataAccess dataaccess, ISvrDocEdit doc, ITaskData taskData)     at Microsoft.Office.Project.Server.BusinessLayer.Statusing.ImportTimesheet(Guid periodUID)     at Microsoft.Office.Project.Server.WebService.Statusing.ImportTimesheet(Guid periodUID)

Verbose Level Log
Error is: GeneralUnhandledException. Details: Attributes:  System.ArgumentException: Statusing - Cannot create new Saved Task for resource's task modifications.  Parameter name: taskData     at Microsoft.Office.Project.Server.BusinessLayer.Statusing.CreateSavedTask(PlatformContext context, DalDataAccess dataaccess, ISvrDocEdit doc, ITaskData taskData)     at Microsoft.Office.Project.Server.BusinessLayer.Statusing.ImportTimesheet(Guid periodUID)     at Microsoft.Office.Project.Server.WebService.Statusing.ImportTimesheet(Guid periodUID)  . Standard Information: PSI Entry Point: Statusing.ImportTimesheet  Project User: REDMOND\pkmuser2  Correlation Id: f5af0b6e-a9be-4b88-88fa-3529021a1696  PWA Site URL: http://servername/pwa  SSP Name: SharedServices1  PSError: GeneralUnhandledException (42)

Medium Level Log
PWA:http://servername/pwa, SSP:SharedServices1, User:DOMAIN\User, PSI: Statusing.ImportTimesheet  Undefined Attributes: PSError: GeneralUnhandledException  Undefined attributes list: System.ArgumentException: Statusing - Cannot create new Saved Task for resource's task modifications.  Parameter name: taskData     at Microsoft.Office.Project.Server.BusinessLayer.Statusing.CreateSavedTask(PlatformContext context, DalDataAccess dataaccess, ISvrDocEdit doc, ITaskData taskData)     at Microsoft.Office.Project.Server.BusinessLayer.Statusing.ImportTimesheet(Guid periodUID)     at Microsoft.Office.Project.Server.WebService.Statusing.ImportTimesheet(Guid periodUID)

Verbose Level Log
PWA:http://servername/pwa, SSP:SharedServices1, User:DOMAIN\User, PSI: Statusing.ImportTimesheet  PSI Entry Point: Statusing.ImportTimesheet  Project User: DOMAIN\User Correlation Id: f5af0b6e-a9be-4b88-88fa-3529021a1696  PWA Site URL: http://servername/pwa  SSP Name: SharedServices1  PSError: Success (0) XML: <errinfo><general><class name="An unhandled exception occurred in Statusing.ImportTimesheet."><error id="42" name="GeneralUnhandledException" uid="862074be-9315-4c83-9a1f-66053d785061" /></class></general></errinfo>

Technorati Tags:

Quite a few customers have run into an issue since loading MS08-18 and the restriction on “legacy” file formats introduced in both this fix and SP3 for Project 2003 http://support.microsoft.com/kb/941466. If you are not familiar with the issue then the following file formats are blocked, but can be “unblocked” by going to Options on the Tools menu in Project 2003, click the Security tab, and check the required option under Legacy Formats

By default, the following file formats are blocked in Project 2003 SP3:

•.mpp
Note By default, Microsoft Project 98 versions of .mpp files are blocked. However, Microsoft Project 2000 .mpp files, Microsoft Project 2002 .mpp files, and Microsoft Office Project 2003 .mpp files are not blocked.

•.mpx

•.xls

•.txt

•.csv

Also blocked is HTML – but this was omitted from the KB (sorry).

The problem we gave our customers is that if you loaded MS08-18 and not SP3 then the UI is not in place in Project to make these changes.  You can however still follow the details in the KB article to change the settings in the registry – noting the warning in the KB about editing the registry!

Our recommendation is that if you cannot deploy SP3 to enable this interface for your users, that you deploy the registry key setting to enable these needed file types.  There are several methods for deploying a registry key including:

1. Use the Custom Maintenance Wizard to modify the installation to include this key.

2. Use SMS to deploy this key to the appropriate machines.

3. Use a logon script to deploy this key.

4. Use the Office Policy Templates to deploy this via a GPO.

We apologize for any confusion that was created by this not being documented in the KB Article that discusses this Security Patch.  We will be getting this changed to include HTML in this list so other customers do not run into this in the future.

Technorati Tags:

In Project Server 2003 updates needed to be made with Project Professional , and then when Project was closed an auto publish would happen – if you wanted it to or not.  Some people wanted it, some didn’t.  In 2007 as we don’t need to use Project Professional and this can happen purely on the server then we leave the choice of when to publish up to you. 

One way you can simulate the 2003 behavior is to use a server-side event handler to act on the Statusing.OnApplied method, and carry out a publish of the project via the PSI.  This would then ensure that all accepted updates were reflected in PWA.  The following code samples is based on the SDK TestEventHandler, and apart from the code, would also require web references to the Project and LoginWindows web services.  I am also not using impersonation, so my SSP Administrator (the account that would be running the Event services) does need to be a user in PWA with the right permissions to publish the projects.  You could use the same techniques as I recently published for impersonation if you did not want to give any PWA permissions to this account.  If multiple publish events hit the queue at one time then most will get skipped for optimization – but if you are processing a great deal of updates you should consider the load this extra publish work will put on your server.  I use the “post” rather than the “pre” event so that even if the event handler fails it will not block or cancel the event.  Obviously my writing to the event log is optional too – just helped me to see it was working.

As usual with MSDN blog postings the code is supplied “as-is”, with no warranties or support and could probably do with some better exception handling – but hopefully for any customers wanting to get auto publishing this will be a help.  You will need to replace servername and pwa with your own servername and pwa instance names.

using System;
using System.Net;
using System.Diagnostics;
using Microsoft.Office.Project.Server.Events;
using Microsoft.Office.Project.Server.Library;

namespace TestEventHandler
{
    public class MyPublishingEventHandler : StatusingEventReceiver 
    {
        const string LOGINWINDOWS = "_vti_bin/PSI/LoginWindows.asmx";
        const string PROJECT = "_vti_bin/PSI/Project.asmx";
        private static WebSvcLoginWindows.LoginWindows 
            loginWindows = new 
                WebSvcLoginWindows.LoginWindows();
        private static WebSvcProject.Project project =
            new WebSvcProject.Project();
        private string baseUrl = "http://servername/pwa/";
 
        public override void  OnApplied(PSContextInfo contextInfo, 
            StatusingPostApplyEventArgs e)

        {
             base.OnApplied(contextInfo, e);
            
            loginWindows.Url = baseUrl + LOGINWINDOWS;
            loginWindows.Credentials = 
                CredentialCache.DefaultCredentials;
            project.Url = baseUrl + PROJECT;
            project.Credentials = 
                CredentialCache.DefaultCredentials;

            // I don't do a full publish 
            // You could change the third parameter to true if you wanted to
            Guid jobUid = Guid.NewGuid();
            project.QueuePublish(jobUid, e.ProjectID, false, "");

            // Create an EventLog instance and assign its source.
            EventLog myLog = new EventLog();
            myLog.Source =