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>

—————-

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? ;)

30 thoughts on “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???

  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);
    }
    }

  3. 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)
    {

    }
    }

    }
    }

  4. Pingback: Switchable stylesheets « This page intentionally left ugly

  5. Greate Code, thanks

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

  6. 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.

  7. Any news on the ‘fully functioning version with server side code’?
    I’m just missing the essential ability drag and drop.

    I’m using the original posted code that moment as when I tried the version pasted in the ‘Comments’ section the compiler kept complaining about the PortalWebPartManager. I figured it was a namespace thing but couldn’t sort it out. Was I just being thick?

    Regards, Matt

  8. Hi Matt.

    The “PortalWebPartManager” that David posted was part of his project. From that code, I’d say there’s a significant portion of his own code we’re not seeing.

    The best I can do for now is to suggest using the original code I posted and work from there. There’s a little update where I managed to fix the remaining tables.

    As for a fully functioning and perhaps downlodable version…
    I’m afraid you will have to wait a bit longer. I had virtually none written when I posted this article and it was only theorhetical. However, as far as I know the code does work as advertised. Right now I’m literally up to my eyeballs in another project. Sorry.

    Until I can write a complete version, you may want to consider using a jQuery callback when the module is dropped into its container.

    I.E. An AJAX call to a handler that gives the ID of the WebPart, the ID of the WebPartZone and the Sort order. Then, these can be loaded dynamically when the visitor refreshes the page.

    All the raw material is given. Only a few steps need to be taken to make it all fit together.

    Hope that helps.

  9. Hi All. I would be very interested in you opinion about our tableless webpart implementation. We are working on an open source Portal/ECMS for .NET, which uses Webparts, modified to be tableless. You can download the source from our webpage.

  10. Hello! Thanks a lot!! I was having serious problems with the default table based WebParts layout, and with the help of this post i was be able to solve all my problems. Thanks, again!!

  11. Hi, eksith.

    Why don’t you try building this into a control adapter ?

    So you wouldn’t have to make changes into every page.

    Have you tried this into Sharepoint?

    Thanks
    Gabriel

    • Hi Gabriel.

      I haven’t written up a control adapter due to my dissatisfaction with them ;)
      In fact this whole approach, I feel, is now obsolete due to the introduction of the MVC framework. Only the client side HTML is the critical portion now. The placements can then be assigned via a custom controller.

      I’m hoping to write something about that as well.

  12. Pingback: Tableless WebParts and WebPartZones « A Place for C Sharpers/.Netters

  13. Nicely done David Martin… well done, with a few tweeks now my XHTML successfully validated.

    What I would like to know is how you update the page state – save the personalization blob, when user drops a webpart from one zone to another via a callback?

    chaars

  14. Hello eksith ,

    I m using your code for one of my site,

    web parts are rendring as shown by you but design mode is not working i.e i m not able to move a web part from one zone to another

  15. Pingback: Building Completely Tabeless WebPart Control + Drag and Drop | Colour Blend Creative

  16. Pingback: [RESOLVED]How to create rounded corner webparts in asp.net 3.5?? | ASP Questions & Answers

  17. Pingback: Table Less Asp.Net webparts with SkinID | ASP Questions & Answers

Leave a reply to eksith Cancel reply