Bartosz Sekula
Mar 28, 2023
  5381
(11 votes)

Official List property support

Introduction

Until now users were able to store list properties in three ways:

All those approaches had its own issues and limitations.

The first one's obvious limitation was that it was only able to operate on very basic types. On the other hand it provided a nice editing experience with an inline value editor and autosave.

The next one was much more powerful because it allowed the user to use any kind of POCO and then render the list in a tabular way:

755

and let the user edit each item from within a dialog:

757

However this approach also had a few issues:

  • Property values are serialized to JSON and stored as plain string in the database
  • Permanent links are not validated
  • Some things like import/export, default values, custom metadata extenders need to be implemented manually

Finally the approach with using ContentArea or a list of ContentReference in theory solves all those issues because the editor has the ability to define the block list item anyhow and is able to use existing properties, however each list item is a separate IContent instance which has its own publishing lifecycle. In addition it also goes through the same approval process, can be included in a project as project item etc.

Because of the fact that each block is a separate IContent it is not possible to easily preview the changes or publish all items as once.

There were several attempts to solving this issue, one of them being the Block Enhancements Labs package:

https://nuget.optimizely.com/package/?id=EPiServer.Labs.BlockEnhancements

https://world.optimizely.com/blogs/grzegorz-wiechec/dates/2019/7/episerver-labs---block-enhancements/

The new list property

In EPiServer.CMS 12.18.0 we are releasing a brand new list property which solves all the issues outlined above.

Adding a list property is as simple as:

public virtual IList<ContactBlock> Contacts { get; set; }

Where ContactBlock can be defined like this:

[ContentType(AvailableInEditMode = false, GUID = "38d57768-e09e-4da9-90df-54c73c61b270")]  
public class ContactBlock : BlockData  
{  
   //block properties  
}

What is important here is the fact that each ContactBlock instance will not be a content item of its own but will be stored inside its parent.

That fact has several implications:

  • Editor no longer needs to switch context to edit those inline blocks
  • Inline blocks do not have publishing lifecycle meaning that whenever any list item is changed the user the editor will see a new version of the current content being created
  • List items will inherit approval sequence from their parent, it is the parent content that has to be reviewed and approved
  • List items will not be included in projects, only their parent content as a whole

The editor of such contact block list will look like this:

Of course, having the ability to make the blocks inline is the primary use case of the new List<T> however it is capable of storing any kind of property type inside.

Users can still utilize basic types like:

public virtual IList<int> Numbers { get; set; }

public virtual IList<string> Strings { get; set; }

public virtual IList<DateTime> Dates { get; set; }

But it is also possible to use more complex types:

public virtual IList<XHtmlString> XHtmlStrings { get; set; }

Or for example create a list of images or videos:

[ListItemUIHint(UIHint.Image)]
public virtual IEnumerable<ContentReference> Images { get; set; }

Which will be rendered like this:

Please note that the property is annotated with a new attribute ListItemUIHintAttribute which works the same as regular UIHint but applies to the generic type of the list item.

So we are telling the List property to use UIHint.Image for individual items.

It is still possible to use UIHint("YourCustomListEditor") if you have your own editor.

Rendering

No special attributes or techniques are needed in order to render a list of any property types.

The only thing needed is to provide a display template for a single item and the CMS will do the rest by wrapping each item in a list item.

public virtual IList<EditorialBlock> Blocks { get; set; }

You will just need to provide EditorialBlock.cshtml which could look like this:

@model EditorialBlock

<div class="clearfix" @Html.EditAttributes(x => x.MainBody)>
    @Html.DisplayFor(x => Model.MainBody)
</div>

And to render the `Blocks` property on the page you can use either HtmlHelpers package:

@model MyPage

@Html.PropertyFor(x => x.EditorialBlocks)

Or the new TagHelpers package:

@model MyPage

<div epi-property="EditorialBlocks" />

Migration

The data model behind the scenes is much different from the previous implementations which always were based on some sort of JSON serialization.

There is no automatic way to migrate from the old model to the new one and all migrations might different from case to case but let's imagine a hypothetical scenario like this:

