How to render content areas without using the built in functionality in EPiServer. And how to maintain some sort of on-page-editing functionality.
Content areas is a great new feature in EPiServer 7. One reason for that is of course the obvious one – UI composition using blocks and partial views for pages.
From a developer’s perspective there’s also another reason to be happy about content areas – we finally have a property type that supports multiple page references. That is, beyond using content areas for UI composition we can also use them whenever we want to enable editors to add multiple references to content items.
A couple of examples where content areas can come in handy is lists of links in headers and footers and lists of related pages.
Another example, which I’ll use in this article, is if we have a list of tags where each tag is represented by a page.
What we need here is a property that contains references to those tag pages. As the value of a content area property is a list of references to content items they work perfectly for this.
All we need to do is add a content area property to the page type that should have a tags property and render it using the PropertyFor method, right? Not quite.
We also need to create partial renderers for tag pages. If we’re dealing with pages of a type that may already have other partial renderers, such as “regular” pages that we want to add to a list of related pages or as links to the site’s header, we also need to use a rendering tag to match the content area with a specific renderer.
While that solution works it adds overhead both in terms of development time and (slightly) performance wise.
Rendering content areas ourselves
Luckily there’s a simpler solution. We can render the content are ourselves by iterating over the items in it.
<div class="tags"> @foreach (var tag in Model.CurrentPage.Tags.FilteredContents.OfType<PageData>()) { @Html.PageLink(tag) } </div>
Simple and nice, right? There’s just one problem. The property isn’t editable in on page edit mode.
To fix that when dealing with properties of other types we can normally add edit attributes to the wrapping element.
<div class="tags" @Html.EditAttributes(x => x.CurrentPage.Tags)> @foreach (var tag in Model.CurrentPage.Tags.FilteredContents.OfType<PageData>()) { @Html.PageLink(tag) } </div>
When rendering content areas ourselves applying edit attributes to the wrapping element does make the content area property editable, but EPiServer’s client side editing functionality can’t recognize that there’s anything in the content area.
Replicating native content area editing
In order to fix that we need to tell EPiServer about what content item each link, or whatever element we’re rendering in our loop, represents.
This is done by adding an attribute named data-epi-property-name to such elements, or a wrapping element. It’s value should be the string representation of the content’s ContentLink property.
<div class="tags" @Html.EditAttributes(x => x.CurrentPage.Tags)> @foreach (var tag in Model.CurrentPage.Tags.FilteredContents.OfType<PageData>()) { <span data-epi-block-id="@tag.ContentLink.ToString()"> @Html.PageLink(tag) </span> } </div>
With that in place EPiServer’s edit UI recognizes that there are contents in the content area and also what content each element represents.
Looks like it’s working. We’re not quite done yet though. When a property’s value is changed EPiServer updates it’s place in the DOM with what it believes should be there. In the case of a content area that’s the default content area rendering.
In order to make our own rendering be used instead we’ll have to force a full refresh of the page when the property is changed.
How to do that is a topics of its own considering that it can be done in multiple ways in both Web Forms and MVC. The simplest solution in an MVC view would be to add a line like this:
@Html.FullRefreshPropertiesMetaData(new [] { "Tags"})
Dialog only editing
One issue with the solution described above is that the rendering of the content area in edit mode will differ from how it will actually look, at least when dealing with small elements such as links. EPiServer adds a minimum width of a hundred pixels to each element that represents an item in a content area.
While we could possibly fix that, editing a list of links or other small elements the same way as we do with a “real” content area used with blocks and stuff may not be the best of ideas. It looks a bit cluttered.
Also, if we order the items in the area in some other way than by the order in the area editors will perceive that they can change the order while they can't.
One option to handle this is to disable the on-page drag-n-drop editing of the property while keeping the dialog for editing that normally shows in a right hand panel when clicking on a content area.
To do that we can first disable the standard editor for content areas for the property by adding a UI hint to it.
[UIHint("DialogOnly")] public virtual ContentArea Tags { get; set; }
We then create an editor descriptor that half-way re-enables the standard content area editing for properties with that UI hint. Optionally, we can also position the dialog close to the property by specifying “uiWrapperType”.
[EditorDescriptorRegistration( TargetType = typeof(ContentArea), UIHint = "DialogOnly")] public class FormsOnlyContentAreaEditing : EditorDescriptor { public override void ModifyMetadata( ExtendedMetadata metadata, IEnumerable<Attribute> attributes) { base.ModifyMetadata(metadata, attributes); ClientEditingClass = "epi.cms.contentediting.editors.ContentAreaEditor"; metadata.CustomEditorSettings["uiWrapperType"] = "flyout"; } }
Now, when viewing the page in edit mode the property has the regular blue box around it. Editors can click on it an edit its contents in a dialog.
One nice aspect of this approach is that there’s no need to communicate things to EPiServer by using data attributes. That is, compared to trying to replicate the native content area editing we only need to add edit attributes and force a full page refresh when the property’s value changes.
A third option – faking it
Finally, we may want something in between the two solutions described above. I’m thinking about a solution in which the rendering isn’t affected and the display of the list isn’t cluttered by making the order of the items editable inline on the page but where it’s still possible to drag-n-drop new items into the list.
To accomplish this we don’t need any editor descriptor and we don’t need to add a UI hint to the property. We do need to clutter up the view’s code though.
<div @Html.EditAttributes(x => x.CurrentPage.Tags)> @if (PageEditing.PageIsInEditMode) { foreach (var tag in Model.CurrentPage.Tags.FilteredContents.OfType<PageData>()) { <span style="display: none;" data-epi-block-id="@tag.ContentLink.ToString()"> </span> } } @foreach (var tag in Model.CurrentPage.Tags.FilteredContents.OfType<PageData>()) { @Html.PageLink(tag) } </div>
As you can see, if we’re rendering the page in edit mode we’re iterating over the contents in the content area one more time. The first time we output hidden elements that EPiServer can use to determine what’s in the content area. The second time we render the contents “for real”.
This way we get the parts of the standard content area editing that we (may) like but hide the parts that would look cluttered in a small space and mess with the layout.
Heads up!
In the examples above we’ve rendered the contents of a content area property by iterating over its FilteredContents property. That’s a good thing as the FilteredContents exclude unpublished contents.
However, when displaying it to an editor we do want to include unpublished contents. Especially if we’re using one of the solutions where we allow some sort of inline editing of the area. That is, the two other solutions besides the dialog-only one.
If we don’t, any unpublished item in the area will be removed when editing the property as EPiServer replaces the existing items with whatever it finds in the DOM.
Therefor, we should have some logic that determines whether we should iterate over the content area’s FilteredContents or Contents property depending on what context we’re rendering it in. I’ve omitted that for the sake of brevity in the examples however.
Another thing I’ve done for the sake of brevity is hardcoding the data-epi-block-id attribute. To be on the safe side we should probably use the constant EPiServer.Editor.PageEditing.DataEPiBlockId instead.
PS. For updates about new posts, sites I find useful and the occasional rant you can follow me on Twitter. You are also most welcome to subscribe to the RSS-feed.
My book
Want a structured way to learn EPiServer 7 development? Check out my book on Leanpub!
Comments
comments powered by Disqus