You may add blocks to pages. Blocks should not be accessed directly by an URL.
Hi Hans,
I needed to do something similar to be able to pull in HTML for a block via AJAX.
The way I approached it was to add an interface (IAllowBlockPartialRendering) to the blocks I wanted to be allowed to render. Then I created an action in my PageControllerBase:
public ActionResult BlockPartialHtml(ContentReferenceModelList content)
{
if (content == null)
return new EmptyResult();
// Check we are allowed to render all these items
return PartialView("BlockPartialHtml",
content.Where(c => (c.Reference.GetContent() is IAllowBlockPartialRendering));
}
Where the ContentReferenceModelList is just a list of ContentReferenceModel:
[ModelBinder(typeof(ContentReferenceModelListBinder))]
public class ContentReferenceModelList : List<ContentReferenceModel>
{
}
public class ContentReferenceModel
{
public ContentReference Reference { get; set; }
public string Tag { get; set; }
}
Using a custom binder to get the content references and tag information from the query string. This could be simplified if you only need a single item, however this approach allows the loading of multiple references with different rendering tags. Note the splitting of items using an underscore and the splitting of content references and tags using a hyphen.
public class ContentReferenceModelListBinder: DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
var request = controllerContext.HttpContext.Request;
var content = request.QueryString.Get("content");
if (string.IsNullOrWhiteSpace(content))
return null;
var list = new ContentReferenceModelList();
var contentItems = content.Split('_');
foreach (var contentItem in contentItems)
{
var parts = contentItem.Split('-');
list.Add(new ContentReferenceModel
{
Reference = new ContentReference(parts[0]),
Tag = parts.Length > 1 ? parts[1] : ""
});
}
return list;
}
}
And had a view (BlockPartialHtml) that did the following:
@using MyProject.Shared.Helpers
@model IEnumerable<MyProject.Shared.Models.ContentReferenceModel>
@foreach (var contentReferenceModel in Model)
{
Html.RenderContentReference(contentReferenceModel.Reference, contentReferenceModel.Tag);
}
The RenderContentReference is an html helper that makes use of the IContentDataExtensions.RenderContentData method:
/// <summary>
/// Renders the content reference via the EPiServer template resolution mechanism.
/// </summary>
/// <param name="html">The HTML.</param>
/// <param name="contentReference">The content reference.</param>
/// <param name="tag">The rendering tag.</param>
public static void RenderContentReference(this HtmlHelper html, ContentReference contentReference, string tag = null)
{
if (contentReference == null) return;
IContentData contentData = ServiceLocator.Current.GetInstance<IContentRepository>().Get<IContent>(contentReference);
html.ViewContext.ViewData["tag"] = tag;
IContentDataExtensions.RenderContentData(html, contentData, false, tag ?? html.ViewContext.ViewData["tag"] as string);
}
This allowed me to be able to make a request to:
siteurl.com/BlockPartialHtml?content=123 // to get the content with ID 123
siteurl.com/BlockPartialHtml?content=123-tagname // to get the content with ID 123 and render with the tag "tagname"
siteurl.com/BlockPartialHtml?content=123-tagname_234-othertag // to get the content with ID 123 and render with the tag "tagname" and ID 234 with the tag "othertag"
Hope this helps, give me a shout if you have any quesitons.
Cheers
Tom
I think the official answer would be to use partial routing as described here:
https://world.episerver.com/documentation/developer-guides/CMS/routing/partial-routing/
That said, I like the flexibility of Tom's approach so would be tempted to go with that option.
Thanks for your answers so far. I will test IPartialRouter to start with. What I am out for is to be able to get the url by calling UrlResolver.Current.GetUrl(ContentReference contentLink). I will be back.
/Hans
Hi again
Thank you for your answers. This is what I came up with.
First I would like to say: I think EPiServer is great. I have always thought that. I think you can do almost anything with EPiServer. They have really made it pluggable.
This will be a rather long one but I would like to share. The code for each example is copied from one code-file separated with namespaces.
All solutions require a block-controller decorated with:
[TemplateDescriptor(Inherited = true, TemplateTypeCategory = TemplateTypeCategories.MvcController)]
If you dont have it, it want work.
Before I asked this question here I had already labbed/tested/searched for a while. I had already started trying a custom segment.
Advantages: you can use logic in NodeSegment and only override certain parts
Disadvantages: the route-setup
using System;
using System.Collections.Generic;
using System.Web.Mvc;
using System.Web.Routing;
using EPiServer;
using EPiServer.Configuration;
using EPiServer.Core;
using EPiServer.Framework;
using EPiServer.Framework.DataAnnotations;
using EPiServer.Framework.Initialization;
using EPiServer.Framework.Web;
using EPiServer.ServiceLocation;
using EPiServer.Web;
using EPiServer.Web.Mvc;
using EPiServer.Web.Routing;
using EPiServer.Web.Routing.Segments;
using EPiServer.Web.Routing.Segments.Internal;
using EPiServerSite.Business.Web.Routing;
using EPiServerSite.Business.Web.Routing.Segments;
namespace EPiServerSite.Business.Initialization
{
/// <summary>
/// This could also be handled in Global.asax:
/// protected override void RegisterRoutes(RouteCollection routes)
/// {
/// base.RegisterRoutes(routes);
/// // Do additional stuff with routes below...
/// }
/// </summary>
[InitializableModule]
public class RouteInitialization : IInitializableModule
{
#region Methods
public virtual void Initialize(InitializationEngine context)
{
if(context == null)
throw new ArgumentNullException(nameof(context));
EPiServer.Global.RoutesRegistered += this.OnRoutesRegistered;
}
protected internal virtual void OnRoutesRegistered(object sender, RouteRegistrationEventArgs e)
{
if(e == null)
throw new ArgumentNullException(nameof(e));
e.Routes.MapBlockRoutes(new {action = "Index"}, "Blocks", "BlockStaticPlaceHolder", "{language}/BlockStaticPlaceHolder/{node}/{partial}/{action}");
}
public virtual void Uninitialize(InitializationEngine context)
{
if(context == null)
throw new ArgumentNullException(nameof(context));
EPiServer.Global.RoutesRegistered -= this.OnRoutesRegistered;
}
#endregion
}
}
namespace EPiServerSite.Business.Web.Routing
{
public static class RouteCollectionExtension
{
#region Methods
private static IDictionary<string, ISegment> CreateBlockSegmentMappings(IUrlSegmentRouter urlSegmentRouter)
{
var serviceLocator = ServiceLocator.Current;
return new Dictionary<string, ISegment> {{RoutingConstants.NodeKey, new BlockSegment(serviceLocator.GetInstance<IContentLanguageSettingsHandler>(), serviceLocator.GetInstance<IContentLoader>(), RoutingConstants.NodeKey, serviceLocator.GetInstance<UrlResolver>(), Settings.Instance.UrlRewriteExtension, urlSegmentRouter)}};
}
public static void MapBlockRoute(this RouteCollection routes, Func<SiteDefinition, ContentReference> contentRootResolver, object defaults, string name, string url)
{
var serviceLocator = ServiceLocator.Current;
var basePathResolver = serviceLocator.GetInstance<IBasePathResolver>();
var urlSegmentRouter = serviceLocator.GetInstance<IUrlSegmentRouter>();
urlSegmentRouter.RootResolver = contentRootResolver;
var parameters = new MapContentRouteParameters
{
BasePathResolver = basePathResolver.Resolve,
Direction = SupportedDirection.Both,
SegmentMappings = CreateBlockSegmentMappings(urlSegmentRouter),
UrlSegmentRouter = urlSegmentRouter
};
routes.MapContentRoute(name, url, defaults, parameters);
}
public static void MapBlockRoute(this RouteCollection routes, Func<SiteDefinition, ContentReference> contentRootResolver, object defaults, string name, string staticSegmentPlaceHolder, string staticSegmentReplacement, string url)
{
if(!string.IsNullOrEmpty(staticSegmentPlaceHolder) && !string.IsNullOrEmpty(url))
url = url.Replace(staticSegmentPlaceHolder, staticSegmentReplacement);
routes.MapBlockRoute(contentRootResolver, defaults, name, url);
}
public static void MapBlockRoutes(this RouteCollection routes, object defaults, string name, string staticSegmentPlaceHolder, string url)
{
routes.MapBlockRoute(siteDefinition => siteDefinition.ContentAssetsRoot, defaults, name + " (content)", staticSegmentPlaceHolder, "content-blocks", url);
routes.MapBlockRoute(siteDefinition => siteDefinition.GlobalAssetsRoot, defaults, name + " (global)", staticSegmentPlaceHolder, "global-blocks", url);
routes.MapBlockRoute(siteDefinition => siteDefinition.SiteAssetsRoot, defaults, name + " (site)", staticSegmentPlaceHolder, "site-blocks", url);
}
#endregion
}
}
namespace EPiServerSite.Business.Web.Routing.Segments
{
public class BlockSegment : NodeSegment
{
#region Constructors
public BlockSegment(IContentLanguageSettingsHandler contentLanguageSettingsHandler, IContentLoader contentLoader, string name, UrlResolver urlResolver, string urlRewriteExtension, IUrlSegmentRouter urlSegmentRouter) : base(name, urlRewriteExtension, urlSegmentRouter, contentLoader, urlResolver, contentLanguageSettingsHandler) { }
#endregion
#region Methods
protected override ContentReference GetContentLink(RequestContext requestContext, RouteValueDictionary values)
{
var contentLink = base.GetContentLink(requestContext, values);
if(!ContentReference.IsNullOrEmpty(contentLink) && !this.ContentLoader.TryGet(contentLink, out BlockData _))
return null;
return contentLink;
}
protected override IEnumerable<SegmentNode> GetIncomingNode(ContentReference contentLink, SegmentContext context)
{
var nodes = new List<SegmentNode>(base.GetIncomingNode(contentLink, context));
var segment = this.GetNextValue(context.RemainingPath, context);
// ReSharper disable All
if(Guid.TryParse(segment.Next, out var guid) && this.ContentLoader.TryGet(guid, out IContent content) && content is BlockData)
{
nodes.Add(new SegmentNode
{
ContentLink = content.ContentLink,
Segment = segment.Next
});
context.RemainingPath = segment.Remaining;
}
// ReSharper restore All
return nodes;
}
protected override string GetOutgoingUrlSegment(ContentReference contentLink, string language)
{
// ReSharper disable All
if(this.ContentLoader.TryGet(contentLink, out IContent content))
{
if(content is BlockData)
return content.ContentGuid.ToString("N");
}
// ReSharper restore All
return base.GetOutgoingUrlSegment(contentLink, language);
}
#endregion
}
}
namespace EPiServerSite.Controllers
{
public abstract class SiteBlockController<T> : ActionControllerBase, IRenderTemplate<T> where T : BlockData
{
#region Constructors
protected SiteBlockController(IContentRouteHelper contentRouteHelper)
{
this.ContentRouteHelper = contentRouteHelper ?? throw new ArgumentNullException(nameof(contentRouteHelper));
}
#endregion
#region Properties
protected internal virtual IContentRouteHelper ContentRouteHelper { get; }
#endregion
}
[TemplateDescriptor(Inherited = true, TemplateTypeCategory = TemplateTypeCategories.MvcController)]
public class DefaultBlockController : SiteBlockController<BlockData>
{
#region Constructors
public DefaultBlockController(IContentRouteHelper contentRouteHelper) : base(contentRouteHelper) { }
#endregion
#region Methods
public virtual ActionResult Index()
{
var content = new List<string>
{
"<h1>Yeah!</h1>",
"<ul>",
$"<li>Block-type: <strong>{this.ContentRouteHelper.Content.GetOriginalType()}</strong></li>",
$"<li>Content-link: <strong>{this.ContentRouteHelper.ContentLink}</strong></li>",
$"<li>Name: <strong>{this.ContentRouteHelper.Content.Name}</strong></li>",
};
if(this.ContentRouteHelper.Content is ILocale locale)
content.Add($"<li>Culture: <strong>{locale.Language}</strong></li>");
if(this.ContentRouteHelper.Content is ILocalizable localizable)
content.Add($"<li>Master-culture: <strong>{localizable.MasterLanguage}</strong></li>");
content.Add("</ul>");
return this.Content(string.Join(Environment.NewLine, content));
}
#endregion
}
}
Then after the tips here I tried a partial router.
Advantages: the way you should do it
Disadvantages: I find the routing-logic rather complex because I havent done it before. Have you missed something? Localization?
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Web.Mvc;
using System.Web.Routing;
using EPiServer;
using EPiServer.Core;
using EPiServer.Framework;
using EPiServer.Framework.DataAnnotations;
using EPiServer.Framework.Initialization;
using EPiServer.Framework.Web;
using EPiServer.ServiceLocation;
using EPiServer.Web;
using EPiServer.Web.Mvc;
using EPiServer.Web.Routing;
using EPiServer.Web.Routing.Segments;
using EPiServer.Web.Routing.Segments.Internal;
using EPiServerSite.Business.Web.Routing;
using EPiServerSite.Models.Pages;
namespace EPiServerSite.Business.Initialization
{
/// <summary>
/// This could also be handled in Global.asax:
/// protected override void RegisterRoutes(RouteCollection routes)
/// {
/// base.RegisterRoutes(routes);
/// // Do additional stuff with routes below...
/// }
/// </summary>
[InitializableModule]
public class RouteInitialization : IInitializableModule
{
#region Methods
public virtual void Initialize(InitializationEngine context)
{
if(context == null)
throw new ArgumentNullException(nameof(context));
EPiServer.Global.RoutesRegistered += this.OnRoutesRegistered;
}
protected internal virtual void OnRoutesRegistered(object sender, RouteRegistrationEventArgs e)
{
if(e == null)
throw new ArgumentNullException(nameof(e));
e.Routes.RegisterPartialRouter(ServiceLocator.Current.GetInstance<BlockPartialRouter>());
}
public virtual void Uninitialize(InitializationEngine context)
{
if(context == null)
throw new ArgumentNullException(nameof(context));
EPiServer.Global.RoutesRegistered -= this.OnRoutesRegistered;
}
#endregion
}
}
namespace EPiServerSite.Business.Web.Routing
{
[ServiceConfiguration(typeof(BlockPartialRouter), Lifecycle = ServiceInstanceScope.Transient)]
public class BlockPartialRouter : IPartialRouter<StartPage, BlockData>
{
#region Constructors
public BlockPartialRouter(IContentLanguageSettingsHandler contentLanguageSettingsHandler, IContentLoader contentLoader, ISiteDefinitionResolver siteDefinitionResolver, IUrlSegmentRouter urlSegmentRouter)
{
this.ContentLanguageSettingsHandler = contentLanguageSettingsHandler ?? throw new ArgumentNullException(nameof(contentLanguageSettingsHandler));
this.ContentLoader = contentLoader ?? throw new ArgumentNullException(nameof(contentLoader));
this.SiteDefinitionResolver = siteDefinitionResolver ?? throw new ArgumentNullException(nameof(siteDefinitionResolver));
this.UrlSegmentRouter = urlSegmentRouter ?? throw new ArgumentNullException(nameof(urlSegmentRouter));
}
#endregion
#region Properties
protected internal virtual IContentLanguageSettingsHandler ContentLanguageSettingsHandler { get; }
protected internal virtual IContentLoader ContentLoader { get; }
protected internal virtual ISiteDefinitionResolver SiteDefinitionResolver { get; }
protected internal virtual IUrlSegmentRouter UrlSegmentRouter { get; }
#endregion
#region Methods
protected internal virtual IList<SegmentNode> GetAncestorSegments(ContentReference contentLink, SegmentContext segmentContext)
{
var segmentNodes = new List<SegmentNode>();
// ReSharper disable All
while(!ContentReference.IsNullOrEmpty(contentLink) && !string.IsNullOrEmpty(segmentContext.RemainingPath))
{
var segment = segmentContext.GetNextValue(segmentContext.RemainingPath);
contentLink = this.UrlSegmentRouter.ResolveContentForIncoming(contentLink, segment.Next, segmentContext);
if(!ContentReference.IsNullOrEmpty(contentLink))
{
segmentNodes.Add(new SegmentNode
{
ContentLink = contentLink,
Segment = segment.Next
});
segmentContext.RemainingPath = segment.Remaining;
}
}
// ReSharper restore All
return segmentNodes;
}
public virtual PartialRouteData GetPartialVirtualPath(BlockData content, string language, RouteValueDictionary routeValues, RequestContext requestContext)
{
// ReSharper disable All
if(content is IContent actualContent)
{
var ancestors = this.ContentLoader.GetAncestors(actualContent.ContentLink).ToArray();
const string guidFormat = "N";
var segments = new List<string>();
var siteDefinition = this.SiteDefinitionResolver.GetByContent(actualContent.ContentLink, true);
var startSegment = this.GetStartSegment(ancestors, siteDefinition, out var remainingAncestors);
if(!string.IsNullOrEmpty(startSegment))
segments.Add(startSegment);
foreach(var ancestor in remainingAncestors.Reverse())
{
if(ancestor is IRoutable routable)
segments.Add(routable.RouteSegment);
else
segments.Add(ancestor.ContentGuid.ToString(guidFormat));
}
segments.Add(actualContent.ContentGuid.ToString(guidFormat));
segments.Add(string.Empty);
return new PartialRouteData
{
BasePathRoot = siteDefinition.StartPage,
PartialVirtualPath = string.Join("/", segments)
};
}
// ReSharper restore All
return null;
}
protected internal virtual string GetStartSegment(IEnumerable<IContent> ancestors, SiteDefinition siteDefinition, out IList<IContent> remainingAncestors)
{
remainingAncestors = new List<IContent>();
var startSegmentMap = this.GetStartSegmentMap(siteDefinition);
foreach(var ancestor in ancestors ?? Enumerable.Empty<IContent>())
{
if(startSegmentMap.ContainsKey(ancestor.ContentLink))
return startSegmentMap[ancestor.ContentLink];
remainingAncestors.Add(ancestor);
}
return null;
}
protected internal virtual IDictionary<ContentReference, string> GetStartSegmentMap(SiteDefinition siteDefinition)
{
var startSegmentMap = new Dictionary<ContentReference, string>(ContentReferenceComparer.IgnoreVersion);
// ReSharper disable InvertIf
if(siteDefinition != null)
{
if(!ContentReference.IsNullOrEmpty(siteDefinition.ContentAssetsRoot))
startSegmentMap.Add(siteDefinition.ContentAssetsRoot, "content-blocks");
if(!ContentReference.IsNullOrEmpty(siteDefinition.GlobalAssetsRoot))
startSegmentMap.Add(siteDefinition.GlobalAssetsRoot, "global-blocks");
if(!ContentReference.IsNullOrEmpty(siteDefinition.SiteAssetsRoot) && !siteDefinition.SiteAssetsRoot.CompareToIgnoreWorkID(siteDefinition.GlobalAssetsRoot))
startSegmentMap.Add(siteDefinition.SiteAssetsRoot, "site-blocks");
}
// ReSharper restore InvertIf
return startSegmentMap;
}
protected internal virtual bool IsFallbackOrReplacementCulture(IContent content, string culture)
{
var languageSelectionSource = this.ContentLanguageSettingsHandler.MatchLanguageSettings(content, culture);
// ReSharper disable SwitchStatementMissingSomeCases
switch(languageSelectionSource)
{
case LanguageSelectionSource.Fallback:
case LanguageSelectionSource.Replacement:
return true;
default:
return languageSelectionSource == LanguageSelectionSource.ReplacementFallback;
}
// ReSharper restore SwitchStatementMissingSomeCases
}
protected internal virtual bool IsValidCulture(IContent content, SegmentContext context, string culture)
{
// ReSharper disable InvertIf
if(context.StrictLanguageRoutingResolver() && !string.IsNullOrEmpty(culture))
{
if(content is IRoutable routable)
{
if(!routable.RouteSegment.Equals(context.LastConsumedFragment, StringComparison.OrdinalIgnoreCase) && !this.IsFallbackOrReplacementCulture(content, culture) && !this.UrlSegmentRouter.RootResolver(context.RoutedSiteDefinition).CompareToIgnoreWorkID(content.ContentLink))
return false;
}
if(content is ILocalizable localizable)
{
if(!this.IsFallbackOrReplacementCulture(content, culture) && !localizable.Language.Name.Equals(culture, StringComparison.OrdinalIgnoreCase))
return false;
}
}
// ReSharper restore InvertIf
return true;
}
public virtual object RoutePartial(StartPage content, SegmentContext segmentContext)
{
// ReSharper disable All
if(segmentContext.ContextMode == ContextMode.Default)
{
var segment = segmentContext.GetNextValue(segmentContext.RemainingPath);
var blockRoot = this.GetStartSegmentMap(segmentContext.RoutedSiteDefinition).FirstOrDefault(mapping => string.Equals(mapping.Value, segment.Next, StringComparison.OrdinalIgnoreCase)).Key;
if(!ContentReference.IsNullOrEmpty(blockRoot))
{
this.UrlSegmentRouter.RootResolver = siteDefinition => blockRoot;
segmentContext.RemainingPath = segment.Remaining;
var culture = segmentContext.Language;
this.GetAncestorSegments(blockRoot, segmentContext);
if(string.IsNullOrEmpty(culture))
culture = segmentContext.ContentLanguage;
segment = segmentContext.GetNextValue(segmentContext.RemainingPath);
if(Guid.TryParse(segment.Next, out var guid))
{
var loaderOptions = new LoaderOptions
{
LanguageLoaderOption.FallbackWithMaster(string.IsNullOrEmpty(culture) ? null : CultureInfo.GetCultureInfo(culture))
};
if(this.ContentLoader.TryGet(guid, loaderOptions, out IContent blockContent) && blockContent is BlockData)
{
if(this.IsValidCulture(blockContent, segmentContext, culture))
{
segmentContext.Language = culture;
segmentContext.RemainingPath = segment.Remaining;
segmentContext.RoutedContentLink = blockContent.ContentLink;
segmentContext.RouteData.DataTokens[RoutingConstants.DefaultLanguageKey] = segmentContext.Defaults[RoutingConstants.LanguageKey];
return blockContent;
}
}
}
}
}
// ReSharper restore All
return null;
}
#endregion
}
}
namespace EPiServerSite.Controllers
{
public abstract class SiteBlockController<T> : ActionControllerBase, IRenderTemplate<T> where T : BlockData
{
#region Constructors
protected SiteBlockController(IContentRouteHelper contentRouteHelper)
{
this.ContentRouteHelper = contentRouteHelper ?? throw new ArgumentNullException(nameof(contentRouteHelper));
}
#endregion
#region Properties
protected internal virtual IContentRouteHelper ContentRouteHelper { get; }
#endregion
}
[TemplateDescriptor(Inherited = true, TemplateTypeCategory = TemplateTypeCategories.MvcController)]
public class DefaultBlockController : SiteBlockController<BlockData>
{
#region Constructors
public DefaultBlockController(IContentRouteHelper contentRouteHelper) : base(contentRouteHelper) { }
#endregion
#region Methods
public virtual ActionResult Index()
{
var content = new List<string>
{
"<h1>Yeah!</h1>",
"<ul>",
$"<li>Block-type: <strong>{this.ContentRouteHelper.Content.GetOriginalType()}</strong></li>",
$"<li>Content-link: <strong>{this.ContentRouteHelper.ContentLink}</strong></li>",
$"<li>Name: <strong>{this.ContentRouteHelper.Content.Name}</strong></li>",
};
if(this.ContentRouteHelper.Content is ILocale locale)
content.Add($"<li>Culture: <strong>{locale.Language}</strong></li>");
if(this.ContentRouteHelper.Content is ILocalizable localizable)
content.Add($"<li>Master-culture: <strong>{localizable.MasterLanguage}</strong></li>");
content.Add("</ul>");
return this.Content(string.Join(Environment.NewLine, content));
}
#endregion
}
}
Then I wanted to try routable blocks.
If you use localization and eg. you have an english block and a swedish block with the same route-segment it will only find the master-language-one. Thats why I came up with the solution to replace the assets-route with ones including the language-segment.
Advantages: simple, EPiServer handles it for you
Disadvantages: the rotue-setup, if you need it
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web.Mvc;
using System.Web.Routing;
using EPiServer;
using EPiServer.Core;
using EPiServer.DataAbstraction;
using EPiServer.DataAnnotations;
using EPiServer.Framework;
using EPiServer.Framework.DataAnnotations;
using EPiServer.Framework.Initialization;
using EPiServer.Framework.Web;
using EPiServer.Web;
using EPiServer.Web.Mvc;
using EPiServer.Web.Routing;
using EPiServerSite.Models.Blocks;
namespace EPiServerSite.Business.Initialization
{
/// <summary>
/// This could also be handled in Global.asax:
/// protected override void RegisterRoutes(RouteCollection routes)
/// {
/// base.RegisterRoutes(routes);
/// // Do additional stuff with routes below...
/// }
/// </summary>
[InitializableModule]
public class RouteInitialization : IInitializableModule
{
#region Methods
public virtual void Initialize(InitializationEngine context)
{
if(context == null)
throw new ArgumentNullException(nameof(context));
EPiServer.Global.RoutesRegistered += this.OnRoutesRegistered;
}
protected internal virtual void OnRoutesRegistered(object sender, RouteRegistrationEventArgs e)
{
if(e == null)
throw new ArgumentNullException(nameof(e));
const string contentAssetsName = "contentAssets";
const string mediaName = "Media";
var temporaryRoutes = new RouteCollection();
temporaryRoutes.MapContentAssetsRoute(contentAssetsName, "{language}/content-assets/{node}/{partial}/{action}", new
{
action = "Index"
});
temporaryRoutes.MapAssetRoutes(mediaName, "{language}/MediaStaticPlaceHolder/{node}/{partial}/{action}", new
{
action = "Index"
}, "MediaStaticPlaceHolder", "global-assets", "site-assets");
for(var i = 0; i < e.Routes.Count; i++)
{
if(!(e.Routes[i] is IContentRoute contentRoute))
continue;
foreach(var route in temporaryRoutes.OfType<IContentRoute>())
{
if(!string.Equals(contentRoute.Name, route.Name, StringComparison.Ordinal))
continue;
e.Routes[i] = (RouteBase) route;
break;
}
}
}
public virtual void Uninitialize(InitializationEngine context)
{
if(context == null)
throw new ArgumentNullException(nameof(context));
EPiServer.Global.RoutesRegistered -= this.OnRoutesRegistered;
}
#endregion
}
}
namespace EPiServerSite.Controllers
{
public abstract class SiteBlockController<T> : ActionControllerBase, IRenderTemplate<T> where T : BlockData
{
#region Constructors
protected SiteBlockController(IContentRouteHelper contentRouteHelper)
{
this.ContentRouteHelper = contentRouteHelper ?? throw new ArgumentNullException(nameof(contentRouteHelper));
}
#endregion
#region Properties
protected internal virtual IContentRouteHelper ContentRouteHelper { get; }
#endregion
}
[TemplateDescriptor(Inherited = true, TemplateTypeCategory = TemplateTypeCategories.MvcController)]
public class RoutableBlockController : SiteBlockController<RoutableSiteBlockData>
{
#region Constructors
public RoutableBlockController(IContentRouteHelper contentRouteHelper) : base(contentRouteHelper) { }
#endregion
#region Methods
public virtual ActionResult Index()
{
var content = new List<string>
{
"<h1>Yeah!</h1>",
"<ul>",
$"<li>Block-type: <strong>{this.ContentRouteHelper.Content.GetOriginalType()}</strong></li>",
$"<li>Content-link: <strong>{this.ContentRouteHelper.ContentLink}</strong></li>",
$"<li>Name: <strong>{this.ContentRouteHelper.Content.Name}</strong></li>",
};
if(this.ContentRouteHelper.Content is ILocale locale)
content.Add($"<li>Culture: <strong>{locale.Language}</strong></li>");
if(this.ContentRouteHelper.Content is ILocalizable localizable)
content.Add($"<li>Master-culture: <strong>{localizable.MasterLanguage}</strong></li>");
content.Add("</ul>");
return this.Content(string.Join(Environment.NewLine, content));
}
#endregion
}
}
namespace EPiServerSite.Models.Blocks
{
[ContentType(GUID = "2647d71a-93ea-43a1-85da-eda2b528a7b7")]
public class RoutableBlock : RoutableSiteBlockData
{
#region Properties
[CultureSpecific]
[Display(GroupName = SystemTabNames.Content)]
public virtual string Heading { get; set; }
#endregion
}
/// <summary>
/// Look at <see cref="P:EPiServer.Core.MediaData" /> to see how it's done in that class.
/// </summary>
public abstract class RoutableSiteBlockData : SiteBlockData, IRoutable
{
#region Fields
private bool _isModified;
private string _routeSegment;
#endregion
#region Properties
protected override bool IsModified => base.IsModified || this._isModified;
[UIHint("previewabletext")]
public string RouteSegment
{
get => this._routeSegment;
set
{
this.ThrowIfReadOnly();
this._isModified = true;
this._routeSegment = value;
}
}
#endregion
#region Methods
protected override void ResetModified()
{
base.ResetModified();
this._isModified = false;
}
#endregion
}
public abstract class SiteBlockData : BlockData { }
}
Hope this can help someone.
Regards Hans
EPiServer 11.12.0
What do I need to set up to be able to browse to a block?
http://localhost/{path-to-block}/
http://localhost/en/{path-to-block}/
http://localhost/{path-to-block}/PreferredAction
http://localhost/en/{path-to-block}/PreferredAction
Somehow like you can browse to a file under the content-assets hierarchy. Because the blocks are, like files, saved under that hierarchy.
I can hit the action by the url like this:
http://localhost/MyBlock/Index
But if a try http://localhost/en/MyBlock/Index I get 404
Do I need to set up an extra ContentRoute?
Do I need to add a site-specific block-conroller that implements IRoutable?
I want this because I want to refresh parts of a block with ajax on a page. And instead of setting up an ApiController and returning json and then fix the html I would like to have actions on my BlockController that returns partial-views with just the html I want to replace on the client-side.
If I use UrlResolver.Current.GetUrl() I get an url for a file-content-link but I get null for a block-content-link:
File: UrlResolver.Current.GetUrl(new ContentReference(61)) => /globalassets/alloy-meet/alloymeet.png
Block: UrlResolver.Current.GetUrl(new ContentReference(47)) => null
Hope someone can help me.
/Hans