Second Life of a Hungarian SharePoint Geek

March 23, 2018

Changing SharePoint Search Preferences from Code

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

In my recent post I’ve illustrated with C# and PowerShell examples, how to read search preferences info from code, both for the current user as well as for other users. In this post we will see, how to change that preference from code.

We will use the same object, the Microsoft.Office.Server.Search.Administration.UserPreference class. Unlike its static GetUserPreference method, the either static SetUserPreference method has no overload that accepts the SharePoint context (SPContext) as parameter. The single overload of this method accepts a UserPreference instance. That makes our life not easier when it comes later to changing the preferences for another user. Don’t lose the hope, it is not impossible. Bur first things first.

Once we received a UserPreference instance via the GetUserPreference method, you should change certain preference properties, like OpenDocumentsInClient. How to do it? There are two methods, EnableSettings and DisableSetting (both having a parameter of  the nested enumeration type Settings) defined in the UserPreference class. If you would like to activate a setting, you should call the EnableSettings method, if you need to deactivate it, call the DisableSetting method, then finally invoke the SetUserPreference method to persist the changes. For example:

  1. var userPref = UserPreference.GetUserPreference();
  2. // if you would like to open documents in Office client, like Word or Excel
  3. userPref.UpdateSetting(UserPreference.Settings.OpenDocumentsInClient, true);
  4. // if you would like to open documents in Browser (Office Web Apps)
  5. userPref.UpdateSetting(UserPreference.Settings.OpenDocumentsInClient, false);
  6. UserPreference.SetUserPreference(userPref);

Note: This code works only if  you try it within a SharePoint context, like on an application page or in web part. In a console application you will receive an exception when you invoke the overload of the GetUserPreference method without the SPContext parameter:

ArgumentNullException
The value must not be null.
Parameter name: SPContext.Current

As we’ve already seen, there is an overload of the GetUserPreference method that accepts a SPContext parameter, so you could use that to get the preferences, but as there is no such overload for the SetUserPreference method, at least at this points will be the same type of exception thrown again. We will revisit the question shortly, how to set your own preferences from a console application, but we make a quick detour first.

To tell the truth, I don’t like the above pattern at all. Instead of these two methods I created an extension method with a Boolean parameter that encapsulates the functionality:

  1. public static void UpdateSetting(this UserPreference userPreference, UserPreference.Settings setting, bool value)
  2. {
  3.     if (value)
  4.     {
  5.         userPreference.EnableSetting(setting);
  6.     }
  7.     else
  8.     {
  9.         userPreference.DisableSetting(setting);
  10.     }
  11. }

Using this new method one can enable / disable preference settings like:

  1. var userPref = UserPreference.GetUserPreference();
  2. // if you would like to open documents in Office client, like Word or Excel
  3. userPref.EnableSetting(UserPreference.Settings.OpenDocumentsInClient);
  4. // if you would like to open documents in Browser (Office Web Apps)
  5. userPref.DisableSetting(UserPreference.Settings.OpenDocumentsInClient);
  6. UserPreference.SetUserPreference(userPref);

Back to the question, how to set your own preferences when the code runs without SharePoint context, like from a console application?

The “trivial” way is to fake a SharePoint context, using the method described here:

  1. using (SPSite site = new SPSite(url))
  2. {
  3.     using (SPWeb web = site.OpenWeb())
  4.     {
  5.         HttpRequest request = new HttpRequest(string.Empty, url, string.Empty);
  6.  
  7.         HttpResponse response = new HttpResponse(new System.IO.StreamWriter(new System.IO.MemoryStream()));
  8.  
  9.         HttpContext ctx = new HttpContext(request, response);
  10.         ctx.Items["HttpHandlerSPWeb"] = web;
  11.         HttpContext.Current = ctx;
  12.  
  13.         Console.WriteLine(SPContext.Current.Web.CurrentUser.LoginName);
  14.  
  15.         var userPref = UserPreference.GetUserPreference();
  16.         userPref.UpdateSetting(UserPreference.Settings.OpenDocumentsInClient, true);
  17.         UserPreference.SetUserPreference(userPref);
  18.  
  19.         //set back the original context (e.g. null)
  20.         HttpContext.Current = null;
  21.     }
  22. }

