Sortable data

last modified: 15 April 2015

NOTE: This solution only works for custom perspectives (through tree definitions).

Inspired by the Sortable Package from the contrib project, I added the IGenericSortable interface into C1. Static classes are now sortable from the console, just add the IGenericSortable interface on your static data class.

Using the the built in cut/paste flow you can move around data items into the order you desire. This only makes real sense if it was possible to make the datafolders in tree definitions honor that sorting order so I extended the DataFolderElementsTreeNode class to use the sortorder when the IGenericSortable interface is on the selected FieldGroupingName.

TL;DR

Below are a lot of classes and code to add/update within your Composite C1 project.
Remember this is intended for data that you expose through a (custom) tree definition - this will not work from the data perspective or with data attached to a page.
Here's a short version so you'll know what's coming:

  1. Modify the element class
  2. Add the IGenericSortable interface
  3. Modify the TreeNodeExtensionMethods class
  4. Modify the TreeNodeCreatorFactory class
  5. Modify the DataFolderElementsTreeNode class
  6. Add the SortableElementProvider class
  7. Modify the TreeServices.asmx class
  8. Modify the Tree.xsd

Modifying the Element class

To make it all possible we first have to modify the Element class in 'Composite/C1Console/Elements/'. In an other class we are going to modify the MovabilityInfo property but in its current state the setter is private.

public ElementDragAndDropInfo MovabilityInfo { get; /*private*/ set; }

IGenericSortable

Before we can use/modify the other classes we need to make Composite know about the IGenericSortable interface. This one is copied straight from the Contrib project source. Add it to the 'Composite/Data/Types/' folder.

/// <exclude></exclude>
public interface IGenericSortable : IData
{
		/// <exclude></exclude>
		[StoreFieldType(PhysicalStoreFieldType.Integer)]
		[ImmutableFieldId("b90baf57-f085-41e8-af18-3d470a752f5f")]
		[TreeOrdering(999)]
		int LocalOrdering { get; set; }
}

TreeNodeExtensions

If we want to make the data items sortable within the console we need to tell the tree items that its allowed. This is done by adding 'MovabilityInfo' to the element.
This needs to be on the element itself so 'Cut' is enabled in the context menu. It also has to be enabled on the grouping node that the element belongs to so 'Paste' is enabled.

We do this in the '/Composite/C1Console/Trees/TreeNodeExtensionMethods.cs', in the 'GetElements' method:

public static IEnumerable<Element> GetElements(this IEnumerable<TreeNode> treeNodes, EntityToken parentEntityToken, TreeNodeDynamicContext dynamicContext)
{
	List<Element> elements = null;
	foreach (TreeNode treeNode in treeNodes)
	{
		if (elements == null)
		{
			elements = treeNode.GetElements(parentEntityToken, dynamicContext).ToList();
		}
		else
		{
			elements = elements.Concat(treeNode.GetElements(parentEntityToken, dynamicContext)).ToList();
		}

		// Check if the nodes implement the IGenericSortable interface. We are only interested in nodes of type DataElementsTreeNode
		if (treeNode is DataElementsTreeNode)
		{
			// Cast
			var datanode = treeNode as DataElementsTreeNode;
			// Get the interface type
			var type = datanode.InterfaceType;
			// Does it implement IGenericSortable
			if (type.GetInterfaces().Contains(typeof(IGenericSortable)))
			{
				// Find all elements that have this node as the parent
				var elementsForParent = from e in elements
										where e.ElementHandle.EntityToken.Type.IndexOf(type.ToString()) != -1
										select e;
				// Set MovabilityInfo
				foreach (Element element in elementsForParent)
				{
					// Add the MovabilityInfo to the element (if we found one)
					if (element != null && element.MovabilityInfo.DragType == null)
					{
						// Yep, update the MovabilityInfo
						var MovabilityInfo = new ElementDragAndDropInfo(type);
						MovabilityInfo.AddDropType(type);
						MovabilityInfo.SupportsIndexedPosition = true;
						// Set it on the element
						element.MovabilityInfo = MovabilityInfo;
						// Make the system know that we are sortable
						element.PropertyBag.Add("IsSortable", "true");
					}
				}
			}
		}

		// If we are a grouping node check if its children have the IGenericSortable interface
		if (treeNode.ChildNodes.Count() > 0)
		{
			// It's a treenode - so all it's children should be the same type - we only have to check the first item
			var first = treeNode.ChildNodes.First();
			// Is it a DataElementsTreeNode
			if (first is DataElementsTreeNode)
			{
				// Cast
				var datanode = first as DataElementsTreeNode;
				// Get the interface type
				var type = datanode.InterfaceType;
				// Does it implement IGenericSortable
				if (type.GetInterfaces().Contains(typeof(IGenericSortable)))
				{
					// Find the matching element
					Element matchingElement = (from e in elements
											   where e.ElementHandle.EntityToken.Id == datanode.ParentNode.Id
											   select e).FirstOrDefault();

					// Add the MovabilityInfo to the element (if we found one)
					if (matchingElement != null && matchingElement.MovabilityInfo.DragType == null)
					{
						// Yep, update the MovabilityInfo
						var MovabilityInfo = new ElementDragAndDropInfo(type);
						MovabilityInfo.AddDropType(type);
						MovabilityInfo.SupportsIndexedPosition = true;
						// Set it on the element
						matchingElement.MovabilityInfo = MovabilityInfo;
						// Make the system know that we are sortable
						matchingElement.PropertyBag.Add("IsSortable", "true");
					}
				}
			}
		}
	}

	if (elements == null)
	{
		elements = new List<Element>();
	}

	return elements;
}

