Deploy SharePoint 2013 Display Template through Visual Studio

We create more and more display templates as soon as we utilize Content By Search Webpart or Search Result Webpart in SharePoint 2013. The recent project that I’ve done includes authoring (internal) site and publishing (external) site through Cross Site Publishing. Since the publishing site is customer facing / external site, we use display templates heavily and I think we have around 90 display templates.

In this article, I would like to show you how to deploy them through Visual Studio. Without further ado, I want to show you my very basic display template:

<html xmlns:mso="urn:schemas-microsoft-com:office:office" xmlns:msdt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882">
<head>
    <title>My Custom Item Template</title>

    <!--[if gte mso 9]><xml>
    <mso:CustomDocumentProperties>
    <mso:TemplateHidden msdt:dt="string">0</mso:TemplateHidden>
    <mso:ManagedPropertyMapping msdt:dt="string">'Link URL'{Link URL}:'Path','Line 1'{Line 1}:'Title', 'ListItemID':'ListItemID', 'owstaxIdCNPKeywords':'owstaxIdCNPKeywords'</mso:ManagedPropertyMapping>
    <mso:MasterPageDescription msdt:dt="string">This Item Display Template will show a small thumbnail icon next to a hyperlink of the item title, with an additional line that is available for a custom managed property.</mso:MasterPageDescription>
    <mso:ContentTypeId msdt:dt="string">0x0101002039C03B61C64EC4A04F5361F385106603</mso:ContentTypeId>
    <mso:TargetControlType msdt:dt="string">;#Content Web Parts;#</mso:TargetControlType>
    <mso:HtmlDesignAssociated msdt:dt="string">1</mso:HtmlDesignAssociated>
    <mso:CrawlerXSLFile msdt:dt="string">/_catalogs/masterpage/MyDisplayTemplates/Server_MyItem.xsl, /_catalogs/masterpage/MyDisplayTemplates/Server_MyItem.xsl</mso:CrawlerXSLFile>
    </mso:CustomDocumentProperties>
    </xml><![endif]-->
</head>

<body>
    <div>
        <!--#_
                var title = $getItemValue(ctx, "Line 1");
                var itemURL = $getItemValue(ctx, "Link URL");
        _#-->
        <a class="mydiv" data-item="_#= $htmlEncode(itemURL) =#_">_#= $htmlEncode(title) =#_</a>
    </div>
</body>
</html>

As you noticed above, I’ve specified CrawlerXSLFile property. Basically this is required for Search Engine Optimization, to allow search crawler to crawl content inside your Content By Search Webpart/ Search Result Webpart. You could read great post from Waldek here. This is same thing like we did when we would like to customize Content Query Webpart in SharePoint 2010.

<xsl:stylesheet version='1.0' xmlns:xsl='http://www.w3.org/1999/XSL/Transform' xmlns:ddwrt='http://schemas.microsoft.com/WebParts/v2/DataView/runtime'>
	<xsl:template match='/'>
		<xsl:apply-templates />
	</xsl:template>

	<xsl:template match='ResultTable'>
		<xsl:apply-templates select='Rows'/>
	</xsl:template>

	<xsl:template match='Rows'>
		<xsl:apply-templates select='Row' />
	</xsl:template>

	<xsl:template match='Row'>
		<xsl:for-each select='*'>
			<xsl:if test="name(.) = 'title'">
			   <xsl:value-of select="." />
			</xsl:if>
		</xsl:for-each>
	</xsl:template>
</xsl:stylesheet>

Now, it come to the deployment part in Visual Studio. Below is the list of steps:
1. Open Visual Studio and Create SharePoint Empty Project as Farm Solution
2. Add New Module, called it DisplayTemplates
3. Add your .html and .xsl display templates into DisplayTemplates Module
4. Edit Elements.xml in DisplayTemplates module

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <Module Name="DisplayTemplates" Url="_catalogs/masterPage">
    <File Path="DisplayTemplates\MyItem.html" Url="MyDisplayTemplates/MyItem.html" ReplaceContent="True" Type="GhostableInLibrary" Level="Published"/>
    <File Path="DisplayTemplates\Server_MyItem.xsl" Url="MyDisplayTemplates/Server_MyItem.xsl" Type="GhostableInLibrary" Level="Published" ReplaceContent="TRUE"  />
  </Module>
</Elements>

5. Ensure Display Template Module included in your Feature

 DisplayTemplate1

6. Add Feature Receiver

using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using Microsoft.SharePoint.Utilities;
using System;
using System.Runtime.InteropServices;
using System.Xml;

namespace DisplayTemplatesDemo.Features.Feature1 {
    /// <summary>
    /// This class handles events raised during feature activation, deactivation, installation, uninstallation, and upgrade.
    /// </summary>
    /// <remarks>
    /// The GUID attached to this class may be used during packaging and should not be modified.
    /// </remarks>

