Tableless WebParts and WebPartZones

I think I established why MS web developers have tables tattooed to their foreheads in my previous post on the subject.

Well, here’s my, partial, solution to this. My ultimate goal was to remove as much of the table markup as I can. It doesn’t solve all the problems associated with the bloated markup, but for the most part it cleans it up significantly. You may want to do additional cleanup afterwards, but at least, this will get you on the way.

The following is the basic rendered code for each webpart…

<div id="zoneID" class="webpartzone">
	<div id="partID" class="webpart">
		<div class="webpart_chrome">
			<div class="webpart_title">
				<img src="icon.png" /> Title
			</div>
			<div class="webpart_body">
				<div class="webpart_data">
					Webpart stuff
				</div>
			</div>
		</div>
	</div>
</div>

You may notice that it’s identical to the plain HTML markup from my previous post.
As long as you give a title to your WebPart and give a value to the TitleImageUrl attribute (optional), you should look identical to that.

Now for the WebPartZone…
This is a basic inherited WebPartZone so there is still some table markup. For my project, this was acceptable, but if you can improve it, please do. Post a comment or send me an email with your modifications if you can. This way we can share and solve this mess.

namespace MyApp.UI.Controls
{
   public class MyPartZone : WebPartZone
   {
      protected override void Render(HtmlTextWriter writer)
      {
         RenderContents(writer);
      }

      protected override void RenderContents(HtmlTextWriter writer)
      {
         // Get the current webpartzone
         WebPartZone wpz = this;
         if (wpz != null)
         {
            // Print the current zone ID and css class
            writer.AddAttribute(HtmlTextWriterAttribute.Id,
                        wpz.ID);
            writer.AddAttribute(HtmlTextWriterAttribute.Class,
                        "webpartzone");
            writer.RenderBeginTag(HtmlTextWriterTag.Div);

            // Render the part body
            base.RenderBody(writer);

            writer.RenderEndTag();
         }
         else
         {
            base.RenderContents(writer);
         }
      }

      protected override WebPartChrome CreateWebPartChrome()
      {
         // Return a new derived class to build the chrome
         return new MyPartChrome(this, base.WebPartManager);
      }
   }
}

Now for the MyPartChrome class, which is basically inherited from WebPartChrome. This will be the part of the code that styles the actual webpart itself. If you want rounded corners etc.. on your webparts, this is the code to modify.

namespace MyApp.UI.Controls
{
   public class MyPartChrome : WebPartChrome
   {
      public MyPartChrome(MyPartZone zone, WebPartManager manager)
         : base(zone, manager) { }

      // In case you are rewriting URLs, you need to find the base URL
      private string BaseUrl
      {
         get
         {
            string url = HttpContext.Current.Request.ApplicationPath;
            if (url.EndsWith("/"))
               return url;
            else
               return url + "/";
         }
      }

      public override void RenderWebPart(HtmlTextWriter writer, WebPart webPart)
      {
         // Begin wrapper
         writer.AddAttribute(HtmlTextWriterAttribute.Id,
                  webPart.ID);
         writer.AddAttribute(HtmlTextWriterAttribute.Class,
                  "webpart");
         writer.RenderBeginTag(HtmlTextWriterTag.Div);

         // Begin outer border or chrome
         writer.AddAttribute(HtmlTextWriterAttribute.Class,
                  "webpart_chrome");
         writer.RenderBeginTag(HtmlTextWriterTag.Div);

         // Begin title
         writer.AddAttribute(HtmlTextWriterAttribute.Class,
                  "webpart_title");
         writer.RenderBeginTag(HtmlTextWriterTag.Div);

         //If there's an image url, print that
         if (!String.IsNullOrEmpty(webPart.TitleIconImageUrl))
         {
            writer.AddAttribute(HtmlTextWriterAttribute.Src,
               BaseUrl + webPart.TitleIconImageUrl);
            writer.RenderBeginTag(HtmlTextWriterTag.Img);
            writer.RenderEndTag();
         }

         //Print the title
         if (!String.IsNullOrEmpty(webPart.Title))
         {
            writer.Write(webPart.Title);
         }
         else
         {
            // If your app is localized, use a definition instead
            writer.Write("Untitled");
         }

         // Close title
         writer.RenderEndTag();

         // Begin body
         writer.AddAttribute(HtmlTextWriterAttribute.Class,
                  "webpart_body");
         writer.RenderBeginTag(HtmlTextWriterTag.Div);

         //Write the subtitle if there is one
         if (!String.IsNullOrEmpty(webPart.Subtitle))
         {
            writer.AddAttribute(HtmlTextWriterAttribute.Class,
                  "webpart_subtitle");
            writer.RenderBeginTag(HtmlTextWriterTag.Div);
            writer.Write(webPart.Subtitle);
            writer.RenderEndTag();
         }

         // Begin data (I'm using this because some users may want to
         // have fancy double borders or other custom styles)
         writer.AddAttribute(HtmlTextWriterAttribute.Class,
                  "webpart_data");
         writer.RenderBeginTag(HtmlTextWriterTag.Div);

         // Print the contents
         RenderPartContents(writer, webPart);

         // Close data
         writer.RenderEndTag();

         // Close body
         writer.RenderEndTag();

         // Close chrome
         writer.RenderEndTag();

         // Close wrapper
         writer.RenderEndTag();
      }
   }
}

