Tuesday, 6 March 2012

Extending the SharePoint Search Box (SearchBoxEx) webpart

I've just finished extending SharePoints Search Box (SearchBoxEx) webpart. I had requirements for three different search boxes (wildcarding the people search, adding a custom properties drop down box for advanced searching and adding an extra button for an optional people search).

The process for all three was largely the same. The method below outlines how I created the multi-button search box, embedded in our master page and looks like this:




The basic steps are;
1. Create a new webpart project and inherit from the SearchBoxEx class
2. Add an ImageButton that calls a (client side) javascript function
3. Copy, rename and modify the javascipt function that the oob webpart calls
4. Create the webpart.dwp file
5. Deploy the solution

This is what I did;
1. Create a new Empty SharePoint project
2. Add a reference to Microsoft.SharePoint.Portal
3. Add a new webpart to the project
4. In the webparts class file, adding the using statement and inherit the SearchBoxEx (Search Box webpart) class (See MSDN for more information)

using ...
using Microsoft.SharePoint.WebControls;

namespace Ince.SearchParts.MultiMiniSearchBox{
[ToolboxItemAttribute(false)]
public class MultiMiniSearchBox : Microsoft.SharePoint.Portal.WebControls.SearchBoxEx
{
...
} 

5. Add the ImageButton and initialise it.

private ImageButton _peopleSearch;
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
_peopleSearch = new ImageButton();
_peopleSearch.ID = "peopleMiniSearch";
_peopleSearch.EnableViewState = false;
_peopleSearch.CausesValidation = false;
_peopleSearch.ImageUrl = "/_layouts/images/ince/isqdepartment.png";
_peopleSearch.Height = new Unit(19, UnitType.Pixel);
_peopleSearch.CssClass = "isqPeopleSearchImg";
} 

6. Add two properties for the results pages (the search results page and the people search results page).

private string _peopleResultsUri = String.Empty;
[WebBrowsable(true), WebDisplayName("People Results Url"), Personalizable(PersonalizationScope.Shared), Category("Webpart Settings")]
public String PeopleResultsUri
{
get { return _peopleResultsUri; }
set { _peopleResultsUri = value; }
}

private string _searchResultsUri = String.Empty;
[WebBrowsable(true), WebDisplayName("Search Results Url"), Personalizable(PersonalizationScope.Shared), Category("Webpart Settings")]
public String SearchResultsUri
{
get { return _searchResultsUri; }
set { _searchResultsUri = value; }
} 

7. Add code to the CreateChildControls method to set the search results url properties.

Because I'm primarly using this webpart in the master page (and unable to set the properties from the toolpart pane), I'm looking for the results page URL in two locations. The first is the webparts properties (settable via the toolpart pane). If this is empty, then I'm checking the root webs property bag for the URL.

I'm also configuring the other settings for the webpart, like the scope, CSS, chrome, etc.

protected override void CreateChildControls()
{
base.CreateChildControls();
EnsureChildControls();
String peopleResultsUrl = this.SearchResultPageURL;
String searchResultsUrl = this.SearchResultPageURL;
try
{
var rootWeb = SPContext.Current.Site.RootWeb;
if (PeopleResultsUri != String.Empty)
{
peopleResultsUrl = PeopleResultsUri;
}
else
{
if (rootWeb.Properties.ContainsKey("inceIsqPeopleResultsUri"))
{
peopleResultsUrl = rootWeb.Properties["inceIsqPeopleResultsUri"];
}
}
if (SearchResultsUri != String.Empty)
{
searchResultsUrl = SearchResultsUri;
}
else
{
if (rootWeb.Properties.ContainsKey("inceIsqSearchResultsUri"))
{
searchResultsUrl = rootWeb.Properties["inceIsqSearchResultsUri"];
}
}
}
catch (Exception exception)
{
SPDiagnosticsService.Local.WriteTrace(0, HighCategory(), TraceSeverity.High, 
String.Format("[CreateChildControls] Unhandled Exception getting results page urls. Error: {0}", 
    exception.Message), "");
}
_peopleSearch.Style.Clear();
_peopleSearch.OnClientClick = String.Format("javascript:isqPeopleSearch('{0}', '{1}');return;", 
    m_searchKeyWordTextBox.ClientID, peopleResultsUrl);
Controls.Add(_peopleSearch);
this.UseSiteDropDownMode = false;
this.SearchResultPageURL = searchResultsUrl;
this.AdvancedSearchPageURL = searchResultsUrl;
this.ChromeType = PartChromeType.None;
this.GoImageActiveUrl = this.GoImageUrl;
this.CssClass = "isqSearchBoxExOveride";
this.DropDownMode = DropDownModes.HideScopeDD;
this.DropDownModeEx = DropDownModesEx.HideScopeDD;
this.QueryPromptString = String.Empty;
} 

