Sunday, April 3, 2011

Sharepoint variations guide. Part 1 – overview

In this series of posts I would like to summarize available configuration options for Sharepoint variations and show how they are used in content production. In this first part I will make an overview of Sharepoint functionality, available configuration options and add my comments with details of these options. This material is necessary in order to go with part 2, where I will focus on actual usage of OTB variations for propagating of the content.

As you probably now variations are designed for multilingual sites support in Sharepoint (I recommend you also to check Gary’s Lapointe presentation: Variations and the MUI in SharePoint 2010 and his post Fun with Variations). Here we are talking about variations in Sharepoint 2010 which have several improvements comparing with 2007 version. The most significant improvement is that variations are now propagated via timer jobs (not in w3wp process). There are several different timer jobs related with variations, and in part 2 I will describe each of them – in which case each job is used, how they work with different variations configurations, etc.

Basic idea of variations is simple enough: there is source site (variations source) where content producers publish content (its basic version). There is one variations source site per site collection. There can be also several variations labels (target sites) where changes will be propagated when content producers create new publishing page (or update content on existing publishing page) or create new sub site under variations source site (this sub site will be created under variation targets as well). After that content producers responsible for local content should review propagated content and make translation if necessary. There is one known pain point with it – when source page is published, source page content will be overridden completely (i.e. not only changed part), so people who responsible for managing translated content should revert it and translate changed places manually.

Important thing which you should clearly understand when working with variations is that they do not deal with UI. Sharepoint 2010 has different functionality for supporting multilingual user interface (MUI) (this is out of scope of this article - see Gary’s Lapointe presentation), while variations deal with multilingual content. Variations source site and variations target sites are combined in one entity called variations hierarchy.

It is important to understand also that Variations are part of Publishing infrastructure feature. It means that they work only with publishing pages and publishing sites (plus they affect images located in PublishingImages document library – but not always. Only if you select “Copy resources” in Site settings > Variations > Resources – see below). All another lists and document libraries (both OTB and custom) are not propagated with OTB variations. If you need it you will need to develop custom solution (based on event receivers for example).

As variations exist on site collection level, all configuration is made on site collection’s root site (Site Actions > Site settings). There are 3 variation related settings available:

  • Variations – contains all variation related settings. We will pay special attention on “Automatic Creation” option below. All other options are more or less self descriptive:
Variation setting name Description
Variation Home Type the starting location where variations of this site will be created. Variations of this site will include the site and all subsites.
You can specify root site of site collection to be variation home – in this case you need to specify “/” in this field. The value in this field will be used when you will create variation hierarchy in Site settings > Variation labels (see below).
Automatic Creation Choose whether you want to automatically create corresponding variations when a site is created or a new page is published, or not to create variations automatically.
See part 2 for more details.
Recreate Deleted Target Page Choose whether a new target page should be recreated when the source page is republished if the previous target page has been deleted.
Update Target Page Web Parts Choose whether Web Part changes from source page should be updated to existing target pages. Note that Web Parts personalization will be lost when update is made.
Notification Select the checkbox to have e-mail sent to the contact person of the welcome page of the target site when a new subsite or page is created, or the contact person of a page when a target page is updated with revisions from the source variation.
Resources Choose whether to have new page variations use the same resources as the original page or to use copies of these resources created in the location of the new page.
  • Variations labels – here you create variation hierarchy (source and target sites). Important to know that you can choose site template only for variation source site. This option is disabled for target sites (variation labels) – they will be created using the same site template as source site.
    When you create new variation label you need to specify the following settings:
Variation label setting name Description
Label name and Description Label name is relative URL for the newly created variation site. Full URL is combined from “Variation Home” (see above) and Label name
Display name Technically it will be a title of a newly created site. As official description says: “Most often display names are localized versions or user friendly versions of the label”
Language Specifies language of newly created variation site. This option determines UI language of a site. Amount of available languages depends on how many language packs you installed into the system. Several variations labels may have the same language
Locale Locale specifies format of dates, currency, etc. for newly created variation label site
Hierarchy Creation Specifies what content should be propagated on newly created variation label from source:
- Publishing Sites and All Pages
- Publishing Sites Only
- Root Site Only
Source Variation Specifies whether newly created variation label site will be variation source. It can not be changed once variation hierarchy is created.

When variations hierarchy is defined, you should click “Create Hierarchies” button (in Site settings > Variation labels). After that “Variations Create Hierarchies Job Definition” (see below) will create sites hierarchy.

  • Variation logs – contains log records written by variation-related timer jobs (see below)

Here is table with variation related timer jobs. Descriptions of timer jobs available in Central Administration are not always clear enough – in part 2 I will describe where and how each of job is used:

Job title Description

Variations Create Hierarchies Job Definition 

Creates a complete variations hierarchy by spawning all sites and pages from the source site hierarchy for all Variation labels.
Variations Create Page Job Definition Creates peer pages in variant sites.

Variations Create Site Job Definition

Creates variant sites when the Variations Automatic Creation setting is disabled.
Variations Propagate Page Job Definition Updates peer pages in variant sites.
Variations Propagate Site Job Definition Creates variant sites when the Variations Automatic Creation setting is enabled.

All these jobs are web application specific, so if you have several web applications on your server, these jobs will be created for each of them. Very useful thing with jobs in Sharepoint 2010 is that you can force them from UI (Central Administration > Monitoring > Job Definitions > select job and click Run Now button). It really helps when you make testing of some variation functionality.

The overview of Sharepoint functionality for variations would be complete if I won’t mention Relationships list – hidden list in the root site of site collection where Sharepoint stores relations between variation pages and sites. This list often becomes another pain point for people who work with variations as often relations get corrupted. This theme is out of the scope of my article, for details I send you to mentioned Gary Lapointe blog post.

This is what I would like to show in overview of variations functionality in Sharepoint. In the 2nd part I will show how it is used in content production.

Update 2011-05-01: the 2nd part of the article is published here: Sharepoint variations guide. Part 2– propagation use cases.

Friday, April 1, 2011

Sharepoint MVP 2011

Hello to all readers of my blog. This is the first non-technical post in my technical blog, but I hope that you will forgive me because it worth it :) Today I received a good news from Microsoft: I’ve became Sharepoint MVP 2011:

image

Thanks to community and thanks to Microsoft for the honor! I really hope that my efforts help people in their hard everyday work and thanks in the comments are the best award for me. MVP is indication that my work is not absolutely useless :) I will continue to share my technical knowledge with community using blogs, user groups, forums, twitter, online magazines, open source projects, etc, etc. Thanks everyone who follows my blogs and supports me. Lets go forward and dig into new interesting technical solutions for Sharepoint, ASP.Net, MVC and other technologies.

Wednesday, March 23, 2011

Make SocialCommentWebPart read only dynamically in Sharepoint

SocialCommentWebPart is one of the new features of Sharepoint 2010 which allows users add comments to the pages where this web part is added. This web part makes Sharepoint site more social and increases users communication level. One of the common requirement with this type of functionality is that not all users should be able to leave their comments on site. For example, you can decide that only registered users should be able to leave comments (in case of self-service system), or you can use membership in particular groups in order to grant possibility to leave comments for users. In both cases you will need to make SocialCommentWebPart read only (i.e. allow users only read comments, not post them) dynamically in run time. There is a property SocialCommentWebPart.WebPartPropertyAllowNewComment, but as all other properties it is assumed to be configured once by content producer (or another user with permissions to edit web parts). This is not our goal. Our goal is to allow some users to post comments, while other users should not have this possibility.

