SharePoint 2013 Razor View WebPart

Download SPRazorView

Razor syntax is a very easy, powerful and less verbose format. This is a proof of concept to utilize this powerful format to be hosted in a SharePoint WebPart!

The idea is very simple; to have a SharePoint WebPart that can host a razor view (.cshtml, .vbhtml) and render its output.

We can compile razor files (.cshtml, .vbhtml) during the runtime using System.Web.Compilation.BuildManager. This will require adding cshtml and vbhtml build providers to the application web.config file.

<add extension=".cshtml" type="System.Web.WebPages.Razor.RazorBuildProvider, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
<add extension=".vbhtml" type="System.Web.WebPages.Razor.RazorBuildProvider, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />

So, now all we need is to use the BuildManager to compile a razor file and render it in the normal rendering method of the SharePoint WebPart.

The razor file can be saved in SharePoint content database, and can be edited from SharePoint designer using Visual Studio or any text editor.

The render method of the WebPart will look like the following:

protected override void Render(HtmlTextWriter writer)
{
    // Check that there is a file specified
    if (string.IsNullOrWhiteSpace(RazorViewVirtualPath))
    {
        writer.Write(string.Format("Please specify a razor file to render in the WebPart settings. <a id="" href="\&quot;javascript:ShowToolPane2Wrapper('Edit',">Open tool pane</a>"));
        return;
    }
    Type razorViewType;
    try
    {
        // Compile the razor file into a type (This should be cached to improve the performance)
        razorViewType = BuildManager.GetCompiledType(RazorViewVirtualPath);
        if (null == razorViewType)
        {
            throw new Exception("Unable to compile the razor view.");
        }
    }
    catch (Exception e)
    {
        writer.Write(string.Format("Something went wrong while compiling the razor view at {0}: {1}", RazorViewVirtualPath, e.Message));
        return;
    }
    // Create an object of this type
    var page = Activator.CreateInstance(razorViewType) as WebPageBase;
    page.ExecutePageHierarchy(new WebPageContext(new HttpContextWrapper(this.Context), page, null), writer, page);

    base.Render(writer);
}

That’s all! for the concept. The compilation of Razor is working now but it gives a compilation error!

It was required to add System.Web.WebPages to the assemblies in the application web.config to overcome these compilation errors during the razor file compilation.

<add assembly="System.Web.WebPages, Version=3.0.0.0,Culture=neutral, PublicKeyToken=31bf3856ad364e35" />

But adding the System.Web.WebPages assembly automatically adds an HttpModule to the pipeline which in turn required me to implement a custom VirtualPathProvider to handle some special cases that was making the default SPVirtualPathProvider crashing!

So, I wrote this custom VirtualPathProvider to handle those special cases as follows:

internal sealed class SPRazorViewVirtualPathProvider : VirtualPathProvider
{
    public override string CombineVirtualPaths(string basePath, string relativePath)
    {
        return Previous.CombineVirtualPaths(basePath, relativePath);
    }
    public override string GetCacheKey(string virtualPath)
    {
        return Previous.GetCacheKey(virtualPath);
    }
    public override VirtualDirectory GetDirectory(string virtualDir)
    {
        return Previous.GetDirectory(virtualDir);
    }
    public override VirtualFile GetFile(string virtualPath)
    {
        return Previous.GetFile(virtualPath);
    }
    public override string GetFileHash(string virtualPath, System.Collections.IEnumerable virtualPathDependencies)
    {
        return Previous.GetFileHash(virtualPath, virtualPathDependencies);
    }
    public override bool FileExists(string virtualPath)
    {
        // This is a workaround for System.Web.WebPages module initialization checking for precompiledapp which will 
        // not work with SPVirtualPathProvider
        if (virtualPath.ToLower().EndsWith("precompiledapp.config"))
            return false;
        return Previous.FileExists(virtualPath);
    }
    public override System.Web.Caching.CacheDependency GetCacheDependency(string virtualPath, System.Collections.IEnumerable virtualPathDependencies, System.DateTime utcStart)
    {
        if (virtualPath.ToLower().StartsWith("~/_appstart."))
            virtualPath = virtualPath.TrimStart('~');
        return Previous.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
    }
    public override bool DirectoryExists(string virtualDir)
    {
        try
        {
            return Previous.DirectoryExists(virtualDir);
        }
        catch (Exception e)
        {
            return false;
        }
    }
}

I registered this SPRazorViewVirtualPathProvider to the HostingEnvironment via a custom HttpModule:

internal sealed class SPRazorViewHttpModule : IHttpModule
{
    private static bool razorViewVirtualPathProviderInitialized = false;
    private static object _lock = new object();
    public void Init(HttpApplication context)
    {
        if (razorViewVirtualPathProviderInitialized)
            return;
        lock (_lock)
        {
            var razorViewVirtualPathProvider = new SPRazorViewVirtualPathProvider();
            HostingEnvironment.RegisterVirtualPathProvider(razorViewVirtualPathProvider);
            razorViewVirtualPathProviderInitialized = true;
        }
    }
    public void Dispose()
    {
    }
}

To add the SPRazorViewHttpModule to the application pipeline, I used the PreApplicationStartMethod assembly attribute to register the module via DynamicModuleUtility.RegisterModule during the application starting.

I added the solution assembly to the web.config assemblies:

<add assembly="SPRazorView, Version=1.0.0.0, Culture=neutral, PublicKeyToken=64bd6e273698a7b0" />

I packaged the whole solution along with all the required web.config modification in a SharePoint Package (WSP) to make things easier.

Download SPRazorView

How to change the SharePoint web application physical IIS physical path

Recently, I needed to change the physical path of an already existing SPWebApplication.
This feature is not available from the Central Administration user interface. Of course the first option will be using SharePoint Management Shell.

Here are the steps to do that:

# Get the SPWebApplication using its url or id
$app = Get-SPWebApplication http://servername:port
# Display the currently set iis physical path
$app.IisSettings[[Microsoft.SharePoint.Administration.SPUrlZone]::Default].Path.ToString()
# Create a directory info object with the new required path
$newPathDirectoryInfo = New-Object -TypeName System.IO.DirectoryInfo -Argument List "F:\WebApplications\80"
# Assign the new directory info to the application iis settings
$app.IisSettings[[Microsoft.SharePoint.Administration.SPUrlZone]::Default].Path = $newPathDirectoryInfo
# Update the application
$app.Update()

Converting multiple SharePoint 2010 projects to SharePoint 2013

Converting/ migrating SharePoint 2010 projects to SharePoint 2013 involves changing TargetOfficeVersion to be 15.0, and changing TargetFrameworkVersion to be v4.0 or v4.5 in the Visual Studio csproj file.

When you have multiple SharePoint 2010 projects in your solution, it will not be an easy task!

The following PowerShell script is my trial to simplify converting multiple projects, hope it will help.
Just change the value of the $path variable to be your solution path (the folder containing your SharePoint projects).

# Path containing SharePoint 2010 projects 
$path = ""
cd $path
$files = get-childitem -recurse -filter *.csproj
foreach ($file in $files)
{
  "Filename: {0}" -f $($file.Name)
  $proj = [xml](Get-Content $file.FullName)
  $ns = new-object Xml.XmlNamespaceManager $proj.NameTable
  $ns.AddNamespace("dns", "http://schemas.microsoft.com/developer/msbuild/2003")
  $projectTypeGuids = $proj.SelectSingleNode("//dns:Project/dns:PropertyGroup/dns:ProjectTypeGuids", $ns)
  # Check to see if the project type is SharePoint
  if ($projectTypeGuids."#text" -like "*BB1F664B-9266-4fd6-B973-E1E44974B511*")
  {
    $targetOfficeVersion = $proj.SelectSingleNode("//dns:Project/dns:PropertyGroup/dns:TargetOfficeVersion", $ns)
    if($targetOfficeVersion -eq $null)
    {
      # Create TargetOfficeVersion element if not exist
      $targetOfficeVersion = $proj.CreateElement("TargetOfficeVersion")
      $targetOfficeVersion = $proj.SelectSingleNode("//dns:Project/dns:PropertyGroup", $ns).AppendChild($targetOfficeVersion)
    }
    # Change target office version
    $targetOfficeVersion.InnerText = "15.0"
    # Change target framework version
    $targetFrameworkVersion = $proj.SelectSingleNode("//dns:Project/dns:PropertyGroup/dns:TargetFrameworkVersion", $ns)
    $targetFrameworkVersion.InnerText = "v4.5"
    # Remove empty namespaces
    $proj = [xml] $proj.OuterXml.Replace(" xmlns=`"`"", "")
    $proj.Save($file.FullName)
  }
}

Ghost un-ghosted SharePoint 2010 files

Recently, I faced some performance problems in a big customized SharePoint solution. One of the reasons was that some of the page layouts and master pages are un-ghosted by SharePoint designer users, and some other page layouts were created directly in the SharePoint designer without having a feature to deploy them.

I had to fix that up by re-ghosting the customized pages and create a ghosted version for the files that have been created inside the SharePoint designer without a deployment feature.

Assessing the current situation I have the following cases:

Case 1 Some pages had been deployed using SharePoint features, but after that were edited in SharePoint designer. So, they are now customized (un-ghosted) SharePoint Designer displays the icon beside these files.

Case 2 Some pages had been deployed using SharePoint features, but after that they were replaced in SharePoint designer by uploading another file with the same name and overwriting the existing one. So, they are now customized (un-ghosted) SharePoint Designer doesn’t display anything to show that these file are customized.

Case 3 Some pages had been created directly by importing files to the SharePoint designer using import option, which means they have never been ghosted before. Again, SharePoint Designer doesn’t display anything to show that these file are customized. (Actually, they are not customized because they have never been ghosted before) but I need to make them ghosted.

So, I created a Windows application small tool, SPGhostFilesManager, to help doing the job.

First, I want to know what are the customized files (the three types above)
Second, I have to revert the un-ghosted files that already have ghosted files deployed, but after updating the ghosted files by the customized files content.
Third, I have to create ghosted files for the files created directly in the SharePoint designer and re-ghost those files to keep everything working as is while having all the files ghosted.

Here are the steps I followed to handle each of the above cases:

For Case 1:
For Case 2:

  • Using SPGhostFilesManager, Save each file content to the disk using the “Save” button.
  • Back to the SharePoint feature in Visual Studio update the content of the original deployable files.
  • Redeploy the feature containing the files to have the ghosted files updated.
  • Using SPGhostFilesManager, Click “Revert” button beside each file to revert it back to the ghosted file.
For Case 3: (These are the files with Status None on the SPGhostFilesManager view)
  • Using SPGhostFilesManager, Save each file content to the disk using the “Save” button.
  • Create a SharePoint feature (or use an existing one) to deploy these files as ghosted versions but you have to choose other names for the files to be able to link the old un-ghosted files to these new ghosted files.
  • Deploy the created feature to get your ghosted files in the SharePoint environment.
  • Using SPGhostFilesManager, Click “Ghost” button beside each file to ghost it to the corresponding one you deployed using the feature.

Paginating SharePoint List Items

Paginating SharePoint list items is not a straight forward task. You have to do some plumbing to make it work. especially, if you have sorting and filtering applied to the query you want to get your items with.

This is my trial to make the task easier:
SPPagedListItemsRetriever is a class that we need to instantiate by specifying the SPList we need to work with, and SPQuery we will execute.

SPPagedListItemsRetriever pagedItemsRetriever = new SPPagedListItemsRetriever(list, query);

Then, we have 2 public methods:

public SPListItem[] GetItems(int? startIndex, int? maxRowsCount); 
public int GetTotalItemsCount();

The drawback of the used technique in this implementation, is that it always get the items from the beginning! So, if you are asking to get the items starting from item 200 and with page size 10, It will get the first 199 items and throw them away, then begin getting the required items from 200 to 210.

Here is the complete post of the code:

/// <summary>
/// Retrieves paginated items of SPList with the specified SPQuery.
/// </summary>
public class SPPagedListItemsRetriever
{
    private SPQuery _query;
    private SPList _list;
    private const int MaxRowLimit = 2000;
    private static SPListItemCollectionPosition _emptySPListItemCollectionPosition = new SPListItemCollectionPosition(string.Empty);

    /// <summary>
    /// Constructs a new instance of SPPagedListItemsRetriever
    /// </summary>
    /// <param name="list" />The list to get the items from
    /// <param name="query" />The query by which the items should be retrieved
    public SPPagedListItemsRetriever(SPList list, SPQuery query)
    {
        _list = list;
        _query = query;
    }

    /// <summary>
    /// Get the items of the list with the specified query begining from a specified startIndex and with maxRowsCount (PageSize)
    /// </summary>
    /// <param name="startIndex" />
    /// <param name="maxRowsCount" />
    /// <returns></returns>
    public SPListItem[] GetItems(int? startIndex, int? maxRowsCount)
    {
        SPListItemCollectionPosition listItemCollectionPosition = null;
        uint actualStartIndex = startIndex.HasValue ? (uint)startIndex.Value : 0;
        //If we need items beginning from a specific index (greater that 0, the first one)
        //Create a dummy query to begin getting the items from the first one (0) till we reach the specified startIndex
        if (actualStartIndex > 0)
        {
            SPQuery dummyQuery = new SPQuery();
            //Change the ViewFields returned from this dummy query to minimal, actually we dont need these items so selelct the ID only to minimize the view fields
            dummyQuery.ViewFields = "<fieldref name='ID'>";
            dummyQuery.Query = _query.Query;

            if (null != _query.Folder)
                dummyQuery.Folder = _query.Folder;

            int gotDummyItems = 0;
            do
            {
                //Minimize the number of items not to exceed the recommended 2000 MaxRowLimit for SPQuery
                dummyQuery.RowLimit = Math.Min((uint)(actualStartIndex - gotDummyItems), MaxRowLimit);
                if (null == listItemCollectionPosition)
                    listItemCollectionPosition = _emptySPListItemCollectionPosition;

                dummyQuery.ListItemCollectionPosition = listItemCollectionPosition;
                SPListItemCollection items = _list.GetItems(dummyQuery); gotDummyItems += items.Count;
                listItemCollectionPosition = items.ListItemCollectionPosition;
            }
            while (gotDummyItems < actualStartIndex && listItemCollectionPosition != null);
        }

        //Now we will get the actual items we need
        SPQuery query = new SPQuery();
        query.Query = _query.Query;
        if (null != _query.Folder)
            query.Folder = _query.Folder;
        query.ViewFields = _query.ViewFields;
        List<splistitem> returnedItemsList = new List<splistitem>();
        uint actualMaxRowCount = maxRowsCount.HasValue ? (uint)maxRowsCount.Value : (uint)_list.ItemCount;

        do
        {
            //Minimize the number of items not to exceed the recommended 2000 MaxRowLimit for SPQuery
            query.RowLimit = Math.Min(actualMaxRowCount, MaxRowLimit);
            if (null == listItemCollectionPosition)
                listItemCollectionPosition = _emptySPListItemCollectionPosition;
            query.ListItemCollectionPosition = listItemCollectionPosition;
            SPListItemCollection listItems = _list.GetItems(query);
            returnedItemsList.AddRange(listItems.Cast<splistitem>().Select(i => i));
            listItemCollectionPosition = listItems.ListItemCollectionPosition;
        }
        while (returnedItemsList.Count < actualMaxRowCount && listItemCollectionPosition != null);

        return returnedItemsList.ToArray();
    }

    /// <summary>
    /// Gets the total items count using the specified query
    /// </summary>
    /// <returns></returns>
    public int GetTotalItemsCount()
    {
        SPQuery query = new SPQuery();
        //Change the ViewFields returned from this dummy query to minimal, actually we dont need these items so selelct the ID only to minimize the view fields
        query.ViewFields = "<fieldref name='ID'>";
        query.Query = _query.Query;
        SPFolder folder = _query.Folder;
        if (null != folder)
            query.Folder = folder;
        return _list.GetItems(query).Count;
    }
}

Grouping XML elements using XSLT version 1.0

Suppose you have an XML like:

<Rows>
  <Row Category="Sweet" Value="Chocolate"/>
  <Row Category="Salty" Value="Potato Chips"/>
  <Row Category="Salty" Value="Pop Corn"/>
  <Row Category="Sour" Value="Lemon"/>
  <Row Category="Sweet" Value="Biscuits"/>
  <Row Category="Salty" Value="Fries"/>
</Rows>

And you want to have these rows grouped by the ‘Category’ attribute like:

Salty
Fries
Pop Corn
Potato Chips

Sour
Lemon

Sweet
Biscuits
Chocolate

You can use Muenchian grouping just like the following:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl">
  <!-- First, define a key that uses our element or attribute we need to group by. In our case it's the Category attribute. -->
  <xsl:key name="food-by-Category" match="Row" use="@Category" />
  <xsl:template match="Rows">
  <!-- Now, we need to iterate on the Categories, This can be done by iterating on the first XML Row item for each Category. We will do that by create a node set for the current Row item (.) and the first item in the Key of the Current item  Category. Then we check the count to see is it 1 or 2.. If 1, So we've the same item in the node set; We have a new Category.-->
    <xsl:for-each select="Row[count(. | key('food-by-Category', @Category)[1]) = 1]">
      <!-- Sort by the Category -->
      <xsl:sort select="@Category" />
      <xsl:value-of select="@Category" />
      <hr />
      <!-- Now loop on the items of this Category, We get them from the Key we defined -->
      <xsl:for-each select="key('food-by-Category', @Category)">
        <!-- Sort by the item Value -->
        <xsl:sort select="@Value" />
        <xsl:value-of select="@Value" />
        <br/>      </xsl:for-each>
      <br/>
    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>

DotNetNuke Resources keys

In DotnetNuke Localization support, every resource key (in APP_LocalResources or APP_GlobalResources) should end with an extension.
resourceName.Text – Represents a string used for a common purpose
resourceName.Help – Used mostly in conjuncture with the DNN:Label control, which supplies a help icon () and tooltip.
resourceName.Tooltip – Used for a control’s Tooltip property.
resourceName.Action – Used to represent the Title property of a ModuleAction item shown in an actions menu.
resourceName.Confirm – Text that is displayed in a JavaScript’s confirmation dialog (such as a dialog to confirm deleting a module).
resourceName.Error – Text that is displayed as an error message. This might be for a label or, more commonly, for one of the ASP.NET validation controls.
resourceName.Detail – Text that gives more detail about an error message.

If we add a resource key in a resx file of the APP_LocalResources or APP_GlobalResources and didn’t give it an extension (like one of the above or any other one), we cannot get its value back by the DNN Localization logic of the:

DotNetNuke.Services.Localization.Localization.GetString(key, this.LocalResourceFile);

Or:

DotNetNuke.Services.Localization.Localization.GetString(key, <global resource file name>);

Further, if we call the Localization.GetString function with a key without an extension, it will assume that we are asking about the one with the extension “.Text”.
So the call of

Localization.GetString("SomeKey", LocalResourceFile)

is equivalent to:

Localization.GetString("SomeKey.Text", LocalResourceFile)

Localization support in DotNetNuke

In general, DNN disables the ASP.NET localization (the resources files) by disabling the build providers of the extensions (.resx and .resources) in the web.config.

<buildProviders>
  <remove extension=".resx"/>
  <remove extension=".resources"/>
</buildProviders>

So the calls of HttpContext.GetGlobalResourceObject() and HttpContext.GetLocalResourceObject() will always return null!

If we need to support localization in DNN solutions, we have to use the DNN localization class (DotNetNuke.Services.Localization.Localization) to do so.

DllImport 32bit dlls in .net assemblies throws BadImage exception on 64bit OS

When you compile your .Net executable with “AnyCPU” platform, it will select the “CLR” at runtime depending on the currently running platform.
Which means that your application will run as a 64-bit application on x64 machine and as 32-bit application on x86 machine. On x64 machines if you use any “DllImport” in your .Net executable to load a 32-bit native dll, while you compile your executable with “AnyCPU” platform, it will crash at runtime because no 64-bit application can run 32-bit dll.
So, if you want to load a 32-bit native dll with DllImport in a .Net executable you have to compile this .Net executable for “x86″ platform to make this executable run under the WOW64 as a 32-bit application this will make it capable to load native 32-bit dlls

Follow

Get every new post delivered to your Inbox.