Wednesday, October 20, 2010

FluentPS v1.2 release – Easy Project Server customizations deployment

Hello everyone,

After some delay in tracking our progress with FluentPS library, we are making this delivery notice – new version 1.2 has come! Although detailed list of changes you can find on http://fluentps.codeplex.com, in this post you can find overview of new features of FluentPS.

Assume you are working on customization of Project Server and have to extend it with a couple of tens of project/task/resource/assignment custom fields, lookup tables, security groups, security categories, workflow phases and stages. Let’s add you have to maintain several Project Server environments (development, test, staging or production ones). In this case the manual deployment of Project Server entities becomes painful. Moreover, there is a high chance to forget something, or simply mistype in some object’s name. And the very first thing that comes into your mind is to automate the deployment procedure. Here is where the new FluentPS.Migrator.PSMigrator class can help.

Deploying Custom Fields

Let’s say you have to extend Project Server projects with text custom field. In this case you have to create project class containing the property marked by PSCustomField attribute (later you can use this entity to perform READ/UPDATE operations – this was covered in previous posts). It looks something like this:

[PSEntityType(PSEntityType.Project)]
public class Project
{
    [PSEntityUidField(ColumnName = ProjectCustomFieldsColumnNames.PROJ_UID)]
    public Guid Uid { get; set; }

    [PSNativeField(ColumnName = ProjectCustomFieldsColumnNames.PROJ_NAME)]
    public string Name { get; set; }

    [PSCustomField("{54A30208-4DE7-408a-987E-02AB94FBFFAD}",
                   FieldName = "Additional Info",
                   Description = "Project Additional Info",
                   IsMultiline = true)]
    public string AdditionalInfo { get; set; }
}

Now this class can be used for deployment custom fields. The code looks as following:

var psMigrator = new FluentPS.Migrator.PSMigrator(logService,
                                                  customFieldService,
                                                  environmentService,
                                                  lookupTableService,
                                                  securityService);
psMigrator.CreateCustomFields<Project>();

And that’s it!

NOTE: the GUID of custom filed set in the PSCustomField attribute is used not only for reading/updating Project Server entities, but also for deploying their custom fields. There is one influential reason to stick to those GUIDs instead of custom fields’ names – those names can be manually changed from the UI, and this may affect the system.

Deploying of Lookup Tables

The PSLookupTableService class was extended to support this purpose. You’ll have to use new PSLookupTableInfo and PSLookupValueInfo classes to define lookup tables you need. Please check out "LookupTables" sample project for code sample.

Deploying Security Groups and Security Categories

The new PSSecurityService class allows you to manipulate security groups and categories. You’ll have to use new PSSecurityGroupInfo, PSSecurityCategoryInfo and PSSecurityCategoryInfo.GroupPermissions classes to describe those objects. Please check out sources for code sample.

Read more...

Monday, August 30, 2010

Programmatic Deployment of SSRS reports with strored credentials

When you face the necessity of adding reporting to your application, you have to choose the way how to transform your data into some readable format. And SQL Server Reporting Services (SSRS) might be the case. In this post we’ll show the way how to programmatically deploy SSRS reports.

Suppose you’ve properly configured SSRS (in SharePoint Integrated mode in our case) and developed one report in Visual Studio; all report’s related data sources (either Shared or Custom) run through stored credentials. Now you have to:

  • deploy report definition;
  • adjust and/or deploy related Data Sources (regardless of which type of Data Source you chose, you must change connection strings and user login/password for accessing data from the database).

To run the code below you have to create appropriate application (depending on your needs) and add reference to ReportService2010.asmx web service. Here are two methods used from deploying reports:

public void DeployReport(
            string serverUrl,
            string docLibraryPath,
            string reportFilePath,
            Dictionary<string, DataSourceDefinition> dsDefinitions)
        {
            //create report service instance
            var _rsService = new ReportingService2010
                                 {
                                     Url = string.Format("{0}/_vti_bin/ReportServer/ReportService2010.asmx", serverUrl),
                                     Credentials = System.Net.CredentialCache.DefaultCredentials
                                 };
            //read content from report file
            byte[] reportContent;
            using (var reader = new StreamReader(reportFilePath))
                using (var br = new BinaryReader(reader.BaseStream))
                    reportContent = br.ReadBytes((int) reader.BaseStream.Length);

            //change connections strings in report definition
            reportContent = ChangeConnectionStrings(reportContent, dsDefinitions);

            Warning[] warnings;
            var report = _rsService.CreateCatalogItem(
                "Report",
                new FileInfo(reportFilePath).Name,
                docLibraryPath,
                true,
                reportContent,
                null,
                out warnings);

            //fix datasource definitions
            var dataSources = _rsService.GetItemDataSources(report.Path);
            dataSources
                .Where(_ => dsDefinitions.ContainsKey(_.Name))
                .ToList()
                .ForEach(ds =>
                             {
                                 var templateDS = dsDefinitions[ds.Name];

                                 if (ds.Item is DataSourceDefinition)
                                 {
                                     //set proper user credentials for custom report data source
                                     var dsd = (DataSourceDefinition) ds.Item;
                                     dsd.CredentialRetrieval = templateDS.CredentialRetrieval;
                                     dsd.UserName = templateDS.UserName;
                                     dsd.Password = templateDS.Password;
                                 }
                                 else
                                 {
                                     //create shared datasource and fix reference in report
                                     var newDS=_rsService.CreateDataSource(
                                         string.Format("{0}.rsds",ds.Name),
                                         docLibraryPath,
                                         true,
                                         templateDS,
                                         null);

                                     ds.Item = new DataSourceReference { Reference = newDS.Path };
                                 }
                                 _rsService.SetItemDataSources(report.Path, dataSources);
                             });
        }

        private byte[] ChangeConnectionStrings(
            byte[] reportContent,
            IDictionary<string, DataSourceDefinition> dsDefinitions)
        {
            var doc = XDocument.Load(XmlReader.Create(new MemoryStream(reportContent)));

            //find all connection strings in custom data sources
            //and replace connection strings with proper values
            doc.Descendants(XName.Get("ConnectString", doc.Root.Attributes().First(_ => _.IsNamespaceDeclaration && _.Name.Namespace == XNamespace.None).Value))
                .ToList()
                .ForEach(_ =>
                {
                    var dsName = _.Parent.Parent.Attribute("Name").Value;
                    if (dsDefinitions.ContainsKey(dsName))
                        _.SetValue(dsDefinitions[dsName].ConnectString);
                });

            var newStream = new MemoryStream();
            var xmlWriter = XmlWriter.Create(newStream);
            doc.WriteTo(xmlWriter);
            xmlWriter.Flush();
            return newStream.ToArray();
        }

And here is the example how to use those methods to deploy your reports:

DeployReport(
        "http://<server_name>",
        "http://<server_name>/<document_library_name>",
        "<report_file_path>",
        new Dictionary<string, DataSourceDefinition>
            {
                {
                    "<DataSet_1>",
                    new DataSourceDefinition
                        {
                            ConnectString = "Data Source=<database_server_>>;Initial Catalog=<database_name_2>",
                            CredentialRetrieval = CredentialRetrievalEnum.Store,
                            UserName = "<user_name_1>",
                            Password = "<password_1>",
                            Extension = "SQL"
                        }
                    },
                {
                    "<DataSet_2>",
                    new DataSourceDefinition
                        {
                            ConnectString = "Data Source=<database_server_2>;Initial Catalog=<database_name_2>",
                            CredentialRetrieval = CredentialRetrievalEnum.Store,
                            UserName = "<user_name_2>",
                            Password = "<password_2>",
                            Extension = "SQL"
                        }
                    }
            });
Replace all hardcodes in brackets to make the code working for you. Have fun! :)
Read more...

Monday, July 19, 2010

Enabling Forms Based Authentication for PWA site

There are two authentication types for SharePoint: Forms based and Windows based authentication. There are a lot of articles dedicated to configuring SharePoint to use either both of one of these authentication modes. For instance, this one. But in our post we would like to point out the way how to use Forms-based authentication for Project Server 2010 to enable non-domain users to log in to PWA site.

Suppose you’ve already configured new web application to use Forms Based authentication. Let’s say the name of ASP.NET Membership provider used for it is My_FBA_Provider, and the name of ASP.NET Role manager name is My_FBA_Role. Also suppose you have already created test user (let its login/password pair be test_user1/p@ssw0rd) and verified it’s workability for some test site collection belonging to your web application. Now let’s go through the following list of steps to allow our user to login to PWA.

1. Create PWA site.

Go to your Central Administration site and navigate to Application Management-> Manage service applications-> Project Server Service Application-> Create Project Web App Site. Here you should select proper SharePoint Web Application to host Project Web App – select the one which is configured for Forms Based authentication.


Fill the rest info and click OK button.

2. Add user to PWA.

After the PWA site is provisioned you can login to it using your admin Windows credentials. Navigate to your PWA site, select “Windows Authentication” option from the drop-down list and fill-in your administrator’s credentials to popup window.


