Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

SharePoint Custom Search Results Page

0.00/5 (No votes)
29 Jun 2006 1  
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 for
  • Target - the fully qualified URL of the site to be searched (current site)

Once you've created a copy of the page, do the following:

  1. Modify page directives/includes:
    1. EnableViewState must be set to True
    2. Remove the �Inherits From�
    3. Add references to the System, System.Collections, System.Data, and System.Web namespaces
  2. Strip out all server side code and controls
  3. Add a server-side DataGrid control (or whatever list format you desire for the results)
  4. In a server-side script block, do the following:
    1. Get querystring values
    2. Create DataGrid columns using the fields in the �Search Results� view
    3. Invoke the search on the document library (using querystring values)
    4. Filter out results not from the Content Repository library
    5. Bind the results to the DataGrid columns
    6. Specify the sort method
  5. Add a client-side text box and button, with the same format as in the calling content editor web part
  6. 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 the DataTable
  • Applies the sort order (if any)
  • Binds the DataView to the DataGrid

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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here