    [Guid("4d50c166-1095-4d7c-a4c1-61b35687cbf5")]
    public class Feature1EventReceiver : SPFeatureReceiver {
        /// <summary>
        /// Feature activated, we just change the item title of display template to force SharePoint generates the associated .js file
        /// </summary>
        /// <param name="properties"></param>
        public override void FeatureActivated(SPFeatureReceiverProperties properties) 
        {
            try {
                var site = (SPSite)properties.Feature.Parent;
                var web = site.RootWeb;

                //get all display templates
                string allDisplayTemplates = getAllDisplayTemplates(site.RootWeb, properties.Definition);
                //get the master pages gallery
                var gallery = web.GetCatalog(SPListTemplateType.MasterPageCatalog);

                foreach (var displayTemplate in allDisplayTemplates.Split(new char[] {','}))
                {
                    var url = SPUtility.ConcatUrls(gallery.RootFolder.Url, "CNPGallery/Display Templates");
                    url = SPUtility.ConcatUrls(url, displayTemplate);
                    //get the file
                    var fileOrFolder = web.GetFileOrFolderObject(url);

                    if (fileOrFolder != null && fileOrFolder is SPFile) {
                        var file = (SPFile)fileOrFolder;
                        if (file.Exists) {
                            //determine if the gallery requires files to be checked out before editing
                            //if so, check this one out
                            if (gallery.ForceCheckout)
                                file.CheckOut();

                            //make a simple change
                            file.Item["Title"] = (file.Item.Title != null ? file.Item.Title : displayTemplate);
                            file.Item.Update();

                            //if check out required, check it in
                            if (gallery.ForceCheckout)
                                file.CheckIn(string.Empty);
                            //if the gallery has minor versioning enabled, publish a major
                            if (gallery.EnableMinorVersions)
                                file.Publish(string.Empty);
                            //if the gallery required approval, approve it
                            if (gallery.EnableModeration)
                                file.Approve(string.Empty);
                        }
                    }
                }
            }
            catch (Exception ex) {
                //deactivate the feature
                var site = (SPSite)properties.Feature.Parent;
                site.Features.Remove(properties.Feature.DefinitionId);

                SPUtility.TransferToErrorPage(ex.Message);
            }
        }

        private string getAllDisplayTemplates(SPWeb web, SPFeatureDefinition definition)
        {
            string result = string.Empty;
            try
            {
                string wpcatalogUrl = SPUrlUtility.CombineUrl(web.Url, "_catalogs/masterpage");

                // Get the Feature.xml for the feature
                XmlDocument featureXml = new XmlDocument();
                featureXml.LoadXml(definition.GetXmlDefinition(web.Locale).OuterXml);

                XmlNamespaceManager nsMgr = new XmlNamespaceManager(featureXml.NameTable);
                nsMgr.AddNamespace("sp", "http://schemas.microsoft.com/sharepoint/");

                // Get Location attribute of each ElementManifest inside ElementManifests inside the Feature
                foreach (XmlNode locationNode in featureXml.SelectNodes("/sp:Feature/sp:ElementManifests/sp:ElementFile/@Location", nsMgr))
                {
                    if (result != string.Empty)
                        result += ",";
                    result += locationNode.Value.Replace("Templates\\", string.Empty);
                }
            }
            catch(Exception ex)
            {
                result = string.Empty;
            }
            return result;
        }
    }
}

7. Finally the solution look like below

 DisplayTemplate2

8. Build and Deploy the solution

Query Managed Metadata Column in SharePoint List / Library

Query

Following my previous article to Populate Managed Metadata Column, In this article I would like to show you on How to Query Managed Metadata column in SharePoint List / Library.

If you look at the msdn article, the query would look like below:

<Where>
 <In>
  <FieldRef LookupId='TRUE' Name='FieldName' />
  <Values>
   <Value Type='Integer'>14</Value>
   <Value Type='Integer'>15</Value>
  </Values>
 </In>
</Where>

 

The question is where do we get that Values in the query ?

To get those values, I’ve created a SharePoint console application.

static void Main(string[] args)
{
   using(SPSite site = new SPSite("http://yoursiteurl"))
   {
      using (SPWeb web = site.OpenWeb())
      {
         SPList list = web.Lists.TryGetList("MyList");
         TaxonomyField audienceType = (TaxonomyField)site.RootWeb.Fields.GetField("MyManagedColumn");
         TaxonomySession taxonomySession = new TaxonomySession(site);
         TermStore termStore = taxonomySession.TermStores[audienceType.SspId];
         TermSet termSet = termStore.GetTermSet(audienceType.TermSetId);

         TermCollection termColl = termSet.GetAllTerms();
         foreach (Term eachTerm in termColl)
         {
            int[] wssIds = TaxonomyField.GetWssIdsOfTerm(site, termStore.Id, termSet.Id, eachTerm.Id, false, 500);
            if(wssIds.Length > 0)
               Console.WriteLine(string.Format("Term: {0}, WssId: {1}", eachTerm.Name.Trim(), wssIds[0].ToString()));
         }
      }
   }
   Console.ReadLine();
}

 
Those Wss Id are that you need to put as the Value.
 
References

 
 
 
 
 

Configure Information Management Policy programmatically

Following my previous article to set Expiry column using Reusable Workflow, I would also like to Configure Information Management Policy at Content type programmatically on Feature Activated.

