Rogue IT Horror Story vs. Opportunity

harmon.ieOne of the big players in the SharePoint space, harmon.ie, is currently running a contest to collect the best “Rogue IT Horror Story”. If you have a story and want a chance to win a Samsung Galaxy S IV or a Trip to the Microsoft SharePoint Conference 2014 in Las Vegas in March, by all means head on over and submit your story.

Rogue IT users aren’t intending to harm their companies, they’re just trying to find the most efficient ways to get their work done. But this ‘efficiency’ comes at a high price: a recent survey found that 27% of workers who went rogue — and used consumer or unsanctioned apps for work — reported immediate and direct repercussions to the tune of $2 billion in penalties, lost business, data leakage and clean-up costs.

Mark Fidelman has a post about the contest on EndUserSharePoint today which prompted me to comment and write this post.

The problem that I have with the phrasing of contest and the thinking around it is it sets up more of the us vs. them thinking that ruins IT’s reputation in any organization.

These “horror stories” shouldn’t be water cooler jokes for IT (the most common thing I see in large organizations). They should be seen as opportunities for IT to offer improved services and support. Instead, they are usually met with increased lockdowns and policing which simply causes more effort expended to go around IT.

Every well-intentioned person who circumvents the “right” way to accomplish something:

  • Is trying to get something done in performance of their job
  • Most likely has met with a “no” – or the dreaded “no budget” – response from IT
  • Doesn’t trust that IT will help them or understand their needs
  • Has turned to the best approach they can figure out

We technologists should see these instances as excellent opportunities, not horror stories. We should strive to help people in our organizations before anything ever gets near some potential $500 million loss (which I frankly question as anything more than a great story).

/

Customizing Search Refiners in SharePoint 2010

Search RefinersI wanted to add some custom refiners to each of the custom search result pages I’ve been working on in a project. The steps to do this aren’t complex, but since I needed to read a bunch of posts out there to get it just right, I figured I’d write it up.

Create Custom Managed Properties

Only Managed Properties are available for use as refiners. While Site Columns automatically become Crawled Properties, they must be added as Managed Properties to use them as refiners (or in scopes, etc.).

To do this, we go to Central Administration -> Application Management -> Manage Service Applications -> Search Service Application -> Metadata Properties and whatever new Managed Properties we need.

Note that we cannot use the existing ContentType Managed Property in the refiners. If we do, we get the error

Property doesn’t exist or is used in a manner inconsistent with schema settings.

In order to use the Content Type as a refiner, we must create our own Managed Property which mirrors the existing one. I created a new Managed Property called ContentTypeRefiner and mapped it to ows_ContentType(Text).

Perform a Full Crawl

After creating the Custom Managed Properties, we must do a full crawl so that the new Managed Properties will be in the active index.

To do this, we go to Central Administration -> Application Management -> Manage Service Applications -> Search Service Application [you may have named this differently when you created it] -> Content Sources. From the dropdown for the relevant Search Scope, choose Start Full Crawl.

A full crawl can take a significant amount of time depending on the amount of content you may have, so it’s best to create all of the new Managed Properties before kicking one off.

Add Managed Properties into Refiners

To add the new Managed Properties as refiners in the custom search results pages, we do the following:

  • Navigate to the search results page
  • Go into Edit Mode
  • In the Refinement Panel (left side of the page) select Edit Web Part
  • In the Tool Pane, under the Refinement section, edit the Filter Category Definition XML to include the new refiners
  • Uncheck the Use Default Configuration box, otherwise your changes will disappear!

Note that any of the existing refiners can be removed. The order in which the refiners are included in the Filter Category Definition determines the order in which the refiners are shown, if there is enough content which contains that refiner’s metadata.

Here’s an example of what the XML for your own refiners might look like. This is the XML I added for the ContentTypeRefiner Managed Property. Most of the attributes are pretty self-explanatory.

<Category Title="Content Type"
  Description="Type of resource"
  Type="Microsoft.Office.Server.Search.WebControls.ManagedPropertyFilterGenerator"
  MetadataThreshold="5"
  NumberOfFiltersToDisplay="4"
  MaxNumberOfFilters="20"
  SortBy="Frequency"
  SortByForMoreFilters="Name"
  SortDirection="Descending"
  SortDirectionForMoreFilters="Ascending"
  ShowMoreLink="True"
  MappedProperty="ContentTypeRefiner"
  MoreLinkText="show more"
  LessLinkText="show fewer" />

Useful Reference Links

Finding the SharePoint 2007 / 2010 Thesaurus Files

If you go to TechNet to find out where the thesaurus files for search are so that you can add in your own synonyms, you may be as confused as I was earlier today. It only took me about 20 minutes to figure out, but if three or four people find this post, we’ll have saved enough time for lunch.

The TechNet article you want is Manage thesaurus files (SharePoint Server 2010), though the one for SharePoint 2007 (Edit a thesaurus file (Office SharePoint Server)) is pretty much identical.

In the article, it says

By default, SharePoint Server 2010 installs the thesaurus files for all supported languages at %ProgramFiles%\Microsoft Office Servers\14.0\Data\Office Server\Config. When a search administrator creates a Search service application, the search system automatically copies the thesaurus files from the installation location (including any thesaurus files there that an administrator has edited) to %ProgramFiles%\Microsoft Office Servers\14.0\Data\Office Server\Applications\GUID-query-0\Config, where GUID is the GUID of the new Search service application. The search system performs the same operation on every query server that is running the new Search service application. Thus there is a copy of each thesaurus file on each query server that is running that Search service application.

When I looked in %ProgramFiles%\Microsoft Office Servers\14.0\Data\Office Server\Config, well, there was no %ProgramFiles%\Microsoft Office Servers\14.0\Data\Office Server\Config. Instead, because my client had decided to change the location of the index to another drive, I have to figure out where that actually was. Here’s the trick.

In complex farms, you may have multiple Search Service Application, multiple indices, etc. but these steps should work in most cases.

  • In Central Administration, go to the Search Application -> Central Administration/ Manage service applications / Search Service Application (or whatever you called it)
  • At the bottom of the page, you’ll see a section called ‘Search Application Topology’
  • Click the Modify button and on the next screen look for the ‘Index Partition’ (you may have more than one)
  • Click on the ‘Query Component 0′ link and Edit Properties
  • The field called ‘Location of Index’ contains the root location for the thesaurus files

image

Looking in that folder, you should find folders that look something like this:

image

As noted above, the thesaurus files you want to work with are in the GUID-query-0\Config folder. In my case above, it’s E:\Data\SearchIndex\Office Server\Applications\0f78bae4-05b9-417f-b533-43326409dfcc-query-0\Config

Happy equivalency!

One side note: it boggles my mind that there is no UI to manage synonyms in the thesaurus, but there you go.

SPServices Stories #17: Multiple Form Fields Autocomplete for SharePoint 2010/2013 using JavaScript

Introduction

Anyone who follows this series knows that I’m always on the lookout for interesting and useful implementations of SPServices. A few weeks ago, a tweet in my feeds caught my eye:

Anton Khritonenkov Anton Khritonenkov, who is a Technical Lead at Plumsail in Russia, has come up with a nice way to enable autocomplete behavior for multiple list columns based on related lists.

While I’ve got the SPAutocomplete function in SPServices, it’s pretty rudimentary and I usually suggest using the jQueryUI autocomplete function, as Anton does here. The other function in SPServices that is somewhat related to what Anton is doing in SPDisplayRelatedInfo. By using the completefunc there, you could enable some of the behavior that Anton has built, but not as cleanly.

Anton explains things well in his post below, which he was kind enough to allow me to repost here from his original on the CodeProject site.

Multiple Form Fields Autocomplete for SharePoint 2010/2013 using JavaScript

Introduction

Filling forms could be painful sometimes, especially for forms with many fields. In this article I’ll describe approach which can simplify forms filling significantly. In my case I needed to implement order form interface. In fact it contains a lot of fields I can pre-fill based on chosen customer, such as address, ZIP code and account number. All of this data is already stored in separate list called “Customers directory”.

Final requirements could looks like this:

When user starts typing in the customer field, I need to suggest list of customer names from Customers directory. When user chooses customer from suggestions list, I need to read data multiple field values from Customers directory and fill corresponding fields in the order form. User can correct filled values later if needed.

To implement such functionality I used to use JavaScript and this case is not an exception. There are many reasons for this:

  • It well fits Office 365 restrictions.
  • It easily migrates from older SharePoint versions.
  • It is easily to debug without slow SharePoint solution deployment.
  • REST SharePoint 2013 API or SPServices are almost such powerful as server code.
  • Finally I just like it.

In this article I’ll use SPServices jQuery plugin for communication with SharePoint services. It works for SharePoint 2013 as well as for SharePoint 2010. I’ll also use jQuery UI Autocomplete plugin to implement suggestions functionality.

