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...

SharePoint 2010 Ribbon Customization - controls and commands deployment

Introduction

One of the most valuable interface changes provided in SharePoint 2010 is the new Ribbon. This contextual interface allows users to execute any action related to ribbon controls depending upon the context the user is currently dealing with. The SharePoint 2010 API allows developers to extend and customize the ribbon using SharePoint features on site/site collection level. This article shows the way how to do that.

There are several types of controls which can be deployed to SharePoint Ribbon:

  • Button,
  • CheckBox,
  • DropDown,
  • FlyoutAnchor
  • and ToggleButton.


These controls can be collected for usability purposes to containers such as Group and Tab. Therefore it is possible to add controls with custom functionality not only to existing containers, but deploy new tabs and groups, and then and add necessary controls to them.

Any ribbon customization should be mounted within xml in feature declaration. The special xml tag exists for this purpose. Here is the example how it usually looks like:

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
    ...<br />    <CustomAction
      Id="[Your_custom_action_ID]"
      Location="CommandUI.Ribbon"
      Title="Custom Action title here">
        <CommandUIExtension>
            <CommandUIDefinitions>
            ...
            </CommandUIDefinitions>
            <CommandUIHandlers>
            ...
            </CommandUIHandlers>
        </CommandUIExtension>
    </CustomAction>
    ...
</Elements>

The CustomAction xml tag should have its Id in corresponding attribute – unique text value. The value for Location attribute should always be set to "CommandUI.Ribbon", independently on what kind of ribbon customization it is created for. The CommandUIDefinitions tag should contain CommandUIDefinition child tags declaring all controls or containers which should be created (i.e. buttons, groups, tabs, etc.).

The CommandUIHandlers tag can contain commands declarations for registered controls, so some JavaScript code can be executed. It is not necessary to register all commands in custom action – it can be done on server (webpart/page code-behind). The way how to do it will be described in next posts.

IMPORTANT. If you have deployed some ribbon customization xml and later have to provide some changes into it (change JavaScript for command handler, change FlyoutAnchor control sub-items etc.) it might be necessary to change the Feature ID for SharePoint feature your customization belongs to. For simple modifications (i.e. changing button titles, image URLs etc.) this is not necessary, but if you change the Feature ID every time you change anything in xml, it might save a lot of time and effort.

Adding custom button to existing group

Here is the example of declaring button control to be added to the ribbon which should be added to CommandUIDefinitions xml node:

<CommandUIDefinition Location="[Existing_Group_ID].Controls._children">
  <Button<br />    Id="[Your_Button_ID]"
    Sequence="20"
    Command="[Your_Command_ID]"
    LabelText="Custom Button Label"
    Alt="Custom Button alt text"
    Image16by16="/_layouts/images/RibbonCustomization/images16x16.png"
    Image16by16Top="-16"
    Image16by16Left="-32"
    Image32by32="/_layouts/images/RibbonCustomization/images32x32.png"
    Image32by32Top="0"
    Image32by32Left="-64"
    TemplateAlias="o1"
    ToolTipTitle="Custom Button"
    ToolTipDescription="Executes custom action" />
</CommandUIDefinition>

The Location attribute of CommandUIDefinition xml element should be constructed in the following way: [Existing_Group_ID].Controls._children. You can find group’s ID in its declaration xml or using IE Dev Toolbar (the Group node is rendered as <li id="[TabID]">…</li> html tag).

It is possible to add some image to the button. For this purpose you should specify image URLs in Image16by16 and Image32by32 attributes (Image16by16 image appears if group size is too small for 32x32 image). Attributes i.e. Image32by32Top and Image32by32Left are used to set top/left margins for the images.

To perform some action on deployed control the CommandUIHandler xml element should be added to CommandUIHandlers xml node, and its name should be set to the Command attribute of the control (you don’t need to do it in case if you add command handler programmatically). Here is the example:

<CommandUIHandler
  Command="[Your_Command_ID]"
  CommandAction="javascript:alert('Button clicked.');"
  EnabledScript="true" />

Any ribbon control should have TemplateAlias attribute. Its value determines the location of the control inside the group (like web part zones in page layout) and depends upon the group’s template the control is being added to. To determine what value should be used here, you should check the group’s Template attribute, than find corresponding GroupTemplate template declaration xml tag (predefined templates can be found in c:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\TEMPLATE\GLOBAL\XML\CMDUI.xml file) and choose the TemplateAlias attribute value of necessary ControlRef xml sub-node.

Adding custom group to existing tab

Here is the example of adding new group to existing tab:

