Thursday, December 29, 2011

SharePoint 2010 Custom Advanced Search for Power Users

A few months ago, I had the need to aggregate list items from a list which exists on multiple site collections and display them as a list, filterable by column, on a top level site (as displayed below). 

image

The lists all use the same content type so it was simple enough to go into the Search Service Application and create Managed Properties and a Scope, go to a page on my top level site and add a Search Core Results web part where I cleared the Use Location Visualization selection, added the Columns to include my new Managed Properties (see TechNet), changed the XSLT to display the search results formatted as a list (as a start for previously mentioned steps, see MSDN guidance here for customizing search results and Tobias Zimmergren has the step-by-step for SP 2007 here which isn’t much different in SP 2010), and set the Append Text to Query to the custom Scope keyword so it automatically brings back search results for this Scope (You could use one of your custom managed properties here too – I used Append Text To Query instead of Fixed Keyword Query because Fixed Query won’t allow you to filter by other keywords – there are ways to dynamically change this fixed query which I developed later and is in a future post). 

image

The filter was a challenge.  Using the out-of-the-box Advanced Search web part was my initial thought but the designer’s wireframes had dropdown boxes where the Advanced Search Web Part only allows plain text inputs and doesn’t display the way the designer had imagined.  I did a little research and came across a post from Tom Clarkson where he had a low-dev solution using a content editor web part  to create a Custom Advanced Search Box in SharePoint 2007 (please review Tom’s post to understand what the “ASB_” fields are).  So I thought I could try this for SharePoint 2010.  I did…and it did nothing.  I added some of the properties as a test and placed in a content editor web part as Tom had documented, switched my core results web part results query options to use “Query 2” so it would use the query from the custom search instead of the search query control on the page, ran the query…and nothing.

image

I came across several posts on the web where people implemented Tom’s solution in SharePoint 2007, upgraded to SharePoint 2010, and found the solution no longer worked.  I was under a time crunch so I didn’t have time to start building something in Visual Studio and there was nothing that fit the bill on codeplex so I investigated further by reflecting the code for the Advanced Search web part to see that a lot of what the web part does is generate JavaScript functions so it became clearer that I wasn’t getting results because I didn’t have the JavaScript functions beginning with the key one “SearchButtonOnClick.”  So I needed the JavaScript…simple…I placed an OOTB Advanced Search Web Part on my page and set it’s layout property to “Hidden.”  Not so simple, I had the JavaScript, but it didn’t want to use the HTML form from my content editor web part.  So I created my own function based on SearchButtonOnClick called SearchButtonOnClick2 (as displayed below -- In total, I had to customize 4 of the OOTB functions to get the search results to fit the requirement).

function SearchButtonOnClick2(switchElement)
{
ResetPageHashCode();
DoAdvancedSearch2('k', '\u002fPages\u002fAll-Projects.aspx', 'xyzProject', 'ASB_TQS_AndQ_tb', 'ASB_TQS_PhraseQ_tb', 'ASB_TQS_OrQ_tb', 'ASB_TQS_NotQ_tb', 'ASB_SS_scb_', 'ASB_SS_lcb_', 'ASB_SS_rtlb', 'ASB_PS_plb_', 'ASB_PS_olb_', 'ASB_PS_pvtb_', 'ASB_PS_lolb_');
}



Notice the called function DoAdvancedSearch2 -- it is based on the OOTB function DoAdvancedSearch – and is where I instruct the search to grab values from my form (in sample, 4th element value “xyzProject”) and I also set the results page to the current page in the 2nd element.



The DoAdvancedSearch2 function is as follows:



function DoAdvancedSearch2(queryParameterName, resultsPage, 
idOuterTable, idAndQueryTextBox, idPhraseQueryTextBox,
idOrQueryTextBox, idNotQueryTextBox, idPrefixScopeCheckBox,
idPrefixLangsCheckBox, idResultTypeList,
idPrefixPropNameSelect, idPrefixPropOperatorSelect,
idPrefixPropValueTextBox, idPrefixPropAndOrSelect) {

if (ValidateForm()) {
var elements = findElements(idOuterTable, idAndQueryTextBox,
idPhraseQueryTextBox, idOrQueryTextBox,
idNotQueryTextBox, idPrefixScopeCheckBox,
idPrefixLangsCheckBox, idResultTypeList,
idPrefixPropNameSelect, idPrefixPropOperatorSelect,
idPrefixPropValueTextBox, idPrefixPropAndOrSelect);

var query = ComposeQuery2(elements);
//alert(query);
if (query != '')
{
var url = resultsPage + '?' + queryParameterName + '=' + query;
navigateTo(url);
}
else
{
alert(emptyQueryMessage);
}
}
}


Notice the ComposeQuery2 function being called – again this are based on the OOTB generated function where the only reason I need to change it is to instead call the new ComposePropertySectionQuery2 function.



function ComposeQuery2(elements) {
return encodeURIComponent(ConcatenateQueryParts(
ComposeTextSectionQuery(elements),
ComposeScopingSectionQuery(elements),
ComposePropertySectionQuery2(elements)
));
}

function ComposePropertySectionQuery2(elements) {
var queryParts = [];
var querySeparators = [];
for (var i = 0; i < elements.PropNameSelectArray.length; i++) {
propNameSelect = elements.PropNameSelectArray[i];
propOperatorSelect = elements.PropOperatorSelectArray[i];
propAndOrSelect = elements.PropAndOrSelectArray[i];

if (propNameSelect.selectedIndex >= 0) {
var propIndex = findInArray(arrPropNames, propNameSelect.value);
var propName = arrPropNames[propIndex];
var propDataType = arrPropDTs[propIndex];
var opIndex = findInArray(arrDTOps[propDataType], propOperatorSelect.value);
var propPrefix = arrDTOpsStrPrefix[propDataType][opIndex];
var propOperator = arrDTOpsStr[propDataType][opIndex];
var propValue = elements.PropValueTextboxArray[i].value;
//handle when no input entered or value selected
if (propValue == '')
{

continue;
}
if ((propValue.indexOf(' ') != -1) ||
(propName.toLowerCase() === 'path'))
{
propValue = enquote(propValue);
}
queryParts.push(propPrefix + propName + propOperator + propValue);
querySeparators.push(arrAndOrKeywords[propAndOrSelect.selectedIndex]);

}
}

var query = ConcatenateQueryPartsWithSeparator(querySeparators, queryParts);

if (queryParts.length > 1)
return '(' + query + ')';
else
return query;

}


The difference between ComposePropertySectionQuery2 and the OOTB ComposePropertySectionQuery function is the change to condition “if (propNameSelect.selectedIndex >= 0)” which allows the user to submit a query with no filter values – this is used when the user basically wants to reset to show all results after having previously queried using a filter value, and the logic I added under the comment “handle when no input enter in value selected.” This logic is required because the user needs to filter by all fields, one field, or no fields (a reset).  If there is only one field they filter by, I don’t want the Query to be built to include searching by the other properties.



So there is also a limitation where these search functions will only work on plain text inputs -- however, I have requirement for dropdown boxes.  To get this to work, I need to add hidden text inputs to my form which correlate to the dropdown (sample below).



<select name="projPhase" id="projPhase" >
<option selected="selected" value="">Any Phase</option>
<option value="Project Control">Project Control</option>
<option value="Pre&#45;Initiate">Pre&#45;Initiate</option>
<option value="Intitate">Initiate</option>
<option value="Requirements">Requirements</option>
<option value="Architecture">Architecture&#47;Design</option>
<option value="Development">Development</option>
<option value="Testing">Testing</option>
<option value="Deployment">Deployment</option>
</select>
<input id="nameprefix_ASB_PS_pvtb_4" name="nameprefix$ASB_PS_pvtb_4" type="text" style="display:none" />



With the hidden text inputs, I can do actions on my form dropdown values in the SearchButtonOnClick2 function where I’m grabbing the selected value for my dropdown box using the JavaScript function getElementById, and setting the hidden text input’s value to that value so the search functions can use it.