What I could do first is to manually configure Information Management Policy at Content type to look like below.

RetentionPolicy1

 

Then, I’ve created simple SharePoint console application to retrieve the Information Management Policy custom data. It looks like below:

using System;
using System.Linq;
using System.Text;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Workflow;
using Microsoft.Office.RecordsManagement.InformationPolicy;

namespace GetRetentionPolicyCustomData
{
    class Program
    {
        static void Main(string[] args)
        {
            using (SPWeb web = new SPSite("http://yourSiteURL").OpenWeb())
            {
                // Get Policy Custom Data
                string contentTypeName = "YourContentTypeName";
                SPContentType contentType = web.ContentTypes.Cast<SPContentType>().Where(cty => cty.Name.Trim().ToLower() == contentTypeName.ToLower()).FirstOrDefault();

                if (contentType != null)
                {
                    Policy policy = Policy.GetPolicy(contentType);
                    if (policy != null)
                    {
                        foreach (PolicyItem eachPolicy in policy.Items)
                        {
                            string customdata = eachPolicy.CustomData;
                            Console.WriteLine("Policy " + eachPolicy.Name + ": " + customdata);
                        }
                    }
                }
            }
        }
    }
}

 

And you will get something like below in Information Management Policy Custom Data variable.

 

<Schedules nextStageId="3">
 <Schedule type="Default">
  <stages>
   <data stageId="1">
    <formula id="Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration.Formula.BuiltIn">
     <number>0</number>
     <property>NearExpiry</property>
     <propertyId>15686f6f-8d25-4eba-be79-7792663f4675</propertyId>
     <period>days</period>
    </formula>
    <action type="workflow" id="f5c7ade6-1a7e-4231-bce8-81041b1b7fcd" />
   </data>
   <data stageId="2">
    <formula id="Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration.Formula.BuiltIn">
     <number>7</number>
     <property>Expiration</property>
     <propertyId>d2681865-a6d3-4d5d-850b-cd2a1be22b31</propertyId>
     <period>years</period>
    </formula>
    <action type="action" id="Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration.Action.MoveToRecycleBin" />
   </data>
  </stages>
 </Schedule>
</Schedules>

 

Now, Delete the Information Management Policy that you’ve configured manually on the Content Type and let implements it programmatically.
In Site Collection scope feature event receiver:

 

using Microsoft.Office.RecordsManagement.InformationPolicy;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Workflow;
using System;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;

namespace InformationManagementPolicy
{
    /// <summary>
    /// This class handles events raised during feature activation, deactivation, installation, uninstallation, and upgrade.
    /// </summary>
    /// <remarks>
    /// The GUID attached to this class may be used during packaging and should not be modified.
    /// </remarks>

    [Guid("92cf61e2-0ead-4a9d-b22d-4ff8969e1d05")]
    public class RetentionPolicyEventReceiver : SPFeatureReceiver
    {
        private const string LIST_WORKFLOWTASKS = "Workflow Tasks";
        private const string LIST_WORKFLOWHISTORY = "Workflow History";
        private const string COLUMN_EXPIRATION = "Expiration";
        private const string COLUMN_NEAREXPIRY = "NearExpiry";
        private const string WORKFLOW_SENDNOTIFICATION = "Send Expiry Notification Workflow";

        /// <summary>
        /// Event handler when feature activated
        /// </summary>
        /// <param name="properties"></param>
        public override void FeatureActivated(SPFeatureReceiverProperties properties)
        {
            try
            {
                SPSecurity.RunWithElevatedPrivileges(delegate
                {
                    using (SPWeb web = new SPSite(((SPSite)properties.Feature.Parent).ID).OpenWeb())
                    {
                        string contentTypeName = "YourContentTypeName";
                        SPContentType contentType = web.ContentTypes.Cast<SPContentType>().Where(cty => cty.Name.Trim().ToLower() == contentTypeName.Trim().ToLower()).FirstOrDefault();
                        if (contentType != null)
                        {
                            // Attach Retention Policies to Content Type
                            CreateRetentionPolicyInContentType(web, contentType, COLUMN_EXPIRATION, COLUMN_NEAREXPIRY);
                        }
                    }
                });
            }
            catch (Exception ex)
            {
                // exception "Issue found when CNP.CWE.CDA.Workflows feature activated."
            }
        }

        /// <summary>
        /// Event handler when feature deactivating
        /// </summary>
        /// <param name="properties"></param>
        public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
        {
            try
            {
                SPSecurity.RunWithElevatedPrivileges(delegate
                {
                    using (SPWeb web = new SPSite(((SPSite)properties.Feature.Parent).ID).OpenWeb())
                    {
                        string contentTypeName = "YourContentTypeName";
                        SPContentType contentType = web.ContentTypes.Cast<SPContentType>().Where(cty => cty.Name.Trim().ToLower() == contentTypeName.Trim().ToLower()).FirstOrDefault();
                        if (contentType != null)
                        {
                            // Delete Retention Policies to Content Type
                            if (Policy.GetPolicy(contentType) != null)
                                Policy.DeletePolicy(contentType);
                        }
                    }
                });
            }
            catch (Exception ex)
            {
                // exception "Issue found when CNP.CWE.CDA.Workflows feature deactivating."
            }
        }

