Second Life of a Hungarian SharePoint Geek

April 18, 2016

Re-creating the Missing Out-of-the-Box Search Locations using PowerShell

Filed under: PowerShell, Reflection, Search, SP 2013 — Tags: , , , — Peter Holpar @ 22:40

Recently we migrated a SharePoint 2010 web application into an existing SharePoint 2013 farm to free up the old SharePoint 2010 farm for “recycling” (it was the last application running in the old farm). The web application has a single site collection, and contains a single business application with views and list forms having a lot of customizations via “jQuery magic”. Since a replacement of the business application is planed for the near future, we decided not to upgrade the site collection to the SharePoint 2013 user interface (version 15). Leaving it in the SharePoint 2010 mode (version 14) ensures the views and forms are working further without any modifications in the JavaScript codes. After a few days a user complained, that when searching the local web site an error is displayed instead of the search results on the _layouts/OSSSearchResults.aspx page:

Unable to display this Web Part. To troubleshoot the problem, open this Web page in a Microsoft SharePoint Foundation-compatible HTML editor such as Microsoft SharePoint Designer. If the problem persists, contact your Web server administrator

In the ULS logs we found these entries:

CoreResultsWebpart: Couldnt find location with internal name LocalSearchIndex
CoreResultsDatasourceView: Couldnt find location with internal name LocalSearchIndex

On the web we found a post from Sushant Dukhande with the description of the issue, and a suggestion for the solution.

Using the script on that site it turned out, that the LocationConfigurations property of the search proxy is really empty. In an other environment, where we tested the migration we had no such issue.

Sushant Dukhande suggests to re-provision the search application. It might really solve the problem, however in our case I felt it to be an intense change, and searched for an alternative solution. Having a look into what happens under the cover of a provisioning process, I found the method responsible for provisioning the missing search locations. It is the internal static CreateOOBLocations method of the Microsoft.Office.Server.Search.Administration.LocationFactory class (in the Microsoft.Office.Server.Search assembly).

First, we need a reference to the search service application. You can get it like this (assuming it is named "Search Service Application"):

$ssa = Get-SPEnterpriseSearchServiceApplication "Search Service Application"

or via this script (as long as you are sure, you have a single instance of this service application type in your farm):

[Microsoft.Office.Server.Search.Administration.SearchServiceApplication]$ssa = Get-SPServiceApplication | ? { $_.TypeName -eq "Search Service Application" }

To display the names of the existing search locations:

$locConfigs = $ssa.LocationConfigurations
$locConfigs | % { $_.InternalName }

The following PowerShell script shows how to invoke the CreateOOBLocations method passing the search service application as parameter using PowerShell and Reflection:

$searchAssembly = [Microsoft.Office.Server.Search.Administration.SearchServiceApplication].Assembly
$locationFactory_Type = $searchAssembly.GetType("Microsoft.Office.Server.Search.Administration.LocationFactory")

$bindingFlags = [Reflection.BindingFlags]::NonPublic -bor [Reflection.BindingFlags]::Static
$mi_CreateOOBLocations = $locationFactory_Type.GetMethod("CreateOOBLocations", $bindingFlags)
$mi_CreateOOBLocations.Invoke($null, @([Microsoft.Office.Server.Search.Administration.SearchServiceApplication]$ssa))

Invoking the CreateOOBLocations method might be not always the solution for you. The same is true for the re-provisioning process suggested by Sushant Dukhande, since it invokes the same method as well. The problem, that this method has a condition, before provisioning all of the default search locations:

if (searchApp.LocationConfigurations.Count < 1)
{
    LocationConfigurationCollection locationConfigurationsInternal = searchApp.GetLocationConfigurationsInternal(true);
    CreateLiveLocation(locationConfigurationsInternal);
    CreateLiveSuggestionsLocation(locationConfigurationsInternal);
    CreateLocalSharepointLocation(locationConfigurationsInternal);
    CreateLocalPeopleLocation(locationConfigurationsInternal);
    CreateLocalFSSharePointLocation(locationConfigurationsInternal);
}

I don’t see, how our farm “lost” its search locations, but if it is possible to “lose” only a subset of the search locations (for example, only the one called LocalSearchIndex), it won’t be re-created by the CreateOOBLocations method, as the count of search location is still not zero.

In this case, the solution may be to re-create only the missing search location via the corresponding method. In the case of the LocalSearchIndex search location it is the CreateLocalSharepointLocation method of the LocationFactory class:

$locConfigs = $ssa.LocationConfigurations
$mi_CreateLocalSharepointLocation = $locationFactory_Type.GetMethod("CreateLocalSharepointLocation", $bindingFlags)
$mi_CreateLocalSharepointLocation.Invoke($null, @([Microsoft.Office.Server.Search.Administration.LocationConfigurationCollection]$locConfigs))