If you will check SocialCommentWebPart in reflector you will notice that all functionality is implemented in separate user control SocialCommentControl. It has property AllowNewComment and SocialCommentWebPart sets this property when add SocialCommentControl into controls collection. This property is what we need. But unfortunately it is internal, so we can’t just set it up in our code. Reflection will help us here. Suppose that we put web part on page layout directly (i.e. it is not added via UI on publishing page, but in last case code will remain the same – the only thing you will need to solve is to retrieve web part Id):

   1: <SPSWC:SocialCommentWebPart 
   2:     runat="server" 
   3:     Title="Comments" 
   4:     AllowEdit="True" 
   5:     AllowConnect="True" 
   6:     ConnectionID="00000000-0000-0000-0000-000000000000" 
   7:     IsIncluded="True" 
   8:     Dir="Default" 
   9:     PartImageLarge="" 
  10:     IsVisible="True" 
  11:     AllowMinimize="True" 
  12:     ZoneID="" 
  13:     ID="g_61df80f5_8c13_4355_aeee_a52b497e81c6" 
  14:     FrameState="Normal" 
  15:     ExportMode="All" 
  16:     AllowHide="True" 
  17:     SuppressWebPartChrome="False" 
  18:     DetailLink="" 
  19:     ChromeType="None"
  20:     DescriptionLocId="Null" 
  21:     MissingAssembly="Cannot import this Web Part." 
  22:     PartImageSmall="" 
  23:     AllowRemove="True" 
  24:     HelpMode="Modeless" 
  25:     FrameType="TitleBarOnly" 
  26:     AllowZoneChange="True" 
  27:     PartOrder="1" 
  28:     Description="Enable users to leave short, publicly-viewable notes about this page." 
  29:     HelpLink="" 
  30:     DescriptionLocIdNum="0" 
  31:     ExportControlledProperties="True" 
  32:     IsIncludedFilter="" 
  33:     __MarkupType="vsattributemarkup" 
  34:     __WebPartId="{61df80f5-8c13-4355-aeee-a52b497e81c6}" 
  35:     WebPart="true" 
  36:     Height="" 
  37:     Width="">
  38: </SPSWC:SocialCommentWebPart>

In order to make SocialCommentWebPart read only check the following code which we can add directly into page layout:

   1: <script runat="server" type="text/C#">
   1:  
   2:     protected override void OnLoad(EventArgs e)
   3:     {
   4:         base.OnLoad(e);
   5:         if (yourOwnLogic())
   6:         {
   7:             SocialCommentControl c =
   8:                 ControlFinder.FindChildControl<SocialCommentControl>(g_61df80f5_8c13_4355_aeee_a52b497e81c6);
   9:             if (c != null)
  10:             {
  11:                 ReflectionHelper.CallMethod(c, "set_AllowNewComment", false);
  12:             }
  13:         }
  14:     }
</script>

Function yourOwnLogic() here is any function which you can use in order to determine should current user have possibility to post comments or not (as I said above you can use membership in Sharepoint groups for example). Here we use 2 external components: ControlFinder and ReflectionHelper. They are not complicated. ControlFinder searches for control of specified generic type in controls collection recursively:

   1: public static class ControlFinder
   2: {
   3:     public static T FindChildControl<T>(Control startingControl) where T : Control
   4:     {
   5:         T found = null;
   6:         foreach (Control c in startingControl.Controls)
   7:         {
   8:             found = c as T;
   9:             if (found == null)
  10:             {
  11:                 found = FindChildControl<T>(c);
  12:             }
  13:             if (found != null)
  14:             {
  15:                 break;
  16:             }
  17:         }
  18:         return found;
  19:     }
  20: }

ReflectionHelper is a helper class which can be used if you need to play with internal details of Sharepoint (I understand that it can be necessary in some cases, but don’t make it too much as it non supported way to work with Sharepoint):

   1: public static class ReflectionHelper
   2: {
   3:     public static object CallMethod(object obj, string name, params object[] argv)
   4:     {
   5:         BindingFlags bf = 0 | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
   6:         MethodInfo mi = obj.GetType().FindMembers(MemberTypes.Method, bf, Type.FilterName, name)[0] as MethodInfo;
   7:         return mi.Invoke(obj, argv);
   8:     }
   9:  
  10:     public static object CallStaticMethod(Type t, string name, params object[] argv)
  11:     {
  12:         BindingFlags bf = 0 | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
  13:         MethodInfo mi = t.FindMembers(MemberTypes.Method, bf, Type.FilterName, name)[0] as MethodInfo;
  14:         return mi.Invoke(null, argv);
  15:     }
  16:  
  17:     public static object GetStaticData(Type t, string name)
  18:     {
  19:         BindingFlags bf = 0 | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
  20:         FieldInfo fi = t.FindMembers(MemberTypes.Field, bf, Type.FilterName, name)[0] as FieldInfo;
  21:         return fi.GetValue(null);
  22:     }
  23: }

After this SocialCommentWebPart will be read only for users which don’t have necessary permissions, and will allow post comments for users which have such permissions.

Sunday, March 6, 2011

SPGridView with filtering by multiple columns and support of TaxonomyFieldMulti

