05 Jun 2017
This example comes from a project I've been working on in which my client requested a starter kit and individual featured functionality modules to be developed for their client. This was done to bring all their client's online material under one CMS and allow the different branches and satellites to maintain the brand guidelines while still allowing for unique styling and functionality to be applied.
We've delivered the kit and 9 different modules which have been used on 3 different sites with another site currently in the design phase!
Now, I would like to take this moment quickly to say thank you to all the package devs out there, like @UmCo [i.e. Matt Brailsford (@mattbrailsford) & Lee Kelleher (@leekeleher)], Kevin Giszewski (@kevingizewski), and so many others to mention for all their somewhat thankless hard work on packages for us to use and learn so much from, but especially to Richard Soeteman (@rsoeteman) to whom none of this would have been possible without his work on the Package Actions Contrib project which I referenced throughout this project! #SuperTak #h5yr!
So, to begin, in this example we will be using package actions to create a specific node for our 404 page, assigning it as a child of the root node, and then adjusting the path set in the umbracoSettings.config's <errors>
node
<errors> <error404>1</error404> <!-- The value for error pages can be: * A content item's integer ID (example: 1234) * A content item's GUID ID (example: 26C1D84F-C900-4D53-B167-E25CC489DAC8) * An XPath statement (example: //errorPages[@nodeName='My cool error'] --> <!-- <error404> <errorPage culture="default">1</errorPage> <errorPage culture="en-US">200</errorPage> </error404> --> </errors>
For those of you who are new to Umbraco, the umbracoSettings.config file is just that, a collection of settings for umbraco to use in that instance. Settings like error pages, Disallowed File Types, or even scheduled tasks (but I wouldn't unless you absolutely had to1) can be set here and adjusted when needed in a single file location.
Create the template to the site by adding a new doctype via the Settings > Document Types Create menu, be sure not to choose "Document Type without template" as that would defeat the whole purpose! As I usually have some sort of Masterpage type and template I would create this below that DocType in order to inherit all the properties and compositions shared; however, I would not add this as an allowed template as this is not a type of page you want to create more than one of as that will be handled by the package action code below.
Note: Make sure you remember the naming convention for your doctype as you will need this later on too!
When you save the new Doctype Umbraco will create a new "empty" template for you which you will need to add the codebase from your master doctype to. This blank template looks like this:
@inherits Umbraco.Web.Mvc.UmbracoTemplatePage @using MyNamespace.Helpers @{ Layout = null; }
For this example our 404 template looks like this:
@inherits Umbraco.Web.Mvc.UmbracoTemplatePage @using MyNamespace.Helpers @{ Layout = "Kit_Masterpage.cshtml"; } <!-- START // Content Page --> <article role="article"> @Html.Partial("Site_PageElements/Page__HeroBanner", @Model.Content) <!-- START // Article content --> <div class="content--area wrap clearfix"> <!-- START // Article copy column --> <div class="content article--content"> @Html.Raw(Model.Content.GetProperty("Kit_PageContent").Value) </div> <!-- END // Article copy column --> <!-- START // Article sidebar column --> <aside class="sidebar"> @Html.Partial("Site_NavElements/Page_SideNavigation", @Model.Content) <!-- START // Aside content widget - Latest News, Section signposts --> @Html.Partial("Site_Widgets/Widget__InfoPanel", @Model.Content) @if (Model.Content.AncestorOrSelf(1).Children.Any(x => x.IsDocumentType("Kit_LatestNews")) && Model.Content.HasValue("NEWS_DisplayWidget")) { if (Model.Content.GetPropertyValue<bool>("News_DisplayWidget")) { <!--LATEST NEWS MODULE--> @Html.Partial("Site_Widgets/Widget__LatestNews", @Model.Content) <!-- /LATEST NEWS MODULE --> } } <!-- END // Aside content widget - Latest News, Section signposts --> </aside> <!-- END // Article sidebar column --> </div> <!-- END // Article content --> </article> <!-- END // Content Page --> <!-- START // Page Elements --> @Html.Partial("Site_PageElements/Page__PageElements", @Model.Content) <!-- END // Page Elements -->
In Visual Studio add a new folder to the root of the site called "PackageActions" and inside of there then add a new class file called "Create404Handler"; add the following code to this file. By following through the comments in the code it mainly speaks for itself so I won't go too far into explaining it more other than to say that in Step 3 we will be copying the SampleXML string when we create the actual package in the back office.
using System; using System.CodeDom; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Xml; using ClientDependency.Core; using Newtonsoft.Json.Linq; using umbraco.interfaces; using Umbraco.Core; using Umbraco.Core.Logging; using umbraco.BusinessLogic; using umbraco.cms.businesslogic.packager.standardPackageActions; using Umbraco.Core.Models; using Umbraco.Web; namespace Project.PackageActions { public class Create404Handler : IPackageAction { /// /// Add the new 404 Handler to the site /// ///private static bool Set404Handler() { //set the target to be false bool result = false; try { //Grab hold of the content service var contentService = ApplicationContext.Current.Services.ContentService; //Get the homepage to ensure we have the entire content tree var homeNode = contentService.GetRootContent().FirstOrDefault(x => x.ContentType.Alias.InvariantEquals("Homepage_DocType")); //<-- ADD YOUR HOMEPAGE DOCTYPE HERE //Check to make sure this hasn't happened before if (!homeNode.Children().Any(x => x.ContentType.Alias.InvariantEquals("SITE_404Page"))) //<-- ADD YOUR 404 PAGE DOCTYPE HERE { //Create the 404 Page Not Found node var pageNotFound = contentService.CreateContent("404 Page Not Found", homeNode.Id, "SITE_404Page"); // <-- ENSURE THE 404 PAGE DOCTYPE IS HERE TOO! //Make sure the element has the NaviHide property available if (pageNotFound.HasProperty("umbracoNaviHide")) { //tell it to have the NaviHide Property set to true: pageNotFound.SetValue("umbracoNaviHide", true); } //Add it to the Content Tree contentService.SaveAndPublishWithStatus(pageNotFound); //Check to make sure the 404 page has been created if (pageNotFound.HasIdentity) { // ... then add the 404 Handler node to the UmbracoSettings.config file //Open the Umbraco Settings config file XmlDocument umbracoSettingsFile = XmlHelper.OpenAsXmlDocument("/config/umbracoSettings.config"); //Select errors node from the settings file XmlNode errorsRootNode = umbracoSettingsFile.SelectSingleNode("//errors"); if (errorsRootNode != null) { //Select the first error404 node XmlNode error404Node = errorsRootNode.SelectSingleNode("//error404"); //Make sure we have something to update if (error404Node != null) { //Create a new handler node XmlElement newHandlerNode = (XmlElement)umbracoSettingsFile.CreateElement("errorPage"); //Add the content.node ID value to the new handler node newHandlerNode.InnerText = pageNotFound.Id.ToString(); //Append standard attributes newHandlerNode.SetAttribute("culture", "default"); //then check to see if there are any child nodes if (error404Node.HasChildNodes) { //get the childnode to investigate XmlNode currentChildNode = error404Node.SelectSingleNode("//errorPage"); //Make sure we have a child node if (currentChildNode != null) { //check if the child node has a default culture attribute if (currentChildNode.Attributes["culture"] != null && currentChildNode.Attributes["culture"].Value == "default") { //change the inner text to the page not found node id currentChildNode.InnerText = pageNotFound.Id.ToString(); } else { //Append the new errorPage node to the error404 node error404Node.PrependChild(newHandlerNode); } } } else { //Remove any inner text of the initial 404 node error404Node.InnerText = string.Empty; //Append the new errorPage node to the error404 node error404Node.PrependChild(newHandlerNode); } //Append the new 404 handler node to the Umbraco Settings config file errorsRootNode.PrependChild(error404Node); //Save the Umbraco Settings config file with the new 404 handler node umbracoSettingsFile.Save(System.Web.HttpContext.Current.Server.MapPath("/config/umbracoSettings.config")); //No errors so the result is true result = true; //Add this event to the logs for our records LogHelper.Info ( "404 HANDLER INSTALLER: error404 Node and children were just saved and the xml tree was rebuilt"); } } } } //Refresh the content tree so we don't have to do it manually contentService.RebuildXmlStructures(); //Add this event to the logs for our records LogHelper.Info ("404 HANDLER INSTALLER: Home Node and children were just saved and the xml tree was rebuilt"); return result; } catch (Exception err) { // Get stack trace for the exception with source file information var st = new StackTrace(err, true); // Get the top stack frame var frame = st.GetFrame(0); // Get the line number from the stack frame var line = frame.GetFileLineNumber(); LogHelper.Warn(typeof(Create404Handler), "ERROR ON: Set404Handler - " + err.Message + "[" + line + "]:" + st); return false; } } public string Alias() { return "Create404Handler"; } public bool Execute(string packageName, XmlNode xmlData) { try { return Set404Handler(); } catch (Exception doh) { LogHelper.Error ("INSTALLER: Error at execute Create404Handler package action", doh); return false; } } public bool Undo(string packageName, XmlNode xmlData) { var contentService = ApplicationContext.Current.Services.ContentService; var homenode = contentService.GetRootContent().FirstOrDefault(x => x.ContentType.Alias.InvariantEquals("Homepage_DocType")); //<-- UPDATE TO HOMEPAGE DOCTYPE HERE if (homenode == null) return false; foreach (var node in homenode.Descendants().Where(x => x.ContentType.Alias.InvariantEquals("SITE_404Page"))) //<-- UPDATE TO 404 PAGE DOCTYPE HERE { contentService.UnPublish(node); contentService.Delete(node); } //Clean up the Umbraco Settings config file XmlDocument umbracoSettingsFile = XmlHelper.OpenAsXmlDocument("/config/umbracoSettings.config"); //Select errors node from the settings file XmlNode errorsRootNode = umbracoSettingsFile.SelectSingleNode("//errors"); //Select the first error404 node XmlNode error404Node = errorsRootNode.SelectSingleNode("//error404"); //get the childnode to investigate XmlNode currentChildNode = error404Node.SelectSingleNode("//errorPage"); //check if the child node has a default culture attribute if (currentChildNode.Attributes["culture"] != null && currentChildNode.Attributes["culture"].Value == "default") { currentChildNode.Attributes.RemoveAll(); } //change the inner text to the root node id currentChildNode.InnerText = "1"; //Append the new rewrite scheduled task to the Umbraco Settings config file errorsRootNode.AppendChild(error404Node); //Save the Umbraco Settings config file with the new Scheduled task umbracoSettingsFile.Save(System.Web.HttpContext.Current.Server.MapPath("/config/umbracoSettings.config")); return true; } public XmlNode SampleXml() { const string sample = " "; return ParseStringToXmlNode(sample); } private static XmlNode ParseStringToXmlNode(string value) { var xmlDocument = new XmlDocument(); var xmlNode = AddTextNode(xmlDocument, "error", ""); try { xmlDocument.LoadXml(value); return xmlDocument.SelectSingleNode("."); } catch { return xmlNode; } } private static XmlNode AddTextNode(XmlDocument xmlDocument, string name, string value) { var node = xmlDocument.CreateNode(XmlNodeType.Element, name, ""); node.AppendChild(xmlDocument.CreateTextNode(value)); return node; } } }
Depending on the version of Umbraco you're using the location of the dashboard may vary.
For pre v7.5 you can find it in the Developer > Packages > Created Packages > Right click dashboard:
And for v7.5 and above you will need to go to Developer > Packages > Right click dashboard:
You will need to add the name to the package when you click "create" so I've named this one "Custom 404 Handler" which is then pre-filled in the following dashboard dialogue:
You can see the four tabs outlining each of the fieldsets you will need to populate:
Package Contents ~ Select each of the types of Umbraco content types your package requires to be able to work, please note you will be able to add any dll's, contents of other folders, custom code in the next tab!
For our 404 Package I've chosen the following contents:
As the rest of the code will be added on the next tab!
Package Files ~ As the note on the dashboard tab says "Remember: .xslt and .ascx files for your macros will be added automatically, but you will still need to add assemblies, images and script files manually to the list below.
For our 404 Package I've chosen the following files:
Package Actions ~ Again quoting from the dashboard tab: Here you can add custom installer/uninstaller events to perform certain tasks during installation and uninstallation.
All actions are formed as an xml node, containing data for the action to be performed.
Here is where we finally add our XML string taken from the sample xml method in the custom actions class file:
<Action runat="install" undo="true" alias="Create404Handler"></Action>
This will trigger the custom action we've created above on the installation of the package to any umbraco instance! #woot!
When you are completely finished with the dialogue information and fields you can then choose to Publish the package which will then refresh the page and on the Properties tab you will find a new link for you to download the package to your computer and install it as needed on other sites you would like to:
The "Submit to Repository" button will start the process to send the package back to Our for approval. You will be prompted to sign in or register for an account on Our and submit a documentation PDF. Please read all information requested on this page before continuing the submission process.
To quote the HQ once more "The package administrators group reserves the right to decline packages based on lack of documentation, poorly written readme and missing author information"
Best of luck with your package creation, and I hope this helps!
Jon
1. There's been a lot of issues with using the internal scheduler for tasks as it can require a ping to be set on the website to stop the application pool from sleeping and therefore missing the task time. Search Our for more info if you're interested.