After fixing the issue in the farm, I’ve tested our other farms as well to find out, whether they are affected by the same problem or not. In one of the farm, the script provided in the post I mentioned earlier detected the issue, although I was sure, there is no problem with the search. It turned out to be a false positive test. This farm has its search service as a shared service from another farm, and the user account the script was run with had no permission on the search service in that remote farm. The script simply hid away the access denied error.

However, if we create a LocationConfigurationCollection instance via its internal constructor (either with a parameter of type SearchServiceApplication or of type SearchServiceApplicationProxy), the access denied error is displayed in the case the user has no permissions, and the items of the collection can be accessed if there is no problem with the permissions.

Let’s see first the script using the SearchServiceApplication:

$bindingFlags = [Reflection.BindingFlags]::NonPublic -bor [Reflection.BindingFlags]::Instance
$ci_LocationConfigurationCollection = [Microsoft.Office.Server.Search.Administration.LocationConfigurationCollection].GetConstructor($bindingFlags, $null, @([Microsoft.Office.Server.Search.Administration.SearchServiceApplication]), $null)
$locConfigs = $ci_LocationConfigurationCollection.Invoke(@([Microsoft.Office.Server.Search.Administration.SearchServiceApplication]$ssa))
$locConfigs | % { $_.InternalName }

As I wrote, you can achieve the same via a service proxy. It is useful for example, if the application is connected to a shared search service of another farm. First, we get the proxy as:

$url = "http://YourSharePointApp/&quot;
$site = Get-SPSite $url
$serviceContext = [Microsoft.SharePoint.SPServiceContext]::GetContext($site)
$ssaAppProxy = $serviceContext.GetDefaultProxy([Microsoft.Office.Server.Search.Administration.SearchServiceApplicationProxy])

Next, we can use the same script as earlier, but in this case we invoke the internal constructor having the SearchServiceApplicationProxy parameter type:

$bindingFlags = [Reflection.BindingFlags]::NonPublic -bor [Reflection.BindingFlags]::Instance
$ci2_LocationConfigurationCollection = [Microsoft.Office.Server.Search.Administration.LocationConfigurationCollection].GetConstructor($bindingFlags, $null, @([Microsoft.Office.Server.Search.Administration.SearchServiceApplicationProxy]), $null)
$locConfigs = $ci2_LocationConfigurationCollection.Invoke(@([Microsoft.Office.Server.Search.Administration.SearchServiceApplicationProxy]$ssaAppProxy))
$locConfigs | % { $_.InternalName }

Advertisements

March 5, 2014

February 24, 2013

Favorites in the Cloud: Implementing a Windows 8 Search application with An Office 365 backend

In the previous part of this post I illustrated, how we can share links from IE 10 on O365. For general background info on the subject I suggest you to read that part first. In this part I provide the code for an app that helps to look up the favorites and to open them in IE.

Just like in the former part, I used a sample project from the Windows 8 SDK as a prototype of the development, and concentrate on the SharePoint-specific code only. In this case we use the Search contract sample\JavaScript solution as the framework of the development.

The solution

After opening the Search contract sample solution in Visual Studio 2012, we have to make a few minor modifications in the code, and a major one to achieve the goals.

First, open the sample-utils.js file, look up the ScenarioOutput control, and append the following code snippet at the end of its declaration, after _addStatusOutput (don’t forget the comma separator!):

,
_addResultsOutput: function (element) {
    var resultsDiv = document.createElement("div");
    resultsDiv.id = "results";
    element.insertBefore(resultsDiv, element.childNodes[0]);
}

This div serves as the content placeholder when displaying the results.

In the default.js file we should first find this code block:

// Scenario 1 : Support receiving a search query while running as the main application.
Windows.ApplicationModel.Search.SearchPane.getForCurrentView().onquerysubmitted = function (eventObject) {
    WinJS.log && WinJS.log("User submitted the search query: " + eventObject.queryText, "sample", "status");
};

and replace it with this one (changes are highlighted with yellow):

var query;
// Scenario 1 : Support receiving a search query while running as the main application.
Windows.ApplicationModel.Search.SearchPane.getForCurrentView().onquerysubmitted = function (eventObject) {
    WinJS.log && WinJS.log("User submitted the search query: " + eventObject.queryText, "sample", "status");
    query = eventObject.queryText;
    querySPLinks();

};

Next, add this larger code snippet at the end (but before the closing braces!) of the default.js, and update the credential and the site URL. In a real-world app you should of course prompt for the credentials and optionally store them in a secure location.

