Monday, March 22, 2010

Custom Fields, Part 2

In my Custom Fields, Part 1 post I demonstrated how to create a customized user interface for editing field content in Content Editor. This post is going to cover how to do the same sort of thing for Page Editor.


Demonstration of the component described in this post.

I know that I promised a custom field render in my previous post, but that's going to have to wait until my next post. This post has more ground to cover than I expected! But that's OK because a custom field renderer isn't needed to create the custom user interface. The generic field renderer is able to handle the user interface we're going to build.

A word of warning
But before I go any further I feel it is important to provide a warning. When I started looking into custom field types I was very excited by the idea of them. Then I talked to some colleagues and learned that custom field types are not all that common in the real world. There are some good reasons for this:
  • They are not very flexible - If new content needs to be added to a custom field type, it can result in a lot of work. The amount of work is hard to justify when you consider how easy it is to add a new field on a data template. In other words, using data templates to define the content is easier than creating and maintaining custom fields.
  • The parts of Sitecore that are needed to create custom field types are not entirely documented.
  • Creating this type of customization is risky because of changes that might be made to the API or to the Sitecore user interface in the future.
Having said that, I still believe there is value is knowing how to do this. The exercise has taught me a lot about how Sitecore handles content, as well as how the Sitecore client works. Now, on to the code...

Step 1 - Refactor name-parsing logic
In my previous post I included name-parsing logic directly in the name field control. If you remember, the purpose of this control was to display a custom interface for editing a name field using Content Editor.

This time I am creating a custom interface for editing a name field using Page Editor, so I'm going to need name-parsing logic again. Rather than duplicate the code, I'm going to create a utility class to hold that logic:

Assembly name: sctest.dll
Namespace: Sctest
Class name: NameFieldUtils
Base class: System.Object
public class NameFieldUtils
{
public static string GetFullName(string firstName, string lastName)
{
var name = new string[2];
name[0] = firstName;
name[1] = lastName;
return String.Join(" ", name);
}
public static string GetFirstName(string fullName)
{
string[] parts = fullName.Split(' ');
if (parts.Length >= 1)
{
return parts[0];
}
return string.Empty;
}
public static string GetLastName(string fullName)
{
string[] parts = fullName.Split(' ');
if (parts.Length >= 2)
return parts[1];
return string.Empty;
}
}

Step 2 - Rename the NameField class to NameControl
In order to avoid any possible confusion regarding the purpose of the various classes being created, rename the control created in the last post to NameControl. Recompile your code. Don't forget to change the control name in the core database, too (/sitecore/system/Field types/Custom Field Types/Name Field).

Step 3 - Modify the OnLoad method from NameControl
The name-parsing logic was moved into NameFieldUtils, so the OnLoad method should use NameFieldUtils. OnLoad should look like the following:
protected override void OnLoad(EventArgs e)
{
if (!Sitecore.Context.ClientPage.IsEvent)
{
//
//create the controls
var textFirstName = new Sitecore.Shell.Applications.ContentEditor.Text();
this.Controls.Add(textFirstName);
textFirstName.ID = GetID("textFirstName");
var textLastName = new Sitecore.Shell.Applications.ContentEditor.Text();
this.Controls.Add(textLastName);
textLastName.ID = GetID("textLastName");
//
//set the values on the textbox controls
textFirstName.Value = NameFieldType.GetFirstName(this.Value);
textLastName.Value = NameFieldType.GetLastName(this.Value); ;
}
else
{
//
//read the values from the textbox controls
var textFirstName = FindControl(GetID("textFirstName")) as Sitecore.Shell.Applications.ContentEditor.Text;
var textLastName = FindControl(GetID("textLastName")) as Sitecore.Shell.Applications.ContentEditor.Text;
//
//set the value on the NameControl control
var firstName = (textFirstName != null ? textFirstName.Value : string.Empty);
var lastName = (textLastName != null ? textLastName.Value : string.Empty);
this.Value = NameFieldType.GetFullName(firstName, lastName);
}
base.OnLoad(e);
}

This is a good point to test your changes. Recompile your code and make sure everything works as it did before you started reading this post. No new functionality has been added to Sitecore yes, so your code should work the same as it did before.

Step 4 - Create a layout
The Sitecore client user interface is built using XAML. This technology allows user interface components to be defined using XML. An XML Layout can be used to define the user interface.
  1. Switch to the core database.
  2. Open Developer Center.
  3. In the top menu, select File --New.
  4. The New File window appears. Select XML Layout from the Layouts category.
  5. Click the Create button.
  6. The new file wizard appears. For the name enter "Name Field Editor Layout". Then click the Next button.
  7. For the location select the \Layouts\Dialogs node. Click the Next button.
  8. For the file location select the \Website\layouts node. Click the Create button.
  9. Click the Finish button.
  10. The newly created XAML file appears in Developer Center. This file can be edited using Developer Center, or by editing the file Test Field Editor Layout.xml directly from the file system.
