Second Life of a Hungarian SharePoint Geek

November 23, 2014

Managing Project Server Enterprise Custom Fields via the Managed Client Object Model

Filed under: ALM, Managed Client OM, Project Server — Tags: , , — Peter Holpar @ 07:39

In the previous post I described, how to manage Project Server lookup tables via the managed client object model. In this current post I  provide you some code snippets that help to manage enterprise custom fields.

As I wrote in a former post, we can deploy Project Server entities declaratively, via SharePoint solution packages (WSP), see that post for the details. An example for enterprise custom field (with and without referencing lookup tables) is included below. In the first part we define a lookup table we refer to later, see the second part for the field definitions:

  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <ProjectServerEntities xmlns="http://schemas.microsoft.com/sharepoint/">
  3.   <LookupTables>
  4.     <LookupTable Id="E7397277-1AB0-4096-B2DD-57029A055BA4" Name="YourLookupTable">
  5.       <SortOrder>UserDefined</SortOrder>
  6.       <Mask>
  7.         <MaskLength>0</MaskLength>
  8.         <MaskType>CHARACTERS</MaskType>
  9.         <MaskSeparator>.</MaskSeparator>
  10.       </Mask>
  11.       <LookupEntry>
  12.         <Id>8D3CFDD8-566B-4FA4-8C01-E315BCF13E74</Id>
  13.         <ParentId />
  14.         <SortIndex>86</SortIndex>
  15.         <Value>
  16.           <TextValue>Value1</TextValue>
  17.         </Value>
  18.       </LookupEntry>
  19.       <LookupEntry>
  20.         <Id>8A913280-C8D5-4E85-AE73-526EF5ABC686</Id>
  21.         <ParentId />
  22.         <SortIndex>100</SortIndex>
  23.         <Value>
  24.           <TextValue>Value2</TextValue>
  25.         </Value>
  26.       </LookupEntry>
  27.       <LookupEntry>
  28.         <Id>C89A98F0-795A-49EC-9031-BA892CE0D610</Id>
  29.         <ParentId />
  30.         <SortIndex>114</SortIndex>
  31.         <Value>
  32.           <TextValue>Value3</TextValue>
  33.         </Value>
  34.       </LookupEntry>
  35.     </LookupTable>
  36.   </LookupTables>
  37.  
  38.   <CustomFields>
  39.     <CustomField Name="CustomProjectFieldTextWithLookupValue" Id="7574B64B-F230-4F38-ACB6-1C8E4E3D96DD">
  40.       <FieldType>TEXT</FieldType>
  41.       <EntityType>TaskEntity</EntityType>
  42.       <LookupTableUid>E7397277-1AB0-4096-B2DD-57029A055BA4</LookupTableUid>
  43.       <LookupAllowMultiSelect>false</LookupAllowMultiSelect>
  44.       <LookupDefaultValue></LookupDefaultValue>
  45.       <IsRequired>false</IsRequired>
  46.       <IsWorkflowControlled>false</IsWorkflowControlled>
  47.       <IsMultilineText>false</IsMultilineText>
  48.     </CustomField>
  49.     <CustomField Name="CustomTaskFieldFlag" Id="3ED6B19F-BB55-46B6-B3E0-08E68F56110E">
  50.       <FieldType>FLAG</FieldType>
  51.       <EntityType>ProjectEntity</EntityType>
  52.       <LookupTableUid></LookupTableUid>
  53.       <LookupAllowMultiSelect>false</LookupAllowMultiSelect>
  54.       <LookupDefaultValue></LookupDefaultValue>
  55.       <IsRequired>false</IsRequired>
  56.       <IsWorkflowControlled>false</IsWorkflowControlled>
  57.       <IsMultilineText>false</IsMultilineText>
  58.     </CustomField>
  59.  
  60.   </CustomFields>
  61. </ProjectServerEntities>

In the first code snippet we simply dump out some of the most important information of our enterprise custom fields.

  1. using (var projectContext = new ProjectContext(pwaUrl))
  2. {
  3.     projectContext.Load(projectContext.CustomFields,
  4.         cfs => cfs.Include(
  5.             cf => cf.Name,
  6.             cf => cf.Id,
  7.             cf => cf.FieldType,
  8.             cf => cf.LookupTable.Name));
  9.     projectContext.ExecuteQuery();
  10.  
  11.     projectContext.CustomFields.ToList().ForEach(cf =>
  12.     {
  13.         Console.WriteLine("Name [{0}], Id [{1}], FieldType [{2}], LookupTable [{3}]",
  14.             cf.Name, cf.Id, cf.FieldType, (cf.LookupTable.ServerObjectIsNull == true) ? "none" : cf.LookupTable.Name);
  15.     });
  16. }

Note the condition we use to decide if there is a lookup table associated with this field:

cf.LookupTable.ServerObjectIsNull == true

Personally, I don’t like code where one compares a logical variable to true or false instead of using that variable as the condition itself, but the type of the ServerObjectIsNull property is nullable (bool?), and I found a direct comparison simpler as

(cf.LookupTable.ServerObjectIsNull.HasValue) && (cf.LookupTable.ServerObjectIsNull.Value)

Alternatively, you can use the IsPropertyAvailable method for the condition (it returns a bool not bool?) as shown below:

!cf.LookupTable.IsPropertyAvailable("Name")

Note the exclamation mark at the beginning, as we check the opposite as in the case of ServerObjectIsNull property (property is available with IsPropertyAvailable vs. not available in case of ServerObjectIsNull).

The next sample shows how to create a flag-type enterprise custom field for the task entity:

  1. using (var projectContext = new ProjectContext(pwaUrl))
  2. {
  3.     var fieldInfo = new CustomFieldCreationInformation
  4.     {
  5.         Id = Guid.NewGuid(),
  6.         Name = "FlagFieldName",
  7.         EntityType = projectContext.EntityTypes.TaskEntity,
  8.         FieldType = CustomFieldType.FLAG,
  9.     };
  10.  
  11.     projectContext.CustomFields.Add(fieldInfo);
  12.     projectContext.CustomFields.Update();
  13.  
  14.     projectContext.ExecuteQuery();
  15. }

You can simply assign a lookup table as well, as illustrated below, where we create a text-type enterprise custom field for the project entity:

  1. using (var projectContext = new ProjectContext(pwaUrl))
  2. {
  3.     projectContext.Load(projectContext.LookupTables, lts => lts.Include(lt => lt.Name));
  4.     projectContext.ExecuteQuery();
  5.  
  6.     var lookupTable = projectContext.LookupTables.First(lt => lt.Name == "LookupTableName");
  7.  
  8.     var fieldInfo = new CustomFieldCreationInformation
  9.     {
  10.         Id = Guid.NewGuid(),
  11.         Name = "TextFieldName",
  12.         EntityType = projectContext.EntityTypes.ProjectEntity,
  13.         FieldType = CustomFieldType.TEXT,
  14.         LookupTable = lookupTable
  15.     };
  16.  
  17.     projectContext.CustomFields.Add(fieldInfo);
  18.     projectContext.CustomFields.Update();
  19.  
  20.     projectContext.ExecuteQuery();
  21. }

Note, that we request a list of all lookup tables in a separate batch in this case, the select the right lookup table by name. Of course, if you know the ID (uid or objectId) of your lookup table, you don’t need that extra request, as you can use either

var uidOfTheLookupTable = new Guid("54338ffd-d0fa-e311-83c6-0050a3245643");
var lookupTable = projectContext.LookupTables.GetByGuid(uidOfTheLookupTable);

or

var objectIdOfTheLookupTable = "98238ffd-983a-e431-83c6-00f5e3249834";
var lookupTable = projectContext.LookupTables.GetById(objectIdOfTheLookupTable);

to get the reference directly, instead of a lookup by name.