SearchButtonOnClick2(switchElement)
{
ResetPageHashCode();
var elementStatus = document.getElementById('projStatus');
var elementStatusText = document.getElementById('nameprefix_ASB_PS_pvtb_2');
elementStatusText.value = elementStatus.value;
var elementSecurity = document.getElementById('projSecurity');
var elementSecurityText = document.getElementById('nameprefix_ASB_PS_pvtb_3');
elementSecurityText.value = elementSecurity.value;
var elementPhase = document.getElementById('projPhase');
var elementPhaseText = document.getElementById('nameprefix_ASB_PS_pvtb_4');
elementPhaseText.value = elementPhase.value;
DoAdvancedSearch2('k', '\u002fPages\u002fAll-Projects.aspx', 'mmProject', 'ASB_TQS_AndQ_tb', 'ASB_TQS_PhraseQ_tb', 'ASB_TQS_OrQ_tb', 'ASB_TQS_NotQ_tb', 'ASB_SS_scb_', 'ASB_SS_lcb_', 'ASB_SS_rtlb', 'ASB_PS_plb_', 'ASB_PS_olb_', 'ASB_PS_pvtb_', 'ASB_PS_lolb_');
}



For complete reference, here’s a sample of the full script that I placed in the HTML source of the content editor web part (notice the scopes are hidden from display but they are necessary for the query to run and they need to match whatever scopes you are showing in the hidden advanced search web part.  I’m also hiding the property values and conditions to meet the wireframe requirements but the query functions need them so they are included).



<table id="xyzProject">
<tr>
<td>
<input id="nameprefix_ASB_SS_scb_0_12" type="checkbox" name="nameprefix$ASB_SS_scb_0_12" style="display:none"/>
<input id="nameprefix_ASB_SS_scb_1_10" type="checkbox" name="nameprefix$ASB_SS_scb_1_10" checked="checked" style="display:none"/>
<input id="nameprefix_ASB_SS_scb_2_11" type="checkbox" name="nameprefix$ASB_SS_scb_2_11" style="display:none"/>

<select name="nameprefix$ASB_SS_rtlb" id="nameprefix_ASB_SS_rtlb" title="Result Type" style="display:none">
<option selected="selected" value="default">All Results</option>
</select>
</td>
</tr>

<tr class="ms-vh" nowrap="nowrap" style="font-family: Verdana, Helvetica, sans-serif; font-weight: bold; color: #666; text-decoration: none">

<td>
Project Name:
</td>
<td>
Program Name:
</td>
<td>
Status:
</td>
<td>
Security:
</td>
<td>
Current Phase:
</td>
<td>
Project Manager:
</td>
<td>
Description:
</td>
</tr>
<tr>
<td>
<select name="nameprefix$ASB_PS_plb_0" id="nameprefix_ASB_PS_plb_0" title="Pick a Property" style="display:none">
<option selected="selected" value="xyzProjectName">Project Name</option>
</select>
<select name="nameprefix$ASB_PS_olb_0" id="nameprefix_ASB_PS_olb_0" title="Inclusion Operator" style="display:none">
<option selected="selected" value="Contains">Contains</option>
</select>
<input id="nameprefix_ASB_PS_pvtb_0" name="nameprefix$ASB_PS_pvtb_0" type="text"/>
<select name="nameprefix$ASB_PS_lolb_0" id="nameprefix_ASB_PS_lolb_0" title="And Or Operator" style="display:none">
<option selected="selected" value="And">And</option>
</select>
</td>

<td>
<select name="nameprefix$ASB_PS_plb_1" id="nameprefix_ASB_PS_plb_1" title="Pick a Property" style="display:none">
<option selected="selected" value="xyzProgramName">Program Name</option>
</select>
<select name="nameprefix$ASB_PS_olb_1" id="nameprefix_ASB_PS_olb_1" title="Inclusion Operator" style="display:none">
<option selected="selected" value="Contains">Contains</option>
</select>
<input id="nameprefix_ASB_PS_pvtb_1" name="nameprefix$ASB_PS_pvtb_1" type="text"/>
<select name="nameprefix$ASB_PS_lolb_1" id="nameprefix_ASB_PS_lolb_1" title="And Or Operator" style="display:none">
<option selected="selected" value="And">And</option>
</select>
</td>