Now go to Server Settings->Manage Users->New User page and fill-in “User logon account” field using the following template: FBA_PROVIDER_NAME:USER_NAME. As you can see on the screenshot below, in our case we have My_FBA_Provider:test_user1.


Fill-in the rest of information like you do for any domain user.

3. Login.

Now you can use test user credentials to login to your PWA site. Click “Sign in as Different User” menu item on the top of page, and select “Forms Authentication” option from the drop-down list:


After that, fill-in user’s login/password pair like you do for any web application with Forms Based authentication:


In this case neither domain nor FBA provider prefixes should be added to user name.


Now you can operate with new user like you do with any other domain user.

Read more...

Friday, July 2, 2010

Project Server 2010 Solution Starters

Hello everyone,

There is a new web-resource on http://code.msdn.microsoft.com/P2010SolutionStarter, which is dedicated to set of Solution Starters for Project Server 2010. These Solution Starters can be a good start for developing your own extensions for Project Server 2010, especially based on PSI communication and new SharePoint 2010 UI features, i.e. Ribbon, Status Messages etc.

Hopefully you’ll notice a couple of humble people on “People” tab who made their small contributions to Solution Starters development ;)

Have fun. Read more...

Tuesday, April 20, 2010

Project Server 2010 - PJContext

Hello everyone.

If you work with Project Server via PSI in web context (in your web parts or pages) it’s not necessary to create references to Project Server WCF services.

There is PJContext class in Microsoft.Office.Project.Server.PWA assembly. You just need to write one line of code to create an instance of service:

Microsoft.Office.Project.Server.WebServiceProxy.CustomFields customFieldsService = PJContext.Current.PSI.CustomFieldsWebService;

Now you can run service methods as you did that before with manually added service references:

var dsCustomFields = customFieldsService.ReadCustomFieldsByMdPropUids(mdPropUids, false);

PJContext has references to all PSI services (even to View and PWA services which are non-public). As far as all requests are run under current user account you should care about any impersonation issues in your application by yourself (see post about impersonation).

Have fun.

Read more...

Friday, March 26, 2010

Project Server 2010 - Accessing Intrinsic Fields

MS Project Server (PS) product allows to store and manage three main types of entities: Project, Resource and Task. These entities have out-of-box descriptive (instrinsic) fields, i.e., Name, Unique ID, Start Date etc. If the user needs to extend any of these entities with some additional attribute, it is possible to do this with Custom Field – the field related to the entity. This can be done manually through the PWA site or using the CustomFields.asmx PSI web service. The values stored in these fields can be shown on Project Details Page (PDP) with help of the Project Fields Part webpart. In Edit mode user can select which fields should be shown in it. The goal of this article is to show the way how to access the list of all project-related intrinsic and custom fields as it is done in the Project Fields Editor Part.

The CustomFields.asmx PSI web service allows users to access the list of all custom fields related to PS entity. But if the user needs to get the information regarding intrinsic field (to read its name, type, to check whether it is required etc.), he has to go other way. One of non-public PSI web services is named View.asmx. It can be reached by the following URL:

http://server_name/pwa_name/_vti_bin/psi/View.asmx?wsdl

The list of both intrinsic and custom fields can be received using ReadPortfolioFields web method. Anyway it won’t tell if the fields are globally required, so in case of custom field it is possible to use the CustomFields.asmx service for this purpose; in case of intrinsic fields you should hardcode them (Name and StartDate project native fields are required). For some reason the Description intrinsic field is not returned by ReadPortfolioFields method, so it has to be manually added to the results list.

Here is the code sample how these actions can be performed with the View PSI web service:

var pwaView = new View();
pwaView.Url = "http://server/pwa/_vti_bin/View.asmx";
pwaView.Credentials = System.Net.CredentialCache.DefaultCredentials;
using (var dsViewFields = _pwaView.ReadPortfolioFields())
{
    var requiredNativeFields = new List<Guid>
    {
        new Guid("{8452F6A8-BC6C-4367-8836-A6AE55BBDDA4}"),
        new Guid("{3317C796-5FA1-4231-9212-A4C90A2CE05F}")
    };

    var fields = dsViewFields.ViewFields.Rows
        .Cast<ViewFieldsDataSet.ViewFieldsRow>()
        .ToList()
        .Select(row =>
            new
            {
                Uid = row.WFIELD_UID,
                Name = !row.IsMD_PROP_NAMENull()
                       ? row.MD_PROP_NAME
                       : !row.IsCONV_STRINGNull()
                           ? row.CONV_STRING
                           : "UNKNOWN FIELD",
                IsGloballyRequired = requiredNativeFields.Contains(row.WFIELD_UID)
                                     || IsCustomAndGloballyRequired(row.WFIELD_UID)
            })
        .ToList();

    fields.Add(
        new
        {
            Uid = new Guid("837AAFA9-FA1A-49C0-8A08-6B007865991B"),
            Name = PJUtility.GetLocalizedString(
                "LIGHTWEIGHTPROJECTS_LWPDETAILS_DESCRIPTION_LABEL",
                 new object[0]),
            IsGloballyRequired = false
        });
}

The implementation of IsCustomAndGloballyRequired method is expected to use the CustomFields.asmx PSI web service. ReadCustomFieldsByEntity web method can be used there. Read more...

Tuesday, March 9, 2010

FluentPS Quick Start Videos created

Hello Everyone,

We've created a Quick Start video on how to use FluentPS library to operate with Project Server 2010 PSI (Project Server Interface). We'll show how to perform READ/UPDATE operations of project Native and Enterprise Custom fields with help of simple .NET class.

The source code and binaries of FluentPS project can be downloaded here: http://fluentps.codeplex.com

Quick Start video:


Enterprise Custom Fields mapping:


Read more...

Wednesday, March 3, 2010

Programmatic deployment of Workflow-controlled EPT

In the previous article we’ve shown the way how to programmatically create a Workflow Association. Here you can find the description of programmatic creation of Workflow-controlled EPT.

When you have deployed the Workflow Association, it is possible to create an EPT programmatically. This action can be performed in FeatureActivated overridden SharePoint feature method as well. The Workflow PSI web service should be used for this purpose. Here is the code sample:

public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
    var site = (SPSite)properties.Feature.Parent;
    var pwaWorkflow = new Workflow();
    pwaWorkflow.Url = string.format("{0}/_vti_bin/psi/Workflow.asmx"),site.Url);
    pwaWorkflow.Credentials = System.Net.CredentialCache.DefaultCredentials;

    using (var dsProjectType = pwaWorkflow.ReadEnterpriseProjectType(Guid.Empty))
    {
        WorkflowDataSet.EnterpriseProjectTypeRow row = 
dsProjectType.EnterpriseProjectType.NewEnterpriseProjectTypeRow();

        //Fill EPT Row
        ...
        var wfAssociationName = "[Your_WF_Association_Name]";
        row.WORKFLOW_ASSOCIATION_UID = GetWorkflowUidByName(wfAssociationName);
                row.WORKFLOW_ASSOCIATION_NAME = wfAssociationName;
        ...

        dsProjectType
            .EnterpriseProjectType
            .AddEnterpriseProjectTypeRow(row);

        pwaWorkflow.CreateEnterpriseProjectType(dsProjectType);
    }
}

private Guid GetWorkflowUidByName(Workflow pwaWorkflow, string workflowName)
{
    using (var dsWorkFlow = pwaWorkflow.ReadWorkflows())
    {
        return dsWorkFlow.WorkflowAssociation.Rows
            .Cast<WorkflowDataSet.WorkflowAssociationRow>()
            .First(row =>
                   row.WORKFLOW_ASSOCIATION_NAME == workflowName)
            .WORKFLOW_ASSOCIATION_UID;
    }
}

As it is shown in the code, you should use the ReadWorkflows method of Workflow.asmx web service to reach the ID of Workflow Association. Then you should set the WORKFLOW_ASSOCIATION_UID and WORKFLOW_ASSOCIATION_NAME properties of the EPT row. All other EPT properties should be filled out as usual.

IMPORTANT: If you have your EPT Workflow-controlled, it should have no PDPs attached. If you have to make existent EPT Workflow-controlled, you should first remove PDPs associations for this EPT, and then set WORKFLOW_ASSOCIATION_UID / WORKFLOW_ASSOCIATION_NAME properties.

Read more...

Tuesday, February 23, 2010

FluentPS v1.0 released

We are happy to announce the new FluentPS v1.0 release. The version is adopted and tested on ProjectServer post-Beta2 build (14.0.4730.1010 version).There are number of changes and features in the build:
  • FluentPS is moved from ASMX to WCF interface of the Project Server Interface (PSI)
  • Impersonation changes to work in compliance with WCF interface of PSI
  • Mapping of Nullable properties to PS Custom Fields
  • Solution is moved to VisualStudio 2010 Beta2
Source code and binary is available here: http://fluentps.codeplex.com.

Have fun! Read more...

Monday, February 22, 2010

SharePoint Server and Project Server 2010 Configuration

In the previous post we described SharePoint Server 2010 and Project Server 2010 installation process step-by-step and pointed out some potential issues you might encounter. Once you’ve got these two products installed in the server farm complete configuration you might have noticed that it’s still nothing been made related to SQL Server databases and nothing’s yet been configured.

