KB's

How to wrap a Generic Content control

Note!

Since 3.6 SP1 hotfix it is possible to successfully wrap GeneriContent in a user control.

Note!
This is specific to version 3.6 hotfix. There is an updated article about 3.6 SP1.

Since there have been quite a lot of questions about this in the support tickets, and the documentation might take some time to be upgraded, I decided to show how to wrap a Generic Content control.

First of all, in the existing documentation, an obsolete method is suggested. I advise you not to use it, as it causes a lot of problems.

Here is what has to be done at a glance:
  • Create a custom control that inherits GenericContent
  • Add template support to it
  • Add custom functionality
  • Override Render
In this example I will add a very basic functionality on top of the GenericContent control, as the purpose of this article is to show how to accomplish this, not to be a showcase.

It is important to note that, due to the current implementation of GenericContentDesigner, it is not possible to preview correctly the contents of such a wrapper in edit  (design) mode.

Create a custom control that inherits GenericContent

Let us first determine our goal: we want to use the GenericContent control designer, but we don’t want to have simple text only. What we need is to add some functionality on top of the basic GenericContent. Here, I will show how to:

  • use an embedded template
  • reuse the functionality of the GenericConent control, and
  • plug in the contents of a control in arbitrary place in our template

Now that we have our goals set, we need to examine our base class more closely – namely, the GenericContent control.

GenericContent inherits directly from WebControl and implements the IEmptyControl interface. Since GenericContent was designed to hold only formatted text, it doesn’t support Embedded Templates, and uses the Render method to directly write its content. While there is nothing wrong with this approach, we need to be aware of this limitation if we are going to inherit from GenericContent and extend it. Implementers of IEmptyControl can write an arbitrary message (including html) when the control holds no data (which is determined by the IEmptyControl.IsEmpty property implementation) in design mode.

The attributes that are applied to GenericContent are:  ControlDesignerAttribute and ToolboxItemAttribute. They are the key to achieving the goals we set before. The specified GenericContentDesigner provides the familiar interface for editing a GenericContent control: entering text in RadEditor, sharing the written content and using already shared content. Since it works only with GenericConent classes, and there is no way to circumvent it (including overriding conversion operators), we will inherit from GenericContent. The second attribute is crucial.  It specifies the type of the control that will be used once it is dragged from the toolbox and dropped onto a ContentPlaceholder. Since it is inheritable, we need to re-apply it and specify the type of our class, or the page editor will generate a GenericContent control instead.

A limitation that comes from the GenericContentDesigner, is that the contents of the RadEditor will be the rendered output of the control at design time. That is why our control cannot be properly rendered at design time. This is where the IEmptyControl. SetEmptyControlDefaultMessage method comes into play: we will warn the user of this limitation.

Here is what our code will look like so far:
 
/// <summary> 
/// Demonstrates how to wrap a Generic Content control and use the full functionality 
/// of the base class. 
/// </summary> 
[DefaultProperty("ContentTitle")] 
[ToolboxItem(typeof(GCWrapperToolboxItem))] 
[ToolboxData("<{0}:ServerControl1 runat=server></{0}:ServerControl1>")] 
public class GCWrapper : GenericContent 
… 
  
    /// <summary> 
    /// This message will be displaye when the control is initially dragged 
    /// onto the design surface 
    /// </summary> 
    /// <returns>a message to display when there is no content in the control</returns> 
    public override string SetEmptyControlDefaultMessage() 
    { 
        return "This control will not display properly in design mode!"
    }          
  
    … 
}  

/// <summary> 
/// This will determine what will be shown in the toolbox 
/// </summary> 
public class GCWrapperToolboxItem : System.Drawing.Design.ToolboxItem 
    /// <summary> 
    /// Initializes the toolbox item with the proper type 
    /// </summary> 
    /// <remarks> 
    /// The section under which the control is initially placed is determined by the 
    /// [assembly: AssemblyCompany("Telerik")] attribute. 
    /// Of course, it can later be changed to something else throuh web.config 
    /// </remarks> 
    public GCWrapperToolboxItem() 
        : base(typeof(GCWrapper)) 
    { 
        base.DisplayName = "Generic Content Wrapper"
        base.Description = "Custom control that describes how to wrap a Generic Content control."
    } 


Adding embedded templates support