<td>
<select name="nameprefix$ASB_PS_plb_2" id="nameprefix_ASB_PS_plb_2" title="Pick a Property" style="display:none">
<option selected="selected" value="xyzProjectStatus">Project Status</option>
</select>
<select name="nameprefix$ASB_PS_olb_2" id="nameprefix_ASB_PS_olb_2" title="Inclusion Operator" style="display:none">
<option selected="selected" value="Contains">Contains</option>
</select>
<select name="projStatus" id="projStatus" >
<option selected="selected" value="">Any Status</option>
<option value="Active">Active</option>
<option value="Locked">Locked</option>
<option value="Cancelled">Cancelled</option>
</select>
<input id="nameprefix_ASB_PS_pvtb_2" name="nameprefix$ASB_PS_pvtb_2" type="text" style="display:none" />
<select name="nameprefix$ASB_PS_lolb_2" id="nameprefix_ASB_PS_lolb_2" title="And Or Operator" style="display:none">
<option selected="selected" value="And">And</option>
</select>
</td>

<td>
<select name="nameprefix$ASB_PS_plb_3" id="nameprefix_ASB_PS_plb_3" title="Pick a Property" style="display:none">
<option selected="selected" value="xyzProjectSecurity">Project Status</option>
</select>
<select name="nameprefix$ASB_PS_olb_3" id="nameprefix_ASB_PS_olb_3" title="Inclusion Operator" style="display:none">
<option selected="selected" value="Contains">Contains</option>
</select>
<select name="projSecurity" id="projSecurity" >
<option selected="selected" value="">Any Level</option>
<option value="FALSE">Normal Level</option>
<option value="TRUE">Sensitive Level</option>
</select>
<input id="nameprefix_ASB_PS_pvtb_3" name="nameprefix$ASB_PS_pvtb_3" type="text" style="display:none" />
<select name="nameprefix$ASB_PS_lolb_3" id="nameprefix_ASB_PS_lolb_3" title="And Or Operator" style="display:none">
<option selected="selected" value="And">And</option>
</select>
</td>

<td>
<select name="nameprefix$ASB_PS_plb_4" id="nameprefix_ASB_PS_plb_4" title="Pick a Property" style="display:none">
<option selected="selected" value="xyzProjectPhase">Project Phase</option>
</select>
<select name="nameprefix$ASB_PS_olb_4" id="nameprefix_ASB_PS_olb_4" title="Inclusion Operator" style="display:none">
<option selected="selected" value="Contains">Contains</option>
</select>
<select name="projPhase" id="projPhase" >
<option selected="selected" value="">Any Phase</option>
<option value="Project Control">Project Control</option>
<option value="Pre&#45;Initiate">Pre&#45;Initiate</option>
<option value="Intitate">Initiate</option>
<option value="Requirements">Requirements</option>
<option value="Architecture">Architecture&#47;Design</option>
<option value="Development">Development</option>
<option value="Testing">Testing</option>
<option value="Deployment">Deployment</option>
</select>
<input id="nameprefix_ASB_PS_pvtb_4" name="nameprefix$ASB_PS_pvtb_4" type="text" style="display:none" />
<select name="nameprefix$ASB_PS_lolb_4" id="nameprefix_ASB_PS_lolb_4" title="And Or Operator" style="display:none">
<option selected="selected" value="And">And</option>
</select>
</td>

<td>
<select name="nameprefix$ASB_PS_plb_5" id="nameprefix_ASB_PS_plb_5" title="Pick a Property" style="display:none">
<option selected="selected" value="xyzProjectManager">Project Manager</option>
</select>
<select name="nameprefix$ASB_PS_olb_5" id="nameprefix_ASB_PS_olb_5" title="Inclusion Operator" style="display:none">
<option selected="selected" value="Contains">Contains</option>
</select>
<input id="nameprefix_ASB_PS_pvtb_5" name="nameprefix$ASB_PS_pvtb_5" type="text"/>
<select name="nameprefix$ASB_PS_lolb_5" id="nameprefix_ASB_PS_lolb_5" title="And Or Operator" style="display:none">
<option selected="selected" value="And">And</option>
</select>
</td>

