[su_note note_color=”#fafafa”]Now available in the Sitecore Marketplace[/su_note]
This will be the last post in the series about creating a custom field type with support for Content and Page Editor (see part1, part2, and part3)
Basics
There are two main pieces to an XML control:
- Markup. It will be a simplification but think of it as XHTML with custom tags (controls) and certain implied rules. Take a look here. Having created a few of these I tend to stay on the HTML side of things and only use custom tags where I have to (more about it later). The XML markup is compiled into C# before it gets written back as HTML, not without enrichments and modifications by Sitecore. By the way, you can find quite a few examples in
WebsitesitecoreshellControls
. - Behavior. You can do anything you like with the JavaScript in the client. Unlike Content and Page Editor there’s no jQuery or Prototype.js so it’s a Bring Your Own Script environment. On the server side your XML control is technically a
WebControl
so the behavior will come from the lifecycle events and the controls you use in your markup. To extend the web control itself you would use thedef:inherits
hint and for dialog-style forms you would probably just wrap your markup into<FormDialog>
and use the<Codebeside>
control. The latter is what I did with the YouTube Video picker dialog and the former we will look into a little later.
You declare your XML controls alongsite custom Web Controls:
&lt;configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"&gt; &lt;sitecore&gt; &lt;controlSources&gt; &lt;source mode="on" namespace="BrainJocks.YouTube.Custom.Controls" assembly="BrainJocks.YouTube.Custom" prefix="brainjockscontrols"/&gt; &lt;source mode="on" namespace="BrainJocks.YouTube.Web.XmlControls" assembly="BrainJocks.YouTube.Web" folder="/sitecore modules/BrainJocks" /&gt; &lt;/controlSources&gt; &lt;/sitecore&gt; &lt;/configuration&gt;
With this configuration patch and provided that your XML markup defines the control as:
&lt;control xmlns:def="Definition" xmlns="http://schemas.sitecore.net/Visual-Studio-Intellisense"&gt; &lt;SelectYouTubeVideo&gt; &lt;FormDialog Header="Select YouTube Video" Text="Select YouTube Video" OKButton="OK"&gt; &lt;CodeBeside Type="BrainJocks.YouTube.Web.XmlControls.SelectYouTubeVideo, BrainJocks.YouTube.Web" /&gt; ... &lt;/FormDialog&gt; &lt;/SelectYouTubeVideo&gt; &lt;/control&gt;
And with a codebeside class:
public class SelectYouTubeVideo : Sitecore.Web.UI.Pages.DialogForm { protected override void OnLoad(EventArgs e) { } protected override void OnOK(object sender, EventArgs args) { } }
You can call your dialog from within Sheer UI like this:
var urlString = new Sitecore.Text.UrlString(Sitecore.UIUtil.GetUri("control:SelectYouTubeVideo")); Sitecore.Context.ClientPage.ClientResponse.ShowModalDialog(urlString.ToString(), "650px", "700px", string.Empty, true);
Markup
First, let me show you how to troubleshoot your markup. With the following configuration patch:
&lt;settings&gt; &lt;setting name="XmlControls.OutputDebugFiles"&gt; &lt;patch:attribute name="value"&gt;true&lt;/patch:attribute&gt; &lt;/setting&gt; &lt;/settings&gt;
the C# code that Sitecore control parser (Sitecore.Web.UI.XmlControls.ControlParser
) produces will be dumped into Websitesitecore modulesdebug
. The generated class by default inherits from Sitecore.Web.UI.XmlControls.XmlControl
which not surprisingly is a System.Web.UI.WebControls.WebControl
. The generated code translates your markup (all of it, regardless of the runat
attribute) into a series of AddControl()
methods. It wouldn’t be of any particular importance if only the id
attributes didn’t become variable names by default:
[su_note note_color=”#fafafa”]Rule #1: Use valid C# identifiers as your id
attributes in the XML markup. No dashes. Or use def:id
to hint the ControlParser
on the variable name it should use.
[/su_note]
[su_note note_color=”#fafafa”]Rule #2: Use <Script>
and <Stylesheet>
controls to place your resources into the <head>
. Use regular <script>
if you want to inline it.
[/su_note]
Those special Script
and Stylesheet
tags will be converted into Sitecore.Web.UI.HtmlControls.Script()
and Sitecore.Web.UI.HtmlControls.Stylesheet()
accordingly and will be pushed into the <head>
section by Sitecore. Otherwise – a regular AddControl()
and the script and styles tag will render alongside your markup. Unlike <script>
, the <link rel="stylesheet"
doesn’t work outside of <head>
. Well, it does in HTML5 but XML controls render as XHTML. At least they are trying to:
[su_note note_color=”#fafafa”]Rule #3. Don’t use self enclosing <div/>
. Or any other self enclosing HTML elements that are not special controls as it will unexpectedly break your markup. The alternative is to patch Websitesitecoreshelldefault.aspx
. Our use 7.1+.
[/su_note]
HTML 4 doesn’t have a concept of self-enclosing tags, XHTML does. The XML dialog control tries to render as XHTML but fails to put <!DOCTYPE
first. Here’s how it looks in the browser (Sitecore 7 Update 2):
&lt;meta http-equiv="X-UA-Compatible" content="IE=5"/&gt; &lt;!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" ... &lt;html&gt; ...
The <meta>
is coming from the Websitesitecoreshelldefault.aspx
. From Sitecore Support: That line has been removed from the installation in Sitecore 7.1 rev.130926
.
Behavior
The most straightforward way is to use a def:Code
block right in your XML markup. Here’s how it looks in the FormDialog
:
&lt;control xmlns:def="Definition" xmlns="http://schemas.sitecore.net/Visual-Studio-Intellisense"&gt; &lt;FormDialog CancelButton=""&gt; &lt;def:Code&gt;&lt;![CDATA[ protected override void OnLoad(EventArgs e) { if (CancelButton == "false") { CancelSpace.Visible = false; Cancel.Visible = false; } } ]]&gt;&lt;/def:Code&gt; ...
I don’t like intermixing markup and code so I wouldn’t recommend it even for a very simple behavior like that of the FormDialog
.
The next option is to use the Codebeside
control. In my case I extended DialogForm
that defines a virtual OnOK()
and binds it to the click
event of the OK
button. The codebeside implementation then overrides the OnOK()
behavior to read the video ID off of the RawValue
hidden input and to record it as a dialog value. This way we tell Sitecore what to write back into the field:
protected override void OnOK(object sender, EventArgs args) { SheerResponse.SetDialogValue(RawValue.Value); base.OnOK(sender, args); }
If the behavior of your XML control needs more freedom than what you can get with the Codebeside and out of the box dialog forms you will need to use def:inherits
. Here’s an example from the SelectAccount.xml
:
&lt;control xmlns:def="Definition" xmlns:installer="Sitecore.Shell.Applications.Install.Controls"&gt; &lt;Installer.SelectAccount def:inherits="Sitecore.Shell.Applications.Install.Controls.SelectAccount, Sitecore.Client"&gt; ...
One last thing. The dialog controls bind hot keys:
scForm.registerKey("13", "javascript:scForm.browser.getControl('OK').click()", "");
and you may want to capture some to help your user drive your form from the keyboard. Without the following in the YouTube Video picker dialog pressing the Enter key to finish autocomplete, for example, would submit the whole thing:
var submitQueryOnEnter = function (e) { if (e.which == 13) { // prevent Sitecore from submitting the form e.preventDefault(); e.stopPropagation(); $('#SearchButton').click(); } }; $('#SearchQuery').keydown(submitQueryOnEnter); $('#SearchButton').keydown(submitQueryOnEnter);
[su_divider][/su_divider]
References:
Thanks for a brilliant walkthrough. SheerUI still is relevant for older solutions, where SPEAK is not yet an option. A small note from your post:
“You can do anything you like with the JavaScript in the client. Unlike Content and Page Editor there’s no jQuery or Prototype.js so it’s a Bring Your Own Script environment”.
This is not absolutely true. Sitecore still injects prototype JS as well as client stylesheets. You can, indeed, use
/sitecore/shell/Controls/Lib/jQuery/jquery.noconflict.js but it’s an old version (think it’s 1.5.x), and that might be a problem for some. I’ve just copied my 1.8.3 version of jQuery to a new file, and added this at the very bottom of it:
if (typeof (window.$sc) == “undefined”) window.$sc = jQuery.noConflict();
Referencing this file, makes jQuery work together with prototype js – as long as you use jQuery the noConflict way (by not using the $ reference)