<CommandUIDefinition Location="[ExistingTabID].Scaling._children">
  <MaxSize 
    Id="[Your_Group_ID].MaxSize"
    GroupId="[Your_Group_ID]"
    Size="LargeLarge" 
    Sequence="10" />
</CommandUIDefinition>
<CommandUIDefinition Location="[ExistingTabID].Scaling._children">
    <Scale
    Id="[Your_Group_ID].Popup"
    GroupId="[Your_Group_ID]"
    Size="Popup"
  Sequence="20" />
</CommandUIDefinition>
<CommandUIDefinition Location="[ExistingTabID].Groups._children">
  <Group
    Id="[Your_Group_ID]"
    Sequence="1"
    Title="Custom Group"
    Template="Ribbon.Templates.Flexible2"
    Image32by32Popup="/_layouts/images/RibbonCustomization/images32x32.png"
    Image32by32PopupTop="-128"
    Image32by32PopupLeft="-192">
      <Controls Id="Ribbon.WebPartPage.CustomGroup.Controls">
          ...
      </Controls>
  </Group>
</CommandUIDefinition>

In this sample you should pay attention to the following:

  1. Three different CommandUIDefinition xml nodes should be created: for deploying MaxSize, Scale and Group nodes; MaxSize and Scale nodes should reference to the group which is currently being declared (using GroupId attribute); the Size attributes should be based on the group template. If no MaxSize and Scale declarations for new group is created, on incorrect values of Size attributes are specified, the group won’t be visible or no controls should be shown in it;
  2. The Location attributes of CommandUIDefinition xml elements should be constructed in the following way: [ExistingTabID].Scaling._children – for MaxSize and Scale nodes; [ExistingTabID].Groups._children – for Group node;
  3. All controls for this group can be declared directly in the Controls node inside the group declaration xml.

Adding custom tab to ribbon

Here is the example of declaring custom ribbon tab:

<CommandUIDefinition Location="Ribbon.Tabs._children">
  <Tab
    Description="Custom Tab"
    Id="[Your_Tab_ID]"
    Sequence="1"
    Title="Custom Tab">
      <Scaling Id="[Your_Tab_ID].Scaling">
      ...
      </Scaling>
      <Groups Id="[Your_Tab_ID].Groups">
      ...
    </Groups>
  </Tab>
</CommandUIDefinition>

In this sample you should pay attention to the following:

  1. The Location attribute of CommandUIDefinition xml element should be exactly the following: Ribbon.Tabs._children;
  2. All inner groups and scaling xml nodes for them can be declared directly in Scaling and Groups xml nodes inside the tab declaration node respectively.
    You should make your tab visible in the page you are working with. This should be done programmatically in either page of Web Part classes. Here is the code sample with such functionality:
protected override void OnLoad(EventArgs e)
{
    base.OnLoad(e);

    var ribbon = SPRibbon.GetCurrent(Page);
    if (ribbon != null)
    {
        ribbon.Minimized = false;
        ribbon.CommandUIVisible = true;
        const string initialTabId = "RibbonCustomization.CustomTab";
        if(!ribbon.IsTabAvailable(initialTabId))
            ribbon.MakeTabAvailable(initialTabId);
        ribbon.InitialTabId = initialTabId;
    }
}

The code line #13 will make the tab active by default when the page is opened.

In the next post it will be shown how to add controls’ command handlers programmatically. It might be helpful if you have to construct command script/enabled script JavaScript code for your ribbon controls in runtime or to handle ribbon commands on server.

Read more...

Project Server JS customization: SharePoint 2010 WebPart Callback Interface

SharePoint 2010 webpart client-server interoperation model provides new asynchronous callback interface which allows saving webpart’s data to server and retrieving back the operation result. New model enables users to control webpart’s view mode, perform webpart’s content validation and the asynchronous save operation with handling callbacks. The webpart save operation is tied to the global JavaScript command queue and can be triggered either by calling PDP JavaScript objects’ methods or directly by clicking the Save Ribbon button.

In this post we’ll focus on general requirements that the custom webpart should meet to support asynchronous client-server interoperation as it’s done in the Project Fields webpart.

Let’s consider client JavaScript code that is needed for the webpart having been placed on a PDP:

var WPDP_WebPartID = new object();
WPDP_WebPartID.IsDirty = false;

WPDP_WebPartID.EnablePart = function pfp_EnablePart(enabled) {
    // view mode logic
}

WPDP_WebPartID.Validate = function pfp_Validate() {
    // validation logic
    // possible return true;
}