In the next example, we delete a custom field by name. Again, we use two batches, one for the lookup of the custom field by name, and a second one for the deletion itself. As in the former example, we can reduce the number of batches to one, if we know the ID of the field, and get it either by the GetByGuid or by the GetById method,

  1. using (var projectContext = new ProjectContext(pwaUrl))
  2. {
  3.     projectContext.Load(projectContext.CustomFields, cfs => cfs.Include(cf => cf.Name));
  4.     projectContext.ExecuteQuery();
  5.  
  6.     var custField = projectContext.CustomFields.First(cf => cf.Name == "FieldName1");
  7.  
  8.     custField.DeleteObject();
  9.     projectContext.ExecuteQuery();
  10. }

If you have to delete more fields, you don’t need to send a separate request (or two requests) for each deletion. If you use this last example, you can reduce the number of requests for the field removal:

  1. var fieldNames = new List<string> { "FieldName1", "FieldName2", "FieldName3" };
  2.  
  3. using (var projectContext = new ProjectContext(pwaUrl))
  4. {
  5.     projectContext.Load(projectContext.CustomFields, cfs => cfs.Include(cf => cf.Name));
  6.     projectContext.ExecuteQuery();
  7.  
  8.     // query made case insensitive
  9.     fieldNames.ConvertAll(fn => projectContext.CustomFields
  10.         .FirstOrDefault(cf => cf.Name.ToLower() == fn.ToLower()))
  11.         .Where(fn => fn != null).ToList()
  12.         .ForEach(fn => fn.DeleteObject());
  13.  
  14.     projectContext.ExecuteQuery();
  15. }

November 22, 2014

Managing Project Server Lookup Tables via the Managed Client Object Model

Filed under: ALM, Managed Client OM, Project Server — Tags: , , — Peter Holpar @ 08:25

In my former post I described, how we can manage Project Server event handlers via the managed client object model. In the current post I show you how to do that in case of lookup tables.

As I wrote in the previous post, we can deploy Project Server entities declaratively, via SharePoint solution packages (WSP), see that post for the details. An example for lookup tables is included below:

  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <ProjectServerEntities xmlns="http://schemas.microsoft.com/sharepoint/">
  3.   <LookupTables>
  4.     <LookupTable Id="E7397277-1AB0-4096-B2DD-57029A055BA4" Name="YourLookupTable">
  5.       <SortOrder>UserDefined</SortOrder>
  6.       <Mask>
  7.         <MaskLength>0</MaskLength>
  8.         <MaskType>CHARACTERS</MaskType>
  9.         <MaskSeparator>.</MaskSeparator>
  10.       </Mask>
  11.       <LookupEntry>
  12.         <Id>8D3CFDD8-566B-4FA4-8C01-E315BCF13E74</Id>
  13.         <ParentId />
  14.         <SortIndex>86</SortIndex>
  15.         <Value>
  16.           <TextValue>Value1</TextValue>
  17.         </Value>
  18.       </LookupEntry>
  19.       <LookupEntry>
  20.         <Id>8A913280-C8D5-4E85-AE73-526EF5ABC686</Id>
  21.         <ParentId />
  22.         <SortIndex>100</SortIndex>
  23.         <Value>
  24.           <TextValue>Value2</TextValue>
  25.         </Value>
  26.       </LookupEntry>
  27.       <LookupEntry>
  28.         <Id>C89A98F0-795A-49EC-9031-BA892CE0D610</Id>
  29.         <ParentId />
  30.         <SortIndex>114</SortIndex>
  31.         <Value>
  32.           <TextValue>Value3</TextValue>
  33.         </Value>
  34.       </LookupEntry>     
  35.     </LookupTable>
  36.   </LookupTables>
  37. </ProjectServerEntities>

Alternatively, we can deploy our entities (including the lookup table) via the managed client object model. I prefer this approach to the declarative option, as I feel more control on the process of what / when deployed / retracted.

First, we can list the lookup tables (including value sets) using the code below:

  1. using (var projectContext = new ProjectContext(pwaUrl))
  2. {
  3.  
  4.     projectContext.Load(projectContext.LookupTables,
  5.         lts => lts.Include(
  6.                     lt => lt.Name,
  7.                     lt => lt.Id,
  8.                     lt => lt.FieldType,
  9.                     lt => lt.Entries.Include(
  10.                         lte => lte.FullValue,
  11.                         lte => lte.Id)));
  12.     projectContext.ExecuteQuery();
  13.  
  14.     projectContext.LookupTables.ToList().ForEach(lt =>
  15.         {
  16.             Console.WriteLine("LT: Name [{0}], Id [{1}], FieldType [{2}]", lt.Name, lt.Id, lt.FieldType);
  17.             lt.Entries.ToList().ForEach(lte => Console.WriteLine("LTE: Name [{0}], Id [{1}]", lte.FullValue, lte.Id));
  18.  
  19.         });
  20. }

The next code creates a new lookup table including three entries:

  1. var lookupTableName = "YourLookupTable";
  2. var values = new List<string> {
  3.                     "Value1",
  4.                     "Value2",
  5.                     "Value3"
  6.                 };
  7.  
  8. using (var projectContext = new ProjectContext(pwaUrl))
  9. {
  10.  
  11.     var si = 0;
  12.     var entries = values.ConvertAll(v => new LookupEntryCreationInformation
  13.     {
  14.         Id = Guid.NewGuid(),
  15.         Value = new LookupEntryValue { TextValue = v },
  16.         SortIndex = 100 + (si++) * 10
  17.     });
  18.  
  19.     LookupTableCreationInformation ltci = new LookupTableCreationInformation
  20.     {
  21.         Id = Guid.NewGuid(),
  22.         Name = lookupTableName,
  23.         SortOrder = LookupTableSortOrder.UserDefined,
  24.         Masks = new List<LookupMask> { new LookupMask { Length = 0, MaskType = LookupTableMaskSequence.CHARACTERS, Separator = "." } },
  25.         Entries = entries
  26.     };
  27.  
  28.     projectContext.LookupTables.Add(ltci);
  29.     projectContext.LookupTables.Update();
  30.     projectContext.ExecuteQuery();
  31. }

The code below removes an existing entry from a lookup table and inserts a new one into the entries:

  1. using (var projectContext = new ProjectContext(pwaUrl))
  2. {
  3.  
  4.     projectContext.Load(projectContext.LookupTables, lts => lts.Include(lt => lt.Name, lt => lt.Entries));
  5.     projectContext.ExecuteQuery();
  6.  
  7.     var lookupTable = projectContext.LookupTables.First(lt => lt.Name == "YourLookupTable");
  8.  
  9.     // this value should be removed
  10.     lookupTable.Entries.Remove(lookupTable.Entries.First(pe => pe.FullValue == "ValueToRemove"));
  11.  
  12.     // and a new value has to be created
  13.     lookupTable.Entries.Add(new LookupEntryCreationInformation
  14.     {
  15.         Id = Guid.NewGuid(),
  16.         Value = new LookupEntryValue { TextValue = "ValueToCreate" },
  17.         SortIndex = 165
  18.     });
  19.  
  20.     projectContext.LookupTables.Update();
  21.     projectContext.ExecuteQuery();
  22. }

The last sample illustrates how we can remove a set of lookup tables based on their name:

  1. var tabledNames = new List<string> { "YourLookupTable", "AnotherLookupTable" };
  2.  
  3. using (var projectContext = new ProjectContext(pwaUrl))
  4. {
  5.  
  6.     projectContext.Load(projectContext.LookupTables, lts => lts.Include(lt => lt.Name));
  7.     projectContext.ExecuteQuery();
  8.  
  9.     // query made case insensitive
  10.     tabledNames.ConvertAll(tn => projectContext.LookupTables
  11.         .FirstOrDefault(lt => lt.Name.ToLower() == tn.ToLower()))
  12.         .Where(lt => lt != null).ToList()
  13.         .ForEach(lt => lt.DeleteObject());
  14.  
  15.     projectContext.ExecuteQuery();
  16. }

