Monday, January 12, 2015

Set result source and item display template programmatically to SharePoint 2013 search ResultScriptWebPart


  1. //Get the instance of web part
  2. var homePage = web.GetFile("SitePages/Home.aspx");
  3. using (var webPartManager = homePage.GetLimitedWebPartManager(PersonalizationScope.Shared))
  4. {
  5. var recentDocs = webPartManager.WebParts.Cast<WebPart>().FirstOrDefault
  6.         (wp => wp.Title.Equals("Recent Documents")) as ResultScriptWebPart;
  7. var querySettings = new DataProviderScriptWebPart
  8.         {
  9.            PropertiesJson = recentDocs.DataProviderJSON
  10.         };
  11.  
  12. //Get the search service application proxy
  13. var settingsProxy = SPFarm.Local.ServiceProxies.GetValue
  14.         <SearchQueryAndSiteSettingsServiceProxy>();
  15. var searchProxy = settingsProxy.ApplicationProxies.GetValue
  16.         <SearchServiceApplicationProxy>("Search Service Application");
  17. var siteOwner = new SearchObjectOwner(SearchObjectLevel.SPSite, web);
  18.  
  19. //Set the result source. I set the default for demonstration
  20. var siteResultSource = searchProxy.GetResultSourceByName("Local SharePoint Results", siteOwner);
  21.  
  22.  
  23. querySettings.Properties["SourceName"] = siteResultSource.Name;
  24. querySettings.Properties["SourceID"] = siteResultSource.Id;
  25. querySettings.Properties["SourceLevel"] = siteOwner.Level;
  26.  
  27.  
  28. //Set the item display template
  29. recentDocs.ItemTemplateId = "~sitecollection/_catalogs/masterpage/Display Templates/Search/Item_Contoso.js";
  30. recentDocs.DataProviderJSON = querySettings.PropertiesJson;
  31.  
  32. webPartManager.SaveChanges(recentDocs);
  33. }
  34. homePage.Update();

How to create SharePoint 2013 search Result Types in C#.

Hello!
Recently I had to create Visual Studio 2012 project to deploy SharePoint 2013 search results customizations. Due to lack of documentation the task was quite challenging. In this post I'd like to share my experience.

Introduction.

SharePoint 2013 introduces new web part rendering based on client side code (javascript). For UI customizations developers can create Display Templates. This process described well in the post: Using Query Rules, Result Types and Display Templates for a Custom Search Sales Report in SharePoint 2013 .According to it you have to configure result types (RTs) to map your custom template to specific document or list item type. Lets look how to do this using code.

Object Model.

Analyzing SharePoint 2013 MSDN Search Class Library documentation I found following classes that represent required entities:
  • ResultItemType - represents actual RT.
  • PropertyRule - represents property to value mapping rule that indicates what content should match the rule.
  • SearchServiceApplicationProxy - represents Search Service Application Proxy connection.
When I tried to recreate the manually created RTs using the object model I had no luck. Actually RTs were created and I could ensure that using object model but when I tried to open OOTB RT editing page I received an exception. In order to find out how SharePoint does the same things I looked through RT editing page code using .NET Reflector and was very surprised that most of valuable properties and methods are internal. Luckily it is possible to call them using reflection.

Implementation.

Lets start with RT creation method. If you look at RT settings page you will notice that you should provide name, set of rules, display template URL and optionally check "Optimize for frequent use" checkbox. Hence the method contains all the parameters mentioned.

public static ResultItemType CreateResultType(SPWeb web, string name, string displayTemplateUrl, PropertyRule[] rules, booloptimizeForFrequentUse)
        {
            ResultItemType resType = new ResultItemType(new SearchObjectOwner(SearchObjectLevel.SPWeb, web));
            resType.Name = name;
            resType.SourceID = new Guid();
            resType.DisplayTemplateUrl = "~sitecollection" + displayTemplateUrl;
            SPFile file = web.GetFile(SPUtility.ConcatUrls(web.ServerRelativeUrl, displayTemplateUrl));
            resType.DisplayProperties = ParseManagePropertyMappings(file.ListItemAllFields["Managed Property Mappings"].ToString());
            resType.Rules = new PropertyRuleCollection(new List<PropertyRule>(rules));
            typeof(ResultItemType).GetProperty("OptimizeForFrequentUse").SetValue(resType, optimizeForFrequentUse);
            return resType;
        }


