Thursday, February 6, 2014

Generation of custom Project IDs without programming

Some people love numeric IDs. It is easier to reference things using short tags like “Project 15” instead of long descriptive names.

However, Project Server uses GUIDs instead of numeric IDs to identify entities. Surely, it is done for good reasons, but GUIDs are rarely useful for human beings. We really prefer simple and short numbers.

Assigning a unique ID to project is easy – just create a custom field for it and track it manually, ensuring proper formatting and uniqueness of the number. Sounds easy, but… We are not in the Stone Age, right? There must be a better way doing that.

There is a way to do it. With our product - FluentPro Formulas for Project Server 2013 (version for Project Server 2010 is available too) it is just as easy as writing the following function on the Edit Custom Field screen:

                NewID()

Done! All new projects will receive a sequential number upon creation. Existing projects will get the number on the next update.

But what if just sequential is not enough? Let’s say we need to make it look something like PR_1, PR_2 and so on?  In this case, expression can look like this:

                ‘PR_’ + NewID()

What if we don’t want to start numbering from 1 and need to start from 1000? Here is what we can do:

                NewID(1000)

Need to increment by 10 instead of 1?

                NewID(1000, 10)

But what if we don’t want to use the same sequence for all projects and prefer to have different sequences for different EPTs?

                NewID(1, 1, [Enterprise Project Type ID])

Of course, there is no restriction to use EPT in this context – it can be any custom or native field.

Last but not least: if you have not more than two fields with FluentPro Formula – the tool is free.

You can learn more about FluentPro Formulas on our website - http://www.fluentpro.com/productsformulas2013.html 
Read more...

Solution Starters for Project Server 2013

Our team migrated Solution Starters for Project Server 2010 to the Microsoft Project Server 2013 platform. We are distributing Solution Starters for Project Server 2013, free of charge, available for download with source code and with wps, ready for deployment. Please note - solution starters are available as-is, free of charge and FluentPro Software Inc, FluentPro Software Corporation are not responsible for any direct and indirect damage. Please try / install solution starters at your own risk.

Available Solution Starters for Project Server 2013:


  • Bulk Edit Tool
  • Excel Project Cost Capture
  • Programs
  • Project Workspace List Viewer
  • Report Builder Tool
  • Workspace Project Custom Field Web Part

We are happy to provide commercial support for Solution Starters. We can help you with all your needs in regards to the Solution Starters:


  • Deployment of Solution Starters to your servers
  • Troubleshooting of Solution Starters deployment
  • Customization of Solution Starters
  • Adding new features to the Solution Starters functionality


You can download solution starters from our website - http://www.fluentpro.com/solutionstarters2013.html .

Enjoy! Read more...

Monday, October 14, 2013

Project Server 2013 Online. OData localization

When you deal with on-premise installation of Project Server 2010/2013, you can use Reporting database to access data for reporting purposes. But when we are talking about Project Server Online, the data access gets more complex. The thing is that you don’t have direct access to the databases, and the only way to get your data is to use OData protocol.

There are lots of nice posts all over the Internet (like this or this) which describe how to use OData with Project Online. One of the features of OData is localization - you request and get the data based on language you like.

This is done by adding language prefix to the URL, something like this:

https://site.sharepoint/sites/pwa/_api/ProjectData/[en-US]/

If you don’t specify the language explicitly, the OData feed will have the same language as your site default one (you’ve specified it during PWA provisioning). But other supported languages are available as well.

One thing we’ve noticed: once you’ve provisioned a new instance of PWA site, you get only one, default locale working. For all other locales you’ll get error messages similar to following:

<m:error xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
   <m:code/>
   <m:message xml:lang="ru-RU">
      The requested langauage en-US is not supported or currently installed.
   </m:message>
</m:error>

Even if you navigate to the BI site, you’ll see reports only for site default language. The good thing is that both reports and OData languages will become available after a while. In our case it was about half of a day. For some reason, the languages are “installed” to the PWA instance with some delay. So if you need this feature working on your PWA site, be patient and you’ll get it… soon…

Read more...

Monday, July 15, 2013

SharePoint JSGrid. Creating custom Widgets

Hi! There are not so many info all over the Internet explaining how to use complex features of SharePoint 2010 JSGrid control, such as custom prop types, display controls and widgets. In this post we’ll show how to create custom prop type and attach custom widget to it. In addition, you’ll see the way to handle data validation for columns of such types.

Creating custom Prop Type