Let's say we have a page MyPage.cs with a single property inside:

public class MyPage : PageData 
{
   [EditorDescriptor(EditorDescriptorType = typeof(CollectionEditorDescriptor<Person>))]
   public virtual IList<Person> PersonList { get; set; }
}

public class Person
{    
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
    public string Email { get; set; }
}

[PropertyDefinitionTypePlugIn]
public class PersonListProperty : PropertyList<Person>
{
    public PersonListProperty()
    {
        _objectSerializer = _objectSerializerFactory.Service.GetSerializer(KnownContentTypes.Json);
    }

    private Injected<IObjectSerializerFactory> _objectSerializerFactory;

    private IObjectSerializer _objectSerializer;

    protected override Person ParseItem(string value)
    {
        return _objectSerializer.Deserialize<Person>(value);
    }
}

The property would look like this in Edit Mode:

In order to migrate to the new property and use the new inline editor we would have to:

Define a new PersonBlock type and mark it as AvailableInEdit = false

[ContentType(AvailableInEditMode = false, GUID = "11157768-e09e-4da9-90df-54c73c61b270")] 
public class PersonBlock : BlockData
{    
    public virtual string FirstName { get; set; }
    public virtual string LastName { get; set; }
    public virtual int Age { get; set; }
    public virtual string Email { get; set; }
}

In order to use it on MyPage:

public class MyPage : PageData 
{
   public virtual IList<PersonBlock> PersonsNew { get; set; }
}

And the new editing experience will look like this:

Please note that it is no longer necessary to define your own property type.

In order to migrate from IList<Person> to IList<PersonBlock> a custom piece of code which iterates over all instances of MyPage analyzes what is inside PersonList property and then uses the same values to create an a list of PersonBlock instances.

Next improvements coming up

We are planning to take it even further and allow to inline blocks into Content Area as well so stay tuned for next updates on that matter.

Eventually we will turn off `Quick edit` command and promote the new Inline Blocks which no longer need any kind of tricks to keep their publishing lifecycle synchonized with their parent.

We will also not need any commands like `Publish page with blocks` https://github.com/episerver/EPiServer.Labs.BlockEnhancements#publish-page-and-shared-blocks 

Obsoleting EPiServer.Labs.BlockEnhancements

The whole idea of `inlining` the blocks into page came up as part of our Labs initiative which turned out to be highly successful https://github.com/episerver/EPiServer.Labs.BlockEnhancements#local-content 

Now, since most of Labs functionality ends up in the official package it is time to deprecate it.

Mar 28, 2023

Comments

huseyinerdinc
huseyinerdinc Mar 28, 2023 01:46 PM

This is a long awaited feature which creates a huge value for the clients!🥳

Ove Lartelius
Ove Lartelius Mar 29, 2023 09:26 AM

Nice feature and a really good blog post! Cheers!

Surjit Bharath
Surjit Bharath Mar 29, 2023 01:22 PM

This was a great write up and you've also included a technique for the migration. Thanks for this!

Pierre Vignon
Pierre Vignon Apr 12, 2023 04:07 PM

Thank you! This will be really very useful ! I already started to use it in our new project.

However be aware it seems there is a important bug inside the version 12.18.0: if you have an inline Block in a page you can't use
"public virtual IList<string> myBlockProperty" in a block property the new UI will not show up. It looks like a regression as it was working before.

Bartosz Sekula are you aware of this issue ?


Bartosz Sekula
Bartosz Sekula Apr 28, 2023 07:52 AM

Pierre, the issue has been fixed in https://nuget.optimizely.com/package/?id=EPiServer.CMS.UI&v=12.19.0

PuneetGarg
PuneetGarg May 24, 2023 07:17 AM

Thank you sharing this is important and very knowledge full. 

Shamrez Iqbal
Shamrez Iqbal May 26, 2023 09:20 AM

@bartosz Is it possible to get the ID of the containing page for a block which is defined inside such a list. 

I need it inside a selectionfactory on a property defined in the block definition but the extended metadata paramater does not contain any reference to the containing page.

QuirijnLangedijk
QuirijnLangedijk Jun 2, 2023 08:53 AM

