SPLimitedWebPartManager.AddWebPart Mysteriously Increments zoneIndex
One of the common tasks when using the "DR.DADA" approach to SharePoint development is programmatically creating and configuring pages on a site. This often requires adding numerous Web Parts to various zones on a page -- for example, to configure search results pages.
Over the past couple of years, my teammates and I have created numerous "helper" classes that simplify this task in Windows SharePoint Services (WSS) 3.0 and Microsoft Office SharePoint Server (MOSS) 2007 -- including SharePointWebPartHelper
.
/// <summary>
/// Exposes static methods for commonly used helper functions for
/// SharePoint Web Parts. This class cannot be inherited.
/// </summary>
/// <remarks>
/// All methods of the <b>SharePointWebPartHelper</b> class are static and
/// can therefore be called without creating an instance of the class.
/// </remarks>
public sealed class SharePointWebPartHelper
SharePointWebPartHelper
contains public methods like EnsureWebPart
and FindWebPartByTitle
. As you can probably tell from the names of these methods, this allows you to locate an existing Web Part on a page or create one if the specified Web Part doesn't currently exist on the page.
EnsureWebPart
attempts to find a Web Part based on the specified webPartId and if the Web Part is not found, then it uses the CreateWebPart
method to create a new Web Part. Here is the original implementation of the CreateWebPart
method:
private static WebPart CreateWebPart(
SPWebPartPages.SPLimitedWebPartManager wpm,
string webPartId,
string webPartFilename,
string zoneId,
ref int zoneIndex)
{
Debug.Assert(wpm != null);
Debug.Assert(string.IsNullOrEmpty(webPartId) == false);
Debug.Assert(string.IsNullOrEmpty(webPartFilename) == false);
Debug.Assert(string.IsNullOrEmpty(zoneId) == false);
Logger.LogDebug(
CultureInfo.InvariantCulture,
"Creating Web Part ({0}) on page ({1})...",
webPartId,
wpm.ServerRelativeUrl);
WebPart webPart = null;
SPFile wpFile = GetWebPartFileFromGallery(
wpm.Web.Site,
webPartFilename);
Debug.Assert(wpFile != null);
string errorMessage = string.Empty;
using (Stream stream = wpFile.OpenBinaryStream())
{
XmlReader reader = XmlReader.Create(stream);
webPart = wpm.ImportWebPart(reader, out errorMessage);
}
if (string.IsNullOrEmpty(errorMessage) == false)
{
string message =
"Error importing Web Part ("
+ webPartFilename + "): "
+ errorMessage;
throw new ApplicationException(message);
}
webPart.ID = webPartId;
wpm.AddWebPart(webPart, zoneId, zoneIndex);
zoneIndex++;
Logger.LogDebug(
CultureInfo.InvariantCulture,
"Successfully created Web Part ({0}) on page ({1}).",
webPartId,
wpm.ServerRelativeUrl);
return webPart;
}
Note that SPWebPartPages
is simply an alias for the Microsoft.SharePoint.WebPartPages
namespace (in order to avoid "collisions" with the System.Web.UI.WebControls.WebParts
namespace). Also, please ignore the use of ApplicationException
-- apparently there's still a little "code cleanup" to be done here.
As I called out above, this code shows the original implementation of the method. It turns out there's a very obscure bug in it, that I only discovered within the last couple of months.
As you can probably guess from the title of this post, the problem is that zoneIndex
(i.e. the position of the Web Part relative to other Web Parts within the same Web Part zone) isn't always incremented as you would expect.
When adding the first, second, third, and fourth Web Parts to a zone:
SPLimitedWebPartManager.AddWebPart(webPart, zoneId, zoneIndex)
behaves as expected, meaning that (webPart.ZoneIndex == zoneIndex
).
However, when adding the fifth and sixth Web Parts to a zone, then (webPart.ZoneIndex == zoneIndex + 1
).
I suspect it is due to the SPWebPartManager.MakeSpaceForWebpart
method (which is called from SPLimitedWebPartManager.AddWebPart
). However, the SPWebPartManager.MakeSpaceForWebpart
method is obfuscated and therefore I can’t see what it is doing using Reflector.
This behavior caused some Web Parts to appear in the wrong location, and therefore I had to come up with a hack for it. Here is my updated version of the CreateWebPart
method:
private static WebPart CreateWebPart(
SPWebPartPages.SPLimitedWebPartManager wpm,
string webPartId,
string webPartFilename,
string zoneId,
ref int zoneIndex)
{
Debug.Assert(wpm != null);
Debug.Assert(string.IsNullOrEmpty(webPartId) == false);
Debug.Assert(string.IsNullOrEmpty(webPartFilename) == false);
Debug.Assert(string.IsNullOrEmpty(zoneId) == false);
Logger.LogDebug(
CultureInfo.InvariantCulture,
"Creating Web Part ({0}) on page ({1})...",
webPartId,
wpm.ServerRelativeUrl);
WebPart webPart = null;
SPFile wpFile = GetWebPartFileFromGallery(
wpm.Web.Site,
webPartFilename);
Debug.Assert(wpFile != null);
string errorMessage = string.Empty;
using (Stream stream = wpFile.OpenBinaryStream())
{
XmlReader reader = XmlReader.Create(stream);
webPart = wpm.ImportWebPart(reader, out errorMessage);
}
if (string.IsNullOrEmpty(errorMessage) == false)
{
string message =
"Error importing Web Part ("
+ webPartFilename + "): "
+ errorMessage;
throw new ApplicationException(message);
}
webPart.ID = webPartId;
wpm.AddWebPart(webPart, zoneId, zoneIndex);
// Bug 68288:
//
// HACK: When adding the first, second, third, and fourth Web Parts
// to a zone:
//
// SPLimitedWebPartManager.AddWebPart(webPart, zoneId, zoneIndex)
//
// behaves as expected, meaning that
// (webPart.ZoneIndex == zoneIndex).
//
// However, when adding the fifth and sixth Web Parts to a zone,
// then (webPart.ZoneIndex == zoneIndex + 1).
if (webPart.ZoneIndex > zoneIndex)
{
Debug.Assert(zoneIndex >= 4);
Logger.LogDebug(
CultureInfo.InvariantCulture,
"HACK: webPart.ZoneIndex ({0})"
+ " is greater than zoneIndex ({1})."
+ " Setting zoneIndex to {0}...",
webPart.ZoneIndex,
zoneIndex);
zoneIndex = webPart.ZoneIndex;
}
Debug.Assert(webPart.ZoneIndex == zoneIndex);
zoneIndex++;
Logger.LogDebug(
CultureInfo.InvariantCulture,
"Successfully created Web Part ({0}) on page ({1}).",
webPartId,
wpm.ServerRelativeUrl);
return webPart;
}
I was curious to know why SharePoint behaves this way and to see if this behavior is documented somewhere and I just missed it. Unfortunately, the e-mail I sent back in March to an internal discussion list received no responses. So I guess this will just have to remain a mystery.
Comments
Anonymous
June 07, 2009
PingBack from http://stevepietrek.com/2009/06/07/links-672009/Anonymous
July 26, 2010
Brilliant, I'm glad someone else picked up the same issue! I've been struggling with this issue for a few days now.Anonymous
June 29, 2011
Excellent! I too spend too much time to resolve the mystery. Does Microsoft has released any patch to resolve this?