The idea is that when editors are creating a (or updating an existing) page or a block, we can choose to show a link to instructions page. See the image above for a preview of what it looks like.

I will use Alloy site for this. Of course, depending on your setup, the implementation might be somewhat different.

First, we will create a property definition and a simple model. This will allow us to use PropertyList with custom type (PagesBlocksInstructionsModel), to store all existing page and block types (these will be stored as a list of strings). Generic PropertyLists are unsupported by Episerver, but I've been using them and haven't had any issues.

[PropertyDefinitionTypePlugIn] public class PagesBlocksInstructionsPropertyDefinition : PropertyList<PagesBlocksInstructionsModel> { } public class PagesBlocksInstructionsModel { [Display(Name = "Page or block")] public string PageOrBlock { get; set; } [Display(Name = "Link to instructions")] public string InstructionsLink { get; set; } }

Next, we will create Instructions tab and use it on Start Page (in real-world scenario, you might want to use a dedicated settings page, or similar). This tab will only be available to Administrators (in this case, we're using the GroupNames static class in Global.cs).

[RequiredAccess(EPiServer.Security.AccessLevel.Administer)] [Display(Name = "Instructions", Order = 8)] public const string Instructions = "Instructions";

In the Start Page model, we will add three new properties - one for the display text (i.e. Click here for instructions on how to use this page or block), and two for storing all pages and blocks, and their instructions. For the latter two, we are using the property list we created.

[Display(GroupName = Global.GroupNames.Instructions)] public virtual string InstructionsLinkCaption { get; set; } [Display(GroupName = Global.GroupNames.Instructions)] [EditorDescriptor(EditorDescriptorType = typeof(CollectionEditorDescriptor<PagesBlocksInstructionsModel>))] public virtual IList<PagesBlocksInstructionsModel> PagesInstructionsLinks { get; set; } [Display(GroupName = Global.GroupNames.Instructions)] [EditorDescriptor(EditorDescriptorType = typeof(CollectionEditorDescriptor<PagesBlocksInstructionsModel>))] public virtual IList<PagesBlocksInstructionsModel> BlocksInstructionsLinks { get; set; }

We will populate PagesInstructionsLinks and BlocksInstructionsLink with an initialization module, that will fetch all page and block types.

The Initialize method will get all the page and block types, (using ContentTypeRepository) and fill the properties in Start Page.

var _contentRepository = ServiceLocator.Current.GetInstance<IContentRepository>(); var _contentTypeRepository = ServiceLocator.Current.GetInstance<IContentTypeRepository>(); var startPage = _contentRepository.Get<StartPage>(SiteDefinition.Current.StartPage); var writableClone = startPage.CreateWritableClone() as StartPage; if (writableClone != null) { var instructionsPages = writableClone.PagesInstructionsLinks != null ? writableClone.PagesInstructionsLinks : new List<PagesBlocksInstructionsModel>(); var instructionsBlocks = writableClone.BlocksInstructionsLinks != null ? writableClone.BlocksInstructionsLinks : new List<PagesBlocksInstructionsModel>(); var allPages = _contentTypeRepository.List().Where(contentType => typeof(SitePageData).IsAssignableFrom(contentType.ModelType)); var allBlocks = _contentTypeRepository.List().Where(contentType => typeof(SiteBlockData).IsAssignableFrom(contentType.ModelType)); writableClone.PagesInstructionsLinks = PopulatePagesAndBlocksProperty(allPages, instructionsPages); writableClone.BlocksInstructionsLinks = PopulatePagesAndBlocksProperty(allBlocks, instructionsBlocks); _contentRepository.Save(writableClone, SaveAction.Publish, AccessLevel.NoAccess); }

The PopulatePagesAndBlocksProperty method looks like this:

private IList<PagesBlocksInstructionsModel> PopulatePagesAndBlocksProperty(IEnumerable<ContentType> allPagesOrBlocks, IList<PagesBlocksInstructionsModel> instructionsData) { foreach (var content in allPagesOrBlocks) { var item = new PagesBlocksInstructionsModel() { PageOrBlock = content.Name, InstructionsLink = string.Empty }; if (!instructionsData.Any(instruction => instruction.PageOrBlock == item.PageOrBlock)) { instructionsData.Add(item); } } return instructionsData; }

After starting the site, page and block types appear on the Instructions tab

Now we can add InstructionsLink property to pages and blocks. A good place for it is PageHeader. This property needs to be in abstract classes SitePageData.cs and SiteBlockData.cs, which all pages and blocks inherit from.

[Display( GroupName = SystemTabNames.PageHeader, Order = 20)] [UIHint("InstructionsLink")] public virtual string InstructionsLink { get; set; }

Next, we need to create a service that will fetch the link for the page or block, and an Editor Descriptor that will utilize the service. Also, we need a model that the service will return:

public class PagesBlocksInstructionsReturnModel { public string Caption { get; set; } public string Link { get; set; } }

The first method in our service will return the PagesBlocksInstructionsReturnModel:

public PagesBlocksInstructionsReturnModel GetInstructions(string pageOrBlock) { var startPage = _contentLoader.Get<StartPage>(SiteDefinition.Current.StartPage); var instructionsCaption = startPage.InstructionsLinkCaption; var pagesInstructions = startPage.PagesInstructionsLinks; var blocksInstructions = startPage.BlocksInstructionsLinks; return new PagesBlocksInstructionsReturnModel() { Caption = !string.IsNullOrEmpty(instructionsCaption) ? instructionsCaption : string.Empty, Link = FindLink(pageOrBlock, pagesInstructions, blocksInstructions) }; }

FindLink will simply match the InstructionsLink with the page or block:

private string FindLink(string blockOrPageName, IList<PagesBlocksInstructionsModel> pageInstructions, IList<PagesBlocksInstructionsModel> blockInstructions) { string instructionsLink = string.Empty; foreach (var page in pageInstructions) { if (blockOrPageName == page.PageOrBlock) { instructionsLink = page.InstructionsLink; break; } else { foreach (var block in blockInstructions) { if (blockOrPageName == block.PageOrBlock) { instructionsLink = block.InstructionsLink; break; } } } } return instructionsLink; }

Almost there. Now that we have the service in place, we need the Editor Descriptor for the InstructionsLink property of the base classes:

public class PagesBlocksInstructionsEditorDescriptor { [EditorDescriptorRegistration( TargetType = typeof(string), UIHint = "InstructionsLink")] public class PagesBlocksInstructions : EditorDescriptor { private readonly IPagesBlocksInstructionsService _instructionsService; public PagesBlocksInstructions(IPagesBlocksInstructionsService instructionsService) { ClientEditingClass = "alloy/Editors/PagesBlocksInstructions"; _instructionsService = instructionsService; } public override void ModifyMetadata(ExtendedMetadata metadata, IEnumerable<Attribute> attributes) { base.ModifyMetadata(metadata, attributes); dynamic metadataRuntime = metadata; var ownerContent = metadataRuntime.OwnerContent as IContent; var pageOrBlock = ownerContent != null ? ownerContent.GetType().BaseType : null; if (pageOrBlock != null) { var linkUrl = _instructionsService.GetInstructions(pageOrBlock.Name).Link; var linkCaption = _instructionsService.GetInstructions(pageOrBlock.Name).Caption; var instructionsExist = string.IsNullOrEmpty(linkUrl) || string.IsNullOrEmpty(linkCaption) ? false : true; metadata.EditorConfiguration.Add("linkUrl", linkUrl); metadata.EditorConfiguration.Add("linkCaption", linkCaption); metadata.EditorConfiguration.Add("instructionsExist", instructionsExist); metadata.DisplayName = string.Empty; } } } }

Here we are basically using the service we created to fetch the instructions link, and then sending that to a dojo module that will finally render it. We are checking if instructions exist, and if not, we will leave it empty.

define( [ "dojo/_base/declare", "dijit/_Widget", "dijit/_TemplatedMixin" ], function ( declare, _Widget, _TemplatedMixin, ) { return declare([_Widget, _TemplatedMixin], { postCreate: function () { console.log(this) if (this.instructionsExist) { this.instructionslink.href = this.linkUrl; this.instructionslink.innerHTML = `<strong>${this.linkCaption}</strong>`; } }, templateString: `<div> \ <a href="" data-dojo-attach-point="instructionslink" target="_blank"></div>`, }) } );

To add a link to a page or block type, an admin will first add the main caption on the Instructions tab on Start Page:

And then it's a matter of editing the desired page or block type item in the property list, and adding the URL:

The editor will see it like this:

Complete code available on GitHub.