WSS / SharePoint: adding filtering capabilities to the CustomListViewWebPart (Current User Filter and others)

Print Content | More

Two of the biggest limitation of the standard ListViewWebPart is that it’s unable to filter the data using more than one single data provider and that it cannot use multiple values if the provider is capable of providing them (only the first value will be used). This is by design and you cannot avoid it in any way.

To solve both these problems we can take the code we already wrote for our CustomListViewWebPart (see: WSS/SharePoint: Create a Custom ListViewWebPart) and modify it a bit to add the following features:

  • The support for multiple data providers (using the standard SP selection dialog to wire-up the fields with the provider values).
  • The support for multiple values from a single provider.
  • The <where> cause of the filter will be an <or> concatenation of all the values from a single provider, and will be an <and> concatenation of the different providers filter; a pretty standard way to filter the data from a table.

Most of the code is very similar to the previous version, so I will not go through it; let’s see the key points:

Multiple Providers Support

To make this Web Part able to connect to more than one provider, using the standard interfaces provided by SharePoint, we need to create a method with the signature: public void methodname(IFilterValues filterValues) and marked with then ConnectionConsumer attribute that specifies AllowMultipleConnections=true.

Inside that method we call the IFilterValues.SetConsumerParameters function that - given a collection of ConsumerParameter object (that specify the name and the capabilities of the parameters) - displays the standard SharePoint interface that allows you to choose to which field wire-up the provider.

To allow you to filter on all the fields of the list we will build the parameters starting from the list you are displaying through the instance of the Web Part. Here’s the code snippet:

[ConnectionConsumer("Filters", "FiltersForConsumer", AllowsMultipleConnections=true)]
public void SetFilter(IFilterValues filterValues)
{
   if (filterValues != null)
   {
      // EnsureChildControls();
      filterValues.SetConsumerParameters(GetParameters());
 
      FilterProviders.Add(filterValues);
   }
}
 
/// <summary>
/// Build a parameter List to allow the filtering of the List on the values provided
/// by a series of filters.
/// </summary>
/// <returns></returns>
private ReadOnlyCollection<ConsumerParameter> GetParameters()
{
   List<ConsumerParameter> parameters = new List<ConsumerParameter>();
   // we get all the fields of the list we are displaying
   SPList list = SPContext.Current.Web.Lists[SourceList];
   // we build a parameter for any field of the list (similar to the standard ListViewWebPart)
   foreach (SPField item in list.Fields)
      parameters.Add(new ConsumerParameter(item.Title,
          ConsumerParameterCapabilities.SupportsMultipleValues |
          ConsumerParameterCapabilities.SupportsAllValue |
          ConsumerParameterCapabilities.SupportsEmptyValue |
          ConsumerParameterCapabilities.SupportsSingleValue));
 
   return new ReadOnlyCollection<ConsumerParameter>(parameters);
}

FilterProviders is a List<IFilterValues> that will contains the associations made by the user.

Filtering the data

If you review the code from my previous article you will remind that to render the data we used a new instance of an SPView build from the schema of the selected view, and then we altered its properties to apply a fixed filter specified by the user. Here we will do the same but we will build this CAML <where> clause based on the wiring of the providers and consumer specified by the user.

The code is pretty straightforward and doesn’t need much comment, the ‘hardest’ part was to figure out how the CAML to write. Here’s the snippet:

   1: /// <summary>
   2: /// how to concatenate different filters
   3: /// </summary>
   4: const string FiltersConcatenation = "And";
   5: /// <summary>
   6: /// how to concatenate multiple values in the same filter
   7: /// </summary>
   8: const string FiltersMultipleValuesConcatenation = "Or";
   9:  
  10: private void BuildFilter(SPList list, SPView view)
  11: {
  12:    if (FilterProviders.Count == 0)
  13:       view.Query = FilterQuery;
  14:    else
  15:    {
  16:       if (FilterProviders.Count > 0)
  17:       {
  18:          // build the filter according to the passed data
  19:  
  20:          // holds each internal filter
  21:          List<string> filters = new List<string>();
  22:  
  23:          ReadOnlyCollection<string> paramValues;
  24:          foreach (IFilterValues f in FilterProviders)
  25:          {
  26:             paramValues = f.ParameterValues;
  27:  
  28:             if ((paramValues != null) && (paramValues.Count > 0))
  29:             {
  30:                StringBuilder innerSb = new StringBuilder();
  31:                if (paramValues.Count > 1)
  32:                   innerSb.AppendFormat("<{0}>", FiltersMultipleValuesConcatenation);
  33:  
  34:                foreach (var value in paramValues)
  35:                {
  36:                   innerSb.AppendFormat("<Eq><FieldRef Name='{0}' /><Value Type='{1}'>{2}</Value></Eq>", 
  37:                                        list.Fields[f.ParameterName].InternalName,
  38:                                        list.Fields[f.ParameterName].TypeAsString,
  39:                                        value);
  40:                }
  41:                if (paramValues.Count > 1)
  42:                   innerSb.AppendFormat("</{0}>", FiltersMultipleValuesConcatenation);
  43:  
  44:                filters.Add(innerSb.ToString());
  45:             }
  46:          }
  47:          if (filters.Count > 0)
  48:          {
  49:             StringBuilder sb = new StringBuilder();
  50:             sb.Append("<Where>");
  51:  
  52:             if (filters.Count > 1)
  53:                sb.AppendFormat("<{0}>", FiltersConcatenation);
  54:  
  55:             // concatenate the filters
  56:             filters.ForEach(f => sb.Append(f));
  57:  
  58:             if (filters.Count > 1)
  59:                sb.AppendFormat("</{0}>", FiltersConcatenation);
  60:  
  61:             sb.Append("</Where>");
  62:             string query = sb.ToString();
  63:             view.Query = query;
  64:          }
  65:       }
  66:    }
  67: }

