Removing Web Parts tables in SharePoint 2010

, , , , , ,

Standard Web Part HTML markup containing tables
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:

Exception thrown after adding a Web Part that uses validators

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.

Web Part displayed properly on a page

Technorati Tags:

14 Responses to “Removing Web Parts tables in SharePoint 2010”

  1. John Livingston Says:

    This is almost exactly what I was looking for. In my case, I will be adding to the HTML. Huge thanks!

  2. Simon Rawson Says:

    HiSoftware (www.hisoftware.com) has released a commercial product which takes care of this and provides a Control Adaptor framework.

    It's called the Accessibility Fuondation Module (AFM), which is available for MOSS / SP2010. AFM is the successor to the Accessibility Kit for SharePoint (AKS), released free by Microsoft, and developed under contract by HiSoftware.

    HiSoftware also has an accessibilty validation tool for SharePoint, which is triggered by SharePoint workflow. It's called Compliance Sheriff.

  3. Victor Says:

    Hi,

    I deployed the control adapter to my GAC. But it is not removing the table markup. After putting a breakpoint through the code, I found out that the regex cWebPartWrapperIdRegex is not matching any ids. I think is because the markup generated by sharepoint looks like:

    div class="ms-rtestate-notify ms-rtestate-read f5eee4fa-e829-4473-9571-67a3a0963b46" id="div_f5eee4fa-e829-4473-9571-67a3a0963b46"

    Notice the ms-rtestate-notify class

  4. Waldek Mastykarz Says:

    @Victor: It is possible that the code rendered by SharePoint changed at some point. Modifying the regex should make it work.

  5. GM Says:

    Should I be adding this in a javascript tag? It doesn't seem do be working when I add it that way to my masterpage.

  6. Waldek Mastykarz Says:

    @GM: A Control Adapter is a server-side code that has to be compiled into an assembly.

  7. GM Says:

    Thanks, should both code snippets be in the same file, or is this a command line thing, one first then the other?

  8. Chris Pettigrew Says:

    Hi Waldek,

    Do you know how you can get a WebPart Adapter which removes the tables but does NOT remove the ability to run webparts with contextual tabs? If you try to use a calendar list view webpart then the ajax functionality fails and the context tab also breaks… Any ideas?

  9. Anna Says:

    Thank you for a great article! I have one question though since I do not get this adapter to trigger on a team site in SharePoint 2010. Which controlType are you using in your .browser file to attach this control adapter?

  10. Waldek Mastykarz Says:

    The same control adapter should be trigger no matter the site template. Have you cleared the ASP.NET temp cache?

  11. ath Says:

    I have a publishing HTML field and this does not seem to be working for it. When I place a web part into that field the control adapter is pulling this back:

    <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\">
    <tr>
    <td id=\"MSOZoneCell_WebPartWPQ1\" valign=\"top\" class=\"s4-wpcell-plain\"></td>
    </tr>
    </table>

    It seems that the html is is getting is not the final version which is what it needs. Any ideas?

  12. Waldek Mastykarz Says:

    As you can see this post has been written a while ago. It could be that something about how the HTML is rendered has changed in the meanwhile, what could be causing the RegEx to fail matching. Which patchlevel are you on?

  13. ath Says:

    I figured it out. I had to modify the first Regex to grab all the WP Id(s). Then I had to do something different(not Regex) to grab the content itself, because I'm using this with custom Wp(s) which can contain many div tags.

    Love your blog, keep up the great work!

  14. Katie Says:

    For anyone that may be trying this and having issues with all three expressions, thought i would share. I modified them to the following and am having success so far (testing still to be done):

    private static readonly Regex cWebPartWrapperIdRegex = new Regex(@"]*)?class=""([^>]*)?ms-rtestate-read\s([\w]{8}-(?:[\w]{4}-){3}[\w]{12})""[^>]*>", RegexOptions.IgnoreCase | RegexOptions.Compiled);
    private static readonly Regex cWebPartBodyRegex = new Regex(@".*]+class=\""ms-WPBody[^""]*\""[^>]+>(.*)]+id=\""vid_[^>]+>.*", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline);

    and
    content = Regex.Replace(content, String.Format(@"]+>]+)?id=""div_{0}""([^>]+)?>(.*)?]+)?id=""vid_{0}""([^>]+)?>", wpId), (m1) =>

Leave a Reply

WP Theme & Icons by N.Design Studio
Entries RSS Comments RSS
Copyright © 2007 - 2013 Waldek Mastykarz

Creative Commons License