First of all, we should create custom property type. The Init method of JSGrid controller is the place to do that. The easiest way is to derive new type from existing one. We’ll use standard String prop type:
this._MyCustomPropType = createMyCustomPropType('String');
function createMyCustomPropType(basePropType) {
            var newPropType = SP.Internal.JS.object(basePropType);
            newPropType.ID = 'MyCustomPropType';
            newPropType.BeginValidateNormalizeConvert = function (recordKey, fieldKey, newValue, bIsLocalized, fnCallback, fnError) {
                if (!newValue) {
                    fnCallback({ isValid: false, dataValue: newValue, normalizedLocValue: newValue, errorMsg: 'Value is not set.' });
                } else {
                    $.ajax({
                        url: 'some_validation_url',
                        data: {
                            value: newValue
                        },
                        success: function (result) {
                            fnCallback(result);
                        },
                        error: function (request) {
                            fnCallback({ isValid: false, dataValue: newValue, normalizedLocValue: newValue, errorMsg: request.responseText });
                        }
                    });
                }
            };
            newPropType.widgetControlNames = ['MyCustomWidget'];
            return newPropType;
        }

The ID property should be unique to identify the prop type between others.

BeginValidateNormalizeConvert method contains the validation logic. In our case we make the field required and add server-side validation (jQuery ajax call in our case). If you don’t need a delayed validation, you can directly call fnCallback function which comes as an argument of validation fuction.

It is important to set widgetControlNames property of newly created prop type to the same value as the ID of the widget you’d like to use for editing data. Please pay attention that this is not the ID itself, but rather an array containing this ID.

Registering custom Prop Type

When the prop type is created, it should be registered in JSGrid infrastructure. You'll have to specify both edit and display controls for the prop type. In our case we use OOB Text display control and EditBox edit control. The code:

SP.JsGrid.PropertyType.RegisterNewCustomPropType(this._MyCustomPropType,
            SP.JsGrid.DisplayControl.Type.Text, SP.JsGrid.EditControl.Type.EditBox, this._MyCustomPropType.widgetControlNames);

Creating custom Widget

Now you can create and register the widget control itself:

SP.JsGrid.PropertyType.Utils.RegisterWidgetControl(
            'MyCustomWidget',
            function (gridContext) {
                return new MyCustomWidget(gridContext);
            },
            []);
MyCustomWidget = function (gridContext) {
    this.SupportedWriteMode = SP.JsGrid.EditActorWriteType.DataOnly;
    this.SupportedReadMode = SP.JsGrid.EditActorReadType.DataOnly;
    var self = this;
    this.cellContext = null;

    this.Dispose = function () {
    };
    this.GetIcon = function () {
        return new SP.JsGrid.Image('../images/editheader.png').Render('');
    };
    this.OnValueChanged = function () {
    };
    this.Expand = function () {
        var options = {
            allowautoresize: false,
            allowmaximize: false,
            title: 'Pick Resource',
            width: 600,
            height: 400
        };

        SP.UI.ModalDialog.showModalDialog ({
            url: 'some_url',
            args: { },
            dialogReturnValueCallback: function (oDialogResult, oRet) {
                if (oDialogResult) {
                    self.cellContext.SetCurrentValue({ data: oRet.Guid, localized: oRet.Name });
                    self.cellContext.NotifyEditComplete();
                }
                self.Collapse();
            }
});
    };
    this.Collapse = function() {
        this.cellContext.NotifyCollapseWidget();
    };
    this.BindToCell = function (cellContext) {
        this.cellContext = cellContext;
    };
    this.Unbind = function () {
    };
};

The most important part in the widget definition is Expand method. In our case we open modal dialog and use the data returned when the dialog is closed.

The only thing left is to specify the ID of new prop type when the grid data are being built and serialized. PropertyTypeId field of GridField should be used for this purpose.

Read more...

Wednesday, May 22, 2013

Project Server Timesheets. Programmatic tricks and work-arounds

Programmatic update of Project Server timesheets is designed to be done via PSI Timesheets web service. Everything looks pretty simple until your code needs to support the whole set of Project Server Timesheet functionality. Below is the list of “non-trivial” timesheet-related tasks we managed to accomplish.

Updating task actuals in Single Entry Mode. Dealing with new tasks.

Problem - GeneralItemDoesNotExist error on QueueUpdateTimesheet web service method call. The issue was described here. The solution we've proposed was “fixing” timesheet in case if some assignments were created later than timesheet. The workaround would be any minor change of the timesheet, i.e. change the timesheet name to itself to mark the dataset row changed.

