Monday, February 28, 2011

Problem with SPSiteDataQuery and TaxonomyFieldTypeMulti

Recently I encountered with problem in Sharepoint with looks like a bug: SPSiteDataQuery doesn’t work properly with TaxonomyFieldTypeMulti fields, i.e. with taxonomy fields which allow multiple values. As you probably know SPSiteDataQuery class can be used for retrieving data from Sharepoint lists and document libraries from several sites at once (you can retrieve data from one web site, from web sites and all of its subsites and from whole site collection). It gives you very good performance so it is widely used in cross site queries scenarios.

In order to retrieve all documents from site collection you can use the following code (in my examples I will use Camlex.NET library):

   1: var q = new SPSiteDataQuery();
   2: q.ViewFields = Camlex.Query().ViewFields(x => x["FileRef"]);
   3: q.Webs = "<Webs Scope='SiteCollection' />";
   4: q.Lists = "<Lists BaseType='1' />";
   5: q.Query = Camlex.Query().OrderBy(x => x["FileRef"]).ToString();
   6: var data = web.GetSiteData(q);

Which is equivalent of the following:

   1: var q = new SPSiteDataQuery();
   2: q.ViewFields = "<FieldRef Name=\"FileRef\" />";
   3: q.Webs = "<Webs Scope='SiteCollection' />";
   4: q.Lists = "<Lists BaseType='1' />";
   5: q.Query = "<OrderBy>" +
   6:               "<FieldRef Name=\"FileRef\" />" +
   7:           "</OrderBy>";
   8: var data = web.GetSiteData(q);

Now suppose that we have custom Taxonomy field “Language” which allows multiple values:

   1: <Field Type="Note"
   2:   DisplayName="Language_0"
   3:   MaxLength="255"
   4:   Group="Custom"
   5:   ID="{7211375B-BC77-49F2-9B3F-1835DD479502}"
   6:   StaticName="Language_0"
   7:   Name="Language_0"
   8:   Hidden="TRUE"
   9:   ShowInViewForms="FALSE"
  10:   Description="" />
  11: <Field ID="{0E70EA75-C6FA-4549-832D-F2CDBE8DB1FF}"
  12:   Type="TaxonomyFieldType"
  13:   DisplayName="Language"
  14:   ShowField="Term1033"
  15:   Required="TRUE"
  16:   EnforceUniqueValues="FALSE"
  17:   Group="Custom"
  18:   StaticName="Language"
  19:   Name="Language"
  20:   Hidden="FALSE"
  21:   Mult="TRUE">
  22:   <Default></Default>
  23:   <Customization>
  24:     <ArrayOfProperty>
  25:       <Property>
  26:         <Name>IsPathRendered</Name>
  27:         <Value xmlns:q7="http://www.w3.org/2001/XMLSchema" p4:type="q7:boolean" xmlns:p4="http://www.w3.org/2001/XMLSchema-instance">
  28:           false
  29:         </Value>
  30:       </Property>
  31:       <Property>
  32:         <Name>TextField</Name>
  33:         <Value xmlns:q6="http://www.w3.org/2001/XMLSchema" p4:type="q6:string" xmlns:p4="http://www.w3.org/2001/XMLSchema-instance">
  34:           {7211375B-BC77-49F2-9B3F-1835DD479502}
  35:         </Value>
  36:       </Property>
  37:     </ArrayOfProperty>
  38:   </Customization>
  39: </Field>

I added here also supplementary field of Note type as each taxonomy field should be defined with such fields. Suppose that we added this field to OTB Document content type. Our field is required so we need to specify value for each uploaded document now. Now we need to modify our cross site CAML query showed above in order to retrieve also values of Language field of each document. The first solution which you can think about may be to add additional column in ViewFields property:

   1: q.ViewFields = Camlex.Query().ViewFields(x => new object[] { x["FileRef"], x["Language"] });

or its equivalent:

   1: q.ViewFields = "<FieldRef Name=\"FileRef\" /><FieldRef Name=\"Language\" />";

All other code is the same. I was surprised when this code didn’t return to me any items. After some investigation I found that ViewFields may affect number of returned items from SPSiteDataQuery (see SPSiteDataQuery.ViewFields):