Great future! But, it'd be great if drag-and-drop ordening was brought back (like IList<> used to work), moving items by 1 position per 2 clicks doesn't feel as user friendly as being able to reorder them by dragging/dropping, especially for longer lists

Ted
Ted Jun 16, 2023 01:44 PM

Great work - we've looked forward to this for a long time!

Is it possible to override what text is displayed for each item?

Let's say you have a lot of contacts, like the `PersonBlock` example. Each item will simply say "PersonBlock", making it difficult to find a specific item/contact.

It would be helpful if the item text could be for example the name of the person. Finding "Todd" in the list is easier than expanding all items to look for the right one. :)

Pierre Vignon
Pierre Vignon Jun 16, 2023 03:17 PM

@bartosz yes I confirm this specific issued was fixed in the 12.19.0, thank you!

However we noticed few other issues with this feature.

If the MyBLock of field IList<MyBLock> as [CultureSpecific] field(s) it will just not work (in contrary of single inlined MyBLock). The support answer me that I have to make the field IList<MyBlock> itself [CultureSpecific] and redo the list in other language which is not ideal. So it's an important limitation to be aware of!

Bartosz Sekula
Bartosz Sekula Jun 19, 2023 08:47 AM

Ted, 

It is possible to decide which property to use to render list item header:

[ListItemHeaderProperty(nameof(Blocks.ContactBlock.Heading))]
public virtual IEnumerable<ContactBlock> ContactBlocks { get; set; }

I've updated the docs, new version will be published very soon:

https://docs.developers.optimizely.com/content-management-system/edit/property-value-list

Bartosz Sekula
Bartosz Sekula Jun 19, 2023 08:50 AM

Pierre, what is your expectation? Let's say you have an English version with IList<ContactBlock> and inside you have 2 contacts. What should happen after translating that page to Italian?

Ted
Ted Jun 19, 2023 08:53 AM

Fantastic news on the `ListItemHeaderProperty`, nicely done! Thanks!

Pierre Vignon
Pierre Vignon Jun 20, 2023 02:54 PM

@bartosz I guess I would assume it to work in the same way a solo inlined block is working ?
ie:
If the field `IList<TestBlock> ...` in the Parent Page is not `culture specific` the list will be the same in all languages but
fields of the `TestBlock` which are `culture specific` will be modifiable in all the Parent Page language. (
public virtual IList<TestBlock> TestBlockList { get; set; })
This would be the same behavior as directly adding the TestBlock in a field of ParentPage. (
public virtual TestBlock TestBlockInlined { get; set; })

It is easy to quickly test it using Alloy demo.

public class TestBlock : SiteBlockData
{ [CultureSpecific]
public virtual string MyTitle { get; set; }
// Not culture specific field:
public virtual string TestId { get; set; }
}


Thank you!

Wojciech Gabel
Wojciech Gabel Aug 31, 2023 02:28 PM

Hi Bartosz!
Can you elaborate on how to migrate list items? Should we use the standard GetDefault<PersonBlock> method and then Save those instances like standard blocks, using repository.Save()? How are they going to be saved? Where are they stored? Do we need to specify parent for those blocks?

Please login to comment.
Latest blogs
Recraft's image generation with AI-Assistant for Optimizely

Recraft V3 model is outperforming all other models in the image generation space and we are happy to share: Recraft's new model is now available fo...

Luc Gosso (MVP) | Nov 8, 2024 | Syndicated blog

ExcludeDeleted(): Prevent Trashed Content from Appearing in Search Results

Introduction In Optimizely CMS, content that is moved to the trash can still appear in search results if it’s not explicitly excluded using the...

Ashish Rasal | Nov 7, 2024

CMS + CMP + Graph integration

We have just released a new package https://nuget.optimizely.com/package/?id=EPiServer.Cms.WelcomeIntegration.Graph which changes the way CMS fetch...

Bartosz Sekula | Nov 5, 2024

Block type selection doesn't work

Imagine you're trying to create a new block in a specific content area. You click the "Create" link, expecting to see a CMS modal with a list of...

Damian Smutek | Nov 4, 2024 | Syndicated blog