c#

5 posts

JOS.ContentJson gets replaced by JOS.ContentSerializer

I've spent some time rebuilding the JOS.ContentJson library from scratch and when doing that I realised that it was kind of dumb to limit the library to only JSON. So I've added the possibility to replace the default serializer with a custom one, so you can return the data in any format you like(you have to build the serializer yourself though, I only provide a JSON serializer out of the box). By doing that, the name ContentJson looks kind of lame, so I've released a new package named JOS.ContentSerializer instead.

You can install it by doing Install-Package JOS.ContentSerializer
If you are using the JOS.ContentJson package, you will see that I've released a new major version, 3.0. I've marked many of the methods obsolete and I've also added a readme.txt that tells you to uninstall the package and install the new one instead.

One thing that's great(and bad) with this rewrite is that I could remove/refactor whatever I wanted so it's possible that I've removed something that you guys where using. I can't be sure since everything was based on extension methods in the old library which kind of makes it impossible for me to know which breaking changes I might have introduced, hence the new major version.

Another great thing is that the library now uses dependency injection everywhere, so it's extremely easy to customize the behavior. You can find the code and a more detailed README here.

If I've removed anything you were using, please create a new issue on GitHub and I will fix it asap.

Quick example

Consider the following pagetype:

public class DemoPage : PageData  
{
    [CultureSpecific]
    [Display(
        Name = "String",
        GroupName = SystemTabNames.Content,
        Order = 100)]
    public virtual string String { get; set; }

    [CultureSpecific]
    [Display(
        Name = "ContentArea",
        GroupName = SystemTabNames.Content,
        Order = 200)]
    public virtual ContentArea MainContentArea { get; set; }

    [CultureSpecific]
    [Display(
        Name = "Degrees",
        GroupName = SystemTabNames.Content,
        Order = 300)]
    public virtual double Degrees { get; set; }

    [CultureSpecific]
    [Display(
        Name = "Int",
        GroupName = SystemTabNames.Content,
        Order = 400)]
    public virtual int Int { get; set; }

    [CultureSpecific]
    [Display(
        Name = "Date",
        GroupName = SystemTabNames.Content,
        Order = 500)]
    public virtual DateTime DateTime { get; set; }

    [CultureSpecific]
    [Display(
        Name = "Bool",
        GroupName = SystemTabNames.Content,
        Order = 600)]
    public virtual bool Bool { get; set; }

    [CultureSpecific]
    [Display(
        Name = "PageType",
        GroupName = SystemTabNames.Content,
        Order = 700)]
    public virtual PageType PageType { get; set; }

    [CultureSpecific]
    [Display(
        Name = "ContentReference",
        GroupName = SystemTabNames.Content,
        Order = 800)]
    public virtual ContentReference ContentReference { get; set; }

    [CultureSpecific]
    [Display(
        Name = "PageReference",
        GroupName = SystemTabNames.Content,
        Order = 900)]
    public virtual PageReference PageReference { get; set; }

    [CultureSpecific]
    [Display(
        Name = "Url",
        GroupName = SystemTabNames.Content,
        Order = 1000)]
    public virtual Url Url { get; set; }

    [Display(
        Name = "InternalBlock",
        GroupName = SystemTabNames.Content,
        Order = 1100)]
    public virtual VimeoVideoBlock InternalBlock { get; set; }

    [Display(
        Name = "ContentReferenceList",
        GroupName = SystemTabNames.Content,
        Order = 1200)]
    public virtual IList<ContentReference> ContentReferenceList { get; set; }

    [Display(
        Name = "XhtmlString",
        GroupName = SystemTabNames.Content,
        Order = 1300)]
    public virtual XhtmlString XhtmlString { get; set; }

    [Display(
        Name = "LinkItemCollection",
        GroupName = SystemTabNames.Content,
        Order = 1400)]
    public virtual LinkItemCollection LinkItemCollection { get; set; }

    [Display(
        Name = "SelectOne",
        GroupName = SystemTabNames.Content,
        Order = 1500)]
    [SelectOne(SelectionFactoryType = typeof(ContactPageSelectionFactory))]
    public virtual string SelectOne { get; set; }

    [Display(
        Name = "SelectMany",
        GroupName = SystemTabNames.Content,
        Order = 1600)]
    [SelectMany(SelectionFactoryType = typeof(ContactPageSelectionFactory))]
    public virtual string SelectMany { get; set; }
}

By calling .ToJson on it like this

public class DemoPageController : PageController<DemoPage>  
{
    public string Index(DemoPage currentPage)
    {
        return currentPage.ToJson();
    }
}

You would get the following result

