Second Life of a Hungarian SharePoint Geek

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.

Advertisements

Leave a Comment »

No comments yet.

RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Create a free website or blog at WordPress.com.

%d bloggers like this: