-
April 23rd, 2013SitecoreIn Sitecore, field names alone dont always give context to the data that editors are updating. On template fields you can set the Short Description field – this then shows the text next to the field. Some examples are:
It may be a strict requirement that help text is set on all fields, or help text on all fields matches a given pattern. The command and / or query below shows how to find missing Sitecore help text.
In the sitecore command example the code also modifies the help text to set the first letter as a capital if its lower case.
using System.Collections.Generic; using System.Linq; using System.Text; using ###.Store.Configuration; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Data.Managers; using Sitecore.Shell.Framework.Commands; using Sitecore.Web.UI.Sheer; namespace ###.Store.Developer.Commands { /// <summary> /// Find all template fields which either don't have /// help text or dont start with a capital letter /// </summary> public class ValidateHelpTextCommand : Command { public override void Execute(CommandContext context) { //storeTemplateFolder is the root folder for all templates in /sitecore/content/templates Item storeTemplateFolder = context.Items.FirstOrDefault() .Database.GetItem(TemplateIds.SitecoreTemplates.StoreTemplateFolder, LanguageManager.GetLanguage("en")); if (storeTemplateFolder != null) { ScanChildItems(storeTemplateFolder); } } private void ScanChildItems(Item storeTemplateFolder) { Dictionary<string, string> invalidItems = new Dictionary<string, string>(); foreach (Item child in storeTemplateFolder.Axes.GetDescendants()) { if (child.TemplateID == new ID(TemplateIds.SitecoreTemplates.TemplateField)) { string shortDescription = "__Short Description"; string helpText = child.Fields[shortDescription].Value; invalidItems[child.Paths.ContentPath] = helpText; if (!string.IsNullOrEmpty(helpText) && !HelpHasValidValue(helpText)) { using (new EditContext(child)) { //this corrects all the help text, it may be you don't want to apply the same rules child.Fields[shortDescription].SetValue(char.ToUpper(helpText[0]) + helpText.Substring(1), true); } } } } StringBuilder output = new StringBuilder(); foreach (string key in invalidItems.Keys) { if (string.IsNullOrEmpty(invalidItems[key])) { output.AppendLine(string.Concat(key, " is missing help text")); } else if (!HelpHasValidValue(invalidItems[key])) { output.AppendLine(string.Concat(key, " help text starts with lower case.")); } } output.AppendLine("Will now fix all lower case first letters"); SheerResponse.Alert(output.ToString()); } private static bool HelpHasValidValue(string value) { //invert the check so that things like { are considered valid return !char.IsLower(value.Trim().ToCharArray()[0]); } } }Note, there are constants available for all fields ids. The text value was used to highlight the __. The code forces the language to ‘en’ since all our templates are created in that language.
You then patch this into the application via:
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/"> <sitecore> <commands> <command name="item:validatehelptext" type="###.Store.Developer.Commands.ValidateHelpTextCommand, ###.Store.Developer" /> </commands> </sitecore> </configuration>Finally, you need to add a button to core and set its click to be ‘item:validatehelptext’
I thought it would be interesting to try the same thing using Sitecore Rocks Query. There are some really handy blog posts on this – have a look at Sitecore Rocks Query Examples for more info.
set language='en'; select @@Name, @#__Short Description#, @@Path from /sitecore/templates/User Defined//*[@@templatekey = 'Template field']
I like both these approaches for tracking down missing items – it’s safe to say trawling through fields manually is both time consuming, boring and error prone!
-
April 5th, 2013SitecoreThis post ties nicely into the idea thats shown in /automatically-set-the-language-of-the-content-editor
If you have a multi site solution, chances are each site has a finite set of languages available to it. In this example we have one language per site ala:
- sitecore
– Content
— English site – field with value for default language set to be ‘en’
— French site – field with value for default language set to be ‘fr-fr’Its easy to create invalid language content under each site. This not only bloats the amount of data being stored but can also be quite misleading, its easy to edit content on the wrong language.
The code below demonstrates a Sitecore command which allows these invalid items to be purged from the tree.
It assumes the website root item has a template with a known ID (WebsiteTemplate) and on this item there is a shared droplist field: Default Language which has its source set to /sitecore/system/Languages
using System; using System.Linq; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Globalization; using Sitecore.Shell.Framework.Commands; namespace ####.SitecoreCustom.Shell.Framework.Commands { /// <summary> /// Find the default site for the current item, /// then scan all descendents and remove versions /// for items other than the current language /// </summary> public class VersionPurgerCommand : Command { private static readonly ID WebsiteTemplate = new ID(new Guid("{B5BBED63-8A9F-41DA-AE3D-B043FF5DD7DF}")); public override void Execute(CommandContext context) { Item rootItem = context.Items.FirstOrDefault(); if (rootItem != null) { string currentSiteLanguage = SiteLanguageItem(context); if (!String.IsNullOrEmpty(currentSiteLanguage)) { RemoveVersions(rootItem, currentSiteLanguage); } } } private static void RemoveVersions(Item rootItem, string validLanguageName) { CleanItem(rootItem, validLanguageName); foreach (Item child in rootItem.Axes.GetDescendants()) { CleanItem(child, validLanguageName); } } private static void CleanItem(Item child, string validLanguageName) { foreach (Language language in child.Languages) { if (!String.Equals(language.Name, validLanguageName, StringComparison.OrdinalIgnoreCase)) { Item childInLanguage = child.Database.GetItem(child.ID, language); if (childInLanguage != null) { childInLanguage.Versions.RemoveAll(false); } } } } public override CommandState QueryState(CommandContext context) { if (SiteLanguageItem(context) == String.Empty) { return CommandState.Hidden; } return base.QueryState(context); } /// <summary> /// Find the current site language for source item /// </summary> private static string SiteLanguageItem(CommandContext context) { Item rootItem = context.Items.FirstOrDefault(); if (rootItem != null) { Item siteRootItem = rootItem.Axes.GetAncestors().FirstOrDefault(a => a.TemplateID == WebsiteTemplate); if (siteRootItem != null && siteRootItem.Versions.Count > 0) { return siteRootItem.Fields["Default Language"].Value; } } return ""; } } }This is then patched into the Sitecore commands via:
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/"> <sitecore> <commands> <command name="item:purgeversions" type="####.SitecoreCustom.Shell.Framework.Commands.VersionPurgerCommand, ####.SitecoreCustom" /> </commands> </sitecore> </configuration>You can then setup a new button in your ribbon by creating a new item in core. In this example, within the versions chunk of the ribbon:
/sitecore/content/Applications/Content Editor/Ribbons/Chunks/Versions/You need to fill out ‘Click’ to be item:purgeversions and then choose the icon you want.
By overriding the QueryState function the button only shows if:
- You are below a website root item
- You are on the valid language set on the website root item <- this is really cool since it means you can only remove invalid language versions
Taking this forwards, if you had multiple languages per site you’d need to update some of the logic to deal with lists of languages rather than 1 language.
-
October 23rd, 2012SitecoreIn a multi-lingual build we’d given the user a new toolbar button to create a copy of the given item in a given language. The logic behind the scenes would create a version of the new item in the destination language, scan the original item and copy all the field values to the destination. It would also then reset things like workflow on the destination item.
As part of this logic we’d call:
Item itemTo = item.Versions.AddVersion();
but would sometimes find itemTo would be null.
With the help of support, they pointed us towards the <sites> configuration and the following attribute:
filterItems: If true, the site will always show the current version of an item (without publishing) and advised this could be causing the issue. This setting can be manipulated programatically via setting:Sitecore.Context.Site.DisableFiltering = true / false;
So your final code would then be:
Item itemTo = null; bool oldValue = Sitecore.Context.Site.DisableFiltering; try { Sitecore.Context.Site.DisableFiltering = true; itemTo = item.Versions.AddVersion(); } finally { Sitecore.Context.Site.DisableFiltering = oldValue; }Enjoy your new versions!
-
October 15th, 2012SitecoreWe recently had an odd scenario where Sitecore EditFrame buttons would seemingly disappear randomly. Our edit frame made use of a mixture of Field Editor Buttons (‘/sitecore/templates/System/WebEdit/Field Editor Button’) and Edit Frame Small Buttons (‘/sitecore/templates/System/WebEdit/Edit Frame Small Button’).
We never had problems with the field editor buttons just the edit frame small buttons. On these buttons the clicks are bound to Sitecore commands. Behind the scenes these commands evaluate their querystate to check if they should be visible, disabled or active:
public override CommandState QueryState(CommandContext context) { ... }The buttons had a mixture of commands, some custom and some out the box. Examples of the out the box commands were:
item:movedown, item:moveup
Note, to find the code that runs for these commands, have a look in /App_Config/commands.config and search for the specific command name.
After debugging into our custom commands vs the out the box commands we found things like item:movedown has the following checks:
public override CommandState QueryState(CommandContext context) { if (context.Items.Length == 0) { return CommandState.Disabled; } Item item = context.Items[0]; if (!base.HasField(item, FieldIDs.Sortorder)) { return CommandState.Hidden; } if (item.Appearance.ReadOnly) { return CommandState.Disabled; } if (!item.Access.CanWrite()) { return CommandState.Disabled; } if (Command.IsLockedByOther(item)) { return CommandState.Disabled; } if (!Command.CanWriteField(item, FieldIDs.Sortorder)) { return CommandState.Disabled; } return base.QueryState(context); }The check that was catching us out was the if (Command.IsLockedByOther(item)) clause.
I’d never have thought to check an item locks as being the cause of EditFrame buttons not showing!!! The more I think about it I can see why the check is there – things like sort order are stored as fields against an item so if they are locked, you shouldn’t be able to edit them. From a UI perspective, it appears edit frame buttons don’t distinguish between CommandState.Disabled and CommandState.Hidden.
-
October 11th, 2012SitecoreWe recently setup some ribbon functionality to add custom popups into buttons in the Sitecore content editor ribbon. The code to launch the popup was via a custom command:
public override void Execute(CommandContext context) { Assert.IsTrue(context.Items.Length == 1, "Exactly one item should be selected"); ... CreateWindow(url.ToString(), "Network/32x32/earth_new.png", "Copy Language Content", "400px", "310px"); } private void CreateWindow(string uri, string icon, string header, string width, string height) { MethodInfo method = typeof(Windows).GetMethod("CreateWindow", BindingFlags.NonPublic | BindingFlags.Static); method.Invoke(null, new object[] { UIUtil.GetUri(uri), string.Empty, header, icon, width, height, true, true, true, true }); }In the popup window we wanted to be diligent and check that only certain users could see the form. This was done via:
protected void Page_Load(object sender, EventArgs e) { if (!(SitecoreContext.User.IsAdministrator || SitecoreContext.User.IsInRole("sitecore\RoleName"))) { ContentContainer.Visible = false; return; } }The problem we found was sporadically the ‘SitecoreContext.User.IsAdministrator’ check would fail and the popup would be blank.
After a lot of debugging we found the cookies in the HttpRequest object would sometimes be different hence causing the differences in the users attributes. When Sitecore invokes preview mode, it sets the user to be ‘sitecore\anonymous’ via ‘PreviewManager.StoreShellUser(true);’. This is what was catching us out. If the user had been using the cms and then visited preview the popup window would inherit the anonymous user.
Once we found the solution it was a very easy change to integrate (the WebEdit command -> Run(ClientPipelineArgs args) gave us the answer).
During our command before we open the popup we simply needed to add:
PreviewManager.RestoreUser();
-
October 9th, 2012SitecoreIf you want to change the behavior of when you past content into the Sitecore Rich Text Editor, you can change the settings used by the Telerik control.
This is embedded into the application via:
/sitecore/shell/Controls/Rich Text Editor/EditorPage.aspx
Then the attribute that causes the change in behavior is:
<telerik:RadEditor ID=”Editor” Runat=”server”
…
StripFormattingOnPaste=”All,ConvertWordLists”…
To see all options for the enumeration, have a look at http://demos.telerik.com/aspnet-ajax/editor/examples/cleaningwordformatting/defaultcs.aspx
-
September 27th, 2012SitecoreHere’s a quick tip related to the rich text editor in Sitecore. In 6.4 the telerik rad control was added, via specific style sheets you can expose css classes for the content editor to use.
Mark Stiles has blogged about this here.
These approaches works fine but what if you want the styles to be dynamic ie a content editor can add and remove them?
The solution
- You setup some items in your tree which represent CSS classes (name and required styles).
- You then create an httpHandler which sets its content type to be CSS.
- In your handler you expose all the CSS styles from the tree and render to the output (you may need to explicitly select which db to use in the handler since the requests may not run through all the sitecore pipelines). If needs be you could stream through the styles from the physical existing web stylesheet as well.
- You then point the WebStylesheet setting at your new handler.
Remember, you need to include the CSS link tag in your layout which points to your handler otherwise the classes won’t exist in the front end.
-
August 16th, 2012SitecoreWe had an interesting challenge recently which involved generating email markup via the page editor. Most html emails are build up from tables – in this situation we needed to generate several rows of content such that we were adding multiple sublayouts each representing a row:
- Sublayout in placeholder ‘body’: <tr><td>New row content</td></tr>
- Sublayout in placeholder ‘body’: <tr><td>Next new row content</td></tr>
- Sublayout in placeholder ‘body’: <tr><td>etc</td></tr>
In normal usage of the Page Editor you would be adding div’s to build up your html – unfortunately emails rely on a parent or several nested parent <table> tags.
When adding new sublayouts to the page the page editor didnt recognize where the table cells and rows would live (note the position of Add to here):
If this was done via divs you would expect to see something like:
One of the strict requirements of the work was the output markup matched existing templates exactly – the original templates had been rigorously tested in email clients. Because of this we really wanted to write the markup into our layouts and sublayouts so it matched the original html (albeit broken into placeholders and components).
After testing at cell and row level we found rewriting tags when in page editor mode helped the page editor respect the page layout much better.
The solution we arrived at was to tap in at page level and rewrite the whole markup in page editor. One of the assemblies that ships with Sitecore is the HtmlAgilityPack – its great for parsing and manipulating html.
There are 2 steps required:
- Tap into the page render
- Rewrite the content
First, lets rewrite the content:
using System; using System.Globalization; using System.IO; using System.Text; using HtmlAgilityPack; namespace ###.Web.UI { public static class MarkupRewriter { /// <summary> /// Rewrite table, tr and td tags to divs /// </summary> public static string RewriteTables(string markup) { HtmlDocument document = new HtmlDocument(); document.LoadHtml(markup); bool validTable = ModifyContent(document, "table", "table"); bool validRow = ModifyContent(document, "tr", "table-row"); bool validCell = ModifyContent(document, "td", "table-cell"); return RenderHtmlDocument(document); } private static bool ModifyContent(HtmlDocument document, string tag, string displayStyle) { HtmlNodeCollection cells = document.DocumentNode.SelectNodes(String.Concat("//", tag)); if (cells != null) { foreach (HtmlNode cell in cells) { cell.Name = "div"; AddStyle(cell, "display", displayStyle); if (cell.Attributes["height"] != null) { AddStyle(cell, "height", String.Concat(cell.Attributes["height"].Value, "px;")); } if (cell.Attributes["width"] != null) { AddStyle(cell, "width", String.Concat(cell.Attributes["width"].Value, "px;")); } if (cell.Attributes["bgcolor"] != null) { AddStyle(cell, "background-color", String.Concat(cell.Attributes["bgcolor"].Value, ";")); } //add any new conversions here } } return cells != null; } private static void AddStyle(HtmlNode node, string styleTag, string styleValue) { if (node.Attributes["style"] == null) { node.Attributes.Append("style", String.Concat(styleTag, ":", styleValue, ";")); } else { node.Attributes["style"].Value = CleanTag(String.Concat(node.Attributes["style"].Value, ";", String.Concat(styleTag, ":", styleValue, ";"))); } } private static string CleanTag(string value) { while (value.IndexOf(";;") > 0) { value = value.Replace(";;", ";"); } return value; } private static string RenderHtmlDocument(HtmlDocument document) { StringBuilder sb = new StringBuilder(); StringWriter sw = new StringWriter(sb, CultureInfo.CurrentCulture); document.Save(sw); sw.Close(); return sb.ToString(); } } }Then tie into the layout’s render:
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; using ###.Web.UI; using SitecoreContext = Sitecore.Context; namespace ###.Web.Application.Email.Layouts { public partial class EmailLayout : System.Web.UI.Page { protected override void Render(HtmlTextWriter writer) { if (SitecoreContext.PageMode.IsPageEditorEditing) { // setup a TextWriter to capture the markup TextWriter tw = new StringWriter(); HtmlTextWriter htw = new HtmlTextWriter(tw); // render the markup into our surrogate TextWriter base.Render(htw); // get the captured markup as a string string pageSource = tw.ToString(); // render the markup into the output stream verbatim writer.Write(MarkupRewriter.RewriteTables(pageSource)); } else { base.Render(writer); } } } }What I like about this approach:
- You only need to make the change once rather than per tag
- The markup you write into the layouts and sublayouts is the original
What I don’t like about this approach:
- Adding the style tags doesn’t check for existence – you could end up with duplicate tags if the original style attribute contains the new value. This would be easy to resolve by parsing the existing value in the AddStyle method
-
July 31st, 2012SitecoreThere are certain scenarios when working with Sitecore when you need to hide fields on specific Sitecore items in the content editor. Its worth noting the solution shown below actually hides things rather than correcting the template hierarchy. For certain scenarios this may be a quicker solution which doesn’t require editing any of the existing content of the Sitecore tree.
The two key aspects are:
- Configuring the rules for hiding and showing fields
- Applying the rules to the ui via the getContentEditorFields pipeline
As per normal, Sitecore makes this easy once you know which pipelines to tap into. During the lifecycle of an item in the content editor several pipelines run – the difference here is some are from Sitecore.Client rather than the kernel. In the example shown below the bulk of the code is around querying xml (config) for the rules. This could come from any datastore.
The rules I adopted were pretty simple and based around the following idea. Either you always want to show or always want to hide the field. You would then want to decide the opposite case eg when to show the field based on a set of rules – here it uses the context item as the starting point for each rule.
An example rule then becomes: Always hide the logo field unless you are below the navigation node.
Why do this – surely this points at a problem with my template hierachy? Yes it does. However the amount of regression testing involved with changing base templates of a large tree could be high. Certain assertions in code may apply rules based on templateId so changing base templates isnt a feasible option. It may be down the line you do want the logo field on certain items elsewhere in the tree as well – to bring that into play would mean simply config changes.
First you need the code:
using System; using System.Collections.Generic; using System.Linq; using System.Xml; using Sitecore.Configuration; using Sitecore.Data.Fields; using Sitecore.Data.Items; using Sitecore.Data.Templates; using Sitecore.Diagnostics; using Sitecore.SecurityModel; using Sitecore.Shell.Applications.ContentEditor.Pipelines.GetContentEditorFields; namespace ###.Domain.Cms.Specialization.Pipelines.GetContentEditorFields { /// <summary> /// Hide fields in specific areas of the tree based on filter rules /// </summary> public class FieldFilter : GetFields { protected override bool CanShowField(Field field, TemplateField templateField) { Assert.ArgumentNotNull(field, "field"); Assert.ArgumentNotNull(templateField, "templateField"); using (new SecurityDisabler()) { FilterableField[] fields = LoadFieldRules().ToArray(); if (field.Item != null) { //find matching filter FilterableField filter = fields.FirstOrDefault(a => a.Id == field.ID.Guid); if (filter != null) { if (filter.Rules.Any(a => a.Evaluate(field.Item))) { return filter.AlwaysHide; } else { return !filter.AlwaysHide; } } } } return base.CanShowField(field, templateField); } private IEnumerable<FilterableField> LoadFieldRules() { XmlNode node = Factory.GetConfigNode("fieldFilters"); if (node != null) { foreach (FilterableField field in LoadFields(node.SelectSingleNode("alwaysHide"), true)) { yield return field; } foreach (FilterableField field in LoadFields(node.SelectSingleNode("alwaysShow"), false)) { yield return field; } } } private IEnumerable<FilterableField> LoadFields(XmlNode rootNode, bool alwaysHide) { if (rootNode != null) { foreach (XmlNode fields in rootNode.ChildNodes) { FilterableField field = new FilterableField(); bool valid = true; try { field.Id = CastGuid(fields.Attributes["id"].Value, Guid.Empty); field.Rules = LoadRules(fields).ToArray(); field.AlwaysHide = alwaysHide; } catch { valid = false; } if (valid) { yield return field; } } } } private IEnumerable<IFilterRule> LoadRules(XmlNode alwaysHideField) { foreach (XmlNode rule in alwaysHideField.ChildNodes) { if (String.Equals(rule.Name, "unlessIsDescendentOf", StringComparison.OrdinalIgnoreCase)) { yield return new IsDescendentOfFilterRule(rule.Attributes["value"].Value); } //parse any new rules you require.... } } internal static Guid CastGuid(object value, Guid defaultValue) { try { Guid guid = new Guid(value.ToString()); return guid; } catch (Exception e) { return defaultValue; } } class FilterableField { public Guid Id { get; set; } public IFilterRule[] Rules { get; set; } /// <summary> /// Determines whether to always show or always hide the field /// </summary> public bool AlwaysHide { get; set; } } interface IFilterRule { bool Evaluate(Item currentItem); } class IsDescendentOfFilterRule : IFilterRule { object _value; public IsDescendentOfFilterRule(object value) { _value = value; } public bool Evaluate(Item currentItem) { Guid childValue = CastGuid(_value, Guid.Empty); if (childValue != Guid.Empty && currentItem != null) { return currentItem.Axes.GetAncestors().Any(a => a.ID.Guid == childValue); } return false; } } } }This is then patched into the config via:
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <fieldFilters> <alwaysHide> <field id="{61FB9634-CB92-4845-BE90-7E2D648403C2}" notes="link icon on link template - only show if used in the header"> <unlessIsDescendentOf value="{A6C6D94C-ACC3-497D-AE1B-CFA4E6A48B6B}" notes="Primary Navigation" /> </field> </alwaysHide> <alwaysShow> <!--<field .... />--> </alwaysShow> </fieldFilters> <pipelines> <getContentEditorFields> <processor type="Sitecore.Shell.Applications.ContentEditor.Pipelines.GetContentEditorFields.GetFields, Sitecore.Client" > <patch:attribute name="type">###.Domain.Cms.Specialization.Pipelines.GetContentEditorFields.FieldFilter, ###.Domain.Cms</patch:attribute> </processor> </getContentEditorFields> </pipelines> </sitecore> </configuration>What I like about this approach:
- Additional field rules can be added without any code changes – its all config (as long as the rules you need eg unlessIsDescendentOf exist)
- The patch:attribute on the include file allows swapping out the existing type attribute
- Its very easy to bolt in new IFilterRules
What I don’t like about this approach:
- I’ve subclassed the dtos and interfaces for eg IFilterRule – the rational here was these are only ever going to needed by the FieldFilter class (and it kept the amount of code blocks in the blog post down :S)
- More complex combinations of IFilterRules eg combining and/or logic doesn’t work yet
- It loads the config for every field – some simple caching would get around this
- Big or naive filter rules could really slow down the content editor
-
June 27th, 2012SitecoreOne of my colleagues at True Clarity has come up with a really neat solution to one of the challenges introduced by the Sitecore Rendering Engine. If you want to have the same container sublayout multiple times, its difficult to achieve since the placeholder’s xpath will be the same for each row. The solution was to setup dynamic placeholder keys which allow for similar containers to be repeated in a page’s layouts.
The setup this then allows for is:
Placeholder main
- Container in main with placeholders left and right
– Widget in container left / Widget in container right
- Another container in main with placeholder small
– Widgets in container small
- Container in main with placeholders left and right
– Widget in container left / Widget in container rightWithout the updated placeholder keys, you would never get the last row of widgets since the xpath for each row would evaluate to the same value (/main/left and /main/right for each row)
You can read all about it at http://trueclarity.wordpress.com/2012/06/19/dynamic-placeholder-keys-in-sitecore/