{
    "string": "This is a string",
    "mainContentArea": {
        "vimeoVideoBlock": [{
            "name": "My Vimeo Block"
        }]
    },
    "degrees": 133.7,
    "int": 1337,
    "dateTime": "2017-05-18T00:00:00+02:00",
    "bool": true,
    "pageType": "DemoPage",
    "contentReference": "http://localhost:52467/about-us/management/",
    "pageReference": "http://localhost:52467/alloy-plan/download-alloy-plan/",
    "url": "http://localhost:52467/globalassets/alloy-meet/alloymeet.png",
    "internalBlock": {
        "name": "Im a Vimeo block",
        "mainContentArea": {
            "youtubeVideoBlock": [{
                "name": "I am a youtube block in a contentarea on a Vimeoblock"
            }]
        }
    },
    "contentReferenceList": ["http://localhost:52467/search/", "http://localhost:52467/alloy-meet/"],
    "xhtmlString": "<p>I am a xhtmlstring, do you like it?</p>\n<p>Im <strong>bold not <em>bald</em></strong></p>",
    "linkItemCollection": [{
        "href": "http://localhost:52467/alloy-plan/?query=any",
        "title": "Any title",
        "target": "_blank",
        "text": "Any text"
    }, {
        "href": "https://josef.guru",
        "title": "External link",
        "target": "_blank",
        "text": "External"
    }, {
        "href": "mailto:[email protected]",
        "title": "Email link",
        "target": null,
        "text": "Email"
    }],
    "selectOne": [{
        "selected": false,
        "text": "Amar Gupta",
        "value": "34"
    }, {
        "selected": false,
        "text": "Fiona Miller",
        "value": "33"
    }, {
        "selected": true,
        "text": "Michelle Hernandez",
        "value": "30"
    }, {
        "selected": false,
        "text": "Robert Carlsson",
        "value": "32"
    }, {
        "selected": false,
        "text": "Todd Slayton",
        "value": "31"
    }],
    "selectMany": [{
        "selected": false,
        "text": "Amar Gupta",
        "value": "34"
    }, {
        "selected": true,
        "text": "Fiona Miller",
        "value": "33"
    }, {
        "selected": false,
        "text": "Michelle Hernandez",
        "value": "30"
    }, {
        "selected": false,
        "text": "Robert Carlsson",
        "value": "32"
    }, {
        "selected": false,
        "text": "Todd Slayton",
        "value": "31"
    }]
}

Custom Episerver routes without querystring - IPartialRouter

I will show you how to easily bind parameters to your action methods without using the querystring.

I have a page, MyPage, with a action method that looks like this:

public async Task<ActionResult> Index(MyPage currentPage, int myId)  
{
    var response = await this.thirdPartyApi.Get(myId);
    // Do something with the response and return a view
    var model = MyModel.Create(response);
    return View(model);        
}

I've also created the page in editmode, I named it MyExamplePage so I can access it like this: https://example.com/MyExamplePage

So, in order to populate the myId parameter I can call my page like this:

https://example.com/MyExamplePage?myId=1337

Asp.net will automatically populate the myId parameter from the querystring and everything works just fine, great. But, the URL looks kinda ugly, don't you think?

I want it to look like this instead: https://example.com/MyExamplePage/1337

This doesn't work right out the box, we will need to write some code. I got some great help from Johan Björnfot in this thread over at Episerver World.

It's actually really easy to make this work, here's what you will need to do:

  1. Create a class that inherits from IPartialRouter<MyPage, MyPage>
  2. Implement GetPartialVirtualPath and RoutePartial. In my case I was only interested in the RoutePartial method so I just returned null in GetPartialVirtualPath
  3. Extract and parse/validate the values in the RoutePartial method.
  4. Return the content.
  5. Register the partialrouter in Global.asax(or wherever you configure your routes). routes.RegisterPartialRouter(new ParameterPartialRouter());
  6. Done!

Here's the full code of my PartialRouter.

public class ParameterPartialRouter : IPartialRouter<MyPage, MyPage>
{
    public PartialRouteData GetPartialVirtualPath(
        MyPage content,
        string language,
        RouteValueDictionary routeValues,
        RequestContext requestContext)
    {
        return null;
    }

    public object RoutePartial(
        MyPage content,
        SegmentContext segmentContext)
    {
        var nextValue = segmentContext.GetNextValue(segmentContext.RemainingPath);
        int myId;
        if (Int32.TryParse(nextValue.Next, out myId))
        {
            segmentContext.RouteData.Values["myId"] = myId;
            segmentContext.RemainingPath = nextValue.Remaining;
        }
        return content;
    }
}

Injected AllowedTypes/AvailableContentTypes Revisited

I wrote a pretty long post about how you could "inject" AllowedTypes/AvailableContentTypes at runtime.

