diff --git a/src/Umbraco.Core/Trees/ITree.cs b/src/Umbraco.Core/Trees/ITree.cs
index 567accbd9e..7f7ff816be 100644
--- a/src/Umbraco.Core/Trees/ITree.cs
+++ b/src/Umbraco.Core/Trees/ITree.cs
@@ -3,7 +3,7 @@
// TODO: we don't really use this, it is nice to have the treecontroller, attribute and ApplicationTree streamlined to implement this but it's not used
// leave as internal for now, maybe we'll use in the future, means we could pass around ITree
// TODO: We should make this a thing, a tree should just be an interface *not* a controller
- internal interface ITree
+ public interface ITree
{
///
/// Gets or sets the sort order.
diff --git a/src/Umbraco.Core/Trees/Tree.cs b/src/Umbraco.Core/Trees/Tree.cs
index 4747d2495b..b528443eb1 100644
--- a/src/Umbraco.Core/Trees/Tree.cs
+++ b/src/Umbraco.Core/Trees/Tree.cs
@@ -45,7 +45,7 @@ namespace Umbraco.Web.Trees
///
public Type TreeControllerType { get; }
- internal static string GetRootNodeDisplayName(ITree tree, ILocalizedTextService textService)
+ public static string GetRootNodeDisplayName(ITree tree, ILocalizedTextService textService)
{
var label = $"[{tree.TreeAlias}]";
diff --git a/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs b/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs
index 2d0ffc5d33..56a4ca7def 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -6,25 +6,25 @@ using System.Net;
using System.Net.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Core;
- using Umbraco.Core.Configuration;
- using Umbraco.Core.Configuration.Legacy;
+using Umbraco.Core.Configuration;
+using Umbraco.Core.Configuration.Legacy;
using Umbraco.Core.IO;
- using Umbraco.Core.Mapping;
- using Umbraco.Core.Models;
+using Umbraco.Core.Mapping;
+using Umbraco.Core.Models;
using Umbraco.Core.Services;
- using Umbraco.Core.Strings;
- using Umbraco.Core.Strings.Css;
- using Umbraco.Extensions;
- using Umbraco.Web.Models.ContentEditing;
+using Umbraco.Core.Strings;
+using Umbraco.Core.Strings.Css;
+using Umbraco.Web.Models.ContentEditing;
using Stylesheet = Umbraco.Core.Models.Stylesheet;
using StylesheetRule = Umbraco.Web.Models.ContentEditing.StylesheetRule;
using Umbraco.Web.BackOffice.Filters;
- using Umbraco.Web.Common.ActionsResults;
- using Umbraco.Web.Common.Attributes;
+using Umbraco.Web.Common.ActionsResults;
+using Umbraco.Web.Common.Attributes;
using Umbraco.Web.Common.Exceptions;
using Umbraco.Web.Editors;
+using Umbraco.Web.BackOffice.Trees;
- namespace Umbraco.Web.BackOffice.Controllers
+namespace Umbraco.Web.BackOffice.Controllers
{
// TODO: Put some exception filters in our webapi to return 404 instead of 500 when we throw ArgumentNullException
// ref: https://www.exceptionnotfound.net/the-asp-net-web-api-exception-handling-pipeline-a-guided-tour/
diff --git a/src/Umbraco.Web.BackOffice/Trees/MenuRenderingEventArgs.cs b/src/Umbraco.Web.BackOffice/Trees/MenuRenderingEventArgs.cs
new file mode 100644
index 0000000000..74a557854f
--- /dev/null
+++ b/src/Umbraco.Web.BackOffice/Trees/MenuRenderingEventArgs.cs
@@ -0,0 +1,25 @@
+using Microsoft.AspNetCore.Http;
+using Umbraco.Web.Models.Trees;
+
+namespace Umbraco.Web.BackOffice.Trees
+{
+ public class MenuRenderingEventArgs : TreeRenderingEventArgs
+ {
+ ///
+ /// The tree node id that the menu is rendering for
+ ///
+ public string NodeId { get; private set; }
+
+ ///
+ /// The menu being rendered
+ ///
+ public MenuItemCollection Menu { get; private set; }
+
+ public MenuRenderingEventArgs(string nodeId, MenuItemCollection menu, FormCollection queryStrings)
+ : base(queryStrings)
+ {
+ NodeId = nodeId;
+ Menu = menu;
+ }
+ }
+}
diff --git a/src/Umbraco.Web.BackOffice/Trees/TreeAttribute.cs b/src/Umbraco.Web.BackOffice/Trees/TreeAttribute.cs
new file mode 100644
index 0000000000..ba24dea1c1
--- /dev/null
+++ b/src/Umbraco.Web.BackOffice/Trees/TreeAttribute.cs
@@ -0,0 +1,56 @@
+using System;
+using Umbraco.Web.Trees;
+
+namespace Umbraco.Web.BackOffice.Trees
+{
+ ///
+ /// Identifies a section tree.
+ ///
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
+ public class TreeAttribute : Attribute, ITree
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public TreeAttribute(string sectionAlias, string treeAlias)
+ {
+ SectionAlias = sectionAlias;
+ TreeAlias = treeAlias;
+ }
+
+ ///
+ /// Gets the section alias.
+ ///
+ public string SectionAlias { get; }
+
+ ///
+ /// Gets the tree alias.
+ ///
+ public string TreeAlias { get; }
+
+ ///
+ /// Gets or sets the tree title.
+ ///
+ public string TreeTitle { get; set; }
+
+ ///
+ /// Gets or sets the group of the tree.
+ ///
+ public string TreeGroup { get; set; }
+
+ ///
+ /// Gets the usage of the tree.
+ ///
+ public TreeUse TreeUse { get; set; } = TreeUse.Main | TreeUse.Dialog;
+
+ ///
+ /// Gets or sets the tree sort order.
+ ///
+ public int SortOrder { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the tree is a single-node tree (no child nodes, full screen app).
+ ///
+ public bool IsSingleNodeTree { get; set; }
+ }
+}
diff --git a/src/Umbraco.Web.BackOffice/Trees/TreeController.cs b/src/Umbraco.Web.BackOffice/Trees/TreeController.cs
new file mode 100644
index 0000000000..b28cf5843c
--- /dev/null
+++ b/src/Umbraco.Web.BackOffice/Trees/TreeController.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Collections.Concurrent;
+using Microsoft.Extensions.DependencyInjection;
+using Umbraco.Core;
+using Umbraco.Core.Services;
+using Umbraco.Web.Trees;
+
+namespace Umbraco.Web.BackOffice.Trees
+{
+ ///
+ /// The base controller for all tree requests
+ ///
+ public abstract class TreeController : TreeControllerBase
+ {
+ private static readonly ConcurrentDictionary _treeAttributeCache = new ConcurrentDictionary();
+
+ private readonly TreeAttribute _treeAttribute;
+
+ private readonly ILocalizedTextService _textService;
+
+ protected TreeController()
+ {
+ var serviceProvider = HttpContext.RequestServices;
+ _textService = serviceProvider.GetService();
+ _treeAttribute = GetTreeAttribute();
+ }
+
+ protected TreeController(ILocalizedTextService textService)
+ {
+ _textService = textService ?? throw new ArgumentNullException(nameof(textService));
+ _treeAttribute = GetTreeAttribute();
+ }
+
+ ///
+ public override string RootNodeDisplayName => Tree.GetRootNodeDisplayName(this, _textService);
+
+ ///
+ public override string TreeGroup => _treeAttribute.TreeGroup;
+
+ ///
+ public override string TreeAlias => _treeAttribute.TreeAlias;
+
+ ///
+ public override string TreeTitle => _treeAttribute.TreeTitle;
+
+ ///
+ public override TreeUse TreeUse => _treeAttribute.TreeUse;
+
+ ///
+ public override string SectionAlias => _treeAttribute.SectionAlias;
+
+ ///
+ public override int SortOrder => _treeAttribute.SortOrder;
+
+ ///
+ public override bool IsSingleNodeTree => _treeAttribute.IsSingleNodeTree;
+
+ private TreeAttribute GetTreeAttribute()
+ {
+ return _treeAttributeCache.GetOrAdd(GetType(), type =>
+ {
+ var treeAttribute = type.GetCustomAttribute(false);
+ if (treeAttribute == null)
+ throw new InvalidOperationException("The Tree controller is missing the " + typeof(TreeAttribute).FullName + " attribute");
+ return treeAttribute;
+ });
+ }
+ }
+}
diff --git a/src/Umbraco.Web.BackOffice/Trees/TreeControllerBase.cs b/src/Umbraco.Web.BackOffice/Trees/TreeControllerBase.cs
new file mode 100644
index 0000000000..ab0dacfe9a
--- /dev/null
+++ b/src/Umbraco.Web.BackOffice/Trees/TreeControllerBase.cs
@@ -0,0 +1,399 @@
+using System;
+using System.Linq;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Umbraco.Core;
+using Umbraco.Core.Events;
+using Umbraco.Core.Models;
+using Umbraco.Core.Models.Entities;
+using Umbraco.Core.Persistence;
+using Umbraco.Web.BackOffice.Controllers;
+using Umbraco.Web.Common.Filters;
+using Umbraco.Web.Common.ModelBinders;
+using Umbraco.Web.Models.Trees;
+using Umbraco.Web.Trees;
+using Umbraco.Web.WebApi;
+
+namespace Umbraco.Web.BackOffice.Trees
+{
+ ///
+ /// A base controller reference for non-attributed trees (un-registered).
+ ///
+ ///
+ /// Developers should generally inherit from TreeController.
+ ///
+ [TypeFilter(typeof(AngularJsonOnlyConfigurationAttribute))]
+ public abstract class TreeControllerBase : UmbracoAuthorizedApiController, ITree
+ {
+ // TODO: Need to set this, but from where?
+ // Presumably not injecting as this will be a base controller for package/solution developers.
+ private readonly UmbracoApiControllerTypeCollection _apiControllers;
+
+ protected TreeControllerBase()
+ {
+ }
+
+ ///
+ /// The method called to render the contents of the tree structure
+ ///
+ ///
+ ///
+ /// All of the query string parameters passed from jsTree
+ ///
+ ///
+ /// We are allowing an arbitrary number of query strings to be passed in so that developers are able to persist custom data from the front-end
+ /// to the back end to be used in the query for model data.
+ ///
+ protected abstract TreeNodeCollection GetTreeNodes(string id, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings);
+
+ ///
+ /// Returns the menu structure for the node
+ ///
+ ///
+ ///
+ ///
+ protected abstract MenuItemCollection GetMenuForNode(string id, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings);
+
+ ///
+ /// The name to display on the root node
+ ///
+ public abstract string RootNodeDisplayName { get; }
+
+ ///
+ public abstract string TreeGroup { get; }
+
+ ///
+ public abstract string TreeAlias { get; }
+
+ ///
+ public abstract string TreeTitle { get; }
+
+ ///
+ public abstract TreeUse TreeUse { get; }
+
+ ///
+ public abstract string SectionAlias { get; }
+
+ ///
+ public abstract int SortOrder { get; }
+
+ ///
+ public abstract bool IsSingleNodeTree { get; }
+
+ ///
+ /// Returns the root node for the tree
+ ///
+ ///
+ ///
+ public TreeNode GetRootNode([ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings)
+ {
+ if (queryStrings == null) queryStrings = FormCollection.Empty;
+ var node = CreateRootNode(queryStrings);
+
+ //add the tree alias to the root
+ node.AdditionalData["treeAlias"] = TreeAlias;
+
+ AddQueryStringsToAdditionalData(node, queryStrings);
+
+ //check if the tree is searchable and add that to the meta data as well
+ if (this is ISearchableTree)
+ node.AdditionalData.Add("searchable", "true");
+
+ //now update all data based on some of the query strings, like if we are running in dialog mode
+ if (IsDialog(queryStrings))
+ node.RoutePath = "#";
+
+ OnRootNodeRendering(this, new TreeNodeRenderingEventArgs(node, queryStrings));
+
+ return node;
+ }
+
+ ///
+ /// The action called to render the contents of the tree structure
+ ///
+ ///
+ ///
+ /// All of the query string parameters passed from jsTree
+ ///
+ /// JSON markup for jsTree
+ ///
+ /// We are allowing an arbitrary number of query strings to be passed in so that developers are able to persist custom data from the front-end
+ /// to the back end to be used in the query for model data.
+ ///
+ public TreeNodeCollection GetNodes(string id, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings)
+ {
+ if (queryStrings == null) queryStrings = FormCollection.Empty;
+ var nodes = GetTreeNodes(id, queryStrings);
+
+ foreach (var node in nodes)
+ AddQueryStringsToAdditionalData(node, queryStrings);
+
+ //now update all data based on some of the query strings, like if we are running in dialog mode
+ if (IsDialog(queryStrings))
+ foreach (var node in nodes)
+ node.RoutePath = "#";
+
+ //raise the event
+ OnTreeNodesRendering(this, new TreeNodesRenderingEventArgs(nodes, queryStrings));
+
+ return nodes;
+ }
+
+ ///
+ /// The action called to render the menu for a tree node
+ ///
+ ///
+ ///
+ ///
+ public MenuItemCollection GetMenu(string id, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings)
+ {
+ if (queryStrings == null) queryStrings = FormCollection.Empty;
+ var menu = GetMenuForNode(id, queryStrings);
+ //raise the event
+ OnMenuRendering(this, new MenuRenderingEventArgs(id, menu, queryStrings));
+ return menu;
+ }
+
+ ///
+ /// Helper method to create a root model for a tree
+ ///
+ ///
+ protected virtual TreeNode CreateRootNode(FormCollection queryStrings)
+ {
+ var rootNodeAsString = Constants.System.RootString;
+ queryStrings.TryGetValue(TreeQueryStringParameters.Application, out var currApp);
+
+ var node = new TreeNode(
+ rootNodeAsString,
+ null, //this is a root node, there is no parent
+ Url.GetTreeUrl(_apiControllers, GetType(), rootNodeAsString, queryStrings),
+ Url.GetMenuUrl(_apiControllers, GetType(), rootNodeAsString, queryStrings))
+ {
+ HasChildren = true,
+ RoutePath = currApp,
+ Name = RootNodeDisplayName
+ };
+
+ return node;
+ }
+
+ #region Create TreeNode methods
+
+ ///
+ /// Helper method to create tree nodes
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public TreeNode CreateTreeNode(string id, string parentId, FormCollection queryStrings, string title)
+ {
+ var jsonUrl = Url.GetTreeUrl(_apiControllers, GetType(), id, queryStrings);
+ var menuUrl = Url.GetMenuUrl(_apiControllers, GetType(), id, queryStrings);
+ var node = new TreeNode(id, parentId, jsonUrl, menuUrl) { Name = title };
+ return node;
+ }
+
+ ///
+ /// Helper method to create tree nodes
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public TreeNode CreateTreeNode(string id, string parentId, FormCollection queryStrings, string title, string icon)
+ {
+ var jsonUrl = Url.GetTreeUrl(_apiControllers, GetType(), id, queryStrings);
+ var menuUrl = Url.GetMenuUrl(_apiControllers, GetType(), id, queryStrings);
+ var node = new TreeNode(id, parentId, jsonUrl, menuUrl)
+ {
+ Name = title,
+ Icon = icon,
+ NodeType = TreeAlias
+ };
+ return node;
+ }
+
+ ///
+ /// Helper method to create tree nodes
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public TreeNode CreateTreeNode(string id, string parentId, FormCollection queryStrings, string title, string icon, string routePath)
+ {
+ var jsonUrl = Url.GetTreeUrl(_apiControllers, GetType(), id, queryStrings);
+ var menuUrl = Url.GetMenuUrl(_apiControllers, GetType(), id, queryStrings);
+ var node = new TreeNode(id, parentId, jsonUrl, menuUrl) { Name = title, RoutePath = routePath, Icon = icon };
+ return node;
+ }
+
+ ///
+ /// Helper method to create tree nodes and automatically generate the json url + UDI
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public TreeNode CreateTreeNode(IEntitySlim entity, Guid entityObjectType, string parentId, FormCollection queryStrings, bool hasChildren)
+ {
+ var contentTypeIcon = entity is IContentEntitySlim contentEntity ? contentEntity.ContentTypeIcon : null;
+ var treeNode = CreateTreeNode(entity.Id.ToInvariantString(), parentId, queryStrings, entity.Name, contentTypeIcon);
+ treeNode.Path = entity.Path;
+ treeNode.Udi = Udi.Create(ObjectTypes.GetUdiType(entityObjectType), entity.Key);
+ treeNode.HasChildren = hasChildren;
+ treeNode.Trashed = entity.Trashed;
+ return treeNode;
+ }
+
+ ///
+ /// Helper method to create tree nodes and automatically generate the json url + UDI
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public TreeNode CreateTreeNode(IUmbracoEntity entity, Guid entityObjectType, string parentId, FormCollection queryStrings, string icon, bool hasChildren)
+ {
+ var treeNode = CreateTreeNode(entity.Id.ToInvariantString(), parentId, queryStrings, entity.Name, icon);
+ treeNode.Udi = Udi.Create(ObjectTypes.GetUdiType(entityObjectType), entity.Key);
+ treeNode.Path = entity.Path;
+ treeNode.HasChildren = hasChildren;
+ return treeNode;
+ }
+
+ ///
+ /// Helper method to create tree nodes and automatically generate the json url
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public TreeNode CreateTreeNode(string id, string parentId, FormCollection queryStrings, string title, string icon, bool hasChildren)
+ {
+ var treeNode = CreateTreeNode(id, parentId, queryStrings, title, icon);
+ treeNode.HasChildren = hasChildren;
+ return treeNode;
+ }
+
+ ///
+ /// Helper method to create tree nodes and automatically generate the json url
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public TreeNode CreateTreeNode(string id, string parentId, FormCollection queryStrings, string title, string icon, bool hasChildren, string routePath)
+ {
+ var treeNode = CreateTreeNode(id, parentId, queryStrings, title, icon);
+ treeNode.HasChildren = hasChildren;
+ treeNode.RoutePath = routePath;
+ return treeNode;
+ }
+
+ ///
+ /// Helper method to create tree nodes and automatically generate the json url + UDI
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public TreeNode CreateTreeNode(string id, string parentId, FormCollection queryStrings, string title, string icon, bool hasChildren, string routePath, Udi udi)
+ {
+ var treeNode = CreateTreeNode(id, parentId, queryStrings, title, icon);
+ treeNode.HasChildren = hasChildren;
+ treeNode.RoutePath = routePath;
+ treeNode.Udi = udi;
+ return treeNode;
+ }
+
+ #endregion
+
+ ///
+ /// The AdditionalData of a node is always populated with the query string data, this method performs this
+ /// operation and ensures that special values are not inserted or that duplicate keys are not added.
+ ///
+ ///
+ ///
+ protected void AddQueryStringsToAdditionalData(TreeNode node, FormCollection queryStrings)
+ {
+ foreach (var q in queryStrings.Where(x => node.AdditionalData.ContainsKey(x.Key) == false))
+ node.AdditionalData.Add(q.Key, q.Value);
+ }
+
+ ///
+ /// If the request is for a dialog mode tree
+ ///
+ ///
+ ///
+ protected bool IsDialog(FormCollection queryStrings)
+ {
+ queryStrings.TryGetValue(TreeQueryStringParameters.Use, out var use);
+ return use == "dialog";
+ }
+
+ ///
+ /// An event that allows developers to modify the tree node collection that is being rendered
+ ///
+ ///
+ /// Developers can add/remove/replace/insert/update/etc... any of the tree items in the collection.
+ ///
+ public static event TypedEventHandler TreeNodesRendering;
+
+ private static void OnTreeNodesRendering(TreeControllerBase instance, TreeNodesRenderingEventArgs e)
+ {
+ var handler = TreeNodesRendering;
+ handler?.Invoke(instance, e);
+ }
+
+ ///
+ /// An event that allows developer to modify the root tree node that is being rendered
+ ///
+ public static event TypedEventHandler RootNodeRendering;
+
+ // internal for temp class below - kill eventually!
+ internal static void OnRootNodeRendering(TreeControllerBase instance, TreeNodeRenderingEventArgs e)
+ {
+ var handler = RootNodeRendering;
+ handler?.Invoke(instance, e);
+ }
+
+ ///
+ /// An event that allows developers to modify the menu that is being rendered
+ ///
+ ///
+ /// Developers can add/remove/replace/insert/update/etc... any of the tree items in the collection.
+ ///
+ public static event TypedEventHandler MenuRendering;
+
+ private static void OnMenuRendering(TreeControllerBase instance, MenuRenderingEventArgs e)
+ {
+ var handler = MenuRendering;
+ handler?.Invoke(instance, e);
+ }
+ }
+}
diff --git a/src/Umbraco.Web.BackOffice/Trees/TreeNodeRenderingEventArgs.cs b/src/Umbraco.Web.BackOffice/Trees/TreeNodeRenderingEventArgs.cs
new file mode 100644
index 0000000000..50d7b627d9
--- /dev/null
+++ b/src/Umbraco.Web.BackOffice/Trees/TreeNodeRenderingEventArgs.cs
@@ -0,0 +1,16 @@
+using Microsoft.AspNetCore.Http;
+using Umbraco.Web.Models.Trees;
+
+namespace Umbraco.Web.BackOffice.Trees
+{
+ public class TreeNodeRenderingEventArgs : TreeRenderingEventArgs
+ {
+ public TreeNode Node { get; private set; }
+
+ public TreeNodeRenderingEventArgs(TreeNode node, FormCollection queryStrings)
+ : base(queryStrings)
+ {
+ Node = node;
+ }
+ }
+}
diff --git a/src/Umbraco.Web.BackOffice/Trees/TreeNodesRenderingEventArgs.cs b/src/Umbraco.Web.BackOffice/Trees/TreeNodesRenderingEventArgs.cs
new file mode 100644
index 0000000000..8c9cfebd83
--- /dev/null
+++ b/src/Umbraco.Web.BackOffice/Trees/TreeNodesRenderingEventArgs.cs
@@ -0,0 +1,16 @@
+using Microsoft.AspNetCore.Http;
+using Umbraco.Web.Models.Trees;
+
+namespace Umbraco.Web.BackOffice.Trees
+{
+ public class TreeNodesRenderingEventArgs : TreeRenderingEventArgs
+ {
+ public TreeNodeCollection Nodes { get; private set; }
+
+ public TreeNodesRenderingEventArgs(TreeNodeCollection nodes, FormCollection queryStrings)
+ : base(queryStrings)
+ {
+ Nodes = nodes;
+ }
+ }
+}
diff --git a/src/Umbraco.Web.BackOffice/Trees/TreeQueryStringParameters.cs b/src/Umbraco.Web.BackOffice/Trees/TreeQueryStringParameters.cs
new file mode 100644
index 0000000000..80fba4bb34
--- /dev/null
+++ b/src/Umbraco.Web.BackOffice/Trees/TreeQueryStringParameters.cs
@@ -0,0 +1,13 @@
+namespace Umbraco.Web.BackOffice.Trees
+{
+ ///
+ /// Common query string parameters used for tree query strings
+ ///
+ internal struct TreeQueryStringParameters
+ {
+ public const string Use = "use";
+ public const string Application = "application";
+ public const string StartNodeId = "startNodeId";
+ public const string DataTypeKey = "dataTypeKey";
+ }
+}
diff --git a/src/Umbraco.Web.BackOffice/Trees/TreeRenderingEventArgs.cs b/src/Umbraco.Web.BackOffice/Trees/TreeRenderingEventArgs.cs
new file mode 100644
index 0000000000..a132e52dad
--- /dev/null
+++ b/src/Umbraco.Web.BackOffice/Trees/TreeRenderingEventArgs.cs
@@ -0,0 +1,15 @@
+using System;
+using Microsoft.AspNetCore.Http;
+
+namespace Umbraco.Web.BackOffice.Trees
+{
+ public class TreeRenderingEventArgs : EventArgs
+ {
+ public FormCollection QueryStrings { get; private set; }
+
+ public TreeRenderingEventArgs(FormCollection queryStrings)
+ {
+ QueryStrings = queryStrings;
+ }
+ }
+}
diff --git a/src/Umbraco.Web.BackOffice/Trees/UrlHelperExtensions.cs b/src/Umbraco.Web.BackOffice/Trees/UrlHelperExtensions.cs
index 3c94e3f9a0..eef45976f0 100644
--- a/src/Umbraco.Web.BackOffice/Trees/UrlHelperExtensions.cs
+++ b/src/Umbraco.Web.BackOffice/Trees/UrlHelperExtensions.cs
@@ -2,10 +2,14 @@
using System.Linq;
using System.Text;
using System.Web;
+using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Core;
+using Umbraco.Extensions;
+using Umbraco.Web.Common.Extensions;
+using Umbraco.Web.WebApi;
-namespace Umbraco.Extensions
+namespace Umbraco.Web.BackOffice.Trees
{
public static class UrlHelperExtensions
{
@@ -38,5 +42,28 @@ namespace Umbraco.Extensions
}
return sb.ToString().TrimEnd(",");
}
+
+ public static string GetTreeUrl(this IUrlHelper urlHelper, UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, Type treeType, string nodeId, FormCollection queryStrings)
+ {
+ var actionUrl = urlHelper.GetUmbracoApiService(umbracoApiControllerTypeCollection, "GetNodes", treeType)
+ .EnsureEndsWith('?');
+
+ //now we need to append the query strings
+ actionUrl += "id=" + nodeId.EnsureEndsWith('&') + queryStrings.ToQueryString("id",
+ //Always ignore the custom start node id when generating URLs for tree nodes since this is a custom once-only parameter
+ // that should only ever be used when requesting a tree to render (root), not a tree node
+ TreeQueryStringParameters.StartNodeId);
+ return actionUrl;
+ }
+
+ public static string GetMenuUrl(this IUrlHelper urlHelper, UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, Type treeType, string nodeId, FormCollection queryStrings)
+ {
+ var actionUrl = urlHelper.GetUmbracoApiService(umbracoApiControllerTypeCollection, "GetMenu", treeType)
+ .EnsureEndsWith('?');
+
+ //now we need to append the query strings
+ actionUrl += "id=" + nodeId.EnsureEndsWith('&') + queryStrings.ToQueryString("id");
+ return actionUrl;
+ }
}
}
diff --git a/src/Umbraco.Web.Common/Extensions/FormCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/FormCollectionExtensions.cs
new file mode 100644
index 0000000000..099c2416fc
--- /dev/null
+++ b/src/Umbraco.Web.Common/Extensions/FormCollectionExtensions.cs
@@ -0,0 +1,105 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Microsoft.AspNetCore.Http;
+using Umbraco.Core;
+using Umbraco.Web.Common.Extensions;
+
+namespace Umbraco.Web.Common.Extensions
+{
+ public static class FormCollectionExtensions
+ {
+ ///
+ /// Converts a dictionary object to a query string representation such as:
+ /// firstname=shannon&lastname=deminick
+ ///
+ ///
+ /// Any keys found in this collection will be removed from the output
+ ///
+ public static string ToQueryString(this FormCollection items, params string[] keysToIgnore)
+ {
+ if (items == null) return "";
+ if (items.Any() == false) return "";
+
+ var builder = new StringBuilder();
+ foreach (var i in items.Where(i => keysToIgnore.InvariantContains(i.Key) == false))
+ builder.Append(string.Format("{0}={1}&", i.Key, i.Value));
+ return builder.ToString().TrimEnd('&');
+ }
+
+ ///
+ /// Converts the FormCollection to a dictionary
+ ///
+ ///
+ ///
+ public static IDictionary ToDictionary(this FormCollection items)
+ {
+ return items.ToDictionary(x => x.Key, x => (object)x.Value);
+ }
+
+ ///
+ /// Returns the value of a mandatory item in the FormCollection
+ ///
+ ///
+ ///
+ ///
+ public static string GetRequiredString(this FormCollection items, string key)
+ {
+ if (items.HasKey(key) == false)
+ throw new ArgumentNullException("The " + key + " query string parameter was not found but is required");
+ return items.Single(x => x.Key.InvariantEquals(key)).Value;
+ }
+
+ ///
+ /// Checks if the collection contains the key
+ ///
+ ///
+ ///
+ ///
+ public static bool HasKey(this FormCollection items, string key)
+ {
+ return items.Any(x => x.Key.InvariantEquals(key));
+ }
+
+ ///
+ /// Returns the object based in the collection based on it's key. This does this with a conversion so if it doesn't convert a null object is returned.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static T GetValue(this FormCollection items, string key)
+ {
+ if (items.TryGetValue(key, out var val) == false || string.IsNullOrEmpty(val))
+ {
+ return default;
+ }
+
+ var converted = val.TryConvertTo();
+ return converted.Success
+ ? converted.Result
+ : default;
+ }
+
+ ///
+ /// Returns the object based in the collection based on it's key. This does this with a conversion so if it doesn't convert or the query string is no there an exception is thrown
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static T GetRequiredValue(this FormCollection items, string key)
+ {
+ if (items.TryGetValue(key, out var val) == false || string.IsNullOrEmpty(val))
+ {
+ throw new InvalidOperationException($"The required query string parameter {key} is missing");
+ }
+
+ var converted = val.TryConvertTo();
+ return converted.Success
+ ? converted.Result
+ : throw new InvalidOperationException($"The required query string parameter {key} cannot be converted to type {typeof(T)}");
+ }
+ }
+}
diff --git a/src/Umbraco.Web/FormDataCollectionExtensions.cs b/src/Umbraco.Web/FormDataCollectionExtensions.cs
index 52f86dcc59..3f2840c8ca 100644
--- a/src/Umbraco.Web/FormDataCollectionExtensions.cs
+++ b/src/Umbraco.Web/FormDataCollectionExtensions.cs
@@ -7,7 +7,7 @@ using Umbraco.Core;
namespace Umbraco.Web
{
-
+ // Migrated to .NET Core (as FormCollectionExtensions)
public static class FormDataCollectionExtensions
{
///
diff --git a/src/Umbraco.Web/Trees/MenuRenderingEventArgs.cs b/src/Umbraco.Web/Trees/MenuRenderingEventArgs.cs
index 078eb73026..bb7c43dcdb 100644
--- a/src/Umbraco.Web/Trees/MenuRenderingEventArgs.cs
+++ b/src/Umbraco.Web/Trees/MenuRenderingEventArgs.cs
@@ -3,6 +3,7 @@ using Umbraco.Web.Models.Trees;
namespace Umbraco.Web.Trees
{
+ // Migrated to .NET Core
public class MenuRenderingEventArgs : TreeRenderingEventArgs
{
///
diff --git a/src/Umbraco.Web/Trees/TreeAttribute.cs b/src/Umbraco.Web/Trees/TreeAttribute.cs
index 25921b06f8..1170de5cfa 100644
--- a/src/Umbraco.Web/Trees/TreeAttribute.cs
+++ b/src/Umbraco.Web/Trees/TreeAttribute.cs
@@ -5,6 +5,7 @@ namespace Umbraco.Web.Trees
///
/// Identifies a section tree.
///
+ /// // Migrated to .NET Core
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class TreeAttribute : Attribute, ITree
{
diff --git a/src/Umbraco.Web/Trees/TreeController.cs b/src/Umbraco.Web/Trees/TreeController.cs
index e76b45e623..730fd04bf2 100644
--- a/src/Umbraco.Web/Trees/TreeController.cs
+++ b/src/Umbraco.Web/Trees/TreeController.cs
@@ -14,6 +14,7 @@ namespace Umbraco.Web.Trees
///
/// The base controller for all tree requests
///
+ /// // Migrated to .NET Core
public abstract class TreeController : TreeControllerBase
{
private static readonly ConcurrentDictionary TreeAttributeCache = new ConcurrentDictionary();
diff --git a/src/Umbraco.Web/Trees/TreeControllerBase.cs b/src/Umbraco.Web/Trees/TreeControllerBase.cs
index 49f7eea845..18cb25c912 100644
--- a/src/Umbraco.Web/Trees/TreeControllerBase.cs
+++ b/src/Umbraco.Web/Trees/TreeControllerBase.cs
@@ -27,6 +27,7 @@ namespace Umbraco.Web.Trees
///
/// Developers should generally inherit from TreeController.
///
+ /// Migrated to .NET Core
[AngularJsonOnlyConfiguration]
public abstract class TreeControllerBase : UmbracoAuthorizedApiController, ITree
{
diff --git a/src/Umbraco.Web/Trees/TreeNodeRenderingEventArgs.cs b/src/Umbraco.Web/Trees/TreeNodeRenderingEventArgs.cs
index 30ef008cf7..1914d427e9 100644
--- a/src/Umbraco.Web/Trees/TreeNodeRenderingEventArgs.cs
+++ b/src/Umbraco.Web/Trees/TreeNodeRenderingEventArgs.cs
@@ -3,6 +3,7 @@ using Umbraco.Web.Models.Trees;
namespace Umbraco.Web.Trees
{
+ // Migrated to .NET Core
public class TreeNodeRenderingEventArgs : TreeRenderingEventArgs
{
public TreeNode Node { get; private set; }
diff --git a/src/Umbraco.Web/Trees/TreeNodesRenderingEventArgs.cs b/src/Umbraco.Web/Trees/TreeNodesRenderingEventArgs.cs
index 9eaaad18ee..753e727b05 100644
--- a/src/Umbraco.Web/Trees/TreeNodesRenderingEventArgs.cs
+++ b/src/Umbraco.Web/Trees/TreeNodesRenderingEventArgs.cs
@@ -3,6 +3,7 @@ using Umbraco.Web.Models.Trees;
namespace Umbraco.Web.Trees
{
+ // Migrated to .NET Core
public class TreeNodesRenderingEventArgs : TreeRenderingEventArgs
{
public TreeNodeCollection Nodes { get; private set; }
diff --git a/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs b/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs
index 02a198401b..130f4c2486 100644
--- a/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs
+++ b/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs
@@ -3,6 +3,7 @@
///
/// Common query string parameters used for tree query strings
///
+ /// Migrated to .NET Core
internal struct TreeQueryStringParameters
{
public const string Use = "use";
diff --git a/src/Umbraco.Web/Trees/TreeRenderingEventArgs.cs b/src/Umbraco.Web/Trees/TreeRenderingEventArgs.cs
index d8b67890a3..3f26c8cf50 100644
--- a/src/Umbraco.Web/Trees/TreeRenderingEventArgs.cs
+++ b/src/Umbraco.Web/Trees/TreeRenderingEventArgs.cs
@@ -3,6 +3,7 @@ using System.Net.Http.Formatting;
namespace Umbraco.Web.Trees
{
+ // Migrated to .NET Core
public class TreeRenderingEventArgs : EventArgs
{
public FormDataCollection QueryStrings { get; private set; }
diff --git a/src/Umbraco.Web/Trees/UrlHelperExtensions.cs b/src/Umbraco.Web/Trees/UrlHelperExtensions.cs
index f4b516a764..52f7668aef 100644
--- a/src/Umbraco.Web/Trees/UrlHelperExtensions.cs
+++ b/src/Umbraco.Web/Trees/UrlHelperExtensions.cs
@@ -8,6 +8,7 @@ using Umbraco.Core;
namespace Umbraco.Web.Trees
{
+ // Migrated to .NET Core
public static class UrlHelperExtensions
{
public static string GetTreeUrl(this UrlHelper urlHelper, Type treeType, string nodeId, FormDataCollection queryStrings)