Wednesday, November 30, 2011

Parameters matching in PowerShell

In this short post I would like to share one interesting feature of the PowerShell: parameters matching by partial name. Suppose that you have the test.ps1 script with switch parameter:

   1: param(
   2: [switch]$recreateSite
   3: )
   4:  
   5: if ($recreateSite)
   6: {
   7:     Write-Host "recreateSite"
   8: }

If you run it in PowerShell console like this:

   1: .\test.ps1 –recreateSite

it will output “recreateSite” in console. Now the interesting thing is that you can run it like this:

   1: .\test.ps1 –r

and result will be the same. I.e. it is not necessary to specify whole name for the parameter – PowerShell will find closest match (-r corresponds to -recreateSite).

Let’s add another parameter “recreateDb”:

   1: param(
   2: [switch]$recreateSite,
   3: [switch]$recreateDb
   4: )
   5:  
   6: if ($recreateSite)
   7: {
   8:     Write-Host "recreateSite"
   9: }
  10:  
  11: if ($recreateDb)
  12: {
  13:     Write-Host "recreateDb"
  14: }

If you now will run it like this:

   1: .\test.ps1 –r

you will see the following error:

Parameter cannot be processed because the parameter name 'r' is ambiguous. Possible matches include: -recreateSite -recreateDb.

Now in order to run it you will need to use minimal name which will uniquely match some parameter. In our example:

   1: .\test.ps1 -recreates

will correspond to “recreateSite”. And:

   1: .\test.ps1 -recreated

will match to “recreateDb”.

Friday, November 25, 2011

Enable Telerik Rad Editor Lite for FireFox and other than IE browsers in Sharepoint

Sharepoint has own default editor for RichTextField. However if you are not satisfied with the functionality offered by this html editor, there are several other implementations available from other vendors. One of the popular editors for Sharepoint is Telerik Rad Editor Lite. Advantage of this control is that it is free (it was developed by Telerik using agreement with MS). However it works only in IE and according to information from forums from Telerik site there are no plans to support other browsers in free version.

The good news is that even if it is not officially supported it is still possible to show Rad Editor in other than IE browsers (there is not guarantee that all features will work properly in this case, but most of basic features work). We will need little reflection and knowledge of how rendering templates work in Sharepoint.

At first lets check from where problem comes. If you investigate code of RadHtmlListField in Reflector (from RadEditorSharePoint.dll), you will find that it has the following hierarchy:

RadEditor > MOSSRadEditor > RadHtmlListField

RadHtmlListField has property IsSupportedBrowser:

   1: internal bool IsSupportedBrowser
   2: {
   3:     get
   4:     {
   5:         this.CheckBrowserCapabilities();
   6:         return this._isSupportedBrowser;
   7:     }
   8: }

If you will check another class RadEditorRenderer you will find that this property is used in order to determine will Rad Editor will be rendered as html editor or as text area without any toolbars:

   1: private void RenderConditional()
   2: {
   3:     this._editor.Page.VerifyRenderingInServerForm(this._editor);
   4:     if (!this._editor.HasPermission)
   5:     {
   6:         this.RenderNonEditableContent();
   7:     }
   8:     else if (!this._editor.Editable)
   9:     {
  10:         this.RenderNonEditable();
  11:     }
  12:     else if (this._editor.RenderAsTextArea || !this._editor.IsSupportedBrowser)
  13:     {
  14:         this.RenderAsTextArea(this._editor.GetPostbackErrorMessage());
  15:     }
  16:     else
  17:     {
  18:         this.RenderEditable(this._editor.GetPostbackErrorMessage());
  19:     }
  20: }

(see line 12). Let’s check how Rad Editor determines browser capabilities. The logic is implemented in CheckBrowserCapabilities() method which is called from IsSupportedBrowser property (see above):

   1: private void CheckBrowserCapabilities()
   2: {
   3:     if (!this._browserCapabilitiesRetrieved)
   4:     {
   5:         this._browserCapabilitiesRetrieved = true;
   6:         if (base.IsDesignMode)
   7:         {
   8:             this._isSupportedBrowser = true;
   9:             this._isIe = true;
  10:             this._isSafari = false;
  11:         }
  12:         else
  13:         {
  14:             HttpRequest request = this.Context.Request;
  15:             HttpBrowserCapabilities browser = request.Browser;
  16:             if (browser.Browser == "IE")
  17:             {
  18:                 double num = browser.MinorVersion + browser.MajorVersion;
  19:                 this._isSupportedBrowser = num >= 5.5;
  20:                 this._isIe = true;
  21:             }
  22:             else if ((browser.Browser.ToLower() == "opera") &&
  23:                 (browser.MajorVersion == 9))
  24:             {
  25:                 this._isSupportedBrowser = true;
  26:             }
  27:             else
  28:             {
  29:                 string input =
  30:                     (request.UserAgent != null) ? request.UserAgent.ToLower() : "";
  31:                 if (Regex.Match(input, @"rv:((1\.(3|4|5|6|7|8|9|10))|((2|3|4|5)\.\d))",
  32:                     RegexOptions.Compiled | RegexOptions.IgnoreCase).Success)
  33:                 {
  34:                     this._isSupportedBrowser = true;
  35:                 }
  36:                 else if ((input.IndexOf("safari") > 0) && (input.IndexOf("gecko") > 0))
  37:                 {
  38:                     this._isSafari = true;
  39:                     this._isSupportedBrowser = true;
  40:                 }
  41:                 else
  42:                 {
  43:                     this._isSupportedBrowser = false;
  44:                 }
  45:             }
  46:         }
  47:     }
  48: }

