Removing Web Parts tables in SharePoint 2010
In MOSS 2007 solutions one of the fixes to get cleaner and more accessible HTML markup was to remove Web Part tables using Control Adapters. Because SharePoint 2010 allows us to insert Web Parts in content there is more to this challenge. Find out what has changed and how to deal with it in SharePoint 2010.
How things were: MOSS 2007
If you’ve worked on an Internet-facing website using the MOSS 2007 platform a Web Part Zone Control Adapter for removing tables from Web Parts for anonymous users is nothing new to you:
public class WebPartZoneControlAdapter : ControlAdapter {
protected override void Render(HtmlTextWriter writer) {
if (SPContext.Current != null &&
SPContext.Current.Web != null &&
SPContext.Current.Web.CurrentUser != null) {
base.Render(writer);
}
else {
WebPartZone webPartZone = Control as WebPartZone;
if (webPartZone != null) {
foreach (WebPart wp in webPartZone.WebParts) {
wp.RenderControl(writer);
}
}
}
}
}
For all anonymous users the Control Adapter would cause the Web Part Zone to omit rendering tables around Web Parts and would render only the Web Parts instead. This was a common trick to simplify the HTML markup and make the contents of web pages more accessible.
How things are: SharePoint 2010
The above fix still applies in SharePoint 2010. In SharePoint 2010 Web Part Zones still wrap Web Parts into nested tables. While this behavior allows you to have your solutions backwards compatible with previous versions of SharePoint, it is still as terrible as it was for custom branding and accessibility. Using a Web Part Zone Control Adapter is still a way to get rid of those tables.
One of the great new features of SharePoint 2010 is the ability to add Web Parts in content what allows content editors to easily create rich pages. Unfortunately it adds some additional complexity to removing tables from Web Parts markup. Let me show you why.
Removing tables from Web Parts placed in content
While removing tables from Web Parts rendered in a regular Web Part Zone is pretty straight forward and a matter of a few lines of code, cleaning up the HTML of Web Parts rendered in content is slightly more complex. The reason for this is that Web Parts are rendered using non-public methods that cannot be overridden or plugged into. The only way to get rid of the tables of Web Parts placed in code is to use post processing and remove the tables after they have been rendered as HTML.
public class RichHtmlFieldControlAdapter : ControlAdapter {
private static readonly Regex cWebPartWrapperIdRegex = new Regex(@"<div\sclass=""ms-rtestate-read\s([\w]{8}-(?:[\w]{4}-){3}[\w]{12})""[^>]*>", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex cWebPartBodyRegex = new Regex(@".*<div[^>]+class=""ms-WPBody[^""]*""[^>]+>(.*)</div></td>\s*</tr>\s*</table></td></tr></table></div><div[^>]+id=""vid_[^>]+>.*", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline);
protected override void Render(HtmlTextWriter writer) {
using (new SPMonitoredScope("RichHtmlFieldAdapter")) {
if (SPContext.Current != null &&
SPContext.Current.FormContext.FormMode == SPControlMode.Display) {
StringBuilder sb = new StringBuilder();
using (new SPMonitoredScope("Render original content")) {
using (StringWriter sw = new StringWriter(sb)) {
using (HtmlTextWriter htw = new HtmlTextWriter(sw)) {
base.Render(htw);
}
}
}
string content = sb.ToString();
MatchCollection webPartIds = null;
using (new SPMonitoredScope("Retrieve Web Part IDs")) {
webPartIds = cWebPartWrapperIdRegex.Matches(content);
}
foreach (Match m in webPartIds) {
using (new SPMonitoredScope("Remove Web Part container")) {
string wpId = m.Groups[1].Value;
content = Regex.Replace(content, String.Format(@"<div[^>]+><div[^>]+id=""div_{0}"">.*?<div[^>]+id=""vid_{0}""[^>]+></div></div>", wpId), (m1) => {
string wpHtml = m1.Value;
using (new SPMonitoredScope("Get Web Part body")) {
Match m2 = cWebPartBodyRegex.Match(wpHtml);
if (m2.Success) {
wpHtml = m2.Groups[1].Value;
}
}
return wpHtml;
}, RegexOptions.IgnoreCase | RegexOptions.Singleline);
}
}
writer.Write(content);
}
else {
base.Render(writer);
}
}
}
}
The first step is to get the contents of the Rich Text Field rendered into a string that we can use for further processing (lines 9-18).
Removing Web Part tables consists of two steps. First we have to retrieve all Web Parts. To do this we have to use a rather complex Regular Expression (line 2). With that expression we can retrieve IDs of all Web Parts (lines 20-23) that we can use to construct Regular Expressions to retrieve the body of Web Parts placed in content.
By looping through the previously retrieved list of Web Part IDs, we can construct a Regular Expression that uniquely matches the HTML markup of each Web Part (line 28). After retrieving the complete HTML markup of each Web Part we can remove the tables (line 28-39). The last thing left to do is to write the cleaned HTML markup to the output (line 43).
Important Control Adapters for the RichHtmlField must be deployed to the Global Assembly Cache.
Removing tables from Web Parts placed in content is not easy. The bad part is that it’s not even a complete solution. Although the above code sample would work and would remove Web Part tables there are some scenarios that would cause things to break.
More doesn’t always mean better
Imagine the following Visual Web Part:
<asp:TextBox runat="server" ID="TextBox1" /><asp:RequiredFieldValidator ControlToValidate="TextBox1" runat="server" ErrorMessage="Field is required" /><br />
<asp:Button OnClick="Button_Click" Text="Submit" runat="server" />
Although it doesn’t seem like much, as soon as you add this Web Part to a page and visit the page as an anonymous user to make the Control Adapters work, all you will see is an exception:
The exception is being caused by the Required Field Validator that, called twice, is trying to add an attribute which already exists. As you recall, in order to remove tables from Web Parts added in content, we had to render the content of the Rich HTML Field first. Additionally, because all Web Parts placed in content are internally being stored in a hidden Web Part Zone, the logic within the Web Part Zone Control Adapter causes the Web Parts to be rendered for the second time. The great news is that solving this issue isn’t that complex.
All that you have to do, to prevent Web Parts in content to be rendered twice, is to prevent the Web Part Zone Control Adapter from rendering its Web Parts for the hidden Web Part Zone that is used by the Web Parts in content capability. The following code snippet shows the modified Web Part Zone Control Adapter:
public class WebPartZoneControlAdapter : ControlAdapter {
protected override void Render(HtmlTextWriter writer) {
if (SPContext.Current != null &&
SPContext.Current.Web != null &&
SPContext.Current.Web.CurrentUser != null) {
base.Render(writer);
}
else {
WebPartZone webPartZone = Control as WebPartZone;
if (webPartZone != null &&
webPartZone.ID != "wpz") {
foreach (WebPart wp in webPartZone.WebParts) {
wp.RenderControl(writer);
}
}
}
}
}
The hidden Web Part Zone that is used by the Web Parts in content capability uses the fixed name wpz. By excluding the wpz Web Part Zone from rendering we prevent the Web Parts in content to be rendered twice and solve the issue.
Technorati Tags: SharePoint 2010