Code Snippet
  1. // update these values to match your site and credentials
  2. var usr = 'username@yoursite.onmicrosoft.com';
  3. var pwd = 'password';
  4. var siteFullUrl = "https://yoursite.sharepoint.com&quot;;
  5. var linkListName = "SharedLinks";
  6.  
  7. var tokenReq = '<?xml version="1.0" encoding="utf-8"?>';
  8. tokenReq += '<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance&quot; xmlns:xsd="http://www.w3.org/2001/XMLSchema&quot; xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">&#039;;
  9. tokenReq += '  <soap:Body>';
  10. tokenReq += '    <GetUpdatedFormDigestInformation xmlns="http://schemas.microsoft.com/sharepoint/soap/&quot; />';
  11. tokenReq += '  </soap:Body>';
  12. tokenReq += '</soap:Envelope>';
  13.  
  14. var loginUrl = siteFullUrl + "/_forms/default.aspx?wa=wsignin1.0";
  15. var authReq = '<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope&quot; xmlns:a="http://www.w3.org/2005/08/addressing&quot; xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">&#039;
  16. authReq += '  <s:Header>'
  17. authReq += '    <a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</a:Action>&#039;
  18. authReq += '    <a:ReplyTo>'
  19. authReq += '      <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>&#039;
  20. authReq += '    </a:ReplyTo>'
  21. authReq += '    <a:To s:mustUnderstand="1">https://login.microsoftonline.com/extSTS.srf</a:To>&#039;
  22. authReq += '    <o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">&#039;
  23. authReq += '      <o:UsernameToken>'
  24. authReq += '        <o:Username>' + usr + '</o:Username>'
  25. authReq += '        <o:Password>' + pwd + '</o:Password>'
  26. authReq += '      </o:UsernameToken>'
  27. authReq += '    </o:Security>'
  28. authReq += '  </s:Header>'
  29. authReq += '  <s:Body>'
  30. authReq += '    <t:RequestSecurityToken xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust"><wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">&#039;
  31. authReq += '      <a:EndpointReference>'
  32. authReq += '        <a:Address>' + loginUrl + '</a:Address>'
  33. authReq += '      </a:EndpointReference>'
  34. authReq += '      </wsp:AppliesTo>'
  35. authReq += '      <t:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</t:KeyType>&#039;
  36. authReq += '      <t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType>&#039;
  37. authReq += '      <t:TokenType>urn:oasis:names:tc:SAML:1.0:assertion</t:TokenType>'
  38. authReq += '    </t:RequestSecurityToken>'
  39. authReq += '  </s:Body>'
  40. authReq += '</s:Envelope>';
  41.  
  42. function querySPLinks() {
  43.     // clear former results before submitting the new query
  44.     var resultsDiv = document.getElementById("results");
  45.     while (resultsDiv.childNodes.length > 0) {
  46.         var child = resultsDiv.childNodes[0];
  47.         resultsDiv.removeChild(child);
  48.     }
  49.     getToken();
  50. }
  51.  
  52. // Step 1: we get the token from the STS
  53. var token;
  54. function getToken() {
  55.     WinJS.xhr({
  56.         url: "https://login.microsoftonline.com/extSTS.srf&quot;,
  57.         type: 'POST',
  58.         data: authReq,
  59.         headers: { 'Content-type': 'application/soap+xml; charset=utf-8' }
  60.     }).done(
  61.     function fulfilled(result) {
  62.         // extract the token from the response data
  63.         token = result.responseXML.querySelector("BinarySecurityToken").textContent;
  64.         getFedAuthCookies();
  65.     },
  66.     function errHandler(err) {
  67.         var e = err;
  68.     });
  69. }
  70.  
  71. // Step 2: "login" using the token provided by STS in step 1
  72. function getFedAuthCookies() {
  73.     WinJS.xhr({
  74.         url: loginUrl,
  75.         type: 'POST',
  76.         data: token,
  77.         headers: { 'Content-type': 'application/x-www-form-urlencoded' }
  78.     }).done(
  79.     function fulfilled(result) {
  80.         refreshDigest();
  81.     },
  82.     function errHandler(err) {
  83.         var e = err;
  84.     });
  85. }
  86.  
  87. // Step 3: get the digest from the Sites web service and refresh the one stored locally
  88. var digest;
  89. function refreshDigest() {
  90.     WinJS.xhr({
  91.         url: siteFullUrl + '/_vti_bin/sites.asmx',
  92.         type: 'POST',
  93.         headers: {
  94.             'SOAPAction': 'http://schemas.microsoft.com/sharepoint/soap/GetUpdatedFormDigestInformation&#039;,
  95.             'X-RequestForceAuthentication': 'true',
  96.             'Content-type': 'text/xml; charset=utf-8'
  97.         },
  98.         data: tokenReq
  99.     }).done(
  100.    function fulfilled(result) {
  101.        digest = result.responseXML.querySelector("DigestValue").textContent;
  102.        sendRESTReq();
  103.    },
  104.    function errHandler(err) {
  105.        resportError(err);
  106.    });
  107. }
  108.  
  109. // Step 4: execute the REST request
  110. function sendRESTReq() {
  111.     WinJS.xhr({
  112.         url: siteFullUrl + "/_api/web/lists/GetByTitle('"+ linkListName + "')/items?$select=URL",
  113.         type: 'GET',
  114.         headers: {
  115.             'X-RequestDigest': digest,
  116.             "Accept": "application/json; odata=verbose",
  117.             'Content-type': 'application/json;odata=verbose'
  118.         }
  119.     }).done(
  120.    function fulfilled(result) {
  121.        var response = JSON.parse(result.responseText);
  122.        var links = response.d.results;
  123.        var resultsDiv = document.getElementById("results");
  124.        for (var i = 0; i < links.length; i++) {
  125.            var link = links[i];
  126.            var desc = link.URL.Description;
  127.            var url = link.URL.Url;
  128.            // we make the comparision on the client side,
  129.            // and build the UI for the links dynamically
  130.            if (desc.toLowerCase().indexOf(query.toLowerCase()) !== -1) {
  131.                var aLink = document.createElement("a");
  132.                var br = document.createElement("br");
  133.                aLink.id = "link" + i;
  134.                aLink.innerText = desc;
  135.                aLink.href = url;
  136.                resultsDiv.appendChild(aLink);
  137.                resultsDiv.appendChild(br);
  138.            }
  139.        }
  140.    },
  141.    function errHandler(err) {
  142.        var e = JSON.parse(err.responseText);
  143.        reportErrorMsg("Error: " + e.error.code + "\n" + e.error.message.value);
  144.    });
  145. }
  146.  
  147. function reportError(msg) {
  148.     if (e.message != undefined) {
  149.         reportErrorMsg(e.message);
  150.     }
  151.     else if (e.statusText != undefined) {
  152.         reportErrorMsg(e.statusText);
  153.     }
  154.     else {
  155.         reportErrorMsg("Error");
  156.     }
  157. }
  158.  
  159. function reportErrorMsg(msg) {
  160.     WinJS.log(msg, "sample", "error");
  161. }