You may see that part of the code is cut off toward the ends of some lines, but if you highlight the whole thing, you should still be able to copy the whole code. Don’t worry, the cutoff is in the stylesheet for this blog theme, not the HTML itself.

How to use it

Create two class files in your App_Code folder and drop these in. Be sure to include the following headers at a minimum on top of each class :

using System;
using System.Web;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;

Now you have two choices on how to integrate it into your pages…
Either add the following to your web.config file :

<pages>
   <controls>
      <add tagPrefix="MyApp" namespace="MyApp.UI.Controls" assembly="__code"/>
   </controls>
</pages>

And be sure to change “MyApp.UI.Controls” to reflect what program structure you have (be sure to change it in the code files as well).
This will allow you to call these classes on all pages.

Or

You can add the following to each page :

<%@ Register TagPrefix="MyApp" Namespace="MyApp.UI.Controls" Assembly="__code" %>

And then call the webpartzone on your aspx page :

<MyApp:MyPartZone id="MyWebPartZone1" runat="server">
<ZoneTemplate>
<%-- Insert web part here --%>
</ZoneTemplate>
</MyApp:MyPartZone>

—————-

Hope you will find this helpful.
I will post code at a future date showing how to use jQuery for drag n’ drop functionality and personalization options instead of the native ASP.NET code.

The code I used on my app runs just fine (with the described limitations). I had to modify it a bit before posting here.
Please let me know if there are any errors.

Update 07/02/08

Finally! No more tables!
The fix to the above code was so simple, I hit myself on the head.
Change the following line in the MyPartZone class :

base.RenderBody(writer);

To this:

this.RenderBody(writer);

Anyone else feel relieved? ;)

12 Responses to “Tableless WebParts and WebPartZones”

  1. Hi there!!!.
    just try your code and seems i still get a table wrapping the webparts. something at the
    // Render the part body
    base.RenderBody(writer);
    …. i think.

    output:

    Plain Paragraph

    Please edit this webpart.

    how did you get it to work??, actually i checked your demo, but i just seems plain html… is this just a theoretical approach???

    David Martin - June 30, 2008 at 4:46 am

  2. Problem solved!!!!:

    protected override void RenderBody(HtmlTextWriter writer)
    {
    WebPartCollection wpColl = new WebPartCollection(this.WebParts);
    WebPartChrome chrome = CreateWebPartChrome();
    foreach (WebPart wp in wpColl)
    {
    chrome.RenderWebPart(writer, wp);
    }
    }

    David Martin - June 30, 2008 at 5:16 am

  3. Hi David, thanks for the fix.
    Yes, this was just theoretical, but I was hoping someone else could improve it.

    Thanks!

    eksith - June 30, 2008 at 3:52 pm

  4. David, I just tried your modification and it seems the table is still there.

    eksith - June 30, 2008 at 4:01 pm

  5. Yeah, i know.
    at the end i fixed it!!, let me post some code.

    namespace corecms.framework
    {
    public class MyChrome : WebPartChrome
    {
    WebPartManager _wpm;
    PortalWebPartZone _pzn;
    public MyChrome(PortalWebPartZone myZone, WebPartManager WPM)
    : base(myZone, WPM)
    {
    _wpm = WPM;
    _pzn = myZone;
    }

    public override void RenderWebPart(HtmlTextWriter writer, WebPart webPart)
    {
    bool showChrome = false;

    if (HttpContext.Current.User.Identity.IsAuthenticated && _wpm.DisplayMode == PortalWebPartManager.CMSDesignDisplayMode)
    showChrome = true;

    // Begin wrapper
    writer.AddAttribute(HtmlTextWriterAttribute.Id,webPart.ID);
    writer.AddAttribute(HtmlTextWriterAttribute.Class,string.Format(”part {0}”,webPart.CssClass));
    writer.RenderBeginTag(HtmlTextWriterTag.Div);

    if (showChrome)
    {

    // Begin ToolBar
    writer.AddAttribute(HtmlTextWriterAttribute.Class, “toolbar”);
    writer.RenderBeginTag(HtmlTextWriterTag.Div);

    // Begin TitleBar, which also allows to drag and drop
    writer.AddAttribute(HtmlTextWriterAttribute.Class, “titlebar”);
    writer.RenderBeginTag(HtmlTextWriterTag.Div);

    //Start verbs
    writer.AddAttribute(HtmlTextWriterAttribute.Class, “actions”);
    writer.RenderBeginTag(HtmlTextWriterTag.Div);
    RenderVerbs(webPart, writer);
    writer.RenderEndTag();
    //End verbs

    //If there’s an image url, print that
    if (!String.IsNullOrEmpty(webPart.TitleIconImageUrl))
    {
    writer.AddAttribute(HtmlTextWriterAttribute.Class, “wpicon”);
    writer.AddAttribute(HtmlTextWriterAttribute.Alt, webPart.Description);
    writer.AddAttribute(HtmlTextWriterAttribute.Src, webPart.TitleIconImageUrl);
    writer.RenderBeginTag(HtmlTextWriterTag.Img);
    writer.RenderEndTag();//img
    }

    //Print the title

    if (!String.IsNullOrEmpty(webPart.Title))
    {
    writer.Write(webPart.Title);
    }
    else
    { // If your app is localized, use a definition instead
    writer.Write(”Untitled”);
    }

    writer.RenderEndTag();//Ends TitleBar

    writer.RenderEndTag();//Ends Toolbar
    }
    // Begin body
    writer.AddAttribute(HtmlTextWriterAttribute.Class,”outer”);
    writer.RenderBeginTag(HtmlTextWriterTag.Div);

    writer.AddAttribute(HtmlTextWriterAttribute.Class,”inner”);
    writer.RenderBeginTag(HtmlTextWriterTag.Div);

    // Print the contents
    webPart.RenderControl(writer);

    // Close Inner
    writer.RenderEndTag();

    // Close outer
    writer.RenderEndTag();

    // Close part
    writer.RenderEndTag();
    }

    private void RenderVerbs(WebPart webPart,HtmlTextWriter writer)
    {

    typeof(WebPartChrome).GetMethod(
    “RenderVerbsInTitleBar”,
    System.Reflection.BindingFlags.NonPublic |
    System.Reflection.BindingFlags.Instance).Invoke(this, new object[] { writer, webPart, 1 });

    }

    }

    public class PortalWebPartZone : WebPartZone
    {
    protected override bool HasHeader
    {
    get { return false; }
    }

    protected override WebPartChrome CreateWebPartChrome()
    {
    return new MyChrome(this, base.WebPartManager);
    }

    protected override void OnCreateVerbs(WebPartVerbsEventArgs e)
    {

    base.OnCreateVerbs(e);

    this.ExportVerb.ImageUrl = “/images/chrome/icon_export.gif”;
    this.EditVerb.ImageUrl = “/images/chrome/icon_edit.gif”;
    this.MinimizeVerb.ImageUrl = “/images/chrome/icon_minimise.gif”;
    this.RestoreVerb.ImageUrl = “/images/chrome/icon_restore.gif”;
    this.CloseVerb.ImageUrl = “/images/chrome/icon_close.gif”;
    this.DeleteVerb.ImageUrl = “/images/chrome/icon_delete.gif”;

    this.DeleteVerb.Visible = false;
    this.EditVerb.Visible = false;
    // this.CloseVerb.Visible = false;

    Collection verbs = new Collection();

    HttpContext ctx = HttpContext.Current;

    if (ctx.Request.IsAuthenticated)
    {
    WebPartVerb editVerb = new WebPartVerb(
    “editVerb”,
    new WebPartEventHandler(HandleEditClick)
    );

    verbs.Add(editVerb);

    WebPartVerb deleteVerb = new WebPartVerb(
    “deleteVerb”,
    new WebPartEventHandler(HandleDeleteClick)
    );

    verbs.Add(deleteVerb);

    if (this.WebPartManager.Personalization.Scope == PersonalizationScope.Shared)
    {
    editVerb.Text = “Edit2 Web Part”;
    deleteVerb.Text = “Delete2 Web Part”;
    editVerb.ImageUrl = “/images/chrome/icon_edit.gif”;
    deleteVerb.ImageUrl = “/images/chrome/icon_delete.gif”;
    }
    else
    {
    editVerb.Text = “Edit Web Part”;
    deleteVerb.Text = “Delete Web Part”;
    }
    }

    e.Verbs = new WebPartVerbCollection(verbs);

    }

    protected override void RenderContents(HtmlTextWriter writer)
    {
    // Get the current webpartzone
    PortalWebPartZone wpz = this;
    if (wpz != null)
    {
    // Print the current zone ID and css class
    writer.AddAttribute(HtmlTextWriterAttribute.Id,wpz.ID);
    writer.AddAttribute(HtmlTextWriterAttribute.Class,string.Format(”wpzone {0}”,wpz.CssClass));
    writer.RenderBeginTag(HtmlTextWriterTag.Div);

    // Render the part body
    RenderBody(writer);

    writer.RenderEndTag();
    }
    else
    {
    base.RenderContents(writer);
    }
    }

    protected override void RenderBody(HtmlTextWriter writer)
    {
    WebPartCollection wpColl = new WebPartCollection(this.WebParts);
    WebPartChrome chrome = CreateWebPartChrome();
    //chrome.PerformPreRender();
    foreach (WebPart wp in wpColl)
    {
    chrome.RenderWebPart(writer, wp);

    }
    }
    protected override void Render(HtmlTextWriter writer)
    {
    RenderContents(writer);
    }

    protected override void RaisePostBackEvent(string eventArgument)
    {
    WebPartManager wpm = WebPartManager.GetCurrentWebPartManager(this.Page);

    if (eventArgument.StartsWith(”catalog:”))
    {

    try
    {
    string[] argParts = eventArgument.Split(new char[] { ‘:’ }, StringSplitOptions.RemoveEmptyEntries);
    Type t = BuildManager.GetType(argParts[argParts.Length - 1], true, true);
    WebPart wp1 = (WebPart)Activator.CreateInstance(t);

    wpm.AddWebPart(wp1, this, this.WebParts.Count);
    }
    catch (Exception ex)
    {
    Console.WriteLine(ex.Message);
    }
    }
    else
    {
    base.RaisePostBackEvent(eventArgument);
    }
    }

    void HandleDeleteClick(object sender, WebPartEventArgs e)
    {
    HttpContext.Current.Trace.Warn(”DELETING WEBPART”);

    WebPart wp = e.WebPart;
    CoreCMS.DataAccess.SPs.UDeletewebpartref(wp.ID).Execute();
    this.WebPartManager.DeleteWebPart(wp);
    }

    public void HandleEditClick(object sender, WebPartEventArgs e)
    {

    CMSWebpart wp =(CMSWebpart)e.WebPart;

    if ((this.Page as PageBasePage).portalwp.SelectedPart != wp.ID)
    (this.Page as PageBasePage).portalwp.SelectedPart = wp.ID;

    }

    void HandleConnectClick(object sender, WebPartEventArgs e)
    {

    WebPart wp = e.WebPart;

    if (wp != this.WebPartManager.SelectedWebPart)
    {

    }
    }

    }
    }

    David Martin - July 1, 2008 at 4:26 am

  6. Thanks David, I’ll give this a go.
    But I think you left a few bits of your code out ;)

    I’ll see if this works when I get home.

    eksith - July 1, 2008 at 3:03 pm

  7. [...] This page intentionally left ugly More steam! « Tableless WebParts and WebPartZones [...]

    Switchable stylesheets « This page intentionally left ugly - July 3, 2008 at 8:38 am

  8. Greate Code, thanks

    but I can’t find a way that Webpart’s Drag to Another Zone..
    Can U help me more?….

    bj Park - July 25, 2008 at 6:47 am

  9. Hi bj Park. Thanks!

    I’m working on server side drag n’ drops ;)
    Actually, you can use jQuery to drag and drop it right now.

    I have an example of the HTML (not the server side code) running on a template, but the server side “save” functions need to be implemented.
    I think the best way to do this is to trigger an AJAX call to another page which does the saving.

    I.E. When you drag and drop it to another webpartzone, the AJAX call sends the location of the webpart back to the control page. This way, you don’t have to refresh the page to save a webpart location.

    Hope that helps somewhat.
    I plan to add a fully functioning version with server side code for this later on.

    eksith - July 25, 2008 at 7:04 am

  10. Hi eksith..
    thanks!

    I’ll waiting :)

    sorry.. My English is not good…

    bj Park - July 26, 2008 at 6:28 am

  11. Hello,

    Great job, but I ask you: Why don’t you publish it on the cssfriendly adapter?
    There a group of developers doing a good job changing those tableful asp.net components like asp:menu, asp:sitemap in tableless components.
    Check it out: http://www.codeplex.com/cssfriendly

    See ya!

    Roberto O Santos - August 1, 2008 at 9:07 am

  12. Hi Roberto.

    This is why I didn’t include it on the css friendly adapters ;)

    eksith - August 1, 2008 at 7:49 pm

Leave a Reply