At TechEd, in one of my sessions, I developed a custom placeholder server control. Here's the sample code that I promised. Custom placeholder server controls empower developers to provide a richer publishing experience. The placeholder framework in MCMS 2002 is one of the best things about the product. There is a lot of existing content on server controls (a lot of samples on GotDotNet) as well as a new MCMS Sample Installer

This custom control shows a list of channels in edit mode and in presentation mode, it displays all the postings (their displaynames) with links to the actual postings. It exposes one public property that the template developer can set at design time... and that is the “parent channel“ from which you want to show the other channels. By default, it's the channel in which you are... in which case it will show you the destination channel and all its children.

Again, this is sample code. The implementation of this is not entirely practical. The underlying placeholder for this server control is an HTMLPlaceholder and it stores the actual HTML that is displayed on presentation time. The benefit of this is that the HTML is cached and you don't have to enumerate and create the HTML on presentation mode. Performance boost! However, the downside here is that the presentation format is fixed/hardcoded and that if there is a new posting in a channel, it won't show up until the page is editted again as the HTML only gets created when the content is saved.

So, if I were to implement this for production, I would -cache- but not store the actual HTML in the placeholder. Rather, I would expose a public property for an XSL file that would be used to create the HTML.

Anyway, here's the sample code that I promised. It's relatively short as I've only implemented the 5 manditory methods (excluding private helper methods).

using System;
using System.Web;
using System.Web.UI.WebControls;
using System.Web.UI;

using System.ComponentModel;

using Microsoft.ContentManagement.Publishing;
using Microsoft.ContentManagement.Publishing.Extensions.Placeholders;
using Microsoft.ContentManagement.WebControls;
using Microsoft.ContentManagement.WebControls.Design;

 

namespace TechEd04CustomControl
{
 /// <summary>
 /// Summary description for Class1.
 /// </summary>
 
 [
 ToolboxData
    ("<{0}:ChannelSummaryControl runat=server></{0}:ChannelSummaryControl>")
 , SupportedPlaceholderDefinitionType
    (typeof(HtmlPlaceholderDefinition))
 ]


 public class ChannelSummaryControl : BasePlaceholderControl
 {
  private string specifiedChannel;
  private ListBox channelSelector;
  private Literal channelSummary;

  //public property that the design can set to specificy the parent channel
  [
  Browsable(true),
  Description("The parent channel."),
  Category("Data"),
  DefaultValue("")
  ]

  public string SpecifiedChannel
  {
   get
   {
    return this.specifiedChannel;
   }
   set
   {
    this.specifiedChannel = value;
   }
  }
  


  //***************Private Methods**************************
  private string padHtml(string s, int pad)
  {
   return s.PadLeft(s.Length+pad, '-');
  }

  private void loadSubChannels(ListBox l, Channel c, int ind)
  { 
   foreach(Channel child in c.Channels)
   {
    l.Items.Add(new ListItem(padHtml(child.DisplayName, ind), child.Guid));
    loadSubChannels(l, child, ind+2);
   }
  }

  private void loadChannelSelector()
  {
   Channel c;
   if (specifiedChannel.Equals(".") || specifiedChannel.Equals("")) c = CmsHttpContext.Current.Channel;
   else c = CmsHttpContext.Current.Searches.GetByUrl(specifiedChannel) as Channel;
   if (c != null)
   {
    channelSelector.Items.Add(new ListItem(c.DisplayName, c.Guid));
    loadSubChannels(channelSelector, c, 2);
   }
  
  }
  private void LoadFromCMS()
  {
   EnsureChildControls();
   WebAuthorContext webAuthorContext = WebAuthorContext.Current;
   try
   {
    switch (webAuthorContext.Mode)
    {
     case WebAuthorContextMode.AuthoringNew:
      break;
     case WebAuthorContextMode.AuthoringReedit:
      break;
     case WebAuthorContextMode.PresentationPublished:
     case WebAuthorContextMode.PresentationUnpublished:
     case WebAuthorContextMode.PresentationUnpublishedPreview:
     case WebAuthorContextMode.AuthoringPreview:
      HtmlPlaceholder htmlPH = (HtmlPlaceholder)this.BoundPlaceholder;
      this.channelSummary.Text = htmlPH.Html;
      break;
     default:
      break;
    }
   }
   catch (Exception exp)
   {
    this.channelSummary.Text = "<error>"
     + exp.Message + "</error>";
   }
  }


  //*************BasePlaceHolder Methods*********************
  protected override void CreateAuthoringChildControls(BaseModeContainer authoringContainer)
  {
   this.channelSelector = new ListBox();
   this.channelSelector.ClearSelection();
   channelSelector.ID = "AuthoringControl";
   this.loadChannelSelector();
   authoringContainer.Controls.Add(this.channelSelector);
  }

  protected override void CreatePresentationChildControls(BaseModeContainer presentationContainer)
  {
   this.channelSummary = new Literal();
   channelSummary.Text = "";
   channelSummary.ID = "PresentationControl";
   presentationContainer.Controls.Add(this.channelSummary);
  }
  
  protected override void SavePlaceholderContent(PlaceholderControlSaveEventArgs e)
  {
   EnsureChildControls();
   try
   {
    string summaryText = "";
    Channel c = CmsHttpContext.Current.Searches.GetByGuid(channelSelector.SelectedValue) as Channel;
    if (c != null)
     foreach (Posting p in c.Postings)
      summaryText += "<a href=\""+p.Url+"\">"+p.DisplayName+"</a><br>";
    ((HtmlPlaceholder)this.BoundPlaceholder).Html = summaryText;
   }
   catch (Exception exp)
   {
    throw(exp);
   }
  }

  protected override void LoadPlaceholderContentForAuthoring(PlaceholderControlEventArgs e)
  {
   LoadFromCMS();
  }

  protected override void LoadPlaceholderContentForPresentation(PlaceholderControlEventArgs e)
  {
   LoadFromCMS();
  }
  

  public ChannelSummaryControl()
  {
   specifiedChannel = ".";
   //
   // TODO: Add constructor logic here
   //
  }

 }
}