        // Uncomment the method below to handle the event raised after a feature has been installed.
        //public override void FeatureInstalled(SPFeatureReceiverProperties properties)
        //{
        //}

        // Uncomment the method below to handle the event raised before a feature is uninstalled.
        //public override void FeatureUninstalling(SPFeatureReceiverProperties properties)
        //{
        //}

        // Uncomment the method below to handle the event raised when a feature is upgrading.
        //public override void FeatureUpgrading(SPFeatureReceiverProperties properties, string upgradeActionName, System.Collections.Generic.IDictionary<string, string> parameters)
        //{
        //}

        #region Private methods

        /// <summary>
        /// Method to attach retention policy to specified content type
        /// </summary>
        /// <param name="web">current web</param>
        /// <param name="contentType">content type</param>
        /// <param name="expirationFieldName">expiration field name</param>
        /// <param name="nearExpiryFieldName">near expiry field name</param>
        private void CreateRetentionPolicyInContentType(SPWeb web, SPContentType contentType, string expirationFieldName, string nearExpiryFieldName)
        {
            try
            {
                SPField expiration = contentType.Fields.Cast<SPField>().Where(fld => fld.InternalName == expirationFieldName).FirstOrDefault();
                SPField nearExpiry = contentType.Fields.Cast<SPField>().Where(fld => fld.InternalName == nearExpiryFieldName).FirstOrDefault();
                SPWorkflowAssociation workflowAssoc = contentType.WorkflowAssociations.Cast<SPWorkflowAssociation>().Where(assoc => assoc.Name == WORKFLOW_SENDNOTIFICATION).FirstOrDefault();

                if (expiration != null && nearExpiry != null && workflowAssoc != null)
                {
                    if (Policy.GetPolicy(contentType) == null)
                        Policy.CreatePolicy(contentType, null);
                    Policy policy = Policy.GetPolicy(contentType);

                    string policyCustomData = GeneratePolicyCustomData(expiration, nearExpiry, workflowAssoc.ParentAssociationId.ToString());
                    policy.Items.Add("Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration", policyCustomData);
                    contentType.Update();
                }
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        /// <summary>
        /// Method to generate Policy Custom Data
        /// </summary>
        /// <param name="expiration">expiration column</param>
        /// <param name="nearExpiry">near expiry column</param>
        /// <param name="parentAssociationId">send notification parent workflow association</param>
        /// <returns></returns>
        private string GeneratePolicyCustomData(SPField expiration, SPField nearExpiry, string parentAssociationId)
        {
            StringBuilder sb = new StringBuilder();
            try
            {
                sb.AppendLine("<Schedules nextStageId='3'>");
                sb.AppendLine("<Schedule type='Default'>");
                sb.AppendLine("<stages>");

                // Send Expiry Notification when Today = Near Expiry + 0 days
                sb.AppendLine("<data stageId='1'>");
                sb.AppendLine("<formula id='Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration.Formula.BuiltIn'>");
                sb.AppendLine("<number>0</number>");
                sb.AppendFormat("<property>{0}</property>", nearExpiry.InternalName);
                sb.AppendFormat("<propertyId>{0}</propertyId>", nearExpiry.Id.ToString());
                sb.AppendLine("<period>days</period>");
                sb.AppendLine("</formula>");
                sb.AppendFormat("<action type='workflow' id='{0}' />", parentAssociationId);
                sb.AppendLine("</data>");

                // Delete the item after Expiration + 7 years
                sb.AppendLine("<data stageId='2'>");
                sb.AppendLine("<formula id='Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration.Formula.BuiltIn'>");
                sb.AppendLine("<number>7</number>");
                sb.AppendFormat("<property>{0}</property>", expiration.InternalName);
                sb.AppendFormat("<propertyId>{0}</propertyId>", expiration.Id.ToString());
                sb.AppendLine("<period>years</period>");
                sb.AppendLine("</formula>");
                sb.AppendLine("<action type='action' id='Microsoft.Office.RecordsManagement.PolicyFeatures.Expiration.Action.MoveToRecycleBin' />");
                sb.AppendLine("</data>");

                sb.AppendLine("</stages>");
                sb.AppendLine("</Schedule>");
                sb.AppendLine("</Schedules>");
            }
            catch (Exception ex)
            {
                throw ex;
            }
            return sb.ToString();
        }
        #endregion
    }
}

Import a SharePoint Designer Reusable Workflow into Visual Studio

Everyone googling for Import SharePoint Designer Reusable Workflow into Visual Studio has definitely covered this msdn article. I also have followed the article thoroughly for one my project task this week and found there is something missing at the last step of the article. Do you know what it is? Let me tell you the answer later on =)

So, let’s go start the implementation …

PreRequisites

1. Site Column “Expiration” as Date Time field
2. Site Content Type “MyContentType” as Custom List content type
ReusableWorkflow2
3. Custom List “MyList” based on “MyContentType”
ReusableWorkflow1

 

Step 1: Create Reusable Workflow in SharePoint Designer

I don’t want to go to much detail about this step, cause I found an excellent technet video from Asif Rehmani about creating reusable workflow in SharePoint Designer. Follow that video and you should be good. My reusable workflow looks like below.
ReusableWorkflow3
And after I publish it, I could see the workflow is available on MyContentType. Not in This List, Item or Folder content type.
ReusableWorkflow4

 

Step 2: Save Reusable Workflow as Template

 

In this step, we’re going to Save it As Template and it would be available as WSP file in Site Asset Library. Download the WSP file to your local folder. Note: Do NOT Publish Globally, since you WILL NOT be able to Save As Template afterwards.
ReusableWorkflow5
ReusableWorkflow6

 

Step 3: Import the WSP file into Visual Studio

1. In Visual Studio, on the menu bar, choose File > New > Project.

2. In the New Project dialog box, expand the SharePoint node under either Visual C# and then choose the 2010 node.

3. In the Templates pane, choose the Import Reusable SharePoint 2010 Workflow template, leave the name of the project as MyWorkflowImportProject, and then choose the OK button. The SharePoint Customization Wizard appears.

4. On the Specify the site and security level for debugging page, enter the URL for the second SharePoint site: http://<site url>/.

5. In the What is the trust level for this SharePoint solution? section, choose the Deploy as a farm solution option button, and then choose the Next button.

6. In the Specify the new project source page, browse to the location on the system where you previously saved the .wsp file, open the file, and then choose the Next button. Then, Choose the Finish button to import all available items in the .wsp file. This displays a list of reusable workflows available for importing.

7. In the Select items to import box, choose the SPD Task Workflow workflow, and then choose the Finish button.

After the import operation is finished, a project named MyWorkflowImportProject is created containing a workflow named MyReusableWorkflowFT. In this folder is the workflow’s definition file Elements.xml and the workflow designer file (.xoml). The designer contains two files: the rules file (.rules) and the code-behind .cs file.

8. In Solution Explorer, Delete the Other Imported Files folder.

9. In the Elements.xml file, Delete InstantiationURL=”_layouts/IniErkflIP.sspx” and Update Name=”My Reusable Workflow”.

10. Ensure MyReusableWorkflowFT module included in your Feature, Update Feature Title and Scope to Site.

11. Build and Deploy the solution. Do NOT forget to delete your SharePoint Designer Publishing Workflow that you created on step 1 so you dont get mix up.

 

Step 4: Associate the Workflow

So, I go to MyList Settings > Workflow Settings > Add a Workflow to associate My Reusable Workflow to the list. Then, I found the issue …

ReusableWorkflow7

 

The Problem and Missing Piece

I do NOT want this and I want My Reusable Workflow to be available on MyContentType only. After googling for sometime, I still can not find any clues … So, I have a look and follow on the msdn article again. Then, I started to see there is AssociationCategories element in Elements.xml under  MyReusableWorkflowFT. This is the Ahha moment …

Based on msdn article, the missing piece is to specify the AssociationCategories value whether it is General, List or ContentType. I found Mano Mangaldas blog that provide more example on AssociationCategories value variations.

My updated Elements.xml would look like below:

ReusableWorkflow8

 

 

Resources

 

 

Create Custom Http Handler in SharePoint

I would like to share my experience in Creating Custom Http Handler in SharePoint. There are couple of ways to create Http Handler project, but I prefer to use Ashx Handler template from CKSDev Development Tools for SharePoint. It is an easier way to start your Http Handler project and to deploy the solution.

 

CKSDev installation issue

You have to download and install the tool based on your Visual Studio version, so make sure you’re downloading the right version. The other thing that I would like to mention is when I add new item “Ashx handler”, there is a pop up error message look like:

Could not load file or assembly ‘file:///C:\USERS\XXX\APPDATA\LOCAL\MICROSOFT\VISUALSTUDIO\12.0\EXTENSIONS\WCFZLEFI.TYX\CKS.Dev12.Cmd.Imp.v4.dll’
or one of its dependencies.
This assembly is built by a runtime newer than the currently loaded runtime and cannot be loaded.

Solution to this is to copy all CKS dll into Global Assembly Cache using gacutil command in Visual Studio command prompt. If you don’t face the issue, then probably they have already fixed it.

 

Pre-Requisite

