Don't miss out Virtual Happy Hour this Friday (April 26).

Try our conversational search powered by Generative AI!

Create a custom property inheriting from ContentReference

Nat
Nat
Vote:
 

I am trying to create my own property type so that I can control the serialisation of it when calling the content delivery api.

it basically needs to match the functionality of a ContentReference as I dont actually require any other functionality at all, but will allow (I had hoped) me to use a PropertyConverter as in the post below to return a something other than the default of an ID to the api.

I followed the steps here: https://world.optimizely.com/documentation/developer-guides/content-delivery-api/getting-started/how-to-customize-data-returned-to-clients/customize-conversion-from-icontent-to-contentapimodel/

but i cant get it to work.

having said that, if I create the customdatetime property on the documentation page, it bombs out anyway, complaining that CustomPropertyDateTime could not be mapped to a PropertyDefinitionType,

I think because the PropertyValueType in the example is incorrectly set to DateTime instead of CustomPropertyDateTime. however if I make that change, then the getter bombs out with the below error..

InvalidCastException: Unable to cast object of type 'System.DateTime' to type 'CustomPropertyDateTime', the getter on the property this time as far as I could tell.

that aside, I plodded on and created my CustomContentReference type, and a CustomContentReferenceProperty, hoping I could just do the following:

public class CustomContentReference : ContentReference {
    public CustomContentReference (int id) : base(id)
    {
    }
//maybe I need to duplicate all the constructors here..?
}
[Serializable]
  [PropertyDefinitionTypePlugIn]
  public class CustomContentReferenceProperty: PropertyContentReference
{
    private CustomContentReference _imageReference;

    public CustomContentReference(){}
    public CustomContentReferenceProperty(int contentID) : base(contentID)
    {
      _imageReference = new CustomContentReference(contentID);
    }

    public override object Value
    {
      get => _imageReference;
      set
      {
        _imageReference = new CustomContentReference(int.Parse(value.ToString()));
        Modified();
        return;
      }
    }

    public override Type PropertyValueType => typeof(CustomContentReference);
    public override PropertyDataType Type => PropertyDataType.ContentReference
}

but it seems I need a little more than that, and even doing the below, it will not use the Block picker in the CMS UI to allow the selection of a block - and just renders a textarea.

[Display(GroupName = PropertyGroupNames.Content, Order = 20)]
    [UIHint(UIHint.Block)]
    public virtual CustomContentReference MyBlockBlock { get; set; }

clearly I am missing something, but not sure what it is. any help much appreciated

#279403
Edited, Apr 28, 2022 14:31
Vote:
 

I think that CustomContentReferenceProperty will not work since I suspect the built in JsonConverter for ContentReference will kick in and then the type ending up in your implementation will be ContentReference, not CustomContentReference. So you would probably have to implement your own JsonConverter and override the CanConvert method:

public override bool CanConvert(Type objectType) => typeof(CustomContentReference).IsAssignableFrom(objectType);

Also, the EditorDescriptor for "UIHint.Block" is explicitly targeting the ContentReference type so it will have no effect on your custom type. You will have to create your own editor descriptor, like this:

[EditorDescriptorRegistration(TargetType = typeof (CustomContentReference), UIHint = UIHint.Block)]
public class CustomBlockReferenceEditorDescriptor : BlockReferenceEditorDescriptor
{
}
#279598
May 02, 2022 9:07
Vote:
 

Hi,

If you're just trying to modify the JSON model returned when a ContentReference is used as a property on a page, that should be quite easy.

First, create a custom serializable model, inheriting ContentModelReference (EPiServer.ContentApi.Core.Serialization.Models):

public class CustomContentModelReference : ContentModelReference
{
    public string CustomValue { get; set; }
}

Then add a property model, inheriting ContentReferencePropertyModel (EPiServer.ContentApi.Core.Serialization.Models):

public class CustomContentReferencePropertyModel : ContentReferencePropertyModel
{
    public CustomContentReferencePropertyModel(PropertyContentReference propertyContentReference, ConverterContext converterContext) : base(propertyContentReference, converterContext)
    {
        Value = new CustomContentModelReference
        {
            // Map the existing properties
            Id = Value.Id,
            WorkId = Value.WorkId,
            GuidValue = Value.GuidValue,
            ProviderName = Value.ProviderName,
            Url = Value.Url,

            // Add our custom one
            CustomValue = "Custom"
        };
    }
}

This is detected via reflection--so it should work automatically.

If you also want to customize PageReferences you'll need to do the same but with the PageReferencePropertyModel.

Good luck!

#279659
Edited, May 03, 2022 9:35
Nat
Vote:
 

thanks for the replies, 

@Jake, its more that for a specific block type used in a page, I would like to deal with it a little differently.

I had thought the simplest way to do this was to use a ContentPropertyConverter. But as its a the actual property is a ContentReference I would not be easily able to distinguish what block it was referencing without actually loading it. but if I had my own property type when this block was used, I could have a PropertyConverter just for that.

I mean its quite possible I am looking at this all the wrong way. we have a number of page/block types which will need to be serialized somewhat differently to the default, and I am not finding the docs particularly clear.

should I be using a single ContentConverterProvider and PropertyConverterProvider and switching my way through type checking to load the Conveters to different page/block types?