WPDP_WebPartID.Save = function pfp_Save(ctx) {
    // server callback logic
    // possible calls of ctx.Completed() or WebForm_DoCallback() methods.
}

Let’s consider the code in detail:

WebPart JavaScript object contains at least four members: flag IsDirty and three functions: EnablePart, Validate and Save.

IsDirty flag. Signals that user has made changes to the webpart controls, e.g. entered text to input fields, selected new lookup field value etc.

EnablePart method. Accepts boolean argument “enabled” which tells webpart about current view mode. Ususally webpart set its inner controls to appropriate state according to the “enabled” agrument value. The method is called during the page loading.

Validate method. Is called right when the webpart is about to send the data back to server, usually after user clicked “Save” ribbon button. Should return a boolean value that indicates whether save method is going to be called or not. If the user entered inacceptable information into the fields, validation summary might be displayed on the page and Validate method should return “false” then. Returning “true” indicates about a successul data validation.

Save method. The most important part of the server-client webpart asynchronous interoperation. Basically, the method body should contain the WebForm_DoCallback method call that actually performs asynchronous data sending.

Among a set of the method arguments there is a couple of important ones:

  • arg holds the data collected from the webpart HTML controls.
  • ctx is a client script that is evaluated on the client prior to initiating the callback. The result of the script is passed back to the client event handler. Should be set to incoming ctx argument.

Exact WebForm_DoCallback method signature should be generated by calling ClientScriptManager.GetCallbackEventReference method (for more information check out at http://msdn.microsoft.com/en-us/library/ms153106.aspx).

E.g.:
string callBackScript = Page.ClientScript.GetCallbackEventReference(
    this,
    "arg",
    "[SaveCompletedCallback JavaScript method reference]",
    "ctx",
    "[SaveErrorCallback JavaScript method reference]",
    false);

The SaveCompletedCallback is the name of the client event handler that receives the result of the successful server event.

The SaveErrorCallback is the name of the client event handler that receives the result when an error occurs in the server event handler.

The SaveCompletedCallback and SaveErrorCallback methods examples are below.

function SaveCompletedCallback_WebPartClientID(result, ctx) {
    if (result != '') {
        SaveErrorCallback_WebPartClientID(result, ctx);
    }
    else {
        ctx.Completed();
    }
}

function SaveErrorCallback_WebPartClientID(result, ctx) {
    ctx.FailedShowInlineErrors(result);
}

The result argument is the result of the callback. The ctx.Completed() call informs about the successful callback operation, whereas the ctx.FailedShowInlineErrors(result) call shows the callback errors.

The server side webpart class should implement the ICallbackEventHandler interface:

  • The RaiseCallbackEvent method Processes the callback event, fires when the client data arrives to the server.
  • The GetCallbackResult method returns the results of the callback event.

Next time we will provide a complete webpart sample with a server-side C# webpart class, ICallbackEventHandler interface implementation and all needed JavaScript code generation.

Read more...

Tuesday, January 5, 2010

Project Server entity mapping: Introduction

Enterprise solutions development usually means a next level of business logic implementation complexity. One of the keys to success and maintainability is unit testing which introduces some requirements for the source. Each platform has specific features and data containers. Project Server uses DataSet-based protocol for PSI web services, which is good for large data structures transmission, but forces us to copy source parts between methods and after several months maintenance becomes a real nightmare. In order to avoid extra complexity it is convenient to separate DataSets and PSI calls to services layer and concentrate on business rules.

Let’s say we need to work with base project data (ID and Name) and one custom field “Sample Assumptions”. The base project data is stored in “Project” table and custom fields are in “ProjectCustomFields”. To load project with our custom field we need to search for data in two tables and handle all necessary data type conversion. Let’s imagine that we have number of different custom fields. Every new custom field will increase number of lines in your project and make you to write additional source at least in two places: load and update project data.


To handle all this stuff we implemented services which use entity metadata to map data from DataSets to our custom entity. Entity class for our example will look like:
public class Project
{
[PSEntityUidField(ColumnName = ProjectCustomFieldsColumnNames.PROJ_UID)]
public Guid Uid { get; set; }

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

[PSCustomField("{381b5bb4-2e88-4f73-86e7-f6ffb5e58958}", FieldName = "Sample Assumptions")]
public string SampleAssumptions { get; set; }
}
Now, having all needed mapping data, we can work with project data in the following manner:

This feature brings us a more clean, reusable and testable code which is highly important for big projects and support stuff. Every time you need to get some other project’s data you’ll just add a new property with mapping info attribute.

Next time we’ll talk about PSProjectService implementation.
Read more...