This method is the root of all problems. When you use last version of FireFox (I used FF 8) request.Browser contains “Firefox” and user agent contains “2 mozilla/5.0 (windows nt 5.2; rv:8.0) gecko/20100101 firefox/8.0”. In older FF versions (I tested with 3.6) Rad Editor worked – so it passed another values in specified fields (I didn’t check what exact values before to update it to version 8).

Now when we know the problem let’s find solution. We need to check if browser is Firefox and if yes set _isSupportedBrowser field to true. This field is private so we can’t just inherit the RadHtmlListField class and modify it. We need to use reflection. I created the following helper class:

   1: public static class ReflectionHelper
   2: {
   3:     public static void SetFieldValue(object obj, string name, object value)
   4:     {
   5:         BindingFlags bf = 0 | BindingFlags.Instance | BindingFlags.Static |
   6:             BindingFlags.Public | BindingFlags.NonPublic;
   7:         FieldInfo fi = obj.GetType().BaseType.BaseType.BaseType.FindMembers(MemberTypes.Field,
   8:             bf, Type.FilterName, name)[0] as FieldInfo;
   9:         fi.SetValue(obj, value);
  10:     }
  11: }
 
Now we need to create custom class – inheritor of RadHtmlListField and override OnLoad() method. In the overridden version we will check is the browser is Firefox and if yes – will set private fields by reflection:
 
   1: public class AllBrowsersHtmlListField : RadHtmlListField
   2: {
   3:     private void checkForFirefox()
   4:     {
   5:         HttpRequest request = HttpContext.Current.Request;
   6:         HttpBrowserCapabilities browser = request.Browser;
   7:         if ((browser.Browser.ToLower() == "firefox"))
   8:         {
   9:             ReflectionHelper.SetFieldValue(this, "_browserCapabilitiesRetrieved", true);
  10:             ReflectionHelper.SetFieldValue(this, "_isSupportedBrowser", true);
  11:         }
  12:     }
  13:  
  14:     protected override void OnLoad(EventArgs e)
  15:     {
  16:         this.checkForFirefox();
  17:         base.OnLoad(e);
  18:     }
  19: }
 
Very important to set also _browserCapabilitiesRetrieved field. Otherwise code in base class will be again executed and _isSupportedBrowser will be false.
 
Now there is only one step remaining. When you activate feature “Use RadEditor to edit list items” it copies new rendering template RadEditorList.ascx for RichTextField into ControlTemplate folder (it overrides default rendering template). You need to modify this file in order to use your own class AllBrowsersHtmlListField:
   1: <%@ Control Language="C#" AutoEventWireup="false" %>
   2: ...
   3: <%@ Register TagPrefix="customradeditor" Assembly="CustomTelerikField, Version=1.0.0.0, Culture=neutral, PublicKeyToken=..." Namespace="CustomTelerikField" %>
   4: <SharePoint:RenderingTemplate ID="RichTextField" runat="server">
   5:         <Template>
   6:                 <customradeditor:AllBrowsersHtmlListField id="RadTextField" runat="server" FontSizeCoef="7" />
   7:                 ...
   8:         </Template>
   9: </SharePoint:RenderingTemplate>

It is also worth to make RadEditorList.ascx read only so when “Use RadEditor to edit list items” will be activated next time it won’t override your version. Code which copies file is inside try/catch block so it won’t fail feature activation and site creation if it is stapled with this feature.

After you will perform iisreset Sharepoint will use your own control and those will display Rad Editor in FireFox. If you will have similar problems with another browsers you can fix them by the same way.

Saturday, November 19, 2011

Bug with using Linq 2 Sharepoint in anonymous mode with managed metadata

