Thursday, July 29, 2010

View mode panel for Sharepoint

If you used publishing infrastructure in Sharepoint then you probably know OTB EditModePanel control defined in Microsoft.SharePoint.Publishing.dll assembly. Using this control you can define what content on your publishing page  should be shown only in edit mode:

   1: <PublishingWebControls:EditModePanel runat="server">
   2:     ...
   3: </PublishingWebControls:EditModePanel>

In this example everything inside EditModePanel will be shown only when user modifies page. In view mode it will not be shown.

Unfortunately there is no similar OTB ViewModePanel which shows content only when page is running in view mode, i.e. when user views the page. So I investigated code of OTB EditModePanel via Reflector and created following control:

   1: public class ViewModePanel : Panel
   2: {
   3:     private bool shouldRender;
   4:  
   5:     protected override void AddParsedSubObject(object obj)
   6:     {
   7:         this.calculateShouldRender();
   8:         if (this.shouldRender)
   9:         {
  10:             base.AddParsedSubObject(obj);
  11:         }
  12:     }
  13:  
  14:     private void calculateShouldRender()
  15:     {
  16:         this.shouldRender = false;
  17:  
  18:         if (SPContext.Current.FormContext != null)
  19:         {
  20:             this.shouldRender =
  21:                 (SPContext.Current.FormContext.FormMode ==
  22:                     SPControlMode.Display);
  23:         }
  24:         this.Visible = this.shouldRender;
  25:     }
  26: }

Now we are able to specify it in layout of publishing pages:

   1: <uc:ViewModePanel runat="server">
   2:     ...
   3: </uc:ViewModePanel>

and all content between begin and end tags will be shown only in view mode.

Sunday, July 25, 2010

Using NVelocity installed into GAC with example for Sharepoint

In this post I will describe how you can use NVelocity template engine in Sharepoint and what issues will occur with it. Main problem is that NVelocity works well if it is installed in bin folder of you web application. But in Sharepoint development most common case when assemblies are installed into GAC. Ofcourse you can still add NVelocity.dll into bin folder of target Sharepoint web site (if you use WSPBuilder see this post which describes how to install dll into bin folder instead of GAC). But what if you need still install assemblies into GAC (e.g. because of security reasons)? Unfortunately by default NVelocity can not be used from GAC. I will show how to fix this problem for such scenario. Although I will use examples for Sharepoint this post can be used in other cases when you need to use NVelocity from GAC.

First of all a few words about NVelocity. It is template engine ported from Java world into .Net (when I talk about NVelocity I mean implementation from Castle project instead original version – there are several convenient improvements which will be described below). It is quite useful for many scenarios when you need to generate template-based content using set of parameters. For example you need to send emails which are generated based on template. In this case you can use create custom template with placeholders (using curly brackets) and replace them in runtime using regular expressions for example. It will work, but what if you need add iterative blocks into your template? You will need to add this functionality into your custom template engine, fixing more bugs, add more test, etc. Why do we need to create own implementation of things which are already exists? Lets see how NVelocity may help.

It is quite powerful framework which has native supports of loops inside template. Also with NVelocity you can use C# like syntax for your templates. Lets see example. Suppose that we have the following model classes for our email:

   1: public class Letter
   2: {
   3:     public Header Header { get; set; }
   4:     public List<Block> Blocks { get; set; }
   5: }
   6:  
   7: public class Block
   8: {
   9:     public string Title { get; set; }
  10:     public string Content { get; set; }
  11: }
  12:  
  13: public class Header
  14: {
  15:     public string Title { get; set; }
  16:     public string Image { get; set; }
  17: }