TreeNodeCreatorFactory

For Composite C1 to know about our 'GenericSortable' property in tree definitions we need to modify the 'Composite\C1Console\Trees\Foundation\TreeNodeCreatorFactory.cs' class. Add the 'genericSortableAttribute' in the 'BuildDataFolderElementsTreeNode' method on line 72:

private static TreeNode BuildDataFolderElementsTreeNode(XElement element, Tree tree)
{
    XAttribute typeAttribute = element.Attribute("Type");
    XAttribute fieldGroupingNameAttribute = element.Attribute("FieldGroupingName");
    XAttribute dateFormatAttribute = element.Attribute("DateFormat");
    XAttribute iconAttribute = element.Attribute("Icon");
    XAttribute rangeAttribute = element.Attribute("Range");
    XAttribute firstLetterOnlyAttribute = element.Attribute("FirstLetterOnly");
    XAttribute showForeignItemsAttribute = element.Attribute("ShowForeignItems");
    XAttribute leafDisplayAttribute = element.Attribute("Display");
    XAttribute sortDirectionAttribute = element.Attribute("SortDirection");
    XAttribute genericSortableAttribute = element.Attribute("GenericSortable");

    if (fieldGroupingNameAttribute == null)
    {
        tree.AddValidationError(element, "TreeValidationError.Common.MissingAttribute", "FieldGroupingName");
        return null;
    }

    Type interfaceType = null;
    if (typeAttribute != null)
    {
        interfaceType = TypeManager.TryGetType(typeAttribute.Value);
        if (interfaceType == null)
        {
            tree.AddValidationError(element, "TreeValidationError.Common.UnkownInterfaceType", typeAttribute.Value);
            return null;
        }
    }

    bool firstLetterOnly = false;
    if (firstLetterOnlyAttribute != null)
    {
        if (firstLetterOnlyAttribute.TryGetBoolValue(out firstLetterOnly) == false)
        {
            tree.AddValidationError(element, "TreeValidationError.Common.WrongAttributeValue", "FirstLetterOnly");
        }
    }

    LeafDisplayMode leafDisplay = LeafDisplayModeHelper.ParseDisplayMode(leafDisplayAttribute, tree);
    SortDirection sortDirection = ParseSortDirection(sortDirectionAttribute, tree);

    return new DataFolderElementsTreeNode
    {
        Tree = tree,
        Id = tree.BuildProcessContext.CreateNewNodeId(),
        InterfaceType = interfaceType,
        Icon = FactoryHelper.GetIcon(iconAttribute.GetValueOrDefault(DefaultDataGroupingFolderResourceName)),
        FieldName = fieldGroupingNameAttribute.Value,
        DateFormat = dateFormatAttribute.GetValueOrDefault(null),
        Range = rangeAttribute.GetValueOrDefault(null),
        FirstLetterOnly = firstLetterOnly,
        ShowForeignItems = showForeignItemsAttribute.GetValueOrDefault("true").ToLowerInvariant() == "true",
        Display = leafDisplay,
        SortDirection = sortDirection,
        GenericSortable = genericSortableAttribute.GetValueOrDefault("true").ToLowerInvariant() == "true"
    };
}

DataFolderElementsTreeNode

We want to make Composite honor the sorting order on DataFolderElements (currently not possible as they only support the label). But we do not want to sort on the 'LocalOrdering' value by default so we add a property to'Composite/C1Console/Trees/DataFolderElementsTreeNode.cs':

/// <exclude />
public bool GenericSortable { get; internal set; }         // Optional

With this property in place we can modify the 'CreateSimpleElements' method:

