Download the source code for this project here
In this article we described an Xml field type that could be used for storing Xml and Xml-derived data in a SharePoint list. We got as far as creating a skeleton object that provides the basic functionality of a custom field. In this post we’ll add a custom user interface to make editing Xml data a bit more friendly.
In order to generate a responsive UI I’ve made use of two client side technologies:
- JQuery - specifically the Dialog functionality of JQuery UI
- CodeMirror – specifically the XML/HTML code parser
Step 1 – Make all required resources available
SharePoint provides a few useful methods for this purpose, I’ve opted to use ScriptLink.Register and CSSRegistration.Register. Both methods work in a similar fashion in that they attach additional resources to appropriate pages by making use of controls that are present on the master page. For example, ScriptLink.Register makes use of the <SharePoint:ScriptLink> control to render links to all registered scripts.
It’s worth nothing that there is a CSSLink class and a CSSRegistration class, unlike ScriptLink, the CSSLink object does not directly control the addition of custom CSS links, instead it makes use of one or more CSSRegistration controls as a data source. The reason for splitting the functionality into two classes is that the CSSLink control must be present on a master page whereas the CSSRegistration control(s) can be present on any page. The combination of all CSSRegistration controls are rendered by the CSSLink control on the master page.
Firstly, we need to download the relevant files from the links above then we need to add them to the WSPBuilder project that we created last time. The best place to put the downloaded files is under layouts, in a new folder with your project name, in this case I’ve used XMLTest.
Since we only want to add these resources where we’re displaying our custom field, the most effective way to add them is in the OnPreRender event of our BaseXmlField class. By using ScriptLink and CSSRegistration objects we ensure that only one copy of the relevant file is referenced and that the files are not unnecessarily attached to our master page when not required.
protected override void OnPreRender(EventArgs e)
{
//Make sure we've got the scripts and the stylesheets
ScriptLink.Register(this.Page, "XMLTest/Scripts/jquery-1.3.2.min.js", false);
ScriptLink.Register(this.Page, "XMLTest/Scripts/jquery-ui-1.7.2.custom.min.js", false);
ScriptLink.Register(this.Page, "XMLTest/Scripts/popupDialog.js", false);
ScriptLink.Register(this.Page, "XMLTest/CodeMirror-0.62/js/codemirror.js", false);
CssRegistration.Register("/_Layouts/XMLTest/Css/ui-lightness/jquery-ui-1.7.2.custom.css");
CssRegistration.Register("/_Layouts/XMLTest/CodeMirror-0.62/css/docs.css");
base.OnPreRender(e);
}
Note: There are a number of articles discussing how to make use of JQuery within SharePoint, naturally one of the points discussed is how to reference the JQuery.js file. One of the most common approaches is to attach a link to this file to your default masterpage. While this will undoubtedly work, if you have adopted this approach and are making use of JQuery elsewhere on your site please be aware that the ScriptLink control won’t know about your masterpage reference and will therefore add two references to the JQuery library. I’ve found that more often than not this causes script errors.
Step 2 – Add controls
Now that we’ve got all of our scripts and CSS attached, the next step is to hook them up to our custom field edit control. You’ll notice in the code snippet above there is a reference to XMLTest/Scripts/popupDialog.js. This is a small script file that we’ll add that will enable us to create a JQuery dialog containing a CodeMirror enabled text control.
$(function() {
// Dialog
$(".XmlDialog").dialog({
autoOpen: false,
width: 600,
modal: true,
buttons: {
"Ok": function() {
$(this).dialog("close");
$("#" + $(this).data("parentTextId"))[0].value = $(this).data("editor").getCode();
},
"Cancel": function() {
$(this).dialog("close");
}
}
});
});
function showXmlDialog(dialogId, textAreaId, parentTextId) {
var dialog = $("#" + dialogId);
if (!dialog.data("editor")) {
dialog.data("editor", CodeMirror.fromTextArea(textAreaId, {
height: "100%",
width: "100%",
parserfile: "parsexml.js",
stylesheet: "/_layouts/XMLTest/CodeMirror-0.62/css/xmlcolors.css",
path: "/_layouts/XMLTest/CodeMirror-0.62/js/",
continuousScanning: 500,
lineNumbers: false,
textWrapping: true,
tabMode:"indent"
}));
}
dialog.data("parentTextId", parentTextId)
dialog.data("editor").setCode($("#" + parentTextId)[0].value);
dialog.data("editor").reindent();
dialog.dialog('open');
return false;
};
The first of these two functions makes use of JQuery to find all items with a class name of XMlDialog and registers each as a dialog. For more information on exactly how this works see the JQUery documentation. The second function will be called when a user clicks on a button to show an expanded editor surface. For more information on exactly how this works see the CodeMirror documentation.
We now need to add a button to our renderer control then hook it up to this JavaScript. To do this we add the following code to the BaseXmlField class:
protected override void CreateChildControls()
{
base.CreateChildControls();
textBox = new TextBox();
dialog = new HtmlGenericControl("div");
textArea = new HtmlGenericControl("textarea");
textArea.ID = "code";
dialog.ID = "dialog";
dialog.Attributes.Add("Title", this.FieldName);
dialog.Attributes.Add("Class", "XmlDialog");
ellipsis = new Button();
ellipsis.Text = "...";
dialog.Controls.Add(textArea);
this.Controls.Add(dialog);
this.Controls.Add(textBox);
ellipsis.OnClientClick = "Javascript:return showXmlDialog('" + dialog.ClientID + "','" + textArea.ClientID + "','" + textBox.ClientID + "');";
this.Controls.Add(ellipsis);
}
Here we’ve defined a div called “dialog” which will form the container for the popup dialog, and added a button called “ellipsis” that will cause the dialog to popup. We’ve also added a textarea to the dialog since CodeMirror can attach to this control. This will allow our control to function even if there is some problem with the CodeMirror script since there can simply use the textarea control to enter data.
The ellipsis button calls the javascript function that we defined above passing in control id’s for the dialog and the text area as well as the id of the textbox that we added last time. By making use of a textbox to store the data we make life much easier when it comes to storing and retrieving data while also allowing our control to function even when JavaScript is turned off (although I’d doubt whether you’d even get as far as viewing the page with JavaScript turned off due to the extensive use that SharePoint makes of it!).
Step 3 – Add validation to the controls
We’ve now got the basis of a custom data entry control. It allows users to enter xml in a resizable popup dialog with syntax highlighting and automatic indentation. However, even though our popup dialog will highlight syntax errors it still doesn’t stop users from saving invalid xml. In order to do this we need to implement server side validation in both our BaseXmlField control and in our SPFieldXmlBase control.
When a field value is about to be saved the validation process is:
- Call the GetValidatedString method of the field object (in our case SPFieldXmlBase)
- Call the Validate method of the field renderer object (in our case the BaseXMlField object)
Step 2 only occurs if the data is being changed in the user interface. If the value is being changed programmatically only step 1 is carried out since there is no need to instantiate a renderer when accessing the value programmatically.
Strictly speaking adding code to the Validate method of our renderer serves no real purpose in our case since the validation process is the same. Generally you’d only make use of the Validate method to perform pre-processing of the entered value or to validate the individual inputs that make up the entire field value. For example, if we were implementing an address control, we’d be using multiple text boxes to capture input but would be storing a combined value in the field. The GetValidatedString would only have access to the combined value whereas the Validate method would have access to the individual text boxes.
In our BaseXmlField class we need to add the following code:
public override void Validate()
{
if (ControlMode == SPControlMode.Display || !IsValid)
{
return;
}
base.Validate();
if (Field.Required &&
(Value == null || Value.ToString().Length == 0))
{
this.ErrorMessage = string.Format("{0} must have a value.",Field.Title);
IsValid = false;
return;
}
//Attempt to load into an xml document
XmlDocument doc = new XmlDocument();
try
{
doc.LoadXml(Value.ToString());
IsValid = true;
return;
}
catch (XmlException ex)
{
//Inavlid Xml
this.ErrorMessage = string.Format("{0} contains invalid Xml. {1}", Field.Title, ex.Message);
IsValid = false;
return;
}
}
and in our SPFieldXmlBase control we need to add:
public override string GetValidatedString(object value)
{
if ((this.Required == true) && ((value == null) || ((String)value == "")))
{
throw new SPFieldValidationException(string.Format("{0} must have a value.",this.Title));
}
else
{
XmlDocument doc = new XmlDocument();
try
{
doc.LoadXml(value.ToString());
}
catch (XmlException ex)
{
throw new SPFieldValidationException(string.Format("{0} has an invalid value. {1}", this.Title,ex.Message), ex);
}
return base.GetValidatedString(value);
}
}
Conclusion
Having followed these steps you should now have a custom field control that provides a user friendly editor for Xml data. I’m sure you’ll agree that the control is easier to use than a multiline text box when it comes to adding and editing structured data.
In the next part we’ll start looking at our XmlField class and making use of the additional XSD and XSLT properties that we added in order to provide a higher level of validation and a very powerful rendering mechanism.
The next part of this article can be found here