diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ISecuritySection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ISecuritySection.cs index e170f9fb2a..11eab07ec2 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/ISecuritySection.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ISecuritySection.cs @@ -14,7 +14,7 @@ string AuthCookieName { get; } string AuthCookieDomain { get; } - + /// /// A boolean indicating that by default the email address will be the username /// diff --git a/src/Umbraco.Core/Constants-Examine.cs b/src/Umbraco.Core/Constants-Examine.cs index e701b4ea12..1356e55775 100644 --- a/src/Umbraco.Core/Constants-Examine.cs +++ b/src/Umbraco.Core/Constants-Examine.cs @@ -18,7 +18,7 @@ /// The alias of the external content indexer /// public const string ExternalIndexer = "ExternalIndexer"; - + /// /// The alias of the internal member searcher /// diff --git a/src/Umbraco.Core/DateTimeExtensions.cs b/src/Umbraco.Core/DateTimeExtensions.cs index bb993b8bc9..d82ec99c6a 100644 --- a/src/Umbraco.Core/DateTimeExtensions.cs +++ b/src/Umbraco.Core/DateTimeExtensions.cs @@ -42,7 +42,7 @@ namespace Umbraco.Core Minute, Second } - + /// /// Calculates the number of minutes from a date time, on a rolling daily basis (so if /// date time is before the time, calculate onto next day) @@ -57,10 +57,10 @@ namespace Umbraco.Core { scheduledTime = "0" + scheduledTime; } - + var scheduledHour = int.Parse(scheduledTime.Substring(0, 2)); var scheduledMinute = int.Parse(scheduledTime.Substring(2)); - + DateTime scheduledDateTime; if (IsScheduledInRemainingDay(fromDateTime, scheduledHour, scheduledMinute)) { @@ -71,10 +71,10 @@ namespace Umbraco.Core var nextDay = fromDateTime.AddDays(1); scheduledDateTime = new DateTime(nextDay.Year, nextDay.Month, nextDay.Day, scheduledHour, scheduledMinute, 0); } - + return (int)(scheduledDateTime - fromDateTime).TotalMinutes; } - + private static bool IsScheduledInRemainingDay(DateTime fromDateTime, int scheduledHour, int scheduledMinute) { return scheduledHour > fromDateTime.Hour || (scheduledHour == fromDateTime.Hour && scheduledMinute >= fromDateTime.Minute); diff --git a/src/Umbraco.Core/Persistence/Migrations/IMigrationContext.cs b/src/Umbraco.Core/Persistence/Migrations/IMigrationContext.cs index 30e3ac52b8..620dc8ae03 100644 --- a/src/Umbraco.Core/Persistence/Migrations/IMigrationContext.cs +++ b/src/Umbraco.Core/Persistence/Migrations/IMigrationContext.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.Persistence.Migrations ILogger Logger { get; } ILocalMigration GetLocalMigration(); - + ISqlContext SqlContext { get; } } } diff --git a/src/Umbraco.Core/Persistence/Querying/CachedExpression.cs b/src/Umbraco.Core/Persistence/Querying/CachedExpression.cs index 7c7a6ccdbd..76547265b9 100644 --- a/src/Umbraco.Core/Persistence/Querying/CachedExpression.cs +++ b/src/Umbraco.Core/Persistence/Querying/CachedExpression.cs @@ -20,7 +20,7 @@ namespace Umbraco.Core.Persistence.Querying /// public string VisitResult { - get { return _visitResult; } + get => _visitResult; set { if (Visited) diff --git a/src/Umbraco.Core/Persistence/Querying/PocoToSqlExpressionVisitor.cs b/src/Umbraco.Core/Persistence/Querying/PocoToSqlExpressionVisitor.cs index 6846ea1e30..2b0350d8c7 100644 --- a/src/Umbraco.Core/Persistence/Querying/PocoToSqlExpressionVisitor.cs +++ b/src/Umbraco.Core/Persistence/Querying/PocoToSqlExpressionVisitor.cs @@ -20,6 +20,41 @@ namespace Umbraco.Core.Persistence.Querying _pd = sqlContext.PocoDataFactory.ForType(typeof(TDto)); } + protected override string VisitMethodCall(MethodCallExpression m) + { + var declaring = m.Method.DeclaringType; + if (declaring != typeof (SqlTemplate)) + return base.VisitMethodCall(m); + + if (m.Method.Name != "Arg" && m.Method.Name != "ArgIn") + throw new NotSupportedException($"Method SqlTemplate.{m.Method.Name} is not supported."); + + var parameters = m.Method.GetParameters(); + if (parameters.Length != 1 || parameters[0].ParameterType != typeof (string)) + throw new NotSupportedException($"Method SqlTemplate.{m.Method.Name}({string.Join(", ", parameters.Select(x => x.ParameterType))} is not supported."); + + var arg = m.Arguments[0]; + string name; + if (arg.NodeType == ExpressionType.Constant) + { + name = arg.ToString(); + } + else + { + // though... we probably should avoid doing this + var member = Expression.Convert(arg, typeof (object)); + var lambda = Expression.Lambda>(member); + var getter = lambda.Compile(); + name = getter().ToString(); + } + + SqlParameters.Add(RemoveQuote(name)); + + return Visited + ? string.Empty + : $"@{SqlParameters.Count - 1}"; + } + protected override string VisitMemberAccess(MemberExpression m) { if (m.Expression != null && m.Expression.NodeType == ExpressionType.Parameter && m.Expression.Type == typeof(TDto)) @@ -95,6 +130,10 @@ namespace Umbraco.Core.Persistence.Querying _parameterName1 = lambda.Parameters[0].Name; _parameterName2 = lambda.Parameters[1].Name; } + else + { + _parameterName1 = _parameterName2 = null; + } return base.VisitLambda(lambda); } diff --git a/src/Umbraco.Core/Persistence/SqlTemplate.cs b/src/Umbraco.Core/Persistence/SqlTemplate.cs index ffe0ca99e1..3dc3b7389f 100644 --- a/src/Umbraco.Core/Persistence/SqlTemplate.cs +++ b/src/Umbraco.Core/Persistence/SqlTemplate.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using NPoco; @@ -29,22 +30,61 @@ namespace Umbraco.Core.Persistence // must pass the args in the proper order, faster public Sql Sql(params object[] args) { - return new Sql(_sqlContext, _sql, args); + // if the type is an "unspeakable name" it is an anonymous compiler-generated object + // see https://stackoverflow.com/questions/9256594 + // => assume it's an anonymous type object containing named arguments + // (of course this means we cannot use *real* objects here and need SqlNamed - bah) + if (args.Length == 1 && args[0].GetType().Name.Contains("<")) + return SqlNamed(args[0]); + + if (args.Length != _args.Count) + throw new ArgumentException("Invalid number of arguments.", nameof(args)); + + if (args.Length == 0) + return new Sql(_sqlContext, true, _sql); + + var isBuilt = !args.Any(x => x is IEnumerable); + return new Sql(_sqlContext, isBuilt, _sql, args); } // can pass named args, slower // so, not much different from what Where(...) does (ie reflection) public Sql SqlNamed(object nargs) { + var isBuilt = true; var args = new object[_args.Count]; var properties = nargs.GetType().GetProperties().ToDictionary(x => x.Name, x => x.GetValue(nargs)); for (var i = 0; i < _args.Count; i++) { if (!properties.TryGetValue(_args[i], out var value)) - throw new InvalidOperationException($"Invalid argument name \"{_args[i]}\"."); + throw new InvalidOperationException($"Missing argument \"{_args[i]}\"."); args[i] = value; + properties.Remove(_args[i]); + + // if value is enumerable then we'll need to expand arguments + if (value is IEnumerable) + isBuilt = false; } - return new Sql(_sqlContext, _sql, args); + if (properties.Count > 0) + throw new InvalidOperationException($"Unknown argument{(properties.Count > 1 ? "s" : "")}: {string.Join(", ", properties.Keys)}"); + return new Sql(_sqlContext, isBuilt, _sql, args); + } + + internal void WriteToConsole() + { + new Sql(_sqlContext, _sql, _args.Values.Cast().ToArray()).WriteToConsole(); + } + + public static T Arg(string name) + { + return default (T); + } + + public static IEnumerable ArgIn(string name) + { + // don't return an empty enumerable, as it breaks NPoco + // fixme - should we cache these arrays? + return new[] { default (T) }; } } } diff --git a/src/Umbraco.Tests.Benchmarks/Program.cs b/src/Umbraco.Tests.Benchmarks/Program.cs index d392c09992..9687225a46 100644 --- a/src/Umbraco.Tests.Benchmarks/Program.cs +++ b/src/Umbraco.Tests.Benchmarks/Program.cs @@ -15,6 +15,7 @@ namespace Umbraco.Tests.Benchmarks //typeof(DeepCloneBenchmarks), typeof(XmlPublishedContentInitBenchmarks), typeof(CtorInvokeBenchmarks), + typeof(SqlTemplatesBenchmark), }); switcher.Run(args); diff --git a/src/Umbraco.Tests.Benchmarks/SqlTemplatesBenchmark.cs b/src/Umbraco.Tests.Benchmarks/SqlTemplatesBenchmark.cs new file mode 100644 index 0000000000..bc1bc4c01c --- /dev/null +++ b/src/Umbraco.Tests.Benchmarks/SqlTemplatesBenchmark.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using NPoco; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Mappers; +using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Tests.Persistence.NPocoTests; + +namespace Umbraco.Tests.Benchmarks +{ + // seeing this kind of results - templates are gooood + // + // Method | Mean | Error | StdDev | Scaled | Gen 0 | Allocated | + // ---------------- |-----------:|----------:|----------:|-------:|---------:|----------:| + // WithoutTemplate | 2,895.0 us | 64.873 us | 99.068 us | 1.00 | 183.5938 | 762.23 KB | + // WithTemplate | 263.2 us | 4.581 us | 4.285 us | 0.09 | 50.2930 | 207.13 KB | + // + // though the difference might not be so obvious in case of WhereIn which requires parsing + + [Config(typeof(Config))] + public class SqlTemplatesBenchmark + { + private class Config : ManualConfig + { + public Config() + { + Add(new MemoryDiagnoser()); + } + } + + public SqlTemplatesBenchmark() + { + var mappers = new NPoco.MapperCollection { new PocoMapper() }; + var factory = new FluentPocoDataFactory((type, iPocoDataFactory) => new PocoDataBuilder(type, mappers).Init()); + + SqlContext = new SqlContext(new SqlCeSyntaxProvider(), DatabaseType.SQLCe, factory); + SqlTemplates = new SqlTemplates(SqlContext); + } + + private ISqlContext SqlContext { get; } + private SqlTemplates SqlTemplates { get; } + + [Benchmark(Baseline = true)] + public void WithoutTemplate() + { + for (var i = 0; i < 100; i++) + { + var sql = Sql.BuilderFor(SqlContext) + .Select() + .From() + .Where(x => x.Name == "yada"); + + var sqlString = sql.SQL; // force-build the SQL + } + } + + [Benchmark] + public void WithTemplate() + { + SqlTemplates.Clear(); + + for (var i = 0; i < 100; i++) + { + var template = SqlTemplates.Get("test", s => s + .Select() + .From() + .Where(x => x.Name == SqlTemplate.Arg("name"))); + + var sql = template.Sql(new { name = "yada" }); + + var sqlString = sql.SQL; // force-build the SQL + } + } + + } +} diff --git a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj index df858097ef..f43afa44cd 100644 --- a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj +++ b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj @@ -170,6 +170,7 @@ + diff --git a/src/Umbraco.Tests/Persistence/NPocoTests/NPocoSqlTemplateTests.cs b/src/Umbraco.Tests/Persistence/NPocoTests/NPocoSqlTemplateTests.cs index 682dad65e3..f0ce4cf021 100644 --- a/src/Umbraco.Tests/Persistence/NPocoTests/NPocoSqlTemplateTests.cs +++ b/src/Umbraco.Tests/Persistence/NPocoTests/NPocoSqlTemplateTests.cs @@ -3,7 +3,9 @@ using Moq; using NPoco; using NUnit.Framework; using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Mappers; using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Core; namespace Umbraco.Tests.Persistence.NPocoTests { @@ -11,7 +13,7 @@ namespace Umbraco.Tests.Persistence.NPocoTests public class NPocoSqlTemplateTests { [Test] - public void TestSqlTemplates() + public void SqlTemplates() { var sqlContext = new SqlContext(new SqlCeSyntaxProvider(), DatabaseType.SQLCe, Mock.Of()); var sqlTemplates = new SqlTemplates(sqlContext); @@ -23,7 +25,7 @@ namespace Umbraco.Tests.Persistence.NPocoTests var sql = sqlTemplates.Get("xxx", s => s .SelectAll() .From("zbThing1") - .Where("id=@id", new { id = "id" })).SqlNamed(new { id = 1 }); + .Where("id=@id", new { id = "id" })).Sql(new { id = 1 }); sql.WriteToConsole(); @@ -31,9 +33,171 @@ namespace Umbraco.Tests.Persistence.NPocoTests sql2.WriteToConsole(); - var sql3 = sqlTemplates.Get("xxx", x => throw new InvalidOperationException("Should be cached.")).SqlNamed(new { id = 1 }); + var sql3 = sqlTemplates.Get("xxx", x => throw new InvalidOperationException("Should be cached.")).Sql(new { id = 1 }); sql3.WriteToConsole(); } + + [Test] + public void SqlTemplateArgs() + { + var mappers = new NPoco.MapperCollection { new PocoMapper() }; + var factory = new FluentPocoDataFactory((type, iPocoDataFactory) => new PocoDataBuilder(type, mappers).Init()); + + var sqlContext = new SqlContext(new SqlCeSyntaxProvider(), DatabaseType.SQLCe, factory); + var sqlTemplates = new SqlTemplates(sqlContext); + + const string sqlBase = "SELECT [zbThing1].[id] AS [Id], [zbThing1].[name] AS [Name] FROM [zbThing1] WHERE "; + + var template = sqlTemplates.Get("sql1", s => s.Select().From() + .Where(x => x.Name == "value")); + + var sql = template.Sql("foo"); + Assert.AreEqual(sqlBase + "(([zbThing1].[name] = @0))", sql.SQL.NoCrLf()); + Assert.AreEqual(1, sql.Arguments.Length); + Assert.AreEqual("foo", sql.Arguments[0]); + + sql = template.Sql(123); + Assert.AreEqual(sqlBase + "(([zbThing1].[name] = @0))", sql.SQL.NoCrLf()); + Assert.AreEqual(1, sql.Arguments.Length); + Assert.AreEqual(123, sql.Arguments[0]); + + template = sqlTemplates.Get("sql2", s => s.Select().From() + .Where(x => x.Name == "value")); + + sql = template.Sql(new { value = "foo" }); + Assert.AreEqual(sqlBase + "(([zbThing1].[name] = @0))", sql.SQL.NoCrLf()); + Assert.AreEqual(1, sql.Arguments.Length); + Assert.AreEqual("foo", sql.Arguments[0]); + + sql = template.Sql(new { value = 123 }); + Assert.AreEqual(sqlBase + "(([zbThing1].[name] = @0))", sql.SQL.NoCrLf()); + Assert.AreEqual(1, sql.Arguments.Length); + Assert.AreEqual(123, sql.Arguments[0]); + + Assert.Throws(() => template.Sql(new { xvalue = 123 }).WriteToConsole()); + Assert.Throws(() => template.Sql(new { value = 123, xvalue = 456 }).WriteToConsole()); + + var i = 666; + + template = sqlTemplates.Get("sql3", s => s.Select().From() + .Where(x => x.Id == i)); + + sql = template.Sql("foo"); + Assert.AreEqual(sqlBase + "(([zbThing1].[id] = @0))", sql.SQL.NoCrLf()); + Assert.AreEqual(1, sql.Arguments.Length); + Assert.AreEqual("foo", sql.Arguments[0]); + + sql = template.Sql(123); + Assert.AreEqual(sqlBase + "(([zbThing1].[id] = @0))", sql.SQL.NoCrLf()); + Assert.AreEqual(1, sql.Arguments.Length); + Assert.AreEqual(123, sql.Arguments[0]); + + // but we cannot name them, because the arg name is the value of "i" + // so we have to explicitely create the argument + + template = sqlTemplates.Get("sql4", s => s.Select().From() + .Where(x => x.Id == SqlTemplate.Arg("i"))); + + sql = template.Sql("foo"); + Assert.AreEqual(sqlBase + "(([zbThing1].[id] = @0))", sql.SQL.NoCrLf()); + Assert.AreEqual(1, sql.Arguments.Length); + Assert.AreEqual("foo", sql.Arguments[0]); + + sql = template.Sql(123); + Assert.AreEqual(sqlBase + "(([zbThing1].[id] = @0))", sql.SQL.NoCrLf()); + Assert.AreEqual(1, sql.Arguments.Length); + Assert.AreEqual(123, sql.Arguments[0]); + + // and thanks to a patched visitor, this now works + + sql = template.Sql(new { i = "foo" }); + Assert.AreEqual(sqlBase + "(([zbThing1].[id] = @0))", sql.SQL.NoCrLf()); + Assert.AreEqual(1, sql.Arguments.Length); + Assert.AreEqual("foo", sql.Arguments[0]); + + sql = template.Sql(new { i = 123 }); + Assert.AreEqual(sqlBase + "(([zbThing1].[id] = @0))", sql.SQL.NoCrLf()); + Assert.AreEqual(1, sql.Arguments.Length); + Assert.AreEqual(123, sql.Arguments[0]); + + Assert.Throws(() => template.Sql(new { j = 123 }).WriteToConsole()); + Assert.Throws(() => template.Sql(new { i = 123, j = 456 }).WriteToConsole()); + + // now with more arguments + + template = sqlTemplates.Get("sql4a", s => s.Select().From() + .Where(x => x.Id == SqlTemplate.Arg("i") && x.Name == SqlTemplate.Arg("name"))); + sql = template.Sql(0, 1); + Assert.AreEqual(sqlBase + "((([zbThing1].[id] = @0) AND ([zbThing1].[name] = @1)))", sql.SQL.NoCrLf()); + Assert.AreEqual(2, sql.Arguments.Length); + Assert.AreEqual(0, sql.Arguments[0]); + Assert.AreEqual(1, sql.Arguments[1]); + + template = sqlTemplates.Get("sql4b", s => s.Select().From() + .Where(x => x.Id == SqlTemplate.Arg("i")) + .Where(x => x.Name == SqlTemplate.Arg("name"))); + sql = template.Sql(0, 1); + Assert.AreEqual(sqlBase + "(([zbThing1].[id] = @0)) AND (([zbThing1].[name] = @1))", sql.SQL.NoCrLf()); + Assert.AreEqual(2, sql.Arguments.Length); + Assert.AreEqual(0, sql.Arguments[0]); + Assert.AreEqual(1, sql.Arguments[1]); + + // works, magic + + template = sqlTemplates.Get("sql5", s => s.Select().From() + .WhereIn(x => x.Id, SqlTemplate.ArgIn("i"))); + + sql = template.Sql("foo"); + Assert.AreEqual(sqlBase + "([zbThing1].[id] IN (@0))", sql.SQL.NoCrLf()); + Assert.AreEqual(1, sql.Arguments.Length); + Assert.AreEqual("foo", sql.Arguments[0]); + + sql = template.Sql(new[] { 1, 2, 3 }); + Assert.AreEqual(sqlBase + "([zbThing1].[id] IN (@0,@1,@2))", sql.SQL.NoCrLf()); + Assert.AreEqual(3, sql.Arguments.Length); + Assert.AreEqual(1, sql.Arguments[0]); + Assert.AreEqual(2, sql.Arguments[1]); + Assert.AreEqual(3, sql.Arguments[2]); + + template = sqlTemplates.Get("sql5a", s => s.Select().From() + .WhereIn(x => x.Id, SqlTemplate.ArgIn("i")) + .Where(x => x.Name == SqlTemplate.Arg("name"))); + + sql = template.Sql("foo", "bar"); + Assert.AreEqual(sqlBase + "([zbThing1].[id] IN (@0)) AND (([zbThing1].[name] = @1))", sql.SQL.NoCrLf()); + Assert.AreEqual(2, sql.Arguments.Length); + Assert.AreEqual("foo", sql.Arguments[0]); + Assert.AreEqual("bar", sql.Arguments[1]); + + sql = template.Sql(new[] { 1, 2, 3 }, "bar"); + Assert.AreEqual(sqlBase + "([zbThing1].[id] IN (@0,@1,@2)) AND (([zbThing1].[name] = @3))", sql.SQL.NoCrLf()); + Assert.AreEqual(4, sql.Arguments.Length); + Assert.AreEqual(1, sql.Arguments[0]); + Assert.AreEqual(2, sql.Arguments[1]); + Assert.AreEqual(3, sql.Arguments[2]); + Assert.AreEqual("bar", sql.Arguments[3]); + + // note however that using WhereIn in a template means that the SQL is going + // to be parsed and arguments are going to be expanded etc - it *may* be a better + // idea to just add the WhereIn to a templated, immutable SQL template + + // more fun... + + template = sqlTemplates.Get("sql6", s => s.Select().From() + // do NOT do this, this is NOT a visited expression + //.Append(" AND whatever=@0", SqlTemplate.Arg("j")) + .Append("AND whatever=@0", "j") // instead, directly name the argument + .Append("AND whatever=@0", "k") // same + ); + + sql = template.Sql(new { j = new[] { 1, 2, 3 }, k = "oops" }); + Assert.AreEqual(sqlBase.TrimEnd("WHERE ") + "AND whatever=@0,@1,@2 AND whatever=@3", sql.SQL.NoCrLf()); + Assert.AreEqual(4, sql.Arguments.Length); + Assert.AreEqual(1, sql.Arguments[0]); + Assert.AreEqual(2, sql.Arguments[1]); + Assert.AreEqual(3, sql.Arguments[2]); + Assert.AreEqual("oops", sql.Arguments[3]); + } } } diff --git a/src/Umbraco.Web/Models/Mapping/TabsAndPropertiesResolver.cs b/src/Umbraco.Web/Models/Mapping/TabsAndPropertiesResolver.cs index 879433ab02..490b93fdaa 100644 --- a/src/Umbraco.Web/Models/Mapping/TabsAndPropertiesResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/TabsAndPropertiesResolver.cs @@ -102,7 +102,7 @@ namespace Umbraco.Web.Models.Mapping //now add the user props contentProps.AddRange(currProps); - + //callback onGenericPropertiesMapped?.Invoke(contentProps); @@ -167,7 +167,7 @@ namespace Umbraco.Web.Models.Mapping var listViewConfig = editor.PreValueEditor.ConvertDbToEditor(editor.DefaultPreValues, preVals); //add the entity type to the config listViewConfig["entityType"] = entityType; - + //Override Tab Label if tabName is provided if (listViewConfig.ContainsKey("tabName")) { diff --git a/src/Umbraco.Web/PropertyEditors/ListViewPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/ListViewPropertyEditor.cs index b1a2ae5d44..04b362d600 100644 --- a/src/Umbraco.Web/PropertyEditors/ListViewPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ListViewPropertyEditor.cs @@ -61,7 +61,7 @@ namespace Umbraco.Web.PropertyEditors { [PreValueField("tabName", "Tab Name", "textstring", Description = "The name of the listview tab (default if empty: 'Child Items')")] public int TabName { get; set; } - + [PreValueField("displayAtTabNumber", "Display At Tab Number", "number", Description = "Which tab position that the list of child items will be displayed")] public int DisplayAtTabNumber { get; set; } diff --git a/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs b/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs index 735dc8815b..fdd44972db 100644 --- a/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs @@ -9,7 +9,7 @@ using Umbraco.Web.Mvc; using Umbraco.Web.WebApi.Filters; using Umbraco.Web._Legacy.Actions; using Constants = Umbraco.Core.Constants; - + namespace Umbraco.Web.Trees { /// @@ -24,23 +24,23 @@ namespace Umbraco.Web.Trees [CoreTree] public class ContentBlueprintTreeController : TreeController { - + protected override TreeNode CreateRootNode(FormDataCollection queryStrings) { var root = base.CreateRootNode(queryStrings); - + //this will load in a custom UI instead of the dashboard for the root node root.RoutePath = string.Format("{0}/{1}/{2}", Constants.Applications.Settings, Constants.Trees.ContentBlueprints, "intro"); - + return root; } protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings) { var nodes = new TreeNodeCollection(); - + //get all blueprints var entities = Services.EntityService.GetChildren(Constants.System.Root, UmbracoObjectTypes.DocumentBlueprint).ToArray(); - + //check if we're rendering the root in which case we'll render the content types that have blueprints if (id == Constants.System.Root.ToInvariantString()) { @@ -48,12 +48,12 @@ namespace Umbraco.Web.Trees var contentTypeAliases = entities.Select(x => ((UmbracoEntity) x).ContentTypeAlias).Distinct(); //get the ids var contentTypeIds = Services.ContentTypeService.GetAllContentTypeIds(contentTypeAliases.ToArray()).ToArray(); - + //now get the entities ... it's a bit round about but still smaller queries than getting all document types var docTypeEntities = contentTypeIds.Length == 0 ? new IUmbracoEntity[0] : Services.EntityService.GetAll(UmbracoObjectTypes.DocumentType, contentTypeIds).ToArray(); - + nodes.AddRange(docTypeEntities .Select(entity => { @@ -64,15 +64,15 @@ namespace Umbraco.Web.Trees treeNode.AdditionalData["jsClickCallback"] = "javascript:void(0);"; return treeNode; })); - + return nodes; } - + var intId = id.TryConvertTo(); //Get the content type var ct = Services.ContentTypeService.Get(intId.Result); if (ct == null) return nodes; - + var blueprintsForDocType = entities.Where(x => ct.Alias == ((UmbracoEntity) x).ContentTypeAlias); nodes.AddRange(blueprintsForDocType .Select(entity => @@ -81,14 +81,14 @@ namespace Umbraco.Web.Trees treeNode.Path = $"-1,{ct.Id},{entity.Id}"; return treeNode; })); - + return nodes; } - + protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings) { var menu = new MenuItemCollection(); - + if (id == Constants.System.Root.ToInvariantString()) { // root actions @@ -103,16 +103,16 @@ namespace Umbraco.Web.Trees var ct = Services.ContentTypeService.Get(cte.Id); var createItem = menu.Items.Add(Services.TextService.Localize($"actions/{ActionCreateBlueprintFromContent.Instance.Alias}")); createItem.NavigateToRoute("/settings/contentBlueprints/edit/-1?create=true&doctype=" + ct.Alias); - + menu.Items.Add(Services.TextService.Localize($"actions/{ActionRefresh.Instance.Alias}"), true); - + return menu; } - + menu.Items.Add(Services.TextService.Localize($"actions/{ActionDelete.Instance.Alias}")); - + return menu; } - + } }