Another option is, to try to understand, how the SetUserPreference method internally works. It turns out, that it call the internal static UpdatePreference method:

UpdatePreference(preference, false, SPContext.Current);

So I’ve created just another extension method that wraps invoking the UpdatePreference method using Reflection:

  1. public static void Update(this UserPreference userPreference, bool fClearClickHistory, SPContext context)
  2. {
  3.     Type[] paramTypes = { typeof(UserPreference), typeof(bool), typeof(SPContext) };
  4.     MethodInfo updatePreference = userPreference.GetType().GetMethod("UpdatePreference", BindingFlags.Static | BindingFlags.NonPublic, null, paramTypes, null);
  5.     object[] parameters = { userPreference, fClearClickHistory, context };
  6.     updatePreference.Invoke(null, parameters);
  7. }

And a further helper method that accepts a SPContext object as parameter, writes out, preferences of which user we are to change and performs the change itself via the methods we have already:

  1. private void UpdatePreference(SPContext ctx)
  2. {
  3.     var user = ctx.Web.CurrentUser;
  4.     Console.WriteLine("Setting preferences for '{0}'", user.LoginName);
  5.  
  6.     var userPref = UserPreference.GetUserPreference(false, ctx);
  7.     userPref.UpdateSetting(UserPreference.Settings.OpenDocumentsInClient, false);
  8.     userPref.Update(false, ctx);
  9. }

I think the code we achieved using this extension method is much more readable as the former one with the dummy context:

  1. using (SPSite site = new SPSite(url))
  2. {
  3.     using (SPWeb web = site.OpenWeb())
  4.     {
  5.         var ctx = SPContext.GetContext(web);
  6.         UpdatePreference(ctx);
  7.     }
  8. }

The code snippets until this point have effect only on the current user. How to change the settings for other user? That is possible either, as soon we combine the methods we already have with impersonation.

First, the version that uses the dummy context:

  1. using (SPSite site = new SPSite(url))
  2. {
  3.     using (SPWeb web = site.OpenWeb())
  4.     {
  5.         var user = web.EnsureUser(@"i:0#.w|domain\user");
  6.  
  7.         SPSecurity.RunWithElevatedPrivileges(
  8.           () =>
  9.           {
  10.               using (SPSite impSite = new SPSite(url, user.UserToken))
  11.               using (SPWeb impWeb = impSite.OpenWeb())
  12.               {
  13.                   HttpRequest request = new HttpRequest(string.Empty, url, string.Empty);
  14.  
  15.                   HttpResponse response = new HttpResponse(new System.IO.StreamWriter(new System.IO.MemoryStream()));
  16.  
  17.                   HttpContext impersonatedContext = new HttpContext(request, response);
  18.  
  19.                   impersonatedContext.Items["HttpHandlerSPWeb"] = impWeb;
  20.  
  21.                   HttpContext.Current = impersonatedContext;
  22.  
  23.                   Console.WriteLine(SPContext.Current.Web.CurrentUser.LoginName);
  24.  
  25.                   var userPref = UserPreference.GetUserPreference();
  26.                   userPref.UpdateSetting(UserPreference.Settings.OpenDocumentsInClient, true);
  27.                   UserPreference.SetUserPreference(userPref);
  28.  
  29.                   //set back the original context (e.g. null)
  30.                   HttpContext.Current = null;
  31.               }
  32.           });
  33.     }
  34. }