In this post I would like to share implementation of advanced SPGridView control which has more features comparing with OTB SPGridView. As you probably now SPGridView control can be used in Sharepoint in order to display list-like data. I.e. with its header you can sort item, filter them – and it will work with OTB look and feel. However there is a lack of important features in OTB SPGridView: it supports filtering only by single column. Also if you use SPGridView for displaying some multi values field (e.g. TaxonomyFieldMulti or LookupMulti) – you will need to concatenate all values for single field into one string using some separator (e.g. string.Join(“,”, values)). The disadvantage of this approach is that from SPGridView point of view it will be single string value. As result in filter menu it will be displayed as single string.

Lets consider example: suppose that we have Language taxonomy field which allows multiple values (TaxonomyFieldMulti). It allows you to specify several languages: English, Finnish, Russian, Sweden. Suppose that we selected all of these 4 values for some list item. In our data source we concatenate these values into one string. Now lets see how it will be displayed in OTB SPGridView filter menu:

image

See that all values are shown as one filtering option. It means that when you will select this option you will get only those items which have absolutely the same value in Language field. Event if Language field of another item has the same set of Language but in different order (e.g. Finnish, English, Russian, Sweden) – it will be treat as different value and another filtering option will appear in menu. Now lets see how it looks like in OTB XsltListViewWebPart:

image

See that all options are presented as separate filtering option. That is our goal – to add this functionality in OTB SPGridView control.

But lets start with another problem: multi column filter. As I said above OTB SPGridView control supports filtering only by one column. I.e. you can apply filter on several columns simultaneously. This problem was solved by Erik Burger in his great blog post: Building A SPGridView Control – Part 4: Filtering Multiple Columns. Thanks to Erik for sharing his solution. It works successfully both for Sharepoint 2007 and 2010. However it has own problems: currently selected filter value is not shown in the filtering menu like it is shown in OTB SPGridView:

image

Well we are going to solve this problem as well.

Our solution will consist from 2 parts: server side control – inheritor of OTB SPGridView control and customized javascript aspgridview.js which overrides several functions from OTB spgridview.js. Actually I’m going to use solution from the Erik’s post – just will expand it for mentioned features. So you should definitely check his post first – it contains explanation how it works generally. And after that you can return to my post, because I won’t repeat whole explanation here.