plumFormAutocomplete plugin works well for single field as much as for multiple fields.  Plugin supports text fields only.

Plugin in action looks like this:

How to use plumFormAutocomplete jQuery plugin

You can use this plugin without looking into the code. Firstly, I’ll describe how to use it, then if you still will be interested, you can look inside plugin implementation.

Prerequisites

If you don’t have jQuery, jQuery UI or SPServices, you can download fresh version from official sites. For tutorial purposes I assume that names of downloaded files and folders are following:

  • jquery.min.js 
  • jquery.SPervices.min.js 
  • jquery-ui-1.10.3.custom.min.js
  • jquery-ui-1.10.3.custom.min.css
  • css folder for jQuery UI

I also assume that you have jquery.plumFormAutocomplete.js downloaded from source code of this article (Source code download link).

Upload jquery.min.js, jquery.SPServices.min.js, jquery-ui-1.10.3.custom.min.js and jquery.plumFormAutocomplete.js files to Style Library within your site collection. You also need to upload jQuery UI CSS styles located in the same folder with jquery-ui-1.10.3.custom.min.js. Then open New or Edit form, where autocomplete plugin will be used and add js and CSS links to placeholder PlaceHolderAdditionalPageHead.  You can use following snippet:


Configure and call plugin

Now you are ready to configure and call plugin. For my case plugin call looks like this:

//get control for autocomplete field
var fieldControl = $.getFieldControl('Title');

//call autocomplete plugin for field control
fieldControl.plumFormAutocomplete({
  sourceList: 'Customers directory',
  sourceMatchField: 'Title',
  labelFields: ['Title', 'ZIPCode'],
  labelSeparator: ', ',
  fillConcatenatedLabel: false,
  fieldsMapping: [{sourceField: 'Address', targetField: 'CustAddress'},
    {sourceField: 'AccountNumber', targetField: 'CustAccountNumber'},
    {sourceField: 'ZIPCode', targetField: 'CustZIPCode'}]
});

You can wrap plugin call inside jQuery $(document).ready() function to ensure that code will be executed after page is loaded.

Let us look at this code sample in more detail. Code is divided into two parts:

  1. Get control for autocomplete field
  2. Call autocomplete plugin for field control

For the first step you need to specify internal name of autocomplete field for getFieldControl function. It is ‘Title’ in my case.

In the second step you need to call plugin for received autocomplete field and configure plugin options. Plugin options are structured as a single object as any jQuery plugin options.

Plugin options

  • sourceList – name or GUID of source list, where suggestions will be taken. It is ‘Customers directory’ in my case.
  • sourceMatchField – internal name of the field in the source list. This field will be used to find matching list items for autocomplete keywords.
  • labelFields – an optional parameter, you can specify source list field internal names array. All field values for these  fields will be concatenated with labelSeparator and displayed in autocomplete suggestion as a single string like this: Value1, Value2, …, ValueN.
  • labelSeparator – an optional parameter, it is separator for labelFields concatenation, for example it could be comma with space (‘, ‘).
  • fillConcatenatedLabel – an optional parameter, set true if you need to fill autocomplete textbox with all concatenated labelFields values, set false if you need to fill autocomplete text box only with single field value.
  • fieldsMapping – an optional parameter, it is an array of field mapping objects. Each object declares mapping from source list field to target list field. In my case names of source and target fields are the same. For example Address in Customer directory and Address in Orders list.

Mapping object has following syntax:

{sourceField: 'Internal name of source field', targetField: 'Internal name of target field'}

Note: You can specify only non optional parameters, plugin will work correctly. This plugin works well as single field autocomplete too, just do not fill optional parameters.

Plugin configuration without optional parameters could look like this:

fieldControl.plumFormAutocomplete({
    sourceList: 'Customers directory',
    sourceMatchField: 'Title'
});

Internal implementation of the plugin

Let us look at the full plugin source code. You can download it here.

There are three major part inside the code:

  1. Getting text field input.
  2. Apply jQueryUIi autocomplete plugin and SPServices to get suggestions.
  3. Implementing helper functions.

To get field input I used jQuery selectors and simple regular expression. Unfortunately SharePoint doesn’t provide any method to get field controls from JavaScript, it only stores field internal name inside html comment in the following format:

<!-- FieldName="Title" FieldInternalName="Title" FieldType="SPFieldText" -->

So, I had to parse it to find control I needed. Final function was added to jQuery:

