Sean's profileThe 7th (Sean Dai's Proj...BlogListsSkyDrive Tools Help

Blog


    February 28

    Blog Moved

    My blog has moved to http://blogs.msdn.com/seanday/.
     
    January 08

    Coordinate Project Server and Windows SharePoint Services security

    Project Server 2007 did some improvement on WSS integration (If you want to know about integration between project server 2003 and WSS 2.0, please see my post:Customize WSS Role/User permission for Project Server 2003).  You may read Microsoft TechNet article about this: http://technet2.microsoft.com/Office/en-us/library/c9ca0b66-c167-4cd1-8ecf-6b311cf6976a1033.mspx?mfr=true.
     
    Project Server 2007 is completed built on the top of WSS 3.0.  It will create four SharePoint groups on every project workspace:
    • Web Administrator (Microsoft Office Project Server): Users who have global permission "Manage Windows SharePoint Services".
    • Project Managers (Microsoft Office Project Server): Users who have category permission "Save Project to Project Server" on the project.
    • Team Members  (Microsoft Office Project Server): Users who are added in project team and assigned to task.
    • Readers  (Microsoft Office Project Server):  Users who have category permission "View Project Workspace" on the project OR who are added in project team but not assigned to task.
    Normally user would not be in two groups at same time, except team members and readers.  User could be in team members and readers groups if users assigned to tasks and have view project workspace permission on my organization category.
     
    By default, WSS security settings will change according to PWA security changes.  However, you can change the settings on the Project Workspace Provisioning Settings page to check the box "Automatically add Project Web Access users to project team Web site when SharePoint site is created or when the project manager publishes the project information to Project Server"
     
    Then, what scenarios will change the WSS security?
    • New user is created with "Manage Windows SharePoint Services", "Save Project to Project Server", and/or "View Project Workspace" permission.
    • A existing user's permissions have changed.  The above three permissions have been changed.
    • A resource with those three permissions are activated or de-activated.
    • A resource is added to project and project is published.
    Since the first three scenarios may affect large number of project workspace, the best practice is to create/change those resources during non-working hours.
     
    Microsoft documents that denying Logon permission and View Project Workspace will affect WSS security.  However, as I tested, I did not find denying those permission will affect WSS.

    Timesheet Event Workflow

    I did not find any thing about Timesheet workflow and which event will occur if you do something in PWA.  So I did some testing and come out with this diagram.  If you found something different, please email me.
     
    Timesheet Workflow

    Use PSI event to validate timesheet

    In project server 2007, timesheet is a very good new feature.  However, when user enters timesheet and save it back to server, the server does not validate the number.  For example, user can enter 30 hours for a day.  You can set the limitation in Timesheet settings, however project server only validate timesheet hours when user submits it.  What if you don't want team members to enter ridiculous hours, here is a solution.  You can use PSI event handler to track the changes and cancel the change for whatever reason.  Here is how to do it.

    Step 1: create a project server event handler to validate user input.

    Timesheet Updating event occurs when a user tries to save timesheet but before it is actually saved.  You can use this event to validate user input.  If the timesheet is valid, you will let timesheet being saved.  Otherwise you can cancel the process and abort saving. 

    You can use Timesheet web service to find the hours saved before.  And you can find what user changed in the TimesheetPreUpdateArgs class, which is stored in DsDelta dataset.  DsDelta dataset would only store the changed hours not all hours.  You need to merge the two dataset to get “real” updating dataset.

    If you find the hours are not valid, you can set the TimesheetPreUpdateEventArgs.Cancel=True and TimesheetPreUpdateEventArgs.CancelReason=”Timesheet cancel reson”.  Then project server will stop processing timesheet updating and return to timesheet page.  Project server will also save an error message to event log.

    However, when you return to timesheet page, the project server does not show any message for cancelling timesheet.  You will need the second step to show the actual reason.

    Here is some sample code that it can check if actual hours in one day is more than 24 hours.  If so, it will cancel the event.

    public override void OnUpdating(PSContextInfo contextInfo, TimesheetPreUpdateEventArgs e)

    {

            if (e.DsDelta == null)

            {

                return;

            }

     

        TimesheetSvc.TimeSheet ts = new TimesheetSvc.TimeSheet();

        ts.Url = _EPMSite + _TimesheetSvc;

        ts.Credentials = CredentialCache.DefaultCredentials;

        TimesheetSvc.TimesheetDataSet tsDs = ts.ReadTimesheet(e.TsUID);

     

        TimesheetSvc.TimesheetDataSet.ActualsDataTable tblActuals = MergeActuals(tsDs.Actuals, e.DsDelta.Actuals);

     

        Dictionary<DateTime, Decimal> TblTotal = AccumulateActuals(tblActuals);

     

        string Msg = ValidateActuals(TblTotal);

        if (Msg != "")

        {

            e.CancelEvent(Msg);

        }

    }

     

    Step 2: create a custom web part to show cancel reason.

     

    Since the timesheet page cannot show cancel reason from event handler, you may need to create a custom web part to show the reason.  You can just create a very simple web part which contains a label control.  The web part will read event log and display if it found any recent error log.

    Add More Time Hierarchy in Project Server 2007 Cubes

    Many organizations have their own definitions of time.  Project Server standard time dimension may not fit their needs.  Don't worry, we can extend the cube by adding more time hierarchies. 
     
    In Analysis Service 2005, one dimension could have multiply hierarchies.  Now we don't need to add time dimension but just add another time hierarchy.  If you want to find information about adding time dimension in project server 2000, you may check another post: http://netsleeper.spaces.live.com/blog/cns!2D6B305EBBD50AA5!127.entry
     
    Another good thing in project server 2005 is that we can use event and managed code.  We can write code on OnCubeProcessed event.  This event occurs after project server processed OLAP cube.  Since we are creating time hierarchy, we need to wait project server finishing creating time dimension.
     
    Here, I am using Analysis Management Object API for Analysis Service 2005, which is similiar but a little different from old way.  The following sample code is to create another time hierarchy that only have year, week and day.  So that week will not split between months.
     
            public override void OnCubeProcessed(PSContextInfo contextInfo, CubeAdminPostCubeProcessEventArgs e)
            {
               
                try
                {
                    WriteMessage("Start Cube Extension");
                    // connect analysis service server
                    Server OLAPServer = new Server();
                    OLAPServer.Connect("Data Source=" + e.ServerName + ";Provider=msolap;");
                   
                    WriteMessage("Analysis service server connected:" + e.ServerName);

                    // open EPM OLAP database
                    Database EPMOLAP = OLAPServer.Databases.FindByName(e.DbName);
                    if (EPMOLAP == null)
                    {
                        EventLog.WriteEntry(LogSource, "Database: " + e.DbName + " does not exist in Project Server 2007 cube.", EventLogEntryType.Error);
                        return;
                    }
                    WriteMessage("Start building new Time hirarchy");
                    // open Time dimension
                    Dimension dmnTime = EPMOLAP.Dimensions[TimeDimensionID];
                    if (dmnTime == null)
                    {
                        EventLog.WriteEntry(LogSource, "Time dimension does not exist.", EventLogEntryType.Error);
                        return;
                    }
                    // check if the custom hierarchy is already created
                    if (dmnTime.Hierarchies.FindByName(NewTimeDimensionName) != null)
                    {
                        EventLog.WriteEntry(LogSource, "New Time hierarchy has already been created by other process.", EventLogEntryType.Error);
                        return;
                    }
                    // create custom hierarchy in time dimension
                    Hierarchy NewTime = dmnTime.Hierarchies.Add(NewTimeDimensionName);
                    // find year, week, and day attribute in time dimension
                    DimensionAttribute atrYear = dmnTime.Attributes.FindByName(YearAttributeName);
                    if (atrYear == null)
                    {
                        EventLog.WriteEntry(LogSource, "Year attribute does not exist in Time dimension.", EventLogEntryType.Error);
                        return;
                    }
                    DimensionAttribute atrWeek = dmnTime.Attributes.FindByName(WeekAttributeName);
                    if (atrWeek == null)
                    {
                        EventLog.WriteEntry(LogSource, "Week attribute does not exist in Time dimension.", EventLogEntryType.Error);
                        return;
                    }
                    DimensionAttribute atrDay = dmnTime.Attributes.FindByName(DayAttributeName);
                    if (atrDay == null)
                    {
                        EventLog.WriteEntry(LogSource, "Day attribute does not exist in Time dimension.", EventLogEntryType.Error);
                        return;
                    }
                    // create level for year, week, and day in custom hierarchy
                    Level lvlYear = PPGTime.Levels.Add(YearLevelName);
                    lvlYear.SourceAttributeID = atrYear.ID;
                    Level lvlWeek = PPGTime.Levels.Add(WeekLevelName);
                    lvlWeek.SourceAttributeID = atrWeek.ID;
                    Level lvlDay = PPGTime.Levels.Add(DayLevelName);
                    lvlDay.SourceAttributeID = atrDay.ID;

                    WriteMessage("Complete building hirarchy");
                    // save the changes to database.
                    dmnTime.Update();
                   
                    WriteMessage("Complete Cube Extension");

                }
                catch (Exception ex)
                {
                    EventLog.WriteEntry(LogSource, ex.ToString(), EventLogEntryType.Error);
                }
            }
    November 21

    Access Active Directory From SharePoint 2007 Web Part

    DirectoryEntry, and DirectorySearch class in System.DirectoryService namespace can be used to access and search Windows Active Directory.  When you create DirectoryEntry without any credential, the system will use your default credential to access Active Directory.  In ASP.NET web application, if you set impersonate=true, the application will use your login to access Active Directory.
     
    However, in SharePoint 2007 web part, this does not work.  You have to explicitly specify the username and password.  Therefore, in SharePoint 2007 web part, you have to use service account to access Active Directory. 
     

    Move Project Server 2003 & WSS 2.0 Workspace From One Server to Another Server Step by Step

    Before move to the new servers
    1. Prepare the new servers.  Install OS with latest service pack, and install .NET framework 1.1
    2. Install SQLServer 2000 on database server.  You need to install at least service pack 3, recommended service pack 4.
    3. Install STS 2.0 and service pack on project server.  Select "Server Farm" mode to install and configure the STS and default web site after installation. 
    4. Install project server 2003 and service pack.

    1. Make sure WSS, Project Server of two system on the same version
        a. For project server
            All project server instances should be updated to at least SP2a.  Go to the installed folder of project server, find PDS.dll file.  Here is the version table:

     

    Project Server 2003

    SP2

    11.2005.0.3801

    SP2a

    11.2005.0.4110

    SP3

    11.2007.0.3529

        B. For WSS
            Go to Control PanelAdd and Remove Programs, select the Microsoft Windows SharePoint Service 2.0, and then click: Click here for support.  The versions will be shown in dialog.  Here is the table of WSS 2.0 version

     

    Windows SharePoint Services v2

    SharePoint Portal Server 2003

    Unserviced pack

    11.0.5608.0

    11.0.5704.0

    With Service Pack 1

    11.0.6361.0

    11.0.6715.0

    SP1+KB887981

    11.0.6411.0

     

    R2 WSS SP2 Beta

    11.0.6551.0

     

    SP2

    11.0.7969.0

    11.0.8126.0

    Post SP2 rollup

    11.0.8000.0

    11.0.8126.2

    SP3

    11.0.8173.0

    11.0.8168.0

               To find database schema version, you may open the globals table in configure database.  The field SchemaVersion will be the database schema version.

     

    Database Schema Version

    Unserviced pack

    6.0.2.5530

    WSS SP1

    6.0.2.6361

    WSS SP1 + KB887981

    6.0.2.6411

    R2 WSS SP2 Beta

    6.0.2.6551

    WSS SP2

    6.0.2.6568

    WSS SP2 rollup KB900929

    6.0.2.8000

    WSS SP2 KB900929+KB924881

    6.0.2.8117

    WSS SP3

    6.0.2.8165

            Please see the document for detail information.

              http://mindsharpblogs.com/penny/articles/481.aspx

     

    2. Restore Project Server database

    3. Change Project Server database

    1. Open MSP_WEB_STS_SERVERS table
    2. Change WSTS_SERVER_NAME to new project server name.
    3. Change WSTS_SERVER_ADMIN_PORT to the port of WSS admin site.
    4. Change WSTS_ADMIN_SERVER_NAME to new project server name.

    4. Re-assign project user and projectserver user

    1. Remove the MSProjectUser and MSProjectServerUser from ProjectServer Database.
    2. Go to securitylogins
    3. Right-click on MSProjectServerUser and select Property
    4. In user property dialog, select “Database Access
    5. Check the ProjectServer database and select “MSProjectServerRole” in the database role list
      SetUser
    6. Select OK to close dialog.
    7. Right-click on MSProjectUser and select Property
    8. In user property dialog, select “Database Access
    9. Check the ProjectServer database and select “MSProjectRole” in the database role list
    10. Select OK to close dialog.

    5. Restore WSS Content database

    6. Add content database to virtual server

    1. Open SharePoint admin site, select Configure Virtual Server Settings.
      WSS1
    2. Select Default Web Site
      WSS2
    3. Select Manage Content Databases
      WSS3
    4. Select WSS_Content_Prod database
      WSS4
    5. Check Remove content database in Remove Content Database section and click OK.
    6. After done, Click “Add a Content Database
    7. Select Specify database server settings
      Enter SQL server name as database server name
      Enter WSS_Content_Prod as database name
      Enter 9000 in Number of sites before a warning event is generated
      Enter 15000 in Maximum number of sites that can be created in this database
      WSS5
    8. Click OK to add the content database, the WSS will find all sites in that new content database.

     

    Project Server 2003 PWA Menu Customization

    Microsoft Project Server 2003 Web Access provides a mechanism for developer to customize the menu.  However, any menu added by “server configuration” in admin page will no security validation, that all user can view it, even if it is under admin submenu.

     

    Solution

     

    The reason is that the default global permission of a custom menu is 0, which means every user has permission to view it.  Since the PWA does not provide an interface to change the permission setting, the custom menu cannot act like other menus.  However, the custom menu uses the same data structure as the build-in menu.  Therefore developers can directly modify the database to make menu secure.

     

    The project server database uses two tables to store the menu information, MSP_WEB_SECURITY_MENUS and MSP_WEB_SECURITY_PAGES.  MSP_WEB_SECURITY_MENUS saves the information of each menu, such as menu ID, menu name, indicator that menu is a top-menu or submenu, sequence of menu, the web page a menu link to, and etc.  MSP_WEB_SECURITY_PAGES saves the information of each page, such as URL of page and etc.  Among them, there is a property, WSEC_PAGE_ACT_ID, which is the feature action ID of the menu, defined in MSP_WEB_SECURITY_FEATURES_ACTIONS. 

     

    When a developer added a custom menu to PWA, the project server will add a record in MSP_WEB_SECURITY_MENUS table and a record in MSP_WEB_SECURITY_PAGES table.  By default, the WSEC_PAGE_ACT_ID is set to 0 of custom page.  To make the page secure, developer can modify the value with any valid feature action ID.  For example, a developer adds a function page in admin menu.  It can be set as the same permission as “Manage Views” menu in admin menu.  To set so, developer can simply set the WSEC_PAGE_ACT_ID to 730.  Then the new page will have the permission setting as manage views page, and only those user who can manage views, have permission to view the new custom page.

    November 16

    How to setup analysis service with project server 2007

    Project Server 2007 can work with SQLServer 2000 and SQLServer 2005.  To setup OLAP cube for project server 2007, here is some references and some tips:

     

    1. For Analysis Service 2000

    You can just follow the microsoft MSDN document: http://technet2.microsoft.com/Office/en-us/library/5d90076f-bbcc-48c1-a569-bd236862d47c1033.mspx?mfr=true

     

    2. For Analysis Service 2005

    Here is the microsoft MSDN document:

    http://technet2.microsoft.com/Office/en-us/library/86910044-701c-4b02-89d7-e094e9cb7dcd1033.mspx?mfr=true

     

    It is working if you install analysis service on the same computer of project server.  However, it does not work if you separate those two, which is the most case.

     

    You will get the following error:

    Failed to build the OLAP cubes. Error: Analysis Services session failed with the following error: Failed to delete the Olap database: PWA_AnalysisServices. Error: Your permissions on the server computer do not allow you to administer this Analysis server.

     

    The reason is because the SSP account does not have permission to manage analysis service remotely by using DSO.  To enable remote administer analysis service through DSO, you will need to create shared folder for analysis service: MSOLAPRepository$.  This is just for back compatible to DSO.  Analysis service 2000 will create it during the installation.  However, since analysis service 2005 no longer need it.  It will not create the shared folder.  Then you have to do it yourself.  The folder will only be used for remote administer.  So if you install analysis service on the same box of project server, you don't need it.

     

    Use this two steps with the MSDN document, you can setup OLAP cube.

    Create the shared folder

    1.     In Windows Explorer, browse to the folder containing the Analysis Services installation. By default, it is located at:
    C:\Program Files\Microsoft SQL Server\MSSQL.2\OLAP

    2.     In this folder, create a new folder and name it DSO9.

    3.     Right-click the DSO9 folder, and choose Sharing and Security.

    4.     On the Sharing and Security page, in the Sharing tab, select Share this folder. In the Share Name box, type MSOLAPRepository$ as the share name for the folder.

    5.     On the Sharing tab, in the Group or user names list, click Add. On the Select User, Computers, or Groups page, add the SQLServer2005MSOLAPUser$<Servername>$MSSQLServer account from the local computer. Click OK.

    6.     In the Group or user names list, select the SQLServer2005MSOLAPUser$<Servername>$MSSQLServer account. In the Permissions for SQLServer2005MSOLAPUser$<Servername>$MSSQLServer box, select Allow next to Full Control. Click OK.

    7.     On the Security tab, in the Group or user names list, select SQLServer2005MSOLAPUser$SERVERNAME$MSSQLSERVER. Select the Allow check box next to the Full Control item in the Permissions list, and then click OK.

    8.     On the New Folder Properties page, click OK.

    Configure Analysis Services For DSO

    1.     In SQL Server 2005, start SQL Server Management Studio. On the Connect to Server window, select Analysis Services, and then click Connect.

    2.     In SQL Server Management Studio, in the Object Explorer pane, right-click the Analysis Services name, and then choose Properties.

    3.     On the Analysis Services Properties page, in the Select a page section select General. Select Show Advanced (All) Properties.

    4.     Select DSO\RepositoryConnectionString from the Name list.
    a.   Select the corresponding value for the string in the Value column, and then click the box that appears to the right of the value to display the Connection Manager page.
    b.   On the Connection Manager page, in the Provider list, select Native OLE DB\SQL Native Client.
    c.   In the Server Name list, select the server on which the repository database is located.
    d.   In the Logon to the server field, enter the account information to log onto the server.
    e.   In the Connect to database section, select Select or enter a database name and enter the name of the repository file.
    f.    Click OK.

    5.     Select DSO\RemoteRepositoryConnectionString from the Name list.
    a.   Select the corresponding value for the string in the Value column, and then click the box that appears to the right of the value to display the Connection Manager page.
    b.   On the Connection Manager page, in the Provider list, select Native OLE DB\SQL Native Client.
    c.   In the Server Name list, select the server on which the repository database is located.
    d.   In the Logon to the server field, enter the account information to log onto the server.
    e.   In the Connect to database section, select Select or enter a database name and enter the name of the repository file.
    f.    Click OK.

    6.     Select DSO\LocksDirectory from the Name list, enter the local path of the shared folder.  For example: E:\MSSQL.3\OLAP\DSO9\

    7.     Select DSO\RemoteLocksDirectory from the Name list, enter the UNC name of the shared folder.  For example: \\HACSSDB028\MSOLAPRepository$\

    8.     On the Analysis Server Properties page, click OK.

    NOTES:

    1.     When you configure analysis service 2005, MSDN gaves you two choices: creating Access MDB file or SQLServer 2005 database.  You can do either one of them.  However, you cannot migrate from MDB to SQLServer 2005.

    2.     To view project server OLAP view in PWA, the client computer (with Windows XP.  Vista already has those component) has to install the following components:

        • Microsoft Core XML Services (MSXML) 6.0
        • Microsoft SQL Server 2005 Analysis Services 9.0 OLE DB Provider

    You can find them in SQLServer 2005 feature pack: http://www.microsoft.com/downloads/details.aspx?FamilyID=50b97994-8453-4998-8226-fa42ec403d17&DisplayLang=en

    A Better Way to Create Indicator

    Sometimes, you need to create a very complex formula for indicator.  It's very hard to understand and maintain.   

    For example, client wants to show schedule indicator green when duration variance within 10% of baseline duration, 11%~20% for yellow and above 20% shows red. 

     

    Sample formula:

    SWITCH([Finish Variance]=0,"On Target",[Finish Variance]<0,CSTR(CINT([Finish Variance]*(-100)/[Baseline Duration]))+"% Ahead Of Schedule", true,CSTR(CINT([Finish Variance]*100/[Baseline Duration]))+"% Over Schedule")

    Then when you set graphic indicator, use "Is Less Than" or "Is Greater Than".  You can set as

    Is Less Than     11      Green

    Is Less Than     21      Yellow

    Any Value                  Red

     

    Project Professional is smarter enough to compare the percent as number.  You can event put percent in the middle of the data, such as "Task 10% Behind Schedule".

     

    Project Server 2007 Active Directory Synchronization Error

    I have experienced Project Server 2007 Active Directory Synchronization failure on client.  As the result, Active Directory cannot be synchronized and even worse, the user/resource edit page shows error when you tried to edit a synchronized user/resource.  It may potentially create corrupted assignments in project plan.

     

    In the event log on project server, there are errors in application category.

     

    EventLog1

     

    Error Message:

     

    Standard Information:PSI Entry Point:

    Project User: IHESS\sdai

    Correlation Id: 837bfd5f-9502-4e09-a193-2af20c8392b0

    PWA Site URL: http://hacssia021/PWA

    SSP Name: SharedServices_MOPS

    PSError: Success (0)

    A general exception occurred during communication with Active Directory. Context: SyncGroup. Additional Information: An exception occurred while syncing a project server group with active directory. GroupName: '%s'. PWA Group Guid: '%s' Exception: '%s'.. Exception Info: EPIS Team Members

     

    For more information, see Help and Support Center at http://go.microsoft.com/fwlink/events.asp.

     

     

     

     

    There also are errors in system category:

     

    EventLog2

     

     

     

    Error Message:

     

    The application-specific permission settings do not grant Local Activation permission for the COM Server application with CLSID

    {61738644-F196-11D0-9953-00C04FD919C1}

     to the user IHESS\SRV_PROJSVR_DB SID (S-1-5-21-789336058-2052111302-839522115-106752).  This security permission can be modified using the Component Services administrative tool.

     

     

     

    From the error message, I decided to change the DCOM configuration first.  By searching the registry, I found that (S-1-5-21-789336058-2052111302-839522115-106752) is the ID of IIS WAMREG Admin Service component.

     

    Open the Component Services on project server and select “Component Services” – “DCOM Config”.

     

    c1

     

    Right click on component and select “Property”.  Then select “Security”.

     

    n1

     

    Select “Edit” button in “Launch and Activation Permissions”, then add the SharePoint farm service user in and give it “Local Launch” and “Local Activation” permissions.

     

    n2 

     

    After done, I tried to synchronize the users again.  However, I still got errors.  When I check the event log, the errors are different.

     

    EventLog3 

     

     

    Error Message:

     

    Standard Information:PSI Entry Point:

    Project User: IHESS\sdai

    Correlation Id: 837bfd5f-9502-4e09-a193-2af20c8392b0

    PWA Site URL: http://hacssia021/PWA

    SSP Name: SharedServices_MOPS

    PSError: Success (0)

    Changes were detected for a resource during Project Server Active Directory Synchronization. However, the changes could not be applied because the resource could not be checked out. The checkout failed because the resource is checked out to another user. Resource GUID: 67581218-21cd-4f5e-9ee0-6ad4f0e25799

     

    For more information, see Help and Support Center at http://go.microsoft.com/fwlink/events.asp.

     

     

     

    For more information, see Help and Support Center at http://go.microsoft.com/fwlink/events.asp.

     

    It shows some resource has been checked out.  But in force-in resource page, I did not find any check-out resources.  Actually, those resources are checked-out by AD synchronization process. 

     

    I used “SQL Server Management Studio” to open the Project Server 2007 published database. 

     

    Run the script to check back in the resources

     

    UPDATE MSP_RESOURCES
    SET RES_CHECKOUTBY = NULL
    WHERE RES_UID in (
    SELECT
    RES_UID
    FROM MSP_Resources
    WHERE RES_CHECKOUTBY = '00000000-0000-0000-0000-000000000000')

     

     

    If you have corrupted assignments, you may use the script to find out:

     

    SELECT MSP_PROJECTS.PROJ_NAME,
    MSP_ASSIGNMENTS_SAVED.TASK_NAME,
    MSP_RESOURCES.RES_NAME,
    MSP_ASSIGNMENTS_SAVED.ASSN_UID,
    MSP_ASSIGNMENTS_SAVED.PROJ_UID,
    MSP_ASSIGNMENTS_SAVED.TASK_UID,
    MSP_ASSIGNMENTS_SAVED.RES_UID
    FROM MSP_ASSIGNMENTS_SAVED INNER JOIN
    MSP_PROJECTS ON MSP_ASSIGNMENTS_SAVED.PROJ_UID =
    MSP_PROJECTS.PROJ_UID INNER JOIN
    MSP_RESOURCES ON MSP_ASSIGNMENTS_SAVED.RES_UID =
    MSP_RESOURCES.RES_UID
    WHERE (NOT EXISTS
    (SELECT TASK_UID
    FROM MSP_TASKS_SAVED
    WHERE (TASK_UID = MSP_ASSIGNMENTS_SAVED.TASK_UID)))

     

     

     

    Reference:

     

    http://connect.microsoft.com/feedback/ViewFeedback.aspx?FeedbackID=288068&SiteID=235

     

    March 16

    PDS with multiple hosted sites - Error 405 method not allowed

    Problem
     
    I have a problem with PDS and an additional instance of Project Web Access (created with EditSite). I followed the description in pj11PDSref.chm and made a new copy of pds.wsdl and pds.wsml with the proper URL.

    I am able to logon, but when i try to execute "ProjectsStatus" I get the error "... 405 method not allowed". The same application works fine with the "Main" Project Web Access.
     
    Solution
     
    1. Ensure the MS SOAP Toolkit 3.0 is installed on your Project Server
    2. Navigate into IIS
    3. Right click on the original /projectserver virtual directory created during installation.
    4. Click on Properties for the virtual directory
    5. Click on the Configuration...button
    6. Scroll all the way to the bottom and take note of the path of the "wsdl" extension
        This should be something like "c:\Program Files\Common Files\MSSOAP\BINARIES\SOAPIS30.dll"
    7. Click cancel twice to return to the main IIS screen

    Now configure the new virtual directories:
    1. Right click on the new virtual directory created using EditSite
    2. Click on Properties for the new virtual directory
    4. Click on the Configuration... button
    5. Click Add...
    6. Enter the path from step #6 above (with the quotes)
    7. Enter GET, POST in the Limit To box
    8. Enter wsdl in the Extension box
    9. Click OK Twice.
    10. Reset IIS.

    This should fix your problem.
    February 20

    Customize Issue on Homepage of PWA

    The Issues list of WSS workspace has three status, Active Postpone and Closed.  Clients may want to replace it with their values.  The problem of custom status field is that PWA homepage would search WSS for "Active" issues(Risks).  If there is no "Active" status for issue(risks), then the homepage can not be any issue.  If we remove "Active.aspx" file from workspace template, the link of active issue will cause 404 error - page not found.
     
    To custom status field and show proper issues in homepage, you have to do:
    1. Change the status field as you want.
    2. Open ".\Home\IssuesHome.asp" file and find IssueHome_Write function.
    3. Find "nActive = GetIssuesCount(rsMyProjects, oStringsIssues.GetString(IDS_ISSUES_STATUS_ACTIVE), true);", the value "oStringsIssues.GetString(IDS_ISSUES_STATUS_ACTIVE)" is the specific status PWA will search on.  You may replace it with any status string you want to search on.
    3. Find "var sLink = '<A ID="idIssueProj' + nProjID + '" CLASS="link2" HREF="../Issues/IssueShell.asp?ProjID=' + nProjID + '&Frame=Active.aspx" TITLE="' + oStringsHome.GetString(IDS_HOME_ISSUES_ACTIVE_ALT) + '" ACCESSKEY="' + oStringsHome.GetString(IDS_HOME_ISSUES_ACTIVE_KEY) + '">';". The value "Frame=Active.aspx" is the view page that shows the issues.  You may replace it with any view page you want.

    February 01

    Customize WSS Role/User permission for Project Server 2003

    By default, project server will create four roles in WSS workspace and put users in corresponding roles.  This is default setting:
     
    Role Name Project Server User Permissions
    Project Managers (Microsoft Office Project Server) Project Managers who have published the project, or who have Save Project permission on the project All permissions except Manage List Permission, Manage Site Groups, and Manage Web Site
    Readers (Microsoft Office Project Server) Who have View Issue permission on the project View Items and View Pages permissions.
    Team Members (Microsoft Office Project Server) Who have assignments in the project or who are the team lead of the assignments in the project Add Items, Edit Items, Delete Items, View Items, Browse Categories, View Pages, Manage Personal Views, Add/Remove Private  WebPart, and Update Private WebPart
    Web Administrators (Microsoft Office Project Server) Who have Manage Sharepoint global permission All Permissions

     

    However, clients may have different requirement.  For example, let project manager be the web administrator of its own project workspace.  Select/deselect one of the default permission of specific role.  Project Server cannot customize the roles and users.  If we change the role configuration in WSS, then after synchonization, the setting will be restored to default settings.

     

    The customization can be done by modifing the project server ASP file.  The permission of each role is defined in DOCLIB\STSADUTL.ASP, and the function name is StsAdminUtil_DoCustomSOAPToWSSOM.  The mask defines the permission matrix.  You may find the permission matrix in WSS SDK or as follows:

    Name Value Description Site Groups
    AddAndCustomizePages 0x00040000 Add, change, or delete ASPX pages, HTML pages, or Web Part Pages, and edit the Web site using a Windows SharePoint Services-compatible editor. Web Designer, Administrator
    AddDelPrivateWebParts 0x10000000 Add or remove Web Parts on a personalized Web Part Page. Contributor, Web Designer, Administrator
    AddListItems 0x00000002 Add items to lists, add documents to document libraries, and add Web discussion comments. Contributor, Web Designer, Administrator
    ApplyStyleSheets 0x00100000 Apply a style sheet (.CSS file) to the Web site. Web Designer, Administrator
    ApplyThemeAndBorder 0x00080000 Apply a theme or borders to the entire Web site. Web Designer, Administrator
    BrowseDirectories 0x04000000 Browse directories in a Web site. Contributor, Web Designer, Administrator
    BrowseUserInfo 0x08000000 View information about users. This right is not available through the user interface. Guest, Reader, Contributor, Web Designer, Administrator
    CancelCheckout 0x00000100 Check in a document without saving the current changes. Web Designer, Administrator
    CreatePersonalGroups 0x01000000 Create, change, and delete site groups, including adding users to the site groups and specifying which rights are assigned to a site group. Contributor, Web Designer, Administrator
    CreateSSCSite 0x00400000 Create a Web site using Self-Service Site Creation. Reader, Contributor, Web Designer, Administrator
    DeleteListItems 0x00000008 Delete items from a list, documents from a document library, and Web discussion comments in documents. Contributor, Web Designer, Administrator
    EditListItems 0x00000004 Edit items in lists, edit documents in document libraries, edit Web discussion comments in documents, and customize Web Part Pages in document libraries. Contributor, Web Designer, Administrator
    EmptyMask 0x00000000 Has no permissions on the Web site. Not available through the user interface. N/A
    FullMask -1 Has all permissions on the Web site. Not available through the user interface. N/A
    ManageListPermissions 0x00000400 Grant, deny, or change user permissions to a list. Administrator
    ManageLists 0x00000800 Approve content in lists, add or remove columns in a list, and add or remove public views of a list. Web Designer, Administrator
    ManagePersonalViews 0x00000200 Create, change, and delete personal views of lists. Contributor, Web Designer, Administrator
    ManageRoles 0x02000000 Create, change, and delete site groups, including adding users to the site groups and specifying which rights are assigned to a site group. Administrator
    ManageSubwebs 0x00800000 Manage or create subsites. Administrator
    ManageWeb 0x40000000 Manage a site, including the ability to perform all administration tasks for the site and manage contents and permissions Administrator
    OpenWeb 0x00010000 Open the SharePoint Web site and get metadata related to the site, as well as see the underlying navigation structure (not exposed in the user interface). Guest, Reader, Contributor, Web Designer, Administrator
    UpdatePersonalWebParts 0x20000000 Update Web Parts to display personalized information. Contributor, Web Designer, Administrator
    ViewListItems 0x00000001 View items in lists, documents in document libraries, view Web discussion comments, and set up e-mail alerts for lists. Reader, Contributor, Web Designer, Administrator
    ViewPages 0x00020000 View pages in a Web site. Reader, Contributor, Web Designer, Administrator
    ViewUsageData 0x00200000 View reports on Web site usage. Administrator

     

    To change the permission or add more roles, you can modify the mask or the whole function.

    Also in STSADUTL.ASP file, StsAdminUtil_RoleUsers function defines who should be put in each role.  You may change the function to change the user role.

    December 07

    Generate .NET Code by using Olymars

    Olymars, full name as SQL Server Centric .NET Code Generator, is a beta software by Microsoft, which can generate .NET code based on SQL Server database.  By default, it has five build-in templates.  However, developers can easily extend it to fit your needs.  Here is a example to extend the templates, which can generate data access layer for application by using Microsoft Application Blocks.
     
    To extend the templates:
    1. Add a new groups in repository
    2. Add a reference to System.dll in repository
    3. Add a dynamic variable as CompanyName
    4. Add reference of System.dll, System.Window.Forms.dll to your new group
    5. Add dynamic variables as CompanyName and DeveloperAlias to your new group
    6. Add three elements to your new group.
    7. Edit the templates.
     
    [Code of Templates]
    <*
    // ----------------------------------------------------------------------------------------------------
    // GetName function for element: 'Data Access Layer Interface'
    // ----------------------------------------------------------------------------------------------------
    public string IDataAccessLayer_GetName() {
    
    	// Choose whatever name you wish for this object
    	return (@"IDataAccessLayer.cs");
    
    }
    
    // ----------------------------------------------------------------------------------------------------
    // Creation method for element: 'Data Access Layer Interface'
    // ----------------------------------------------------------------------------------------------------
    public void IDataAccessLayer() {
    
    	try {
    
    		Source.CurrentTemplate.ExecutionStatusMessage = "Code Generation succeed. ";
    		Response.WriteLine("//  File Name: " + IDataAccessLayer_GetName());
    		Response.WriteLine("//  Code Generated By: " + DynamicParameters.DeveloperAlias);
    		Response.WriteLine("//  Code Generated On: " + System.DateTime.Now.ToShortDateString());
    		Response.WriteLine();
    		Response.WriteLine("using System;");
    		Response.WriteLine("using System.Data;");
    		Response.WriteLine();
    		Response.WriteLine("namespace " + DynamicParameters.CompanyName + ".Data");
    		Response.WriteLine("{");
    		Response.WriteLine("\tpublic interface IDataAccessLayer");
    		Response.WriteLine("\t{");
    		
    		Response.WriteLine("\t\tstring ConnectionString {get;}");
    		Response.WriteLine("\t\tbool WithTransaction {get;}");
    		StoredProcedureCollection Procedures = Source.SelectedStoredProcedures;
    		foreach (StoredProcedure SP in Procedures)
    		{
    			string ReturnType;
    			if (SP.ReturnsData == true)
    			{
    				if (SP.Resultsets.Count > 1)
    					ReturnType = "DataSet";
    				else
    					ReturnType = "DataTable";
    			}
    			else
    			{
    				ReturnType = "void";
    			}
    
    			ParameterCollection Parameters = SP.Parameters;
    			string ParameterString = "";
    			foreach (Parameter P in Parameters)
    			{
    				string Direction = "";
    				if (P.Direction == ParameterDirection.Output)
    				{
    					Direction = "out ";
    				}
    				else if (P.Direction == ParameterDirection.ReturnValue)
    				{
    					continue;
    				}
    				if (ParameterString != "")
    				{
    					ParameterString += ",";
    				}
    				string DotNetName = P.Name;
    
    				if (DotNetName.Substring(0,1) == "@")
    				{
    					DotNetName = DotNetName.Remove(0,1);
    				}
    
    				ParameterString += Direction + P.DotNetType.CommonTypeSystemEquivalent + " " + DotNetName;
    			}
    
    			Response.WriteLine("\t\t" + ReturnType + " " + SP.Name.Replace(" ", "_") + "(" + ParameterString + ");");
    		}
    
    		Response.WriteLine("\t}");
    		Response.WriteLine("}");
    	}
    	catch (System.Exception GlobalException) {
    
    		Source.CurrentTemplate.ExecutionStatusCode = GenerationStatus.Error;
    		Source.CurrentTemplate.ExecutionStatusMessage = GlobalException.Message;
    
    		Response.WriteLine();
    		Response.WriteLine("*************************");
    		Response.WriteLine(" AN EXCEPTION WAS THROWN");
    		Response.WriteLine("*************************");
    		Response.WriteLine(GlobalException.Message);
    		Response.WriteLine();
    		Response.WriteLine("****************");
    		Response.WriteLine(" FULL EXCEPTION");
    		Response.WriteLine("****************");
    		Response.WriteLine(GlobalException.ToString());
    		if (Source.DesignMode) {
    			System.Windows.Forms.MessageBox.Show(GlobalException.ToString(), "An error has occured", System.Windows.Forms.MessageBoxButtons.OK, System.Windows.Forms.MessageBoxIcon.Error);
    		}
    		else {
    			throw;
    		}
    	}
    }
    
    // ----------------------------------------------------------------------------------------------------
    // GetName function for element: 'Data Access Layer Basic Implementation'
    // ----------------------------------------------------------------------------------------------------
    public string DataAccessLayer_GetName() {
    
    	// Choose whatever name you wish for this object
    	return (@"DataAccessLayer.cs");
    
    }
    
    // ----------------------------------------------------------------------------------------------------
    // Creation method for element: 'Data Access Layer Basic Implementation'
    // ----------------------------------------------------------------------------------------------------
    public void DataAccessLayer() {
    
    	try {
    
    		Source.CurrentTemplate.ExecutionStatusMessage = "Code Generation succeed. ";
    		Response.WriteLine("//  File Name: " + DataAccessLayer_GetName());
    		Response.WriteLine("//  Code Generated By: " + DynamicParameters.DeveloperAlias);
    		Response.WriteLine("//  Code Generated On: " + System.DateTime.Now.ToShortDateString());
    		Response.WriteLine();
    		Response.WriteLine("using System;");
    		Response.WriteLine("using System.Data;");
    		Response.WriteLine("using System.Data.SqlClient;");
    		Response.WriteLine("using Microsoft.ApplicationBlocks.Data;");
    		Response.WriteLine("using " + DynamicParameters.CompanyName + ".Configure;");
    		Response.WriteLine();
    		Response.WriteLine("namespace " + DynamicParameters.CompanyName + ".Data");
    		Response.WriteLine("{");
    		Response.WriteLine("\tpublic class DataAccessLayer : IDataAccessLayer");
    		Response.WriteLine("\t{");
    		Response.WriteLine("\t\t// connection string of SQL database");
    		Response.WriteLine("\t\tprivate string _ConnectionString = \"\";");
    		Response.WriteLine("\t\t// transaction option");
    		Response.WriteLine("\t\tprivate bool _WithTransaction = false;");
    		Response.WriteLine("\t\t// connection");
    		Response.WriteLine("\t\tprivate SqlConnection _Connection = null;");
    		Response.WriteLine("\t\t// transaction");
    		Response.WriteLine("\t\tprivate SqlTransaction _Transaction = null;");
    		Response.WriteLine();
    		Response.WriteLine("\t\t// get connection string");
    		Response.WriteLine("\t\tpublic string ConnectionString");
    		Response.WriteLine("\t\t{");
    		Response.WriteLine("\t\t\tget {return _ConnectionString;}");
    		Response.WriteLine("\t\t}");
    		Response.WriteLine();
    		Response.WriteLine("\t\t// get or set transaction option");
    		Response.WriteLine("\t\tpublic bool WithTransaction");
    		Response.WriteLine("\t\t{");
    		Response.WriteLine("\t\t\tget {return _WithTransaction;}");
    		Response.WriteLine("\t\t}");
    		Response.WriteLine();
    
    		Response.WriteLine("\t\t// construction with connection string");
    		Response.WriteLine("\t\tpublic DataAccessLayer(string Connection)");
    		Response.WriteLine("\t\t{");
    		Response.WriteLine("\t\t\t_ConnectionString = Connection;");
    		Response.WriteLine("\t\t}");
    		Response.WriteLine();
    		Response.WriteLine("\t\t// construction with configure");
    		Response.WriteLine("\t\tpublic DataAccessLayer(IConfigure Config)");
    		Response.WriteLine("\t\t{");
    		Response.WriteLine("\t\t\t_ConnectionString = Config.GetConnectionString();");
    		Response.WriteLine("\t\t}");
    		Response.WriteLine();
    		Response.WriteLine("\t\t// construction with connection, caller will manage the connection");
    		Response.WriteLine("\t\tpublic DataAccessLayer(SqlConnection Connection)");
    		Response.WriteLine("\t\t{");
    		Response.WriteLine("\t\t\tif (Connection == null)");
    		Response.WriteLine("\t\t\t\tthrow new Exception(\"Connection cannot be null.\");");
    		Response.WriteLine("\t\t\t_Connection = Connection;");
    		Response.WriteLine("\t\t}");
    		Response.WriteLine();
    		Response.WriteLine("\t\t// construction with transaction, caller will manage the connection and transaction");
    		Response.WriteLine("\t\tpublic DataAccessLayer(SqlTransaction Transaction)");
    		Response.WriteLine("\t\t{");
    		Response.WriteLine("\t\t\tif (Transaction == null)");
    		Response.WriteLine("\t\t\t\tthrow new Exception(\"Transaction cannot be null.\");");
    		Response.WriteLine("\t\t\t_Connection = Transaction.Connection;");
    		Response.WriteLine("\t\t\t_ConnectionString = _Connection.ConnectionString;");
    		Response.WriteLine("\t\t\t_Transaction = Transaction;");
    		Response.WriteLine("\t\t\t_WithTransaction = true;");
    		Response.WriteLine("\t\t}");
    		Response.WriteLine();
    
    		StoredProcedureCollection Procedures = Source.SelectedStoredProcedures;
    		foreach (StoredProcedure SP in Procedures)
    		{
    			string ReturnType;
    			if (SP.ReturnsData == true)
    			{
    				if (SP.Resultsets.Count > 1)
    					ReturnType = "DataSet";
    				else
    					ReturnType = "DataTable";
    			}
    			else
    			{
    				ReturnType = "void";
    			}
    
    			ParameterCollection Parameters = SP.Parameters;
    			string ParameterString = "";
    			string SqlDBParameterString = "";
    			int TrueParameter = 0;
    			foreach (Parameter P in Parameters)
    			{
    				string Direction = "";
    				if (P.Direction == ParameterDirection.Output)
    				{
    					Direction = "out ";
    				}
    				else if (P.Direction == ParameterDirection.ReturnValue)
    				{
    					continue;
    				}
    				if (ParameterString != "")
    				{
    					ParameterString += ",";
    					SqlDBParameterString += ",";
    				}
    				string DotNetName = P.Name;
    
    				if (DotNetName.Substring(0,1) == "@")
    				{
    					DotNetName = DotNetName.Remove(0,1);
    				}
    
    				ParameterString += Direction + P.DotNetType.CommonTypeSystemEquivalent + " " + DotNetName;
    				SqlDBParameterString += P.Name + " " + P.SqlFullType;
    				TrueParameter ++;
    			}
    			Response.WriteLine("\t\t// Execute stored procedure:");
    			Response.WriteLine("\t\t//     " + SP.Name + "(" + SqlDBParameterString + ")");
    			Response.WriteLine("\t\tpublic " + ReturnType + " " + SP.Name.Replace(" ", "_") + "(" + ParameterString + ")");
    			Response.WriteLine("\t\t{");
    			if (SP.ReturnsData == true)
    				Response.WriteLine("\t\t\tDataSet ds = null;");
    			Response.WriteLine();
    			Response.WriteLine("\t\t\ttry");
    			Response.WriteLine("\t\t\t{");
    			if (TrueParameter > 0)
    			{
    				Response.WriteLine("\t\t\t\tSqlParameter[] Params = new SqlParameter[" + TrueParameter + "];");
    				Response.WriteLine();
    				int i = 0;
    				foreach (Parameter P in Parameters)
    				{
    					if (P.Direction == ParameterDirection.ReturnValue)
    					{
    						continue;
    					}
    					string DotNetName = P.Name;
    
    					if (DotNetName.Substring(0,1) == "@")
    					{
    						DotNetName = DotNetName.Remove(0,1);
    					}
    
    					Response.WriteLine("\t\t\t\tParams[" + i + "] = new SqlParameter(\"" + P.Name+ "\",  "+ P.DotNetType.FullSqlDbTypeEquivalent + ", " + P.Length+ ");");
    					Response.WriteLine("\t\t\t\tParams[" + i + "].Value = " + DotNetName +";");
    					if (P.Direction == ParameterDirection.Output)
    						Response.WriteLine("\t\t\t\tParams[" + i + "].Direction = ParameterDirection.Output;");
    					Response.WriteLine();
    					i++;
    				}
    			}
    			string MethodName = "";
    			string ReturnDataSet = "";
    			if (SP.ReturnsData == true)
    			{
    				MethodName = "ExecuteDataset";
    				ReturnDataSet = "ds = ";
    			}
    			else
    			{
    				MethodName = "ExecuteNoQuery";
    			}
    			string ParamString = "";
    			if (TrueParameter > 0)
    			{
    				ParamString = ", Params";
    			}
    			Response.WriteLine("\t\t\t\tif (_Transaction != null)");
    			Response.WriteLine("\t\t\t\t{");
    			Response.WriteLine("\t\t\t\t\t" + ReturnDataSet+ "SqlHelper." + MethodName + "(_Transaction, CommandType.StoredProcedure, \"" + SP.Name + "\"" + ParamString + ");");
    			Response.WriteLine("\t\t\t\t}");
    			Response.WriteLine("\t\t\t\telse if (_Connection != null)");
    			Response.WriteLine("\t\t\t\t{");
    			Response.WriteLine("\t\t\t\t\t" + ReturnDataSet+ "SqlHelper." + MethodName + "(_Connection, CommandType.StoredProcedure, \"" + SP.Name + "\"" + ParamString + ");");
    			Response.WriteLine("\t\t\t\t}");
    			Response.WriteLine("\t\t\t\telse");
    			Response.WriteLine("\t\t\t\t{");
    			Response.WriteLine("\t\t\t\t\t" + ReturnDataSet+ "SqlHelper." + MethodName + "(_ConnectionString, CommandType.StoredProcedure, \"" + SP.Name + "\"" + ParamString + ");");
    			Response.WriteLine("\t\t\t\t}");
    			if (SP.ReturnsData == true)
    			{
    				if (SP.Resultsets.Count > 1)
    					Response.WriteLine("\t\t\t\treturn ds;");
    				else
    					Response.WriteLine("\t\t\t\treturn ds.Tables[0];");
    			}
    			else
    				Response.WriteLine("\t\t\t\treturn;");
    			Response.WriteLine("\t\t\t}");
    			Response.WriteLine("\t\t\tcatch(Exception ex)");
    			Response.WriteLine("\t\t\t{");
    			Response.WriteLine("\t\t\t\tthrow new Exception(\"Cannot execute the stored procedure.\", ex);");
    			Response.WriteLine("\t\t\t}");
    			Response.WriteLine("\t\t}");
    			Response.WriteLine();
    		}
    
    		Response.WriteLine("\t}");
    		Response.WriteLine("}");
    
    	}
    	catch (System.Exception GlobalException) {
    
    		Source.CurrentTemplate.ExecutionStatusCode = GenerationStatus.Error;
    		Source.CurrentTemplate.ExecutionStatusMessage = GlobalException.Message;
    
    		Response.WriteLine();
    		Response.WriteLine("*************************");
    		Response.WriteLine(" AN EXCEPTION WAS THROWN");
    		Response.WriteLine("*************************");
    		Response.WriteLine(GlobalException.Message);
    		Response.WriteLine();
    		Response.WriteLine("****************");
    		Response.WriteLine(" FULL EXCEPTION");
    		Response.WriteLine("****************");
    		Response.WriteLine(GlobalException.ToString());
    		if (Source.DesignMode) {
    			System.Windows.Forms.MessageBox.Show(GlobalException.ToString(), "An error has occured", System.Windows.Forms.MessageBoxButtons.OK, System.Windows.Forms.MessageBoxIcon.Error);
    		}
    		else {
    			throw;
    		}
    	}
    }
    
    // ----------------------------------------------------------------------------------------------------
    // GetName function for element: 'Configuration Interface'
    // ----------------------------------------------------------------------------------------------------
    public string IConfigure_GetName() {
    
    	// Choose whatever name you wish for this object
    	return (@"IConfigure.cs");
    
    }
    
    // ----------------------------------------------------------------------------------------------------
    // Creation method for element: 'Configuration Interface'
    // ----------------------------------------------------------------------------------------------------
    public void IConfigure() {
    
    	try {
    
    		Source.CurrentTemplate.ExecutionStatusMessage = "Code Generated Succeed.";
    		Response.WriteLine("//  File Name: " + IConfigure_GetName());
    		Response.WriteLine("//  Code Generated By: " + DynamicParameters.DeveloperAlias);
    		Response.WriteLine("//  Code Generated On: " + System.DateTime.Now.ToShortDateString());
    		Response.WriteLine();
    		Response.WriteLine("using System;");
    		Response.WriteLine();
    		Response.WriteLine("namespace " + DynamicParameters.CompanyName + ".Configure");
    		Response.WriteLine("{");
    		Response.WriteLine("\tpublic interface IConfigure");
    		Response.WriteLine("\t{");
    		Response.WriteLine("\t\tstring GetConnectionString();");
    		Response.WriteLine("\t}");
    		Response.WriteLine("}");
    
    	}
    	catch (System.Exception GlobalException) {
    
    		Source.CurrentTemplate.ExecutionStatusCode = GenerationStatus.Error;
    		Source.CurrentTemplate.ExecutionStatusMessage = GlobalException.Message;
    
    		Response.WriteLine();
    		Response.WriteLine("*************************");
    		Response.WriteLine(" AN EXCEPTION WAS THROWN");
    		Response.WriteLine("*************************");
    		Response.WriteLine(GlobalException.Message);
    		Response.WriteLine();
    		Response.WriteLine("****************");
    		Response.WriteLine(" FULL EXCEPTION");
    		Response.WriteLine("****************");
    		Response.WriteLine(GlobalException.ToString());
    		if (Source.DesignMode) {
    			System.Windows.Forms.MessageBox.Show(GlobalException.ToString(), "An error has occured", System.Windows.Forms.MessageBoxButtons.OK, System.Windows.Forms.MessageBoxIcon.Error);
    		}
    		else {
    			throw;
    		}
    	}
    }
    *>
    
    December 06

    Close a project in EPM

    EPM system does not have ability to manage projects in all lifecycle.  It can only manage a project when it is ongoing.  When a project is finished, it does not have an indicator, and most important, it cannot remove the project from project list.  As the result, project manager can still open the project, modify the project plan, and etc.
     
    One way to avoid this is to remove the project from EPM system.  However, we may need the project data for statistic.  There is one way we can keep the project in EPM.  Portfolio manager can view the data in OLAP cube, but the project manager cannot modify it, and team member cannot put any hours against project.  To do so, we need:
     
    1. Create a custom outline code in EPM global.  This will act as an indicator to show the project status.
    2. Open every project in Project Professional set the value of the custom outline code and republishes it.
    3. Create a security, Closed Projects, which disables projects in the list for opening and viewing.
    4. Open the finished projects in Project Professional to change the resource from committed to purposed.
    5. Put those finished project in the Closed Projects.
     
    Then all finished projects will be removed from timesheet, project center, and project professional.  However, for each finished project, it requires administrator to add it in the security.  To improve it, there are several solutions:
     
    1. Create a custom page in admin page of the PWA, which has a data grid to show all projects with closed status.  Then administrator can select from the list, then click button to close the project.
    2. Create a SQL agent that execute periodically, which close all projects with closed status.
    3. Create a SQL trigger that execute immediately when project manager saves the project.  If the project manager set the project status to be closed, then the trigger will close the project.

    Extend the OLAP cube for week in Time dimension

    By default, the OLAP cube of project server does not have week in time dimension.  However, in real world, week is important.  To add week into time dimension, we need to extend the standard OLAP cube building process.
     
    In fact, the MSP_CUBE_TIME_BY_DAY table has already had week number in it.  We can use it to create a new level in time dimension.  However, to have more friendly interface, you may create additional column in MSP_CUBE_TIME_BY_DAY table as the display of the week level. 
     
    To extend the week, you need to do the following:
    1. For all physical cube, delete time dimension if used and remember all the cube name.
    2. For all virtual cube, delete time dimension if used and remember all the cube name.
    3. Delete the time dimension
    4. Create a new time dimension and create six levels of it.
    5. Add the time dimension into the physical cube of step 1.
    6. Add the time dimension into the virtual cube of step 2.
     
    There is one thing you may need to know.  When you remove or add a dimension from a physical cube, you need to reconstruct the join clause. 
     
    Here is the source code in VB6 for SQLServer 2000 database.  If your database is SQLServer 2005, it may be a little different.
     
    ' Modify the time dimension to insert week of month.
    Public Function ModifyTimeDimension(dsoDatabase As DSO.MDStore)
        Dim dsoDimension As DSO.Dimension
        Dim dsoLevelAll As DSO.DbLevel
        Dim dsoLevel As DSO.Level
        Dim strDimensionName As String
        Dim strDimensionTableName As String
        Dim dsoCube As DSO.MDStore
        Dim DicCubes As Dictionary
        Dim fSuccess As Boolean
        Dim jcJoin As JoinClauseManager
        Dim Condition As JoinClause
        Dim nPos  As Integer
       
        fSuccess = False
       
        On Error GoTo LblErrorHandler
       
        strDimensionName = "Time"
        strDimensionTableName = "MSP_CUBE_TIME_BY_DAY"
       
        Set DicCubes = New Dictionary
        Set jcJoin = New JoinClauseManager
       
        If dsoDatabase.Dimensions.Find(strDimensionName) Then
            For nPos = 1 To dsoDatabase.MDStores.Count
                Set dsoCube = dsoDatabase.MDStores(nPos)
                If dsoCube.Dimensions.Find(strDimensionName) And dsoCube.SubClassType = sbclsRegular Then
                    Call DicCubes.Add(dsoCube.Name, dsoCube.Name)
                    dsoCube.Dimensions.Remove (strDimensionName)
                   
                    Call jcJoin.LoadJoinClause(dsoCube.JoinClause)
                    Call jcJoin.RemoveJoinCondition(TrimQuate(dsoCube.SourceTable), "TIME_ID", strDimensionTableName, "TIME_ID")
                    Call jcJoin.RemoveJoinCondition(TrimQuate(dsoCube.SourceTable), "TIME_DATE", strDimensionTableName, "TIME_DATE")
                    dsoCube.JoinClause = jcJoin.GetJoinClause
                    dsoCube.Update
                    dsoCube.Process processFull
               End If
            Next nPos
           
            For nPos = 1 To dsoDatabase.MDStores.Count
                Set dsoCube = dsoDatabase.MDStores(nPos)
                If dsoCube.Dimensions.Find(strDimensionName) And dsoCube.SubClassType = sbclsVirtual Then
                    Call DicCubes.Add(dsoCube.Name, dsoCube.Name)
                    dsoCube.Dimensions.Remove (strDimensionName)
                   
                    dsoCube.Update
                    dsoCube.Process processFull
               End If
            Next nPos
            dsoDatabase.Dimensions.Remove (strDimensionName)
        End If
       
        Set dsoDimension = dsoDatabase.Dimensions.AddNew(strDimensionName, sbclsRegular)
        
        Set dsoDimension.DataSource = dsoDatabase.DataSources(1)
        dsoDimension.FromClause = """" & strDimensionTableName & """"
       
        Set dsoLevelAll = dsoDimension.Parent.Parent.CreateObject(clsDatabaseLevel)
           
        dsoLevelAll.Name = "All"
        dsoLevelAll.IsAll = True
       
        If dsoDimension.Levels.Count > 0 Then
            Call dsoDimension.Levels.Add(dsoLevelAll, , 1)
        Else
            Call dsoDimension.Levels.Add(dsoLevelAll)
        End If
         
        dsoLevelAll.MemberKeyColumn = "All Years"
        dsoLevelAll.LevelType = levAll
       
        Set dsoLevel = dsoDimension.Levels.AddNew("Year", sbclsRegular)
        dsoLevel.MemberKeyColumn = """" & strDimensionTableName & """.""" & "TIME_YEAR"""
        dsoLevel.MemberNameColumn = """" & strDimensionTableName & """.""" & "TIME_YEAR"""
        dsoLevel.LevelType = levTimeYears
        dsoLevel.columnType = adSmallInt
       
        Set dsoLevel = dsoDimension.Levels.AddNew("Quarter", sbclsRegular)
        dsoLevel.MemberKeyColumn = """" & strDimensionTableName & """.""" & "TIME_QTR"""
        dsoLevel.MemberNameColumn = """" & strDimensionTableName & """.""" & "TIME_QTR"""
        dsoLevel.LevelType = levTimeQuarters
        dsoLevel.columnType = adWChar
       
        Set dsoLevel = dsoDimension.Levels.AddNew("Month", sbclsRegular)
        dsoLevel.MemberKeyColumn = """" & strDimensionTableName & """.""" & "TIME_MONTH_OF_YEAR"""
        dsoLevel.MemberNameColumn = """" & strDimensionTableName & """.""" & "TIME_MONTH"""
        dsoLevel.LevelType = levTimeMonths
        dsoLevel.columnType = adSmallInt
        dsoLevel.Ordering = orderKey
       
        Set dsoLevel = dsoDimension.Levels.AddNew("Week", sbclsRegular)
        dsoLevel.MemberKeyColumn = """" & strDimensionTableName & """.""" & "TIME_WEEK_OF_YEAR"""
        dsoLevel.MemberNameColumn = """" & strDimensionTableName & """.""" & "TIME_WEEK"""
        dsoLevel.LevelType = levTimeWeeks
        dsoLevel.columnType = adSmallInt
        dsoLevel.Ordering = orderKey
       
        Set dsoLevel = dsoDimension.Levels.AddNew("Days", sbclsRegular)
        dsoLevel.MemberKeyColumn = """" & strDimensionTableName & """.""" & "TIME_DAY_OF_WEEK"""
        dsoLevel.MemberNameColumn = """" & strDimensionTableName & """.""" & "TIME_DAY_OF_MONTH"""
        dsoLevel.LevelType = levTimeDays
        dsoLevel.columnType = adSmallInt
        dsoLevel.Ordering = orderKey
       
        'Update and process the dimension.
        dsoDimension.Update
        dsoDimension.Process (processFull)
       
        For Each dsoCube In dsoDatabase.MDStores
            If DicCubes.Exists(dsoCube.Name) And dsoCube.SubClassType = sbclsRegular Then
                dsoCube.Dimensions.AddNew (strDimensionName)
                Call jcJoin.LoadJoinClause(dsoCube.JoinClause)
                Call jcJoin.AddJoinCondition(TrimQuate(dsoCube.SourceTable), "TIME_ID", strDimensionTableName, "TIME_ID")
                Call jcJoin.AddJoinCondition(TrimQuate(dsoCube.SourceTable), "TIME_DATE", strDimensionTableName, "TIME_DATE")
                dsoCube.JoinClause = jcJoin.GetJoinClause
                dsoCube.Update
                dsoCube.Process (processFull)
            End If
        Next dsoCube
       
        For Each dsoCube In dsoDatabase.MDStores
            If DicCubes.Exists(dsoCube.Name) And dsoCube.SubClassType = sbclsVirtual Then
                dsoCube.Dimensions.AddNew (strDimensionName)
                dsoCube.Update
                dsoCube.Process (processFull)
            End If
        Next dsoCube
       
        fSuccess = True
       
    LblErrorHandler:
      
       Set dsoDimension = Nothing
       Set dsoLevelAll = Nothing
       Set dsoLevel = Nothing
       Set dsoCube = Nothing
      
       If (Not fSuccess) Then
          Call TraceAndRaise("Error in ModifyTimeDimension.")
       End If
       
    End Function

    Synchronize All

    PWA provides a function to synchronize the users between project server and SharePoint server, however, it only allow the administrator to synchronize one project at a time.  If administrator wants to synchronize multiply projects or all projects, it would be time consuming.
     
    Fortunately, PWA is an extendable framework, that we can extend PWA to support Synchronize multiple projects at a time.  To extend the PWA for synchronizing all projects, we need to do:
    1. Modify the ManageSTS.ASP page
        a. Add a button in the toolbar.
        b. Add a form with a hidden field in the page.
        c. Add an event handler for the button.
    2. Copy the SyncUpUsers.ASP page as SyncUpAllUsers.ASP, and modify it to send a request for all projects.  The original page can send one request, we only need to add a loop in the code so it can send multiply requests.

    Extend OLAP cube for Task - create virtual cube

    Microsoft provides a document and source code for extending OLAP cube for task data, however, they did not mention two things which are important for viewing a cube.  One is to add all role in database level to cube level, Victor has already discussed it in his article.  Another thing is to create virtual cube with project cube and resource cube.  I will discuss it here.
     
    Custom task cube only contains the task information, not associated with projects and resources.  Normally people like to view data with project information, even resource.  We need to create virtual cube for it. 
     
    Virtual cube, like view in database, is a logical structure that itself contains no data.  In this extension, we will create two virtual cube, MSP_TASK_PORTFOLIO, which is based on task cube and project cube.  Other is MSP_PORTFOLIO, which is based on all three physical cubes.  The following is the sample code to create virtual cube:
     
    Private Sub createPortfolioVirtualCube(dsoDatabase As DSO.MDStore)
        Dim dsoVirtualCube As DSO.MDStore
        Dim dsoAssnCube As DSO.MDStore
        Dim dsoTaskCube As DSO.MDStore
        Dim dsoResCube As DSO.MDStore
        Dim dsoDim As DSO.Dimension
        Dim dsoMea As DSO.Measure
        Dim dsoNewMea As DSO.Measure
       
        Set dsoVirtualCube = dsoDatabase.MDStores.AddNew(VIRTUAL_PORTFOLIO, sbclsVirtual)
        Set dsoAssnCube = dsoDatabase.MDStores(ASSN_CUBE)
        Set dsoTaskCube = dsoDatabase.MDStores(TASK_CUBE)
        Set dsoResCube = dsoDatabase.MDStores(RES_CUBE)
        For Each dsoDim In dsoTaskCube.Dimensions
            dsoVirtualCube.Dimensions.AddNew (dsoDim.Name)
        Next
        For Each dsoDim In dsoAssnCube.Dimensions
            If dsoVirtualCube.Dimensions(dsoDim.Name) Is Nothing Then
                dsoVirtualCube.Dimensions.AddNew (dsoDim.Name)
            End If
        Next
        For Each dsoDim In dsoResCube.Dimensions
            If dsoVirtualCube.Dimensions(dsoDim.Name) Is Nothing Then
                dsoVirtualCube.Dimensions.AddNew (dsoDim.Name)
            End If
        Next
        For Each dsoMea In dsoTaskCube.Measures
            Set dsoNewMea = dsoVirtualCube.Measures.AddNew(dsoMea.Name)
            dsoNewMea.SourceColumn = "[" & TASK_CUBE & "].[" & dsoMea.Name & "]"
        Next
        dsoVirtualCube.Update
        dsoVirtualCube.Process
    End Sub

    Task Flag can roll up to Project Flag

    Custom Flag field is different from other custom fields, since the task flag field can roll up to project flag field.  For example, if you set task flag 1 to roll up to summary task, then the project flag 1 will be changed by task flag settings.