Step 5 - Specify the user interface components
The XML file created in the previous step must have controls in order to display a meaningful user interface.
<control xmlns:def="Definition" xmlns="http://schemas.sitecore.net/Visual-Studio-Intellisense">
<NameFieldEditorLayout>
<FormDialog Icon="Applications/32x32/text_marked.png" Header="Enter Name" Text="Enter the name." OKButton="OK">
<GridPanel Columns="2">
<Literal Text="First name:"/>
<Edit ID="FirstName" Width="300" />
<Literal Text="Last name:"/>
<Edit ID="LastName" Width="300" />
</GridPanel>
<CodeBeside Type="sctest.NameFieldEditor,sctest.dll"/>
</FormDialog>
</NameFieldEditorLayout>
</control>

Some of the code above is self-explanatory, I think. Here's a description of the parts that aren't obvious:
  • NameFieldEditorLayout - If you look in the core database you can find the item that was generated when you used Developer Center to create a new XML layout: /sitecore/layout/Layouts/Dialogs/Name Field Editor Layout. One of the properties of this item is its control name: NameFieldEditorLayout. This value was automatically generated by Developer Center. When Sitecore uses the XML layout, it is going to look for the XAML file that has the definition of NameFieldEditorLayout. By containing the tag NameFieldEditorLayout, this file becomes the file Sitecore will use.
  • FormDialog - Creates a modal window.
  • GridPanel - Creates a 2-column table. Each control inside the GridPanel tag gets its own cell. The 1st control gets the 1st cell in the 1st column. The 2nd control gets the 2nd cell in the 1st column. The 3rd control gets the 1st cell in the 2nd column. The 4th control gets the 2nd cell in the 2nd column.
  • CodeBeside - Defines the file that will contain code like event handlers.
Step 6 - Create the CodeBeside file
This file contains the code to handle events generated by the user interface. There are 2 methods in this file:
  1. OnLoad - This code runs when the dialog window is loaded. Specifically, the code sets the values of the FirstName and LastName input fields to be the current field value. This is done by reading a query string value. The query string value is set by code I will write in a later step.
  2. OnOK - This code runs when the OK button is clicked. Specifically, the code passes the values in the FirstName and LastName input fields back to the code that opened the dialog window in the first place. That, also, is code that I will write in a later step.

public class NameFieldEditor : DialogForm
{
protected Sitecore.Web.UI.HtmlControls.Edit FirstName;
protected Sitecore.Web.UI.HtmlControls.Edit LastName;
protected override void OnLoad(EventArgs e)
{
if (! Sitecore.Context.ClientPage.IsEvent)
{
//
//The values are not actually stored on the item itself
//until the save button is clicked, so we can't read
//the values from the item. Instead, read the values
//from the webedit interface. These values should have
//been included in the QueryString by the WebEditCommand
//instance that opened the dialog.
var name = Sitecore.Context.Request.QueryString["name"];
if ((name != null) && (! string.IsNullOrEmpty(name)))
{
FirstName.Value = NameFieldType.GetFirstName(name); ;
LastName.Value = NameFieldType.GetLastName(name); ;
}
}
base.OnLoad(e);
}
protected override void OnOK(object sender, EventArgs args)
{
//
//Write the response value that will be available to the
//WebEditCommand class that caused this dialog to appear.
var xml = string.Format("<name><first>{0}</first><last>{1}</last></name>", FirstName.Value, LastName.Value);
SheerResponse.SetDialogValue(xml);
base.OnOK(sender, args);
}
}


Step 7 - Create an item for the user interface
Sitecore needs to know that my user interface is available. This is done by creating an item in the core database:

Item to create the item on: /sitecore/content/Applications/Dialogs
Template: /sitecore/templates/Common/Folder
Item Name: Name Field Editor

Step 8 - Identify the layout that applies to the user interface item
Open the Presentation Details for the item created in the previous step. Set the layout to be Layouts/Dialogs/Name Field Editor Layout.

Step 9 - Define buttons for the custom field type
When a content author wants to set a value for a name field, I want the author to be able to click a button that will cause the name field editor dialog to appear. I need to define the button. But first I need to create a folder to hold the button. Create the following item:

Item to create the item on: /sitecore/system/Field types/Custom Field Types/Name Field
Template: /sitecore/templates/Common/Folder
Item Name: WebEdit Buttons

Next I create the button itself. Create the following item:

Item to create the item on: /sitecore/system/Field types/Custom Field Types/Name Field/WebEdit Buttons
Template: /System/WebEdit/WebEdit Button
Item Name:
Edit Name