//function gets text field control by internal name
$.getFieldControl = function (fieldInternalName) {
  var regexStr = 'FieldInternalName="' + fieldInternalName + '"'
  var regex = new RegExp(regexStr, 'i');
  var fieldCell = $('td.ms-formbody').filter(function () {
    return (regex).test($(this).html())
  });
  return $(fieldCell.find('input')[0]);
}

In the next step I applied jQuery UI autocomplete plugin and implemented source and select plugin functions. Source function calls source list using SPServices and CAML to get suggestions. When suggestion found, I store all mapped field values inside autcomplete object:

source: function (request, response) {
  var autocompleteVals = [];
  var k = 0;

  $().SPServices({
    operation: "GetListItems",
    async: false,
    listName: options.sourceList,
    CAMLViewFields: getViewFields(options.fieldsMapping),
    CAMLQuery: getCamlQuery(options.sourceMatchField, request.term),
    completefunc: function (xData, Status) {
      $(xData.responseXML).SPFilterNode("z:row").each(function () {
        var queryResult = this;
        var fieldsValues = getFieldsValues(options.fieldsMapping, queryResult);
        var labelText = getLabelText(fieldsValues);
        autocompleteVals[k] = {
          label: labelText,
          value: options.fillConcatenatedLabel ? labelText  :
          extractFieldValue(fieldsValues, options.sourceMatchField),
          fieldsValues: fieldsValues
        };

        k++;

        function getLabelText(fieldValues){
          var result = '';
          if(options.labelFields) {
            for(i = 0; i < options.labelFields.length; i++) {	        		               var fieldName = options.labelFields[i];               var fieldVal = extractFieldValue(fieldValues, fieldName);               if(fieldVal != '') {                 if(i > 0) {
                  result += options.labelSeparator;
                }
                result += fieldVal;
              }
            }
          } else {
            result += extractFieldValue(fieldValues, options.sourceMatchField);
          }
          return result;
        }

      });
      response(autocompleteVals);
    }
  });

}

Select function fills values inside mapped fields according to matched item from source lists. It reads values stored inside ui.item and fills corresponding fields based on suggestion selection.

select: function (event, ui) {
  //Fill all depended fields
  $.each(ui.item.fieldsValues, function () {
    var fieldVal = this;
    var fieldInput = $.getFieldControl(fieldVal.key);

    var outputVal = fieldVal.value;

    if (outputVal) {
      var lookupSeparator = ';#';
      if (outputVal.indexOf(lookupSeparator) != -1) {
        var ind = outputVal.indexOf(lookupSeparator);
        var length = lookupSeparator.length;
        var startInd = ind + length;
        outputVal = outputVal.substring(startInd, outputVal.lenght)
      }
      fieldInput.val(outputVal);
    }

  });
}

Maybe you discovered that there are three helper functions inside plugin: getFieldsValues, getViewFields, and getCamlQuery.

getFieldsValues parses SPServices response and fills autocomplete object according to specified fields mapping.

//get values for all mapped fields
function getFieldsValues(fieldsMapping, queryResult) {
  var result = [];
  $.each(fieldsMapping, function () {
    var fieldMapping = this;
    var val = $(queryResult).attr("ows_" + fieldMapping.sourceField);
    result.push({ key: fieldMapping.targetField, value: val, sourceKey: fieldMapping.sourceField});
  });

  var sourceVal = $(queryResult).attr("ows_" + options.sourceMatchField);
  result.push({ value: sourceVal , sourceKey: options.sourceMatchField});

  return result;
}

getViewFields generates ViewFields xml for CAML query according to fields mapping.

//get view fields for all mapped fields
function getViewFields(fieldsMapping) {
  var result = "";
  var isSourceFieldAdded = false;

  if(fieldsMapping){
    $.each(fieldsMapping, function () {
      var mapping = this;
      var viewField = "";
      result += viewField;
    });

    isSourceFieldAdded = fieldsMapping.filter(function(){
      return this.sourceField == options.sourceMatchField;
    }).length > 0;
  }

  if(!isSourceFieldAdded){
    result += "";
  }

  result += "";
  return result;
}

getCamlQuery generates CAML query according to filter column internal name and keyword from input.

//get CAML query for keyword
function getCamlQuery(colname, keyword) {
  var where = "" + keyword + "";
  var orderBy = "";
  var query = "" + where + orderBy + "";
  return query;
}

Updates

03.10.2013

  • Additional optional parameters added: labelFields, labelSeparator, fillConcatenatedLabel.
  • Reading lookup fields from source list implemented.
  • Minor bug fixes.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)