Adding new assignments in Single Entry Mode.

This operation requires the same timesheet “fixing” work (described above).

Updating task actuals in Single Entry Mode. Non-administrative tasks.

This action requires additional action – creating StatusingChangeLog row. All the properties should be properly set - ENTITY_TYPE, ENTITY_UID, PID, PROJ_UID and VALUE.

Deleting Assignment rows.

The changes dataset should be appended with new RemovedAssignments data table, which is not in dataset schema. It should contain a row with ASSN_UID column set. In addition, in case if Single Entry Mode is turned on, this action requires adding two StatusingChangeLog rows – with PID s_apid_update_needed and s_apid_removed_by_res.

Determining the erroneous state of the timesheet.

The initial dataset schema does not allow doing this. But this task can be accomplished by checking “invisible” timesheet row column TS_QUEUE_JOB_STATUS.

We’ve spent quite a lot of our time to make these things working. As usual, the working code is encapsulated to the FluentPS library.

Have fun!

Read more...

Monday, April 29, 2013

Project Server 2013 PSI from the web context. Unauthorized issue

There are number of changes in SharePoint 2013 and Project Server 2013. One of them is that the Claims Authentication becomes default authentication mechanism now.

Project Server 2013 is claims aware, so all out of the box functionality works fine with claims authentication. But things get worse when you try to use Project Server ClientOM or PSI services from the web context where the claims authentication used. Result - "Unauthorized" message.

This post is about to show some workarounds to cope with the issue.

The scenario

PSI services are ok to be called from console app, but fail when they are called from the web part in the web application with claims authentication set up. Current user Windows identity is known in the first scenario, which is ok. But in the second scenario current identity is not the identity of logged on user ("iusr" by default). Below are 4 approaches to make PSI working from the web context.

Approach #1. Use Windows classic authentication

Pros: The easiest way; no code changes required.

Cons: Not recommended way. Microsoft highly recommends using claims authentication for all new solutions. The use of classic mode authentication, also known as Windows classic authentication, is discouraged in SharePoint 2013 and you can only create or configure web applications for classic mode authentication with Windows PowerShell cmdlets. (See Create web applications that use classic mode authentication in SharePoint 2013).

Approach #2. Use Application Pool account

This approach is applicable when your web application uses the domain account as the application pool account. In this case you can use "Service account" pattern. For example you can make PSI calls using the application pool account. The code:

SPSecurity.RunWithElevatedPrivileges(delegate()
{
    //PSI call
});

This code impersonates current thread under the application pool identity, so any call within the supplied delegate will be done under the application pool account. Do not forget to grant appropriate permissions to pool account (For example, you can add it to Project Server administrators group). This trick will not work as expected if NETWORK SERVICE account is used as an application pool account. Also, external system will not have access to identity of the original user of web page. It will think that application pool made a call.

Pros: It works :)

Cons: Code changes required; service won't be aware of the actual caller - Project Server impersonation will have to be used; application pool needs to be configured with domain account.

Approach #3. Use UpnLogon method of "Claims to Windows Token Service"

The most complex one. It requires you to manually call the UpnLogon method of “Claims to Windows Token Service”. First, you need to get your current user claims identity. It can be done as follows:

if (Thread.CurrentPrincipal.Identity is ClaimsIdentity)
{
    var identity = (ClaimsIdentity)Thread.CurrentPrincipal.Identity;
}

Than you need to extract UPN-Claim from the identity. It indicates a Kerberos-style user principal name (UPN), for example, user@realm. Only one claim can be of the UPN type. Additional UPNs can be configured as a custom claim types. the code:

string upn = null;
foreach (Claim claim in identity.Claims)
{
   if (StringComparer.Ordinal.Equals(System.IdentityModel.Claims.ClaimTypes.Upn, claim.ClaimType))
   {
       upn = claim.Value;
   }
}

After you got the UPN-Claim, you can get the windows user identity from this claim with help of “Claims to Windows Token Service”. By default, this service is disabled. You need to enable it in SharePoint central administration (See Claims-based identity and concepts in SharePoint 2013). Steps:

1. Go to Central Administration -> System Settings -> Manger services on server and ensure that “Claims to Windows Token Service” state is “Started”.