Normally, you will inherit either from SimpleControl, ViewModeControl<T> or ViewModeUserControl<T>, and therefore – have support for embedded templates out of the box. If this were the case, you would only need to override LayoutTemplateName and LayoutTemplatePath and apply an EmbeddedTemplateAttribute (or derivative) to the latter. The template name would hold the relative path to the template file (see where our template is found in the solution explorer and compare it to the value of the GCWrapperLayoutTemplatePath constant). The attribute would hold the same relative path plus some descriptive information. LayoutTemplatePath holds the current url of the template, if it is not embedded. In most cases, you would use the base implementation and apply EmbeddedTemplateAttrubute.

Since we have to inherit from GenericControl, which doesn’t inherit from either of the above mentioned classes, we need to implement this by ourselves. Here comes another drawback from inheriting from GenericContent’s base – WebControl, and not from CompositeControl. If we overrode CreateChildControls, it would never be called!

These are the properties that are needed for embedded templates support:

  • Container
  • LayoutTemplate
  • LayoutTemplatePath
  • LayoutTemplateName

Two of those we already discussed. Let us explain the two remaining: LayoutTemplate is to eventually return an ITemplate implementer, and the Container will hold the template data (the template will be initialized in the container).

LayoutTemplate has to be marked with TemplateContainerAttribute, which defines the type we will use for our Container properly – namely, an IGenericContainer implementer.

Container is used to load controls from the embedded template.

There is a problem – as stated above, we need to somehow make the Container be a part of our Controls collection (that is – rendered on the page), and this can’t be done in CreateChildControls. A workaround is described later in Override Render.

Here is what our very simple template looks like:

<h2 class="gc_wrapper_title">                
    <asp:Literal ID="title" runat="server" />     
</h2>   
  
<div class="gc_wrapper_content"
    <asp:Literal ID="content" runat="server" />   
</div> 


The first label, title, is a custom title that will be displayed above the content. The second label, content, is used to hold the contents of the generic content control.

Note that it is important to compile this file as an embedded resource. To do so:

  • Open solution explorer
  • Navigate to the template file
  • Right-click and choose Properties from the context menu
  • Set  Build Action to Embedded Resource

In this step we add the following code:

#region layout properties 
  
