Sitefinity CMS

Implementing TypeEditors for Complex Properties Send comments on this topic.
Developing with Sitefinity > Controls > Adding New Controls to Sitefinity > Implementing TypeEditors for Complex Properties

Glossary Item Box

In the previous sample (download from here: LinksList5) we have demonstrated how to implement complex properties on Sitefinity controls and represent them as strings. While we have achieved our goal of being able to expose complex properties, we have ended up with a not-very-user-friendly way of editing these properties. In this topic we will implement a TypeEditor which will give our users simple and straightforward way of editing complex properties.

 

Let’s start by looking at the screenshot of the end result:

Controls - Implementing TypeEditors - Control Properties

Figure 1: "Select" button appears next to the property which implements TypeEditor attribute

After clicking on the "Select" button, the end user will get the user friendly screen for editing ListLinks property:

Controls - Implementing TypeEditors - Property implementing TypeEditor

Figure 2: User interface for ListLinks property TypeEditor

 

The TypeEditor is actually a new control which has the single purpose of providing user interface for editing the property which has implemented it. As you can see from Figure 2, the list of links has been represented through a grid which has commands for adding new links, editing and deleting existing ones. While our Links List control is becoming more complex now (we have implemented two additional classes for it: DictionaryConverter and TypeEditor) it is also becoming more powerful and flexible, and at the same time maintaining its simplicity for the end user.


Now that we have seen the end result, let’s see the code that will make this happen. First of all we need to implement an additional attribute on our ListLinks property:

C# Copy Code
/// <summary>
/// Gets or sets the list of items to be displayed by the bulleted list control
/// </summary>
[Category("List settings")]
[TypeConverter(
"Telerik.Samples.DictionaryConverter, App_Code")]
[WebEditor(
"Telerik.Samples.LinksListItemEditor, App_Code")]
public Dictionary<string, string> ListLinks
{
   get
   {
       
// if listLinks dictionary is null, we'll create an empty dictionary
       
// to avoid dealing with null reference in code
       
if (listLinks == null)
           listLinks =
new Dictionary<string, string>();
       
return listLinks;
   }
   set
   {
       listLinks = value;
   }
}

 

In addition to the Category and TypeConverter attributes that we have implemented in previous samples, we will also add a WebEditor attribute which points to the class (control) which provides user friendly interface for editing ListLinks property.

The WebEditor class must be implemented as a custom control because it has to inherit from the WebUITypeEditor generic class, and C# classes can inherit only one base class. Naturally, User Controls have to inherit from UserControl class which makes it impossible to implement WebEditor as User Control.

 

Type Editor Class

Our TypeEditor class is nothing more than a custom control which has its own user interface and handles the events of that user interface. The only important thing to note is that the sole purpose of the TypeEditor is to come up with the value for the property it is editing. By inheriting from WebUITypeEditor generic class, we will have to implement Value property on our TypeEditor class. The importance of this property is that once the user clicks on the "I’m done" button, Sitefinity will take that property and set its value as the value of the property that we were editing with TypeEditor (in our case - ListLinks property).

Now, let us examine the code for LinksListItemEditor class:

LinksListItemEditor.cs Copy Code
using System;
using System.Collections;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Collections.Generic;
namespace Telerik.Samples
{
   
using Cms.Web.UI;
   
using Framework.Web;
   
using Web.UI;
   
/// <summary>
   
/// WebEditor control used for editing the items (ListLinks property)
   
/// of the links list in user friendly manner
   
/// </summary>
   
public class LinksListItemEditor : WebUITypeEditor<Dictionary<string,string>>, IControlPropertyEditor
   {
       #region Properties
       
/// <summary>
       
/// Gets or sets the Value property - Value property represents the property which TypeEditor
       
/// will edit of the parent control which fired the type editor. In our sample, Value property
       
/// represents ListLinks property of the LinksList6.ascx user control
       
/// </summary>
       
public override Dictionary<string , string> Value
       {
           get
           {
               
return listLinks;
           }
           set
           {
               listLinks = value;
           }
       }
       
/// <summary>
       
/// Gets or sets the path of the Dialog template
       
/// </summary>
       
public string DialogTemplatePath
       {
           get
           {
               
string value = (string)ViewState["DialogTemplatePath"];
               
return String.IsNullOrEmpty(value) ? "~/SampleControls/Utilities/TypeEditorTemplate.ascx" : value;
           }
           set
           {
               ViewState[
"DialogTemplatePath"] = value;
           }
       }
       
/// <summary>
       
/// Gets or sets the Dialog template
       
/// </summary>
       
public ITemplate DialogTemplate
       {
           get
           {
               dialogTemplate = ControlUtils.GetTemplate<DefaultDialogTemplate>(DialogTemplatePath);
               
return dialogTemplate;
           }
           set
           {
               dialogTemplate = value;
           }
       }
       
/// <summary>
       
/// Returns a dictionary with the dependent properties
       
/// </summary>
       
/// <remarks>
       
/// DependentProperties property allows us to modify additional properties
       
/// (not just the value of the property which fired TypeEditor) of the control which
       
/// has called TypeEditor. In this sample we will modify LinkTarget property in addition
       
/// to ListLinks property which is represented through ValueProperty of this control.
       
/// </remarks>        
       
public IDictionary<string, object> DependentProperties
       {
           get
           {
               
if (dependentProps == null)
               {
                   dependentProps =
new Dictionary<string, object>();
                   dependentProps.Add(
"LinkTarget", linkTarget);
               }
               
return dependentProps;
           }
       }
       #endregion
       #region Methods
       
/// <summary>
       
/// Overriden. Cancels the rendering of a beginning HTML tag for the control.
       
/// </summary>
       
/// <param name="writer">The HtmlTextWriter object used to render the markup.</param>
       
public override void RenderBeginTag(HtmlTextWriter writer)
       {
           
// Do not render begin tag
       }
       
/// <summary>
       
/// Overriden. Cancels the rendering of an ending HTML tag for the control.
       
/// </summary>
       
/// <param name="writer">The HtmlTextWriter object used to render the markup.</param>
       
public override void RenderEndTag(HtmlTextWriter writer)
       {
           
// Do not render end tag
       }
       
/// <summary>
       
/// Overrides the base method and registers ControlDesignerBase control as one whose control state
       
/// must be persisted.
       
/// </summary>
       
/// <param name="e"></param>
       
protected override void OnInit(EventArgs e)
       {
           
base.OnInit(e);
           
if (Page != null)
               Page.RegisterRequiresControlState(
this);
       }
       
/// <summary>
       
/// Restores control state information from a previous page request that was saved by the SaveControlState
       
/// method.
       
/// </summary>
       
/// <param name="savedState">Represents the control state to be restored.</param>
       
protected override void LoadControlState(object savedState)
       {
           
if (savedState != null)
           {
               
object[] state = (object[])savedState;
               listLinks = (Dictionary<
string, string>)state[0];
               linkTarget = (
string)state[1];
           }
       }
       
/// <summary>
       
/// Saves server control state changes.
       
/// </summary>
       
/// <returns></returns>
       
protected override object SaveControlState()
       {
           
return new object[] {  
   listLinks,
               linkTarget
  };
       }
       
/// <summary>
       
/// Overriden. Called to populate the child control hierarchy. This is the main
       
/// method to render the control's markup, since it is a CompositeControl and contains
       
/// child controls.
       
/// </summary>
       
protected override void CreateChildControls()
       {
           
// initialize the container and template
           
dialogContainer = new Container(this);
           DialogTemplate.InstantiateIn(dialogContainer);
           dialogContainer.LinksGrid.MasterTableView.DataKeyNames =
new string[] {"Key"};
           dialogContainer.LinksGrid.DeleteCommand +=
new GridCommandEventHandler(LinksGrid_DeleteCommand);
           dialogContainer.LinksGrid.EditCommand +=
new GridCommandEventHandler(LinksGrid_EditCommand);
           dialogContainer.LinksGrid.CancelCommand +=
new GridCommandEventHandler(LinksGrid_CancelCommand);
           dialogContainer.LinksGrid.UpdateCommand +=
new GridCommandEventHandler(LinksGrid_UpdateCommand);
           dialogContainer.LinksGrid.InsertCommand +=
new GridCommandEventHandler(LinksGrid_InsertCommand);
           dialogContainer.LinksGrid.NeedDataSource +=
new GridNeedDataSourceEventHandler(LinksGrid_NeedDataSource);
           dialogContainer.TargetRadioButtonList.AutoPostBack = true;
           dialogContainer.TargetRadioButtonList.SelectedIndexChanged +=
new EventHandler(TargetRadioButtonList_SelectedIndexChanged);
           
if (DependentProperties["LinkTarget"].ToString() == "_BLANK")
               dialogContainer.TargetRadioButtonList.SelectedIndex = 1;
           
else
               
dialogContainer.TargetRadioButtonList.SelectedIndex = 0;
           DependentProperties[
"LinkTarget"] = linkTarget;
           Controls.Add(dialogContainer);
       }
       #endregion
       #region Private methods
       
private void BindGrid()
       {
           dialogContainer.LinksGrid.DataSource = Value;
           dialogContainer.LinksGrid.DataBind();
       }
       #endregion
       #region Event handlers
       
private void LinksGrid_NeedDataSource(object source, GridNeedDataSourceEventArgs e)
       {
           dialogContainer.LinksGrid.DataSource = Value;
       }
       
private void LinksGrid_InsertCommand(object source, GridCommandEventArgs e)
       {
           GridEditableItem editedItem = e.Item
as GridEditableItem;
           Hashtable newValues =
new Hashtable();
           e.Item.OwnerTableView.ExtractValuesFromItem(newValues, editedItem);
           listLinks.Add(newValues[
"Key"].ToString(), newValues["Value"].ToString());
           BindGrid();
       }
       
       
private void LinksGrid_DeleteCommand(object source, GridCommandEventArgs e)
       {
           
string key = e.Item.OwnerTableView.Items[e.Item.ItemIndex]["Key"].Text;
           
if (listLinks.ContainsKey(key))
               listLinks.Remove(key);
           BindGrid();
       }
       
private void LinksGrid_EditCommand(object source, GridCommandEventArgs e)
       {
           e.Item.OwnerTableView.Items[e.Item.ItemIndex].Edit = true;
           BindGrid();
       }
       
private void LinksGrid_CancelCommand(object source, GridCommandEventArgs e)
       {
           
if (e.Item.ItemIndex > 0 && e.Item.OwnerTableView.Items[e.Item.ItemIndex].IsInEditMode)
               e.Item.OwnerTableView.Items[e.Item.ItemIndex].Edit = false;
           BindGrid();
       }
       
private void LinksGrid_UpdateCommand(object source, GridCommandEventArgs e)
       {
           GridEditableItem editedItem = e.Item
as GridEditableItem;
           
string key = e.Item.OwnerTableView.Items[e.Item.ItemIndex]["Key"].Text;
           Hashtable newValues =
new Hashtable();
           e.Item.OwnerTableView.ExtractValuesFromItem(newValues, editedItem);
           Dictionary<
string, string> updatedListLinks = new Dictionary<string, string>();
           
foreach(KeyValuePair<string, string> link in listLinks)
           {
               
if(link.Key == key)
                   updatedListLinks.Add(newValues[
"Key"].ToString(), newValues["Value"].ToString());
               
else
                   
updatedListLinks.Add(link.Key, link.Value);
           }
           listLinks = updatedListLinks;
           BindGrid();
       }
       
private void TargetRadioButtonList_SelectedIndexChanged(object sender, EventArgs e)
       {
           linkTarget = dialogContainer.TargetRadioButtonList.SelectedValue;
       }
       #endregion
       #region Private Propeties
       
private Dictionary<string, string> listLinks;
       
private string linkTarget;
       
private ITemplate dialogTemplate;
       
private Container dialogContainer;
       
private Dictionary<string, object> dependentProps;
       #endregion
       #region Default Templates
       
/// <summary>
       
/// Class which defines the template for this control, in case template has not been
       
/// defined through an external file or through inline declaration
       
/// </summary>
       
private class DefaultDialogTemplate : ITemplate
       {
           
public void InstantiateIn(Control container)
           {
               
throw new Exception("Default template for LinksListItemEditor has not been implemented.");
           }
       }
       #endregion
       #region Containers
       
/// <summary>
       
/// Class which provides easy, strongly-typed access to the child controls defined in a template
       
/// regardless of their position in the child control hierarchy
       
/// </summary>
       
private class Container : GenericContainer<LinksListItemEditor>
       {
           
public Container(LinksListItemEditor owner)
               :
base(owner)
           {
           }
           
public RadGrid LinksGrid
           {
               get
               {
                   
if (linksGrid == null)
                       linksGrid = FindRequiredControl<RadGrid>(
"linksGrid");
                   
return linksGrid;
               }
           }
           
public RadioButtonList TargetRadioButtonList
           {
               get
               {
                   
if (targetRadioButtonList == null)
                       targetRadioButtonList = FindRequiredControl<RadioButtonList>(
"targetRadioButtonList");
                   
return targetRadioButtonList;
               }
           }
           
private RadGrid linksGrid;
           
private RadioButtonList targetRadioButtonList;
       }
       #endregion
   }
}

 