So here is the code of changed SPGridViewProxy:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Text;
   5: using System.Text.RegularExpressions;
   6: using Microsoft.SharePoint.WebControls;
   7: using Menu = Microsoft.SharePoint.WebControls.Menu;
   8: using System.Web.UI;
   9: using System.Reflection;
  10: using System.Web.UI.WebControls;
  11:  
  12: namespace CustomControls
  13: {
  14:     public class SPGridViewProxy : SPGridView, IPostBackEventHandler
  15:     {
  16:         // Contains names of fields splited by comma which may contain several values.
  17:         // E.g. some Taxonomy fields may have several values (Language = "Finnish,Russian").
  18:         // These fileds will be filtered using LIKE expression instead of "=" operation
  19:         public string MultiValuesFields { get; set; }
  20:  
  21:         protected override void OnLoad(EventArgs args)
  22:         {
  23:             base.OnLoad(args);
  24:             ScriptLink.Register(this.Page, "Fiskars/scripts/aspgridview.js", false);
  25:         }
  26:  
  27:         protected override void Render(System.Web.UI.HtmlTextWriter writer)
  28:         {
  29:             base.Attributes["callbackEventReference"] =
  30:               base.Attributes["callbackEventReference"].Replace("SPGridView_FilterCallbackHandler",
  31:                                                                  "ASPGridView_FilterCallbackHandler");
  32:  
  33:             MenuItemTemplate t = this.FindControl("SPGridViewFilterMenuTemplate").Controls[3] as MenuItemTemplate;
  34:             t.ClientOnClickScript = t.ClientOnClickScript.Replace("__ClearFilter__", "__ClearFilter__;%FIELDNAME%");
  35:  
  36:             if (this.HeaderRow != null)
  37:             {
  38:                 foreach (TableCell cell in this.HeaderRow.Cells)
  39:                 {
  40:                     Menu menu = (cell.Controls[0] as Menu);
  41:                     if (menu != null)
  42:                     {
  43:                         menu.ClientOnClickPreMenuOpen =
  44:                           menu.ClientOnClickPreMenuOpen.Replace("SPGridView_FilterPreMenuOpen",
  45:                                                                  "ASPGridView_FilterPreMenuOpen");
  46:                     }
  47:                 }
  48:             }
  49:  
  50:             base.Render(writer);
  51:         }
  52:  
  53:         protected new void DoFilterPostBackEventProcessing(string filterData)
  54:         {
  55:             string filterExpression = String.Empty;
  56:             if (filterData.StartsWith("__ClearFilter__"))
  57:             {
  58:                 string[] data = filterData.Split(';');
  59:                 string fieldName = data[1];
  60:  
  61:                 string oldFilterFieldName = base.Attributes["filterFieldName"];
  62:                 List<string> fieldNames = new List<string>(oldFilterFieldName.Split(';'));
  63:                 fieldNames.Remove(fieldName);
  64:                 base.Attributes["filterFieldName"] = String.Join(";", fieldNames.ToArray());
  65:  
  66:                 string oldFilterExpression = Convert.ToString(ViewState["FilterExpression"]);
  67:                 string[] filters = oldFilterExpression.Split(new string[] { " AND " }, StringSplitOptions.RemoveEmptyEntries);
  68:                 List<string> newFilters = new List<string>();
  69:                 foreach (string filter in filters)
  70:                 {
  71:                     if (!filter.Contains(String.Format("[{0}]", fieldName)))
  72:                     {
  73:                         newFilters.Add(filter);
  74:                     }
  75:                 }
  76:                 filterExpression = String.Join(" AND ", newFilters.ToArray());
  77:             }
  78:             else
  79:             {
  80:                 filterExpression = ExtractFilterExpression(filterData);
  81:             }
  82:  
  83:             ViewState["FilterExpression"] = filterExpression;
  84:             base.Attributes["filterExpression"] = filterExpression;
  85:  
  86:             if (filterExpression == null) { return; }
  87:  
  88:             DataSourceControl control = this.NamingContainer.FindControl(this.DataSourceID) as DataSourceControl;
  89:             PropertyInfo pi = control.GetType().GetProperty("FilterExpression");
  90:             pi.SetValue(control, filterExpression, null);
  91:  
  92:             base.RequiresDataBinding = true;
  93:         }
  94:  
  95:         private string ExtractFilterExpression(string filterData)
  96:         {
  97:             string[] data = filterData.Split(';');
  98:             string fieldName = data[0];
  99:             if (String.IsNullOrEmpty(fieldName))
 100:             {
 101:                 return null;
 102:             }
 103:  
 104:             string oldFilterFieldName = base.Attributes["filterFieldName"];
 105:             if (oldFilterFieldName != null)
 106:             {
 107:                 if (!oldFilterFieldName.Contains(fieldName))
 108:                 {
 109:                     base.Attributes["filterFieldName"] += ";" + fieldName;
 110:                 }
 111:             }
 112:             else
 113:             {
 114:                 base.Attributes.Add("filterFieldName", fieldName);
 115:             }
 116:  
 117:             string fieldValue = data[1].Replace("%3b", ";").Replace("%25", "%");
 118:  
 119:             string oldFilterExpression = Convert.ToString(ViewState["FilterExpression"]);
 120:             string newFilterExpression = "";
 121:             // if field is multi value we need to perform search using LIKE expression instead of "=" because
 122:             // multi values are displayed as single string
 123:             if (this.isMultipleValues(fieldName))
 124:             {
 125:                 newFilterExpression = String.Format("[{0}] LIKE '%{1}%'", fieldName, fieldValue);
 126:             }
 127:             else
 128:             {
 129:                 newFilterExpression = String.Format("[{0}] = '{1}'", fieldName, fieldValue);
 130:             }
 131:  
 132:             if (!String.IsNullOrEmpty(oldFilterExpression))
 133:             {
 134:                 if (oldFilterExpression.Contains(newFilterExpression))
 135:                 {
 136:                     newFilterExpression = oldFilterExpression;
 137:                 }
 138:                 // also we need to check that current filed name already exists in the old filter expression
 139:                 // with another filed value. We need to replace it with new value. Otherwise it will be possible to filter
 140:                 // within single column using several values. This is not OTB behavior. Sharepoint allows to select only one
 141:                 // field value per column
 142:                 else if (oldFilterExpression.Contains(string.Format("[{0}]", fieldName)))
 143:                 {
 144:                     string[] filters = oldFilterExpression.Split(new string[] { " AND " }, StringSplitOptions.RemoveEmptyEntries);
 145:                     filters = filters.Where(f => !f.Contains(string.Format("[{0}]", fieldName))).ToArray();
 146:                     if (filters.Length != 0)
 147:                     {
 148:                         newFilterExpression = String.Join(" AND ",
 149:                                                           new[] {String.Join(" AND ", filters), newFilterExpression});
 150:                     }
 151:                 }
 152:                 else
 153:                 {
 154:                     newFilterExpression = String.Join(" AND ",
 155:                                                   new [] { oldFilterExpression, newFilterExpression });
 156:                 }
 157:             }
 158:  
 159:             return newFilterExpression;
 160:         }
 161:  
 162:         private bool isMultipleValues(string fieldName)
 163:         {
 164:             try
 165:             {
 166:                 if (string.IsNullOrEmpty(this.MultiValuesFields))
 167:                 {
 168:                     return false;
 169:                 }
 170:                 var multiValuesFields = this.MultiValuesFields.Split(new[] {","}, StringSplitOptions.RemoveEmptyEntries);
 171:                 if (multiValuesFields.Length == 0)
 172:                 {
 173:                     return false;
 174:                 }
 175:                 return multiValuesFields.Any(t => t == fieldName);
 176:             }
 177:             catch
 178:             {
 179:                 return false;
 180:             }
 181:         }
 182:  
 183:         private string GetValueAfterPrefix(string value, string prefix)
 184:         {
 185:             if (String.IsNullOrEmpty(value) ||
 186:                  String.IsNullOrEmpty(prefix) ||
 187:                  !value.StartsWith(prefix))
 188:             {
 189:                 return null;
 190:             }
 191:  
 192:             if (prefix.Length < value.Length)
 193:             {
 194:                 return value.Substring(prefix.Length);
 195:             }
 196:  
 197:             return String.Empty;
 198:         }
 199:  
 200:         #region IPostBackEventHandler Members
 201:  
 202:         void IPostBackEventHandler.RaisePostBackEvent(string eventArgument)
 203:         {
 204:             string valueAfterPrefix = GetValueAfterPrefix(eventArgument, "__SPGridView__;");
 205:             if (valueAfterPrefix != null)
 206:             {
 207:                 string filterData = GetValueAfterPrefix(valueAfterPrefix, "__Sort__;");
 208:                 if (filterData == null)
 209:                 {
 210:                     filterData = GetValueAfterPrefix(valueAfterPrefix, "__Filter__;");
 211:                     if (filterData != null)
 212:                     {
 213:                         DoFilterPostBackEventProcessing(filterData);
 214:                     }
 215:                 }
 216:                 else
 217:                 {
 218:                     DoSortPostBackEventProcessing(filterData);
 219:                 }
 220:             }
 221:             else
 222:             {
 223:                 base.RaisePostBackEvent(eventArgument);
 224:             }
 225:         }
 226:  
 227:         #endregion
 228:     }
 229: }