<td>
<select name="nameprefix$ASB_PS_plb_6" id="nameprefix_ASB_PS_plb_6" title="Pick a Property" style="display:none">
<option selected="selected" value="xyzProjectDescription">Project Description</option>
</select>
<select name="nameprefix$ASB_PS_olb_6" id="nameprefix_ASB_PS_olb_6" title="Inclusion Operator" style="display:none">
<option selected="selected" value="Contains">Contains</option>
</select>
<input id="nameprefix_ASB_PS_pvtb_6" name="nameprefix$ASB_PS_pvtb_6" type="text"/>
<select name="nameprefix$ASB_PS_lolb_6" id="nameprefix_ASB_PS_lolb_6" title="And Or Operator" style="display:none">
<option selected="selected" value="And">And</option>
</select>
</td>

</tr>

<tr><td></td><td></td><td></td><td></td><td></td><td></td>
<td align="right">
<input type="submit" name="nameprefix$ASB_BS_SRCH_1" value="Submit Query" onclick="SearchButtonOnClick2(); return false; WebForm_DoPostBackWithOptions(new WebForm_PostBackOptions(&quot;nameprefix$ASB_BS_SRCH_1&quot;, &quot;&quot;, true, &quot;&quot;, &quot;&quot;, false, false))" id="nameprefix_ASB_BS_SRCH_1" title="Search" />
</td></tr>
</table>

<script type="text/javascript">

function SearchButtonOnClick2(switchElement)
{
ResetPageHashCode();
var elementStatus = document.getElementById('projStatus');
var elementStatusText = document.getElementById('nameprefix_ASB_PS_pvtb_2');
elementStatusText.value = elementStatus.value;
var elementSecurity = document.getElementById('projSecurity');
var elementSecurityText = document.getElementById('nameprefix_ASB_PS_pvtb_3');
elementSecurityText.value = elementSecurity.value;
var elementPhase = document.getElementById('projPhase');
var elementPhaseText = document.getElementById('nameprefix_ASB_PS_pvtb_4');
elementPhaseText.value = elementPhase.value;
DoAdvancedSearch2('k', '\u002fPages\u002fAll-Projects.aspx', 'xyzProject', 'ASB_TQS_AndQ_tb', 'ASB_TQS_PhraseQ_tb', 'ASB_TQS_OrQ_tb', 'ASB_TQS_NotQ_tb', 'ASB_SS_scb_', 'ASB_SS_lcb_', 'ASB_SS_rtlb', 'ASB_PS_plb_', 'ASB_PS_olb_', 'ASB_PS_pvtb_', 'ASB_PS_lolb_');
}

function DoAdvancedSearch2(queryParameterName, resultsPage,
idOuterTable, idAndQueryTextBox, idPhraseQueryTextBox,
idOrQueryTextBox, idNotQueryTextBox, idPrefixScopeCheckBox,
idPrefixLangsCheckBox, idResultTypeList,
idPrefixPropNameSelect, idPrefixPropOperatorSelect,
idPrefixPropValueTextBox, idPrefixPropAndOrSelect) {

if (ValidateForm()) {
var elements = findElements2(idOuterTable, idAndQueryTextBox,
idPhraseQueryTextBox, idOrQueryTextBox,
idNotQueryTextBox, idPrefixScopeCheckBox,
idPrefixLangsCheckBox, idResultTypeList,
idPrefixPropNameSelect, idPrefixPropOperatorSelect,
idPrefixPropValueTextBox, idPrefixPropAndOrSelect);

var query = ComposeQuery2(elements);
//alert(query);
if (query != '')
{
var url = resultsPage + '?' + queryParameterName + '=' + query;
navigateTo(url);
}
else
{
alert(emptyQueryMessage);
}
}
}

function ComposeQuery2(elements) {
return encodeURIComponent(ConcatenateQueryParts(
ComposeTextSectionQuery(elements),
ComposeScopingSectionQuery(elements),
ComposePropertySectionQuery2(elements)
));
}