8. Attach the CSS and JavaScript

protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
CssRegistration.Register("/_layouts/styles/ince/ince-searchparts.css?v2");
ClientScriptManager cs = Page.ClientScript;
if (!cs.IsStartupScriptRegistered("incesearchparts"))
{
cs.RegisterStartupScript(this.GetType(), "incesearchparts", 
    "<script type='text/javascript\' src='/_layouts/incejs/ince-searchparts.js?v2'></Script>");
cs.RegisterClientScriptInclude(this.GetType(), 
    "incesearchparts", "/_layouts/incejs/ince-searchparts.js?v2");
}
} 

9. Add the JavaScript function

The OOB search button calls a JavaScript function, which in turn calls the GoSearch(...) found in search.js

I pulled this function apart and copied out the bits I needed, then modified it to do what I needed (which was simply to wildcard the keyword if the people search button was clicked). This is what I ended up with:

function isqPeopleSearch(keywordElement, resultsPage)
{
var i = document.forms[0].elements[keywordElement].value;
i = i.replace(/\s*$/, "");
var b = "?s=People";
if (i == "")
{
alert("Please enter one or more search words.");
if (null != event) {
event.returnValue = false; return false
}
else
return;
}
b += "&k="+i+"*";
window.location = resultsPage + b;
try {
if (null != event)
event.returnValue = false
}
catch (s) {
}
return
} 

10. Create the dwp file.

Before deploying the solution and adding the webpart, you'll need to create a .dwp webpart description file, and modify the elements.xml file.

10.1 Elements XML

Open the elements.xml file, and change the extension of the webpart file from .webpart to .dwp. My elements.xml file now looks like this:

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/" >
<Module Name="MultiMiniSearchBox" List="113" Url="_catalogs/wp">
<File Path="MultiMiniSearchBox\MultiMiniSearchBox.dwp" 
    Url="MultiMiniSearchBox.dwp" Type="GhostableInLibrary">
<Property Name="Group" Value="Ince Search" />
</File>
</Module>
</Elements> 

10.2 Create the dwp file

Right click the webpart and select Add. Add new XML file, giving it the same file name used in the elements.xml file (i.e. MultiMiniSearchBox.dwp)

Now add all the webpart properties to the xml file (make sure you get the Assembly and TypeName correct). There's other documentation and blogs on how to do this, but my slimmed down version was:

<?xml version="1.0" encoding="utf-8"?>
<WebPart xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
    xmlns="http://schemas.microsoft.com/WebPart/v2">
<Title>Multi Min Search Box</Title>
<FrameType>None</FrameType>
<Description>Displays a search box that allows users to search for information or people.</Description>
<IsIncluded>true</IsIncluded>
<IsVisible>true</IsVisible>
<HelpMode>Modeless</HelpMode>
<Dir>Default</Dir>
<MissingAssembly>Cannot import this Web Part.</MissingAssembly>
<Assembly>Ince.SearchParts, Version=1.0.0.0, Culture=neutral, PublicKeyToken=e69a99a27d408090</Assembly>
<TypeName>Ince.SearchParts.MultiMiniSearchBox.MultiMiniSearchBox</TypeName>
<ShouldTakeFocusIfEmpty xmlns="urn:schemas-microsoft-com:SearchBoxEx">true</ShouldTakeFocusIfEmpty>
</WebPart> 

10.3 Change the Deployment Type for the new .dwp file

Click the new .dwp file in Solution Explorer, and view the properties. Change the Deployment Type to ElementFile.

10.4 Delete the old .webpart file

11. Deploy the solution