Letter has header and several blocks. Now create template for this email using NVelocity:

   1: <html>
   2:   <head>
   3:   </head>
   4:   <body>
   5:     <table>
   6:       <tr>
   7:         <td><img src="$letter.Header.Image" /></td>
   8:         <td>$letter.Header.Title</td>
   9:       </tr>
  10:  
  11:       #foreach($block in $letter.Blocks)
  12:       #before
  13:       <tr>
  14:       #each
  15:         <td>$block.Title</td>
  16:         <td>$block.Content</td>
  17:       #after
  18:        </tr>
  19:       #end
  20:  
  21:     </table>
  22:   </body>
  23: </html>

Here you can find more detailed description of NVelocity improvements made in Castle project. As you can see template code is quite straightforward as we use C# like syntax. We used nested properties $letter.Header.Title and foreach macro for expanding list of blocks. In order to generate email based on this template you can use the following code:

   1: var velocity = new VelocityEngine();
   2: var props = new ExtendedProperties();
   3: string templateFolder = ...; // physical path to the folder with template files
   4: props.AddProperty("file.resource.loader.path", templateFolder);
   5:  
   6: velocity.Init(props);
   7:  
   8: var template = velocity.GetTemplate("template.vm");
   9: var context = new VelocityContext();
  10: context.Put("letter", letter);
  11:  
  12: using (var writer = new StringWriter())
  13: {
  14:     template.Merge(context, writer);
  15:     string result = writer.GetStringBuilder().ToString();
  16:     return result;
  17: }

If you use this code in Sharepoint you template files can be located in 12/template/layouts folder. But there is another problem if you install NVelocity.dll into GAC. Running this code the following exception will be thrown:

The specified class for Resourcemanager (NVelocity.Runtime.Resource.ResourceManagerImpl,NVelocity) does not exist.

There are lot of mentions of this problem in internet (e.g. here) but I didn’t find any working complete solution. As you can see NVelocity doesn’t use assembly strong name for type resolution. So if assembly is installed into GAC, this type can not be resolved. Fortunately there are several extension points in NVelocity which can be used to resolve this and several other similar exceptions. We need to add the following code into NVelocity initialization process:

   1: var velocity = new VelocityEngine();
   2: var props = new ExtendedProperties();
   3: string templateFolder = ...;
   4: props.AddProperty("file.resource.loader.path",
   5:     templateFolder);
   6: props.AddProperty("resource.manager.class",
   7:     typeof(ResourceManagerImpl).AssemblyQualifiedName);
   8: props.AddProperty("file.resource.loader.class",
   9:     typeof(FileResourceLoader).AssemblyQualifiedName);
  10: props.AddProperty("directive.manager",
  11:     typeof(DirectiveManager).AssemblyQualifiedName);
  12:  
  13: velocity.Init(props);

Unfortunately this is not enough as after you added this code you will get another exception:

Could not resolve type NVelocity.Runtime.Directive.Include,NVelocity
   at NVelocity.Runtime.Directive.DirectiveManager.Register(String directiveTypeName)
   at NVelocity.Runtime.RuntimeInstance.initializeDirectives()
   at NVelocity.Runtime.RuntimeInstance.Init()
   at NVelocity.Runtime.RuntimeInstance.Init(ExtendedProperties p)
   at NVelocity.App.VelocityEngine.Init(ExtendedProperties p)

In order to resolve this error we need to dig into NVelocity source code located currently at GitHub and change it. Lets see falling code of RuntimeInstance.initializeDirectives() method:

   1: private void initializeDirectives()
   2: {
   3:     initializeDirectiveManager();
   4:  
   5:     ExtendedProperties directiveProperties = new ExtendedProperties();
   6:  
   7:     try
   8:     {
   9:         directiveProperties.Load(
  10:             Assembly.GetExecutingAssembly().GetManifestResourceStream(
  11:                 RuntimeConstants.DEFAULT_RUNTIME_DIRECTIVES));
  12:     }
  13:     catch(System.Exception ex)
  14:     {
  15:         throw new System.Exception(
  16:             string.Format(
  17:                 "Error loading directive.properties!...\n{0}",
  18:                 ex.Message));
  19:     }
  20:  
  21:     IEnumerator directiveClasses = directiveProperties.Values.GetEnumerator();
  22:  
  23:     while(directiveClasses.MoveNext())
  24:     {
  25:         String directiveClass = (String) directiveClasses.Current;
  26:         directiveManager.Register(directiveClass);
  27:     }
  28:  
  29:     String[] userdirective = configuration.GetStringArray("userdirective");
  30:     for(int i = 0; i < userdirective.Length; i++)
  31:     {
  32:         directiveManager.Register(userdirective[i]);
  33:     }
  34: }

So it reads values from manifest resource stream (manifest resource name DEFAULT_RUNTIME_DIRECTIVES = NVelocity.Runtime.Defaults.directive.properties) which contains list of classes and creates instances of these classes. File with these classes is located in Runtime/Defaults/directive.properties file in NVelocity solution:

   1: directive.1=NVelocity.Runtime.Directive.Foreach\,NVelocity
   2: directive.2=NVelocity.Runtime.Directive.Include\,NVelocity
   3: directive.3=NVelocity.Runtime.Directive.Parse\,NVelocity
   4: directive.4=NVelocity.Runtime.Directive.Macro\,NVelocity
   5: directive.5=NVelocity.Runtime.Directive.Literal\,NVelocity

As you can see it doesn’t contains assembly strong name. In order to fix mentioned problem we need to modify this file by adding strong name:

   1: directive.1=NVelocity.Runtime.Directive.Foreach\,NVelocity,Version=1.1.0.0,Culture=neutral,PublicKeyToken=407dd0808d44fbdc
   2: directive.2=NVelocity.Runtime.Directive.Include\,NVelocity,Version=1.1.0.0,Culture=neutral,PublicKeyToken=407dd0808d44fbdc
   3: directive.3=NVelocity.Runtime.Directive.Parse\,NVelocity,Version=1.1.0.0,Culture=neutral,PublicKeyToken=407dd0808d44fbdc
   4: directive.4=NVelocity.Runtime.Directive.Macro\,NVelocity,Version=1.1.0.0,Culture=neutral,PublicKeyToken=407dd0808d44fbdc
   5: directive.5=NVelocity.Runtime.Directive.Literal\,NVelocity,Version=1.1.0.0,Culture=neutral,PublicKeyToken=407dd0808d44fbdc

We also need to modify RuntimeInstance.initializeDirectives() method in order to allow NVelocity to read assemblies strong names:

   1: private void initializeDirectives()
   2: {
   3:     initializeDirectiveManager();
   4:  
   5:     ExtendedProperties directiveProperties = new ExtendedProperties();
   6:  
   7:     try
   8:     {
   9:         directiveProperties.Load(
  10:             Assembly.GetExecutingAssembly().GetManifestResourceStream(
  11:                 RuntimeConstants.DEFAULT_RUNTIME_DIRECTIVES));
  12:     }
  13:     catch(System.Exception ex)
  14:     {
  15:         throw new System.Exception(
  16:             string.Format(
  17:                 "Error loading directive.properties!...\n{0}",
  18:                 ex.Message));
  19:     }
  20:  
  21:     IEnumerator directiveClasses = directiveProperties.Values.GetEnumerator();
  22:  
  23:     while(directiveClasses.MoveNext())
  24:     {
  25:         ArrayList tokens = (ArrayList)directiveClasses.Current;
  26:         string[] typeTokens =
  27:             tokens.Cast<object>().Select(o => (string)o).ToArray();
  28:         string directiveClass = string.Join(",", typeTokens);
  29:  
  30:         directiveManager.Register(directiveClass);
  31:     }
  32:  
  33:     String[] userdirective = configuration.GetStringArray("userdirective");
  34:     for(int i = 0; i < userdirective.Length; i++)
  35:     {
  36:         directiveManager.Register(userdirective[i]);
  37:     }
  38: }

Now recompile NVelocity and reinstall it in GAC. After this you will be able to use it from GAC.