Next, the other version using Reflection:

  1. using (SPSite site = new SPSite(url))
  2. {
  3.     using (SPWeb web = site.OpenWeb())
  4.     {
  5.         var user = web.EnsureUser(@"i:0#.w|domain\user");
  6.  
  7.         SPSecurity.RunWithElevatedPrivileges(
  8.           () =>
  9.           {
  10.               using (SPSite impSite = new SPSite(url, user.UserToken))
  11.               using (SPWeb impWeb = impSite.OpenWeb())
  12.               {
  13.                   var impCtx = SPContext.GetContext(impWeb);
  14.                   UpdatePreference(impCtx);
  15.               }
  16.           });
  17.     }
  18. }

Mission completed.

For those of you who would like to have the same functionality from PowerShell (of course, there are no SharePoint context inherited from the process at all), I include the equivalent methods below.

These are the helper methods we rely on:

  1. function UpdateSetting($userPreference, $setting, $value) {
  2.     If ($value)
  3.     {
  4.         $userPreference.EnableSetting($setting)
  5.     }
  6.     Else
  7.     {
  8.         $userPreference.DisableSetting($setting)
  9.     }
  10. }
  11.  
  12. function Update($userPreference, $fClearClickHistory, $context) {
  13.     $paramTypes = ($up, [bool], [Microsoft.SharePoint.SPContext])
  14.     $updatePreference = $up.GetMethod("UpdatePreference", [System.Reflection.BindingFlags]"Static, NonPublic" , $null, $paramTypes, $null)
  15.     $parameters = ($userPreference, $fClearClickHistory, $context)
  16.     $updatePreference.Invoke($null, $parameters)
  17. }

Furthermore, we declared the following shortcuts:

  1. # shortcut for UserPreference
  2. $up = [Microsoft.Office.Server.Search.Administration.UserPreference]
  3. # shortcut for the nested class Settings in UserPreference
  4. $ups = [Microsoft.Office.Server.Search.Administration.UserPreference+Settings]

Set preferences for the current user via Reflection:

  1. $web = Get-SPWeb $url
  2. $ctx = [Microsoft.SharePoint.SPContext]::GetContext($web)
  3.  
  4. $pref = $up::GetUserPreference($false, $ctx)
  5. UpdateSetting $pref $ups::OpenDocumentsInClient $true
  6. Update $pref $false $ctx

Set preferences for another user via Reflection:

  1. $userName = 'i:0#.w|domain\user'
  2.  
  3. $web = Get-SPWeb $url
  4. $user = $web.EnsureUser($userName)
  5. $userToken = $user.UserToken
  6.  
  7. $impersonatedSite = New-Object Microsoft.SharePoint.SPSite($url, $userToken)
  8. $ctx = [Microsoft.SharePoint.SPContext]::GetContext($impersonatedSite.RootWeb)
  9.  
  10. $pref = $up::GetUserPreference($false, $ctx)
  11. UpdateSetting $pref $ups::OpenDocumentsInClient $true
  12. Update $pref $false $ctx

Set preferences for the current user using a dummy context (see this post about injecting a fake SharePoint context into PowerShell):

  1. $web = Get-SPWeb $url
  2. $ctx = [Microsoft.SharePoint.SPContext]::GetContext($web)
  3.  
  4. $sw = New-Object System.IO.StringWriter
  5. $request = New-Object System.Web.HttpRequest "", $url, ""
  6. $response = New-Object System.Web.HttpResponse $sw
  7. $dummyContext = New-Object System.Web.HttpContext $request, $response
  8. [System.Web.HttpContext]::Current = $dummyContext
  9. $dummyContext.Items["HttpHandlerSPWeb"] = $ctx.Web;
  10.  
  11. $pref = $up::GetUserPreference($false, $ctx)
  12. #$pref.EnableSetting($ups::OpenDocumentsInClient)
  13. #or
  14. #$pref.DisableSetting($ups::OpenDocumentsInClient)
  15. UpdateSetting $pref $ups::OpenDocumentsInClient $true
  16. $up::SetUserPreference($pref)
  17.  
  18. [System.Web.HttpContext]::Current = $null