In my next post I describe how to work with enterprise custom fields using the managed client object model.

November 12, 2014

Managing Project Server Event Handlers via the Managed Client Object Model

Project Server events provide a great way to extend the functionality of the product by custom logic. For example, you can write code that will be executed if the web site of the project was created and customize the site with features that can not be integrated into the project site template itself, or respond to the deletion of the project and perform some kind of housecleaning, that is not part of the default deletion, like removing custom created project-specific SharePoint groups.

There is a page in the Central Administration that supports the event handler maintenance (see Central Administration / General Application Settings / PWA Settings (Manage) / Server Side Event Handlers, illustrated below).

image

There is unfortunately a major Application Lifecycle Management (ALM) related issue with this approach: It is a manual process that cannot be easily automated as part of the solution deployment.

Alternatively, one can deploy event handlers as part of a SharePoint package, as stated in the Project Server entity provisioning schema section of the Project Server programmability article. This method is described and demonstrated in the presentation Developer to developer – learn the Microsoft Project Online and Server extensibility (authors: Bill Avery and Chris Givens), see further details in section “Project Server App Packages” beginning at slide 75 of the presentation.

A sample of the Project Server entity provisioning XML including an event handler registration:

  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <ProjectServerEntities xmlns="http://schemas.microsoft.com/sharepoint/">
  3.   <EventHandlers>
  4.     <EventHandler Id="6B92EF26-25CA-4716-9352-67FC2EF57BE3" Name="PWSCreated">
  5.       <EventName>WssInteropWssWorkspaceCreated</EventName>
  6.       <Description>Called when a new PWS site was created</Description>
  7.       <EndpointUrl>http://dummysite.org</EndpointUrl>
  8.       <AssemblyName>$SharePoint.Project.AssemblyFullName$</AssemblyName>
  9.       <ClassName>Customer.EPM.PSEventHandler</ClassName>
  10.       <CancelOnError>false</CancelOnError>
  11.     </EventHandler>
  12.   </EventHandlers>
  13. </ProjectServerEntities>

Note the EndpointUrl element, that contains only a dummy value in this case. In this case we use a local event handler implemented in a custom assembly, however we could use remote event handlers as well. This element is optional by the XSD of the Project Server entity provisioning schema, however, if we did not include this in the XML, we would receive a deployment error on the feature activation:

Value cannot be null. Parameter name: uriString

An empty value causes a similar error in the feature receiver:

Invalid URI: The URI is empty.

If the URL is not well formatted:

Invalid URI: The format of the URI could not be determined.

Formerly I applied this approach to deploy my event handlers, lookup tables and enterprise custom fields, but it turned out quickly, that whenever I deploy a new version of the package, the former entities are retracted and deployed again, resulting in a loss of information, for example, on tasks, projects and resources that already had a re-deployed enterprise custom fields assigned. In a developer environment it was only inconvenient, but in test and production system I found that simply unacceptable.

So I decided to write my own tools to list, register and remove Project Server event handlers using the managed client object model.

First, let’s see how to enumerate the event handlers and dump the most important properties:

  1. using (var projectContext = new ProjectContext(pwaUrl))
  2. {
  3.     projectContext.Load(projectContext.EventHandlers, ehs => ehs.Include(
  4.         eh => eh.Id,
  5.         eh => eh.Event,
  6.         eh => eh.AssemblyName,
  7.         eh => eh.ClassName,
  8.         eh => eh.Order));
  9.     projectContext.ExecuteQuery();
  10.  
  11.     projectContext.EventHandlers.ToList().ForEach(eh =>
  12.         Console.WriteLine("Event \r\nEvent Handler [Id: {3} AssemblyName: {4} ClassName: {5} Order: {6}]\r\n",
  13.         eh.Event.SourceName, eh.Event.EventName, eh.Event.Id, eh.Id, eh.AssemblyName, eh.ClassName, eh.Order));
  14. }

The output in our case:

image

Using the code below, we can register our custom event handler. To be able to use the predefined values in the PSEventID enumeration, you should include a reference to the Microsoft.Office.Project.Server.Library.dll assembly.

  1. using (var projectContext = new ProjectContext(pwaUrl))
  2. {
  3.     var ehci = new EventHandlerCreationInformation
  4.     {
  5.         Id = projDeletedEventHandlerId,
  6.         Name = "ProjDeleting",
  7.         AssemblyName = "Customer.EPM, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b28db3dcb95229e1",
  8.         ClassName = "Customer.EPM.ProjectEvents",
  9.         Description = "Called when a project is being deleted",
  10.         EventId = (int)PSEventID.ProjectDeleting, // value is 49, handling ProjectEventReceiver.OnDeleting
  11.         Order = 100,
  12.         CancelOnError = false
  13.     };
  14.  
  15.     projectContext.EventHandlers.Add(ehci);
  16.     projectContext.EventHandlers.Update();
  17.     projectContext.ExecuteQuery();
  18. }

Note: this code only submits the requests for the event handler registration- The registration itself is performed asynchronously in a background process, so there is a delay after running the code until the event handler appears in the list.

Finally, if you don’t need the event handler any more, you can remove it using the following code (assuming you already know the ID of the event handler, for example, from the output of the event handler enumeration example):

  1. var projDeletedEventHandlerId = new Guid("0FB1389D-E620-4062-A99B-9E5716CC958E");
  2.  
  3. using (var projectContext = new ProjectContext(pwaUrl))
  4. {
  5.     var eh = projectContext.EventHandlers.GetByGuid(projDeletedEventHandlerId);
  6.     projectContext.EventHandlers.Remove(eh);
  7.     projectContext.EventHandlers.Update();
  8.     projectContext.ExecuteQuery();
  9. }

A similar code for unregistering the event can be found here, but the sample above is a bit simpler, using only a single ExecuteQuery call. You might need the double call if you don’t know the ID of the event handler yet. In this case you should look up the event handler by name, or by the type of the event it handles first, and it requires a former ExecuteQuery call to get the complete list of the event handlers. Then in the next step, you can already remove the event handler as illustrated above.

November 6, 2014

Error when opening PWA Settings in Central Administration

Filed under: Project Server, Troubleshooting — Tags: , — Peter Holpar @ 23:11

Today I faced an error when I was to configure some of the PWA setting in the Central Administration (Central Administration / General Application Settings / PWA Settings [Manage]).

When I clicked the link PWA Settings, an error page was displayed with the text:

No Project Web App instances found. Create at least one PWA instance before accessing this page

The message was wrong, as I had already two PWA instances on the server.

In the ULS log I found these details:

11/06/2014 08:32:45.82     w3wp.exe (0x20608)                          0x1FE3C    SharePoint Foundation             General                           8nca    Medium      Application error when access /_layouts/15/pwa/Admin/Admin.aspx, Error=No Project Web App instances found. Create at least one PWA instance before accessing this page.   at Microsoft.Office.Project.PWA.PJBasePage..ctor(String pwaUrl)     at Microsoft.Office.Project.PWA.PJWebPage..ctor(String url)     at __ASP.FastObjectFactory_app_web_admin_aspx_ad835a1b_9lzjizur.Create_ASP__layouts_15_pwa_admin_admin_aspx()     at System.Web.Compilation.BuildManager.CreateInstanceFromVirtualPath(VirtualPath virtualPath, Type requiredBaseType, HttpContext context, Boolean allowCrossApp)     at System.Web.UI.PageHandlerFactory.GetHandlerHelper(HttpContext context, String requestType, VirtualPath virtualPath, String physicalPath)     at System.Web.HttpApplication.MaterializeHandlerExecutionStep.System.Web.Ht…    1e63c99c-45c3-507f-40b1-b67382eb125e
11/06/2014 08:32:45.82*    w3wp.exe (0x20608)                          0x1FE3C    SharePoint Foundation             General                           8nca    Medium      …tpApplication.IExecutionStep.Execute()     at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)    1e63c99c-45c3-507f-40b1-b67382eb125e
11/06/2014 08:32:45.82     w3wp.exe (0x20608)                          0x1FE3C    SharePoint Foundation             Runtime                           tkau    Unexpected    Microsoft.SharePoint.SPException: No Project Web App instances found. Create at least one PWA instance before accessing this page.    at Microsoft.Office.Project.PWA.PJBasePage..ctor(String pwaUrl)     at Microsoft.Office.Project.PWA.PJWebPage..ctor(String url)     at __ASP.FastObjectFactory_app_web_admin_aspx_ad835a1b_9lzjizur.Create_ASP__layouts_15_pwa_admin_admin_aspx()     at System.Web.Compilation.BuildManager.CreateInstanceFromVirtualPath(VirtualPath virtualPath, Type requiredBaseType, HttpContext context, Boolean allowCrossApp)     at System.Web.UI.PageHandlerFactory.GetHandlerHelper(HttpContext context, String requestType, VirtualPath virtualPath, String physicalPath)     at System.Web.HttpApplication.MaterializeHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute…    1e63c99c-45c3-507f-40b1-b67382eb125e
11/06/2014 08:32:45.82*    w3wp.exe (0x20608)                          0x1FE3C    SharePoint Foundation             Runtime                           tkau    Unexpected    …()     at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)    1e63c99c-45c3-507f-40b1-b67382eb125e
11/06/2014 08:32:45.82     w3wp.exe (0x20608)                          0x1FE3C    SharePoint Foundation             General                           ajlz0    High        Getting Error Message for Exception Microsoft.SharePoint.SPException: No Project Web App instances found. Create at least one PWA instance before accessing this page.     at Microsoft.Office.Project.PWA.PJBasePage..ctor(String pwaUrl)     at Microsoft.Office.Project.PWA.PJWebPage..ctor(String url)     at __ASP.FastObjectFactory_app_web_admin_aspx_ad835a1b_9lzjizur.Create_ASP__layouts_15_pwa_admin_admin_aspx()     at System.Web.Compilation.BuildManager.CreateInstanceFromVirtualPath(VirtualPath virtualPath, Type requiredBaseType, HttpContext context, Boolean allowCrossApp)     at System.Web.UI.PageHandlerFactory.GetHandlerHelper(HttpContext context, String requestType, VirtualPath virtualPath, String physicalPath)     at System.Web.HttpApplication.MaterializeHandlerExecutionStep.System.Web.H…    1e63c99c-45c3-507f-40b1-b67382eb125e
11/06/2014 08:32:45.82*    w3wp.exe (0x20608)                          0x1FE3C    SharePoint Foundation             General                           ajlz0    High        …ttpApplication.IExecutionStep.Execute()     at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)    1e63c99c-45c3-507f-40b1-b67382eb125e

If I tried to open the related page directly (/_layouts/15/pwa/Admin/Admin.aspx), and not via the link, another error message was displayed:

File Not Found

The related details in the ULS log:

11/06/2014 08:34:51.37     w3wp.exe (0x20608)                          0x1CE48    SharePoint Foundation             General                           8nca    Medium      Application error when access /_layouts/15/pwa/Admin/Admin.aspx, Error=The site with the id 5584122d-2bdd-48b5-bd15-7885712b9892 could not be found.   at Microsoft.SharePoint.SPSite..ctor(Guid id, SPFarm farm, SPUrlZone zone, SPUserToken userToken)     at Microsoft.Office.Project.PWA.PJContext.GetContextForCentralAdmin(Boolean isWebServiceCall, Guid siteID)     at Microsoft.Office.Project.PWA.PJBasePage..ctor(String pwaUrl)     at Microsoft.Office.Project.PWA.PJWebPage..ctor(String url)     at __ASP.FastObjectFactory_app_web_admin_aspx_ad835a1b_9lzjizur.Create_ASP__layouts_15_pwa_admin_admin_aspx()     at System.Web.Compilation.BuildManager.CreateInstanceFromVirtualPath(VirtualPath virtualPath, Type requiredBaseType, HttpContext context, Boolean allowCrossApp)     at System.Web.UI.PageHand…    3d63c99c-d56d-507f-40b1-be1a311bf3d0
11/06/2014 08:34:51.37*    w3wp.exe (0x20608)                          0x1CE48    SharePoint Foundation             General                           8nca    Medium      …lerFactory.GetHandlerHelper(HttpContext context, String requestType, VirtualPath virtualPath, String physicalPath)     at System.Web.HttpApplication.MaterializeHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()    3d63c99c-d56d-507f-40b1-be1a311bf3d0
11/06/2014 08:34:51.37     w3wp.exe (0x20608)                          0x1CE48    SharePoint Foundation             Runtime                           tkau    Unexpected    System.IO.FileNotFoundException: The site with the id 5584122d-2bdd-48b5-bd15-7885712b9892 could not be found.    at Microsoft.SharePoint.SPSite..ctor(Guid id, SPFarm farm, SPUrlZone zone, SPUserToken userToken)     at Microsoft.Office.Project.PWA.PJContext.GetContextForCentralAdmin(Boolean isWebServiceCall, Guid siteID)     at Microsoft.Office.Project.PWA.PJBasePage..ctor(String pwaUrl)     at Microsoft.Office.Project.PWA.PJWebPage..ctor(String url)     at __ASP.FastObjectFactory_app_web_admin_aspx_ad835a1b_9lzjizur.Create_ASP__layouts_15_pwa_admin_admin_aspx()     at System.Web.Compilation.BuildManager.CreateInstanceFromVirtualPath(VirtualPath virtualPath, Type requiredBaseType, HttpContext context, Boolean allowCrossApp)     at System.Web.UI.PageHandlerFactory.GetHandlerHelper(HttpConte…    3d63c99c-d56d-507f-40b1-be1a311bf3d0
11/06/2014 08:34:51.37*    w3wp.exe (0x20608)                          0x1CE48    SharePoint Foundation             Runtime                           tkau    Unexpected    …xt context, String requestType, VirtualPath virtualPath, String physicalPath)     at System.Web.HttpApplication.MaterializeHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()    3d63c99c-d56d-507f-40b1-be1a311bf3d0
11/06/2014 08:34:51.37     w3wp.exe (0x20608)                          0x1CE48    SharePoint Foundation             General                           ajlz0    High        Getting Error Message for Exception System.Web.HttpException (0x80004005): Exception of type ‘System.Web.HttpException’ was thrown. —> System.IO.FileNotFoundException: The site with the id 5584122d-2bdd-48b5-bd15-7885712b9892 could not be found.     at Microsoft.SharePoint.SPSite..ctor(Guid id, SPFarm farm, SPUrlZone zone, SPUserToken userToken)     at Microsoft.Office.Project.PWA.PJContext.GetContextForCentralAdmin(Boolean isWebServiceCall, Guid siteID)     at Microsoft.Office.Project.PWA.PJBasePage..ctor(String pwaUrl)     at Microsoft.Office.Project.PWA.PJWebPage..ctor(String url)     at __ASP.FastObjectFactory_app_web_admin_aspx_ad835a1b_9lzjizur.Create_ASP__layouts_15_pwa_admin_admin_aspx()     at System.Web.Compilation.BuildManager.CreateInstanceFromVirtualPath(VirtualPath virtualP…    3d63c99c-d56d-507f-40b1-be1a311bf3d0
11/06/2014 08:34:51.37*    w3wp.exe (0x20608)                          0x1CE48    SharePoint Foundation             General                           ajlz0    High        …ath, Type requiredBaseType, HttpContext context, Boolean allowCrossApp)     at System.Web.UI.PageHandlerFactory.GetHandlerHelper(HttpContext context, String requestType, VirtualPath virtualPath, String physicalPath)     at System.Web.HttpApplication.MaterializeHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()     at System.Web.HttpApplication.MaterializeHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()     at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)    3d63c99c-d56d-507f-40b1-be1a311bf3d0