  • CKSDev Tools for SharePoint installed
  • List “MyList” exists in Root Web

 

Implementation 

So without further ado, lets start create one.

1. Create SharePoint Empty Project, called it CustomHttpHandler and Click OK.
CustomHttpHandler1
2. Enter your Site URL and Deploy it as farm solution and Click Finish.
3. Right Click CustomHttpHandler project > Add > New Item, then Select SharePoint 2013 – Ashx handler (CKSDev) template, called it MyAshxHandler and Click Add.
CustomHttpHandler2
4. Now, the solution explorer will look like below and Set the Build Action of MyAshxHandler.ashx to Content.
CustomHttpHandler4
5. Include MyAshxHandler module in the CustomHttpHandler Feature.
CustomHttpHandler4
6. Update MyAshxHandler.ashx.cs code to below:
using System;
using System.Web;
using System.Runtime.InteropServices;
using Microsoft.SharePoint;
using System.Linq;

namespace CustomHttpHandler
{
    [Guid("6aa41c89-943a-4242-824b-85c2f433ede3")]
    public partial class MyAshxHandler : IHttpHandler
    {
        private const string ParameterName = "Title";
        private const string ColumnName = "Title";
        #region IHttpHandler Members

        /// <summary>
        /// Gets a value indicating whether another request can use the <see cref="T:System.Web.IHttpHandler"/> instance.
        /// </summary>
        /// <value></value>
        /// <returns>true if the <see cref="T:System.Web.IHttpHandler"/> instance is reusable; otherwise, false.
        /// </returns>
        public bool IsReusable
        {
            get { return false; }
        }