2. Edit “C:\Program Files\Windows Identity Foundation\v3.5\c2wtshost.exe” and ensure that your application pool account is in the list of allowed callers. The best way to do this is to add your application pool account to WSS_WPG security group. Your “allowedCallers” section would look like this:

<allowedCallers>
    <clear />
    <add value="WSS_WPG" />
</allowedCallers>

More info can be found at Claims to Windows Token Service (C2WTS).

Now you can do the call to “Claims to Windows Token Service”. The code:

WindowsIdentity windowsIdentity = null;
SPSecurity.RunWithElevatedPrivileges(delegate()
{
     windowsIdentity = S4UClient.UpnLogon(upn);
});

Now you can perform the impersonation and actual call:

using (windowsIdentity.Impersonate())
{
    // PSI call
}

Pros: Recommended way to work with legacy systems; this approach does not work if there is no user with given UPN in the domain where “Claims 2 Windows Token Service” is running.

Cons: The manual configuration required; code changes required.

Approach #4. Enable the mapToWindows

There is also fourth solution of this problem. If your application always requires impersonation-level Windows security tokens, then you may choose to enable the mapToWindows property on samlSecurityTokenRequirement on the <securityTokenHandlers> element. You need to enable Claims 2 Windows Token Service” as described above.

Pros: no code change required

Cons: PSI services are not configured to use securityTokenHandlers. Not sure it this can be done for PSI in particular.

Read more...

Monday, March 18, 2013

How to use security tokens in SharePoint / ProjectServer 2013

Hi! As we know, SharePoint / Project Server 2013 Online allows to extend its functionality with help of SharePoint apps. In order to identify users and perform communication between the app and SharePoint itself, special mechanism is proposed - security tokens are used for this purpose. This post describes how to create such app and access SharePoint / Project Server 2013 Online data using security tokens mechanism. In our case we will consider reading Project Server data from provider-hosted web app via OData protocol.

1. Create App for SharePoint 2013 (provider-hosted app) in Visual Studio 2012. MS Office development Tools should be installed for this purpose (Web Platform Instaler is used for this purpose). This will create 2 projects in your solution - SP App project and Web App project.

2. Fill in the ClientId app settings in Web.config of Web project. It should be your Company’s ClientId received when you register as MS Seller here. For dev purposes you can generate it yourself using http://SP_HOST_NAME/SP_SITE_NAME/_layouts/15/AppRegNew.aspx page.

3. Declare all necessary permissions. This can be done in AppManifest.xml file. In our case we are adding Reporting permission for future OData requests.

4. Publish SP App and upload it to the App Store. For development purposes it can be deployed to local App Store.

5. Add app to SharePoint site

6. Click on app icon to get to the hosted site

Hosted site actions:

7. Get context token from request. This can be done with help of TokenHelper class generated by Visual Studio. The code looks like the following:

var contextToken = TokenHelper.GetContextTokenFromRequest(Page.Request);

8. Get ClientContext instance with context token. Code:

var hostWeb = Page.Request["SPHostUrl"];
var clientContext = TokenHelper.GetClientContextWithContextToken(hostWeb, contextToken, Request.Url.Authority);

9. Get access token. Code:

var accessToken = TokenHelper.GetAccessToken(contextToken, targetHost);

10. In case if you need delayed assess to SP / PS data, you can use Refresh token. It can be built from the context token the following way:

var appToken = GetAppToken(Page.Request);
var refreshToken = TokenHelper.ReadAndValidateContextToken(appToken, null).RefreshToken;
var accessToken = new TokenHelper().GetAccessToken(refreshToken, targetPrincipal, targetHost, realm);
public string GetAppToken(HttpRequest request)
{
   string[] paramNames = { "AppContext", "AppContextToken", "AccessToken", "SPAppToken" };
            foreach (string paramName in paramNames)
            {
                if (!string.IsNullOrEmpty(request.Form[paramName])) return request.Form[paramName];
                if (!string.IsNullOrEmpty(request.QueryString[paramName])) return request.QueryString[paramName];
            }
}

11. Obtain client context with access token. Code:

var context = TokenHelper.GetClientContextWithAccessToken(targetUrl, accessToken.AccessToken);

12. Now we can finally perform data request. In our code we use OData:

var req = (HttpWebRequest)WebRequest.Create(url);
       req.Credentials = CredentialCache.DefaultCredentials;
       req.Headers["Authorization"] = "Bearer " + accessToken;
       req.Accept = "application/atom+xml";
var response = req.GetResponse();
Hope these instructions will save someone's time. Have fun!
Read more...