Changing Microsoft Office SharePoint Portal Server 2003 Search Results
Overview This article intends how to use the override functionality to deliver up your own rendered search results.
Typical Problem
I see lots of people asking questions on how to customise the Microsoft Office SharePoint Portal Server 2003 search results page with the typical question being “How can I edit the Search.ASPX file?”
When trying to modify the Search Results rendered by SPS2003 there are some concepts and information that you should be aware of:
- The Search Results page is actually made up of 2 web parts out of the box on a provisioned portal.
- The first web part is the Search Constructor and is responsible for building the required query string for the user in order to be able to execute the query against the SPS index.
- The second web part is called the Search Results web part and this is where the returned results from the index are rendered and displayed to the user. This web part also controls what grouping is available on the navigation bar.
To be able to view or manipulate these web parts, you need to enter the following command into your browser of choice (replacing <SPSserver> with your SPS server name and you must have the necessary permissions at the portal level)
http://<SPSserver>/search.aspx?mode=edit&PageView=Shared
This will expose the Modify Shared Page link at the top right of the page

Now by selecting “Design this page”, you will see the 2 default web parts that are deployed at portal provisioning time.
Displaying your own Search Results
To be able to change the way the search results are rendered to the user, you have to override the default Search Results web part.
Some sketchy information can be found at: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/spptsdk/html/cSearchResults.asp
This shows you the SearchClass that you can you programmatically override.
Sample
This sample is quite non-invasive into the Search Results. It just adds a new button as you can see below:

By selecting “Item parent” at 1, will display the parents’ site listing: (This is the only site collection in this instance of a portal)

By selecting “Item parent” at 2, will display the document library that the documents live in;