Set preferences for another user using a dummy context:

  1. $userName = 'i:0#.w|domain\user'
  2.  
  3. $web = Get-SPWeb $url
  4. $user = $web.EnsureUser($userName)
  5. $userToken = $user.UserToken
  6.  
  7. $impersonatedSite = New-Object Microsoft.SharePoint.SPSite($url, $userToken)
  8. $ctx = [Microsoft.SharePoint.SPContext]::GetContext($impersonatedSite.RootWeb)
  9.  
  10. $sw = New-Object System.IO.StringWriter
  11. $request = New-Object System.Web.HttpRequest "", $url, ""
  12. $response = New-Object System.Web.HttpResponse $sw
  13. $dummyContext = New-Object System.Web.HttpContext $request, $response
  14. [System.Web.HttpContext]::Current = $dummyContext
  15. $dummyContext.Items["HttpHandlerSPWeb"] = $ctx.Web;
  16.  
  17. $pref = $up::GetUserPreference($false, $ctx)
  18. #$pref.EnableSetting($ups::OpenDocumentsInClient)
  19. #or
  20. #$pref.DisableSetting($ups::OpenDocumentsInClient)
  21. UpdateSetting $pref $ups::OpenDocumentsInClient $true
  22. $up::SetUserPreference($pref)
  23.  
  24. [System.Web.HttpContext]::Current = $null

Advertisements

Querying SharePoint Search Preferences from Code

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

Recently a user complained, that he is not able to open office documents from the SharePoint portal of the company when working at home. He attached a screenshot of the error message in the browser to his mail. From this screenshot was it obvious, that there is an issue with the accessibility of the Office Web Applications server (OWA, also known as WAC), see the word wac in the address on the browser screen or WopiFrame.aspx in the address bar.

image

As it turned out, the WAC-server of the company has not been published externally via the firewall, but it was on purpose. Users should have been able to work with documents using their locally installed Office applications.

As you might know, you can configure the behavior, if document would be opened in browser or in the Office client application instead on either the site collection or on the document library level (see details here). The documents the user complained about were located in a library with the setting “Use the server default (Open in the client application)”, but it has not helped, when we changed it to “Open in the client application” explicitly.

It was really curious, but after a little while it turned out, that he wanted to open the document not from the library, but from a search result. At least a step further to the solution, have we thought.

You should know, that the behavior, if the Office documents get opened in the client application or in the browser is independent from the site collection level settings as well as from the document library settings. There is a (in my personal opinion pretty hidden) Preference link at the bottom of the search results page:

image

On this page the users can configure their own preferences, among others, if they would like to open the Office documents in the client application or in the browser:

image

It’s a cool option to enable users to decide which way they prefer, although it is pretty inconsistent with the other options (available for the administrators) we mentioned earlier. But there is an even bigger issue (at least, for me) with that. There is (as far as I know) no option / UI for administrators to query the value configured for a user, not to mention, how to change it remotely, without end user interaction.

Although it might have been the easiest choice to ask the user, which value he has configured for himself , I’m not the man of easy options if there might be a programmatic approach as well and a chance to learn something new. So let’s see, what I’ve learned.

The user preferences regarding the search are available via the Microsoft.Office.Server.Search.Administration.UserPreference class. If you need the user preferences from the current SPContext (e.g. for the current user), you can use either the static GetUserPreference() method or the other static overload GetUserPreference(bool lookupFromCache). If, however, you need the preference for another user, you can inject it via the static GetUserPreference(bool lookupFromCache, SPContext context) method.