By default, if a list included in the query does not contain one of the fields specified in the ViewFields tag, no items from that list appear in the results. To return an empty value for a field on items in lists that do not contain that field, set the Nullable attribute to TRUE on that FieldRef tag.

So I tried to add Nullable=“TRUE” attribute to the FieldRef for Language field:

   1: q.ViewFields = "<FieldRef Name=\"FileRef\" /><FieldRef Name=\"Language\" Nullable=\"TRUE\" />";

After that code returned the same amount of documents as before. It means that SPSiteDataQuery always treats fields of type TaxonomyFieldTypeMulti as empty fields (remember that Language field is mandatory and all documents have some values in it). It looks like a bug for me.

As workaround I used regular SPQuery class and make a loop through all sites in site collections manually using recursive function call. Surprisingly that SPQuery works with TaxonomyFieldTypeMulti properly, e.g. if some document has 3 values in Language fields SPQuery will return the following values in this field:

   1: English|84325810-eb21-4a5b-b9ee-533726450674;Finnish|239d8804-b7d7-4193-8bc1-5fdf9f98
   2: 8f7c;Russian|547e35b7-4d8d-4bb0-904d-93e4be9386f7

I found information in internet about problems with SPSiteDataQuery and LookupMulti fields, but not with Taxonomy fields, so probably this information will be helpful.

Update 2011-04-20: colleague snaggywolf shared solution: TaxonomyFieldTypeMulti and SPSiteDataQuery work well if in ViewFields you will use hidden text field instead of taxonomy field. Although I didn’t test it by myself, it can be useful to mention.

Sunday, February 20, 2011

Create custom AD attribute and map it to Sharepoint user profile property

In this post I would like to show the guide of how you can expand your AD schema with custom attributes and then map them on User Profile properties in Sharepoint. Although there is a lot of standard attributes which can be used for storing information in AD, this is common situation when they don’t match requirements of enterprise infrastructure. Imagine situation when company has various business units which should be stored in AD with other information about users and we need to make them searchable in the Sharepoint intranet (in this article by Sharepoint I mean Sharepoint 2010). Once user profiles have indexed business unit property Sharepoint will crawl them OTB and show in the people search result. So our goal is to map AD attribute, which is used for storing business units, on User Profile property.

There is no attribute “Business Unit” in standard AD schema. Of course there is a lot of other attributes and you can use them for storing business units information. The only thing you need to do is to find some suitable attribute (which name is more or less close to the real attribute purpose and which has appropriate data type), e.g. if we assume that business units are text data we can use standard Business-Category (businessCategory) attribute (Type = Unicode String). But this approach has own disadvantages: we introduce one more implicit solution into IT infrastructure of the company (how many such solutions already exist in your IT infrastructure? :) ), if in the future we will need to use business category attribute for its real purposes we will need to perform additional work: select another standard attribute (and we are also not saved from the same problems in the future with the new attribute), migrate existing data, reconfigure Sharepoint, etc.

Another solution is to expand your AD schema by creating new custom attribute and then map this attribute on Sharepoint user profile property. It will require more work from us, but solution will be explicit (data will be stored in the place where it is supposed to be) and we will not problems with attributes reusing in future.

