Taking control of block rendering in XhtmlString properties

Mattias Olsson 1/13/2016 4:26:22 AM

Method 1 - Tag and view naming

This is the simplest solution. Imagine you have a view with the filename MyBlock.cshtml. If you call your tag "Wysiwyg", just create a copy of your view, name it MyBlock.Wysiwyg.cshtml and make some changes to it. Don't forget to set the tag in PropertyFor as shown below.

@Html.PropertyFor(m => m.MainBody, new { Tag = "Wysiwyg" })

 

Pros:

  • Very easy to implement.
  • Easy to understand for new developers.

Cons:

  • Can lead to many views if you have lots of blocks in your project.

Method 2 - Tag and IViewTemplateModelRegistrator

If you don't have that many blocks and don't like the naming convention in method 1 you can manually add template models at site startup.

[ServiceConfiguration(typeof(IViewTemplateModelRegistrator))]
public class ViewRegistrator : IViewTemplateModelRegistrator
{
    public virtual void Register(TemplateModelCollection viewTemplateModelRegistrator)
    {
            viewTemplateModelRegistrator.Add(MyBlock, new TemplateModel
            {
                Name = "MyBlockWysiwyg",
                AvailableWithoutTag = false,
                Tags = new[] { "Wysiwyg" },
                Path = "~/Views/Shared/MyBlockInWysiwyg.cshtml"
            });
    }
}

 

Pros:

  • You have full control of your view filenames.

Cons:

  • A lot of registration required if you have many blocks.
  • Might not be as obvious as method 1 for new developers.

Method 3 - Create your own implementation of IContentRenderer

The last method is useful in scenarios where your block markup needs to be wrapped in a container element when it is rendered in a XhtmlString for some reason, but you don't want to create extra views just because of that simple fact. One of those reasons might be that you are working together with another company on the project and they deliver the frontend code so you'll need to implement the way they write it. This is also applicable in other situations where core block markup should be the same no matter where it is rendered. IContentRenderer to the rescue!

First of all, I create an interface that I can implement on all blocks that require a wrapping element:

public interface IWysiwygBlock
{
    string CssClassInXhtmlString { get; }
}

Now let's move on to the custom implementation of IContentRenderer. For this example I'll just inherit the default one, MvcContentRenderer, and override the Render method. I wish it was virtual. ;)

[ServiceConfiguration(typeof(IContentRenderer))]
public class SiteContentRenderer : MvcContentRenderer, IContentRenderer
{
    public SiteContentRenderer(
            DisplayOptions displayOptions, 
            CachingViewEnginesWrapper cachingViewEngineWrapper, 
            IServiceLocator serviceLocator) 
: base(displayOptions, cachingViewEngineWrapper, serviceLocator) { } public new void Render( HtmlHelper helper, PartialRequest partialRequestHandler, IContentData contentData, TemplateModel templateModel) { var tag = helper.ViewContext.ViewData["tag"] as string; var wysiwygBlock = contentData as IWysiwygBlock; if (templateModel != null && tag == "Wysiwyg" && wysiwygBlock != null) { var contentDataHtml = new StringBuilder(); TextWriter originalWriter = helper.ViewContext.Writer; using (var tempWriter = new StringWriter(contentDataHtml)) { // Temporarily switch the writer helper.ViewContext.Writer = tempWriter; // Write the IContentData to the temporary writer base.Render(helper, partialRequestHandler, contentData, templateModel); // Switch back to original writer helper.ViewContext.Writer = originalWriter; // The block is not empty, add a wrapping element for it // and write it to the original writer. if (contentDataHtml.Length > 0) { var div = new TagBuilder("div"); div.AddCssClass(wysiwygBlock.CssClassInXhtmlString); helper.ViewContext.Writer.Write(div.ToString(TagRenderMode.StartTag)); helper.ViewContext.Writer.Write(contentDataHtml.ToString()); helper.ViewContext.Writer.Write(div.ToString(TagRenderMode.EndTag)); } } return; } base.Render(helper, partialRequestHandler, contentData, templateModel); } }

Dont forget to replace the default implementation of IContentRenderer with your own at site startup in an initializable module or something similar.

Example of usage

Let's say I have an expandable facts block that the editor should be able to align either center, left or right.

Model:

[ContentType]
public class EditorialBlock : BlockData, IWysiwygBlock
{
    [Display(Name = "Heading", Order = 100)]
    [CultureSpecific]
    public virtual string Heading { get; set; }

    [CultureSpecific]
    [Display(Name = "Text", Order = 110)]
    public virtual XhtmlString Text { get; set; }

    [Display(Name = "Alignment when placed in text editor", Order = 120)]
    // The selection factory is not included in this example.
    // Possible selections are center, left and right.
    [SelectOne(SelectionFactoryType = typeof(BlockAlignmentSelection))]
    public virtual string AlignmentInXhtmlString { get; set; }

    public string CssClassInXhtmlString
    { 
        get { return string.Format("block-{0}", this.AlignmentInXhtmlString); }
    }
}

 

View:

using EPiServer.Web.Mvc.Html
@model EditorialBlock

<div class="block-facts">
    <h2 class="block-facts-heading" @Html.EditAttributes(m => m.Heading)>
        @Html.DisplayFor(m => m.Heading)
    </h2>
    <div class="block-facts-content">@Html.PropertyFor(m => m.Text)
        <div class="block-text-toggle">Read more</div>
    </div>
</div>

 

When rendered in an XhtmlString this will be the end result if the editor chose "left" as alignment:

<div class="block-left">
    <div class="block-facts">
        <h2 class="block-facts-heading">
            Some heading
        </h2>
        <div class="block-facts-content">
            <p>
                Lorem Ipsum is simply dummy text of the printing and typesetting industry. 
                Lorem Ipsum has been the industry's standard dummy text ever since the 
                1500s, when an unknown printer took a galley of type and scrambled it 
                to make a type specimen book.
            </p>
            <div class="block-text-toggle">Read more</div>
        </div>
    </div>
</div>

 

Pros:

  • No extra views needed.

Cons:

  • Not so obvious for new developers (where the hell is that wrapping element coming from?!).

Of course, you can combine all of the methods in your project. Let me know if you have any further ideas. Happy coding!