For example, the DisplayPreference method below displays a few of the available preferences from a context it receives as parameter:

  1. private void DisplayPreference(SPContext ctx)
  2. {
  3.     var user = ctx.Web.CurrentUser;
  4.     Console.WriteLine("Reading preferences for '{0}'", user.LoginName);
  5.  
  6.     var userPref = UserPreference.GetUserPreference(false, ctx);
  7.  
  8.     Console.WriteLine("ShowPrequerySuggestion: {0}", userPref.IsSettingEnabled(UserPreference.Settings.ShowPrequerySuggestion));
  9.     Console.WriteLine("ShowPersonalSuggestions: {0}", userPref.IsSettingEnabled(UserPreference.Settings.ShowPersonalSuggestions));
  10.     Console.WriteLine("OpenDocumentsInClient: {0}", userPref.IsSettingEnabled(UserPreference.Settings.OpenDocumentsInClient));
  11. }

The following code snippet (taken from a console application) invokes the DisplayPreference method first to display the preferences of the current user, then again to display the preferences of an impersonated user:

  1. using (SPSite site = new SPSite(url))
  2. {
  3.     using (SPWeb web = site.OpenWeb())
  4.     {
  5.         var ctx = SPContext.GetContext(web);
  6.         DisplayPreference(ctx);
  7.  
  8.         var user = web.EnsureUser(@"i:0#.w|domain\user");
  9.  
  10.         SPSecurity.RunWithElevatedPrivileges(
  11.           () =>
  12.           {
  13.               using (SPSite impSite = new SPSite(url, user.UserToken))
  14.               using (SPWeb impWeb = impSite.OpenWeb())
  15.               {
  16.                   var impCtx = SPContext.GetContext(impWeb);
  17.  
  18.                   DisplayPreference(impCtx);
  19.               }
  20.           });
  21.     }
  22. }

Of course, if your code runs in a SharePoint process, you can get the context as SPContext.Current as well for the current user.

The same information is available via PowerShell either. For example, displaying preferences for the current user:

  1. $web = Get-SPWeb $url
  2. $ctx = [Microsoft.SharePoint.SPContext]::GetContext($web)
  3.  
  4. # shortcut for UserPreference
  5. $up = [Microsoft.Office.Server.Search.Administration.UserPreference]
  6. # shortcut for the nested class Settings in UserPreference
  7. $ups = [Microsoft.Office.Server.Search.Administration.UserPreference+Settings]
  8.  
  9. $pref = $up::GetUserPreference($false, $ctx)
  10. $pref.IsSettingEnabled($ups::ShowPrequerySuggestion)
  11. $pref.IsSettingEnabled($ups::ShowPersonalSuggestions)
  12. $pref.IsSettingEnabled($ups::OpenDocumentsInClient)

If you need the preferences of another user, you should impersonate it first as described here. After the impersonation, the code is pretty the same as earlier:

  1. $userName = 'i:0#.w|domain\user'
  2.  
  3. $web = Get-SPWeb $url
  4. $user = $web.EnsureUser($userName)
  5. $userToken = $user.UserToken
  6.  
  7. $impersonatedSite = New-Object Microsoft.SharePoint.SPSite($url, $userToken)
  8. $ctx = [Microsoft.SharePoint.SPContext]::GetContext($impersonatedSite.RootWeb)
  9.  
  10. # shortcut for UserPreference
  11. $up = [Microsoft.Office.Server.Search.Administration.UserPreference]
  12. # shortcut for the nested class Settings in UserPreference
  13. $ups = [Microsoft.Office.Server.Search.Administration.UserPreference+Settings]
  14.  
  15. $pref = $up::GetUserPreference($false, $ctx)
  16. $pref.IsSettingEnabled($ups::ShowPrequerySuggestion)
  17. $pref.IsSettingEnabled($ups::ShowPersonalSuggestions)
  18. $pref.IsSettingEnabled($ups::OpenDocumentsInClient)

Using this code we were able to detected that the complaining user has really the wrong preference (OpenDocumentsInClient was false). Now we had two choices: either to call the user, and ask him to change the preference, or to find a solution, how it would be possible to change it from code on behalf of the user remotely. Of course, this time we didn’t want to change the preferences without the explicit permission of the user, so took option 1, but I show you in my next post, how you could do it from code.

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 }

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.