for instance, how would I go about dealing with serialisation for certain properties with reside in a BasePage, which all other pages inherit from whilst allowing for page specific custom serialisation aswell.

#279686
Edited, May 03, 2022 12:51
Vote:
 

I'm not sure I particular like switching the API model depending on the context, because it means the frontend can't rely on a consistent ContentReference model.

Having said that, if you want to do it—you're correct that you'd want to use a IPropertyConverterProvider.

If I take your example above, you'd want something like:

[ServiceConfiguration(typeof(IPropertyConverterProvider), Lifecycle = ServiceInstanceScope.Singleton)]
public class CustomContentReferencePropertyConverterProvider : IPropertyConverterProvider
{
	private readonly IContentLoader _contentLoader;

	public CustomContentReferencePropertyConverterProvider(IContentLoader contentLoader)
	{
		_contentLoader = contentLoader;
	}

	public int SortOrder => 1;

	public IPropertyConverter Resolve(PropertyData propertyData)
	{
		// Only handle content references
		if (!(propertyData is PropertyContentReference))
		{
			return null;
		}

		// Check propertyData.Name?

		var ownerLink = propertyData.Parent?.OwnerLink;

		if (ContentReference.IsNullOrEmpty(ownerLink))
		{
			return null;
		}

		if (!_contentLoader.TryGet<IContent>(ownerLink, out var ownerContent))
		{
			return null;
		}

		// Validate that the property belongs to the base page type
		if (ownerContent is BasePageType)
		{
			// Return your custom IPropertyConverter
		}

		return null;
	}
}

You could put in a whole hierarcy of logic here, depending on what your precisely needed.

Hope that helps.

#279688
Edited, May 03, 2022 13:58
Nat
Vote:
 

it does, thanks, although I think I may have confused matters by asking a seperate question in my last post.
the reason I was trying to create a custom property was because although I wanted it to function like a ContentReference in the CMS editor interface, I wanted the API to return json in a different format to the default.

Something else related to this, is to deal with properties which are not in all pages.

Say I have a HomePage which has the following properties

public class HomePage : BasePage {
    [Display(GroupName = PropertyGroupNames.Footer, Order = 100)]
    public virtual XhtmlString Disclaimer { get; set; }

    [Display(GroupName = PropertyGroupNames.Footer, Order = 110)]
    public virtual ContentArea Buttons { get; set; }

    [Display(GroupName = PropertyGroupNames.Footer, Order = 120)]
    public virtual ContentReference Logo { get; set; }

    [Display(GroupName = PropertyGroupNames.Footer, Order = 130)]
    public virtual Url LogoLink { get; set; }
}

public class SimplePage : BasePage {
   //SimplePage Specific Properties
}

When I query the API for the HomePage the properties are rightly included in the json.

But I also have child pages below that HomePage, which inevitably also need these header properties in their responses, and I dont want to have to duplicate these header properties on all pages. 

I understood that the converters/filters sounded like the way to approach this.

public class SimplePageConverter : DefaultContentConverter{
  private IMyHeaderService MyHeaderService{ get; }
  public SimplePageConverter(IMyHeaderService myHeaderService, dependencies) : base(dependencies)
  {
    MyHeaderService = myHeaderService;
  }

  public override ContentApiModel Convert(IContent content, ConverterContext converterContext){
    var model = base.Convert(content, converterContext);
    var headerProperties = MyHeaderService.GetHeaderProperties(content); 
    var convertedHeader = ???(headerProperties); //expanded properties as they would be when included in the Homepage conversion based on the flags in ConverterContext
    model.Properties["header"] = convertedHeader;
    return model;
  }
}

Create some service that can find the page with the desired properties, grab them and tack them onto the response via the converter for the requested page type. The trouble seems to be, at the point of hitting said page converter, and loading in the header bits unless I completely control the entire serialisation (ideally not) there doesnt seem to be an easy way to Convert the header properties so that I can add them to the .Properties[] collection of the ContentApiModel. It seems like I might have to spin up all the Property converters from within my PageConverters to do this.
Maybe I should be wrapping the header properties in a block instead, but not really sure how to deal with blocks like this as they are effectively just a group of properties.

tried very hard to make that make sense, but may well have failed 

#279689
Edited, May 03, 2022 15:48
Vote:
 

Have a look at how this is done for the MusicFestival sample site:

Basically, the DefaultContentModelMapper is intercepted so you can extend the existing functionality. When you implement your TransformContent method, you could call the default one then add the additional properties to the resulting ContentApiModel based on the content type.

However, if you wanted to add the properties to all content types then it's easier just to add an IContentApiModelFilter (as per the documentation).

#279691
May 03, 2022 16:30
Nat
Vote:
 

thanks Jake,

I had looked at that, but I am using the v12/v3 version of the ContentApi, and the IContentModelMapper/DefaultContentModelMapper doesnt seem to be in the nuget packages anymore and seems to have been replaced with the converters - unless I am missing something?. docs

I was avoiding using the ContentApiModelFilter as I didnt want to have to load the content in again, seemed in the converters the IContent instance is loaded already, I guess it would be cached though - but still would have to convert/serialise it somehow.

#279699
Edited, May 03, 2022 16:56
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.