Part 1 - Creating a Tabbed View
Part 2 - Creating Dynamic Tabs
Part 3 - Dynamic Tabbed Web Part Pages
Download the code for this article.
This is the third and final article in the series of how to create a web part page that allows the user to create new pages dynamically. Remember from previous articles that our aim it to create an effect similar to the dynamic pages feature on the Google portal:

In the first article we looked at what is involved in creating a tabbed effect. The following diagram shows the effect that we were interested in creating:

In this picture we can see that the user drags a web part from a zone that is visible when one tab is selected and they can then drag that web part over a second tab which "activates" that tab and exposes the zone that is associated with that tab. The user is then able to drop the web part onto that zone.
The second article in the series looked at the problem of creating tabs and pages dynamically. Unlike in the first article where the tabs and pages were hard-coded into the page, the second article showed how to render tabs and pages dynamically. The following diagram shows the position of a special tab that allowed the user to create new pages at runtime:

The workflow behind the dynamically created pages can be seen in the following diagram:

The workflow diagram is quite self-explanatory so I won't go too far into it. If you'd like to understand it better then I suggest you go back and take a read of the second article.
In this article we will take the knowledge gained from the first two articles and use it to combine both features - dynamically adding pages and dragging web parts between tabbed views. As you will see, it's not a long stretch to get to the solution that we want from where we are, but there are just a couple of technical problems to solve. The biggest part of the problem that we haven't tackled yet is that we haven't yet shown how to dynamically create the zones that will live within each page and have them properly registered with the WebPartManager at runtime.
Remember from the second article that when the user created the page we stored that information in an XML file. Now that we'll need to access that information in 2 places - first when we lay out the tabs and second when we dynamically create a zone which represents each page - we can encapsulate the logic for accessing that information in a helper method:
private List<string> _pages ;
public List<string> TabPages {
get {
if (_pages == null) {
SetPages();
}
return _pages;
}
}
private List<string> SetPages() {
_pages = new List<string>();
XmlDocument doc = new XmlDocument();
doc.Load(HttpContext.Current.Server.MapPath("~/SitePages.xml"));
List<string> pages = new List<string>();
foreach (XmlNode node in doc.DocumentElement.SelectNodes("site-page")) {
string pageName = node.Attributes["title"].Value;
_pages.Add(pageName);
}
return _pages;
}
The list of pages can now be accessed from our readonly property TabPages which uses the SetPages helper method to read from the file the first time that it is called. Ideally we might cache this information and set a FileDependancy on the cached item, but for the purposes of keeping things relatively simple I'll leave that up to the reader to implement. We can see how this is used to bind to the Repeater to display the tabs in the following snippet of code:
rpt1.DataSource = this.TabPages;
rpt1.DataBind();
Likewise, when it comes to rendering out the zones, we can simply loop through each of the pages in the TabPages property and dynamically render each page:
private void AddPages() {
int counter = 0;
foreach (string page in this.TabPages) {
string visibility = counter == 0 ? "visible" : "hidden"
PlaceHolder ctl = new PlaceHolder();
ctl.Controls.Add(new LiteralControl(
string.Format(
"<div id=\"tab{0}\" class=\"tabpage_{1}\">",
counter,
visibility
)));
WebPartZone zone = new WebPartZone();
zone.ID = "Zone_" + page;
ctl.Controls.Add(zone);
ctl.Controls.Add(new LiteralControl("</div>"));
dynamicLayout.Controls.Add(ctl);
counter++;
}
string arrayName = "tabs"
string arrayValue = ""
for (int i = 0; i < counter; i++) {
arrayValue += string.Format("{1}\"tab{0}\"", i, i > 0 ? "," : "");
}
ClientScript.RegisterArrayDeclaration(arrayName, arrayValue);
}
That's quite a meaty chunk of code, so I'll walk through it in 2 parts. First we have the main foreach loop which spits out the pages. As you can see, the first thing that we do when displaying the pages is to work out the visibility - because we only want the first page to be visible. Next we can start to build up the control hierarchy for the tabbed page. At the root of the control hierarchy for each page is a PlaceHolder control and the first control that we add to it's controls collection is an HTML DIV element that will serve as our main handle to the tabbed page. We give this outer DIV a known ID so that we'll be able to work with it at runtime - these ID's start with "tab" and then have a number at the end which represents the index of the page within the tab collection. So you will end up with a group of tabbed pages with ID's like: tab0, tab1... tab10, and so on.
Once we have our outer DIV we dynamically create WebPartZone instance, give it a unique ID and add it to our placeholder. Finally, you can see that we are adding this control collection to a PlaceHolder in the page named dynamicLayout - this just allows us to have control over where the zones will get displayed in the page.
Outside of the main foreach loop you can see that the last task we perform is to register an array named tabs with the page which contains each of the tab ID's as it's elements. This array is used by clientside Javascript to match the ordinal position of tabs with their named pages.
Before we can run our page to view our work there's something important that we must attend to. The thing about zones is that they need to be added to the page in or before the Init stage of the page lifecycle - this is discussed in detail on pages 100-101 of my book. This leaves us with an interesting dilemma, take a look at the following diagram:

As we can see from the diagram, whenever the user clicks on the "Create New Page" function in the page we will postback to the server and handle this event by updating our XML definition file. But notice where we will be handling the postback - well after the page's Init event! This means that by the time we handle the event we will already have drawn our tabbed pages with their contained zones and so the new tabbed page will not be present for the user to use. The only way around this is to call out to a helper page which will update the XML definition file and then redirect back to the page that the user came from. The mechanics of this are shown in the following diagram:

So now, when we handle the postback event, we grab the argument that is sent from the client and do a redirect over to our helper page like so:
public void RaisePostBackEvent(string eventArgument) {
if (eventArgument.StartsWith("pb::")) {
string arg = eventArgument.Substring(4);
Response.Redirect(string.Format("PortalPostbackHandler.ashx?pageName={0}", arg));
} else {
base.RaisePostBackEvent(this, eventArgument);
}
}
And when our helper page has updated the XML file, it can transfer back:
public void ProcessRequest(HttpContext context) {
string arg = context.Request["pageName"];
if (!string.IsNullOrEmpty(arg)) {
AddTabPage(arg);
}
string referrer = context.Request.UrlReferrer.AbsolutePath;
context.Response.Redirect(referrer);
}
So that's all there is to creating dynamic tabbed web part pages. I'd encourage you to download the sample code that comes with this article and have a play with it.