As highlighted above the source of the problem was:

The site with the id 5584122d-2bdd-48b5-bd15-7885712b9892 could not be found

I searched the configuration database of the farm for the object that includes this id in its properties:

SELECT [Id]
      ,[ClassId]
      ,[ParentId]
      ,[Name]
      ,[Status]
      ,[Version]
      ,[Properties]
  FROM [ConfigDB].[dbo].[Objects]
  where Properties like ‘%5584122d-2bdd-48b5-bd15-7885712b9892%’

The resulting object was a former PWA instance (of type Microsoft.Office.Project.Server.Administration.ProjectSite) with the Id13B1DAD3-B005-46D9-B712-CDF7CC2FEAFF’, that still referenced to a SharePoint site collection, that did not exist any more. From the name of the instance (specified by the value of sFld type="String" name="_name")  I was able to identify, that it should be an instance, that was deleted already a few weeks ago, and was neither displayed on the UI of Central Administration nor available programmatically. Obviously, the removal of that instance has not been performed successfully.

To remove the inconsistency, I’ve deleted the orphaned instance using

STSADM -o deleteconfigurationobject -id 13B1DAD3-B005-46D9-B712-CDF7CC2FEAFF

Important! I suggest to create database backups (SharePoint configuration, content databases, and Project database as well) before executing this type of commands in your environment.

The deletion of the orphaned instance solved my issue, and the page PWA Settings was available again in the Central Administration.

November 1, 2014

No more “Show More” in Tasks lists

Filed under: ListFieldIterator, PowerShell, SP 2013 — Tags: , — Peter Holpar @ 05:58

I meet frequently with the request to turn off the new “Show More” feature at the SharePoint 2013 Tasks lists. The next screenshot shows the default new task form as displayed after page load with the limited set of fields, the “Show More” button is highlighted:

image

After clicking on that button, the remaining fields are displayed, as shown below:

image

Surprisingly, the solutions I found on the web (like this, this or this one) try to solve the issue and expand the fields automatically on the client side using JavaScript – that I consider rather hacking – instead of solving the real reason of the problem.

In this blog post I would like to introduce a few other solutions to the problem, that are worth considering instead of the JavaScript approach.

As you probably know, the various list item forms (NewForm.aspx, DispForm.aspx and EditForm.aspx) use the ListFormWebPart web part to render the item in new, display and edit mode. Which rendering template the web part uses is specified by its TemplateName property. If no TemplateName is specified on the page itself for the web part, the default value is used:

image

As you can see from this code, the template is read from the configuration of the content type of the item being edited / displayed.

You can display this value in the case of a standard Tasks list using the next PowerShell script:

$web = Get-SPWeb http://YourSharePointSite
$list = $web.Lists["Tasks"]
$ct = $list.ContentTypes[0]
$ct.DisplayFormTemplateName

The output of this should be “TaskForm”, and you get the same result for the other two properties (NewFormTemplateName and EditFormTemplateName).

In the case of other list types the form used is the “ListForm”. So if you would like to use the standard form layout without the “Show More” button, you can simply replace the form template for the content type assigned to the list (solution 1).

$web = Get-SPWeb http://YourSharePointSite
$list = $web.Lists["Tasks"]
$ct = $list.ContentTypes[0]
$ct.DisplayFormTemplateName = "ListForm"
$ct.NewFormTemplateName = "ListForm"
$ct.EditFormTemplateName = "ListForm"
$ct.Update()

Note: The change affect only the given list, but no other Tasks lists, as we change the property only for the local copy of the Task content type.

Alternatively, you can open the form using SharePoint Designer, and set the TemplateName property of the ListFormWebPart web part explicitly (solution 2):

<TemplateName xmlns="http://schemas.microsoft.com/WebPart/v2/ListForm">ListForm</TemplateName>

But what’s the difference between the TaskForm and ListForm templates? Don’t we lose any functionality if we simply switch the form template? What`s included in TaskForm and what in ListForm?

These questions can be answered if we have a look at these templates in DefaultTemplates.ascx (located in folder [SharePoint Root]\TEMPLATE\CONTROLTEMPLATES).

If we look for the templates having id="ListForm" and “TaskForm”, we find that there are several differences between them. Just to name a few, the ListForm uses a standard ListFieldIterator control, while in the TaskForm we find a TaskFieldIteratorSpecifiedListFieldIterator combo, and various combinations of an EditDatesSelector control. I had not yet time to investigate the purpose of the latter one, but having a look at the code of the TaskFieldIterator and its base class DividingListFieldIterator (via Reflector or dotPeek), this control itself seems powerful enough to find an other way to eliminate the “Show More” button.

In one of my former blog posts I’ve already described the process how can we customize a standard SharePoint rendering template. A altering an out-of-the-box file is definitely not recommended, you should create a new .ascx file (for example, call it CustomTaskForm.ascx) in the CONTROLTEMPLATES folder, and copy the content of the rendering template with id="TaskForm" into the file, and include the same Control, Assembly and Register headers as found in the DefaultTemplates.ascx. Alter the id property of the template to CustomTaskForm.

Add the ShowExpanded="true" attribute to the TaskListFieldIterator control in the file (solution 3).

<SharePoint:TaskListFieldIterator ShowExpanded="true" …

Save the changes and execute IISRESET.

Open the list item forms (NewForm.aspx, DispForm.aspx and EditForm.aspx) and assign the new custom rendering template to the ListFormWebPart.

<TemplateName xmlns="http://schemas.microsoft.com/WebPart/v2/ListForm"&gt;CustomTaskForm</TemplateName>

Save the changes again. The item should be displayed now with all its fields without the “Show More” button.

If you want to keep the button, but would like to include / exclude other fields into / from the top, you can change the value of the TopFields property. Mandatory fields of the content type and the fields specified in this property should be displayed by default without clicking on the “Show More” button.

For example, we could include the Priority field on the form, if we include ;#Priorty in this property.

<SharePoint:TaskListFieldIterator TopFields="…Title;#StartDate;#DueDate;#AssignedTo;#Priorty;#PercentComplete;#RelatedItems;#Description" runat="server"/>

Of course, in this case we should remove the ShowExpanded="true" we just included in the former step!

After saving the changes and an IISRESET, the forms should include the Priority field as well.

image

By studying the constructor of the TaskListFieldIterator class, we can find two further ways to show the form with all the fields.

We can pass the Expanded=1 in the request query string like NewForm.aspx?Expanded=1 (solution 4), or change the column order of the list content type, for example, switch the order of the Priority and Task Status fields (solution 5).

image

In both cases the form will be displayed with the fields expanded automatically. Of course, in the second case the field display order will differ from the standard one, but it’s only a minor difference.

image

I hope you find a method from the above described ones that fulfills your needs, and can easily eliminate the “Show More” button if you wish, without any kind of JavaScript magic.

October 22, 2014

How to set the value of a Project Server Enterprise Custom Field via the Project Server Managed Client Object Model

Filed under: Managed Client OM, Project Server — Tags: , — Peter Holpar @ 21:58

Recently I had to set the value of a Project Server Enterprise Custom Field from a .NET client application. The samples I found on the web (like this one) used the PSI , and I found them rather developer-unfriendly. For example, with PSI we should work with DataSet objects, check if the custom field already exists or should be first created, and should set various properties depending of the data type of the custom field.

So I’ve decided to create my own implementation based on the Managed Client Object Model. In my case I had to set a project-related custom field. In the first step I assumed, that the project ID and the internal name of the custom field are known. The internal name of the custom field consists of the prefix Custom_ and the GUID of the field without the separator dashes.

The result is the following code:

  1. var url = @"http://YourProjectServer/pwa&quot;;
  2. var projectContext = new ProjectContext(url);
  3.  
  4. var projId = new Guid("98138ffd-d0fa-e311-83c6-005056b45654");
  5. var cfInternalName = "Custom_b278fdf35d16e4119568005056b45654";
  6. object cfValue = "Some value";
  7.  
  8. var proj = projectContext.Projects.GetByGuid(projId);
  9. var draftProj = proj.CheckOut();
  10. draftProj.SetCustomFieldValue(cfInternalName, cfValue);
  11. var cfsX = proj.CustomFields;
  12. draftProj.Publish(true);
  13.  
  14. projectContext.ExecuteQuery();

If the project ID and the internal name of the custom field are not known, it is easy to get them from the names like this:

  1. projectContext.Load(projectContext.Projects, ps => ps.Include(p => p.Id, p => p.Name));
  2. projectContext.Load(projectContext.CustomFields, cfs => cfs.Include(cf => cf.InternalName, cf => cf.Name));
  3. projectContext.ExecuteQuery();
  4.  
  5. var projId = projectContext.Projects.First(p => p.Name == "Your Project Name").Id;
  6. var cfInternalName = projectContext.CustomFields.First(cf => cf.Name == "NameOfTheField").InternalName;

Note, how much this solution is simpler than the PSI-alternative.

September 29, 2014

Importing multi-level Lookup Tables using PowerShell

Filed under: ALM, PowerShell, Project Server, PSI — Tags: , , , — Peter Holpar @ 21:54

Recently I’m working quite a lot with Project Server 2013. My tasks include – beyond development – creation of methods that supports the continuous delivery of the results from the development environment to the test and production environments. I found that my old friend, PowerShell is an invaluable tool in this field as well.

Recently I had to solve a problem, where we had a rather complex, multi-level lookup table (RBS) on the development server, and we had to transfer the same structure on each deployment to the test server. Typing the same structure via the UI each time would have been a very boring and time consuming activity.

If we export the structure via the UI to Excel,

image

the result looks like this:

image

However, when we try to paste the values to the lookup list via the UI, the fields are shifted to the right: the values in the Level field become to the values of the Value field, the Value becomes to the Description, and the original Description is lost, making the whole pasting worthless.

image

I found a very useful PowerShell script on the blog of Paul Mather (the code is available in the TechNet Script Center as well). This script utilizes the PSI interface, however is limited to a single level of values, no hierarchical lookup tables.

I’ve extended the sample using the generic Stack object of the .NET Framework, pushing and popping the Guids of the parent items, and importing the value of the Description field as well. Otherwise most of the code was borrowed from, and the functionality is identical to the original version of Paul. As input file, a TAB separated text file is used without field name headers, including the Level, Value and Description fields, in the case above, for example:

1    Value 1    Desc 1
2    Value 1_1    Desc 1.1
3    Value 1_1_1    Desc 1.1.1
2    Value 1_2    Desc 1.2
3    Value 1_2_1    Desc 1.2.1
2    Value 1_3    Desc 1.3

This sample is limited to lookup tables with character-based code sequences.

The PowerShell script that enables the mulit-level import:

  1. #Get lookup table values to add
  2. $values = Get-Content "C:\Data\PowerShell\RBSValues.txt"
  3.  
  4. #Specify Lookup table to update
  5. $lookupTableName = "RBS"
  6. $lcid = 1033
  7. $emptyString = [String]::empty
  8. $svcPSProxy = New-WebServiceProxy -Uri "http://sp2013/pwa/_vti_bin/PSI/LookupTable.asmx?wsdl&quot; -UseDefaultCredential
  9. $lookupTableGuid = ($svcPSProxy.ReadLookupTables($emptyString, 0, $lcid).LookupTables  | ? {$_.LT_NAME -eq $lookupTableName }).LT_UID
  10. $lookupTable = $svcPSProxy.ReadLookupTablesbyUids($lookupTableGuid, 1, $lcid)
  11. #get lookup table count
  12. $lookuptableValues = $svcPSProxy.ReadLookupTablesbyUids($lookupTableGuid, 0, $lcid).LookupTableTrees
  13. $count = $lookuptableValues.Count + 1
  14. #update lookup table…
  15. $stack = New-Object System.Collections.Generic.Stack[Guid]
  16. $lastLevel = 1
  17.  
  18. $values | % {
  19.     $fields = $_ -split '\t+'
  20.         $level = $fields[0]
  21.         $text = $fields[1]
  22.         $desc = $fields[2]
  23.  
  24.     $guid = [Guid]::NewGuid()
  25.     # Write-Host Count: $count, text: $text, Guid: $guid, Level: $level, Last level: $lastLevel
  26.     $parentGuid = $lastGuid
  27.     If ($lastLevel -lt $level) {
  28.         $stack.Push($lastGuid)
  29.         # Write-Host Parent GUID Pushed: $parentGuid
  30.     }
  31.     Else {
  32.         While (($stack.Count -ge ($level)) -and ($stack.Count -gt 1)) {
  33.             # Write-Host Popping level ($stack.Count + 1)
  34.             $parentGuid = $stack.Pop()
  35.             # Write-Host Parent GUID Popped: $parentGuid
  36.         }
  37.         If ($stack.Count -gt 0) {
  38.             $parentGuid = $stack.Peek()
  39.             # Write-Host Parent GUID Peeked: $parentGuid
  40.         }
  41.     }
  42.  
  43.  
  44.     $LookupRow = $lookuptable.LookupTableTrees.NewLookupTableTreesRow()
  45.     If (-Not [String]::IsNullOrEmpty($desc)) {
  46.         $LookupRow.LT_VALUE_DESC = $desc
  47.     }
  48.     $LookupRow.LT_STRUCT_UID = $guid
  49.     $LookupRow.LT_UID = $lookupTableGuid
  50.     $LookupRow.LT_VALUE_TEXT = $text
  51.     If ($level -gt 1) {
  52.         # Write-Host Parent GUID set: $parentGuid
  53.         $LookupRow.LT_PARENT_STRUCT_UID = $parentGuid
  54.     }
  55.     $LookupRow.LT_VALUE_SORT_INDEX =  ($count++)
  56.     $lookuptable.LookupTableTrees.AddLookupTableTreesRow($LookupRow)
  57.  
  58.     $lastGuid = $guid
  59.     $lastLevel = $level
  60. }
  61.  
  62. $Error.Clear()
  63. Try
  64.     {
  65.         $svcPSProxy.UpdateLookupTables($lookuptable , 0 , 1 , $lcid)
  66.     }
  67. Catch
  68.     {
  69.         Write-Host "Error updating the Lookup table, see the error below:" -ForeGroundColor Red -BackGroundColor White
  70.         Write-Host "$error" -ForeGroundColor Red
  71.     }
  72. If ($Error.Count -eq 0)
  73.     {
  74.         Write-Host "The lookup table $lookupTablename has been updated with the values from the text file specified" -ForeGroundColor Green
  75.     }
  76. Else
  77.     {
  78.         Write-Host "The lookup table $lookupTablename has not been updated with the values from the text file specified, please see error" -ForeGroundColor Red -BackGroundColor White
  79.     }
  80. #force checkin in case of failure
  81. $Error.Clear()
  82. Try
  83.     {
  84.      $svcPSProxy.CheckInLookUpTables($lookupTableGuid, 1)
  85.     }
  86. Catch
  87.     {
  88.         If ($error -match "LastError=CICONotCheckedOut")
  89.             {
  90.     
  91.             }
  92.         Else
  93.         {
  94.             Write-Host "Error checking the Lookup table, see the error below:" -ForeGroundColor Red -BackGroundColor White
  95.             Write-Host "$error" -ForeGroundColor Red
  96.         }
  97.     }

The script includes a lot of Write-Host cmdlets to enable tracking of the process. These are commented in the version above. You are free to either use or delete these lines as you wish.

Note: Don’t forget to alter the file path, the URI and the lookup table name, and the LCID as well, if you are working with a non-English version of PWA.

August 31, 2014

How to use PowerShell to check if a SharePoint Group with a specified ID or name exists–Without error handling

Filed under: PowerShell, SP 2013, Tips & Tricks — Tags: , , — Peter Holpar @ 23:18

Recently I created a PowerShell script that should delete a group that has a specific name. If the script runs the second time, it throws an exception since the group is already deleted.

If you want to get/delete/add a group from/to a SPGroupCollection (like SiteGroups or Groups of an SPWeb) the methods throw typically exceptions of different kinds if the group does not exist (or already does exist in case of addition):

$web.SiteGroups.Remove(12345) throws
Specified argument was out of the range of valid values.

$web.SiteGroups.Remove("YourGroupName") throws
Group cannot be found.

$web.SiteGroups.GetByID(12345) and
$web.SiteGroups.GetByName("YourGroupName") throw
Group cannot be found.

$web.SiteGroups.Add("YourGroupName", $usr, $null, "Group description") throws
The specified name is already in use.

I wanted to eliminate the error messages. If these commands were PowerShell Cmdlets, we could use the common ErrorAction parameter with the value SilentlyContinue (see more here), however with standard .NET object calls only the Try/Catch block would be available.

Throwing and handling exceptions has always a performance penalty. How could we check if the group exists before trying to get/delete/add it from/to the collection?

After a short search on the .NET based samples I found:

  • A generic- and lambda expression-based solution, that is nice, but not easy to transfer to PowerShell.
  • An interesting solution, that uses the Xml property of the SPGroupCollection object .
  • A solution that is based on the GetCollection method of the SPGroupCollection object.

I chose the third sample to transfer to PowerShell. The equivalent PowerShell condition to check the group by ID:

@($web.SiteGroups.GetCollection(@(12345))).Count -eq 1

To check the group by name, we can use:

@($web.SiteGroups.GetCollection(@("YourGroupName"))).Count -eq 1

The parameter of the GetCollection method is an array, so we can use the same method to check if all or any of multiple groups exists.

For example, to check by ID if both of the groups we need exist:

@($web.SiteGroups.GetCollection(@(12345, 54321))).Count -eq 2

To check by name if any of the groups we need exists:

@($web.SiteGroups.GetCollection(@("YourGroupName1", "YourGroupName2"))).Count –gt 0

July 22, 2014

How to validate file names of attachments on SharePoint forms

Filed under: Attachments, jQuery, SP 2010 — Tags: , , — Peter Holpar @ 18:36

A few months ago I already posted a solution to check duplicated attachment names on SharePoint forms. Another common problem source are invalid file names.

For example, if you have a standard list item edit form, and would like to upload an attachment with invalid file name (for example, invalid characters or exceeding length limitation), on submitting the form you receive an exception. Despite of the error, the item itself is saved, however, if you appended other files (with valid names) as well, the ones you appended after the attachment having invalid name are not saved to the item.

The error message you receive if the file name contains invalid characters:

The file or folder name contains characters that are not permitted.  Please use a different name.<nativehr>0x81020073</nativehr><nativestack></nativestack>

The stack trace:

[COMException (0x81020073): The file or folder name contains characters that are not permitted.  Please use a different name.<nativehr>0x81020073</nativehr><nativestack></nativestack>]
   Microsoft.SharePoint.Library.SPRequestInternalClass.AddOrUpdateItem(String bstrUrl, String bstrListName, Boolean bAdd, Boolean bSystemUpdate, Boolean bPreserveItemVersion, Boolean bUpdateNoVersion, Int32& plID, String& pbstrGuid, Guid pbstrNewDocId, Boolean bHasNewDocId, String bstrVersion, Object& pvarAttachmentNames, Object& pvarAttachmentContents, Object& pvarProperties, Boolean bCheckOut, Boolean bCheckin, Boolean bMigration, Boolean bPublish, String bstrFileName, ISP2DSafeArrayWriter pListDataValidationCallback, ISP2DSafeArrayWriter pRestrictInsertCallback, ISP2DSafeArrayWriter pUniqueFieldCallback) +0
   Microsoft.SharePoint.Library.SPRequest.AddOrUpdateItem(String bstrUrl, String bstrListName, Boolean bAdd, Boolean bSystemUpdate, Boolean bPreserveItemVersion, Boolean bUpdateNoVersion, Int32& plID, String& pbstrGuid, Guid pbstrNewDocId, Boolean bHasNewDocId, String bstrVersion, Object& pvarAttachmentNames, Object& pvarAttachmentContents, Object& pvarProperties, Boolean bCheckOut, Boolean bCheckin, Boolean bMigration, Boolean bPublish, String bstrFileName, ISP2DSafeArrayWriter pListDataValidationCallback, ISP2DSafeArrayWriter pRestrictInsertCallback, ISP2DSafeArrayWriter pUniqueFieldCallback) +406

[SPException: The file or folder name contains characters that are not permitted.  Please use a different name.]
   Microsoft.SharePoint.SPGlobal.HandleComException(COMException comEx) +27609826
   Microsoft.SharePoint.Library.SPRequest.AddOrUpdateItem(String bstrUrl, String bstrListName, Boolean bAdd, Boolean bSystemUpdate, Boolean bPreserveItemVersion, Boolean bUpdateNoVersion, Int32& plID, String& pbstrGuid, Guid pbstrNewDocId, Boolean bHasNewDocId, String bstrVersion, Object& pvarAttachmentNames, Object& pvarAttachmentContents, Object& pvarProperties, Boolean bCheckOut, Boolean bCheckin, Boolean bMigration, Boolean bPublish, String bstrFileName, ISP2DSafeArrayWriter pListDataValidationCallback, ISP2DSafeArrayWriter pRestrictInsertCallback, ISP2DSafeArrayWriter pUniqueFieldCallback) +28003823
   Microsoft.SharePoint.SPListItem.AddOrUpdateItem(Boolean bAdd, Boolean bSystem, Boolean bPreserveItemVersion, Boolean bNoVersion, Boolean bMigration, Boolean bPublish, Boolean bCheckOut, Boolean bCheckin, Guid newGuidOnAdd, Int32& ulID, Object& objAttachmentNames, Object& objAttachmentContents, Boolean suppressAfterEvents, String filename) +26729805
   Microsoft.SharePoint.SPListItem.UpdateInternal(Boolean bSystem, Boolean bPreserveItemVersion, Guid newGuidOnAdd, Boolean bMigration, Boolean bPublish, Boolean bNoVersion, Boolean bCheckOut, Boolean bCheckin, Boolean suppressAfterEvents, String filename) +26726414
   Microsoft.SharePoint.SPListItem.Update() +161
   Microsoft.SharePoint.WebControls.SaveButton.SaveItem(SPContext itemContext, Boolean uploadMode, String checkInComment) +848
   Microsoft.SharePoint.WebControls.SaveButton.OnBubbleEvent(Object source, EventArgs e) +1315
   System.Web.UI.Control.RaiseBubbleEvent(Object source, EventArgs args) +70
   System.Web.UI.Page.RaisePostBackEvent(IPostBackEventHandler sourceControl, String eventArgument) +29
   System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +2981

image

The error message you receive if the file name is too long:

The specified file or folder name is too long. The URL path for all files and folders must be 260 characters or less (and no more than 128 characters for any single file or folder name in the URL). Please type a shorter file or folder name.<nativehr>0x800700ce</nativehr><nativestack></nativestack>

image

The issue is even more annoying if you have an edit form with some kind of custom logic behind it. For example, in one of our applications, there is a custom approval workflow attached to the list that is started automatically after the item is saved. If the file name is invalid, the workflow starts without the (probably for the approval important) attachment. It doesn’t really help customer satisfaction, I shouldn’t say.

How could we validate attachment file names and prohibit attachments having invalid names? We can use the same method as described in my former post. We should update our onAttachOKbuttonClicked method with the file name validations. The new version, including the duplicate check as well as file name length and special character validation:

  1. $(document).ready(attachEventHandlers);
  2.  
  3. function attachEventHandlers() {
  4.     // override the default event handler with our custom method
  5.     $('#attachOKbutton').attr("onclick", "onAttachOKbuttonClicked()");
  6. }
  7.  
  8. function onAttachOKbuttonClicked() {
  9.     var newFilePath = $('#attachmentsOnClient').find('input').last().val();
  10.     // get the file name from the file path as described at
  11.     // http://stackoverflow.com/questions/423376/how-to-get-the-file-name-from-a-full-path-using-javascript
  12.     // TrimWhiteSpaces is a js method of SharePoint to filter out special characters from the file name
  13.     var newFileName = TrimWhiteSpaces(newFilePath).replace(/^.*[\\\/]/, '');
  14.  
  15.     var maxFileNameLength = 128;
  16.  
  17.     // Information about the characters that you cannot use in site names, folder names, and file names in SharePoint
  18.     // http://support.microsoft.com/kb/905231
  19.     // http://www.sysadminsblog.com/microsoft/file-name-length-and-character-restrictions-for-sharepoint/
  20.     // ivalid characters in SP:
  21.     // ~#%&*{}\:<>?/+|"
  22.     // ivalid characters in file system:
  23.     // \/:*?"<>|
  24.     // we have to check only:
  25.     // ~#%&{}+|
  26.     /*
  27.     Cant be longer than 128 characters
  28.     Cant use: ~ # % & * { } \ : < > ? / + | "; RegExp: [~#%\&{}+\|] – do not include characters that are not allowed in the file system
  29.     Cant use the period character consecutively in the middle of a file name (blahblah.docx); RegExp: \\.\\.
  30.     Cant use the period character at the end of a file name; RegExp:  ^\\.
  31.     Cant use the period character at the start of a file name; RegExp:  \\.$    
  32.     */
  33.  
  34.     var match = (new RegExp('[~#%\&{}+\|]|\\.\\.|^\\.|\\.$')).test(newFileName);
  35.     if (match) {
  36.         alert("Ivalid file name. The name of the attached file contains invalid characters.");
  37.     }
  38.     else if (newFileName.length > maxFileNameLength) {
  39.         alert("Ivalid file name. The name of the attached file is too long.");
  40.     }
  41.     else {
  42.         // it is the same duplicate check code from former post (http://pholpar.wordpress.com/2014/03/12/how-to-check-for-duplicated-attachments-on-sharepoint-forms/)
  43.         var foundDuplication = false;
  44.  
  45.         $('#idAttachmentsTable').find('tbody').find('tr').each(function () {
  46.             var existingFileName = $(this).find('.ms-vb').find('a').text();
  47.             // if the existingFileName is empty then the attachment was uploaded in this session
  48.             // that is, it is not saved yet
  49.             if (existingFileName == '') {
  50.                 var existingFilePath = $(this).find('.ms-vb').find('span').text();
  51.                 existingFileName = existingFilePath.replace(/^.*[\\\/]/, '');
  52.             }
  53.  
  54.             if (newFileName == existingFileName) {
  55.                 foundDuplication = true;
  56.                 return false;
  57.             }
  58.         });
  59.  
  60.         if (foundDuplication) {
  61.             alert("A file with name '" + newFileName + "' is already attached to this item.");
  62.         }
  63.         else {
  64.             // call the OkAttach js method of SharePoint
  65.             // this is the method that is originally called by uploading attachments
  66.             OkAttach();
  67.         }
  68.  
  69.     }
  70. }

Note: I check only special characters that are supported in the NTFS file system, but not supported in SharePoint file names. I don’t check if the file name contains officially forbidden strings (.files, –Dateien, etc.). Reason is that up to now I had no issues with such files.

Including this method on the form helps us to prevent submitting the form with invalid attachments, and let the user to fix potential issues around attachment file names before saving the item.

June 25, 2014

debugging issues and SSL Failure caused by a DNS-Configuration Mistake

Filed under: SP 2010 — Tags: — Peter Holpar @ 22:55

Recently we had two issues in one of our development environments, that were rather weird at first sight.

In the farm there are three web applications (AppA, AppB and AppC), all of them accessible only via HTTPS. AppA and AppB share the same (non-wildcard) SSL certificate, AppC has its own one. All of the applications has its own IP address (IP_A, IP_B, IP_C) as we cannot use host headers with HTTPS in this case, and its own application pool (AppPoolA, AppPoolB and AppPoolC) assigned in IIS.

Symptoms:

1. We want to debug our custom application pages used in AppA. After an IISRESET we open the page using the URL of AppA in the browser, however, instead of the process of AppPoolA, the process of AppPoolB is started (we can see it in Task Manager), although the content of the pages was rendered in the browser using the context of AppA. If we try to deploy the solution from Visual Studio, and would like to automatically attach the debugger to the corresponding web application (AppA), for example using CKSDev, the breakpoints remain inactive. If we attach the debugger manually to the process of AppPoolB, we can debug the pages, and SPContext.Current reflects the correct values in context of AppA. This behavior is not always reproducible, about in 10-20% of the cases we can debug the pages “normally”, via the process of AppPoolA.

2. When accessing AppA in the browser, about in 60-70% of the cases we receive a security warning, that the certificate was issued to an other URL. When checking the certificate, we see, that the server tries to build up the HTTPS connection using the certificate assigned to AppC. If we ignored the warning, content of AppA was displayed.

Reason:

Analyzing the IIS and SharePoint Server settings and SSL certificate / IP bindings I found no errors. However, when PINGing the Fully Qualified Domain Name (FQDN) of AppA, I saw, that the host name of the front end server is displayed, and not always the IP address of AppA (IP_A) are resolved, but quite frequently the IP of AppB and AppC (IP_B, IP_C) as well. Using NSLOOKUP I realized that all of these IPs are assigned to the FQDN of AppA. The host name was resolved to IP addresses randomly in a round-robin fashion by DNS. The AppB FQDN -  IP_B, AppC FQDN – IP_C assignments were correctly configured.

Since the request was sent to the incorrect IP address, it was routed to an other virtual server at the IIS level, this is why the other application pool was started and the other SSL certificate was returned. However, as all of the sites were bound to SharePoint web applications, at the SharePoint level the correct target web application was recognized based on the URL of the request, so the content was rendered as expected, and not based on the content / in the context of the other web applications.

You should be able to reproduce this behavior if you have more SharePoint web applications bound to the same port number but different IPs without host headers, if you set the incorrect IP address in the hosts file locally, and do not let the browser to use a proxy server for accessing the sites.

Solution:

Discussing the issue with the network group it turned out, that FQDNs of the web applications were configured as CNAME records in DNS, referring to the host name of the front end server. Deleting the faulty CNAME assignments in DNS has solved both of the above described symptoms.

Older Posts »

The Shocking Blue Green Theme. Create a free website or blog at WordPress.com.

Follow

Get every new post delivered to your Inbox.

Join 53 other followers