I got it to work for AvailableContentTypes but yesterday when I was about to try it out in a real project I noticed that my AllowedTypes implementation worked only until I tried to save the page. I would get an validation error saying that my injected allowedtypes wasn't allowed to be saved.

My class looks like this, when creating the contenttype I specify that my ContentArea can only contain VideoBlocks.

[ContentType(DisplayName = "Media Page", GUID = "5b393025-d856-4392-aa99-61f1a468ea79", Description = "")]
[AvailableContentTypes(Include = new [] {typeof(MediaPage) })]
public class MediaPage : PageData  
{
    [AllowedTypes(AllowedTypes = new[] {typeof(VideoBlock)})]
    public virtual ContentArea ContentArea { get; set; }
    public virtual IList<ContentReference> Items { get; set; }
    public virtual ContentReference ContentReference { get; set; }
}

I would then inject another type when starting up the application(see my previous post) and that would allow me to add other types to the contentarea, it would however NOT allow me to save the page.

I did some decompiling of the AllowedTypesAttribute and found the following:

protected override ValidationResult IsValid(object value, ValidationContext validationContext)  
{
    if (value == null)
        return (ValidationResult) null;
    foreach (IAllowedTypesValidator allowedTypesValidator in this.AllowedTypesValidators.Services)
    {
        if (allowedTypesValidator.CanValidate(value))
            return allowedTypesValidator.IsValid(value, validationContext, (IEnumerable<Type>) this.AllowedTypes, (IEnumerable<Type>) this.RestrictedTypes);
    }
  return (ValidationResult) null;
}

See the problem? The validation uses this.AllowedTypes and this.RestrictedTypes when getting the Allowed/RestrictedTypes. This means that it will ONLY allow types specified directly on the property with the AllowedTypesAttribute, NOT the types that I inject. That sucks.

Since the AllowedTypesAttribute is sealed I can't do anything with that class, I can't inherit/override anything.

So I created my own attribute, InjectedAllowedTypes.
It behaves almost exactly like the AllowedTypesAttribute but I've changed the OnMetadataCreated and IsValid-method a bit.

OnMetaDataCreated

public void OnMetadataCreated(ModelMetadata metadata)  
{
    var extendedMetadata = metadata as ExtendedMetadata;
    if (extendedMetadata == null)
    {
        return;
    }

    var suffix = this.TypesFormatSuffix ?? (extendedMetadata.AdditionalValues.ContainsKey("TypesFormatSuffix") ?
                extendedMetadata.AdditionalValues["TypesFormatSuffix"] as string :
                null);

    var allowedTypes = this.AllowedTypes.Select(a => a.FullName.ToLowerInvariant()).ToList();
    var restrictedTypes = this.RestrictedTypes.Select(r => r.FullName.ToLowerInvariant()).ToList();

    var injectedAllowedTypesAttribute = InjectedAllowedTypes.GetInjectedAllowedTypesAttribute(extendedMetadata.ContainerType, extendedMetadata.PropertyName);

    if (injectedAllowedTypesAttribute != null)
    {
        allowedTypes =
                allowedTypes.Concat(
                    injectedAllowedTypesAttribute.AllowedTypes.Select(x => x.FullName.ToLowerInvariant()))
                    .Distinct()
                    .ToList();

            restrictedTypes =
                restrictedTypes.Concat(injectedAllowedTypesAttribute.RestrictedTypes.Select(x => x.FullName.ToLowerInvariant()))
                    .Distinct()
                    .ToList();
   }


   if (!string.IsNullOrEmpty(suffix))
   {
       allowedTypes = allowedTypes.ToArray().Select(a => a + "." + suffix).ToList();
       restrictedTypes = restrictedTypes.ToArray().Select(a => a + "." + suffix).ToList();
   }

   var allowedTypesArray = allowedTypes.ToArray();
   var restrictedTypesArray = restrictedTypes.ToArray();

   extendedMetadata.EditorConfiguration["AllowedTypes"] = allowedTypesArray;
   extendedMetadata.EditorConfiguration["RestrictedTypes"] = restrictedTypesArray;
   extendedMetadata.EditorConfiguration["AllowedDndTypes"] = allowedTypesArray;
   extendedMetadata.EditorConfiguration["RestrictedDndTypes"] = restrictedTypesArray;
   extendedMetadata.OverlayConfiguration["AllowedDndTypes"] = allowedTypesArray;
   extendedMetadata.OverlayConfiguration["RestrictedDndTypes"] = restrictedTypesArray;
}

IsValid