The user interface for this control has been declared through the external template whose path is defined through DialogTemplatePath. The template code looks like this:

TypeEditorTemplate.ascx Copy Code
<h3>
   
Edit the list of links you want to display</h3>
<
p>
   
Open links in :
   
<asp:RadioButtonList ID="targetRadioButtonList" runat="server"
       
RepeatDirection="Horizontal">
       
<asp:ListItem Selected="True" Value="">Current Window</asp:ListItem>
       
<asp:ListItem Value="_BLANK">New Window</asp:ListItem>
   
</asp:RadioButtonList>
</
p>
<telerik:RadGrid ID="linksGrid" runat="server" AutoGenerateColumns="False" GridLines="None"
   
Skin="Web20">
   
<MasterTableView CommandItemDisplay="Top">
       
<commanditemsettings addnewrecordtext="Add new link" />
       
<RowIndicatorColumn Visible="False">
           
<HeaderStyle Width="20px"></HeaderStyle>
       
</RowIndicatorColumn>
       
<ExpandCollapseColumn Visible="False" Resizable="False">
           
<HeaderStyle Width="20px"></HeaderStyle>
       
</ExpandCollapseColumn>
       
<Columns>
           
<telerik:GridBoundColumn DataField="Key" HeaderText="Link title" UniqueName="Key">
           