        /// <summary>
        /// Enables processing of HTTP Web requests by a custom HttpHandler that implements the <see cref="T:System.Web.IHttpHandler"/> interface.
        /// </summary>
        /// <param name="context">An <see cref="T:System.Web.HttpContext"/> object that provides references to the intrinsic server objects (for example, Request, Response, Session, and Server) used to service HTTP requests.</param>
        public void ProcessRequest(HttpContext context)
        {
            try
            {
                context.Response.Clear();

                // Get Title parameter
                string title = GetParameters(ParameterName);
                if(title != string.Empty)
                {
                    Guid siteId = SPContext.Current.Site.ID;

                    // Elevate permission for Anonymous
                    SPSecurity.RunWithElevatedPrivileges(delegate
                    {
                        using (SPWeb web = new SPSite(siteId).OpenWeb())
                        {
                            // Get List
                            SPList myList = web.Lists.Cast<SPList>().Where(lst => lst.Title == "MyList").FirstOrDefault();
                            if (myList == null)
                                throw new Exception("Bad Request, My list is NOT found ...");

                            // Get List item
                            SPListItem myItem = myList.Items.Cast<SPListItem>().Where(strslt => strslt[ColumnName].ToString() == title).FirstOrDefault();

                            // Add / Update Item
                            web.AllowUnsafeUpdates = true;

                            // If new item
                            if (myItem == null)
                                myItem = myList.Items.Add();

                            myItem[ColumnName] = title;
                            myItem.Update();

                            web.AllowUnsafeUpdates = false;

                            // Set the response
                            context.Response.StatusCode = 200;
                            context.Response.StatusDescription = "Successful";
                            context.Response.ContentType = "text/plain";
                            context.Response.Write(String.Format("Item with GUID:{0} and Title:{1} has been created / updated", myItem.ID.ToString(), myItem[ColumnName].ToString()));
                        }
                    });
                }
            }
            catch(Exception ex)
            {
                context.Response.Clear();
                context.Response.StatusCode = 400;
                context.Response.StatusDescription = ex.Message;
            }
            context.Response.End();
        }
        #endregion

