Sunday, December 11, 2011

Use Telerik Rad Editor Lite without features activation

In one my previous post I wrote how to use Telerik Rad Editor Lite for Firefox (and other than IE browsers). Here we will extend the control which we created in order to use it in all sites without additional actions from your side. In order to use Telerik Rad Editor you need to activate 2 features:

  • Use RadEditor to edit List Items (located in RadEditorFeature)
  • Use RadEditor to edit List Items in Internet Explorer as well (located in RadEditorFeatureIE)

First feature during activation copies new rendering template for RichTextField with Telerik control which replaces default Sharepoint editor for html fields (copying is done in feature receiver). Second feature doesn’t have feature receiver – instead it has activation dependency on 1st feature and used as a flag (see below).

Control checks that these features are activated in the OnLoad() method:

   1: protected override void OnLoad(EventArgs e)
   2: {
   3:     base.ToolsFile = base.SetWebSpecificConfiguration(base.RadControlsDir + "Editor/ListToolsFile.xml");
   4:     base.ConfigFile = base.SetWebSpecificConfiguration(base.RadControlsDir + "Editor/ListConfigFile.xml");
   5:     base.OnLoad(e);
   6:     if (this.ShowEditorOnPage())
   7:     {
   8:         this.SetSPField();
   9:         ((HtmlControl) this.htmlBox.Parent).Style.Add("display", "none");
  10:         if (this.baseField.NumberOfLines > 6)
  11:         {
  12:             this.Height = (this.baseField.NumberOfLines * this.FontSizeCoef) * 2;
  13:         }
  14:         if (this.rtField.DisplaySize > 0x4b)
  15:         {
  16:             this.Width = this.rtField.DisplaySize * this.FontSizeCoef;
  17:         }
  18:         if (this.baseField.RichTextMode == SPRichTextMode.Compatible)
  19:         {
  20:             base.Toolbars.Remove(base.Toolbars["EnhancedToolbar"]);
  21:             base.ShowHtmlMode = false;
  22:         }
  23:         this.RegisterSubmitScript();
  24:         this.Page.PreRenderComplete += new EventHandler(this.Page_PreRenderComplete);
  25:     }
  26:     else
  27:     {
  28:         this.Visible = false;
  29:     }
  30: }
  31:  
  32: private bool ShowEditorOnPage()
  33: {
  34:     SPFeature feature = this.GetFeature("F374A3CA-F4A7-11DB-827C-8DD056D89593");
  35:     SPFeature feature2 = this.GetFeature("747755CD-D060-4663-961C-9B0CC43724E9");
  36:     if ((feature2 == null) || (feature2.Definition == null))
  37:     {
  38:         return false;
  39:     }
  40:     if (((feature == null) || (feature.Definition == null)) &&
  41:         (this.Page.Request.Browser.IsBrowser("IE") && (this.Page.Request.Browser.MajorVersion >= 6)))
  42:     {
  43:         return false;
  44:     }
  45:     return true;
  46: }

If we want to use Telerik editor on all sites without activating the features, we need to override ShowEditorOnPage() method and always return true.

In the previous post we already created custom control which inherits RadHtmlListField. Now we need extend it – override OnLoad() method. As it access private variables and methods we need to reuse reflection in order to simulate work of the RadHtmlListField. Mostly all places where it access private variables can be solved with basic reflection, but not all. See that it calls base.OnLoad() method (line 5), i.e. it calls OnLoad() method of the base class MOSSRadEditor. As we have our own class which inherits RadHtmlListField, then inheritance chain looks like this:

AllBrowsersHtmlListField –> RadHtmlListField –> MOSSRadEditor

In our AllBrowsersHtmlListField we want to call MOSSRadEditor.OnLoad(), but not RadHtmlListField.OnLoad() – it means that we can’t call base.OnLoad(). I.e. we need to skip one tier in the inheritance chain and call virtual method OnLoad(), but not from base class, but from base of base class. And this is not trivial task because it means that we need to avoid the rules of the underlying programming language.