As a note: if you keep adding and removing fields to a list in SharePoint you never know how the environment internally renames the fields (it happened to me once that a field with an internal name ‘Owner’ got renamed to ‘Owner1’), and you have no notice about it. The queries however need to be built using the Internal Names of the fields (that are different from those you usually see on the interface, there are lots of books and documentation around that cover this argument), so I opted to store the Display Name as the parameter name and to use that as a lookup field to recover the internal name (and type) when needed - lines 36-38 of the previous code snippet.

Putting all together

After some more refactoring operation, here is the complete source code for the Web Part:

[Guid("b7ea3f7d-b260-4ce8-9fd0-3af5aee8e0d6")]
public class CustomListViewWebPart : System.Web.UI.WebControls.WebParts.WebPart
{
   #region Properties
 
   private readonly ILogger Logger = LoggerFactory.GetLogger();
 
   /// <summary>
   /// The List we are displaying
   /// </summary>
   [Personalizable(true),
   WebBrowsable,
   WebPartStorage(Storage.Shared),
   SPWebCategoryName("Settings"),
   WebDisplayName("Source List Name"),
   WebDescription("Pass the name of the List to show")]
   public string SourceList { get; set; }
 
   /// <summary>
   /// The Default View of the list
   /// </summary>
   [Personalizable(true),
   WebBrowsable,
   WebPartStorage(Storage.Shared),
   SPWebCategoryName("Settings"),
   WebDisplayName("View"),
   WebDescription("Pass the name of the View that you want to apply to the List")]
   public string ViewOfSourceList { get; set; }
 
   /// <summary>
   /// a CAML query to filter the object
   /// </summary>
   /// <remarks>
   /// in a later revision we will use one or more filter providers to set this
   /// </remarks>
   [Personalizable(true),
   WebBrowsable,
   WebPartStorage(Storage.Shared),
   SPWebCategoryName("Settings"),
   WebDisplayName("Query"),
   WebDescription("Pass the Filter Query, if you wire up some Filter WebParts they have priority over the query and this will be ignored")]
   public string FilterQuery { get; set; }
 
   private readonly List<IFilterValues> _filterProviders = new List<IFilterValues>();
 
   private List<IFilterValues> FilterProviders
   {
      get { return _filterProviders; }
   }
 
   #endregion
 
   public CustomListViewWebPart()
   {
      ExportMode = WebPartExportMode.All;
   }
 
   protected override void CreateChildControls()
   {
      base.CreateChildControls();
 
      SPWeb web = SPContext.Current.Web;
      {
         try
         {
            SPList list = web.Lists[SourceList];
 
            // create the toolbar, actually we cannot hide it, we'll need to extend the webpart and those options
            ViewToolBar toolbar = new ViewToolBar();
            SPContext context = SPContext.GetContext(Context, list.Views[ViewOfSourceList].ID, list.ID, SPContext.Current.Web);
            toolbar.RenderContext = context;
            Controls.Add(toolbar);
 
            // get a reference to the view we want to use (or use the default view if nothing is specified)
            SPView webPartView;
            if (!string.IsNullOrEmpty(ViewOfSourceList))
               webPartView = web.Lists[SourceList].Views[ViewOfSourceList];
            else
               webPartView = web.Lists[SourceList].DefaultView;
 
            // create a new view based on the original one and attach the filter query to it
            // in this way we do not need to modify/update the original element and
            // even a user without updating permissions can use this webpart
            XmlDocument domDoc = new XmlDocument();
            domDoc.LoadXml(webPartView.SchemaXml);
            SPView view = new SPView(list, domDoc);
            Logger.AppendLogFormat("View ID: {0}", view.ID);
 
            // build the filter
            BuildFilter(list, view);
 
            // render the view
            Literal lbl = new Literal();
            lbl.Text = view.RenderAsHtml();
            Controls.Add(lbl);
 
            // add the logging messages if there are any
            string lg = Logger.ToString();
            if (!string.IsNullOrEmpty(lg))
            {
               Literal logLbl = new Literal();
               logLbl.Text = "<br/>" + lg;
               Controls.Add(logLbl);
            }
         }
         catch (Exception ex)
         {
            // todo: have a better way to report errors!
            Label lbl = new Label();
            lbl.Text = Logger.ToString() + "<br />";
            lbl.Text += "Error occured: ";
            lbl.Text += ex.Message + "<br />" + ex.StackTrace;
            Controls.Add(lbl);
         }
      }
   }
 