</telerik:GridBoundColumn>
           
<telerik:GridBoundColumn DataField="Value" HeaderText="Link URL" UniqueName="Value">
           
</telerik:GridBoundColumn>
           
<telerik:GridButtonColumn CommandName="Delete" Text="Delete" UniqueName="Delete">
           
</telerik:GridButtonColumn>
           
<telerik:GridButtonColumn CommandName="Edit" Text="Edit" UniqueName="Edit">
           
</telerik:GridButtonColumn>
       
</Columns>
       
<EditFormSettings>
           
<PopUpSettings ScrollBars="None"></PopUpSettings>
       
</EditFormSettings>
   
</MasterTableView>
</
telerik:RadGrid>

 

The topic of the control development in ASP.NET is beyond the scope of this article. The important thing to note is that the TypeEditor control is focused on managing the Value property, which is nothing more than a mapped ListLinks property from our Links List control.

Notice how we do not have to worry about serializing the Value property, but we are able to work directly with the Dictionary<string, string> type. By implementing the TypeConverter on that property we have killed two birds with one stone: we have enabled Sitefinity to treat this property as a string, while at the same time we are free to work with the property in a more natural way – which, in this case, is a Dictionary<string, string> type.

 

Dependant Properties : How to Modify More Properties through TypeEditor of One Property

Sometimes you will have properties that, when edited, affect other properties. A classic example for this is the ProviderName property used in majority of built-in Sitefinity controls. Namely, let’s take the built-in Sitefinity control Categories List. You may choose to display the categories from Blogs provider, but also you may choose to select categories from the News provider. Once you open the TypeEditor for SelectedCategories property you can change the provider from which you want to select categories. So, although the TypeEditor’s primary task is to let users select categories, the user could also choose a provider, and the TypeEditor will set the ProviderName property of the CategoriesList control as well (not only SelectedCategories property). This is where the DependantProperties comes into play.

If you take a look at the TypeEditor code above you will notice that LinkTarget property of Links List control has been declared as DependantProperty. While, in reality this does not make much sense, it lends itself quite well to demonstrate the principle.


You can download the control for this sample from here: LinksList6.