Welcome GuestLogin
Follow Starznet on Twitter for the latest updates
In The Know
RSS

Navigation


Search InTheKnow
»


Categories


The title may infer that this article is about consolidating list data across multiple sites - sorry, it is actually about looking up list data from a single list using the OOTB XsltListViewWebPart in a different site

"Value does not fall within the expected range" - one of the more common errors received when developing with the .NET framework - but one that can usually be resolved very simply - unless of course it is being thrown by the SharePoint API during, what could be considered, a standard user process!

Background

SharePoint Designer 2010 gives user's the ability to save the web part used to display views of a list to it's own .webpart file, enabling the view to be placed on any other pages in the site. This functionality suggests that the web part can then be placed on any page in any site within the site collection.

To save an XsltListViewWebPart instance of a list view, open the nameofview.aspx page in SharePoint Designer 2010, ensure Design view is visible, select the existing web part and use the Save Web Part ribbon commands (found in the Web Part tab).

sharepoint designer save webpart image

When saving the view web part to the site gallery the following dialog will be displayed, select "Yes" to ensure the list reference is NOT relative to the site in which the web part is placed.

save webpart sharepoint designer list reference dialog

So, take the following scenario:

  1. A web application consists of a root site collection and multiple sub-sites.
  2. The root site collection contains a list containing items that pages in the sub-site will be displaying

Error

When editing a page in a sub-site, the Insert web part ribbon panel will not typically show lists or libraries from the root site collection, but having saved an instance of a view web part it will be made available (usually in the miscellaneous group, unless set to another group by the user).

Unfortunately, errors occur when adding the web part to a site other than that where the original list instance exists. After adding the web part onto a page the "Value does not fall within the expected range" exception will occur during certain usage scenarios e.g. paging, filtering, item deletion, addition of other web parts.

value does not fall within the expected range exception image

What makes the error occur

Taking a quick look into the .webpart file that is saved to the Site Gallery shows that all required values are being serialised into the web part, e.g. WebId, ListId and XmlDefinition. (To see this for yourself just export the web part to a file using SharePoint Designer and open it). So what could possibly be wrong? Looking into the exception call stack you will notice a call to Microsoft.SharePoint.WebControls.NewMenu.AddMenuItems(), this is called by the embedded ToolBar, the one which displays the Add new item link at the bottom of the list view, and this is the source of all the errors!

add new menu item link image

This toolbar is created regardless of whether it is actually displayed or not - so simply setting the ToolbarType to None in the .webpart file will not eliminate any of the potential issues.

The problem with the AddMenuItems() method is that internally defaults to using the current context SPWeb in order to locate the RootFolder of the List being queried - now in our case the current context is different to the actual SPWeb where the List resides, hence the Value out of range exception. This does not occur when creating the underlying datasource as this is set to correctly pull in the correct view from the correct list.

addmenuitems code image

The important line in the above source code is line 119. this.Web references the current web where the page housing the web part is located - unfortunately this then causes the call to GetFolder to fail, as the base.RenderContext.RootFolderUrl is referencing the list where the data is.

We know the problem, so how to fix it?

This has certainly been a headache for me recently, and one which I have spent a lot of time trying to resolve. Unfortunately this article is not going to resolve all the issues that occur when trying to view list data from a parent site using a saved instance of the XsltListViewWebPart but it does go some way to enable the majority of use-cases to succeed - that is until Microsoft resolve the issue themselves.

First, we understand where the error occurs - at least we have the call stack telling us where - but how to go about resolving this? Taking a look into the XsltListViewWebPart class you will notice a property named ToolbarControl - this holds a reference to the toolbar which inevitably renders the add new links. Luckily this property exposes the RenderContext object, which is used to populate the Web property of the NewMenu control. So, if we can set the RenderContext's Web to the correct SPWeb then we will no longer get the exception…

Resolution (aka workaround, aka potentially-unsupported-fix)

Below is a class file for a web part, that, when placed on a page will fix-up the RenderContext of the toolbar control. It will find all XsltListViewWebPart's and determine any where the current SPWeb is different to the one necessary to render the list view correctly...and fix it!

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.ComponentModel;
   4:  using System.Linq;
   5:  using System.Reflection;
   6:  using System.Web;
   7:  using Microsoft.SharePoint;
   8:  using Microsoft.SharePoint.WebControls;
   9:  using Microsoft.SharePoint.WebPartPages;
  10:  using WebPart = System.Web.UI.WebControls.WebParts.WebPart;
  11:  
  12:  namespace XlstListModifier.XsltListViewModifier
  13:  {
  14:      /// <summary>
  15:      /// An error exists when an XsltListViewWebPart is retrieving its data from a List on a different SPWeb that that of the 
  16:      /// SPContext.Current.Web. It occurs when the Microsoft.SharePoint.WebControls.NewMenu is rendered (The NewMenu is rendered 
  17:      /// due to the default rendering of the embedded ToolbarControl in the XsltListViewWebPart. During the rendering of the NewMenu
  18:      /// the list of SPContentType's that are used by the SPList being rendered is retrieved:
  19:      /// 
  20:      /// [Microsoft.SharePoint.WebControls.NewMenu.AddMenuItems()]
  21:      /// IList<![CDATA[<SPContentType>]]> list;
  22:      /// if (base.RenderContext.RootFolderUrl != null)
  23:      /// {
  24:      ///     list = new List<![CDATA[<SPContentType>]]>(this.Web.GetFolder(base.RenderContext.RootFolderUrl).ContentTypeOrder);
  25:      /// }
  26:      /// else
  27:      /// {
  28:      ///     list = new List<![CDATA[<SPContentType>]]>(this.List.RootFolder.ContentTypeOrder);
  29:      /// }
  30:      /// 
  31:      /// The base.RenderContext is not null and therefore the first statement gets executed. this.Web is referencing the
  32:      /// RenderContext.Web object (which by default uses the SPContext.Current.SPWeb (unless otherwise specified when creating)
  33:      /// and therefore fails when the RootFolder object is for an SPList in a different SPWeb.
  34:      /// 
  35:      /// The code below sets the RenderContext of the ViewToolBar to the correct SPWeb, as well as setting the SPList and SPView
  36:      /// GUID's. The property exposing the toolbar object is unfortunately an internal property though, hence the use of 
  37:      /// reflection to access the property and set it correctly.
  38:      /// </summary>
  39:      [ToolboxItemAttribute(false)]
  40:      public class XsltListViewModifier : WebPart
  41:      {
  42:          private bool _bFixed;
  43:          private readonly Dictionary<Guid, SPWeb> _oWebs = new Dictionary<Guid, SPWeb>();
  44:  
  45:          protected override void OnInit(EventArgs e)
  46:          {
  47:              base.OnInit(e);
  48:              Title = "XSLT List View Modifier WebPart";
  49:          }
  50:  
  51:          protected override void OnLoad(EventArgs e)
  52:          {
  53:              base.OnLoad(e);
  54:  
  55:              if (!_bFixed && Page.IsPostBack)
  56:              {
  57:                  ApplyFix();
  58:                  _bFixed = true;
  59:              }
  60:          }
  61:  
  62:          protected override void OnPreRender(EventArgs e)
  63:          {
  64:              base.OnPreRender(e);
  65:  
  66:              ApplyFix();
  67:              _bFixed = true;
  68:          }
  69:  
  70:          private void ApplyFix()
  71:          {
  72:              IEnumerable<XsltListViewWebPart> oParts = GetXsltParts();
  73:              foreach (XsltListViewWebPart oPart in oParts.Where(oPart => oPart.WebId != SPContext.Current.Web.ID))
  74:                  SetToolbarContext(oPart);
  75:          }
  76:  
  77:          /// <summary>
  78:          /// Get an array of XsltListViewWebPart's that exist on the current WebPartPage
  79:          /// </summary>
  80:          /// <returns>An array of XstlListViewWebPart's</returns>
  81:          private IEnumerable<XsltListViewWebPart> GetXsltParts()
  82:          {
  83:              return WebPartManager.WebParts.OfType<XsltListViewWebPart>().ToArray();
  84:          }
  85:  
  86:          private void SetToolbarContext(XsltListViewWebPart part)
  87:          {
  88:              try
  89:              {
  90:                  //Get hold of the toolbar that renders the new menu items
  91:                  ViewToolBar oTbar = GetPrivatePropertyValue<ViewToolBar>(part, "ToolbarControl");
  92:                  if (oTbar == null) return;
  93:  
  94:                  //We keep a collection of SPWeb objects in case there is more than on XsltListViewWebPart on
  95:                  //the page looking at lists in more than one site (SPWeb). This way we don't instanciate more
  96:                  //than one SPWeb for each SPWeb needed
  97:                  SPWeb oWb;
  98:                  if (_oWebs.ContainsKey(part.WebId))
  99:                  {
 100:                      oWb = _oWebs[part.WebId];
 101:                  }
 102:                  else
 103:                  {
 104:                      oWb = SPContext.Current.Site.AllWebs[part.WebId];
 105:                      _oWebs.Add(part.WebId, oWb);
 106:                  }
 107:                  
 108:                  //Create the SPContext object that references the correct SPList, SPView and most importantly..SPWeb
 109:                  SPContext oCtx = SPContext.GetContext(HttpContext.Current, new Guid(part.ViewGuid), part.ListId, oWb);
 110:                  //Set the RenderContext of the ToolbarControl
 111:                  oTbar.RenderContext = oCtx;
 112:              }
 113:              catch (Exception)
 114:              { }
 115:          }
 116:  
 117:          public override void Dispose()
 118:          {
 119:              base.Dispose();
 120:  
 121:              //Ensure that any webs are disposed of incase the GC doesn't pick it up during object disposal
 122:              foreach (Guid oId in _oWebs.Keys) _oWebs[oId].Dispose();
 123:          }
 124:  
 125:          public static T GetPrivatePropertyValue<T>(object o, string name)
 126:          {
 127:              if (o == null) throw new ArgumentNullException("o");
 128:              PropertyInfo oInfo = o.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
 129:              if (oInfo == null) throw new ArgumentOutOfRangeException("name", string.Format("Property {0} was not found in Type {1}", name, o.GetType().FullName));
 130:              return (T)oInfo.GetValue(o, null);
 131:          }
 132:      }
 133:  }