Show me the code
On lots of sites, I can normally find descriptive information on what I need to do but I always think that the best way to demonstrate a concept or approach is through the use of some sample code that you can take and modify for yourself.
Below is some code I wrote in C# that shows how to override the default search results and add a new button at the Actions level for a search result hit. The button enables the user to go to the parent level for particular items such as Documents (will go to the Documents Document Library), List Items (will go to the List Items List) and Sites (will go to the Sites parent Site)
using System;
using System.ComponentModel;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Xml.Serialization;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Utilities;
using Microsoft.SharePoint.WebPartPages;
using System.Data;
using System.Text;
namespace SearchResultExtension
{
// <summary>
// This webpart has been developed to enable users to be able to navigate
// easier to a particular items parent container.
// Items that are supported by this functionality are:
// . Documents within SharePoint
// . Lists within SharePoint
// . SharePoint Sites
// . Files trawled from NTFS (file:// prefix'd)
// </summary>
[DefaultProperty("Text"),
ToolboxData("<{0}:Override runat=server></{0}:Override>"),
XmlRoot(Namespace="SearchResultExtension")]
public class Override : Microsoft.SharePoint.Portal.WebControls.SearchResults
{
// Use the following constants to manage what pages are rendered for which result type
const string c_DSP_SiteCollection = "/SiteDirectory/Lists/Sites/AllItems.aspx";
const string c_DSP_SubWeb = "/_layouts/1033/mngsubwebs.aspx?view=sites";
const string c_DSP_Lists = "/_layouts/1033/viewlsts.aspx";
//
// The following constant defines the default button name
const string c_ButtonDefault = "Item parent";
//
private string _button;
public Override()
{
_button = c_ButtonDefault; //Initialise private variables
}
[Category("Custom Properties")] //Create a custom category on the property sheet.
[DefaultValue(c_ButtonDefault)] // Assign the default value.
[WebPartStorage(Storage.Personal)] // Property is available in both Personalisation and Customisation mode.
[FriendlyNameAttribute("Items' parent button display name.")] // The caption that appears in the property sheet
[Description("Type the parent button name.")] // The tool tip that appears when pausing the mouse pointer
[Browsable(true)] // Display the property in the property pane.
[XmlElement(ElementName="Button")] // The accessor for this property.
public string Button
{
get
{
return _button;
}
set
{
_button = value;
}
}
//
//********************************************************************************
//Description:
// This webpart is built to override the standard default out of the box Search
// Results webpart functionality. The part adds a new option on the search
// results page to be able to navigate to specific items parent folder.
//
//Author:
//
//
//Date:
// ??th October 2004
//
//Version:
// V1.0.0.0
//
//Modifications:
// Who When Why
// NB **/**/** Initial webpart creation.
//
//********************************************************************************
//
protected override void GenerateHtmlOneRowForOneItem(
System.Data.DataRow oneDataRow,
System.Text.StringBuilder sbRenderRowHtml,
int rowID,
string strStyleClass,
int iIndexOfItemInDataSet,
int iIndexOfItemInGroup
)
{
base.GenerateHtmlOneRowForOneItem(oneDataRow,
sbRenderRowHtml,
rowID,
strStyleClass,
iIndexOfItemInDataSet,
iIndexOfItemInGroup);
if (rowID == 3) //This is the row where the actions are!
{
string sHref = GetRowValue(oneDataRow, "DAV:href"); //Get the URL for the item
string sFCC = GetRowValue(oneDataRow, "DAV:contentclass"); //See what sort of item it is
if (sFCC.StartsWith("STS_")) //It is a SharePoint internal item
{
sHref = sHref.Substring(0, sHref.LastIndexOf("/")); //Format the URL to remove the filename
switch (sFCC) //We need to do different things to different items to render the parent
{
case "STS_Site": //Site Collection
sHref = sHref.Substring(0, sHref.LastIndexOf("/")) + c_DSP_SiteCollection;
break;
case "STS_ListItem_300": //Site collection but to be treated differently
sHref = sHref.Substring(0, sHref.LastIndexOf("/")) + c_DSP_SiteCollection;
break;
case "STS_Web": //WSS Subweb
sHref = sHref + c_DSP_SubWeb;
break;
case "STS_ListItem_DocumentLibrary": //List item within a Document Library
break;
case "STS_List_DocumentLibrary": //WSS Document Library
string sListDisp = GetRowValue(oneDataRow, "DAV:displayname"); //Get the displayname
sHref = sHref.Substring(0, sHref.LastIndexOf("/" + sListDisp + "/")) + c_DSP_Lists;
break;
case "STS_List_Announcements": //WSS List
sHref = sHref.Substring(0, sHref.LastIndexOf("/Lists/")) + c_DSP_Lists; //Need to cut off /Lists also
break;
case "STS_Document": //A SharePoint specific file. Do not touch
return;
default: //Anything else, ignore
return;
}
if (sHref.Length > 7) //Bigger than "https://" i.e. Make sure we have something to replace with
{
string sSrcResPage = sbRenderRowHtml.ToString(); //HTML generated so far
string sAnchor = " | <A href=\"" + SPEncode.UrlEncodeAsUrl(sHref) + "\">" + this._button + "</A>"; //Create new string
int iInsert = sSrcResPage.LastIndexOf("</A>"); //Last occurrence of the string
if (iInsert > 0) //if we've found the search string at all...
{
//insert the new button into the HTML at the right place
sbRenderRowHtml.Replace(sSrcResPage, sSrcResPage.Insert(iInsert + 4, sAnchor));
}
}
}
else if (sHref.StartsWith("file://")) //on an external file
{
sHref = sHref.Substring(0, sHref.LastIndexOf("/")); //Format the URL to remove the filename
string sSrcResPage = sbRenderRowHtml.ToString(); //HTML generated so far
string sAnchor = " | <A href=\"" + SPEncode.UrlEncodeAsUrl(sHref) + "\">" + this._button + "</A>"; //Create new string
int iInsert = sSrcResPage.LastIndexOf("</A>"); //Last occurrence of the string
if (iInsert > 0) //if we've found the search string at all...
{
//insert the new button into the HTML at the right place
sbRenderRowHtml.Replace(sSrcResPage, sSrcResPage.Insert(iInsert + 4, sAnchor));
}
}
}
}
private string GetRowValue(DataRow dbRow, string strUri)
{
DataColumnCollection rowColums = dbRow.Table.Columns;
if (rowColums.Contains(strUri)) return dbRow[strUri].ToString();
else return "";
}
}
}
How the code works
The class is set to Override and class inherits from
Microsoft.SharePoint.Portal.WebControls.SearchResults.
Beneath the class definition, a number of constants are exposed. These constants can be used to direct what pages are displayed for particular items. For example, a WSS List can be returned in the search results and the parent container for this item is really just the site where it resides. However, SharePoint has a number of pages that render just the lists of the sites that presents a much more intuitive container.
const string c_DSP_SiteCollection = "/SiteDirectory/Lists/Sites/AllItems.aspx";
This is used when a site collection item is returned by the search. It directs the browser to the portals SiteDirectory listing view
const string c_DSP_SubWeb = "/_layouts/1033/mngsubwebs.aspx?view=sites";
This is used when a sub web is returned in the search results. Note that this site is not at the site collection level. The view that is used is for the parent site of the subweb and the subweb collection for the parent site is displayed.
const string c_DSP_Lists = "/_layouts/1033/viewlsts.aspx";
This is used when a list is found in the search results and points the browser to the list view for the parent site.
Then, the code looks to set up the custom property for the button name. The default string is maintained by the logical const string c_ButtonDefault = "Items parent";.
The custom button property is then set up by the following code snippet. This section controls the page where the control is displayed (Category property) and the button description, FriendlyNameAttribute:
private string _button;
public Override()
{
_button = c_ButtonDefault;
}
[Category("Custom Properties")]
[DefaultValue(c_ButtonDefault)]
[WebPartStorage(Storage.Personal)]
[FriendlyNameAttribute("Items' parent button display name.")]
[Description("Type the parent button name.")]
[Browsable(true)]
[XmlElement(ElementName="Button")]
public string Button
{
get
{
return _button;
}
set
{
_button = value;
}
}
The code then defines the override function that is the main driver of the web part:
protected override void GenerateHtmlOneRowForOneItem(
System.Data.DataRow oneDataRow,
System.Text.StringBuilder sbRenderRowHtml,
int rowID,
string strStyleClass,
int iIndexOfItemInDataSet,
int iIndexOfItemInGroup
)
This is the routine that is called by default by the SharePoint process on a result list of search items so that they can be rendered by the Search Results web part. The data row, oneDataRow contains the data that needs to be formatted.
The search page is built up from the hits that are returned, to a maximum of 40 items per page by default, and this data is organised by oneDataRow. For each of the hits, the GenerateHtmlOneRowForOneItem is called. When this is called, the string builder, sbRenderRowHtml, is appended to with the current row information from oneDataRow. So, we allow the base call to execute that would add the standard buttons such as Add to My Links | Alert Me | Item details. We then perform some tests to see if we are working on a known object type and should therefore be adding the Item Parent button.
The first test is just to see if we are actually on the correct row for the buttons. This is known as row 3 and contained within the integer rowID. If the row is anything but 3, we exit from the code and allow SharePoint to continue as normal.
If the rowID is 3, then we pull the items’ HRef property from oneDataRow. The HRef property is passed in and contains the full URL to the item that is currently being rendered. We must remember that all items known to SharePoint are URL addressable.
We extract the property from the data row by passing it, along with the property we are looking for, DAV:href, to another routine called GetRowValue
The routine passes back the property asked for, DAV:href, and it is assigned to the string sHref.
We also then use GetRowValue but request the value for DAV:contentclass. The contentclass of the object tell us whether or not we should be handling the item at all as we only want to act on known object types.
To see if we may need to add the button, we perform a test on the first 4 characters of the content class. If the item begins with STS_ then it is a SharePoint object and we may need to update with our new button. If the contentclass does not begin with STS_ then we may still need to add our button as long as the sHref string begins with file://. If neither of these conditions is met, then the routine is exited and normal SharePoint functionality continues.
Next we truncate sHref from the last “\”. This is in effect dropping the filename or object name from the string so that we have a more representative URL for the parent object.
Now we perform a switch test on the contentclass and we are looking for:
- STS_Site: and STS_ListItem_300: Both of these content classes relate to a site collection level site and will be prefixed by /sites by default. We need to remove this part of the URL to get to the portal URL and so we again truncate from the last “/”. We strip off the /sites term and then append the string held by c_DSP_SiteCollection. Note that /sites is the default string but this can be some other configured string.
- STS_Web: This is a normal WSS subweb. As we already have the parent URL in sHref, we just need to append to constant c_DSP_SubWeb to modify the URL to point to the site list for the parent.
- STS_ListItem_DocumentLibrary: No requirement to modify the URL here as it is full and already correctly formatted in sHref.
- STS_List_DocumentLibrary: This is a WSS Document Library listing and we need to perform more tests to ensure that we get the correct URL set. A call is made to GetRowValue and we ask for the DAV:displayname of the list. This display name will be contained within the URL. We then format the sHref, truncating from the appearance of the display name and append c_DSP_Lists
- STS_List_Announcements: This is the same operation as above but this needs to work on a different list type and is always noted by /Lists
- STS_Document: This is a SharePoint internal file that is used to display the portal and it is not sensible or appropriate to do anything with it, so we would return out of the routine on this condition.
Then, a check if made on the length of the formatted URL in sHref just to ensure that it contains a valid address.
At this stage, we have a formatted string that we want to insert into the string builder that will be used for rendering the results to the user. This string is stored in sbRenderRowHtml. To be able to perform string manipulations on it, we coerce the string builder type into a string called sSrcResPage
Another string is created called sAnchor, and this will contain the formatted HTML codes for our new button. The string is built up using normal HTML code and the sHref that has been built up as the target of the anchor tag. UrlEncodeAsUrl is a standard routine available in the SharePoint OM to replace particular characters with their printable form. this._button uses the button name that we have configured for it.
A search is then performed on the string version of the page being built stored in sSrcResPage for the particular </A> string which denotes the end of the last button. This will return to us the last position in the string for the current item, of the last button. We can then add our newly formatted button stored in sAnchor after it.
Sample DWP file
You can use this sample to build a DWP for the web part. (This assumes a strongly named assembly so you would have to update with your own projects details.)
<?xml version="1.0"?>
<WebPart xmlns="http://schemas.microsoft.com/WebPart/v2">
<Description>This webpart displays the ability to goto particular items parent container</Description>
<Assembly>SearchOverride, Version=1.0.0.0, Culture=neutral, PublicKeyToken=…………….</Assembly>
<TypeName>SearchResultExtension.Override</TypeName>
<Title>Search Results Override</Title>
<FrameType>None</FrameType>
<ResultListID xmlns="urn:schemas-microsoft-com:sharepoint:DataResultBase">sch</ResultListID>
<MaxMatchingItemsNumber xmlns="urn:schemas-microsoft-com:sharepoint:DataResultBase">40</MaxMatchingItemsNumber>
<Button xmlns="SearchResultExtension">Items parent</Button>
</WebPart>
Happy overriding!