In this post we will show how to configure SharePoint Server 2010 application, how to activate necessary services and create a PWA site.


SharePoint Server 2010 configuration.

Once Project Server 2010 installation is complete you’re prompted to run the SharePoint Products Configuration Wizard by leaving the corresponding check box in a checked state and pressing “Close” button. If for some reason you left the item unchecked or put off the product configuration you can easily launch it again at “Start -> Microsoft SharePoint 2010 Products –> SharePoint 2010 Products Configuration Wizard”.

By pressing the “Next” button some services are going to be will be restarted.


Next step choose “Create a new server farm” and press “Next”:


Next step you’ll be prompted to set an SQL Server instance name, SharePoint configuration database name and SQL Server account to proceed under:


Type a passphrase:


You could specify a port for a SharePoint Central Administration Web Application and change an authentication provider to Negotiate (Kerberos), but we would leave default settings:


The next step is a review of the settings to be applied:


Press the “Next” button to start configuration off:


Press the “Finish” button. IE will open SharePoint Central Administration page:


Creating a PWA site.

You'll be prompted to start a wizard to configure your SharePoint farm. You will need to choose which services will run in your farm. One of these is going to be Project Server Service Application.


You’re also prompted to choose existing managed account or create a new one. The account that is going to be specified with be used for “SharePoint - 80” and “SharePoint Central Administration v4” application pools.


Press “Next” button to activate chosen services.
Once the services are activated go to Central Administration -> Manage Service Applications -> Project Server Service Application.





In order to create a PWA site click “Create Project Web App Site” link.


Next step prompts you to choose PWA settings: hosting SharePoint Web Application, Project Web App path, Administrator account, DB names etc. Click “OK” and wait for PWA site to get created.


We noticed from time to time PWA creating fails, in most cases the “Retry” link helps.


When the PWA is created it becomes accessible at “http://servername/pwa”. If you would like it to be accessed at “http://localhost/pwa“ as well you should define an alternative URL at “Central Administration -> Application Management -> Configure alternate access mappings” section.

The last thing that should be done is changing the farm admin account. After installation the farm account is the same as the account you’re accessing the PWA site, you could notice that your name is “System Account” on the PWA site under. Changing the farm account “fixes” this and eventually PWA is going to recognize you correctly.

Proceed to “Central administration -> Security -> Configure service accounts”. Expand the dropdown “Select an account for this component”: if the account you need is not listed you should register it by clicking “Register new managed account”. Select “Farm account” from the dropdown at the top right corner; select the appropriate account below and then press OK button.

Read more...

Thursday, February 18, 2010

PS Project Workflow - Programmatic deployment of Workflow Association

One of the key features of MS Project Server (PS) is the ability to track its Project entity life-cycle with the workflow. The basic idea of this feature is to have several stages the project can go through. These stages can manage the accessibility of any particular Project Detail Page (PDP) for the project under the current stage; make its fields required or read-only; track Strategic Impact behavior for project business drivers.