You most probably heard about problem with using Linq 2 Sharepoint in anonymous mode. August 2010 cumulative update fixed this problem. However recently we faced with one of variations of this problem: Linq 2 Sharepoint doesn’t work with managed metadata in anonymous mode. Below I will describe the problem.

Suppose that you have a list of products which have Applications managed metadata column which contains possible usage areas of the product (applications may be nested, i.e. term set is hierarchical). You use Linq 2 Sharepoint in order to generate business entities with strongly typed properties. What about Applications column? In won’t be mapped by default. In order to map it to list of business objects (IEnumerable<Application>) you need create POCO class Application by yourself which may look like this:

   1: public class Application
   2: {
   3:     public Guid Id { get; set; }
   4:     public string Name { get; set; }
   5:     public string Description { get; set; }
   6:     public Application Parent;
   7: }

(Linq 2 Sharepoint doesn’t map entities from term store). After that you need extend mapping generated by SPMetal and add mapping from field value to collection of business object. Check the following link for more details: Extending the Object-Relational Mapping. Idea is the following: you need to create partial class (for those class which was generated by SPMetal) and implement MapFrom/MapTo and Resolve methods. I won’t explain it in details (link above contains enough information about them), just show how it may look like in our example:

   1: public partial class Products : ICustomMapping
   2: {
   3:     [CustomMapping(Columns = new[] { Constants.APPLICATIONS_FIELD_NAME })]
   4:     public void MapFrom(object listItem)
   5:     {
   6:         var item = (SPListItem)listItem;
   7:         this.ApplicationsRaw = item[Constants.APPLICATIONS_FIELD_NAME] as TaxonomyFieldValueCollection;
   8:     }
   9:  
  10:     public void MapTo(object listItem)
  11:     {
  12:         var item = (SPListItem)listItem;
  13:         item[Constants.APPLICATIONS_FIELD_NAME] = this.ApplicationsRaw;
  14:     }
  15:  
  16:     public void Resolve(RefreshMode mode, object originalListItem, object databaseListItem)
  17:     {
  18:         ...
  19:     }
  20:  
  21:     private bool applicationsDirty = true;
  22:     private TaxonomyFieldValueCollection applicationsRaw;
  23:     private TaxonomyFieldValueCollection ApplicationsRaw
  24:     {
  25:         get
  26:         {
  27:             return applicationsRaw;
  28:         }
  29:         set
  30:         {
  31:             if (value == applicationsRaw)
  32:             {
  33:                 return;
  34:             }
  35:  
  36:             this.OnPropertyChanging(Constants.APPLICATIONS_FIELD_NAME, applicationsRaw);
  37:             applicationsRaw = value;
  38:             this.applicationsDirty = true;
  39:             this.OnPropertyChanged(Constants.APPLICATIONS_FIELD_NAME);
  40:         }
  41:     }
  42:  
  43:     private IEnumerable<Application> applications = new List<Application>();
  44:     public IEnumerable<Application> Applications
  45:     {
  46:         get
  47:         {
  48:             if (this.applicationsDirty)
  49:             {
  50:                 this.applications = ... // logic for mapping from Terms to Applications POCO
  51:                 this.applicationsDirty = false;
  52:             }
  53:             return this.applications;
  54:         }
  55:     }
  56: }

