I recently had to use the general link field for one of my projects, which is a multisite solution. What I needed was a way to define this field in one template, and that template would be used in multiple sites. Given that you can ‘Internal’ links on this field, I didn’t want them to be able to add a link to an item from another site. You can achieve part of this with security, but we also have some admins that manage multiple sites. So what we needed was for the field to be aware of where it is, and figure out the root of that site, and only show that part of the tree.
As you know, each field for Sitecore has a column called ‘Source’, which basically tells the field what tree to show (if the field needs to show a part of the tree). I want to be able to put queries like this in Source field:
query:./ancestor-or-self::*[@@templatename='Site Root']/Home
What this should do, in theory is to go up from the context item all the way up to where the template type is ‘Site Root’, and show the /Home part of that tree. This works in a lot of other fields such as droptree and droplink, but it didn’t work in the General Link field.
The General Link Field Source
I decided to take a look into the Sitecore.Kernel and I was disappointed to learn that the General Link field does not support the Source query. If you look into at the LinkBase class (which the Link class is derived from) you’ll see that the Source field does nothing, except read from the text-box:
public virtual string Source { get { return this.GetViewStateString("Source"); } set { Assert.ArgumentNotNull((object) value, "value"); string str = MainUtil.UnmapPath(value); if (str.EndsWith("/", StringComparison.InvariantCulture)) str = str.Substring(0, str.Length - 1); this.SetViewStateString("Source", str); } }
I was hoping it was added in Sitecore 8, but it doesn’t seem like it I’ve customized some of these fields before, so I went ahead and built out a custom field for this one as well.
The Field Class
You can view the whole class on Github: https://github.com/rahm0277/SC-MS-CustomFields/blob/master/Fields/GeneralLinkWithSource.cs – I break it down here in chunks to explain it better.
This is actually a pretty simple class with two major parts. First I made a property to grab the itemID for the item that the field is on:
private string _itemid; public string ItemID { get { return StringUtil.GetString(new string[1] { this._itemid }); } set { Assert.ArgumentNotNull((object)value, "value"); this._itemid = value; } }
In the second part, we override the Source property, that wasn’t doing anything other than reading the text field. We modify the ‘set’ method to check to see if the string has ‘query’ in front. If it does, then first get the context item object based on the itemID. Then, extract the query from the string value, and execute it, based on the context item (by context item, in this case, I mean the item the field is on).
For now, I excluded ‘fast:’ queries, so I want to make sure we don’t execute those.
If there is no ‘query:’, we just let it continue based on what the base class was doing.
public override string Source { get { return this.GetViewStateString("Source"); } set { Assert.ArgumentNotNull(value, "value"); String newValue = value; if (value.StartsWith("query:", StringComparison.InvariantCulture)) { string query = value.Substring(6); bool flag = query.StartsWith("fast:", StringComparison.InvariantCulture); if (!flag) { Item item = Client.ContentDatabase.GetItem(this.ItemID); if (item != null) { Item sourceItem = item.Axes.SelectSingleItem(value.Substring("query:".Length)); if (sourceItem != null) { base.SetViewStateString("Source", sourceItem.Paths.FullPath); } } } } else { string str = MainUtil.UnmapPath(newValue); if (str.EndsWith("/", StringComparison.InvariantCulture)) { str = str.Substring(0, str.Length - 1); } base.SetViewStateString("Source", str); } } }
Configurations and Adding the Custom fields
Once done and compiled, you’ll need to add the field items in the Core database, in /system/Field Types/Link Types and point it to your assembly. For this, I duplicated the existing field and added my assembly:
And then added an include file to add my fields:
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <fieldTypes> <fieldType name="General Link with Source Query" type="MS.CustomFields.Fields.GeneralLinkWithSource,MS.CustomFields" /> <fieldType name="General Link with Search and Source Query" type="MS.CustomFields.Fields.GeneralLinkWithSource,MS.CustomFields" /> </fieldTypes> </sitecore> </configuration>
The Dialog to Choose the Link (InsertLinkDialogTree)
The above class and configs alone will be enough get the fields working. You should be able to see the tree which your query resolves. However, once you select a value and go back to edit it, the item you selected is not shown, and the tree is collapsed.
Sitecore Support had to help me on this one – you basically have to replace the InsertLinkDialogTree class, which is in Sitecore.Support.Speak.Applications assembly.
The method that selects the item in the tree has to be customized – the reason nothing is selected is because in the original class, the PreLoadPath parameter is setting the path value to be the value in the field based on the main root item (of the content tree, not the site).
this.TreeView.Parameters["PreLoadPath"] = SelectMediaDialog.GetMediaItemFromQueryString(InsertLinkDialogTree.GetXmlAttributeValue(element, "id")).Paths.LongID.Substring(1);
But if there is a query in the source column, the main root item is not the root for the field – so we have to find it, and then append the path to that item:
Item contextItem = ((Database)ClientHost.Databases.ContentDatabase).GetItem(queryString1 ?? string.Empty) ?? ((Database)ClientHost.Databases.ContentDatabase).GetRootItem(); Item linkedItem = (Item)SelectMediaDialog.GetMediaItemFromQueryString(InsertLinkDialogTree.GetXmlAttributeValue(element, "id")); if (contextItem != null && linkedItem != null && linkedItem.Paths.LongID.StartsWith(contextItem.Paths.LongID)) this.TreeView.Parameters["PreLoadPath"] = contextItem.ID.ToString() + linkedItem.Paths.LongID.Substring(contextItem.Paths.LongID.Length);
Setting Sitecore to use the dialog
This can be done in two ways:
Via Sitecore Rocks:
- Open your site via Sitecore Rocks
- Go to /sitecore/client/Applications/Dialogs/InsertLinkViaTreeDialog item in core DB
- Select “Tasks -> Design Layout”
- Select “Page Code” rendering
- Set “PageCodeTypeName” to “MS.CustomFields.Fields.SpeakDialog.InsertLinkDialogTree, MS.CustomFields” and save the changes.
If you don’t have Sitecore Rocks:
- Select the /sitecore/client/Applications/Dialogs/InsertLinkViaTreeDialog item in Core database.
- Switch to the Raw values view.
- Modify the “Renderings” field from the “Layout” section by replacing this string:
par=”PageCodeTypeName=Sitecore.Speak.Applications.InsertLinkDialogTree%2c+Sitecore.Speak.Applications”
with the following string:par=”PageCodeTypeName=MS.CustomFields.Fields.SpeakDialog.InsertLinkDialogTree%2c+MS.CustomFields”
- Save the item.
Conclusions
That should be it – now when you open it up, it should pre-select the item that you originally selected.
The source for this is here: https://github.com/rahm0277/SC-MS-CustomFields – I’ve included the package in here in case you didn’t want to go through this exercise. The package can also be found at the marketplace.
This has been tested on Sitecore 8, update 3. Let me know if you have any questions.