In order to develop the Workflow for PS project it is necessary go through the following steps:

  1. construct the Sequence Workflow in MS Visual Studio Designer (check the http://msdn.microsoft.com/en-us/library/ee767686(office.14).aspx article);
  2. deploy workflow template, create a Workflow Association entity in PWA and Connect the particular PS Enterprise Project Type (EPT) to the newly created Workflow Association (check the http://msdn.microsoft.com/en-us/library/ee767699(office.14).aspx article).

Nevertheless, there is a way to automate workflow deployment process – deploy Workflow Association and Workflow-controlled EPT programmatically. In this post you can find the description of how to deploy a Workflow Association SharePoint feature Event Receiver.

It is possible to create a Workflow Association with help of custom code in FeatureActivated overridden SharePoint feature method. Here is the code sample:

public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
    var site = (SPSite)properties.Feature.Parent;
    using (var web = site.OpenWeb())
    {
        var info = new
                    {
                        Name = "[Your_WF_Association_Name]",
                        TemplateName = "[Your_WF_Template_Name]",
                        TaskListTitle = "[Your_Task_list_Title]",
                        HistoryListTitle = "[Your_istory_list_Title]",
                    };

        var workflowTemplate = web.WorkflowTemplates
            .OfType()
            .First(wfTemplate => wfTemplate.Name == info.TemplateName);

        for (int i = 0; i < web.WorkflowAssociations.Count; i++)
            if (web.WorkflowAssociations[i].Name == info.Name)
                web.WorkflowAssociations.Remove(web.WorkflowAssociations[i]);

        SPList taskList = GetListByNameAndBaseType(
                              web,
                              info.TaskListTitle,
                              SPListTemplateType.Tasks)
                          ?? GetFirstListByNameAndType(
                                 web,
                                 SPListTemplateType.Tasks);

        SPList historyList = GetListByNameAndBaseType(
                              web,
                              info.HistoryListTitle,
                               SPListTemplateType.WorkflowHistory)
                          ?? GetFirstListByNameAndType(
                                 web,
                                 SPListTemplateType.WorkflowHistory);

        SPWorkflowAssociation wfAssociation = SPWorkflowAssociation.CreateWebAssociation(
        workflowTemplate,
        info.Name,
        taskList, 
        historyList);

        web.WorkflowAssociations.Add(wfAssociation);
    }
}

private SPList GetFirstListByNameAndType(
    SPWeb web,
    SPListTemplateType listTemplateType)
{
    return web.Lists
        .OfType()
        .FirstOrDefault(list =>
                        list.BaseTemplate == listTemplateType);
}

private SPList GetListByNameAndBaseType(
    SPWeb web,
    string taskListTitle,
    SPListTemplateType listTemplateType)
{
    return web.Lists
        .Cast()
        .FirstOrDefault(list =>
                        list.BaseTemplate == listTemplateType
                        && list.Title == taskListTitle);
}

In this code there is an object named "info" that contains the following properties:

  1. Name – the name of the Association to be created;
  2. TemplateName – should be taken from the Name attribute of Workflow XML declaration node;
  3. TaskListTitle – the SharePoint list name to be used by this workflow association ("Project Server Workflow Tasks" value is used by default);
  4. HistoryListTitle – the SharePoint list name to be used by this workflow association ("Project Server Workflow History" value is used by default);

IMPORTANT. The Workflow Association should be added to the WorkflowAssociations collection of the Root Web of site collection.

In the next post we will show how to programmatically create Workflow-controlled EPT.

Read more...

Monday, February 8, 2010

SharePoint 2010 Ribbon Customization - Adding FlyoutAnchor control

In the previous posts we’ve described the way to add custom buttons, groups and tabs to the SharePoint 2010 Ribbon. In this post we’ll show the way of adding FlyoutAnchor control to the Ribbon – the control representing pull-down menu items list.

It is possible to deploy all the menu items either in FlyoutAnchor declaration or construct them dynamically with JavaScript. Here are the samples of both models.

FlyoutAnchor with static menu items

Here is the example of adding custom FlyoutAnchor control with static menu items:

<CommandUIDefinition Location="[Exisiting_Group_ID].Controls._children">
  <FlyoutAnchor
    Id="[Your_FlyoutAnchor_ID]"
    Command="[Your_FlyoutAnchor_Command_Name]"
    Sequence="10"
    Image32by32="/_layouts/images/RibbonCustomization/images32x32.png"
    Image32by32Top="0"
    Image32by32Left="-32"
    LabelText="FlyoutAnchor Static"
    TemplateAlias="o1"
    ToolTipTitle="FlyoutAnchor Static"
    ToolTipDescription="FlyoutAncor with static menu items">
    <Menu Id="[Your_FlyoutAnchor_ID].Menu">
      ...
      <MenuSection
        Id="[Section_ID]"
        Sequence="10"
        DisplayMode="Menu16">
        <Controls Id="[Section_ID].Controls">
          ...
          <Button
            Id="[Button_ID]"
            Command="[StaticButtonCommand_Name]"
            Sequence="10"
            Alt="Btn 1"
            LabelText="Static Button 1"/>
          ...
        </Controls>
      </MenuSection>
      ...
    </Menu>
  </FlyoutAnchor>
</CommandUIDefinition>

In order to add custom FlyoutAnchor control to the Ribbon you should declare it with all its menu sub-items. There is a way to split menu items into several groups by putting your menu items (buttons) to different MenuSection xml tags. The value of DisplayMode attribute (Menu16/Menu32) shows the size of menu items images – large or small.

If you specify the Command attribute of xml FlyoutAnchor tag, be sure to register this command, otherwise you won’t be able to force menu items to pull down.

If you need one of the menu items to have sub-items, you have to add a FlyoutAnchor xml declaration as a menu item control to the menu section, like you do with usual buttons.

FlyoutAnchor with dynamic menu items

In some cases you have to populate FlyoutAnchor menu items dynamically. Here is the example of adding FlyoutAnchor with dynamic menu items:

<CommandUIDefinition Location="[Exisiting_Group_ID].Controls._children">
  <FlyoutAnchor
    Id="[Your_FlyoutAnchor_ID]"
    Command="[Your_FlyoutAnchor_Command_Name]"
    Sequence="10"
    Image16by16="/_layouts/images/RibbonCustomization/images16x16.png"
    Image16by16Top="0"
    Image16by16Left="-16"
    Image32by32="/_layouts/images/RibbonCustomization/images32x32.png"
    Image32by32Top="0"
    Image32by32Left="-32"
    LabelText="FlyoutAnchor Dymamic"
    TemplateAlias="o1"
    PopulateDynamically="true"
    PopulateOnlyOnce="false"
    PopulateQueryCommand="[Populate_Dymamic_Menu_Items_Query_Command_Name]"
    ToolTipTitle="FlyoutAnchor Dymamic"
    ToolTipDescription="FlyoutAnchor wit dymamic menu items" />
</CommandUIDefinition>

There are three important attributes in the FlyoutAnchor node:

  1. PopulateDynamically – this attribute has to be set to "true";
  2. PopulateOnlyOnce – this Boolean attribute tells if menu items should be constructed every time the FlyoutAnchor is being pulled down. Use "false" to make them build every time;
  3. and PopulateQueryCommand – the name of command which is being invoked every time when the menu items have to be shown (once if PopulateOnlyOnce attribute value is set to "true").

The PopulateQueryCommand has to set PopulationXML value to command properties. In order to do this you have to register this command on the server side (see Server-side command handling post). Here is the example:

commands.Add(new SPRibbonCommand(
                    "[Populate_Dymamic_Menu_Items_Query_Command_Name]",
                    "properties.PopulationXML = GetDynamicMenuXml()"));
The GetDynamicMenuXml JavaScript function mentioned here should construct exactly the same XML, you would use in case of adding menu items declaratively. Here is the example:
function GetDynamicMenuXml() {
    var dynamicMenuXml =
    '<Menu Id="[Your_FlyoutAnchor_ID].Menu">'
    + '<MenuSection Id="[Section_ID]" DisplayMode="Menu16">'
    + '<Controls Id="[Section_ID].Controls">';
    var itemsNumber = Math.floor(Math.random()*10) + 1;
    for (i = 0; i < itemsNumber; i++) {
        var buttonXML = String.format(
           '<Button Id="DynamicButton{0}" '
           + 'Command="[Dynamic_Button_Command_Name]" '
           + 'MenuItemId="{0}" '
           + 'LabelText="DynamicButton {0}" '
           + 'ToolTipTitle="Dynamic Button" '
           + 'ToolTipDescription="Dynamic Button" />',
           i);
        dynamicMenuXml += buttonXML;
    }
    dynamicMenuXml += '</Controls>' + '</MenuSection>' + '</Menu>';
    return dynamicMenuXml;
}

If you have to handle all the menu items commands with one command, you can bypass kind of "unique argument" to this command. You should use MenuItemId button attribute, and then it will be available on the command handler. Here is the example of CommandUIHandler declaration:

<CommandUIHandler
  Command="[Dynamic_Button_Command_Name]"
  CommandAction="javascript:alert('Dynamic Button ' + arguments[2].MenuItemId + ' clicked.');"
  EnabledScript="true" />

The same approach can be applied for extending the Ribbon with DropDown controls. Those can have static/dynamic items declaration as well.

Read more...

Friday, February 5, 2010

Project Server Queue System support in FluentPS

Most of Project Server actions run asynchronously. Every asynchronous action becomes a separate job in queue. You can check them in the “Server Settings”->”Manage Queue Jobs” section on your PWA site. To manage the queue MS Project Server has a special Queuing system service, which works as a separate process. QueueSystem web service from PSI allows developers to communicate with the queue. There is a nice article on MSDN about QueueSystem you may check for more details.( http://msdn.microsoft.com/en-us/library/ms461006.aspx)

We added Project Server queue system support to FluentPS lib, and you can wait for job completion by using WaitForQueue method of the PSQueueSystemService class. So to wait for some operation you can do something like:

var queueService = new PSQueueSystemService(logService, sessionService);
var jobId = projectsService.CheckIn(project.Uid, Guid.NewGuid());
// wait for queue
queueService.WaitForQueue(jobId);

For complete example source code please check EntityMapping project from FluentPS samples solution.

Read more...

Project Server entity mapping: Lookup Custom Fields

The FluentPS library allows mapping of Lookup Custom Fields for projects, resources and tasks to entity properties with help of custom property attributes. Here you can find the examples of mapping Lookup Custom Fields.

Mapping Non-Multiple Value Lookup Fields

Here is the example of mapping text lookup field:
[PSLookupField("{178dcaec-4f4f-4968-a768-8cacd5cf60a7}", "{4b5aa4d3-80ce-4346-bbcc-dd797a111706}",
        FieldName = "Sample Areas Impacted", LookupTableName = "Sample Areas Impacted")]
    public string SampleAreasImpacted { get; set; }

The constructor of PSLookupFieldAttribute class contains two arguments – the string representing the UID of the custom field, and the string representing the UID of the Lookup Table this field is related to.

The type of property points to the type of the Lookup field values. Therefore you should use int or decimal to map Number fields, DateTime for Date etc.

Mapping Multiple Value Lookup Fields

The mapping of fields which allow multiple values to be selected from the lookup table can be produced in the following manner:

[PSLookupField("{eda98e8a-6e2c-49f4-bdf0-97e8d3941e3e}", "{9d02dfc9-cde1-4087-b278-7745086c0830}",
        FieldName = "Sample Primary Objectives", LookupTableName = "Sample Primary Objective")]
    public IList SamplePrimaryObjectives { get; set; }

The inheritor classes or interfaces of IEnumerable<> type (IList<>, List<> etc.) can be used to map a multiple value lookup field. The instance of List<> will be created during filling propertied of your entity, and all selected lookup values will be added as an elements to this list. For instance, if the lookup table has three items (“value 1”, “value 2” and “value 3”), and two of them are chosen in your object, you’ll have “value 1” and “value 2” items in the list.

To add, remove or change the selection of values for the mapped object, you should manipulate with list items.

Mapping Hierarchical Lookup Fields

The Text lookup tables can have hierarchical structure. In this case you have to manipulate with the “full” values of lookup items. For instance, if the lookup table has “value 1” item and “sub-value 1” as its sub-item, you’ll get the “value1 .sub-value1” value in the mapped property if the first sub-item is selected (the “.” character is the delimiter from the lookup table; it is possible to change it to another one). The mapping for hierarchical lookup field is much the same as for text lookup field.

Read more...

Monday, February 1, 2010

PS Project Workflow - InfoPath form usage

There is a way to store any xml-formed data as a metadata of a Workflow Association based on Project Workflow Template. It can be useful in case of dynamic building of the workflow, or if the business rules can be managed by one workflow template with different initial arguments. For this purpose it is possible to use workflow’s association page – the page which is shown right after fills all required Workflow Association data i.e. Association name, Template, etc. This page can be created from scratch and contain any custom logic it needs (check out the article). But there is a way to do create an InfoPath form for this purpose.


Design an InfoPath Form for Project Workflow Association

There is great article posted in MSDN how to create an InfoPath form to be shown in Association page. It can be found in http://msdn.microsoft.com/en-us/library/ms548723(office.14).aspx. When you are done with the development of your InfoPath form, please pay attention to the digital signing and publishing instructions. There is a note in the article: “Do not specify an alternative path to the form. Doing so causes an error that prevents you from publishing the form to the server”. This is very important note, so follow all the publishing steps mentioned in the article.

Deploy InfoPath form for Project Workflow Association

The instructions regarding deployment of InfoPath form for Project Workflow Association can be found in post. To get the value for Association_FormURN xml node you should check your InfoPath FormTemplate Properties->ID.
IMNPORTANT. If your InfoPath form has a code-behind class, than you should deploy both published .xsn and .dll files to the feature folder.

Read more...

Friday, January 29, 2010

SharePoint Server and Project Server 2010 Installation

In this post our goal is to describe SharePoint Server 2010 and Project Server 2010 public Beta build installation process from the very beginning step by step. While installing the products various issues might be faced and we will show possible working solutions.

SharePoint Server 2010 and Project Server 2010 installation packages are provided with “default.hta” files for each of the products which are recommended to run first of all (http://technet.microsoft.com/en-us/library/ee662114(office.14).aspx). However, it is not necessary. Each product installation consists of two phases: installation of prerequisites and product installation itself, which are possible to run separately. Anyway, the Prerequisite Installer should be run before the main product installation.


SharePoint Server 2010 installation.

Run “Setup.cmd” file:


Click “Install software prerequisites” menu item:


The main purpose of the Prerequisite Installer is to download and install a set of products and frameworks that SharePoint Server 2010 is dependent on. If any of these products are already installed on the target system, it highly recommended uninstalling them first as the Prerequisite Installer always downloads the latest products versions including Betas and CTPs. If the server where you install SharePoint Server 2010 is not connected to the Internet, you must install all the software prerequisites manually. The list of prerequisites and the download locations are available at http://technet.microsoft.com/en-us/library/cc262485(office.14).aspx. After the Prerequisite Installer is done a system restart might be required.

Another issue is a pending system restart. For instance, you uninstalled a product and didn’t restart the server. It could be SharePoint Server 2010 application, and in fact, after the product is finally uninstalled it doesn’t warn about a system restart. You might be seeing the following error message:


Surprisingly, but this problem can be overcome the same way as in case of Microsoft SQL Server 2008 product installation, with help of Windows Registry changes (http://social.msdn.microsoft.com/Forums/en/sqlsetupandupgrade/thread/988ab9e3-26ce-48da-ad61-c458f5c9c539):

  1. Open Regedit.
  2. Find the key "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager".
  3. Rename the "PendingFileRenameOperations" value to "PendingFileRenameOperations2".
  4. Restart your machine and then try again.

By the way, the last step is not necessary; a system restart is not needed.

Click “Next”:


Wait until the installer finishes up and click “Finish”. Now your server is prepared for SharePoint 2010 installation. Click “Install SharePoint Server” menu:


SharePoint Server 2010 can be installed of one of three types:

  1. Standalone.
  2. Server Farm: Stand-alone.
  3. Server Farm: Complete.

Standalone and Server Farm (stand-alone) types include Microsoft SQL Server 2008 Express Edition installation by default and do not let user to change the default settings: SQL Server Administration account to use, SharePoint configuration database name etc.

Server Farm (complete) type installs all the components except Microsoft SQL Server 2008 instance itself. You should have SQL Server 2008 instance installed on the server before launching the SharePoint Configuration wizard. Different SharePoint Server 2010 build actually require different SQL Server 2008 instance and SQL Server 2008 Native Client product versions. For instance, the latest build requires SQL Server 2008 SP1 and KB976761 hotfix that upgrades SQL Server 2008 Native Client to 10.0.2740.0 version.

We will take a look at a Server Farm (complete) installation.
Click “Server Farm” button to start the installation:


Select “Complete” menu item and then press “Install Now”, leave “File Location” tab unchanged:


Wait for the installation process to finish:


In fact, there are two ways of finishing of SharePoint Server 2010 installation: to check or uncheck the “Run the SharePoint Products Configuration Wizard now” item. We would recommend leaving in unchecked as it saves your time: you could run the wizard after Project Server 2010 is installed. By leaving the item checked you will start the wizard right away and you will need to do it one more time having Project Server 2010 installed.

Project Server 2010 installation.

Install Project Server 2010 product following the same steps as used for SharePoint Server 2010: install the software prerequisites and the product itself.




Now leave the “Run the SharePoint Products Configuration Wizard now” item checked and press “Close” button, the SharePoint Configuration Wizard will be launched.

In the next post we will take a look at configuring SharePoint Server 2010 using the SharePoint Configuration Wizard utility, activating Project Server Service Application, creating and setting up a PWA site.

Read more...

Monday, January 25, 2010

Project Server JS customization: Sample of webpart callback interface implementation

In the previous post “SharePoint 2010 webpart callback interface” we have considered new SharePoint client-server interoperation model and the asynchronous callback interface implementation on the client.

In this post we provide a complete server-side webpart C# class that implements ICallbackEventHandler interface and generates all necessary JavaScript code for the interoperation.

public class CustomWebPart : Microsoft.SharePoint.WebPartPages.WebPart, ICallbackEventHandler
{
    private string _result = String.Empty;

    #region ICallbackEventHandler members

    public void RaiseCallbackEvent(string eventArgument)
    {
        try
        {
            // Save logic
            // eventArgument can be all webpart fields data in JSON
        }
        catch (Exception ex)
        {
            _result = ex.Message;
        }
    }

    public string GetCallbackResult()
    {
        return _result;
    }

    #endregion

    protected override void OnPreRender(EventArgs e)
    {
        base.OnPreRender(e);
        
        //register script for hooking up into the PDP structure
        string uniqueScriptName = "PDP Class " + ClientID;
        
        string callBackScript = Page.ClientScript.GetCallbackEventReference(
            this,
            "arg",
            string.Format("SaveCallback_{0}", ClientID),
            "ctx",
            string.Format("SaveErrorCallback_{0}", ClientID), false);

        string script =
            string.Format(
                @"var WPDP_{0} =  new object();
       WPDP_{0}.IsDirty = false;
             
                  // Controls webpart’s state
                  WPDP_{0}.EnablePart = function pfp_EnablePart_{0}(enabled) {{
                      // Can pass “enable” argument value to other methods
                  }}

                  // Initiates Save operation
                  WPDP_{0}.Save = function pfp_Save_{0}(ctx) {{
                      var arg = ConstructArg_{0}();
                      {1};
                  }}

                  // Performs client validation
                  WPDP_{0}.Validate = function pfp_Validate_{0}() {{
                      return true;
                  }}

                  function SaveCallback_{0}(result, ctx) {{
                      if (result != '') {{
                          SaveErrorCallback_{0}(result, ctx);
                      }}
                      else {{
                          ctx.Completed();
                      }}
                  }}

                  function SaveErrorCallback_{0}(result, ctx) {{
                      ctx.FailedShowInlineErrors(result);
                  }}
                  
                  // Collects and returns webpart’s client data
                  function ConstructArg_{0}() {{
                      return “some data”;
                  }}",
                    ClientID,
                    callBackScript);

        Page.ClientScript.RegisterClientScriptBlock(GetType(), uniqueScriptName, script, true);
    }
} 

The class should implement ICallbackEventHandler interface. The RaiseCallbackEvent method accepts a string argument that should be a representation of the client data to be processed on the server side. The operation result is to be returned to the client side using the GetCallbackResult method call. As the operation result is not returned directly by RaiseCallbackEvent method, it should be stored within the class between the calls. On the client side the operation result is accepted by the “SaveCallback” JS method.

The JS object and all its methods’ names are unique due to the “ClientID” property. The “Save” JS method constructs the argument (using the “ConstructArg” JS method) and passes it to the callback script which actually initiates the client-server interoperation.

Next time we will talk of the out-of-box Project Fields webpart and the client JavaScript code it generates.

Read more...

FluentPS v0.8 released

We are happy to announce the new FluentPS v0.8 release. There are number of new features:
  • Impersonation support (for Project Server 2010 beta2 build)
  • Lookup fields support
  • Tasks support
  • Resources support
  • Queue system support
Source code and binary can be downloaded from http://fluentps.codeplex.com.

Have fun! Read more...

Thursday, January 21, 2010

Impersonation in Project Server 2010 beta

There are a lot of articles about impersonation in Project Server. It is almost about two additional headers in each request to PSI web services: “PjAuth” with impersonation data created by PSContextInfo class and “ForwardedFrom” with target relative path. One additional thing you have to add is ConnectionGroupName property of a web request to your site UID. So it looks like:

webRequest.UseDefaultCredentials = true;
webRequest.Headers.Add("PjAuth", context.GetImpersonationHeader());
webRequest.Headers.Add("ForwardedFrom", forwardFrom);
webRequest.PreAuthenticate = true;
webRequest.ConnectionGroupName = context.SiteId.ToString();

if (webRequest is HttpWebRequest)
    ((HttpWebRequest) webRequest).UnsafeAuthenticatedConnectionSharing = true;

You can do this by overriding "GetWebRequest" method in web service proxy classes or by interceptors using “method by name” injection, it is up to you.

But there are several changes in Project Server 2010 beta regarding the impersonation feature. Actually there are only two issues: a claim instead of the user name and the secure URLs for the web services.


We are completely ok with the claim, just pass the user claim which can be created by SPClaimProviderManager to PSContextInfo structure. Here is the example of how to get a claim for the specified user name:

public static string GetClaim(string userAccount)
{
    string claimsRepresentation = userAccount;
    if (!SPClaimProviderManager.IsEncodedClaim(userAccount))
    {
        SPClaim claim = SPClaimProviderManager.Local.ConvertIdentifierToClaim(userAccount, SPIdentifierTypes.WindowsSamAccountName);
        claimsRepresentation = claim.ToEncodedString();
    }
    return claimsRepresentation;
}

But there is a trick about secure URLs. Secure path looks like “/ad7c153145e443d6a8dea014cab422e6/PSI/project.asmx” and as we can see the first part is some UID. There is no information about how we could get a secure URL or UID of the web service in runtime. Of course you always can find UID in the IIS Manager and add it to the configuration file.


But what if you want to avoid any manual actions at deployment? And there is a way. It can’t be recommended due to possible future changes, but the idea is to invoke “GetPsiBaseUrl” method of PSI class from Microsoft.Office.Project.PWA namespace. It is not a public one but static and will find for you a secure URL to any service using loading balancer. So the following code will get a secure URL:

MethodInfo methodInfo = typeof(PSI).GetMethod("GetPsiBaseUrl",                                                                   BindingFlags.Static | BindingFlags.NonPublic);
object url = methodInfo.Invoke(null, new object[] { serviceName });

The impersonation in the RTM build is pretty much the same as the impersonation in Project Server 2007, with some minor issues. We’ll talk about it some other time. For more information there is a great article in MSDN about impersonation with WCF http://msdn.microsoft.com/en-us/library/ff181538(office.14).aspx.

Read more...

ProjectServer 2010 Workflow - Accessing PSI web services as Proxy User

Project Server Workflows need to run under the context of a user. However, they do not run under context of the user that started the project, instead, the workflows are run under the Workflow Proxy Account. This means that the user account which you specify as the workflow proxy account must have the proper rights to execute all of the commands a project server workflow will need to do. All workflow activities should be happening under the workflow proxy account. By default all PSI-calls from built-in activities are running under this proxy account. However, it is possible to perform PSI-calls from custom workflow activities under the Application Pool account.

How to Setup the Workflow Proxy Account

It is recommended that you setup a service user to serve as this function. The steps below show how to define and setup a workflow proxy account.

  1. Within Project Web Access, go to Server Settings
  2. Under “Workflow and Project Detail Pages” Click on “Project Workflow Settings”
  3. Workflow Proxy User: type in the user name of the account you wish to have all workflows run under.
  4. The minimum rights needed for the account to execute PSI-calls (regarding Project Server security) are:
    • Global permissions:
      • Log On
      • Manage Users And Groups
      • Manage Workflow
    • Category permissions:
      • Open Project
      • Save Project
      • View Enterprise Resource Data
      • Edit Project Properties
      • View Enterprise Resource Data

Note 1: By default, all Project Server Interface (PSI) calls within a workflow will be made under the context of the Workflow Proxy User Account. For these PSI calls to be successful, the Workflow Proxy User Account should have appropriate permissions in Project Server (built-in workflow activities are meant here).

Note 2: Exercise caution when changing this account. All workflows that are started after the Workflow Proxy User Account change will use the new account. All workflows already in progress will continue to use the original Workflow Proxy User Account, and if the original Workflow Proxy User Account is deleted or does not have sufficient permissions, the PSI calls made from the workflows will fail. So, it is highly recommended not to change the Workflow Proxy User Account.

Note 3: If the Workflow Proxy User Account needs to be changed and the original Workflow Proxy User Account needs to be removed, you may need to re-start all the currently running workflows after the change.

How to Use the Workflow Proxy Account

The sample of usage Proxy user is below:

1. First of all we have to create custom workflow service that should be extending workflow service base class.

[ExternalDataExchange]
interface IProxyLookupTableService
{
    void UpdateLookupTableValue(LogService logService, 
                                ProjectWorkflowContext projectWorkflowContext,
                                Guid lookupTableUid,
                                PSLookupTableValueInfo lookupTableValue,
                                bool isCreate);
}

public class ProxyLookupTableService : PSWorkflowServiceBase, IProxyLookupTableService
{
    public void UpdateLookupTableValue(LogService logService,
                                       ProjectWorkflowContext projectWorkflowContext,
                                       Guid lookupTableUid,
                                       PSLookupTableValueInfo lookupTableValue)
    {
        LookupTable _pwaLookupTable = GetPSI(projectWorkflowContext).LookupTableWebService;
        using (LookupTableDataSet dsLookupTables = _pwaLookupTable.ReadLookupTablesByUids(
            new Guid[] { lookupTableUid }, false, 0))
        {
        ……
        }
    ……

2. We have to register our ProxyLookupTableService in web.config.

<configuration>
  <SharePoint>
    <WorkflowServices>
      <WorkflowService 
        Assembly="Programs, Version=1.0.0.0, Culture=neutral, PublicKeyToken=4b0916729dd423c6"
        Class="Programs.Services.ProxyLookupTableService" />
    </WorkflowServices>
  </SharePoint>
</configuration>

3. And now we can use our custom workflow service (ProxyLookupTableService) in any workflow events or activities.

protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
{
    IProxyLookupTableService ltService = executionContext
        .GetService<IProxyLookupTableService>();

    ltService.UpdateLookupTableValue(…);
    return base.Execute(executionContext);
}

Note 1: Be sure to use your proxy service from inside the overridden Execute method. You are not allowed to store it as a field of custom activity class. So all PSI-calls should be performed from the place where you instantiate your proxy service.

Note 2: You do not need to add Web References to PSI web services. You can instantiate them with GetService() method of base PSWorkflowServiceBase class.

How to Setup the Application Pool Account

Any PSI-call which is made from the custom workflow activity (or code activity) runs under the account of the Application Pool which holds the web application with PSI web services. By default this account is Network Service. The minimum rights needed for the account are mentioned above, so you should grant appropriate permissions to this account. Here are the steps you should go through:

  1. Find out which account should receive appropriate rights in the Project Server. To do this, check IIS Manager -> Sites -> SharePoint Web Services -> the application containing PSI folder in it -> Manage Application context menu -> Advanced Settings -> Application Pool; then go to Application Pools list and find corresponding Identity for this pool. This account should receive appropriate permissions.
  2. Go to http://server_name/pwa_name/Admin/ManageUsers.aspx to create PS user for the account the SharePoint Web Services/PSI web services run as, and add appropriate permissions for this user (the easiest way is to add this account to the Administrators group).
Read more...

Tuesday, January 19, 2010

Project Server 2010: Changing Master Page

Once I tried simply to change master page, just to change an image. First of all I found out the master page. Project Server 2010 uses v4.master master page for all application pages. When my changes were applied I uploaded back master page to the master page gallery. And I was surprised, now all PS pages showed me an error: “The base type 'Microsoft.Office.Project.PWA.PJBaseWebPartPage' is not allowed for this page. The type is not registered as safe”. I did rollback, but still got the same error.

It was fixed by adding the following line to the web.config:

<SafeControl Assembly="Microsoft.Office.Project.Server.PWA, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" Namespace="Microsoft.Office.Project.PWA" TypeName="*" Safe="True" />

Atferwards, instead of getting the previous error I got the new one: “An error occurred during the processing of /PWA/default.aspx. Code blocks are not allowed in this file”. It happened because SharePoint disabled the ability to create the server-side scripts by default, so you had to turn it on. You are able to do that in the web.config file, in the configuration/SharePoint/PageParserPaths configuration section:

<PageParserPath VirtualPath="/pwa/*" CompilationMode="Always" AllowServerSideScript="true" />

But new error appeared: “The control type 'Microsoft.Office.Project.PWA.CommonControls.PageProperty' is not allowed on this page. The type is not registered as safe.”

<SafeControl Assembly="Microsoft.Office.Project.Server.PWA, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" Namespace="Microsoft.Office.Project.PWA.CommonControls" TypeName="*" Safe="True" />

Issue is finnaly fixed. Now you can apply any changes to the master page without getting errors.

Read more...

Friday, January 15, 2010

SharePoint 2010 - Solution Deployment using PowerShell

Couple posts ago I described how to deploy a SharePoint solution using STSADM tool. But there is another way to deploy your solution and do any customization. You can do same operations as retract/deploy, add/delete solution etc. using PowerShell. Actually I recommend using PowerShell because it works faster and more flexible. You can dynamically activate or deactivate features on all webs in your site collection, you can dynamically apply any changes to webs etc.

To get started with PowerShell, run the SharePoint 2010 Management Console located in your Microsoft SharePoint 2010 Products folder in your start menu. This automatically loads the Microsoft.SharePoint.PowerShell snappin, so that we can execute SharePoint commands.

Below I will describe how to do common deployment steps using STSADM and PowerShell commands:

  1. Adding solution.
    stsadm –o addsolution –name SolutionPackage.wsp
    
    Same operation in powershell:
    Add-SPSolution c:\code\SolutionPackage.wsp
    
    Only one difference, If you would like use relative path, you must add “./” before solution name.

  2. Deploying solution.
    stsadm –o deploysolution –name SolutionPackage.wsp –url http://servername –allowgacdeployment –immediate
    stsadm -o execadmsvcjobs
    
    We must also follow this operation with a call to stsadm with the execadmsvcjobs operation.
    Install-SPSolution SolutionPackage.wsp –WebApplication http://servername –GACDeployment
    
    Executing this command does the deployment operation. You don’t have to fire off something to execute a job later like you did with STSADM. You can deploy solution to specified web application (as shown above) or to all web applications using –AllWebApplications parameter.

  3. Uninstalling solution.
    stsadm -o retractsolution -name SolutionPackage.wsp -immediate
    stsadm -o execadmsvcjobs
    
    Uninstall-SPSolution SolutionPackage.wsp –WebApplication http://servername
    
  4. Removing solution.
    And lastly removing solution.
    stsadm -o deletesolution -name SolutionPackage.wsp
    
    Remove-SPSolution SolutionPackage.wsp

  5. Activating feature.
    stsadm -o activatefeature -name FeatureName -url http://siteurl
    
    You can also manipulate with features using PowerShell.
    Enable-SPFeature FeatureName -Url http://siteurl
    
  6. Deactivating feature.
    stsadm -o deactivatefeature -name FeatureName -url http://siteurl
    
    Disable-SPFeature FeatureName -Url SiteUrl
    

The nice thing is that you can script these things together very easily and create highly flexible PowerShell scripts. I use the following script for deploying the solution and activating the feature:

$SiteUrl = $args[0]
$SolutionFolder = $args[1]
$SolutionName = $args[2]
$FeatureName = $args[3]

$AdminServiceName = "SPAdminV4"
$IsAdminServiceWasRunning = $true;

Add-PSSnapin microsoft.sharepoint.powershell

if ($(Get-Service $AdminServiceName).Status -eq "Stopped")
{
    $IsAdminServiceWasRunning = $false;
    Start-Service $AdminServiceName       
}

$SingleSiteCollection = Get-SPSite $SiteUrl

Write-Host

if ($FeatureName -ne $null)
{
    Write-Host "Deactivating feature: $FeatureName" -NoNewline
        $WebAppsFeatureId = $(Get-SPFeature -limit all | ? {($_.displayname -eq $FeatureName)}).Id
        if (($WebAppsFeatureId -ne $null) -and ($SingleSiteCollection.Features | ? {$_.DefinitionId -eq $WebAppsFeatureId}))
        {
            Disable-SPFeature $FeatureName -Url $SiteUrl -Confirm:$false
        }
    Write-Host " - Done."
}

Write-Host "Rectracting solution: $SolutionName" -NoNewline
    $Solution = Get-SPSolution | ? {($_.Name -eq $SolutionName) -and ($_.Deployed -eq $true)}
    if ($Solution -ne $null)
    {
        if($Solution.ContainsWebApplicationResource)
        {
            Uninstall-SPSolution $SolutionName -AllWebApplications -Confirm:$false
        }
        else
        {
            Uninstall-SPSolution $SolutionName -Confirm:$false
        }
    }
    
    while ($Solution.JobExists)
    {
        Start-Sleep 2
    }
Write-Host " - Done."
    
Write-Host "Removing solution: $SolutionName" -NoNewline
    if ($(Get-SPSolution | ? {$_.Name -eq $SolutionName}).Deployed -eq $false)
    {
        Remove-SPSolution $SolutionName -Confirm:$false
    } 
Write-Host " - Done."
Write-Host

Write-Host "Adding solution: $SolutionName" -NoNewline
    $SolutionPath = $SolutionFolder + $SolutionName
    Add-SPSolution $SolutionPath | Out-Null
Write-Host " - Done."
    
Write-Host "Deploying solution: $SolutionName" -NoNewline
    $Solution = Get-SPSolution | ? {($_.Name -eq $SolutionName) -and ($_.Deployed -eq $false)}
    if(($Solution -ne $null) -and ($Solution.ContainsWebApplicationResource))
    {
        Install-SPSolution $SolutionName -AllWebApplications -GACDeployment -Confirm:$false
    }
    else
    {
        Install-SPSolution $SolutionName -GACDeployment -Confirm:$false
    }
    while ($Solution.Deployed -eq $false)
    {
        Start-Sleep 2
    }
Write-Host " - Done."
    
if ($FeatureName -ne $null)
{
    Write-Host "Activate feature: $FeatureName" -NoNewline
        Enable-SPFeature $FeatureName -Url $SiteUrl
    Write-Host " - Done."
}

if (-not $IsAdminServiceWasRunning)
{
    Stop-Service $AdminServiceName     
}

Echo Finish

Save that script to deploy.ps1. Now you can use it in command file. Here is the example:

@echo off
Set DeploymentPackageFolder="D:\Projects\DemandManagement\trunk\Src\Programs\Programs\DeploymentPackage\"

Set SiteUrl="http://localhost/pwa"
Set SolutionFolder="./"
Set SolutionName="Programs.wsp"
Set FeatureName="Programs_Programs"

cd %DeploymentPackageFolder%

PowerShell -file .\Deploy.ps1 %SiteUrl% %SolutionFolder% %SolutionName% %FeatureName%

Note: You must run that command file under x64 shell, otherwise “microsoft.sharepoint.powershell” snapin can’t be added.

I hope this post will help you to start with PowerShell.

Read more...

Tuesday, January 12, 2010

Project Server entity mapping: Data Access Layer

In the previous post we introduced our vision of dealing with data on Project Server platform. Now we’ll show main ideas of data mapping. If you are not rather familiar with ProjectDataSet type check out the following link first http://msdn.microsoft.com/en-us/library/websvcproject.projectdataset_members.aspx.

There are two types of fields in Project Server: intrinsic and custom fields. We implemented mapping attributes for both types to get all necessary information for data access. For intrinsic fields we specify name of the column in Project data table. For custom fields data access we use custom field UID to find proper row in ProjectCustomFields table. And for both cases we have a simple data type conversion which updates entity property with loaded data and vice-versa.

So, our Get method implementation from PSProjectService from previous post is following:
public T Get(Guid entityUid)
{
    try
    {
        int dataFlags = (int)ProjectEntityType.Project |
                        (int)ProjectEntityType.ProjectCustomFields;

        using (DataSet dsProject = _wssProject.ReadProjectEntities(
                                       entityUid,
                                       dataFlags,
                                       DataStore))
        {
            if (dsProject != null &&
                dsProject.Tables.Contains(PsDataTableNames.Project) &&
                dsProject.Tables[PsDataTableNames.Project].Rows.Count > 0)
            {
                DataRow projectRow = dsProject.Tables[PsDataTableNames.Project].Rows[0];
                var project = new T();
                var entityDataMapper = new EntityDataMapper<T>(_logService);

                // load intrinsic fields data
                entityDataMapper.MapDataRowToEntityNativeFields(
                    projectRow,
                    project,
                    DataRowVersion.Current);

                // load custom fields data
                entityDataMapper.MapCustomFieldsToEntity(
                    project,
                    entityUid,
                    dsProject.Tables[PsDataTableNames.ProjectCustomFields],
                    DataRowVersion.Current,
                    new ProjectCustomFieldsMappingInfo());

                return project;
            }
        }
    }
    catch (SoapException ex)
    {
        _logService.Log("Read project exception: " + ex.Message);
    }
    return null;
}
Here we are using EntityDataMapper class for mapping data from DataSet to entity. MapDataRowToEntityNativeFields method loads data from the project row:
public T MapDataRowToEntityNativeFields(DataRow row, T entity, DataRowVersion dataRowVersion)
{
    typeof(T).ForeachProperty(propertyInfo =>
    {
        var attr = typeof(T)
                       .GetPropertyCustomAttribute<PSNativeFieldAttribute>(propertyInfo);
        if (attr != null)
        {
            string columnName = attr.ColumnName.Trim();
            if (row.Table.Columns.Contains(columnName))
            {
                try
                {
                    object val = RowDataAdapter.GetRowData(
                                     row,
                                     columnName,
                                     propertyInfo.PropertyType,
                                     dataRowVersion);

                    if (val != null || !propertyInfo.PropertyType.IsValueType)
                        propertyInfo.SetValue(entity, val, null);
                }
                catch (Exception ex)
                {
                    _logService.Error("Native field mapping exception: " + ex.Message);
                    throw;
                }
            }
        }
    });

    return entity;
}
And MapCustomFieldsToEntity method loads custom fields’ data:
internal void MapCustomFieldsToEntity(T entity,
                                      Guid entityUid,
                                      DataTable dtCustomFields,
                                      DataRowVersion version,
                                      ICustomFieldsMappingInfo cfMappingInfo)
{
    var customFields = new Dictionary<Guid, object>();

    foreach (DataRow customFieldRow in dtCustomFields.Rows)
    {
        if (entityUid == RowDataAdapter.GetRowData<Guid>(
                             customFieldRow,
                             cfMappingInfo.EntityUidColumnName,
                             version))
        {
            try
            {
                var propId = RowDataAdapter.GetRowData<Guid>(
                                 customFieldRow,
                                 CustomFieldsColumnNames.MD_PROP_UID,
                                 version);

                object fieldValue = GetCustomFieldRowValue(customFieldRow, version);

                if (!customFields.ContainsKey(propId))
                    customFields.Add(propId, fieldValue);                        

            }
            catch (Exception ex)
            {
                _logService.Error("Custom fields mapping exception: " + ex.Message);
                throw;
            }
        }
    }

    // set loaded fields data to entity properties
    SetPropertiesValues(entity, customFields);
}
The SetPropertiesValues method is defined as following:
internal void SetPropertiesValues(T entity, Dictionary<guid, object> customFieldsValues)
{
    typeof(T).ForeachProperty(propertyInfo =>
    {
        var attr = typeof(T)
                       .GetPropertyCustomAttribute<PSCustomFieldAattribute>(propertyInfo);

        if (attr != null)
        {
            Guid fieldId = attr.FieldUid;
            if (customFieldsValues.ContainsKey(fieldId))
            {
                try
                {
                    object val = customFieldsValues[fieldId];

                    if (propertyInfo.PropertyType.Equals(typeof(int)) && val != null)
                    {
                        val = (int)val;
                    }

                    propertyInfo.SetValue(entity, val, null);
                }
                catch (Exception ex)
                {
                    _logService.Error("Native field mapping exception: " + ex.Message);
                    throw;
                }
            }
        }
    });
}
The GetCustomFieldRowValue method loads proper custom field value from a row using FIELD_TYPE_ENUM column to detect the stored data type.

As you can see the data mapping layer is quite simple and clear to understand. Updating data is much the same. Complete source code you can find at http://fluentps.codeplex.com.

Read more...

Wednesday, January 6, 2010

Announcing a FluentPS - an open source ORM-like library for Project Server 2010

We are pleased to announce that we are starting a FluentPS - an open source library aimed to simplify development and improve maintainability of Project Server 2010 custom solutions by providing ORM-like interface to PSI. You can find the sourcecode and releases at http://fluentps.codeplex.com. We will cover the FlientPS development process in this blog.

We envision FluentPS as an utility which helps developing some types of enterprise applications. This is not going to be a silver bullet, but yet another useful tool in Project Server 2010 developer's toolbelt. More details to follow!

Read more...

SharePoint 2010 Ribbon Customization - server-side command handling

In the previous post we’ve discussed the way to customize SharePoint 2010 Ribbon bar by deploying its xml description with SharePoint feature. Using this approach it is possible to deploy all necessary controls (buttons, checkboxes, dropdowns etc.), control containers (groups and tabs) and JavaScript command handlers. But sometimes it is necessary to generate a JavaScript command handler code dynamically or to handle ribbon actions on the server side. For this purpose the SharePoint ribbon allows user to create command handlers programmatically.

Here is the example:

var commands = new List<IRibbonCommand>();
commands.Add(new SPRibbonCommand(
                "Your_Command_ID",
                "alert('this is server generated command')"));

When you have created a command, you should register it with help of SPRibbonScriptManager class. It has protected method RegisterInitializeFunction, so it is necessary to use reflection to invoke it:

var manager = new SPRibbonScriptManager();
var methodInfo = typeof(SPRibbonScriptManager).GetMethod(
    "RegisterInitializeFunction",
    BindingFlags.Instance | BindingFlags.NonPublic);
methodInfo.Invoke(
    manager,
    new object[]
    {
        Page,
        "InitPageComponent",
        "/_layouts/INC/RibbonCustomization/PageComponent.js",
        false,
        "RibbonCustomization.PageComponent.initialize()"
    });

This method is used to register a JavaScript class for managing server-created command handlers. It should implement the appropriate interface for registering ribbon commands. Here how it should look like:


function ULS_SP() {
    if (ULS_SP.caller) {
        ULS_SP.caller.ULSTeamName = "Windows SharePoint Services 4";
        ULS_SP.caller.ULSFileName = "/_layouts/INC/RibbonCustomization/PageComponent.js";
    }
}

Type.registerNamespace('RibbonCustomization');

// RibbonApp Page Component
RibbonCustomization.PageComponent = function () {
    ULS_SP();
    RibbonCustomization.PageComponent.initializeBase(this);
}
RibbonCustomization.PageComponent.initialize = function () {
    ULS_SP();
    ExecuteOrDelayUntilScriptLoaded(
        Function.createDelegate(
            null,
            RibbonCustomization.PageComponent.initializePageComponent),
        'SP.Ribbon.js');
}
RibbonCustomization.PageComponent.initializePageComponent = function () {
    ULS_SP();
    var ribbonPageManager = SP.Ribbon.PageManager.get_instance();
    if (null !== ribbonPageManager) {
        ribbonPageManager.addPageComponent(RibbonCustomization.PageComponent.instance);
        ribbonPageManager
            .get_focusManager()
            .requestFocusForComponent(RibbonCustomization.PageComponent.instance);
    }
}
RibbonCustomization.PageComponent.refreshRibbonStatus = function () {
    SP.Ribbon.PageManager
        .get_instance()
        .get_commandDispatcher()
        .executeCommand(Commands.CommandIds.ApplicationStateChanged, null);
}
RibbonCustomization.PageComponent.prototype = {
    getFocusedCommands: function () {
        ULS_SP();
        return [];
    },
    getGlobalCommands: function () {
        ULS_SP();
        return getGlobalCommands();
    },
    isFocusable: function () {
        ULS_SP();
        return true;
    },
    receiveFocus: function () {
        ULS_SP();
        return true;
    },
    yieldFocus: function () {
        ULS_SP();
        return true;
    },
    canHandleCommand: function (commandId) {
        ULS_SP();
        return commandEnabled(commandId);
    },
    handleCommand: function (commandId, properties, sequence) {
        ULS_SP();
        return handleCommand(commandId, properties, sequence);
    }
}

// Register classes
RibbonCustomization.PageComponent.registerClass(
    'RibbonCustomization.PageComponent',
    CUI.Page.PageComponent);
RibbonCustomization.PageComponent.instance = new RibbonCustomization.PageComponent();

// Notify waiting jobs
NotifyScriptLoadedAndExecuteWaitingJobs(
    "/_layouts/INC/RibbonCustomization/PageComponent.js");

The last thing you should do to enable server-registered commands on your page is registering three client functions:

manager.RegisterGetCommandsFunction(Page, "getGlobalCommands", commands);
manager.RegisterCommandEnabledFunction(Page, "commandEnabled", commands);
manager.RegisterHandleCommandFunction(Page, "handleCommand", commands);

If you are creating your own page, it might be necessary to link SP.Runtime.js and SP.js
JavaScript files. You can do this in the following way:

ScriptLink.RegisterScriptAfterUI(Page, "SP.Runtime.js", false, true);
ScriptLink.RegisterScriptAfterUI(Page, "SP.js", false, true);

This is enough to enable the handling of your server-registered commands for the ribbon controls.

Registering server-side ribbon command handlers

In order to handle ribbon command on the server you should create and register an instance of SPRibbonPostBackCommand command class. Here is the example:

commands.Add(new SPRibbonPostBackCommand(
                "ServerCommandID",
                this,
                "ServerCmd",
                null));

In order to use the server command handling from the web part it has to implement IPostBackEventHandler interface. Then you can perform all necessary server actions in RaisePostBackEvent method. You can determine which command was run by eventArgument method argument.

The source of sample project you can find here

Read more...

SharePoint Solution Deployment

Every time you develop Project Server solutions you are faced with deployment procedure.

Two main steps of getting SharePoint solution installed are adding solution to the solution store and solution deployment. The solution deployment can be performed either with help of GUI or using the STSADM tool, whereas adding the solution to the solution store can be done only by using STSADM. Therefore we recommend using STSADM tool for the entire procedure.


Example of a batch file is shown below:

@echo off
SET STSADM="c:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\BIN\STSADM.EXE"
SET SOLUTIONNAME="Programs.wsp"
SET SITEURL="http://SERVER_NAME/pwa"
SET FEATURENAME="Programs_Programs"

echo Deactivating Programs feature
%STSADM% -o deactivatefeature -name "%FEATURENAME%" -url "%SITEURL%"

echo --- Attempting to deactivate/retract existing solution...
%STSADM% -o retractsolution -name %SOLUTIONNAME% -immediate
%STSADM% -o execadmsvcjobs 
%STSADM% -o deletesolution -name %SOLUTIONNAME% -override 

echo --- done --- 
%STSADM% -o execadmsvcjobs 

echo --- Adding solution to solution store...
%STSADM% -o addsolution -filename %SOLUTIONNAME%
if errorlevel == 0 goto :deploySolution

echo ### Error adding solution
echo . 
goto end

:deploySolution
echo --- Deploying solution 
%STSADM% -o deploysolution -name %SOLUTIONNAME% -immediate -allowGacDeployment -force
%STSADM% -o execadmsvcjobs 

if errorlevel == 0 goto :deploySolutionSuccess

echo ### Error deploying solution
echo .
goto end

:deploySolutionSuccess
echo Activating Programs feature
%STSADM% -o activatefeature -name "%FEATURENAME%" -url "%SITEURL%"
if errorlevel == 0 goto :success
goto error

:error
echo ERROR OCCURS
goto :end

:success
echo Solution and feature install successfully

:end
pause

Everything works fine unless “SharePoint 2010 Administration” windows service is started. Started service prevents the “%STSADM% -o execadmsvcjobs” command for successful execution. On the other hand if you remove the “execadmsvcjobs” command, “deploysolution – activatefeature” (and also “retractsolution - deletesolution”) command sequence will not execute synchronously. For these commands a timer job will be created and the second command (“activatefeature” or “deletesolution”) is going to be executed immediately. The solution can’t be deleted until it’s not yet retracted and the feature can’t be activated until the solution isn’t yet deployed. So you will get an error in this case.

To solve this problem we strongly recommend adding the following command to the very beginning of the batch file:

net stop SPAdminV4

and

net start SPAdminV4

to the very end.

Commands above stop and start “SharePoint 2010 Administration” service.

One more thing related to updating Project Server’s workflow. To apply your changes you have to restart “Microsoft Project Server Events Service 2010” and “Microsoft Project Server Queue Service 2010” windows services after deploying solution (workflow). You can do that using the following commands:

@echo off
echo stop Timer..
net stop SPTimerV4
echo stop Queue..
net stop ProjectQueueService14
echo stop Event..
net stop ProjectEventService14
echo start Event..
net start ProjectEventService14
echo start Queue..
net start ProjectQueueService14
echo start Timer..
net start SPTimerV4
echo All done...
rem pause
@echo on
Read more...