protected override ValidationResult IsValid(object value, ValidationContext validationContext)  
{
    if (value == null) return null;

    var contentLocator = ServiceLocator.Current.GetInstance<IContentLoader>();
    var validationMessage = string.Empty;
    var stringBuilder = new StringBuilder();
    var allowedTypes = this.AllowedTypes;
    var restrictedTypes = this.RestrictedTypes;
    var contentReferences = new List<ContentReference>();

    if (value is ContentArea)
    {
        var contentArea = value as ContentArea;
        contentReferences = contentArea.Items.Select(x => x.ContentLink).ToList();
    }
    else if (value is IEnumerable<ContentReference>)
    {
        var references = value as IEnumerable<ContentReference>;
        contentReferences = references.ToList();
    }
    else if (value is ContentReference)
    {
        var contentReference = value as ContentReference;
        contentReferences.Add(contentReference);
    }

    foreach (var contentReference in contentReferences)
    {
        var content = contentLocator.Get<IContent>(contentReference);
        var type = content.GetOriginalType();
        var injectedAllowedTypesAttribute = InjectedAllowedTypes.GetInjectedAllowedTypesAttribute(validationContext.ObjectInstance.GetOriginalType(),
                    validationContext.MemberName);

        if (injectedAllowedTypesAttribute != null)
        {
            allowedTypes =            allowedTypes.Concat(injectedAllowedTypesAttribute.AllowedTypes).Distinct().ToArray();
            restrictedTypes =            restrictedTypes.Concat(injectedAllowedTypesAttribute.RestrictedTypes).Distinct().ToArray();
        }

        if (restrictedTypes.Contains(type) || !allowedTypes.Contains(type))
        {
            var message =
                    string.Format(        LocalizationService.Current.GetString("/injectedallowedtypes/errormessages/notallowed"), type.Name, validationContext.MemberName);
            validationMessage = stringBuilder.Append(message).AppendLine(".").ToString();
        }
    }

    if (string.IsNullOrEmpty(validationMessage))
    {
        return null;
    }

    var validationResult = new ValidationResult(validationMessage);
    return validationResult;
}

I've basically made OnMetaDataCreated and IsValid aware of the injected types as well, not only the types specified with attributes.

To inject some types you would install my nuget packet and then create a InitializationModule and register your own types like this.

[InitializableModule]
[ModuleDependency(typeof(EPiServer.Shell.ShellInitialization))]
public class InjectedAllowedTypesInitialization : IInitializableModule  
{
    public void Initialize(InitializationEngine context)
    {
        InjectedAvailableModelSettings.RegisterCustomAvailableModelSettings(new Dictionary<Type, ContentTypeAvailableModelSetting>
        {

            {
                typeof(MediaPage) , new ContentTypeAvailableModelSetting
                                    {
                                        Availability = Availability.Specific,
                                        Included = new HashSet<Type> {typeof(CoolPage) }
                                    }
            }
        });

        InjectedAllowedTypes.RegisterInjectedAllowedTypesAttributes(new Dictionary<string, AllowedTypesAttribute>
        {
            {
                string.Format("{0}.{1}",typeof(MediaPage).Name, "ContentArea"), new AllowedTypesAttribute
                {
                    AllowedTypes = new [] {typeof(MusicBlock)}
                }
            },
            {
                string.Format("{0}.{1}", typeof(MediaPage).Name, "Items"), new AllowedTypesAttribute
                {
                    AllowedTypes = new [] {typeof(CoolPage)}
                }
            },
            {
                string.Format("{0}.{1}", typeof(MediaPage).Name, "ContentReference"), new AllowedTypesAttribute
                {
                    AllowedTypes = new [] {typeof(CoolPage)}
                }
            }
        });
    }

    public void Uninitialize(InitializationEngine context)
    {
    }

    public void Preload(string[] parameters)
    {
    }
}

So my MediaPage now looks like this

[ContentType(DisplayName = "Media Page", GUID = "5b393025-d856-4392-aa99-61f1a468ea79", Description = "")]
[AvailableContentTypes(Include = new [] {typeof(MediaPage) })]
public class MediaPage : PageData  
{
    [InjectedAllowedTypes(AllowedTypes = new[] {typeof(VideoBlock)})]
    public virtual ContentArea ContentArea { get; set; }
    public virtual IList<ContentReference> Items { get; set; }
    public virtual ContentReference ContentReference { get; set; }
}

The ContentArea would look like this in Editmode, allowing me to create a VideoBlock(because of the attribute) and also a MusicBlock(because Im injecting it in RegisterInjectedAllowedTypesAttributes above)
Filtered blockview in editmode when creating new blocks

The ContentReferenceList would look like this because of the injected allowedtypes(only allowing the CoolPagetype)
Filtered contentreferencelist only allowing CoolPage

The ContentReference would look like this because of the injected allowedtypes(only allowing the CoolPagetype)
Filtered contentreference picker only allowing CoolPage

You can find all code on github and you can get the nuget package here