I will describe only differences from the Erik’s post. First of all I added MultiValuesFields property (line 19) – using this property you can tell to custom SPGridViewProxy control what values are really multi values and should be split into tokens. For our case we will specify MultiValuesFields=”Language”. However you can specify as many fields as you need here separated by comma.

Next check the line 84:

   1: base.Attributes["filterExpression"] = filterExpression;

We need to store actual filter expression in the attribute as it will be needed on client side in javascript.

Another change is lines 120-130. Here we make actual trick for multiple values fields. We check whether or not current field name corresponds to a field with multiple values (using MultiValuesFields property mentioned above). If yes – then instead of simple equal expression (“=”) we will use LIKE expression. E.g. if we select Finnish language then filter expression will be: [Language] LIKE ‘%Finnish%’. So our list item with Language = “English,Finnish,Russian,Sweden” will match this filter.

Next change on lines 138-150. Here I added functionality which restricts number of selected filtering values for single column by one value. This is how OTB SPGridView and XsltListViewWebForms work. However it is not very hard to expand it to several values per column and if you will need it – you can implement it by yourself.

The last change is isMultipleValues() function (lines 162-181). It just check whether or not specified field name exists in MultiValuesFields property of the control.

Now lets check client side of the solution:

   1: function ASPGridView_FilterPreMenuOpen(gridViewClientId, templateClientId, menuClientId, dataFieldName, e) {
   2:     var gridView = document.getElementById(gridViewClientId);
   3:     var callbackEventReference = gridView.getAttribute("callbackEventReference");
   4:     var callbackArgumentPrefix = gridView.getAttribute("callbackArgumentPrefix");
   5:     var postbackEventReference = gridView.getAttribute("postbackEventReference");
   6:     var filterFieldName = gridView.getAttribute("filterFieldName");
   7:     var filterFieldValue = gridView.getAttribute("filterFieldValue");
   8:  
   9:     var filterCurrentlyOn = false;
  10:     if ((filterFieldName != null) &&
  11:         (filterFieldName.length > 0) &&
  12:         (filterFieldName.match(dataFieldName) != null)) 
  13:     {
  14:         filterCurrentlyOn = true;
  15:     }
  16:  
  17:     var menuTemplate = document.getElementById(templateClientId);
  18:     var menuLink = document.getElementById(menuClientId);
  19:     if ((menuLink.getAttribute("suppressBubbleIfPostback") != null) &&
  20:         (e != null) && (e.srcElement != null) && 
  21:         (e.srcElement.href != null) &&
  22:         (e.srcElement.href.substr(0, MMU_postbackPrefix.length) == MMU_postbackPrefix)) 
  23:     {
  24:         event.cancelBubble = true;
  25:         return;
  26:     }
  27:     SPGridView_FixupFilterValuesFromMenuTemplate(menuTemplate, filterCurrentlyOn);
  28:     var menuItem = CAMOpt(menuTemplate, L_Loading_Text, "null");
  29:     menuItem.setAttribute("isFilterItem", "true");
  30:     menuItem.setAttribute("enabled", "false");
  31:     SPGridView_CallbackContext = new Object();
  32:     SPGridView_CallbackContext.gridViewClientId = gridViewClientId;
  33:     SPGridView_CallbackContext.templateClientId = templateClientId;
  34:     SPGridView_CallbackContext.menuClientId = menuClientId;
  35:     SPGridView_CallbackContext.dataFieldName = dataFieldName;
  36:     callbackEventReference = callbackEventReference.replace(/__CALLBACKARGUMENT__/g, dataFieldName);
  37:     callbackEventReference = callbackEventReference.replace(/__CALLBACKCONTEXT__/g, gridViewClientId + ";" + templateClientId + ";" + menuClientId + ";" + dataFieldName);
  38:     eval(callbackEventReference);
  39: }
  40:  
  41: function Array_Contains(a, obj) {
  42:     for(var i = 0; i < a.length; i++) {
  43:         if(a[i] === obj) {
  44:             return true;
  45:         }
  46:     }
  47:     return false;
  48: }
  49:  
  50: function ASPGridView_IsNullOrUndefined(val) {
  51:     return (val == null || typeof(val) == 'undefined');
  52: }
  53:  
  54: function ASPGridView_IsValueInFilter(fieldName, fieldVal, filterExpression) {
  55:     if (ASPGridView_IsNullOrUndefined(fieldName) || ASPGridView_IsNullOrUndefined(fieldVal) ||
  56:         ASPGridView_IsNullOrUndefined(filterExpression)) {
  57:         return false;
  58:     }
  59:  
  60:     fieldName = fieldName.toLowerCase();
  61:     fieldVal = fieldVal.toLowerCase();
  62:     filterExpression = filterExpression.toLowerCase();
  63:  
  64:     var case1 = '[' + fieldName + "] = '" + fieldVal + "'";
  65:     var case2 = '[' + fieldName + "] like '%" + fieldVal + "%'";
  66:     return (filterExpression.indexOf(case1) != -1 || filterExpression.indexOf(case2) != -1);
  67: }
  68:  
  69: function ASPGridView_FilterCallbackHandler(result, context) {
  70:     var values = context.split(';');
  71:     if (values.length != 4) { alert("ERROR: ASPGridView_FilterCallbackHandler() - values.length !=4"); return; }
  72:     var gridViewClientId = values[0];
  73:     var templateClientId = values[1];
  74:     var menuClientId = values[2];
  75:     var dataFieldName = values[3];
  76:     var gridView = document.getElementById(gridViewClientId);
  77:     if (gridView == null) { alert("ERROR: ASPGridView_FilterCallbackHandler() - gridView==null"); return; }
  78:     var menuTemplate = document.getElementById(templateClientId);
  79:     if (menuTemplate == null) { alert("ERROR: ASPGridView_FilterCallbackHandler() - menuTemplate==null"); return; }
  80:     var menu = document.getElementById(menuClientId);
  81:     if (menu == null) { alert("ERROR: ASPGridView_FilterCallbackHandler() - menu==null"); return; }
  82:     var postbackEventReference = gridView.getAttribute("postbackEventReference");
  83:     if ((postbackEventReference == null) || (postbackEventReference.length <= 0)) { alert("ERROR: ASPGridView_FilterCallbackHandler() - postbackEventReference is null or empty"); return; }
  84:     var filterFieldName = gridView.getAttribute("filterFieldName");
  85:     var filterFieldValue = gridView.getAttribute("filterFieldValue");
  86:     var filterExpression = gridView.getAttribute("filterExpression");
  87:     
  88:     var filterCurrentlyOn = false;
  89:     if ((filterFieldName != null) &&
  90:         (filterFieldName.length > 0) &&
  91:         (filterFieldName.match(dataFieldName) != null)) {
  92:         filterCurrentlyOn = true;
  93:     }
  94:  
  95:     SPGridView_FixupFilterValuesFromMenuTemplate(menuTemplate, filterCurrentlyOn);
  96:     values = result.split(';');
  97:  
  98:     // collect unique values
  99:     var uniqueValues = new Array();
 100:     for (var valueIndex = 0; valueIndex < values.length; valueIndex++) {
 101:         var value = unescape(values[valueIndex]);
 102:         if (typeof (value) != 'undefined' && value != null) {
 103:             var multipleValues = value.split(','); // the same as used for joining TaxonomyFieldTypeMulti
 104:             if (typeof (multipleValues) != 'undefined' && multipleValues != null && multipleValues.length > 0) {
 105:                 for (var multiValueIndex = 0; multiValueIndex < multipleValues.length; multiValueIndex++) {
 106:                     var v = multipleValues[multiValueIndex];
 107:                     if (!Array_Contains(uniqueValues, v)) {
 108:                         uniqueValues.push(v);
 109:                     }
 110:                 }
 111:             }
 112:         }
 113:     }
 114:  
 115:     // create menu items only for unique values
 116:     for (var valueIndex = 0; valueIndex < uniqueValues.length; valueIndex++) {
 117:         var value = unescape(uniqueValues[valueIndex]);
 118:         var script = postbackEventReference.replace(/__POSTBACKARGUMENT__/g, dataFieldName + ";" + value.replace(/\\/g, "\\\\").replace(/\'/g, "\\'").replace(/%/g, "%25").replace(/;/g, "%3b"));
 119:         var newMenuItem = CAMOpt(menuTemplate, value, script);
 120:         newMenuItem.setAttribute("isFilterItem", "true");
 121:         if (value == filterFieldValue || ASPGridView_IsValueInFilter(dataFieldName, value, filterExpression)) {
 122:             newMenuItem.setAttribute("checked", "true");
 123:         }
 124:     }
 125:     HideMenu(menuTemplate);
 126:     MMU_Open(menuTemplate, menu);
 127: }

Lets also check changes comparing with Erik’s solution. I added 3 helper functions: Array_Contains(), ASPGridView_IsNullOrEmpty() and ASPGridView_IsValueInFilter() (see lines 41-58). I didn’t want to use jquery here in order to avoid introducing dependencies on external libraries and in order to keep things as much easier to use as possible. Check line 86:

   1: var filterExpression = gridView.getAttribute("filterExpression");

Remember that I told you above that we need to store whole filter expression in the attributes collection in order to use it in javascript.

In lines 98-112 we split all multi values fields into single array of unique values. These values will be used for filter menu in lines 115-124. Note that on the line 121 we use ASPGridView_IsValueInFilter() in order to check selected filter values in the menu.

That’s how it works. Again thanks to Erik for his solution. I also realize that my solution is not ideal – and it contains its own problems (e.g. in javascript we split unique values for all fields regardless of is this field multi value or not). Howhever it works for our case. And I think it can be useful for your cases as well. Anyway if you will find any problems in it I will appreciate if you will share it in this post’s comments. And I will double appreciate if you will share solution for it as well :).