Now suppose that our web application is extended to Internet zone which allows anonymous access (http://example.com and http://public.example.com). And we need to retrieve list of Products using repository pattern (which uses Linq 2 Sharepoint in actual implementation of course) and it should work both for authenticated users and anonymous users. If we would not have managed metadata it would work (as I mentioned above August 2010 CU fixed this and tests showed that it really works :) ). However in our case it won’t work for anonymous.

Let’s see how it is used:

   1: var repository = SharePointServiceLocator.GetCurrent(SPContext.Current.Site)
   2:     .GetInstance<IProductsRepository>();
   3: repository.SetWeb(SPContext.Current.Web);
   4: var products = repository.GetAll();

Here we also use SharePointServiceLocator, but this is topic for other posts. When we set web and call GetAll() method it initializes Linq 2 Sharepoint data context (ProductsGeneratedCodeDataContext in this case) using URL of specified web:

   1: public class ProductsByApplicationRepository : IProductsByApplicationRepository
   2: {
   3:     private override ProductsGeneratedCodeDataContext getDataContext(SPWeb web)
   4:     {
   5:         return new ProductsGeneratedCodeDataContext(web.Url);
   6:     }
   7:     ...
   8: }

When this code is executed for authenticated users, web site URL points to the default authentication zone http://example.com, for anonymous users it points to http://public.example.com. And for anonymous users it doesn’t work – list of managed metadata terms are not mapped. If we will check in debugger MapFrom() method:

   1: [CustomMapping(Columns = new[] { Constants.APPLICATIONS_FIELD_NAME })]
   2: public void MapFrom(object listItem)
   3: {
   4:     var item = (SPListItem)listItem;
   5:     this.ApplicationsRaw = item[Constants.APPLICATIONS_FIELD_NAME] as TaxonomyFieldValueCollection;
   6: }

we will find that item.Web points to http://public.example.com. And for TaxonomyFieldValueCollection all terms contain Label, but TermGuid property is empty (00000000-0000-0000-0000-000000000000).

The first try to fix it is to use RunWithElevatedPrivileges() and open site in default authentication zone (http://example.com). I wrote about it here: Open SPSite under RunWithElevatedPrivileges in proper zone:

   1: SPSecurity.RunWithElevatedPrivileges(() =>
   2: {
   3:     using (var site = new SPSite(SPContext.Current.Site.ID))
   4:     {
   5:         using (var web = site.OpenWeb(SPContext.Current.Web.ID))
   6:         {
   7:             var repository = SharePointServiceLocator.GetCurrent(site)
   8:                 .GetInstance<IProductsRepository>();
   9:             repository.SetWeb(web);
  10:             var products = repository.GetAll();
  11:         }
  12:     }
  13: });

After that when context is created – web URL points to http://example.com (i.e. to authenticated zone). But if we will check in debugger MapFrom() method we will see that item.Web.Url still points to http://public.example.com.

The problem is that internally Linq 2 Sharepoint uses SPContext.Current.Site and as result it points to anonymous zone. The following post contains explanation of this behavior: Linq to SharePoint: Query Across Site Collections. The root problem is in SPServerDataConnection:

   1: public void SPServerDataConnection(string url)
   2: {
   3:     if (SPContext.Current != null)
   4:     {
   5:         this.defaultSite = SPContext.Current.Site;
   6:         this.defaultWeb = (SPContext.Current.Web.Url == url) 
   7:             ? SPContext.Current.Web 
   8:             : this.defaultSite.OpenWeb(new Uri(url).PathAndQuery);
   9:     }
  10:     else
  11:     {
  12:         this.defaultSite = new SPSite(url);
  13:         this.defaultWeb = this.defaultSite.OpenWeb(new Uri(url).PathAndQuery);
  14:     }
  15:     if (!this.defaultWeb.Exists)
  16:     {
  17:         throw new ArgumentException
  18:             (Resources.GetString("CannotFindWeb", new object[] { url }));
  19:     }
  20:     this.defaultWebUrl = this.defaultWeb.ServerRelativeUrl;
  21:     this.openedWebs = new Dictionary<string, SPWeb>();
  22:     this.openedWebs.Add(this.defaultWebUrl, this.defaultWeb);
  23: }

In order to fix it we need to fake SPContext.Current and set it to null (as shown in code above in this case SPSite will be reopened). After code will be executed – we need to restore SPContext.Current. I.e. this is the same solution which was used for general Linq 2 Sharepoint in anonymous mode problem. Nice wrapper is shown here: Making Linq to SharePoint work for Anonymous users:

   1: public static class AnonymousContextSwitch
   2: {
   3:     public static void RunWithElevatedPrivelligesAndContextSwitch(SPSecurity.CodeToRunElevated secureCode)
   4:     {
   5:         try
   6:         {
   7:             //If there is a SPContext.Current object and there is no known user, we need to take action
   8:             bool shouldSwitch = (SPContext.Current != null && SPContext.Current.Web.CurrentUser == null);
   9:  
  10:             HttpContext backupCtx = HttpContext.Current;
  11:             try
  12:             {
  13:                 if (shouldSwitch)
  14:                     HttpContext.Current = null;
  15:  
  16:                 SPSecurity.RunWithElevatedPrivileges(secureCode);
  17:             }
  18:             finally
  19:             {
  20:                 if (shouldSwitch)
  21:                     HttpContext.Current = backupCtx;
  22:             }
  23:         }
  24:         catch (Exception x)
  25:         {
  26:             // log
  27:         }
  28:     }
  29: }

So the final fix will look like this:

   1: var site = SPContext.Current.Site;
   2: var web = SPContext.Current.Web;
   3: AnonymousContextSwitch.RunWithElevatedPrivelligesAndContextSwitch(
   4:     () =>
   5:         {
   6:             var repository = SharePointServiceLocator.GetCurrent(site)
   7:                 .GetInstance<IProductsRepository>();
   8:             repository.SetWeb(web);
   9:             var products = repository.GetAll();
  10:         });

Note that we can’t use SPContext.Current inside AnonymousContextSwitch because it will be null.

After this in MapFrom() method item.Web will point to http://example.com and TermGuid won’t be empty for all terms and will be mapped successfully. From my vision this problem looks like a bug in Sharepoint.