   /// <summary>
   /// how to concatenate different filters
   /// </summary>
   const string FiltersConcatenation = "And";
   /// <summary>
   /// how to concatenate multiple values in the same filter
   /// </summary>
   const string FiltersMultipleValuesConcatenation = "Or";
 
   private void BuildFilter(SPList list, SPView view)
   {
      Logger.AppendLogFormat("Filters numbers: {0}", FilterProviders.Count);
      if (FilterProviders.Count == 0)
         view.Query = FilterQuery;
      else
      {
         if (FilterProviders.Count > 0)
         {
            // build the filter according to the passed data
 
            // holds each internal filter
            List<string> filters = new List<string>();
 
            ReadOnlyCollection<string> paramValues;
            foreach (IFilterValues f in FilterProviders)
            {
               paramValues = f.ParameterValues;
 
               Logger.AppendLogFormat("Filter: {0}", f.ParameterName);
               Logger.AppendLogFormat("Filter Params: {0}", (paramValues != null));
 
               if ((paramValues != null) && (paramValues.Count > 0))
               {
                  Logger.AppendLogFormat("Found filter: {0}  values n: {1}", f.ParameterName, paramValues.Count);
 
                  StringBuilder innerSb = new StringBuilder();
                  if (paramValues.Count > 1)
                     innerSb.AppendFormat("<{0}>", FiltersMultipleValuesConcatenation);
 
                  foreach (var value in paramValues)
                  {
                     innerSb.AppendFormat("<Eq><FieldRef Name='{0}' /><Value Type='{1}'>{2}</Value></Eq>", 
                                          list.Fields[f.ParameterName].InternalName,
                                          list.Fields[f.ParameterName].TypeAsString,
                                          value);
                  }
                  if (paramValues.Count > 1)
                     innerSb.AppendFormat("</{0}>", FiltersMultipleValuesConcatenation);
 
                  filters.Add(innerSb.ToString());
               }
            }
            if (filters.Count > 0)
            {
               StringBuilder sb = new StringBuilder();
               sb.Append("<Where>");
 
               if (filters.Count > 1)
                  sb.AppendFormat("<{0}>", FiltersConcatenation);
 
               // concatenate the filters
               filters.ForEach(f => sb.Append(f));
 
               if (filters.Count > 1)
                  sb.AppendFormat("</{0}>", FiltersConcatenation);
 
               sb.Append("</Where>");
               string query = sb.ToString();
               view.Query = query;
               Logger.AppendLog("query: {0}" + query);
            }
            Logger.AppendLog("query: -");
         }
      }
   }
 
   protected override void Render(HtmlTextWriter writer)
   {
      EnsureChildControls();
      base.Render(writer);
   }
 
   [ConnectionConsumer("Filters", "FiltersForConsumer", AllowsMultipleConnections=true)]
   public void SetFilter(IFilterValues filterValues)
   {
      if (filterValues != null)
      {
         // EnsureChildControls();
         Logger.AppendLog("Assigning filters");
         Logger.AppendLogFormat("Assigning filter: {0}", filterValues.ParameterName);
         Logger.AppendLogFormat("Assigning filter: {0}", (filterValues.ParameterValues != null));
         
         filterValues.SetConsumerParameters(GetParameters());
 
         FilterProviders.Add(filterValues);
 
         Logger.AppendLog("Filters Assigned");
      }
   }
 