function ComposePropertySectionQuery2(elements) {
var queryParts = [];
var querySeparators = [];
for (var i = 0; i < elements.PropNameSelectArray.length; i++) {
propNameSelect = elements.PropNameSelectArray[i];
propOperatorSelect = elements.PropOperatorSelectArray[i];
propAndOrSelect = elements.PropAndOrSelectArray[i];

if (propNameSelect.selectedIndex >= 0) {
var propIndex = findInArray(arrPropNames, propNameSelect.value);
var propName = arrPropNames[propIndex];
var propDataType = arrPropDTs[propIndex];
var opIndex = findInArray(arrDTOps[propDataType], propOperatorSelect.value);
var propPrefix = arrDTOpsStrPrefix[propDataType][opIndex];
var propOperator = arrDTOpsStr[propDataType][opIndex];
var propValue = elements.PropValueTextboxArray[i].value;
//handle when no input entered or value selected
if (propValue == '')
{
continue;
}
if ((propValue.indexOf(' ') != -1) ||
(propName.toLowerCase() === 'path'))
{
propValue = enquote(propValue);
}
queryParts.push(propPrefix + propName + propOperator + propValue);
querySeparators.push(arrAndOrKeywords[propAndOrSelect.selectedIndex]);

}
}

var query = ConcatenateQueryPartsWithSeparator(querySeparators, queryParts);

if (queryParts.length > 1)
return '(' + query + ')';
else
return query;

}

function findElements2(idOuterTable, idAndQueryTextBox, idPhraseQueryTextBox, idOrQueryTextBox, idNotQueryTextBox,
idPrefixScopeCheckBox, idPrefixLangsCheckBox, idResultTypeList,
idPrefixPropNameSelect, idPrefixPropOperatorSelect,
idPrefixPropValueTextBox, idPrefixPropAndOrSelect) {
var inputFields = document.getElementById(idOuterTable).getElementsByTagName('input');
var selectFields = document.getElementById(idOuterTable).getElementsByTagName('select');

var elements = createEmptyElements();

for (var i = 0; i < inputFields.length; i++) {
if (matchRegex(idAndQueryTextBox, inputFields[i].id)) {
elements.AndQueryTextBox = inputFields[i];
}
else if (matchRegex(idPhraseQueryTextBox, inputFields[i].id)) {
elements.PhraseQueryTextBox = inputFields[i];
}
else if (matchRegex(idOrQueryTextBox, inputFields[i].id)) {
elements.OrQueryTextBox = inputFields[i];
}
else if (matchRegex(idNotQueryTextBox, inputFields[i].id)) {
elements.NotQueryTextBox = inputFields[i];
}
else if (matchRegex(idPrefixScopeCheckBox, inputFields[i].id)) {
elements.ScopeCheckBoxArray[elements.ScopeCheckBoxArray.length] = inputFields[i];
}
else if (matchRegex(idPrefixLangsCheckBox, inputFields[i].id)) {
elements.LangCheckBoxArray[elements.LangCheckBoxArray.length] = inputFields[i];
}
else if (matchRegex(idPrefixPropValueTextBox, inputFields[i].id)) {
elements.PropValueTextboxArray[elements.PropValueTextboxArray.length] = inputFields[i];
}
}

for (i = 0; i < selectFields.length; i++) {
if (matchRegex(idResultTypeList, selectFields[i].id)) {
elements.ResultTypeList = selectFields[i];
}
else if (matchRegex(idPrefixPropNameSelect, selectFields[i].id)) {
elements.PropNameSelectArray[elements.PropNameSelectArray.length] = selectFields[i];
}
else if (matchRegex(idPrefixPropOperatorSelect, selectFields[i].id)) {
elements.PropOperatorSelectArray[elements.PropOperatorSelectArray.length] = selectFields[i];
}
else if (matchRegex(idPrefixPropAndOrSelect, selectFields[i].id)) {
elements.PropAndOrSelectArray[elements.PropAndOrSelectArray.length] = selectFields[i];
}
else if (matchRegex(idPrefixPropValueTextBox, selectFields[i].id)) {
elements.PropValueTextboxArray[elements.PropValueTextboxArray.length] = selectFields[i];
}
}
return elements;
}

</script>

No comments:

Post a Comment