How does it work?

The authentication mechanism against O365 is the same that I applied in my former post, and we use REST and WinJS.xhr as in the former part of this post.

Instead of selecting the matching items only using CAML, we load all items from the list to the client side, and filter the items in the client app. To limit the bandwidth usage we limit the scope of requested data to the URL property, that includes both the URL and the title of the link.

_api/web/lists/GetByTitle(‘SharedLinks’)/items?$select=URL

I found that if one would like to submit a REST request that includes a field of type URL as a filter, like the query below, a HTTP 400 status is returned by the server. However, when using IE alone to submit the query, we got no info on the reason.

_api/web/lists/GetByTitle(‘SharedLinks’)/items?$select=URL&$filter=startswith(URL,’code’)

First, when monitoring with Fiddler, could we recognize, that an XML document is returned as the body of the response with an error message:

The field ‘URL’ of type ‘URL’ cannot be used in the query filter expression.

Sad, but true.

BTW, using the following two images I illustrate, how the format of the fields of type URL has been changed in the response from the SP2010-style REST (_vti_bin/ListData.svc) to the SP2013-style (_vti_bin/client.svc or simply _api).

_vti_bin/ListData.svc/SharedLinks

image

_api/web/lists/GetByTitle(‘SharedLinks’)/items

image

Testing the search app

After deploying the app, in Window 8 you can activate the Search charm using the Windows + Q shortcut. You should select the Search contract JS sample from the available list, then provide the query term, for example, “project”, and submit the query.

image

The results will be displayed in the Output section of the app. Users can open the links in IE 10 simply by a click.

image

This app and the former one illustrate, that it is rather easy to integrate the cloud based SharePoint with your custom Windows 8 apps using the new, enhanced REST API. Having this type of extensions one can already organize the favorites in IE.

May 8, 2011

Hunting for the lost Advanced search link

Filed under: Dynamic method, Reflection, Search, SP 2010, Web part — Tags: , , , , — Peter Holpar @ 01:37

The other day I had to configure SharePoint search web parts. The plans were to create a Search Center, and set each web parts to include the Advanced search link. After configuring the People Search Box web part I realized that nothing seems to be changed on the UI.

image

I’ve double-checked the settings and found that the Display advanced search link checkbox was checked and the URL of the advanced search page was specified in the Advanced Search page URL text box, so the advanced link should be there.

image

I found a question on the web about the same issue (based on its date, probably for MOSS 2007) but no answer. So I started Reflector to see what’s happening in the web part.

Here are the results:

PeopleSearchBoxEx class (Microsoft.SharePoint.Portal.WebControls namespace, Microsoft.SharePoint.Portal assembly) is responsible for rendering the People Search Box web part. This class is inherited from the SearchBoxEx class (Microsoft.SharePoint.Portal.WebControls namespace, Microsoft.Office.Server.Search assembly) that is behind the standard Search Box web part, where the Advanced link was displayed with the very same settings.

image

There is an internal virtual CreateAdvanceSearchLink method in the SearchBoxEx class that is overridden in the PeopleSearchBoxEx class. This method is called from the CreateChildControls method of the corresponding class. I suspected that the issue is somewhere there, and yes, although the SearchBoxEx version of the CreateAdvanceSearchLink method handles the advanced search link settings, the PeopleSearchBoxEx version does not, nor does it call its base class version.

So next question, how to fix this problem? In the following I show you a solution for that, although I have to admit, it is neither trivial nor probably supported, as it uses techniques that may fail after a SharePoint service pack. I suggest you to use it at your own risk, if you decide to use it at all. My real goal of publishing this workaround is to show you some “dirty” programming methods that may help you to give solutions in such situations.

If you are a regular reader of my posts, you may already know that I like using reflection to call hidden methods to enable features that is not enabled through the public SharePoint API. In this case this was my first intention either.

The CreateAdvanceSearchLink method has three parameters. The first one is a TableRow, it is the row of the search box, and the method adds a new cell to the row if there is a link to display. The second parameter is a String, it is the search keyword that is typically got from the k query string parameter and appended to the advanced search link URL. Although this parameter exists for the PeopleSearchBoxEx class version as well, it is not used in that implementation. The last parameter is an integer value that is passed by reference. It contains the actual cell count of the row, and is incremented if a new cell is added to the row in the method. It might be not the most elegant way of handling that but it works this way.

So I created a custom web part derived from the PeopleSearchBoxEx class and was to call the original CreateAdvanceSearchLink version (in the base-base class SearchBoxEx) using reflection that would create the lost link for me if the web part settings require that.

This code gets the  MethodInfo of the the  internal CreateAdvanceSearchLink method of the SearchBoxEx type, casts the current web part instance to the SearchBoxEx class and invoke the MethodInfo on that instance. It typically looks like that:

  1. protected void CreateAdvanceSearchLink(TableRow row, string keyword, ref int colsRest)
  2. {
  3.     Type searchBoxExType = typeof(SearchBoxEx);
  4.     Type[] parameterTypes = { typeof(TableRow), typeof(String), typeof(int).MakeByRefType() };
  5.     MethodInfo mi_CreateAdvanceSearchLink = searchBoxExType.GetMethod("CreateAdvanceSearchLink",
  6.             BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly, null, parameterTypes, null);
  7.     if (mi_CreateAdvanceSearchLink != null)
  8.     {
  9.         object[] args = { row, keyword, colsRest };
  10.         SearchBoxEx searchBoxEx = (SearchBoxEx)this;
  11.         mi_CreateAdvanceSearchLink.Invoke(searchBoxEx, args);
  12.         colsRest = (int)args[2];
  13.     }
  14. }

After the first test I found, that in this case not the SearchBoxEx version of the CreateAdvanceSearchLink method is called, but the PeopleSearchBoxEx version. That is because the original version is marked as virtual, so even reflection calls the overridden version.

What can we do in this case to force the .NET framework to call the original version? One can find the answer for this question in this forum thread. Although the answer from Doggett is not the accepted answer for the question at the time of writing this post, I found it useful to find the right way.

So let’s try the same using a dynamic method!

After declaring the delegate that corresponds to the signature of the CreateAdvanceSearchLink method (including the type itself), I created a member variable to store the reference to the delegate that I create in the class constructor. That is reasonable, as it does not change between calls (if we were to call it multiple times) so it helps better performance.

In this case we get the MethodInfo similarly to our first try, then use the ILGenerator class to generate the DynamicMethod. Finding the correct IL code requires some knowledge of the CLR and usually done through the trial and error approach (at least, for me).