OK...so this resolves it, it really does...but notice the use of reflection? Yes, the ToolbarControl property is an internal property, we are not provided access to this using standard API methods, but it is integral to the problem and we MUST solve the RenderContext issue in order to proceed.

Thoughts

Some may consider that viewing list view information in this manner is not actually supposed to occur, others will totally disagree (me included). The facts are simple:

  1. SharePoint Designer 2010 exposes the ability to save the XsltListViewWebPart instances
  2. SharePoint 2010 itself then enables users to add the web part to any page in any site
  3. No other OOTB control or web part exposes the exact same functionality as the XsltListViewWebPart (seriously, try matching it 100% using something else - without using more lines of code than what I posted above!!)
  4. There are certainly cases where viewing information from a list in another site is a business requirement - I wouldn't have spent so long on the issue had it not been a "must have"

So, the trade-off is simple, for the sake of ensuring that any future updates and hot-fixes get tested thoroughly to ensure we don't lose anything by using an internal property, we get an XsltListViewWebPart that supports all OOTB features without spending hours and hours trying to rewrite SharePoint! The errors covered in this article have been reported to Microsoft, and I am currently waiting for any feedback. At least using my approach for fixing the issue, if an official fix is ever released the above web part can just be removed from any pages, instead of painfully replacing a custom carbon copy that will rarely tick all the boxes.

Download

Download the web part code (as posted above)



Comments

© Starznet Ltd 2010

Powered By the excellent ScrewTurn Wiki
Modified on Tuesday, 02 November 2010 22:47 by Stuart Starrs Categorized as Code Examples, Controls, Errors, MOSS 2010