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? ;)
Loading...
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
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
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
David, I just tried your modification and it seems the table is still there.
eksith - June 30, 2008 at 4:01 pm
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
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
[...] 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
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
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
Hi eksith..
thanks!
I’ll waiting :)
sorry.. My English is not good…
bj Park - July 26, 2008 at 6:28 am
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
Hi Roberto.
This is why I didn’t include it on the css friendly adapters ;)
eksith - August 1, 2008 at 7:49 pm