   /// <summary>
   /// Build a parameter List to allow the filtering of the List on the values provided
   /// by a series of filters.
   /// </summary>
   /// <returns></returns>
   private ReadOnlyCollection<ConsumerParameter> GetParameters()
   {
      List<ConsumerParameter> parameters = new List<ConsumerParameter>();
      // we get all the fields of the list we are displaying
      SPList list = SPContext.Current.Web.Lists[SourceList];
      // we build a parameter for any field of the list (similar to the standard ListViewWebPart)
      foreach (SPField item in list.Fields)
         parameters.Add(new ConsumerParameter(item.Title,
             ConsumerParameterCapabilities.SupportsMultipleValues |
             ConsumerParameterCapabilities.SupportsAllValue |
             ConsumerParameterCapabilities.SupportsEmptyValue |
             ConsumerParameterCapabilities.SupportsSingleValue));
 
      return new ReadOnlyCollection<ConsumerParameter>(parameters);
   }
 
}

This time I was lazy and I didn’t stripped out the logging code (I’m not particularly proud of that...but solved some logging problems fast).

You can see this Web Part in action in the following screenshots, here I defined a Document Library with a field called ‘Owner’ of type ‘Person or Group’, I dropped this CustomListViewWebPart together with the CurrentUserAndGroupsFilerWebPart from my previous article:

SharePointCLVWB1 SharePointCLVWB2_wp_config

When you drop the Web Part you have to configure some of its basic settings, like:

  • The List that it has to display
  • The View associated to the list to use (optional, if you leave this blank the default view will be used)
  • A custom query (optional, also if you specify some data providers this will be ignored).

And this is the interface you see when wiring the provider and the consumer:

SharePointCLVWB2_config

As you can see the appearance is almost identical to the standard Web Part, but you can filter on more providers and values (as a bonus you have the standard pagination capability too, cause it’s exposed by the view you decided to use).

Using these two simple Web Part we are able to achieve something that is extremely difficult to perform in WSS: filtering the data based on the User currently logged on the website (and I think that this approach is also easier that using the Target Audience in MOSS).

Further improvement to this Web Part will be made in the near future, because I’m hoping to introduce the ability to ‘plug-in’ even the algorithm that builds the actual filter combination; so stay tuned for more.



Sharepointwsscustomcurrent userfilterlistviewwebpart, Wss

10 comments

Related Post

  1. #1 da Guardian - Saturday March 2010 alle 01:15

    I didn't had the time to fix it yet..too busy on other problems, but it shouldn't be hard. I'll try to schedule it in the near future

  2. #2 da PAFleche - Saturday March 2010 alle 01:15

    Hello, I'm ok to provide my modifications... But i did it quickly... so it's a "ugly code"... Moreover, blog's comment is not the right place to write modifications....

  3. #3 da Guardian - Saturday March 2010 alle 01:15

    Np had other requests too for this fix, I'll work on it tonight (I'm sure it's not hard to do) and upload the code tomorrow; so..stay tuned :D

  4. #4 da TheLyvestOne - Saturday March 2010 alle 01:15

    Awesome post!!! Very useful! Regarding the comment above, does the complete source code now incorporate the change to properly construct the CAML filter for "ORs"? PAFleche, can you provide your modification to make this work with more than 2 values? THX!!!

  5. #5 da Guardian - Saturday March 2010 alle 01:15

    You can now find a fixed version at this link: http://www.nablasoft.com/guardian/index.php/2009/11/06/wss-sharepoint-adding-filtering-capabilities-customlistviewwebpart-filter-chain-fixed/

  6. #6 da PAFleche - Saturday March 2010 alle 01:15

    Ok thank you, i just wanna be sure. So i created a recursive function to build the Query.

  7. #7 da Guardian - Saturday March 2010 alle 01:15

    Aye, you're right! In the code above I've made this common mistake you usually do while building CAML queries dynamically. I didn't noticed it cause in production and in all my tests I used only 2 values..so everything worked fine :D.

  8. #8 da PAFleche - Saturday March 2010 alle 01:15

    Hello ! First of all, congratulation and thank you for this code. I just want to say that I noticed a mistake in your code when you build the CAML query when there are several filter values from 1 filter. You build the query like that : ... ... ... Or i thinks that it should be : ... ... ...

  9. #9 da PAFleche - Saturday March 2010 alle 01:15

    Edit : (or) (eq)...(/eq) (eq)...(/eq) (eq)...(/eq) (/or) Or i thinks that it should be : (or) (or) (eq)...(/eq) (eq)...(/eq) (/or) (eq)...(/eq) (/or)

  10. #10 da Anydayaffinna - Wednesday March 2010 alle 11:05

    i easily enjoy your own posting taste, very helpful,
    don't quit as well as keep creating seeing that it just worth to follow it.
    looking forward to look over more and more of your own stories, cheers!

All fields are required and you must provide valid data in order to be able to comment on this post.


(will not be published)
(es: http://www.mysite.com)