A couple of field values must be specified for this item. Set the following values:

Header: Edit Name
Click: javascript:return Sitecore.WebEdit.editControl($JavascriptParameters, "sctest:EditNameButton")
Tooltip: Edit Name

Step 10 - Register the command
In the previous step I configured a button. One of the settings was the Javascript code to run when the button is clicked. The code tells Sitecore to execute a command named "sctest:EditNameButton" when the button is clicked. I need to tell Sitecore what code corresponds to this command. This is done by adding a command to the commands.config file.
<command name="sctest:EditNameButton" type="sctest.EditNameButtonCommand,sctest">

Step 11 - Implement the command
The class identified in the previous step contains the logic that is run when the specified command is executed (meaning when the edit name button is clicked).

This class extends Sitecore.Shell.Applications.WebEdit.Commands.WebEditCommand. The class has 2 methods in it:
  1. Execute - This method reads the current name field value. It sets this value as a parameter, which is important because this will make the value available inside the Run() method. Then the method calls the Run() method.
  2. Run - If the code is running during a PostBack it means either the Cancel or the OK button was clicked. If the Cancel button was clicked (there is no result, so args.HasResult is false) there is nothing else to do so the method returns. If the OK button was clicked, the XML generated by NameFieldEditor.OnOK() is parsed and the first and last names are set for the field in Page Editor.

    If the code is not running during a PostBack, that means someone has clicked the edit name button. The name field editor dialog window is displayed, and the current field value must be passed to the window.

public class EditNameButtonCommand : WebEditCommand
{
public override void Execute(CommandContext context)
{
//
//Read the value that is currently set on the field
//in the page editor. Since the page editor interface
//only writes field values after the user clicks the
//save button it is important that the value be read
//from the page editor rather than the item itself.
var formValue = WebUtil.GetFormValue("scPlainValue");
context.Parameters.Add("name", formValue);
Sitecore.Context.ClientPage.Start(this, "Run", context.Parameters);
}

protected void Run(ClientPipelineArgs args)
{
if (args.IsPostBack)
{
if (! args.HasResult)
{
return;
}
//
//Read the first and last names from the returned XML.
var dom = new XmlDocument();
dom.LoadXml(args.Result);
var firstName = dom.SelectSingleNode("/name/first").InnerText;
var lastName = dom.SelectSingleNode("/name/last").InnerText;
var fullName = NameFieldType.GetFullName(firstName, lastName);
//
//Set the first and last names. These values should not be
//saved directly on the item. The page editor will handle
//that when the user clicks the save button. Instead, the
//values should be set in a way that the page editor will
//store the values until the user clicks the save button.
SheerResponse.SetAttribute("scHtmlValue", "value", fullName);
SheerResponse.SetAttribute("scPlainValue", "value", fullName);
SheerResponse.Eval("scSetHtmlValue('" + args.Parameters["controlid"] + "')");
}
else
{
//
//get the url for the dialog that needs to be displayed
var db = Sitecore.Data.Database.GetDatabase("core");
var item = db.GetItem("{6C57B044-C860-4DAF-982E-5A64E467C1CD}");
var url = new UrlString(LinkManager.GetItemUrl(item));
//
//pass the current value to the dialog
var name = args.Parameters["name"];
if (name != null)
url.Add("name", name);
//
//display the dialog
Sitecore.Context.ClientPage.ClientResponse.ShowModalDialog(url.ToString(), true);
args.WaitForPostBack();
}
}
}


Note: in order to compile this code, you may need to add a reference to the Sitecore.Client.dll assembly. If using Visual Studio because to set CopyLocal to false.

Step 12 - Test the code
After I compile my code I am ready to test it. I need to make sure I have a field in a data template that uses the Name Field type. In the sublayout (or rendering) you're using to display the field value, make sure you're using the field renderer to display the value. You should be able to go into page editor and click the Edit Name button.


And when I click the button, I see the name field editor.


Next steps
In my next post I will demonstrate how to create a custom field renderer. Also, when you use the default field renderer on a name field, you may notice there is some unwanted functionality. I'll tell you what it is in my next post - along with a solutions for it. In the meantime, here's a hint: is there any way for a Page Editor user to circumvent the name field editor interface?

Want to learn more?

1 comment:

  1. The challenges stated at the beginning of the post generally steer me towards item editors and the iframe field type rather than custom fields, I think these two approaches are easier to implement than custom field types and less likely to suffer from those risks. The Client Configuration Cookbook covers those topics.

    http://sdn.sitecore.net/SDN5/Reference/Sitecore%206/Client%20Configuration%20Cookbook.aspx

    ReplyDelete

Note: Only a member of this blog may post a comment.