private IEnumerable<Element> CreateSimpleElements(EntityToken parentEntityToken, Type referenceType, object referenceValue, TreeNodeDynamicContext dynamicContext, IEnumerable<object> objects)
{
	bool shouldBeSortedByLabel;
	Func<object, string> labelFunc = GetLabelFunction(out shouldBeSortedByLabel);

	if (shouldBeSortedByLabel)
	{
		objects = objects.Evaluate();
	}

	var objectsAndLabels = objects.Select(o => new { Object = o, Label = labelFunc(o) });

	if (shouldBeSortedByLabel)
	{
		// We are default sorting on the label...but we might be IGenericSortable
		var referenceProperties = DataAttributeFacade.GetDataReferenceProperties(PropertyInfo.DeclaringType);
		var referenceInfo = referenceProperties.FirstOrDefault(p => p.SourcePropertyName == this.PropertyInfo.Name);

		// Check for IGenericSortable type
		if (GenericSortable && referenceInfo != null && referenceInfo.TargetType.GetInterfaces().Contains(typeof(IGenericSortable)))
		{
			// Yep, sort by local ordering
			objectsAndLabels = this.SortDirection == SortDirection.Ascending
				? objects.OrderBy(o => ((IGenericSortable)DataFacade.TryGetDataByUniqueKey(referenceInfo.TargetType, o)).LocalOrdering).Select(o => new { Object = o, Label = labelFunc(o) })
				: objects.OrderByDescending(o => ((IGenericSortable)DataFacade.TryGetDataByUniqueKey(referenceInfo.TargetType, o)).LocalOrdering).Select(o => new { Object = o, Label = labelFunc(o) });
		}
		else
		{
			objectsAndLabels = this.SortDirection == SortDirection.Ascending
				? objectsAndLabels.OrderBy(a => a.Label)
				: objectsAndLabels.OrderByDescending(a => a.Label);
		}
	}

	foreach (var pair in objectsAndLabels)
	{
		Element element = CreateElement(
			parentEntityToken,
			referenceType,
			referenceValue,
			dynamicContext,
			pair.Label,
			f => f.GroupingValues.Add(this.GroupingValuesFieldName, this.DateTimeFormater.Serialize(pair.Object))
		);

		yield return element;
	}
}

SortableElementProvider

When an element has been moved ('Cut/Paste') in the console, the LocalOrdering property of the data item should be updated. That's what this class does - it's called from the TreeServices.asmx class:

namespace nuFaqtz.Core.ActionProviders
{
	#region [==== Using ================================]
	using Composite.C1Console.Events;
	using Composite.C1Console.Security;
	using Composite.Data;
	using Composite.Data.Transactions;
	using Composite.Data.Types;
	using System;
	using System.Collections.Generic;
	using System.Linq;
	using System.Transactions;
	#endregion