/// <summary> 
/// Used for loading controls from a template (defines the layout) 
/// </summary> 
[Browsable(false)] 
protected virtual GenericContainer Container 
    get 
    { 
        if (this.container == null
        { 
             this.container = new GenericContainer(); 
             if (this.LayoutTemplate != null
             { 
                 this.LayoutTemplate.InstantiateIn(this.container); 
             } 
        } 
        return this.container; 
    } 
  
/// <summary> 
/// Gets or sets the template defining the layout of the control. 
/// </summary> 
[TemplateContainer(typeof(GenericContainer))] 
[PersistenceMode(PersistenceMode.InnerProperty)] 
[Description("Gets or sets the template defining the layout of the control.")] 
[Browsable(false)] 
[DefaultValue(typeof(ITemplate), "")] 
public virtual ITemplate LayoutTemplate 
     get 
     { 
         if (this.layoutTemplate == null
         { 
            this.layoutTemplate = ControlUtils.GetTemplate(this.LayoutTemplatePath, 
                this.LayoutTemplateName, this.GetType());                      
         } 
         return this.layoutTemplate; 
     } 
     set 
     { 
         this.layoutTemplate = value; 
     } 
          
          
/// <summary> 
/// Gets or sets the path to a custom layout template for the control. 
/// </summary> 
[Browsable(true)] 
[Category("Appearance")] 
[Description("Gets or sets the path to a custom layout template for the control.")] 
[EmbeddedTemplate(GCWrapper.GCWrapperLayoutTemplatePath, "Layout template path for GC wrapper""/Templates/"true"2009/3/31")] 
public virtual string LayoutTemplatePath 
    get 
    { 
        object persistedValue = this.ViewState["LayoutTemplatePath"]; 
                  
        if (persistedValue != null
        { 
            return (string)persistedValue; 
        } 
        else 
        { 
            return String.Empty; 
        } 
    } 
    set 
    { 
        this.ViewState["LayoutTemplatePath"] = value; 
    } 
  
/// <summary> 
/// Gets the name of the embedded layout template. This property must be overridden to provide the path (key) to an embedded resource file. 
/// </summary> 
[Browsable(false)]          
public virtual string LayoutTemplateName 
   get 
   { 
       return GCWrapper.GCWrapperLayoutTemplatePath; 
   } 
#endregion 
#region constants 
  
public const string GCWrapperLayoutTemplatePath =  "DidoTestControls.Resources.Templates.GC_WrapperTemplate.ascx"
#endregion 
#region fields 
  
/// <summary> 
/// Holds the value of the LayoutTemplate property 
/// </summary> 
private ITemplate layoutTemplate; 
          
/// <summary> 
/// Holds the value of the Container property 
/// </summary> 
private GenericContainer container; 
#endregion  

 
Add custom functionality

We have already added the functionality by adding the title label in the template, so we simply load the two labels from the template and add another to control them. And set the default property for this class.

Here is what the code looks like:

#region properties 
  
/// <summary> 
/// The text of the title above the content area 
/// </summary> 
[Browsable(true)] 
[Category("Appearance")] 
[Description("The text of the title above the content area")] 
[TypeConverter(typeof(StringConverter))] 
public string ContentTitle 
     get { return this.ContentTitleText.Text; } 
     set { this.ContentTitleText.Text = value; } 
#endregion 
        
#region control references 
  
/// <summary> 
/// The title above the content 
/// </summary> 
[Browsable(false)]          
public ITextControl ContentTitleText 
    get { return this.Container.GetControl<ITextControl>("title"true); } 
  
/// <summary> 
/// The wrapped generic content control 
/// </summary> 
[Browsable(false)]          
public ITextControl ContentPlaceholder 
    get { return this.Container.GetControl<ITextControl>("content"true); } 
#endregion  

Override Render

In Render, we have to do the following things:

•    If in normal mode, render the template and the two labels
•    If in normal mode, plug in the contents of the generic control in the content label
•    If in design mode, render GenericContent only

Everything is explained so far, so let the code speak for itself:

/// <summary> 
/// Called to write the actual html for this control 
/// </summary> 
/// <remarks> 
/// Will not render properly in design mode due to the implementation of the 
/// GenericContentDesigner. 
/// </remarks> 
/// <param name="writer">a stream that is specialized in wrtinig html output</param> 
protected override void Render(HtmlTextWriter writer) 
    StringWriter content = null
    HtmlTextWriter contentWriter = null
      
    try 
    { 
        // if in normal mode, output the full content of the control 
        if (!this.DesignMode) 
        { 
            // first, we need to "extract" 
            content = new StringWriter(); 
            contentWriter = new HtmlTextWriter(content); 
            base.Render(contentWriter); 
  
            this.ContentPlaceholder.Text = content.ToString(); 
  
            // we need to do this manually, as generic content control renders its 
            // content directly and does not inherit CompositeControl, 
            // so it will not render out template correctly (not at all, actually) 
            foreach (Control child in this.Controls) 
            { 
                child.RenderControl(writer); 
            } 
            // for some reason, CreateChildConrols is never called, so we do this manually 
            if (!this.Controls.Contains(this.Container)) 
            { 
                this.Container.RenderControl(writer); 
            } 
        } 
        else   
        { 
            // if in edit (design) mode, output the content of the genreric content control 
            // THIS IS REQUIRED 
            base.Render(writer); 
        } 
    } 
    finally 
    { 
        // I don't use "using", as I dislike nested "using"-s 
        if (content != null
        { 
            content.Dispose(); 
        } 
        if (contentWriter != null
        { 
            contentWriter.Dispose(); 
        } 
    } 
}  


How do we extract the contents of the generic content control? There is more than one way, but the chosen one here is to render it to a string, then plug in the rendered output as the content label’s text.

How do we render the template? Well, we render all child controls and then the layout (the Container).

Since GenericContentDesigner sets its RadEditor content to the rendered output of the GenericContent control’s output, we have to do as expected. This is the reason the control won’t render normally in design mode.

Conclusion

This is a very basic example, which leaves actual customization of the looks of the control as an exercise for the reader. If you are not familiar how to apply css to Sitefinity pages, watch this web cast.

I know there are limitations that come from inheriting GenericContent, but in the end, this is the very reason to write this article – to show you that it is not difficult to accomplish it once you know what you have to do.

 

Related Links:

3.6 Dev Manual Preview