SharePoint Custom Search Results Page






2.95/5 (11 votes)
Jun 29, 2006
11 min read

215719

1412
An article on how to create a custom search results page in a Windows SharePoint Services site.
Introduction
The built-in Windows SharePoint Services search results page (SearchResults.aspx) is rather limited in the document metadata that it displays in the result list. Furthermore, the format of the list is not what every client wants. My client wanted lots of document metadata displayed in a "table", and only wanted results from a single document library in the site. So, I created a custom search page to implement this functionality. To display the results, I used a DataGrid
, but you could use any formatting you want.
This example shows how to create a custom search results page that will display any metadata available for a document. It is totally flexible in that it gets the metadata fields to display from a client-configured document library view. This means that if the client decides s/he wants more (or fewer) categories to display, or wants to change their order, the results page will automatically adjust when the document library view is changed. This solution uses a simple Content Editor web part with some client-side HTML and JavaScript to invoke the search. This leaves the original search box and search results page for normal site-wide searches (which includes multiple libraries and lists).
Summary of Features:
- The search form is implemented as a simple Content Editor web part, containing only client-side HTML and JavaScript.
- The search itself is implemented by creating a custom search results page, based on the pre-existing SearchResults.aspx, which uses the built-in SharePoint search functionality and manipulates the results to display more metadata than the standard search results page.
- The metadata to display, and the order to display it in, can be changed using a standard SharePoint view on the document library to be searched.
- The search results page provides a form field for immediate re-search without having to return to the originating page.
- Results can be sorted by clicking on any column header.
- Clicking on the document title itself (or the icon) takes the user to the document’s property page.
- Built-in search functionality is not affected.
Sample results page:
Background
In order for you to understand and apply the contents of this article, you need to know your way around SharePoint - both in custom development, and in standard configuration. In particular, you need to know how to create a custom view for a document library, and how to add and configure a content editor web part. These are standard configuration processes not requiring any hard-core development.
This article also assumes you know a little bit about the SharePoint object model.
Using the Code
There are three main steps to implementing this solution: creating the custom search aspx page, configuring a "view" on a SharePoint document library, and adding a Content Editor web part to a SharePoint page. We will cover them in decreasing order of complexity.
The ASPX Page
To begin, make a copy of the original search results page. In a default installation, this is found here: C:\Program Files Common Files\Microsoft Shared\web server extensions\60\TEMPLATE\LAYOUTS\1033.
You can give it any name you want, such as CustomSearchResults.aspx. By copying the original, you get all the "chrome" that should be on a standard SharePoint page, to ensure your page looks like part of the site. Be aware that a re-install of SharePoint will likely overwrite this directory, wiping out your file, so be sure to save a backup somewhere!
All of our code has to go in the aspx page itself (no code-behind), since we can't rebuild SharePoint as a web app.
The page will read two values from the query string (passed from our custom search form on another page). These values are:
CRSearchString
- the string the user wants to search forTarget
- the fully qualified URL of the site to be searched (current site)
Once you've created a copy of the page, do the following:
- Modify page directives/includes:
EnableViewState
must be set toTrue
- Remove the “
Inherits From…
” - Add references to the
System
,System.Collections
,System.Data
, andSystem.Web
namespaces
- Strip out all server side code and controls
- Add a server-side
DataGrid
control (or whatever list format you desire for the results) - In a server-side script block, do the following:
- Get querystring values
- Create
DataGrid
columns using the fields in the “Search Results” view - Invoke the search on the document library (using querystring values)
- Filter out results not from the Content Repository library
- Bind the results to the
DataGrid
columns - Specify the sort method
- Add a client-side text box and button, with the same format as in the calling content editor web part
- In the client-side JavaScript, insert the same code as in the calling content editor web part, for re-search
Let's go into more details on step 4, shall we? There's actually a lot more happening in that server-side code block!
There are four server-side methods: Page_Init
, Page_Load
, BindData
, and DataGrid1_Sort
. We shall discuss each one in detail.
Page_Init Method
This method is used to dynamically create the DataGrid
columns from the fields found in the search results view. Using the Target
parameter of the querystring, it creates a SPWeb
object pointing to the current site. Then, using the the Lists
collection of the SPWeb
object, we get a reference to the document library to search. I've hard-coded the library name, but you could just as easily pass in the name of the document library as another querystring parameter. Then, we get the fields from the search results view. It is the sort view that determines the metadata to be displayed and its order - making the results format completely dynamic.
One thing to note is that the fields collection returns the internal field name - not the display name. The internal name is perfect for manipulating the DataGrid
rows, but not what we want the user to see as column headers. So there's a bit of extra work to get the display name of each field in the DataGrid
columns.
Another important item of note regards the sorting capability of the DataGrid
. Because the Title field is formatted with the HTML anchor tag for the document (using the document ID in the URL), if you sort on this column, the grid ends up being sorted by document ID, not by title. So a hidden column allows sorting by the actual document title, not the URL.
Here's the Page_Init
method:
void Page_Init(Object Sender, EventArgs e)
{
//Get reference to site from querystring
string url = Request.QueryString["Target"].ToString();
SPSite siteCollection = new SPSite(url);
SPWeb CRsite = siteCollection.OpenWeb();
//get reference to target document library
SPListCollection AllLists = CRsite.Lists;
AllLists.IncludeRootFolder = true;
//you could pass this in querystring
SPList ContRep = AllLists["DocLibName"];
//get field objects from Content Repository
// - needed to obtain display name of fields
SPFieldCollection ListFields = ContRep.Fields;
//get field names used in search results view
//(these are "internal" names, not display names)
SPView SRView = ContRep.Views["Search Results"];
SPViewFieldCollection SRViewFields = SRView.ViewFields;
System.Collections.Specialized.StringCollection
InternalViewFldNames = SRViewFields.ToStringCollection();
//using internal names, build the datagrid columns, in field order
BoundColumn C = null;
string str="";
for (int i=0; i< InternalViewFldNames.Count; i++)
{
C = new BoundColumn();
if (i>0) //no header for doc icon
{
C.HeaderText = ListFields.GetFieldByInternalName(
InternalViewFldNames[i]).Title;
//this is the DISPLAY name
//change "name" column to be "file name"
if (C.HeaderText == "Name")
C.HeaderText = "File Name";
if (C.HeaderText == "Title")
C.SortExpression = "SortByTitle";
}
C.DataField = InternalViewFldNames[i];
C.ItemStyle.VerticalAlign = VerticalAlign.Top;
DataGrid1.Columns.Add(C);
}//end for loop
//add hidden column for sorting by title (NOT URL of title!)
C = new BoundColumn();
C.HeaderText = "SortByTitle";
C.SortExpression = "SortByTitle";
C.DataField = "SortByTitle";
C.Visible = false;
DataGrid1.Columns.Add(C);
}//end page_init
BindData Method
The BindData
method is the real "workhorse" of the server-side code. It does the following:
- Invokes the search, passing the
CRSearchString
from the querysting - Builds a
DataTable
from the search result items - Creates a
DataView
from theDataTable
- Applies the sort order (if any)
- Binds the
DataView
to theDataGrid
It takes one parameter: sortOrder
(string). If it's not an empty string, this contains the data column name to perform the sort on.
Let's look at this method in more detail:
The search is performed by invoking the SearchDocuments
method of the site object. Results are returned in a SPSearchResultCollection
:
//get search string from querystring
string SearchString = Request.QueryString["CRSearchString"].ToString();
SearchString = Server.UrlDecode(SearchString);
lblSearchString.Text = SearchString;
//Invoke document search with string passed in
SPSearchResultCollection Results =
CRsite.SearchDocuments(SearchString);
We loop through this collection, saving the document URL (which contains the document ID, to be used later) and the URL to the document icon (to be displayed in the grid). Because the search will have returned items from throughout the site, and we only wanted to search documents in a particular document library, we only keep items that have the specified document library address in their URL:
//place URL of each doc in arraylist for later processing
string itemURL = "";
foreach(SPSearchResult item in Results)
{
itemURL = item.Url.ToString();
if (itemURL.IndexOf("/sitename/documentlibraryname/") > 0)
//only keep docs in target library
{
//increment actual result count
ActualResultCount++;
//save document URL
DocURLs.Add(itemURL);
//save doc icon URL
IconURLs.Add(item.IconUrl.ToString());
}
}//end foreach item in search results collection
Next, we obtain the fields specified in the search results view for the specified document library, and loop through them to build our table to hold the results:
//get field objects from list (which contain
//the display names of the fields)
SPFieldCollection ListFields = ContRep.Fields;
//get field names used in search results view
//(these are "internal" names, not display names)
SPView SRView = ContRep.Views["Search Results"];
//"Search Results" is the name of the doc lib view
SPViewFieldCollection SRViewFields = SRView.ViewFields;
System.Collections.Specialized.StringCollection
InternalViewFldNames = SRViewFields.ToStringCollection();
//string s="";
//process each view field, building results table columns
for (int j=0; j< InternalViewFldNames.Count; j++)
{
ResultsTable.Columns.Add(new DataColumn(
InternalViewFldNames[j], typeof(string)));
//s += InternalViewFldNames[j] + " ";
}
//add the hidden column
ResultsTable.Columns.Add(new DataColumn("SortByTitle",
typeof(string)));
We then loop through the document URL array list to obtain the document ID, use that to get the actual document object, extract the needed metadata, build a results row, and add it to a results table:
//for each doc url in array, get document object
//and build results row
for (int k=0; k< DocURLs.Count; k++)
{
//using URL, get id of item to retrieve item object
//URL is in this format: http://server/sitecollection/currentsite/
documentlibraryname/Forms/DispForm.aspx?ID=nn
strURL = DocURLs[k].ToString();
//extract only the digits after "?ID=" or "&ID="
Regex exp1 = new Regex(@"[\?|&]ID=([0-9]+)");
if (!exp1.IsMatch(strURL))
//ignore any file w/o and ID, since it's not
//a list item (some web pages in forms library might match)
{
continue;
}
ID = exp1.Matches(strURL)[0].Groups[1].Value; //save the ID
//get the item object using the ID just obtained
ThisItem = ContRep.GetItemById(Convert.ToInt32(ID));
//create new row for item
ResultsRow = ResultsTable.NewRow();
//for each view field name, fill results row with item metadata
for (int j=0; j< InternalViewFldNames.Count; j++)
{
//use the internal field name to obtain the title
ResultsRow[InternalViewFldNames[j]] =
ThisItem[ListFields.GetFieldByInternalName(
InternalViewFldNames[j]).Title];
strField = ResultsRow[InternalViewFldNames[j]].ToString();
//some fields may have "x;#" at the beginning - strip
//it off as well as any others
if (strField.IndexOf("#") >=0)
{
//get's the first occurrence
strField = strField.Substring(strField.IndexOf("#")+1);
//get rid of any other #'s, leaving the ";"
strField = strField.Replace(";#", ";");
ResultsRow[InternalViewFldNames[j]] = strField;
}
}//end foreach field name
//customize "special columns":
//make icon row display an image
ResultsRow["DocIcon"] = "< a href='" + strURL +
"'>< img src='" +
IconURLs[k].ToString() +
"' border=0 ></a>";
//make doc name link to doc property page
ResultsRow["Title"] = "< a href='" + strURL + "'>" +
ThisItem["Title"] + "</a>";
//set value of sorting row to the actual document title
ResultsRow["SortByTitle"] = ThisItem["Title"].ToString();
//add row to table
ResultsTable.Rows.Add(ResultsRow);
}//end for each docURL
There are a couple of things to note in the above code. First, multi-valued fields have their values in a string, separated by "x;". Even if there is only one value, it will begin with those characters. So we strip those out, but leave a semicolon in between values. Secondly, there are three "special" columns that require some extra tweaking:
- Document icon - We have to format it so that it will display the actual image, and display it as a link.
- Document title -It must be hyperlinked to the document property page (which is why it can't be the value that we do the sort on).
- Sort by title - This is the hidden column, which contains simply the document title, and it is used when the user clicks on the "Title" row, to sort the grid by title.
The last bit of code in this method places the DataTable
into a DataView
, applies the sort (if any) to the DataView
, and then binds the DataView
to the DataGrid
.
Page_Load Method
This method is very simple. All it does is call the BindData
method with an empty sort parameter, when the page first loads.
DataGrid1_Sort Method
The last method in the server-side code block is the method to apply the sort. It simply calls BindData
and passes the name of the column to sort on (whichever column the user clicked on):
void DataGrid1_Sort(object source,
System.Web.UI.WebControls.DataGridSortCommandEventArgs e)
{
BindData(e.SortExpression);
}
Client-side Custom Code
The only other custom code on the page is the client-side JavaScript to allow the user to re-search. This is the same code you place in the content editor web part on the page to start the search process. It's very straightforward:
function DoCRSearch()
{
//get value of textbox
//CRSearchString is the name of the textbox
var obj = document.getElementById("CRSearchString");
var strSearch = obj.value;
//make sure we don't have empty string
if (strSearch == "")
alert("Please enter a search term.");
else
{
//URL encode search string
strSearch = escape(strSearch);
//build target URL for site
var loc = document.location.href;
var baseURL = loc.substring(0,loc.lastIndexOf("/"));
var strTarget = baseURL + "/default.aspx";
//redirect to search page, adding on target URL
//and search string as querystring parameters
document.location.href = baseURL +
"/_layouts/1033/searchresultsCR.aspx?CRSearchString=" +
strSearch + "&Target=" + strTarget;
}//end else
}//end function
Note that if you place this code on a standard SharePoint page, don't name the textbox "SearchString". This is the name of the textbox used by the built-in SharePoint search box, and the function will grab it, instead!
To use this code, insert it in a client-side script block. Then, place an HTML textbox and button on your page, and wire up the button to call the above method. Do not place these in a client-side <form>
tag as you normally would. (You can't use client-side <form>
tags on SharePoint pages.) This is not well-formed HTML, but it does work.
Because this custom page has all of the HTML for the "chrome" of your site, your custom code (the HTML, the DataGrid
, and whatever else you want to place on your page) goes in a table cell near the bottom of the page. The sample aspx page included with this article has this section clearly marked.
Document Library View
Creating a view on a SharePoint document library is very simple. From the document library page, click the "Modify Settings and Columns" link in the left-hand action menu. On this page, you can specify the metadata for the documents, and at the bottom of the page is the link to allow you to create a new view or modify an existing view. You can select the columns to be displayed, and also specify their order. Create a view called "Search Results" (or whatever you want - just make sure the Page_Init
and Bind_Data
methods refer to this view). Any time you want to change the metadata displayed on the results page, simply change this view.
Content Editor Web Part
To invoke the search from another page on the site, simply add a content editor web part, placing the JavaScript function and the HTML textbox and button in it. You can add any other content/formatting as you choose. To add a content editor web part to a page, click the "Modify this page" link, then choose "Add Web Parts | Browse...", and drag the content editor web part onto your page. Use the source editor to add your HTML and JavaScript.
Points of Interest
When I first started this project, I thought it would be fairly simple - just take the object returned by the search tool and access the other metadata fields. Then I discovered that the object returned by the SharePoint search engine does not include all the metadata for the documents. Hence the need for using the ID obtained from the search result object to obtain the actual document object, which does contain the metadata.
Adding somewhat to the complexity, was using the sometimes awkward SharePoint object model. For example, dealing with both the internal name and the display name of the metadata fields led to code like this: ResultsRow[InternalViewFldNames[j]] = ThisItem[ListFields.GetFieldByInternalName(InternalViewFldNames[j]).Title];
.
Another "gotcha" was the problem of sorting the grid by title. During testing, sorting on all the other columns was fine, but the Title column was not sorting properly. Since the sort is a method of the DataGrid
object, I was initially stumped. But when I realized that with the anchor tag containing the URL to do the document, if the column was sorted "alphabetically", it was really sorting by ID. But by adding a hidden column (as described above) that contains simply the document title, and making the search action of the Title column invoke the search on the hidden column, the grid is sorted properly by title.
History
- 06.22.06 - Initial version.