First of all we need to create custom AD attribute. In order to do it I recommend to follow this article: Adding Custom Attributes in Active Directory. We don’t need to perform all steps from it, so I will put exact steps (steps 1-18) here (also I want to collect all steps in one article for future references) and add my comments to the steps:

  1. Ensure that you have enabled AD schema updates (parameter “Schema Update Allowed” = 1 of type REG_DWORD in the HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NTDS\Parameters
  2. Install the Schema snap-in (Start > Run > regsvr32 schmmgmt.dll)
  3. Go to Start > Run > Type MMC and press Enter
  4. Go to File > Add/Remove Snap-in > click Add > Select “Active Directory Schema” (it will occur after step 2 only, i.e. after you installed Schema snap-in) and click Add
  5. Expand the Active Directory schema and Right Click Attributes
  6. Click “Create Attribute”
  7. Create New Attribute window will appear
  8. In Common name enter “Business Unit” (LDAP name as “businessUnit”)
  9. Get OID please refer http://msdn2.microsoft.com/en-us/library/ms677620.aspx. Shortly you need to obtain the basic ID using the script available here. You will get basic object identifier, which will look like sequences of digits separated by dots: “1.2.840.113556.1.6.1.8000.xxxx”. In order to create OID for attribute you can add “.2.1” suffix, so the final ID will be “1.2.840.113556.1.6.1.8000.xxxx.2.1”
  10. Select the Syntax = “Unicode String”
  11. Click Ok. After that new attribute will be created
  12. Now we need to add this attribute to “Person” class
  13. Expand Classes > Select Person
  14. Rick click Person and select Properties
  15. Click Attribute Tab and click Add
  16. Select the Attribute “businessUnit” and click OK (on this step in my environment snap-in shows exception, but after I reopen MMC console it shows that changes were saved successfully)

Now you can specify business units for users in AD. Go to Start > Administrative Tools > Active Directory Users and Computers. Enable advanced features in View > Advance Features. Now if you click on some user account you will find “Attribute Editor” tab. In the attributes list you will find added attribute “businessUnit” (it will occur here after you added attribute to the Person class – see above). In order to specify some value for new attribute select it and click Edit, then enter new value in the Value field.

Ok, we created custom AD attribute. Now we need to map it on User Profile property in Sharepoint User Profile Service application. There is another useful article which you can start with: Mapping User Profile Properties in SharePoint 2010 to LDAP Attributes. Unfortunately for my case it didn’t work like described in the article. The main problem is that new custom attribute was not shown in User Profile properties mappings (Central Administration > Manage service applications > User Profile Service Application > Manage User Properties > New property > Add new mapping section > Attribute list). Mentioned post says that in this case you need to run PowerShell script which will map attribute:

   1: function MapAttribute([string]$url, [string]$fimProperty, [string]$spsProperty, [string]$connectionName)
   2: {
   3:     $site = Get-SPSite $url 
   4:  
   5:     if ($site) 
   6:         {Write-Host "Successfully obtained site reference!"} 
   7:     else 
   8:         {Write-Host "Failed to obtain site reference"} 
   9:  
  10:     $serviceContext = Get-SPServiceContext($site) 
  11:  
  12:     if ($serviceContext) 
  13:     {Write-Host "Successfully obtained service context!"} 
  14:     else 
  15:         {Write-Host "Failed to obtain service context"} 
  16:     $upManager = new-object Microsoft.Office.Server.UserProfiles.UserProfileConfigManager($serviceContext) 
  17:  
  18:     if ($upManager) 
  19:         {Write-Host "Successfully obtained user profile manager!"} 
  20:     else 
  21:         {Write-Host "Failed to obtain user profile manager"} 
  22:     $synchConnection = $upManager.ConnectionManager[$connectionName] 
  23:  
  24:     if ($synchConnection) 
  25:         {Write-Host "Successfully obtained synchronization connection!"} 
  26:     else 
  27:         {Write-Host "Failed to obtain user synchronization connection!"} 
  28:  
  29:     Write-Host "Adding the attribute mapping..." 
  30:     $synchConnection.PropertyMapping.AddNewMapping([Microsoft.Office.Server.UserProfiles.ProfileType]::User, $spsProperty, $fimProperty) 
  31:     Write-Host "Done!"
  32: }

If you will put this script into “map.attribute.ps1” file, then you can call it like this:

   1: $thisDir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent;
   2: . ($thisDir + '\map.attribute.ps1')
   3:  
   4: $url = "http://example.com" #URL of any site collection that is associated to the user profile service application. 
   5: $spsProperty = "BusinessUnit" #Internal name of the SharePoint user profile property 
   6: $fimProperty = "businessUnit" #Name of the attribute in FIM/LDAP source 
   7: $connectionName = "MyConnection" #Name of the SharePoint synchronization connection
   8:  
   9: MapAttribute $url $fimProperty $spsProperty $connectionName

When I ran this script on my environment, new attribute didn’t appear in the available mapping list in the User Profile properties. However didn’t try to map without it, so I decided to put it here also for reference (may be on your environment it will work). In my case in order to get it work (i.e. in order to display “businessUnit” attribute in mappings list) I needed to recreate User Profile Service Application synchronization connection:

  1. Go to Central Administration > Manage service applications > User Profile Service Application > Configure Synchronization Connection
  2. Delete existing connection (in script above it is called “MyConnection”) and create a new one with the same name
  3. Perform full profile synchronization: Central Administration > Manage service applications > User Profile Service Application > Start Profile Synchronization
  4. After full synchronization will be finished go to User Profile properties. Now you should see “businessUnit” attribute in the attributes list available for mapping. Create new property and map it to the custom AD attribute. Check “Indexed” checkbox – in this case it will be available for Sharepoint crawler.

This is all what I wanted to say about creation of custom AD attribute and its mapping to the User Profile property. Hope this information will help you in your work.

Update 2011-04-09: in the following post I show how you can customize People search and display custom AD attributes on search result page: Add custom AD attributes to People search results in Sharepoint.

Tuesday, February 15, 2011

Programmatically change background color of the web part header

Recently I faced with the following problem: we had requirement for particular custom web part: its header should have appropriate color (which differs from default color of all another web parts headers). During investigation I found that there are not so much solutions, which match all our requirements. It is relatively easy to change header color of all web parts on your site: you just need to play with “ms-WPHeader” style as described here. But it is not so easy to apply change on particular web parts. E.g. solution described here assumes that you should create custom class – inheritor of WebPartZone (ASP.Net WebPartZone, not Sharepoint WebPartZone which is sealed) and custom Chrome type. It is interesting solution, but with it you will need to use custom classes everywhere you want to customize header of the web part. Also it assumes that all web parts from WebPartZone will have the same chrome (which is better comparing with the case of all web parts, but not perfect still).

Another solution shows how to use Content Editor web part in order to add a style which will be uniquely identify header of one particular web part (because of using identifier “MSOZoneCell_WebPartWPQ2” in style) and apply css style to this header. This is also interesting solution – unfortunately it requires manual actions.

In ideal I would want to have control over web part header occurrence inside web part itself. And of course changes made to this web part should not affect all other web parts.

I tried several ways to achieve the result (both server side and client side). E.g. I tired to use Control Adapter for WebPartZone – where you can override Render() method and add additional styles to the header (technically web part header is “tr” element as you will show below). It works but requires modification of .browser files and complicated regex manipulation. Also overridden Render() method when you render control to the string and then replace something in this string can affect performance.

After playing a bit with client side solution (using jquery) I decided to combine server side and client side approaches.

First of all we need to add jquery reference on our master page (into head section):

   1: <SharePoint:ScriptLink ID="ScriptLink4" language="javascript" name="/_layouts/Fiskars/scripts/jquery-1.4.4.min.js" OnDemand="False" LoadAfterUI="False" Localizable="False" runat="server"/>

Then you need to add the following javascript function in the master page also:

   1: <script type="text/javascript">
   2:     function changeTitleColor(id) {
   3:         jQuery('#' + id).parentsUntil("td[id^='MSOZoneCell_']").find('tr.ms-WPHeader').css('background-color', 'Orange');
   4:     }
   5: </script>

In order to understand what it does lets consider what is actually web part in document object model:

image

Generally web part is rendered as a table (class s4-wpTopTable) with 2 rows: first row is for header, 2nd row is for web part content. The problem that inside web part we can control only those part which is rendered in 2nd row (i.e. only web part content – see picture). All another parts like header and border are controlled using web part chrome which is retrieved from WebPartZone. The good news is that inside web part we have access to it ClientID which we can use for accessing mentioned components using jquery (on the picture above it is “ctl00_m_g_72570dc0_fe27_43ca_82b9_8f324b8370eb”). Now lets return to the javascript function “changeTitleColor()” and see what it does. First of all it goes top most to the parent element which has “id” attribute beginning with “MSOZoneCell_” string:

   1: .parentsUntil("td[id^='MSOZoneCell_']")

As shown on the picture it stops on the following element:

   1: <table class="s4-wpTopTable" border="0" cellpadding="0" cellspacing="0" width="100%">

(parentUntil() method doesn’t include the element itself which matches condition). The rest is simple: we find all descendent “tr” elements which have “ms-WPHeader” class and change their css “background-color” property to the “Orange”:

   1: .find('tr.ms-WPHeader').css('background-color', 'Orange');

The only thing we need to do is to call our javascript function with identifier of the web part. It can be done by single line of code which should be added into CreateChildControls() method of your web part:

   1: protected override void CreateChildControls()
   2: {
   3:     ...
   4:     this.Controls.Add(new LiteralControl(string.Format("<script type=\"text/javascript\">changeTitleColor('{0}');</script>", this.ClientID)));
   5:     ...
   6: }

That’s all: after this you will have possibility to specify web part header color in the code of web part itself.

Saturday, February 12, 2011

Converting ASP.Net MVC 2.0 application to 3.0

Recently I converted medium size project to ASP.Net MVC 3.0. In parallel I decided to move from web forms view engine to the Razor as it is wordless and more clean. In this post I would like to share my experience and show problems which I found during migration.

First of all check the following article of Scott Guthrie: Announcing release of ASP.NET MVC 3, IIS Express, SQL CE 4, Web Farm Framework, Orchard, WebMatrix. In the “How to Upgrade Existing Projects” he says:

ASP.NET MVC 3 is compatible with ASP.NET MVC 2 – which means it should be easy to update existing MVC projects to ASP.NET MVC 3

It is generally true for MVC itself. But in real life applications there are a lot of 3rd party libraries which should be also compatible with new MVC version. Also if you perform migration from web forms view engine to Razor there will be another problems. So generally migration process is not so easy and straightforward.

For begin check manual upgrade guide available from here (there is also automated ASP.NET MVC 3 upgrade tool available for download, but I don’t really like these kind of tools because I want to know what I need to do and why I need to do it). Start with the steps described in the manual upgrade guide. Note that there is typo in step 6: instead of System.WebPages.dll you need to add System.Web.WebPages.dll.

After you have performed all steps described in the guide try to compile your application. Most probably 1st compilation will fail. In my case it was caused by incompatibility with another libraries:

MVCContrib assembly version for MVC 3.0 is available for download from codeplex. For MVCFutures (Microsoft.Web.Mvc.dll) the process is not straightforward: I didn’t find binary version available for download on the http://aspnet.codeplex.com. There is ASP.NET MVC 3 Beta release which contains link on ASP.NET MVC 3 Beta Futures. But unfortunately beta version is not compatible with final release of MVC 3. So the only way I found was to download sources from ASP.NET MVC 3 RTM and build it by myself.

At the moment of writing this article MvcSiteMapProvider  was also released for MVC 3.0 (date of release is 4 Feb 2011). When I started migration there were no version for MVC 3 available – fortunately MvcSiteMapProvider for MVC 2 also works in my scenarios.

Telerik extensions are available for MVC 3.0 at the moment of writing this article. Do not forget that for Telerik extensions with Telerik.Web.Mvc.dll you also need to add css and scripts for appropriate version (these files are located in appropriate folder with version number name, e.g. 2010.3.1318).

Now about converting from web forms view engine to Razor. There is Razor converter tool available from Telerik. I recommend to use this tool as it will make most of simple transformations automatically. Although it has problems, so you will still need to perform manual work. I found the following problems during migration:

  • doesn’t convert placeholders correctly
  • If you use strong typed ActionLinks from MVCFutures:
    @Html.ActionLink<RequestController>(c => c.Create(), ...)
    you will need to add parenthesis after @ for each expression by yourself, e.g.:
    @(Html.ActionLink<RequestController>(c => c.Create(), …))
  • ignores Import directives, so you have to do it manually:
    <%@ Import Namespace="UI.Controllers" %>
    to
    @using UI.Controllers
  • doesn’t convert server scripts in html tags and javascript variables, so you will also need to do it manually:
    <img alt="" src="<%= Url.Content("~/content/images/foo.gif") %>" />
    to
    <img alt="" src="@Url.Content("~/content/images/foo.gif")" />

In addition to the mentioned problems you may face with situation that some html helpers which worked for web forms view engine doesn’t work for Razor. E.g. Grid from MVCContrib has known limitation: action syntax is not working for Razor. From twitter conversation with Jeremy Skinner (author of MVCContrib grid): the workaround is to replace action syntax with custom columns.

That’s all what I wanted to share with you in this post. Hope it will help you in your work.

Saturday, February 5, 2011

Get “Manage Hierarchy” and “Approve” role definitions programmatically in Sharepoint

In Sharepoint there are several OTB role definitions which are created during site creation:

Role name Description
Full Control Has full control
Design  Can view, add, update, delete, approve, and customize
Contribute Can view, add, update, and delete list items and documents
Read Can view pages and list items and download documents
Limited Access Can view specific lists, document libraries, list items, folders, or documents when given permissions
Approve Can edit and approve pages, list items, and documents
Manage Hierarchy Can create sites and edit pages, list items, and documents
Restricted Read Can view pages and documents, but cannot view historical versions or user permissions

You can access some of these role definitions via object model using SPRoleType enum using the following mapping table:

Role name SPRoleType member
Full Control SPRoleType.Administrator
Design SPRoleType.WebDesigner
Contribute SPRoleType.Contributor
Read SPRoleType.Reader
Limited Access SPRoleType.Guest
Approve SPRoleType.None
Manage Hierarchy SPRoleType.None
Restricted Read SPRoleType.None

Note that last 3 roles have SPRoleType.None type. It means that you can’t retrieve these role definitions using SPRoleDefinitionCollection.GetByType() method. Restricted Access is special role – it is shown when e.g. user has access on some folder in the list, but not has access to the whole list or web site. In most cases you should not use this role programmatically. But what with “Manage Hierarchy” and “Approve” roles? We also can’t use SPRoleDefinitionCollection.GetByType() method in order to get them programmatically. What options do we have?

The most often approach I found during investigation is to use hardcoded literals for the role name:

   1: string roleName = "Manage Hierarchy";
   2: SPRoleDefinition roleDefinition =
   3:     web.RoleDefinitions.Cast<SPRoleDefinition>().FirstOrDefault(r => r.Name == roleName);

This approach works, but if you have multilingual topology you will have problems, because “Manage Hierarchy” role name will be localized according to the web site name (e.g. on Finnish it will be “Hierarkian hallinta”). The best option would be retrieving of the role definition using the same way as Sharepoint uses.

After some investigation I found Microsoft.SharePoint.Publishing.Internal.StringIds class (defined in the Microsoft.SharePoint.Publishing.dll). It has a field StringIds.RoleNameHierarchyManager which seems like what I looking for. Unfortunately there is no so much information about it, so I used reflector to watch how Sharepoint uses it. Sharepoint uses these strings via internal class Microsoft.SharePoint.Publishing.Internal.Resources in the same Microsoft.SharePoint.Publishing.dll assembly like this (see Microsoft.SharePoint.Publishing.Internal.RootProvisioner.AddSecuritySettings() method):

   1: Resources.GetString("RoleNameHierarchyManager")

If you will investigate Microsoft.SharePoint.Publishing.Internal.Resources class you will find that it uses Microsoft.SharePoint.Publishing.Intl.dll assembly for retrieving resource values (this assembly is located in the GAC).

So in order to retrieve role name by the same way as Sharepoint retrieves it, you have several options:

  • use reflection
  • create custom class which works like internal class Microsoft.SharePoint.Publishing.Internal.Resources

I will show you 2nd variant. You can use the following class in order to retrieve resource strings from Microsoft.SharePoint.Publishing.Intl.dll assembly:

   1: public class PublishingResources
   2: {
   3:     private static ResourceManager resourceManager;
   4:  
   5:     static PublishingResources()
   6:     {
   7:         var resourceAssembly = Assembly.Load("Microsoft.SharePoint.Publishing.intl, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c");
   8:         resourceManager = new ResourceManager("Microsoft.SharePoint.Publishing.Strings", resourceAssembly);
   9:     }
  10:  
  11:     internal static string GetString(string resourceName, CultureInfo cultureInfo)
  12:     {
  13:         return resourceManager.GetString(resourceName, cultureInfo);
  14:     }
  15: }

Now you are able to retrieve “Manage Hierarchy” role definition programmatically without hardcoding role name:

   1: string roleName = PublishingResources.GetString(StringIds.RoleNameHierarchyManager, web.UICulture);
   2: SPRoleDefinition roleDefinition =
   3:     web.RoleDefinitions.Cast<SPRoleDefinition>().FirstOrDefault(r => r.Name == roleName);

All information from this post is also appliable for “Approve” role definition. Just instead of StringIds.RoleNameHierarchyManager you will need to use StringIds.RoleDescriptionApprover for it.