        #region private methods
        private string GetParameters(string queryParam)
        {
            try
            {
                return HttpContext.Current.Request.QueryString[queryParam];
            }
            catch (Exception)
            {
                return string.Empty;
            }
        }
        #endregion
    }
}
7. Build and Deploy the solution.

 

Results

CustomHttpHandler5

CustomHttpHandler6

 

References

Enable SharePoint 2013 Search REST Queries for Anonymous

Recently in my project, I have to enable / call SharePoint REST Search Queries for Anonymous users. The reason behind that is because the site is public external site and I would like the search box to return search results as suggestions for Anonymous users. There is msdn article to do that step by step, but in this article I would like to share my own experience and knowledge on how did I enable SharePoint 2013 Search REST Queries for Anonymous users.

Pre-Requisites

1. Web Application and Site Collection created with Anonymous Access configured
2. Search Service Application working, Full Crawled completed successfully and Search Setting configured
3. Search Input Box ID must contains “_csr_sbox”

Solution

Without further ado, please find below is the list of steps:

1. Run EnableAnonymousRESTSearch.ps1 and Enter Root Web URL. This PowerShell script will create QueryPropertiesTemplate library in root web and upload QueryParameterTemplate.xml into the library to override QueryProperties element for Anonymous users. It also deactivate ViewFormPagesLockDown Site Collection Feature when activated.

Param(
[string] $url = $(Read-Host -prompt "Enter Site Collection URL: ")
)

# Include SharePoint reference
if ( (Get-PSSnapin -Name Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue) -eq $null )
{
    Add-PsSnapin Microsoft.SharePoint.PowerShell
}

# Get All variables required
$farmID=(Get-SPFarm).Id
$spSite=Get-SPSite $url
$siteColId=$spSite.Id
$topLevelSiteID=$spSite.RootWeb.Id
$rootWeb=$spSite.RootWeb
$featureLockDownFolderName = "ViewFormPagesLockDown"

# Deactivate ViewFormPagesLockDown Site Feature
$featureLockDown = Get-SPFeature -Site $url | Where {$_.DisplayName -eq $featureLockDownFolderName}
if($featureLockDown -ne $null)
{
   Disable-SPFeature $featureLockDown -url $url
   Write-Host "ViewFormPagesLockDown Deactivated ... "
}

# Create QueryPropertiesTemplate Library and Upload QueryParameterTemplate.xml into the library
if($null -ne $rootWeb)
{
	$list=$rootWeb.Lists["QueryPropertiesTemplate"]
		if($null -eq $list)
		{
			$template=$rootWeb.ListTemplates["Document Library"]
			$list=$rootWeb.Lists[$rootWeb.Lists.Add("QueryPropertiesTemplate","QueryPropertiesTemplate",$template)]
		}

	$scriptFile = $MyInvocation.MyCommand.Definition
	[string]$currentSource = Get-Content $scriptFile
	[int]$startScript=$currentSource.LastIndexOf("<?xml ");
	[int]$closeComment=$currentSource.LastIndexOf("#>");
	$xmlFile=[xml]($currentSource.Substring($startScript,$closeComment-$startScript))
	$xmlFile.QueryPropertiesTemplate.QueryProperties.FarmId=$farmID.ToString()
	$xmlFile.QueryPropertiesTemplate.QueryProperties.SiteId=$siteColId.ToString()
	$xmlFile.QueryPropertiesTemplate.QueryProperties.WebId=$topLevelSiteID.ToString()

	$xmlFile.OuterXml | Out-File queryparametertemplate.xml -Encoding UTF8
	$tempFile=Get-Item -LiteralPath "queryparametertemplate.xml"

	$folder = $list.RootFolder
	$stream=$tempFile.OpenRead()
	$file = $folder.Files.Add($folder.Url+"/queryparametertemplate.xml",$stream,$true, "created by script",$false)
	$stream.Close()

	if($null -ne $file)
	{
		Write-Host ("File Created At " + $rootWeb.Url + "/" + $file.Url)
	}
	Write-Host " "
}

<#
<?xml version="1.0" encoding="utf-8"?>
<QueryPropertiesTemplate xmlns="http://www.microsoft.com/sharepoint/search/KnownTypes/2008/08" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
  <QueryProperties i:type="KeywordQueryProperties">
    <EnableStemming>true</EnableStemming>
    <FarmId>22222222-fa49-4987-b1ea-9fad99e81a0f</FarmId>
    <SiteId>11111111-68f0-41c5-a525-7e7fda7666b3</SiteId>
    <WebId>00000000-8d97-40a2-b07b-cf05e4c85084</WebId>
    <IgnoreAllNoiseQuery>true</IgnoreAllNoiseQuery>
    <KeywordInclusion>AllKeywords</KeywordInclusion>
    <SummaryLength>180</SummaryLength>
    <TrimDuplicates>true</TrimDuplicates>
    <WcfTimeout>120000</WcfTimeout>
    <Properties xmlns:a="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
      <a:KeyValueOfstringanyType>
        <a:Key>_IsEntSearchLicensed</a:Key>
        <a:Value i:type="b:boolean" xmlns:b="http://www.w3.org/2001/XMLSchema">true</a:Value>
      </a:KeyValueOfstringanyType>
      <a:KeyValueOfstringanyType>
        <a:Key>EnableSorting</a:Key>
        <a:Value i:type="b:boolean" xmlns:b="http://www.w3.org/2001/XMLSchema">true</a:Value>
      </a:KeyValueOfstringanyType>
      <a:KeyValueOfstringanyType>
        <a:Key>MaxKeywordQueryTextLength</a:Key>
        <a:Value i:type="b:int" xmlns:b="http://www.w3.org/2001/XMLSchema">4096</a:Value>
      </a:KeyValueOfstringanyType>
      <a:KeyValueOfstringanyType>
        <a:Key>TryCache</a:Key>
        <a:Value i:type="b:boolean" xmlns:b="http://www.w3.org/2001/XMLSchema">true</a:Value>
      </a:KeyValueOfstringanyType>
    </Properties>
    <PropertiesContractVersion>15.0.0.0</PropertiesContractVersion>
    <EnableFQL>false</EnableFQL>
    <EnableSpellcheck>Suggest</EnableSpellcheck>
    <EnableUrlSmashing>true</EnableUrlSmashing>
    <IsCachable>false</IsCachable>
    <MaxShallowRefinementHits>100</MaxShallowRefinementHits>
    <MaxSummaryLength>185</MaxSummaryLength>
    <MaxUrlLength>2048</MaxUrlLength>
    <SimilarType>None</SimilarType>
    <SortSimilar>true</SortSimilar>
    <TrimDuplicatesIncludeId>0</TrimDuplicatesIncludeId>
    <TrimDuplicatesKeepCount>1</TrimDuplicatesKeepCount>
  </QueryProperties>
  <WhiteList xmlns:a="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
    <a:string>RowLimit</a:string>
    <a:string>SortList</a:string>
    <a:string>StartRow</a:string>
    <a:string>RefinementFilters</a:string>
    <a:string>Culture</a:string>
    <a:string>RankingModelId</a:string>
    <a:string>TrimDuplicatesIncludeId</a:string>
    <a:string>ReorderingRules</a:string>
    <a:string>EnableQueryRules</a:string>
    <a:string>HiddenConstraints</a:string>
    <a:string>QueryText</a:string>
    <a:string>QueryTemplate</a:string>
    <a:string>SelectProperties</a:string>
    <a:string>SourceID</a:string>
  </WhiteList>
</QueryPropertiesTemplate>
#>

2. Create QuerySuggestion.js java script file. The java script will utilize AutoComplete interaction from jquery-ui and call Search Query REST to populate search results in the AutoComplete drop down.

$(function () {
    // If Anonymous Users
    if (_spPageContextInfo.systemUserKey === undefined) {
        var siteurl = _spPageContextInfo.webAbsoluteUrl;

        // Get all search input boxes
        $("input[id*='_csr_sbox']").each(function () {
            var searchinput = $(this);
            searchinput.data("old_value", searchinput.val());

            // Bind Cut, Paste and KeyDown Event Listener to current search input box
            searchinput.bind("paste cut keydown", function (e) {
                var that = this;
                setTimeout(function () {
                    // If search input box not empty
                    if ($(that).val()) {
                        $(that).data("old_value", $(that).val());

                        // Query Suggestion using AutoComplete
                        searchinput.autocomplete({
                            minLength: 0,
                            source: function (request, response) {
                                // Call Search REST API and get search result for query suggestion
                                $.ajax({
                                    url: siteurl + "/_api/search/query?querytext='" + $(that).val() + "*'&QueryTemplatePropertiesUrl='spfile://webroot/queryparametertemplate.xml'",
                                    method: "GET",
                                    headers: { "Accept": "application/json; odata=verbose" },
                                    success: function (data) {
                                        response($.map(data.d.query.PrimaryQueryResult.RelevantResults.Table.Rows.results, function (item) {
                                            return {
                                                fields: getFields(item.Cells.results)
                                            }
                                        }));
                                    },
                                    error: function (data) {
                                        alert("Query Suggestion Error: Fail to call Search REST API");
                                    }
                                });
                            },
                            focus: function (event, ui) { // If Query Suggestion get focused
                                searchinput.val(ui.item.fields.Title);
                                return false;
                            },
                            select: function (event, ui) { // If Query Suggestion get selected
                                searchinput.val(ui.item.fields.Title);
                                return false;
                            }
                        })
                        .data("ui-autocomplete")._renderItem = function (ul, item) { // render Query suggestion with <ul> and <li>
                            return $("<li></li>")
                                .data("ui-autocomplete-item", item)
                                .append(item.fields.Title)
                                .appendTo(ul);
                        };
                    }
                }, 200);
            })
        });
    }
});

// getFields function: to return all properties to look like json result
function getFields(results) {
    r = {};
    for (var i = 0; i < results.length; i++) {
        if (results[i] != undefined && results[i].Key != undefined) {
            r[results[i].Key] = results[i].Value;
        }
    }
    return r;
}

3. Place javascripts and css in Style Library Root Web:
– jquery.js to /Style Library/QuerySuggestion/js/jquery.js
– jquery-ui.min.js to /Style Library/QuerySuggestion/js/jquery-ui.min.js
– QuerySuggestion.js to Style Library/QuerySuggestion/js/QuerySuggestion.js
– jquery-ui.min.css to Style Library/QuerySuggestion/css/jquery-ui.min.css
4. Update and Publish custom MasterPage to include below: (under existing jquery script element)

<script src="/Style Library/CNP.CWE.QuerySuggestion/js/jquery.js" type="text/javascript"></script>
<script src="/Style Library/CNP.CWE.QuerySuggestion/js/jquery-ui.min.js" type="text/javascript"></script>
<script src="/Style Library/CNP.CWE.QuerySuggestion/js/CNP.CWE.QuerySuggestion.js" type="text/javascript"></script>
<link href="/Style Library/CNP.CWE.QuerySuggestion/css/jquery-ui.min.css" rel="stylesheet" type="text/css"/>

I’ve got the PowerShell and Javascripts from my reference below with bit of modification from me.
QuerySuggestion

References

SharePoint 2013 Hosted App (Part 1) – Configure Apps for SharePoint 2013

SharePoint Hosted App series:

The first part of my SharePoint Hosted App series and this step is required to develop SharePoint Apps in SharePoint 2013. I don’t want to provide you detail steps in this article because I think Gaurav Mahajan & Ahmed Farag has excellent article about it and there is also further explanation from Chris Whitehead technet article. Please check those article out, follow those steps and your environment should be setup successfully.

At a high level the configuration steps are as follows:

  • Infrastructure Configuration
    1. Determine your App Domain.
    2. Configure domain name in DNS.
    3. Create a new wildcard SSL certificate.
  • Farm Configuration
    1. Create SharePoint Service Applications and enable services.
    2. Configure App settings in SharePoint
  • ·App Auth Configuration
    1. Configure SharePoint for low-trust Apps
    2. Configure SharePoint for high-trust Apps

Stay tuned for the next part of the SharePoint Hosted App series.

References

%d bloggers like this: