Tree Element Picker

Learn to create a simple text box with a tree element picker

In the following example, you will:

  1. create a text box with a picker button as a custom ASP.NET control
  2. create a custom tree of elements to select in a standard dialog
  3. register the custom control in C1 CMS configuration
  4. register the custom tree element provider C1 CMS configuration
  5. use it on a data form

Creating a custom control

First, you need to find a place for your ASCX file on your website. We recommend that you place it in ~/Composite in its own subfolder.

  1. In ~/Composite, create this folder structure: CustomControls/CustomTreeControl/controls.
  2. In the “controls” folder, create a custom control file “CustomTreeInput.ascx”.
  3. Add the following code to the file and save it:
 
<%@ Control Language="C#" Inherits="Composite.Plugins.Forms.WebChannel.UiControlFactories.TextInputTemplateUserControlBase"  %>
<%@ Import Namespace="Composite.Data.Validation.ClientValidationRules" %>
<%@ Import Namespace="System.Xml" %>
<%@ Import Namespace="System.IO" %>

<script type="text/javascript">
	if (!ViewDefinitions["Composite.Samples.CustomTreeDialog"]) {
		ViewDefinitions["Composite.Samples.CustomTreeDialog"] = new DialogViewDefinition({
			isMutable: true,
			handle: "Composite.Samples.CustomFormDialog",
			position: Dialog.MODAL,
			url: Dialog.URL_TREEACTIONSELECTOR,
			argument: {
				label: "Title of Tree",
				image: "${icon:image}",
				selectionProperty: "DataId",
				selectionValue: null,
				selectionResult: "DataId",
				actionGroup: "Folder",
				nodes: [{
					key: "SampleElementProvider",
					search: null
				}]
			}
		});
	}
</script>

<script runat="server">
    private string _currentStringValue = null;

    protected void Page_Init(object sender, EventArgs e)
    {
        if (_currentStringValue == null)
        {
            _currentStringValue = Request.Form[this.UniqueID];
        }
    }
    
    protected override void BindStateToProperties()
    {
        this.Text = _currentStringValue;
    }

    protected override void InitializeViewState()
    {
        _currentStringValue = this.Text;
    }

    public override string GetDataFieldClientName()
    {
	    return this.UniqueID;
    }

</script>
<ui:datainputdialog id="<%= this.UniqueID  %>" handle="Composite.Samples.CustomTreeDialog" name="<%= this.UniqueID  %>" value="<%= Server.HtmlEncode(_currentStringValue) %>" readonly="true" />
Download CustomTreeInput.ascx

Some code highlights:

  • The control inherits from the standard text box: Composite.Plugins.Forms.WebChannel.UiControlFactories.TextInputTemplateUserControlBase
  • The script run at the server contains standard code to initialize and properly set the control in the methods: Page_Init, BindStateToProperties, InitializeViewState, GetDataFieldClientName
  • The picker button is represented by the <ui:datainputdialog/> control.
  • The click on the button is handled by invocation of a custom dialog (Composite.Samples.CustomTreeDialog).
  • As the dialog is custom, the control also includes a JavaScript that creates and initializes this dialog
  • The dialog is created from the standard dialog for tree structures (Dialog.URL_TREEACTIONSELECTOR)

You also need to register the control in C1 CMS (see below).

Creating a custom tree for the dialog

  1. In Visual Studio, create a Class Library project (e.g. “Composite.Samples.CustomTreeControl”).
  2. Add a reference to ~\Bin\Composite.dll from your website.
  3. Add a class (e.g. “SampleElementProvider”) and add the following code:
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Runtime.Remoting.Metadata.W3cXsd2001;
    using System.Text;
    using System.Threading.Tasks;
    using Composite.C1Console.Elements;
    using Composite.C1Console.Elements.Plugins.ElementProvider;
    using Composite.C1Console.Security;
    using Composite.Core.ResourceSystem;
    using Composite.Core.ResourceSystem.Icons;
    using Composite.Data;
    
    namespace Composite.Samples.CustomTreeControl.ElementProvider
    {
    	public class SampleElementProvider : IHooklessElementProvider
    	{
    		private static ResourceHandle FolderIcon { get { return CommonElementIcons.Folder; } }
    		private static ResourceHandle OpenFolderIcon { get { return CommonElementIcons.FolderOpen; } }
    
    
    		public ElementProviderContext Context
    		{
    			get;
    			set;
    		}
    
    		public IEnumerable<Element> GetChildren(EntityToken entityToken, SearchToken seachToken)
    		{
    			if (entityToken.Type == "Root")
    			{
    				foreach (var i in new []{1,2,3})
    				{
    					var element = new Element(Context.CreateElementHandle(new SampleEntityToken( i.ToString(), "Folder")))
    					{
    						VisualData = new ElementVisualizedData()
    						{
    							Label = string.Format("Folder {0}", i),
    							ToolTip = string.Format("Folder {0}", i),
    							HasChildren = true,
    							Icon = FolderIcon,
    							OpenedIcon = OpenFolderIcon
    						}
    					};
    					yield return element;
    				}
    
    				yield return new Element(Context.CreateElementHandle(new SampleEntityToken("PageFolders", "Pages")))
    				{
    					VisualData = new ElementVisualizedData()
    					{
    						Label = "Pages",
    						ToolTip = "Pages",
    						HasChildren = true,
    						Icon = FolderIcon,
    						OpenedIcon = OpenFolderIcon
    					}
    				};
    
    			}
    			else if (entityToken.Type == "Folder")
    			{
    				foreach (var i in new[] { 1, 2, 3 })
    				{
    					var folderid = entityToken.Id;
    					var element = new Element(Context.CreateElementHandle(new SampleEntityToken("Item", i.ToString(), entityToken.Id)))
    					{
    						VisualData = new ElementVisualizedData()
    						{
    							Label = string.Format("Item {0}", i),
    							ToolTip = string.Format("Item {0}", i),
    							HasChildren = false,
    							Icon =  new ResourceHandle(BuildInIconProviderName.ProviderName,"data")
    						}
    					};
    
    					//Value of element in tree
    					element.PropertyBag.Add("DataId", string.Format("{0},{1}", folderid, i) );
    					yield return element;
    				}
    			}
    
    			else if (entityToken.Type == "Pages")
    			{
    				foreach (var pageId in PageManager.GetChildrenIDs(Guid.Empty))
    				{
    					var page = PageManager.GetPageById(pageId);
    					var element = new Element(Context.CreateElementHandle(page.GetDataEntityToken()))
    					{
    						VisualData = new ElementVisualizedData()
    						{
    							Label = page.GetLabel(),
    							ToolTip = page.Description,
    							HasChildren = false,
    							Icon =  new ResourceHandle(BuildInIconProviderName.ProviderName,"page")
    						}
    					};
    					element.PropertyBag.Add("DataId", pageId.ToString());
    					yield return element;
    				}
    				
    			}
    			yield break;
    		}
    
    		public IEnumerable<Element> GetRoots(SearchToken seachToken)
    		{
    			var element = new Element(Context.CreateElementHandle(new SampleEntityToken("RootId", "Root")))
    			{
    				VisualData = new ElementVisualizedData()
    				{
    					Label = "Root",
    					ToolTip = "Root",
    					HasChildren = true,
    					Icon = FolderIcon,
    					OpenedIcon = OpenFolderIcon
    				}
    			};
    			yield return element;
    		}
    	}
    }
    
    Download SampleElementProvider.cs
  4. Add another class (e.g. “SampleEntityToken”) and add the following code:
    using System.Collections.Generic;
    using Composite.C1Console.Security;
    using Composite.Plugins.Elements.ElementProviders.PageElementProvider;
    
    namespace Composite.Samples.CustomTreeControl.ElementProvider
    {
    	[SecurityAncestorProvider(typeof(SubsiteManagerEntityTokenAncestorProvider))]
    	public class SampleEntityToken : EntityToken
    	{
    		private string _id;
            private string _type;
    		private string _source;
    
    
    		public SampleEntityToken(string id, string type, string source = "")
    		{
    			_id = id;
    			_type = type;
    			_source = source;
    		}
    
    		public override string Id
    		{
    			get { return _id; }
    		}
    
    		public override string Serialize()
    		{
    			return DoSerialize();    
    		}
    
    		public override string Source
    		{
    			get { return _source; }
    		}
    
    		public override string Type
    		{
    			get { return _type; }
    		}
    
    		public static EntityToken Deserialize(string serializedData)
    		{
    			string type, source, id;
    
    			DoDeserialize(serializedData, out type, out source, out id);
    
    			return new SampleEntityToken(id, type, source);
    		}
    	}
    
    
    	internal sealed class SubsiteManagerEntityTokenAncestorProvider : ISecurityAncestorProvider
    	{
    		public IEnumerable<EntityToken> GetParents(EntityToken entityToken)
    		{
    			return new[] { new PageElementProviderEntityToken("PageElementProvider") }; 
    		}
    	}
    
    }
    
    Download SampleEntityToken.cs
  5. Build the solution.
  6. Copy the output DLL (e.g. “Composite.Samples.CustomTreeControl.dll”) to ~/Bin on your website.

You also need to register the tree element provider in C1 CMS (see below).

Some code highlights:

  • The security ancestor provider (SubsiteManagerEntityTokenAncestorProvider) implements the ISecurityAncestorProvider interface
  • An entity token is inherited from the abstract EntityToken class and and is assigned the implemented security ancestor provider (in the SecurityAncestorProvider attribute).
  • The example's entity token class is used for all types of tree elements in this example.
  • The SampleElementProvider implements the IHooklessElementProvider interface and creates a custom tree of elements (Please see "How to create a tree structure of elements programmatically?" for more information.)
  • The root contains two types of folders: those for "virtual" items and those for root pages.

Registering the custom control in Composite.config

To be able to use the custom text box (CustomTreeInput) you need to register it in Composite.config specifying the virtual path to it on the website:

  1. Edit ~/App_Data/Composite/Composite.config
  2. Locate this configuration element:

    /configuration/Composite.C1Console.Forms.Plugins.UiControlFactoryConfiguration/Channels/Channel[@name='AspNet.Management']/Namespaces/Namespace[@name='http://www.composite.net/ns/management/bindingforms/std.ui.controls.lib/1.0']/Factories
  3. Add this element below after the last <add/> element:

    <add 
    userControlVirtualPath="~/Composite/CustomControls/CustomTreeControl/controls/CustomTreeInput.ascx" 
    cacheCompiledUserControlType="false" 
    type="Composite.Plugins.Forms.WebChannel.UiControlFactories.TemplatedTextInputUiControlFactory, Composite" 
    name="CustomTreeInput" />
  4. Save the file.

The custom control (CustomTreeInput.ascx) has been registered as the ‘CustomTreeInput’ element, which now can be used in the form markup.

Registering the element provider in Composite.config

The tree element provider that appears the invoked dialog should be registered in Composite.config, too, specifying its full name (Composite.Samples.CustomTreeControl.ElementProvider.SampleElementProvider) and the assembly that contains it (Composite.Samples.CustomTreeControl):

  1. Edit ~/App_Data/Composite/Composite.config
  2. Locate this configuration element:

    /configuration/Composite.C1Console.Elements.Plugins.ElementProviderConfiguration/ElementProviderPlugins
  3. Add this element below after the last <add/> element:
    <add 
    type="Composite.Samples.CustomTreeControl.ElementProvider.SampleElementProvider, Composite.Samples.CustomTreeControl" 
    name="SampleElementProvider" />
  4. Save the file.

Using the custom text box with the tree element picker on a data form

You can quickly replace the default TextBox widget on a data form with the custom text box with a picker button editing the form's markup.

  1. Create a global data type.
  2. Add fields making sure at least one of which is of the String type. It will be used for the custom control.
  3. Edit the form markup of this data type.
  4. Locate the TextBox element you’ll be using for the custom control.
  5. Replace the ' TextBox’ in the control markup with ‘ CustomTreeInput’ so that it looks like this:
    <CustomTreeInput Label="Text" Help="" SpellCheck="true">
      <CustomTreeInput.Text>
        <cms:bind source="Text" />
      </CustomTreeInput.Text>
    </CustomTreeInput>
  1. Save the changes.

If you want to make the widget available for selection as a standard one in the data field editor, you need to create and register a widget provider for this custom widget.

Using the data form with the custom control and picker

  1. Add a data item of this global type.
  2. In the custom text box field, click the picker button. The dialog with a custom tree appears.
  3. Select a tree element and click OK. The value appears in the field on the Add Data form.
  4. Save the data item.

The dialog with a custom tree shows:

  • A few folder-like elements that contain a few item-like elements. These are built virtually, on the fly. When one selects such a data element, it appears in the corresponding field on a data form as the respective numbers of the folder and the item, separated with a comma: e.g. 2,3.
  • Under the “Pages” element, all the root pages available in C1 CMS at the moment. When one selects a page, its Page ID appears in the corresponding field on a data form.