I had a lot of good feedbacks on that SharePoint filtering articles series, the guys that wrote me spotted on a nasty bug on how I was building the chain of filters in CAML.

In my code I did something like: <or><eq>...</eq><eq>...</eq><eq>...</eq></or> which is WRONG! Due to the lack of a good testing (I worked with only two values I didn’t get it at start).

The correct chain should be: <or><or><eq>...</eq><eq>...</eq></or><eq>...</eq></or>.

Today I found 1 hour to fix the problem which was present in two pieces of the code: once when building the single filters and once when concatenating the multiple filters to obtain the final query.

Here’s the code that contains the bug fixes. As a note to all my readers: again I did not had time to test it deeply (I checked the algorithm to generate the query, but I didn’t tested it against a SharePoint database due to the lack of time), so check it before using it in a production environment.
[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
            ApplyFilters(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 ApplyFilters(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 = BuildFiltersList(list);

            // assign the list of filters, concatenate them if more than one
            if (filters.Count > 0)
            {
               StringBuilder sb = new StringBuilder();
               sb.Append("<Where>");

               if (filters.Count == 1)
                  sb.Append(filters[0]);
               else
               {
                  // we have more than 1 filter and we need to concat
                  for (int i = 0; i < filters.Count; i++)
                  {
                     if (i != 1)
                        sb.Insert(0, String.Format("<{0}>", FiltersConcatenation));
                     sb.Append(filters[i]);
                     if (i > 0)
                        sb.AppendFormat("</{0}>", FiltersConcatenation);
                  }
               }

               sb.Append("</Where>");

               string query = sb.ToString();
               view.Query = query;
               _logger.AppendLog("query: {0}" + query);
            }
            _logger.AppendLog("query: -");
         }
      }
   }

   /// <summary>
   /// build the filter list to apply to the view
   /// </summary>
   /// <param name="list"></param>
   /// <returns></returns>
   private List<string> BuildFiltersList(SPList list)
   {
      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))
         {
            StringBuilder innerSb = new StringBuilder();
            _logger.AppendLogFormat("Found filter: {0}  values n: {1}", f.ParameterName, paramValues.Count);

            // check for single parameter value
            if (paramValues.Count == 1)
               innerSb.AppendFormat("<Eq><FieldRef Name='{0}' /><Value Type='{1}'>{2}</Value></Eq>",
                                    list.Fields[f.ParameterName].InternalName,
                                    list.Fields[f.ParameterName].TypeAsString,
                                    paramValues[0]);
            else
            {
               // multiple values we need to concat in the right way for a CAML query
               for (int i = 0; i < paramValues.Count; i++)
               {
                  if (i != 1)
                     innerSb.Insert(0, String.Format("<{0}>", FiltersMultipleValuesConcatenation));
                  innerSb.AppendFormat("<Eq><FieldRef Name='{0}' /><Value Type='{1}'>{2}</Value></Eq>",
                                       list.Fields[f.ParameterName].InternalName,
                                       list.Fields[f.ParameterName].TypeAsString,
                                       paramValues[i]);
                  if (i > 0)
                     innerSb.AppendFormat("</{0}>", FiltersMultipleValuesConcatenation);
               }
            }

            filters.Add(innerSb.ToString());
         }
      }
      return filters;
   }

   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);
   }

}
Any further suggestion, modification and fixes are welcome :D

Related Content