Friday, January 25, 2013

SharePoint & Project Server Remote debugging

In the following text, „Production“ is server environment on which solution is deployed and which you need to debug for errors.  „Test“ is server environment on which your code is located and Visual Studio is installed.
 
User on „Test“ and user on „Production“ must have same username, they do not have to be in the same domain, but, they must have same username / logon name.

1. First check on „Test“ does msvsmon.exe exist in:

C:\Program Files\Microsoft Visual Studio 10.0\Common7\IDE\Remote Debugger\x64\

or in:

C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\Remote Debugger\x64

If you have msvsmon.exe on „Test“ skip step 2 and go to step 3.

2. If msvsmon.exe does not exist, it can be downloaded from the Microsoft's web pages.

(there is 64 bit and 32 bit version, choose the version that matches your environment) 

3. Copy to msvsmon.exe to „Production“ and Run it as Administrator


4. User on „Production“ must have permissions for debugging


5. Build and deploy your solution/wsp to the server (make sure that the versions of application on „Test“ and applications code on „Production“ are the same


6. Copy the .pdb file from your build directory from „Test“  to the GAC folder on the „Production“ 

NOTE: You can't open GAC folder and copy there directly, follow these steps to copy:
· On „Production“ go to Start à Run

· Write c:\windows\assembly\gac_msil

· Now, you can copy file to the folder where your application  is located in GAC

7. Back to „Test“ and open Visual Studio with code of application


8. Go to Debug --> Attach to process


9. Write the name of „Production“ machine in field Qualifier


10. Attach to appropriate (or all) w3wp.exe


It should sit there for a while loading a whole bunch of symbol files


You should now be able to debug the server


Thursday, January 17, 2013

Project Server 2010 - Get group members emails

In one of my previous posts, I showed you how can you get email addresses of all members of a Project Server group using SQL query.

But, SQL query isn't practical if you want to use it in code behind, nor does Microsoft advise accessing project Servers Published database from code behind through SQL queries. It is always better to use PSI functions.

This is the code for accessing user emails using PSI functions:

  
public static bool GetMembersEmails(Guid projectUid, string pGroupName)
{
   var logService = new LogService();
   var sessionService = new PSSessionService()
   {
       HostName = "serverName", // your PWA host name
       SiteName = "pwa" // your PWA site name
   };

   // we need this user for LoginContext (it can be read from app settings)
   string _user = "Username";
   string _userPwd = "Pass";
   string _userDomain = "Domain";

   using (var _loginCtx = new LoginContext(_userDomain + "\\" + _user, _userPwd, sessionService))
   {

       sessionService.SetLoginContext(_loginCtx);
       FluentPS.Services.Impl.PsiContextService psiContextService = new PsiContextService();
       FluentPS.Services.Impl.PSISvcsFactory psiSvcsFactory = new PSISvcsFactory(sessionService, psiContextService);

       FluentPS.WebSvcProject.Project svcProject = psiSvcsFactory.CreateSvcClient<FluentPS.WebSvcProject.Project>();
       FluentPS.WebSvcResource.Resource svcResource = psiSvcsFactory.CreateSvcClient<FluentPS.WebSvcResource.Resource>();
       FluentPS.WebSvcSecurity.Security svcSecurity = psiSvcsFactory.CreateSvcClient<FluentPS.WebSvcSecurity.Security>();

       try
       {
            List<Guid> _userGuidList = new List<Guid>();
            List<string> _emailsList = new List<string>();

            Guid _groupGuid = Guid.Empty;
            using (var dsSecurityGroups = svcSecurity.ReadGroupList())       
            {
                 SecurityGroupsDataSet.SecurityGroupsDataTable _groups = dsSecurityGroups.SecurityGroups;
                 for (int i = 0; i < _groups.Count; i++)
                 {
                      if (_groups[i].WSEC_GRP_NAME == pGroupName.Trim().Split('@')[0])
                      {
                           _groupGuid = _groups[i].WSEC_GRP_UID;
                      }
                 }
                
if (_groupGuid.Equals(Guid.Empty))  return null;
            }

            using (var dsSecurityGroupsMem = svcSecurity.ReadGroup(_groupGuid))
            {
                 //read group members' uids
                 for (int i = 0; i < dsSecurityGroupsMem.GroupMembers.Count; i++)
                 {
                      Guid _userGuid = dsSecurityGroupsMem.GroupMembers[i].RES_UID;
                      _userGuidList.Add(_userGuid);
                 }
            }

            foreach (Guid _resUid in _userGuidList)
            {
                 using (var dsResource = svcResource.ReadResource(_resUid))
                 {
                      var resourceRow = dsResource.Resources.FindByRES_UID(_resUid);
                      _emailsList.Add(resourceRow.WRES_EMAIL);                                         }
            }

            return _emailsList;
 
       }
       catch (Exception ex)
       {
            return false;
       }
   }
}

Monday, January 14, 2013

Project Server update custom lookup field

This post will show you how to update Custom field in Project Server that contains values of a Lookup table using PSI functions from FluentPS library (described in this post) which is free and easy to use but the same PSI functions can be used when you set your own call of Project Server web services.

Let's say we have custom field called "Product" and it contains values from Lookup table called "Products". Lookup table "Products" contains values: "TV", "Mobile phone", "Laptop", etc.

We need to set (or change) value of Custom field (from "TV") to "Mobile phone".




This is the code to do it:

        public bool SetProduct(string projectUid)
        {        
            Guid projectGuid = new Guid(projectUid);

            if (projectGuid.Equals(Guid.Empty)) return false;
            var logService = new LogService();
            var sessionService = new PSSessionService()
            {
                HostName = "server_name",
                SiteName ="PWA"
            };

             FluentPS.Services.Impl.PsiContextService psiContextService = new PsiContextService();
             FluentPS.Services.Impl.PSISvcsFactory psiSvcsFactory = new                 PSISvcsFactory(sessionService, psiContextService);

             FluentPS.WebSvcLookupTable.LookupTable svcLookupTable = psiSvcsFactory.CreateSvcClient<FluentPS.WebSvcLookupTable.LookupTable>();
             FluentPS.WebSvcCustomFields.CustomFields svcCustomFields = psiSvcsFactory.CreateSvcClient<FluentPS.WebSvcCustomFields.CustomFields>();

             FluentPS.WebSvcProject.Project svcProject = psiSvcsFactory.CreateSvcClient<FluentPS.WebSvcProject.Project>();


             FluentPS.WebSvcProject.ProjectDataSet tProjEntities = svcProject.ReadProjectEntities(projectGuid, 32, FluentPS.WebSvcProject.DataStoreEnum.WorkingStore);
             FluentPS.WebSvcProject.ProjectDataSet tProject = svcProject.ReadProject(projectGuid, FluentPS.WebSvcProject.DataStoreEnum.WorkingStore);

             try
             {
                 //Guid of lookup table (Product) in which we look for value (Mobile phone)
                 Guid _lookupTableUid = Guid.Empty;
                 //MD Guid of lookup custom field
                 Guid _lookupCustomFieldGuid = Guid.Empty;
                 int _lookupId = 0;
                 byte _lookupByte = 0;
                 bool _customFieldAlreadyFilled = false;

                 FluentPS.WebSvcCustomFields.CustomFieldDataSet customFieldsDs = svcCustomFields.ReadCustomFields("", false);

                 FluentPS.WebSvcCustomFields.CustomFieldDataSet.CustomFieldsDataTable cfDataTable = customFieldsDs.CustomFields;
                 for (int i = 0; i < cfDataTable.Count; i++)
                 {
                        if (cfDataTable[i].MD_PROP_NAME == "Product")
                        {
                            _lookupCustomFieldGuid = cfDataTable[i].MD_PROP_UID;
                            _lookupTableUid = cfDataTable[i].MD_LOOKUP_TABLE_UID;
                            _lookupId = cfDataTable[i].MD_PROP_ID;
                            _lookupByte = cfDataTable[i].MD_PROP_TYPE_ENUM;
                            break;
                        }

                 }



                 if (_lookupCustomFieldGuid == Guid.Empty || _lookupTableUid == Guid.Empty)  
                           return false;

                 Guid _lookupTableValueGuid = Guid.Empty;
                 using (FluentPS.WebSvcLookupTable.LookupTableDataSet lookupTableDs = svcLookupTable.ReadLookupTables(string.Empty, false, 1033))
                 {
                        //now we look for guid that contains value "Mobile phone" in lookup table
                        for (int i = 0; i < lookupTableDs.LookupTableTrees.Count; i++)
                        {
                            try
                            {
                                if (lookupTableDs.LookupTableTrees[i].LT_UID == _lookupTableUid && lookupTableDs.LookupTableTrees[i].LT_VALUE_TEXT == "Mobile phone")
                                {
                                    _lookupTableValueGuid = lookupTableDs.LookupTableTrees[i].LT_STRUCT_UID;
                                    break;
                                }
                            }
                            catch (Exception)
                            {
                               
                            }

                        }

                 }

                 if (_lookupTableValueGuid == Guid.Empty) return false;
                 
                 foreach (FluentPS.WebSvcProject.ProjectDataSet.ProjectCustomFieldsRow cfRow in tProject.ProjectCustomFields)
                 {
                        if (cfRow.MD_PROP_UID == _lookupCustomFieldGuid)
                        {
                            cfRow.CODE_VALUE = _lookupTableValueGuid;

                            _customFieldAlreadyFilled = true;
                        }
                 }

                 //if Custom field exists, but it is empty, then we need to add it to project
                 if (!_customFieldAlreadyFilled)
                 {
                        var cfrow = tProject.ProjectCustomFields.NewProjectCustomFieldsRow();

                        cfrow.CUSTOM_FIELD_UID = Guid.NewGuid();
                        cfrow.FIELD_TYPE_ENUM = _lookupByte;
                        cfrow.MD_PROP_ID = _lookupId;
                        cfrow.MD_PROP_UID = _lookupCustomFieldGuid;
                        cfrow.PROJ_UID = new Guid(projectUid);
                        cfrow.CODE_VALUE = _lookupTableValueGuid;

                        tProject.ProjectCustomFields.AddProjectCustomFieldsRow(cfrow);

                 }

                 //Update of new field and project
                 Guid sessionUid = tProject.Project[0].PROJ_SESSION_UID;

                 Guid jobUid;

                 var tQueueService = new PSQueueSystemService(logService, psiSvcsFactory.CreateSvcClient<FluentPS.WebSvcQueueSystem.QueueSystem>());
                  
                 jobUid = Guid.NewGuid();
                 svcProject.CheckOutProject(projectGuid, sessionUid, "Checked-out");
                 svcProject.QueueUpdateProject(jobUid, sessionUid, tProject, false);
                 tQueueService.WaitForQueue(jobUid);
                 jobUid = Guid.NewGuid();
                 svcProject.QueuePublish(jobUid, projectGuid, true, null);
                 tQueueService.WaitForQueue(jobUid);
                 jobUid = Guid.NewGuid();
                 svcProject.QueueCheckInProject(jobUid, projectGuid, true, sessionUid, "Checked-in");
                 tQueueService.WaitForQueue(jobUid);

                 return true;

             }
             catch (Exception)
             {
                    return false;
             }
           
   }

In my next post, I will show how to read value from Custom field that contains value from Lookup table.

Friday, January 11, 2013

How to use Project Server PSI functions - FluentPS library

All Windows-based and Web-based client applications for Microsoft Project 2010 use the Project Server Interface (PSI), a set of Web services built on the Microsoft .NET Framework 3.5 and the Windows Communication Foundation (WCF).


If you just starting with developing for Project Server, arm yourself with patience. Setting up and using PSI services can be really frustrating. Setting up all endpoints, credentials and security settings by Microsoft's instructions is time consuming and time is luxury in programmers world.

Luckily, there is a much simpler solution. There is a project on Codeplex, called FluentPS. So if you need to use PSI function right away, just download this awesome project from Codexplex and start coding.


In the next few posts, I will show you how to use PSI function to get or set Custom fields, Lookup tables, access user groups and more.

Here are some example posts using PSI with FluentPS:

Read value from Custom Field: post

Update value from Custom Field: post

Get emails of members of Project Server group: post

Restart workflow: post

Change Enterprise Project Type: post

Check if project is checked-out to current user: post 

Saturday, January 5, 2013

Project Server 2010 Workflow - appSettings

If you are using custom workflows in Project Server 2010 like BranchingWorkflow from Codeplex or DM Dynamic Workflow from Microsoft, you can customize it in way that suits you by altering their source code.

But, the problem is that you can't set an application setting in config file and read that setting from workflow. If you try to read app setting from your code like this:

string _var1 = System.Configuration.ConfigurationManager.AppSettings.Get("MySetting");

it will always return null.

 Workflow doesn't read Sharepoint's application pool config file.

It read from another config file; Microsoft.Office.Project.Server.Queuing.exe.config, and it is located in:
C:\Program Files\Microsoft Office Servers\14.0\Bin

But, if you try to add your appSetting there, you will still get null as a result.

So, where is the actual config?

It is called machine.config and it is located in:
C:\Windows\Microsoft.NET\Framework64\v2.0.50727\CONFIG

In that file, you can add your app setting like this:

<appSettings>
        <add key="MySetting" value="MyVar" />
</appSettings>

and read it from workflow code:

string _var1 = System.Configuration.ConfigurationManager.AppSettings.Get("MySetting");

Now, _var1 will have value  "MyVar".


EDIT:

There is a better way of reading values from Application Settings of Web Application then messing with machine.config. More about it in this post.