The first interesting thing that DisplayTemplateUrl should be a path to *.js file in "~sitecollection/...."  format (eg. "~sitecollection/_catalogs/masterpage/Display Templates/Search/Item_Template.js"), so in this case displayTemplateUrl parameter should be in "/.../file.js" format.
The second important property is DisplayProperties. It should correspond the list of Managed Properties you use in the Display Template. You should specify comma separated Managed Property names here. To retrieve the property list dynamically we can use file's property named  "Managed Property Mappings". The trouble is that in the property the list is represented in the same way as in Display Template (in "Name1":"Name2" comma separated values format). So I'm using ParseManagePropertyMappings method (see below) for conversion.
 private static string ParseManagePropertyMappings(string mappings)
        {
            string[] propArray = mappings.Replace("'""").Replace("\"""").Split(',');
            for (int i = 0; i < propArray.Length; i++)
            {
                if (propArray[і].Contains(":"))
                {
                    int n = propArray[і].LastIndexOf(':');
                    if ((n > 0) && (n < (propArray[і].Length - 1)))
                    {
                        propArray[і] = propArray[і].Substring(n + 1);
                    }
                }
                propArray[і] = propArray[і].Replace(";"",");
            }
            return string.Join(",", propArray);
        }

The logic of the method was borrowed from Microsoft's one.
And finally OptimizeForFrequentUse property has internal setter so we should utilize reflection to set its value.

Next valuable part is to create rules for RT. There are 2 types of rules: predefined rules, such as Word document (Mapped rules) and regular property-to-value mapping rules. Lets consider how do we create predefined rules:
public static PropertyRule CommonPropertyRule(string typeOfContent)
        {
            Type type = typeof(PropertyRule.MappedPropertyRules);
            FieldInfo info = type.GetField(typeOfContent, BindingFlags.NonPublic | BindingFlags.Static);
            object value = info.GetValue(null);
            return (PropertyRule)value;
        }

We have to use reflection again to retrieve predefined rules. typeOfContent corresponds values from the "type of content" dropdown of the editing page (Word, Excel, PDF etc).
To create another type of rules we can use following method:
public static PropertyRule CustomPropertyRule(string propertyName, PropertyRuleOperator.DefaultOperator propertyOperator, string[] values)
        {
            Type type = typeof(PropertyRuleOperator);
            PropertyInfo info = type.GetProperty("DefaultOperators", BindingFlags.NonPublic | BindingFlags.Static);
            object value = info.GetValue(null);
            var DefaultOperators = (Dictionary<PropertyRuleOperator.DefaultOperator, PropertyRuleOperator>)value;
            PropertyRule rule = new PropertyRule(propertyName, DefaultOperators[propertyOperator]);
            rule.PropertyValues = new List<string>(values);
            return rule;
        }


Finally, in order to add result types to a Web we can use for example FeatureActivated event of site collection scoped feature:
public override void FeatureActivated(SPFeatureReceiverProperties properties)
        {
            SPSecurity.RunWithElevatedPrivileges(() =>
                {
                    SPSite site = properties.Feature.Parent as SPSite;
                    SPWeb web = site.RootWeb;
                    SPServiceContext serviceContext = SPServiceContext.GetContext(site);
                    if (serviceContext != null)
                    {
                        SPServiceApplicationProxy proxy = serviceContext.GetDefaultProxy(typeof(SearchServiceApplicationProxy));
                        SearchServiceApplicationProxy searchAppProxy = proxy as SearchServiceApplicationProxy;
                        SearchService searchService = SearchService.Service;
                        SearchServiceApplication searchApp = searchService.SearchApplications.GetValue<SearchServiceApplication>(searchAppProxy.GetSearchServiceApplicationInfo().SearchServiceApplicationId);
                        ResultItemType documents = CreateResultType(web, "MyDocuments""/_catalogs/masterpage/Display Templates/Search/MyCustomDocuments.js",
                            new PropertyRule[] { CommonPropertyRule("Word"),
                                CommonPropertyRule("Excel"),
                                CommonPropertyRule("PowerPoint"),
                                CommonPropertyRule("PDF"),
                                CommonPropertyRule("Text")
                            }, true);
                        searchAppProxy.AddResultItemType(documents);
                        ResultItemType MyTask = CreateResultType(web, "MyTask""/_catalogs/masterpage/Display Templates/Search/MyTask.js",
                            new PropertyRule[] { CustomPropertyRule("ContentType", PropertyRuleOperator.DefaultOperator.IsEqual, newstring[] { "Task" }) });
                        searchAppProxy.AddResultItemType(MyTask);                       
                    }
}
            );
        }


Thats all. Happy coding!
Good luck!

No comments:

Post a Comment