Investigation of this problem showed that problem is really not simple. But in this forum thread I found interesting mention that DynamicMethod can be used for this. The idea is that we will build new method in runtime using IL instructions and then call it. In our case it will look like this:

   1: private void NewOnLoad(EventArgs eventArgs)
   2: {
   3:     BindingFlags bf = 0 | BindingFlags.Instance | BindingFlags.Static |
   4:         BindingFlags.Public | BindingFlags.NonPublic;
   5:     MethodInfo mi = this.GetType().BaseType.BaseType.FindMembers(MemberTypes.Method,
   6:         bf, Type.FilterName, "OnLoad")[0] as MethodInfo;
   7:     DynamicMethod dm = new DynamicMethod("BaseBaseOnLoad", null,
   8:         new Type[] { this.GetType(), typeof(EventArgs) }, this.GetType());
   9:     ILGenerator gen = dm.GetILGenerator();
  10:     gen.Emit(OpCodes.Ldarg_0);
  11:     gen.Emit(OpCodes.Ldarg_1);
  12:     gen.Emit(OpCodes.Call, mi);
  13:     gen.Emit(OpCodes.Ret);
  14:  
  15:     var BaseBaseOnLoad = (Action<AllBrowsersHtmlListField, EventArgs>)
  16:         dm.CreateDelegate(typeof(Action<AllBrowsersHtmlListField, EventArgs>));
  17:     BaseBaseOnLoad(this, eventArgs);
  18: }

At first we get a reference to the MOSSRadEditor.OnLoad() method (lines 3-6). Then we create new DynamicMethod (BaseBaseOnLoad) and specify in parameters type of AllBrowsersHtmlListField (type of object which is used for calling) and EventArgs which is passed to the OnLoad() method. Then using IL generator we create method’s body (lines 10-13). Note that on line 9 we construct call to the method which actually makes trick – because this call will go to the MOSSRadEditor type. Then we create delegate and call it with parameters which we specified earlier (lines 15-17).

It was complex part. The rest of things can be done with basic reflection. We will need extended ReflectionHelper class:

   1: public static class ReflectionHelper
   2: {
   3:     public static object CallMethod(object obj, string name, params object[] argv)
   4:     {
   5:         BindingFlags bf = 0 | BindingFlags.Instance | BindingFlags.Static |
   6:             BindingFlags.Public | BindingFlags.NonPublic;
   7:         MethodInfo mi = obj.GetType().BaseType.FindMembers(MemberTypes.Method,
   8:             bf, Type.FilterName, name)[0] as MethodInfo;
   9:         return mi.Invoke(obj, argv);
  10:     }
  11:  
  12:     public static object GetFieldValue(object obj, string name)
  13:     {
  14:         BindingFlags bf = 0 | BindingFlags.Instance | BindingFlags.Static |
  15:             BindingFlags.Public | BindingFlags.NonPublic;
  16:         FieldInfo fi = obj.GetType().BaseType.FindMembers(MemberTypes.Field,
  17:             bf, Type.FilterName, name)[0] as FieldInfo;
  18:         return fi.GetValue(obj);
  19:     }
  20:  
  21:     public static void SetFieldValue(object obj, string name, object value)
  22:     {
  23:         BindingFlags bf = 0 | BindingFlags.Instance | BindingFlags.Static |
  24:             BindingFlags.Public | BindingFlags.NonPublic;
  25:         FieldInfo fi = obj.GetType().BaseType.BaseType.BaseType.FindMembers(MemberTypes.Field,
  26:             bf, Type.FilterName, name)[0] as FieldInfo;
  27:         fi.SetValue(obj, value);
  28:     }
  29: }

With this helper class we can change all calls to the private members and perform them by reflection. Final solution will look like this:

   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:  
  18:         base.ToolsFile = base.SetWebSpecificConfiguration(base.RadControlsDir +
  19:             "Editor/ListToolsFile.xml");
  20:         base.ConfigFile = base.SetWebSpecificConfiguration(base.RadControlsDir +
  21:             "Editor/ListConfigFile.xml");
  22:         //base.OnLoad(e);
  23:         this.NewOnLoad(e);
  24:         //if (this.ShowEditorOnPage())
  25:         if (this.NewShowEditorOnPage())
  26:         {
  27:             //this.SetSPField();
  28:             ReflectionHelper.CallMethod(this, "SetSPField");
  29:  
  30:             var htmlBox = ReflectionHelper.GetFieldValue(this, "htmlBox")
  31:                 as TextBox;
  32:             ((HtmlControl)htmlBox.Parent).Style.Add("display", "none");
  33:  
  34:             var baseField = ReflectionHelper.GetFieldValue(this, "baseField")
  35:                 as SPFieldMultiLineText;
  36:             if (baseField.NumberOfLines > 6)
  37:             {
  38:                 this.Height = (baseField.NumberOfLines * this.FontSizeCoef) * 2;
  39:             }
  40:  
  41:             var rtField = ReflectionHelper.GetFieldValue(this, "rtField")
  42:                 as RichTextField;
  43:             if (rtField.DisplaySize > 0x4b)
  44:             {
  45:                 this.Width = rtField.DisplaySize * this.FontSizeCoef;
  46:             }
  47:             if (baseField.RichTextMode == SPRichTextMode.Compatible)
  48:             {
  49:                 base.Toolbars.Remove(base.Toolbars["EnhancedToolbar"]);
  50:                 base.ShowHtmlMode = false;
  51:             }
  52:             //this.RegisterSubmitScript();
  53:             ReflectionHelper.CallMethod(this, "RegisterSubmitScript");
  54:             this.Page.PreRenderComplete += NewPreRenderComplete;
  55:         }
  56:         else
  57:         {
  58:             this.Visible = false;
  59:         }
  60:     }
  61:  
  62:     private void NewOnLoad(EventArgs eventArgs)
  63:     {
  64:         BindingFlags bf = 0 | BindingFlags.Instance | BindingFlags.Static |
  65:             BindingFlags.Public | BindingFlags.NonPublic;
  66:         MethodInfo mi = this.GetType().BaseType.BaseType.FindMembers(MemberTypes.Method,
  67:             bf, Type.FilterName, "OnLoad")[0] as MethodInfo;
  68:         DynamicMethod dm = new DynamicMethod("BaseBaseOnLoad", null,
  69:             new Type[] { this.GetType(), typeof(EventArgs) }, this.GetType());
  70:         ILGenerator gen = dm.GetILGenerator();
  71:         gen.Emit(OpCodes.Ldarg_0);
  72:         gen.Emit(OpCodes.Ldarg_1);
  73:         gen.Emit(OpCodes.Call, mi);
  74:         gen.Emit(OpCodes.Ret);
  75:  
  76:         var BaseBaseOnLoad = (Action<AllBrowsersHtmlListField, EventArgs>)
  77:             dm.CreateDelegate(typeof(Action<AllBrowsersHtmlListField, EventArgs>));
  78:         BaseBaseOnLoad(this, eventArgs);
  79:     }
  80:  
  81:     private bool NewShowEditorOnPage()
  82:     {
  83:         return true;
  84:     }
  85:  
  86:     protected void NewPreRenderComplete(object sender, EventArgs e)
  87:     {
  88:         ReflectionHelper.CallMethod(this, "Page_PreRenderComplete", sender, e);
  89:     }
  90: }

It works in FF (and you can extend it also for other browsers) and doesn’t require features activation. Note that it will be used on all sites in your farm because rendering template is used globally by Sharepoint. However you can easily to change this behavior – check url of the current web application in the NewShowEditorOnPage() method and decide whether or not you need to use Telerik control.

This post showed that how knowledge of advanced language features may help in applied Sharepoint development. Hope it will help you in your work.

No comments:

Post a Comment