November Happy Hour will be moved to Thursday December 5th.

Browse to a block.

Vote:
 

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

#209308
Nov 13, 2019 13:43
Vote:
 
#209321
Edited, Nov 13, 2019 14:33
Vote:
 

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

#209323
Edited, Nov 13, 2019 14:48
Vote:
 

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.

#209471
Nov 14, 2019 16:26
Vote:
 

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

#209495
Nov 15, 2019 10:45
Vote:
 

Hi Hans,

Yes, the partial router is the solution to this problem.

#209527
Nov 16, 2019 13:41
Vote:
 

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.

Important

All solutions require a block-controller decorated with:

[TemplateDescriptor(Inherited = true, TemplateTypeCategory = TemplateTypeCategories.MvcController)]

If you dont have it, it want work.

3 different solutions

Before I asked this question here I had already labbed/tested/searched for a while. I had already started trying a custom segment.

EPiServer.Web.Routing.Segments.ISegment

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.

EPiServer.Web.Routing.IPartialRouter<TContent, TRoutedData>

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.

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

#209539
Edited, Nov 17, 2019 15:22
This topic was created over six months ago and has been resolved. If you have a similar question, please create a new topic and refer to this one.
* 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.