Finally, we create the delegate instance using our DynamicMethod.

  1. delegate void CreateAdvanceSearchLinkDelegate(PeopleSearchBoxEx peopleSearchBoxEx, TableRow row, String keyword, ref int colsRest);
  2.  
  3. CreateAdvanceSearchLinkDelegate _createAdvanceSearchLink;
  4.  
  5. public PeopleSearchBoxExAdv() : base()
  6. {
  7.     Type searchBoxExType = typeof(SearchBoxEx);
  8.     Type[] parameterTypes = { typeof(TableRow), typeof(String), typeof(int).MakeByRefType() };
  9.     MethodInfo mi_CreateAdvanceSearchLink = searchBoxExType.GetMethod("CreateAdvanceSearchLink",
  10.             BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly, null, parameterTypes, null);
  11.     if (mi_CreateAdvanceSearchLink != null)
  12.     {
  13.         DynamicMethod dm = new DynamicMethod("CreateAdvanceSearchLink", null,
  14.             new Type[] { typeof(PeopleSearchBoxEx), typeof(TableRow), typeof(String), typeof(int).MakeByRefType() },
  15.             typeof(PeopleSearchBoxEx));
  16.         ILGenerator gen = dm.GetILGenerator();
  17.         gen.Emit(OpCodes.Ldarg_0);
  18.         gen.Emit(OpCodes.Ldarg_1);
  19.         gen.Emit(OpCodes.Ldarg_2);
  20.         gen.Emit(OpCodes.Ldarg_3);
  21.         gen.Emit(OpCodes.Call, mi_CreateAdvanceSearchLink);
  22.         gen.Emit(OpCodes.Ret);
  23.         _createAdvanceSearchLink =
  24.             (CreateAdvanceSearchLinkDelegate)dm.CreateDelegate(typeof(CreateAdvanceSearchLinkDelegate));
  25.     }

Having this done, calling the original version of the CreateAdvanceSearchLink method is so simple as this:

  1. protected void CreateAdvanceSearchLink(TableRow row, string keyword, ref int colsRest)
  2. {
  3.     if (_createAdvanceSearchLink != null)
  4.     {
  5.         _createAdvanceSearchLink((PeopleSearchBoxEx)this, row, keyword, ref colsRest);
  6.     }
  7. }

The rest of the code is about implementing the addition or merging the link as requested. If there are other links to display (like Preferences or Search Options) the we have to merge the Advanced link with these ones, otherwise, we have to add the new cell as is.

We have to handle the search keyword that we need to pass to the CreateAdvanceSearchLink method. The keyword itself is accessible through the protected field m_strKSFromPostOrGetOverride of the base class. In the SearchBoxEx implementation of the method it is trimmed using the internal static TrimAndChopStringBySize method of the internal SearchCommon class. Since it is a quite simple method and I did not want to involve more reflection into the code, I simple borrowed the method into my class:

  1. string TrimAndChopStringBySize(string strLongString, int iMaxSize)
  2. {
  3.     if (strLongString == null)
  4.     {
  5.         return string.Empty;
  6.     }
  7.     string str = strLongString.Trim();
  8.     int length = str.Length;
  9.     if (iMaxSize >= length)
  10.     {
  11.         return str;
  12.     }
  13.     if (iMaxSize > 3)
  14.     {
  15.         return (str.Substring(0, iMaxSize – 3) + "…");
  16.     }
  17.     return str.Substring(0, iMaxSize);
  18. }

If we add the Advanced link to a new cell of the row, and there is a second row in the web part for the Additional query description label, then we have to ensure that the column span of its cell is increased by one.

Based on the above, the CreateChildControls method of our class is as follows:

  1. protected override void CreateChildControls()
  2. {
  3.     base.CreateChildControls();
  4.  
  5.     // we have to alter the content only if advanced search link
  6.     // must be shown
  7.     if (ShowAdvancedSearch)
  8.     {
  9.         // table might be at first or later position
  10.         // depending on web part settings
  11.         Table table = null;
  12.         foreach (Control control in Controls)
  13.         {
  14.             if (control is Table)
  15.             {
  16.                 table = (Table)control;
  17.                 break;
  18.             }
  19.         }
  20.  
  21.         // table found
  22.         if (table != null)
  23.         {
  24.             //string str5 = SearchCommon.TrimAndChopStringBySize(this.m_strKSFromPostOrGetOverride, 200 – ((this._AppQueryTerms != null) ? this._AppQueryTerms.Length : 0));
  25.             String keyword = TrimAndChopStringBySize(this.m_strKSFromPostOrGetOverride, 200 – ((AppQueryTerms != null) ? AppQueryTerms.Length : 0));
  26.             //String keyword = Page.Request.QueryString["k"];
  27.  
  28.             // should be always true, but check to be sure…
  29.             if (table.Rows.Count > 0)
  30.             {
  31.                 int colsRest;
  32.                 // if either preferences or options are shown
  33.                 // we have to merge the advanced search link
  34.                 // into the existing cell
  35.                 if ((ShowPerferenceLink) || (ShowSearchOptions))
  36.                 {
  37.                     TableRow tr = new TableRow();
  38.                     colsRest = 0;
  39.  
  40.                     CreateAdvanceSearchLink(tr, keyword, ref colsRest);
  41.  
  42.                     int itemNum = (ShowPerferenceLink) ? 1 : 0;
  43.  
  44.                     // should be always true, but check to be sure…
  45.                     if ((tr.Cells.Count > 0) && (tr.Cells[0].Controls.Count > itemNum) &&
  46.                         (tr.Cells[0].Controls[itemNum] is HtmlGenericControl) &&
  47.                         (table.Rows[0].Cells.Count > 1))
  48.                     {
  49.                         // copy the 'entire DIV' tag, not the HyperLink only
  50.                         // into the next to last cell position
  51.                         HtmlGenericControl gc = (HtmlGenericControl)tr.Cells[0].Controls[itemNum];
  52.                         table.Rows[0].Cells[table.Rows[0].Cells.Count – 2].Controls.Add(gc);
  53.                     }
  54.  
  55.                 }
  56.                 // if neither preferences nor options are shown
  57.                 // we can add the new cell to the end of
  58.                 // of the existing row
  59.                 else
  60.                 {
  61.                     colsRest = table.Rows[0].Cells.Count; ;
  62.                     CreateAdvanceSearchLink(table.Rows[0], keyword, ref colsRest);
  63.  
  64.                     if (AppQueryTermsLabel != null)
  65.                     {
  66.                         // there must be a second line if the Additional query description label
  67.                         // is specified, but we check to be sure…
  68.                         if (table.Rows.Count > 1)
  69.                         {
  70.                             table.Rows[1].Cells[table.Rows[1].Cells.Count – 1].ColumnSpan++;
  71.                         }
  72.                     }
  73.                 }
  74.  
  75.             }
  76.  
  77.         }
  78.     }
  79. }

To test the code, we add the web part to the search center:

image

After setting the web part properties, including the ones that are responsible for the with of the web part and the search box, the result is as it should be out of the box:

image

You can download the sample from here.

May 4, 2011

Setting the target search result page URL of the Search Box web part

Filed under: Search, SP 2010 — Tags: , — Peter Holpar @ 22:10

Setting a custom target search result page usually means nothing extra. One should usually set only the Target search result page URL (the corresponding web part property in Microsoft.SharePoint.Portal.WebControls.SearchBoxEx is called SearchResultPageURL). It is important however, that the Use site level defaults checkbox (UseSiteDefaults) value must be cleared, otherwise site collection level settings take place instead of our custom value.

image

The scope dropdown mode has a similar configuration value pair, see Dropdown mode (DropDownModeEx) below, and Use site dropdown mode (UseSiteDropDownMode) above.

Both UseSiteDefaults and UseSiteDropDownMode have a default value of false.

image

The next figure illustrates global search settings (Site Collection Administration / Search settings), as defined at the site collection level. Notice the site level defaults and dropdown mode.

image

As the page above says, “By checking the "Use site level defaults" web part property, the search box will send custom scope queries to the Search Center defined in this "Site Collection Search Center" property.” and “By default, a Search Box will use the "Dropdown Mode" property in the web part.  By checking the "Use site dropdown mode" property, the search box will use this "Site Collection Search Dropdown Mode" setting instead.”

Recently I had to create a customized version of the OSSSearchResults.aspx that send custom scope queries to itself instead of the Search Center. After adding the correct SearchResultPageURL property value to the MSSWC:SearchBoxEx control, there was no change in the default behavior. Fortunately, I realized found the following property setting on the same control:

UseSiteDefaults = "true"

Removing this setting applied the default "false" value that ensured my custom target URL page settings.

You can read more about these web part settings here:

Change properties for the Search Box Web Part

For the more technologically minded it might be interesting that the SearchBoxEx stores and merges these settings in the private field _PropertiesOverrideableBySite of type private nested class PropertiesOverrideableBySite. See its EffectiveSearchResultPageUrl (String) and EffectiveDropDownMode (DropDownModesEx) property getters for implementation details, as well as the non-standard property setters like in the case of the SearchResultPageUrl property. This latter one is the property we set when assigning value to the SearchResultPageURL property of the SearchBoxEx class.

Effective property values are used on web part rendering (see OnPreRender method) and when getting the scope for contextual search (see GetSpWeb method called by HandleContextualScoping method).

November 14, 2007

Creating Keywords and Best Bets for MOSS Search programmatically

Filed under: Search, SharePoint — Tags: , — Peter Holpar @ 02:24

Note: This is a repost, the original one was published on SharePoint Blogs on November 14, 2007

You can administer keywords and best bets on the MOSS admin UI at Search keywords at Site Collection Administration.

Stefan Goßner gave a code snippet about How To: create Keywords and Best Bets for MOSS Search programmatically. A similar code can be found in the very useful SharePoint Server 2007 Presentations: Enterprise Search Deep Dives presentation series, Customizing and Extending Search in Office SharePoint Server 2007.

Based on my experiments there might be a little but important problem with this code. When creating the new Keyword instance, you should use DateTime.UtcNow instead of DateTime.Now to enable the Keyword immediately:

keywords.AllKeywords.Create("myKeyword", DateTime.UtcNow);

When you create the keyword through the UI, you can specify only the date part, no hours and minutes. In this case the keyword is created with a start date (StartDate property) 0:00 AM UTC for the specified date. For example, currently our local time is GMT+1, so the start date would be 23:00 PM for the previous day.

When I used the code samples “as is”, the keywords were not displayed in the results. After one hour, the repeated search already displayed the keyword matches. When I used the DateTime.UtcNow, the results were displayed immediately. Of course, if your configured time zone is west from the GMT time line, then DateTime.Now should work also, as it is refers to a time in the past if you interpret it as UTC time.

If you try to create a best bet on the UI that refers to an URL already used in an existing best bet you cannot save the new best bet. I found another interesting behavior of creating keywords and best bets from code. The code that creates the best bets with a common URL but different titles and descriptions will run without errors:

Keyword keyword1= keywords.AllKeywords.Create("keyword1", DateTime.UtcNow); 

BestBet bestBet1 = keyword1.BestBets.Create("BestBet1", "Description1", new
Uri("http://www.company.com"));
keyword1.Update();
Keyword keyword2=
keywords.AllKeywords.Create("keyword2", DateTime.UtcNow);
BestBet bestBet1 =
keyword2.BestBets.Create("BestBet2", "Description2", new
Uri("http://www.company.com"));
keyword2.Update();

In the example above the best bet for keyword2 will refer to the BestBet with title BestBet1 created for keyword1.

November 13, 2007

High Confidence Results in MOSS 2007

Filed under: Search, SharePoint — Tags: , — Peter Holpar @ 01:03

Note: This is a repost, the original one was published on SharePoint Blogs on November 13, 2007

At the MSDN forum I found an interesting question (http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=2379969&SiteID=1) that asks what high confidence results are. After some investigation I think I have the answer for this question.

First of all, high confidence results are displayed by the Microsoft.Office.Server.Search.WebControls.HighConfidenceWebPart (Microsoft.Office.Server.Search assembly). On the standard search result page (results.aspx) there are two instances of this web part. One is Search High Confidence Results, the other is Search Best Bets. If you check the properties of the HighConfidenceWebPart either by modifying the shared web part, or by using Reflector, you can see that this web part can display keyword matches with best bets and the mysterious high confidence results. By default the Search High Confidence Results web part instance is configured to display the high confidence results and the Search Best Bets web part instance is configured to display keywords and best bets (as one can guess from their name).

The SDK contains information about that when the Enterprise Search returns the results there is a HighConfidenceResults table that is “The result set containing high-confidence results”. Well, it is not very descriptive, is it?

Let’s see the formatting XSL for the HighConfidenceWebPart. You can check it in the XSL Editor in the Data View Properties section of the tool part. Fortunately, there is a template for the HighConfidenceResults, displayed below:

<xsl:template match="All_Results/HighConfidenceResults/Result"> 

<xsl:if test="$DisplayHC = 'True' and $IsFirstPage = 'True'">
<xsl:variable name="prefix">IMNRC('</xsl:variable>
<xsl:variable name="suffix">'
)</xsl:variable>
<xsl:variable name="url" select="url"/>
<xsl:variable name="id" select="id"/>
<xsl:variable name="pictureurl" select="highconfidenceimageurl"/>
<xsl:variable name="jobtitle" select="highconfidencedisplayproperty1"/>
<xsl:variable name="workphone" select="highconfidencedisplayproperty2"/>
<xsl:variable name="department" select="highconfidencedisplayproperty3"/>
<xsl:variable name="officenumber" select="highconfidencedisplayproperty4"/>
<xsl:variable name="preferredname" select="highconfidencedisplayproperty5"/>
<xsl:variable name="aboutme" select="highconfidencedisplayproperty8"/>
<xsl:variable name="responsibility" select="highconfidencedisplayproperty9"/>
<xsl:variable name="skills" select="highconfidencedisplayproperty10"/>
<xsl:variable name="workemail" select="highconfidencedisplayproperty11"/>

You can see, that all of the properties are related to persons. I found that if you search for a person specifying the full name, and there is match, then it is displayed as a high confidence result. In the web part properties you can specify if the title, image, description and other properties should be displayed for a high confidence match. There is a ResultsPerTypeLimit property (“Maximum matches per High Confidence type”) that similar to the BestBetsLimit property (“Best Bets limit” on the user interface). Based on my experience, the BestBetsLimit works as expected but the ResultsPerTypeLimit seems to have no effect on the displayed results. Checking the default formatting XSL shows that the BestBetsLimit property is used (BBLimit parameter), but the ResultsPerTypeLimit is not used, although it is declared as an XSL parameter with the same name.

You should include the following condition in the HighConfidenceResults template (see above) after checking the "$DisplayHC = ‘True’ and $IsFirstPage = ‘True’" condition:

<xsl:if test="position() &lt;= $ ResultsPerTypeLimit " >

Remark: The XSL parameter values are populated in the ModifyXsltArgumentList method of the HighConfidenceWebPart web part.

Create a free website or blog at WordPress.com.