	/// <summary>
	/// 
	/// </summary>
	public class SortableElementProvider
	{
		/// <summary>
		/// Re-orders data for the element dragged and dropped, i.e. updates the LocalOrdering property of all items of this type.
		/// </summary>
		/// <param name="draggedElementProviderName">Name of the dragged element provider.</param>
		/// <param name="draggedElementSerializedEntityToken">The dragged element serialized entity token.</param>
		/// <param name="draggedElementPiggybag">The dragged element piggybag.</param>
		/// <param name="newParentElementProviderName">New name of the parent element provider.</param>
		/// <param name="newParentElementSerializedEntityToken">The new parent element serialized entity token.</param>
		/// <param name="newParentElementPiggybag">The new parent element piggybag.</param>
		/// <param name="dropIndex">Index of the drop.</param>
		/// <param name="consoleId">The console identifier.</param>
		/// <param name="isCopy">if set to <c>true</c> [is copy].</param>
		/// <returns></returns>
		/// <exception cref="System.InvalidOperationException">Only drag'n'drop enabled elements are allowed</exception>
		public static bool ExecuteElementDraggedAndDropped(string draggedElementProviderName, string draggedElementSerializedEntityToken, string draggedElementPiggybag, string newParentElementProviderName, string newParentElementSerializedEntityToken, string newParentElementPiggybag, int dropIndex, string consoleId, bool isCopy)
		{
			if (draggedElementProviderName != newParentElementProviderName)
			{
				throw new InvalidOperationException("Only drag'n'drop enabled elements are allowed");
			}

			// Create token and handle for the dragged element
			EntityToken draggedElementEntityToken = EntityTokenSerializer.Deserialize(draggedElementSerializedEntityToken);

			// Are we moving (the only one we allow)
			if (!isCopy)
			{
				// Get the type (must be DataEntityToken else we wouldn't be here)
				var token = draggedElementEntityToken as DataEntityToken;
				var type = token.InterfaceType;
				// Do it for both relevant scopes
				foreach (var dataScope in new[] { DataScopeIdentifier.Administrated, DataScopeIdentifier.Public })
				{
					using (new DataScope(dataScope))
					{
						// Get all items of type
						var instances = DataFacade.GetData(type).Cast<IGenericSortable>().OrderBy(e => e.LocalOrdering).ToList();
						var draggedElement = (IGenericSortable)DataFacade.TryGetDataByUniqueKey(type, token.Data.GetUniqueKey());

						// Remove the draggedelement from the list
						instances.Remove(draggedElement);

						using (TransactionScope transationScope = TransactionsFacade.CreateNewScope())
						{
							// Loop the list and update local ordering
							var toBeUpdated = new List<IData>();
							// Update the dragged item
							draggedElement.LocalOrdering = dropIndex;
							toBeUpdated.Add(draggedElement);
							// Loop all instances
							for (int i = 0; i < instances.Count; i++)
							{
								// Get new sortorder
								int newSortOrder = i < dropIndex ? i : i + 1;
								// Validate new order
								if (instances[i].LocalOrdering != newSortOrder)
								{
									// Update the localordering 
									instances[i].LocalOrdering = newSortOrder;
									// Add to the update list
									toBeUpdated.Add(instances[i]);
								}
							}

							// Update the list at once
							DataFacade.Update(toBeUpdated);

							// We're done;
							transationScope.Complete();
						}
					}
				}
			}
			else
			{
				throw new NotImplementedException();
			}

			// Update tree
			var graph = new RelationshipGraph(draggedElementEntityToken, RelationshipGraphSearchOption.Both);
			if (graph.Levels.Count() > 1)
			{
				var level = graph.Levels.ElementAt(1);
				foreach (var token in level.AllEntities)
				{
					var consoleMessageQueueItem = new RefreshTreeMessageQueueItem
					{
						EntityToken = token
					};
					ConsoleMessageQueueFacade.Enqueue(consoleMessageQueueItem, consoleId);
				}
			}

			return true;
		}
	}
}

TreeServices

We need to tell Composite about our SortableElementProvider. When a element is dragged and dropped the console (top.js) calls the TreeServices web service to signal the action. With this modification in 'Website/Composite/services/Tree/TreeServices.asmx' we handle the update within our previously created SortableElementProvider:

// Check if we have an IGenericSortable interface on the token
if (draggedClientElement.PropertyBag.Where(e => e.Key == "IsSortable").Any())
{
	return nuFaqtz.Core.ActionProviders.SortableElementProvider.ExecuteElementDraggedAndDropped(draggedClientElement.ProviderName, draggedClientElement.EntityToken, draggedClientElement.Piggybag, newParentClientElement.ProviderName, newParentClientElement.EntityToken, newParentClientElement.Piggybag, dropIndex, consoleId, isCopy);
}
else
{
	return TreeServicesFacade.ExecuteElementDraggedAndDropped(draggedClientElement.ProviderName, draggedClientElement.EntityToken, draggedClientElement.Piggybag, newParentClientElement.ProviderName, newParentClientElement.EntityToken, newParentClientElement.Piggybag, dropIndex, consoleId, isCopy);
}

Tree.xsd

The Tree.xsd formally describes the elements and its allowed properties in an Tree definition. We need to tell Composite about the existence of the GenericSortable property:

<xs:complexType name="DataFolderElementType">
    <xs:all>
      <xs:element name="Children" type="DataFolderChildrenType" minOccurs="0" maxOccurs="1"/>
      <xs:element name="Actions" type="DataActionsType" minOccurs="0" maxOccurs="1"/>
    </xs:all>

    <xs:attributeGroup ref="DataFolderElementCommonGroup" />

    <xs:attribute name="ShowForeignItems" use="optional" type="xs:boolean" default="true">
      <xs:annotation>
        <xs:documentation>When this is set to true, data items not yet localized are shown with a localize action.</xs:documentation>
      </xs:annotation>
    </xs:attribute>
    
    <xs:attributeGroup ref="DisplayGroup" />

    <xs:attributeGroup ref="SortDirection" />

	  <xs:attribute name="GenericSortable" use="optional" type="xs:boolean">
		  <xs:annotation>
			  <xs:documentation>This attribute will make the folder sort on its FieldGroupingName when the underlying type has the IGenericSortable interface</xs:documentation>
		  </xs:annotation>
	  </xs:attribute>
</xs:complexType>

Remarks, errors or questions

If you have any remarks, notice any errors or have questions drop me a line on twitter.

published on: 31 January 2015