From 8820fa1fdfead8da496e13c5a2da10043cff6780 Mon Sep 17 00:00:00 2001 From: Alejandro Ocampo Date: Thu, 20 Nov 2014 14:27:33 +0000 Subject: [PATCH 01/10] U4-4400 Make upload new media item button bit clearer --- .../src/views/common/dialogs/mediapicker.html | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/mediapicker.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/mediapicker.html index d3f9a5f724..779e6e12b5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/mediapicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/mediapicker.html @@ -63,6 +63,7 @@ data-file-upload="options" data-file-upload-progress="" data-ng-class="{'fileupl + Upload From 633f56a7aacabdd01c0e0e99e0fb10a3d52bec21 Mon Sep 17 00:00:00 2001 From: bjarnef Date: Mon, 19 Jan 2015 16:55:02 +0100 Subject: [PATCH 02/10] U4-5942 - change width of grid cell tools Change width of grid cell tools, so it doesn't overlap the input fields/texteditors. --- src/Umbraco.Web.UI.Client/src/less/gridview.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/gridview.less b/src/Umbraco.Web.UI.Client/src/less/gridview.less index b832f4dd18..1422b18995 100644 --- a/src/Umbraco.Web.UI.Client/src/less/gridview.less +++ b/src/Umbraco.Web.UI.Client/src/less/gridview.less @@ -151,7 +151,7 @@ IFRAME {overflow:hidden;} bottom: 0; top: 0; right: 0; - width: 150px; + width: 50px; opacity: 0.3; z-index: 50; } From c1cc8a3824c598a3356f4687c0ff516d3a863825 Mon Sep 17 00:00:00 2001 From: Sunshine Lewis Date: Tue, 27 Jan 2015 09:57:51 -0500 Subject: [PATCH 03/10] U4-4565 Add margin to bottom bar --- src/Umbraco.Web.UI.Client/src/less/panel.less | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/panel.less b/src/Umbraco.Web.UI.Client/src/less/panel.less index 7fc2d061ea..d9766e6f67 100644 --- a/src/Umbraco.Web.UI.Client/src/less/panel.less +++ b/src/Umbraco.Web.UI.Client/src/less/panel.less @@ -160,7 +160,8 @@ border-top: 1px solid @grayLighter; padding: 10px 0 10px 0; - + + margin-bottom: 17px; position: fixed; bottom: 0px; left: 100px; From 90b3bf59e0855d98669e55d48288245fe70de79e Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 30 Jan 2015 10:32:46 +1100 Subject: [PATCH 04/10] Fixes: U4-2038 Violation of PRIMARY KEY constraint 'PK_cmsContentPreviewXml' YSOD occurs when publishing content Conflicts: src/Umbraco.Core/Umbraco.Core.csproj --- .../Models/ContentPreviewEntity.cs | 10 +- src/Umbraco.Core/Models/ContentXmlEntity.cs | 14 ++- .../Persistence/PetaPocoExtensions.cs | 91 +++++++++++++++++++ .../Persistence/RecordPersistenceType.cs | 9 ++ .../Repositories/ContentPreviewRepository.cs | 41 +++------ .../Repositories/ContentRepository.cs | 12 +-- .../Repositories/ContentXmlRepository.cs | 29 +++--- .../Repositories/MediaRepository.cs | 10 +- .../Repositories/MemberRepository.cs | 10 +- src/Umbraco.Core/Umbraco.Core.csproj | 1 + 10 files changed, 154 insertions(+), 73 deletions(-) create mode 100644 src/Umbraco.Core/Persistence/RecordPersistenceType.cs diff --git a/src/Umbraco.Core/Models/ContentPreviewEntity.cs b/src/Umbraco.Core/Models/ContentPreviewEntity.cs index e9688ff3d5..2f843cf2a7 100644 --- a/src/Umbraco.Core/Models/ContentPreviewEntity.cs +++ b/src/Umbraco.Core/Models/ContentPreviewEntity.cs @@ -10,12 +10,14 @@ namespace Umbraco.Core.Models internal class ContentPreviewEntity : ContentXmlEntity where TContent : IContentBase { - public ContentPreviewEntity(bool previewExists, TContent content, Func xml) - : base(previewExists, content, xml) + public ContentPreviewEntity(TContent content, Func xml) + : base(content, xml) { - Version = content.Version; } - public Guid Version { get; private set; } + public Guid Version + { + get { return Content.Version; } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/ContentXmlEntity.cs b/src/Umbraco.Core/Models/ContentXmlEntity.cs index f137b3b418..0450fdc72e 100644 --- a/src/Umbraco.Core/Models/ContentXmlEntity.cs +++ b/src/Umbraco.Core/Models/ContentXmlEntity.cs @@ -11,13 +11,11 @@ namespace Umbraco.Core.Models internal class ContentXmlEntity : IAggregateRoot where TContent : IContentBase { - private readonly bool _entityExists; private readonly Func _xml; - public ContentXmlEntity(bool entityExists, TContent content, Func xml) - { + public ContentXmlEntity(TContent content, Func xml) + { if (content == null) throw new ArgumentNullException("content"); - _entityExists = entityExists; _xml = xml; Content = content; } @@ -32,6 +30,7 @@ namespace Umbraco.Core.Models { get { return _xml(Content); } } + public TContent Content { get; private set; } public int Id @@ -44,9 +43,14 @@ namespace Umbraco.Core.Models public DateTime CreateDate { get; set; } public DateTime UpdateDate { get; set; } + /// + /// Special case, always return false, this will cause the repositories managing + /// this object to always do an 'insert' but these are special repositories that + /// do an InsertOrUpdate on insert since the data for this needs to be managed this way + /// public bool HasIdentity { - get { return _entityExists; } + get { return false; } } public object DeepClone() diff --git a/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs b/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs index faa247d4a9..d2971ad161 100644 --- a/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs +++ b/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Data; +using System.Data.Common; +using System.Data.SqlClient; using System.Linq; using System.Text.RegularExpressions; using Umbraco.Core.Logging; @@ -18,6 +20,95 @@ namespace Umbraco.Core.Persistence internal static event CreateTableEventHandler NewTable; + /// + /// This will handle the issue of inserting data into a table when there can be a violation of a primary key or unique constraint which + /// can occur when two threads are trying to insert data at the exact same time when the data violates this constraint. + /// + /// + /// + /// + /// Returns the action that executed, either an insert or an update + /// + /// NOTE: If an insert occurred and a PK value got generated, the poco object passed in will contain the updated value. + /// + /// + /// In different databases, there are a few raw SQL options like MySql's ON DUPLICATE KEY UPDATE or MSSQL's MERGE WHEN MATCHED, but since we are + /// also supporting SQLCE for which this doesn't exist we cannot simply rely on the underlying database to help us here. So we'll actually need to + /// try to be as proficient as possible when we know this can occur and manually handle the issue. + /// + /// We do this by first trying to Update the record, this will return the number of rows affected. If it is zero then we insert, if it is one, then + /// we know the update was successful and the row was already inserted by another thread. If the rowcount is zero and we insert and get an exception, + /// that's due to a race condition, in which case we need to retry and update. + /// + internal static RecordPersistenceType InsertOrUpdate(this Database db, T poco) + where T : class + { + return db.InsertOrUpdate(poco, null, null); + } + + /// + /// This will handle the issue of inserting data into a table when there can be a violation of a primary key or unique constraint which + /// can occur when two threads are trying to insert data at the exact same time when the data violates this constraint. + /// + /// + /// + /// + /// If the entity has a composite key they you need to specify the update command explicitly + /// + /// Returns the action that executed, either an insert or an update + /// + /// NOTE: If an insert occurred and a PK value got generated, the poco object passed in will contain the updated value. + /// + /// + /// In different databases, there are a few raw SQL options like MySql's ON DUPLICATE KEY UPDATE or MSSQL's MERGE WHEN MATCHED, but since we are + /// also supporting SQLCE for which this doesn't exist we cannot simply rely on the underlying database to help us here. So we'll actually need to + /// try to be as proficient as possible when we know this can occur and manually handle the issue. + /// + /// We do this by first trying to Update the record, this will return the number of rows affected. If it is zero then we insert, if it is one, then + /// we know the update was successful and the row was already inserted by another thread. If the rowcount is zero and we insert and get an exception, + /// that's due to a race condition, in which case we need to retry and update. + /// + internal static RecordPersistenceType InsertOrUpdate(this Database db, + T poco, + string updateCommand, + object updateArgs) + where T : class + { + if (poco == null) throw new ArgumentNullException("poco"); + + var rowCount = updateCommand.IsNullOrWhiteSpace() + ? db.Update(poco) + : db.Update(updateCommand, updateArgs); + + if (rowCount > 0) return RecordPersistenceType.Update; + + try + { + db.Insert(poco); + return RecordPersistenceType.Insert; + } + //TODO: Need to find out if this is the same exception that will occur for all databases... pretty sure it will be + catch (SqlException ex) + { + //This will occur if the constraint was violated and this record was already inserted by another thread, + //at this exact same time, in this case we need to do an update + + rowCount = updateCommand.IsNullOrWhiteSpace() + ? db.Update(poco) + : db.Update(updateCommand, updateArgs); + + if (rowCount == 0) + { + //this would be strange! in this case the only circumstance would be that at the exact same time, 3 threads executed, one + // did the insert and the other somehow managed to do a delete precisely before this update was executed... now that would + // be real crazy. In that case we need to throw an exception. + throw new DataException("Record could not be inserted or updated"); + } + + return RecordPersistenceType.Update; + } + } + /// /// This will escape single @ symbols for peta poco values so it doesn't think it's a parameter /// diff --git a/src/Umbraco.Core/Persistence/RecordPersistenceType.cs b/src/Umbraco.Core/Persistence/RecordPersistenceType.cs new file mode 100644 index 0000000000..53140afe81 --- /dev/null +++ b/src/Umbraco.Core/Persistence/RecordPersistenceType.cs @@ -0,0 +1,9 @@ +namespace Umbraco.Core.Persistence +{ + internal enum RecordPersistenceType + { + Insert, + Update, + Delete + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentPreviewRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentPreviewRepository.cs index c749dc1204..a7a8dc45f3 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentPreviewRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentPreviewRepository.cs @@ -60,13 +60,20 @@ namespace Umbraco.Core.Persistence.Repositories { throw new NotImplementedException(); } + + //NOTE: Not implemented because all ContentPreviewEntity will always return false for having an Identity + protected override void PersistUpdatedItem(ContentPreviewEntity entity) + { + throw new NotImplementedException(); + } + #endregion protected override void PersistNewItem(ContentPreviewEntity entity) { if (entity.Content.HasIdentity == false) { - throw new InvalidOperationException("Cannot insert a preview for a content item that has no identity"); + throw new InvalidOperationException("Cannot insert or update a preview for a content item that has no identity"); } var previewPoco = new PreviewXmlDto @@ -77,33 +84,13 @@ namespace Umbraco.Core.Persistence.Repositories Xml = entity.Xml.ToString(SaveOptions.None) }; - Database.Insert(previewPoco); + //We need to do a special InsertOrUpdate here because we know that the PreviewXmlDto table has a composite key and thus + // a unique constraint which can be violated if 2+ threads try to execute the same insert sql at the same time. + Database.InsertOrUpdate(previewPoco, + //Since the table has a composite key, we need to specify an explit update statement + "SET xml = @Xml, timestamp = @Timestamp WHERE nodeId=@NodeId AND versionId=@VersionId", + new {NodeId = previewPoco.NodeId, VersionId = previewPoco.VersionId, Xml = previewPoco.Xml, Timestamp = previewPoco.Timestamp}); } - protected override void PersistUpdatedItem(ContentPreviewEntity entity) - { - if (entity.Content.HasIdentity == false) - { - throw new InvalidOperationException("Cannot update a preview for a content item that has no identity"); - } - - var previewPoco = new PreviewXmlDto - { - NodeId = entity.Id, - Timestamp = DateTime.Now, - VersionId = entity.Version, - Xml = entity.Xml.ToString(SaveOptions.None) - }; - - Database.Update( - "SET xml = @Xml, timestamp = @Timestamp WHERE nodeId = @Id AND versionId = @Version", - new - { - Xml = previewPoco.Xml, - Timestamp = previewPoco.Timestamp, - Id = previewPoco.NodeId, - Version = previewPoco.VersionId - }); - } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index c23095ebd6..203393391a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -681,10 +681,8 @@ namespace Umbraco.Core.Persistence.Repositories /// /// public void AddOrUpdateContentXml(IContent content, Func xml) - { - var contentExists = Database.ExecuteScalar("SELECT COUNT(nodeId) FROM cmsContentXml WHERE nodeId = @Id", new { Id = content.Id }) != 0; - - _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(contentExists, content, xml)); + { + _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(content, xml)); } /// @@ -703,11 +701,7 @@ namespace Umbraco.Core.Persistence.Repositories /// public void AddOrUpdatePreviewXml(IContent content, Func xml) { - var previewExists = - Database.ExecuteScalar("SELECT COUNT(nodeId) FROM cmsPreviewXml WHERE nodeId = @Id AND versionId = @Version", - new { Id = content.Id, Version = content.Version }) != 0; - - _contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity(previewExists, content, xml)); + _contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity(content, xml)); } /// diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentXmlRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentXmlRepository.cs index da076506a6..fa65359766 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentXmlRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentXmlRepository.cs @@ -55,6 +55,12 @@ namespace Umbraco.Core.Persistence.Repositories { get { throw new NotImplementedException(); } } + + //NOTE: Not implemented because all ContentXmlEntity will always return false for having an Identity + protected override void PersistUpdatedItem(ContentXmlEntity entity) + { + throw new NotImplementedException(); + } #endregion @@ -68,22 +74,21 @@ namespace Umbraco.Core.Persistence.Repositories { if (entity.Content.HasIdentity == false) { - throw new InvalidOperationException("Cannot insert an xml entry for a content item that has no identity"); + throw new InvalidOperationException("Cannot insert or update an xml entry for a content item that has no identity"); } - var poco = new ContentXmlDto { NodeId = entity.Id, Xml = entity.Xml.ToString(SaveOptions.None) }; - Database.Insert(poco); - } - - protected override void PersistUpdatedItem(ContentXmlEntity entity) - { - if (entity.Content.HasIdentity == false) + var poco = new ContentXmlDto { - throw new InvalidOperationException("Cannot update an xml entry for a content item that has no identity"); - } + NodeId = entity.Id, + Xml = entity.Xml.ToString(SaveOptions.None) + }; - var poco = new ContentXmlDto { NodeId = entity.Id, Xml = entity.Xml.ToString(SaveOptions.None) }; - Database.Update(poco); + //We need to do a special InsertOrUpdate here because we know that the ContentXmlDto table has a 1:1 relation + // with the content table and a record may or may not exist so the + // unique constraint which can be violated if 2+ threads try to execute the same insert sql at the same time. + Database.InsertOrUpdate(poco); + } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs index 7a487acaf4..a6db5387f4 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs @@ -259,18 +259,12 @@ namespace Umbraco.Core.Persistence.Repositories public void AddOrUpdateContentXml(IMedia content, Func xml) { - var contentExists = Database.ExecuteScalar("SELECT COUNT(nodeId) FROM cmsContentXml WHERE nodeId = @Id", new { Id = content.Id }) != 0; - - _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(contentExists, content, xml)); + _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(content, xml)); } public void AddOrUpdatePreviewXml(IMedia content, Func xml) { - var previewExists = - Database.ExecuteScalar("SELECT COUNT(nodeId) FROM cmsPreviewXml WHERE nodeId = @Id AND versionId = @Version", - new { Id = content.Id, Version = content.Version }) != 0; - - _contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity(previewExists, content, xml)); + _contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity(content, xml)); } protected override void PerformDeleteVersion(int id, Guid versionId) diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs index d5c65bd85b..9e3a47efe0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs @@ -675,18 +675,12 @@ namespace Umbraco.Core.Persistence.Repositories public void AddOrUpdateContentXml(IMember content, Func xml) { - var contentExists = Database.ExecuteScalar("SELECT COUNT(nodeId) FROM cmsContentXml WHERE nodeId = @Id", new { Id = content.Id }) != 0; - - _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(contentExists, content, xml)); + _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(content, xml)); } public void AddOrUpdatePreviewXml(IMember content, Func xml) { - var previewExists = - Database.ExecuteScalar("SELECT COUNT(nodeId) FROM cmsPreviewXml WHERE nodeId = @Id AND versionId = @Version", - new { Id = content.Id, Version = content.Version }) != 0; - - _contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity(previewExists, content, xml)); + _contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity(content, xml)); } protected override string GetDatabaseFieldNameForOrderBy(string orderBy) diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 56a7d3e5b0..0a61de7198 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -314,6 +314,7 @@ + From f32e47a36349e3df0b0ec94a7c0a9762325b8d45 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 30 Jan 2015 10:59:11 +1100 Subject: [PATCH 05/10] fixes exception when rendering macros in rte in back office. --- .../Macros/PartialViewMacroEngine.cs | 2 ++ src/Umbraco.Web/umbraco.presentation/macro.cs | 18 +++++++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web/Macros/PartialViewMacroEngine.cs b/src/Umbraco.Web/Macros/PartialViewMacroEngine.cs index d1a435e48e..bf3586a174 100644 --- a/src/Umbraco.Web/Macros/PartialViewMacroEngine.cs +++ b/src/Umbraco.Web/Macros/PartialViewMacroEngine.cs @@ -102,6 +102,8 @@ namespace Umbraco.Web.Macros public string Execute(MacroModel macro, INode node) { + if (node == null) return string.Empty; + var umbCtx = _getUmbracoContext(); //NOTE: This is a bit of a nasty hack to check if the INode is actually already based on an IPublishedContent // (will be the case when using LegacyConvertedNode ) diff --git a/src/Umbraco.Web/umbraco.presentation/macro.cs b/src/Umbraco.Web/umbraco.presentation/macro.cs index 42c3dbe05d..7b9c63e6b8 100644 --- a/src/Umbraco.Web/umbraco.presentation/macro.cs +++ b/src/Umbraco.Web/umbraco.presentation/macro.cs @@ -23,6 +23,7 @@ using Umbraco.Core.Events; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Macros; +using Umbraco.Core.Models; using Umbraco.Core.Xml.XPath; using Umbraco.Core.Profiling; using umbraco.interfaces; @@ -33,7 +34,6 @@ using Umbraco.Web.Models; using Umbraco.Web.Templates; using umbraco.BusinessLogic; using umbraco.cms.businesslogic.macro; -using umbraco.cms.businesslogic.member; using umbraco.DataLayer; using umbraco.NodeFactory; using umbraco.presentation.templateControls; @@ -42,6 +42,9 @@ using Content = umbraco.cms.businesslogic.Content; using Macro = umbraco.cms.businesslogic.macro.Macro; using MacroErrorEventArgs = Umbraco.Core.Events.MacroErrorEventArgs; using System.Linq; +using File = System.IO.File; +using MacroTypes = umbraco.cms.businesslogic.macro.MacroTypes; +using Member = umbraco.cms.businesslogic.member.Member; namespace umbraco { @@ -1849,10 +1852,19 @@ namespace umbraco { //Get the current content request - var content = UmbracoContext.Current.PublishedContentRequest != null + IPublishedContent content; + if (UmbracoContext.Current.IsFrontEndUmbracoRequest) + { + content = UmbracoContext.Current.PublishedContentRequest != null ? UmbracoContext.Current.PublishedContentRequest.PublishedContent : null; - + } + else + { + var pageId = UmbracoContext.Current.PageId; + content = pageId.HasValue ? UmbracoContext.Current.ContentCache.GetById(pageId.Value) : null; + } + return content == null ? null : LegacyNodeHelper.ConvertToNode(content); } From 0380b04b03565eaef338b0f1ad54f4a2f2d0d39a Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 30 Jan 2015 11:07:47 +1100 Subject: [PATCH 06/10] fixes breadcrumb on content/media, ensures a request to load it doesn't happen if there is no parent, ensures that the full breadcrumb is shown and in the right order and ensures that the breadcrumb class is not added if there isn't one so the margins on the buttons work better. --- .../views/content/content.edit.controller.js | 13 +++--- .../src/views/content/edit.html | 40 +++++++++---------- .../src/views/media/edit.html | 28 ++++++------- .../src/views/media/media.edit.controller.js | 17 ++++---- 4 files changed, 48 insertions(+), 50 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js index c74c7c90b9..fee0df5745 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js @@ -32,13 +32,14 @@ function ContentEditController($scope, $rootScope, $routeParams, $q, $timeout, $ editorState.set($scope.content); - //We fetch all ancestors of the node to generate the footer breadcrump navigation + //We fetch all ancestors of the node to generate the footer breadcrumb navigation if (!$routeParams.create) { - entityResource.getAncestors(content.id, "document") - .then(function (anc) { - anc.pop(); - $scope.ancestors = anc; - }); + if (content.parentId && content.parentId != -1) { + entityResource.getAncestors(content.id, "document") + .then(function (anc) { + $scope.ancestors = anc.reverse(); + }); + } } } diff --git a/src/Umbraco.Web.UI.Client/src/views/content/edit.html b/src/Umbraco.Web.UI.Client/src/views/content/edit.html index 29c646a092..b6ad9cb934 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/edit.html @@ -4,42 +4,40 @@ ng-submit="save()" val-form-manager> - +
- +
-
+
- + + current-node="currentNode" + current-section="{{currentSection}}"> -
-
+
+
-
internal static class ExtensionMethodFinder { - private static readonly MethodInfo[] AllExtensionMethods; + /// + /// The static cache for extension methods found that match the criteria that we are looking for + /// + private static readonly ConcurrentDictionary, MethodInfo[]> MethodCache = new ConcurrentDictionary, MethodInfo[]>(); - static ExtensionMethodFinder() - { - AllExtensionMethods = TypeFinder.GetAssembliesWithKnownExclusions() + /// + /// Returns the enumerable of all extension method info's in the app domain = USE SPARINGLY!!! + /// + /// + /// + /// We cache this as a sliding 5 minute exiration, in unit tests there's over 1100 methods found, surely that will eat up a bit of memory so we want + /// to make sure we give it back. + /// + private static IEnumerable GetAllExtensionMethodsInAppDomain(IRuntimeCacheProvider runtimeCacheProvider) + { + if (runtimeCacheProvider == null) throw new ArgumentNullException("runtimeCacheProvider"); + + return runtimeCacheProvider.GetCacheItem(typeof (ExtensionMethodFinder).Name, () => TypeFinder.GetAssembliesWithKnownExclusions() // assemblies that contain extension methods - .Where(a => a.IsDefined(typeof(ExtensionAttribute), false)) + .Where(a => a.IsDefined(typeof (ExtensionAttribute), false)) // types that contain extension methods .SelectMany(a => a.GetTypes() - .Where(t => t.IsDefined(typeof(ExtensionAttribute), false) && t.IsSealed && t.IsGenericType == false && t.IsNested == false)) + .Where(t => t.IsDefined(typeof (ExtensionAttribute), false) && t.IsSealed && t.IsGenericType == false && t.IsNested == false)) // actual extension methods .SelectMany(t => t.GetMethods(BindingFlags.Static | BindingFlags.Public) - .Where(m => m.IsDefined(typeof(ExtensionAttribute), false))) + .Where(m => m.IsDefined(typeof (ExtensionAttribute), false))) // and also IEnumerable extension methods - because the assembly is excluded - .Concat(typeof(Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public)) - .ToArray(); - } + .Concat(typeof (Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public)) + //If we don't do this then we'll be scanning all assemblies each time! + .ToArray(), + + //only cache for 5 minutes + timeout: TimeSpan.FromMinutes(5), - // ORIGINAL CODE IS NOT COMPLETE, DOES NOT HANDLE GENERICS, ETC... - - // so this is an attempt at fixing things, but it's not done yet - // and do we really want to do this? extension methods are not supported on dynamics, period - // we should use strongly typed content instead of dynamics. - - /* - - // get all extension methods for type thisType, with name name, - // accepting argsCount arguments (not counting the instance of thisType). - private static IEnumerable GetExtensionMethods(Type thisType, string name, int argsCount) - { - var key = string.Format("{0}.{1}::{2}", thisType.FullName, name, argsCount); - - var types = thisType.GetBaseTypes(true); // either do this OR have MatchFirstParameter handle the stuff... F*XME - - var methods = AllExtensionMethods - .Where(m => m.Name == name) - .Where(m => m.GetParameters().Length == argsCount) - .Where(m => MatchFirstParameter(thisType, m.GetParameters()[0].ParameterType)); - - // f*xme - is this what we should cache? - return methods; + //each time this is accessed it will be for 5 minutes longer + isSliding:true); } - // find out whether the first parameter is a match for thisType - private static bool MatchFirstParameter(Type thisType, Type firstParameterType) - { - return MethodArgZeroHasCorrectTargetType(null, firstParameterType, thisType); - } + /// + /// Returns all extension methods found matching the definition + /// + /// + /// The runtime cache is used to temporarily cache all extension methods found in the app domain so that + /// while we search for individual extension methods, the process will be reasonably 'quick'. We then statically + /// cache the MethodInfo's that we are looking for and then the runtime cache will expire and give back all that memory. + /// + /// + /// + /// + /// The arguments EXCLUDING the 'this' argument in an extension method + /// + /// + /// + /// NOTE: This will be an intensive method to call! Results will be cached based on the key (args) of this method + /// + internal static IEnumerable GetAllExtensionMethods(IRuntimeCacheProvider runtimeCache, Type thisType, string name, int argumentCount) + { + var key = new Tuple(thisType, name, argumentCount); - // get the single extension method for type thisType, with name name, - // that accepts the arguments in args (which does not contain the instance of thisType). - public static MethodInfo GetExtensionMethod(Type thisType, string name, object[] args) - { - MethodInfo result = null; - foreach (var method in GetExtensionMethods(thisType, name, args.Length).Where(m => MatchParameters(m, args))) - { - if (result == null) - result = method; - else - throw new AmbiguousMatchException("More than one matching extension method was found."); - } - return result; - } + return MethodCache.GetOrAdd(key, tuple => + { + var candidates = GetAllExtensionMethodsInAppDomain(runtimeCache); - // find out whether the method can accept the arguments - private static bool MatchParameters(MethodInfo method, IList args) - { - var parameters = method.GetParameters(); - var i = 0; - for (; i < parameters.Length; ++i) - { - if (MatchParameter(parameters[i].ParameterType, args[i].GetType()) == false) - break; - } - return (i == parameters.Length); - } + //filter by name + var methodsByName = candidates.Where(m => m.Name == name); - internal static bool MatchParameter(Type parameterType, Type argumentType) - { - // public static int DoSomething(Foo foo, T t1, T t2) - // DoSomething(foo, t1, t2) => how can we match?! - return parameterType == argumentType; // f*xme of course! - } - * - */ + //ensure we add + 1 to the arg count because the 'this' arg is not included in the count above! + var isGenericAndRightParamCount = methodsByName.Where(m => m.GetParameters().Length == argumentCount + 1); - // BELOW IS THE ORIGINAL CODE... + //find the right overload that can take genericParameterType + //which will be either DynamicNodeList or List which is IEnumerable` - /// - /// Returns all extension methods found matching the definition - /// - /// - /// - /// - /// - /// - /// - /// TODO: NOTE: This will be an intensive method to call!! Results should be cached! - /// - private static IEnumerable GetAllExtensionMethods(Type thisType, string name, int argumentCount, bool argsContainsThis) - { - // at *least* we can cache the extension methods discovery - var candidates = AllExtensionMethods; + var withGenericParameterType = isGenericAndRightParamCount.Select(m => new { m, t = FirstParameterType(m) }); - /* - //only scan assemblies we know to contain extension methods (user assemblies) - var assembliesToScan = TypeFinder.GetAssembliesWithKnownExclusions(); + var methodsWhereArgZeroIsTargetType = (from method in withGenericParameterType + where + method.t != null && MethodArgZeroHasCorrectTargetType(method.m, method.t, thisType) + select method); - //get extension methods from runtime - var candidates = ( - from assembly in assembliesToScan - where assembly.IsDefined(typeof(ExtensionAttribute), false) - from type in assembly.GetTypes() - where (type.IsDefined(typeof(ExtensionAttribute), false) - && type.IsSealed && !type.IsGenericType && !type.IsNested) - from method in type.GetMethods(BindingFlags.Static | BindingFlags.Public) - // this filters extension methods - where method.IsDefined(typeof(ExtensionAttribute), false) - select method - ); - - //add the extension methods defined in IEnumerable - candidates = candidates.Concat(typeof(Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public)); - */ - - //filter by name - var methodsByName = candidates.Where(m => m.Name == name); - - var isGenericAndRightParamCount = methodsByName.Where(m => m.GetParameters().Length == argumentCount + (argsContainsThis ? 0 : 1)); - - //find the right overload that can take genericParameterType - //which will be either DynamicNodeList or List which is IEnumerable` - - var withGenericParameterType = isGenericAndRightParamCount.Select(m => new { m, t = FirstParameterType(m) }); - - var methodsWhereArgZeroIsTargetType = (from method in withGenericParameterType - where - method.t != null && MethodArgZeroHasCorrectTargetType(method.m, method.t, thisType) - select method); - - return methodsWhereArgZeroIsTargetType.Select(mt => mt.m); + return methodsWhereArgZeroIsTargetType.Select(mt => mt.m).ToArray(); + }); + } private static bool MethodArgZeroHasCorrectTargetType(MethodInfo method, Type firstArgumentType, Type thisType) @@ -182,36 +123,36 @@ namespace Umbraco.Core.Dynamics private static bool MethodArgZeroHasCorrectTargetTypeShareACommonInterface(MethodInfo method, Type firstArgumentType, Type thisType) { - Type[] interfaces = firstArgumentType.GetInterfaces(); + var interfaces = firstArgumentType.GetInterfaces(); if (interfaces.Length == 0) { return false; } - bool result = interfaces.All(i => thisType.GetInterfaces().Contains(i)); + var result = interfaces.All(i => thisType.GetInterfaces().Contains(i)); return result; } private static bool MethodArgZeroHasCorrectTargetTypeIsASubclassOf(MethodInfo method, Type firstArgumentType, Type thisType) { - bool result = thisType.IsSubclassOf(firstArgumentType); + var result = thisType.IsSubclassOf(firstArgumentType); return result; } private static bool MethodArgZeroHasCorrectTargetTypeAnInterfaceMatches(MethodInfo method, Type firstArgumentType, Type thisType) { - bool result = thisType.GetInterfaces().Contains(firstArgumentType); + var result = thisType.GetInterfaces().Contains(firstArgumentType); return result; } private static bool MethodArgZeroHasCorrectTargetTypeTypeMatchesExactly(MethodInfo method, Type firstArgumentType, Type thisType) { - bool result = (thisType == firstArgumentType); + var result = (thisType == firstArgumentType); return result; } private static Type FirstParameterType(MethodInfo m) { - ParameterInfo[] p = m.GetParameters(); + var p = m.GetParameters(); if (p.Any()) { return p.First().ParameterType; @@ -219,74 +160,62 @@ namespace Umbraco.Core.Dynamics return null; } - private static MethodInfo DetermineMethodFromParams(IEnumerable methods, Type genericType, IEnumerable args) + private static MethodInfo DetermineMethodFromParams(IEnumerable methods, Type genericType, IEnumerable args) { - if (!methods.Any()) - { - return null; - } MethodInfo methodToExecute = null; - if (methods.Count() > 1) - { - //Given the args, lets get the types and compare the type sequence to try and find the correct overload - var argTypes = args.ToList().ConvertAll(o => - { - var oe = (o as Expression); - if (oe != null) - { - return oe.Type.FullName; - } - return o.GetType().FullName; - }); - var methodsWithArgTypes = methods.Select(method => new { method, types = method.GetParameters().Select(pi => pi.ParameterType.FullName) }); - var firstMatchingOverload = methodsWithArgTypes.FirstOrDefault(m => m.types.SequenceEqual(argTypes)); - if (firstMatchingOverload != null) - { - methodToExecute = firstMatchingOverload.method; - } - } - if (methodToExecute == null) - { - var firstMethod = methods.FirstOrDefault(); - // NH: this is to ensure that it's always the correct one being chosen when using the LINQ extension methods - if (methods.Count() > 1) - { - var firstGenericMethod = methods.FirstOrDefault(x => x.IsGenericMethodDefinition); - if (firstGenericMethod != null) - { - firstMethod = firstGenericMethod; - } - } + //Given the args, lets get the types and compare the type sequence to try and find the correct overload + var argTypes = args.Select(o => + { + var oe = (o as Expression); + return oe != null ? oe.Type : o.GetType(); + }); + + var methodsWithArgTypes = methods.Select(method => new + { + method, + //skip the first arg because that is the extension method type ('this') that we are looking for + types = method.GetParameters().Select(pi => pi.ParameterType).Skip(1) + }); + + //This type comparer will check + var typeComparer = new DelegateEqualityComparer( + //Checks if the argument type passed in can be assigned from the parameter type in the method. For + // example, if the argument type is HtmlHelper but the method parameter type is HtmlHelper then + // it will match because the argument is assignable to that parameter type and will be able to execute + TypeHelper.IsTypeAssignableFrom, + //This will not ever execute but if it does we need to get the hash code of the string because the hash + // code of a type is random + type => type.FullName.GetHashCode()); + + var firstMatchingOverload = methodsWithArgTypes + .FirstOrDefault(m => m.types.SequenceEqual(argTypes, typeComparer)); + + if (firstMatchingOverload != null) + { + methodToExecute = firstMatchingOverload.method; + } - if (firstMethod != null) - { - if (firstMethod.IsGenericMethodDefinition) - { - if (genericType != null) - { - methodToExecute = firstMethod.MakeGenericMethod(genericType); - } - } - else - { - methodToExecute = firstMethod; - } - } - } return methodToExecute; } - public static MethodInfo FindExtensionMethod(Type thisType, object[] args, string name, bool argsContainsThis) + public static MethodInfo FindExtensionMethod(IRuntimeCacheProvider runtimeCache, Type thisType, object[] args, string name, bool argsContainsThis) { Type genericType = null; if (thisType.IsGenericType) { genericType = thisType.GetGenericArguments()[0]; - } + } - var methods = GetAllExtensionMethods(thisType, name, args.Length, argsContainsThis).ToArray(); - return DetermineMethodFromParams(methods, genericType, args); + args = args + //if the args contains 'this', remove the first one since that is 'this' and we don't want to use + //that in the method searching + .Skip(argsContainsThis ? 1 : 0) + .ToArray(); + + var methods = GetAllExtensionMethods(runtimeCache, thisType, name, args.Length).ToArray(); + + return DetermineMethodFromParams(methods, genericType, args); } } } diff --git a/src/Umbraco.Tests/DynamicsAndReflection/ExtensionMethodFinderTests.cs b/src/Umbraco.Tests/DynamicsAndReflection/ExtensionMethodFinderTests.cs index 56fc36a123..37b0a63866 100644 --- a/src/Umbraco.Tests/DynamicsAndReflection/ExtensionMethodFinderTests.cs +++ b/src/Umbraco.Tests/DynamicsAndReflection/ExtensionMethodFinderTests.cs @@ -3,8 +3,10 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; +using System.Web.Mvc; using NUnit.Framework; using Umbraco.Core; +using Umbraco.Core.Cache; using Umbraco.Core.Dynamics; namespace Umbraco.Tests.DynamicsAndReflection @@ -27,8 +29,8 @@ namespace Umbraco.Tests.DynamicsAndReflection // // Eric Lippert, http://stackoverflow.com/questions/5311465/extension-method-and-dynamic-object-in-c-sharp + [Ignore("This is just testing the below GetMethodForArguments method - Stephen was working on this but it's not used in the core")] [Test] - [Ignore("fails")] public void TypesTests() { Assert.IsTrue(typeof(int[]).Inherits()); @@ -84,7 +86,9 @@ namespace Umbraco.Tests.DynamicsAndReflection Assert.IsNotNull(m5A); // note - should we also handle "ref" and "out" parameters? + // SD: NO, lets not make this more complicated than it already is // note - should we pay attention to array types? + // SD: NO, lets not make this more complicated than it already is } public void TestMethod1(int value) {} @@ -157,42 +161,122 @@ namespace Umbraco.Tests.DynamicsAndReflection : method.MakeGenericMethod(genericArgumentTypes); } - public class Class1 + public class TestClass {} + public class TestClass : TestClass { } + [Test] - [Ignore("fails")] - public void FinderTests() + public void Find_Non_Overloaded_Method() { MethodInfo method; - var class1 = new Class1(); + var class1 = new TestClass(); - method = ExtensionMethodFinder.FindExtensionMethod(typeof (Class1), new object[] {1}, "TestMethod1", false); + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(TestClass), new object[] { 1 }, "SimpleMethod", false); Assert.IsNotNull(method); method.Invoke(null, new object[] { class1, 1 }); - method = ExtensionMethodFinder.FindExtensionMethod(typeof(Class1), new object[] { "x" }, "TestMethod1", false); - Assert.IsNull(method); // note - fails, return TestMethod1! + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(TestClass), new object[] { "x" }, "SimpleMethod", false); + Assert.IsNull(method); + } - method = ExtensionMethodFinder.FindExtensionMethod(typeof(Class1), new object[] { 1 }, "TestMethod2", false); + [Test] + public void Find_Overloaded_Method() + { + MethodInfo method; + var class1 = new TestClass(); + + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(TestClass), new object[] { 1 }, "SimpleOverloadMethod", false); Assert.IsNotNull(method); - method.Invoke(null, new object[] { class1, "1" }); + method.Invoke(null, new object[] { class1, 1 }); - method = ExtensionMethodFinder.FindExtensionMethod(typeof(Class1), new object[] { "x" }, "TestMethod2", false); + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(TestClass), new object[] { "x" }, "SimpleOverloadMethod", false); Assert.IsNotNull(method); method.Invoke(null, new object[] { class1, "x" }); } + + [Test] + public void Find_Overloaded_Method_With_Args_Containing_This() + { + MethodInfo method; + var class1 = new TestClass(); + + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(TestClass), new object[] { class1, 1 }, "SimpleOverloadMethod", true); + Assert.IsNotNull(method); + method.Invoke(null, new object[] { class1, 1 }); + + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(TestClass), new object[] { class1, "x" }, "SimpleOverloadMethod", true); + Assert.IsNotNull(method); + method.Invoke(null, new object[] { class1, "x" }); + } + + [Test] + public void Find_Non_Overloaded_Generic_Enumerable_Method() + { + MethodInfo method; + var class1 = Enumerable.Empty(); + + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(IEnumerable), new object[] { 1 }, "SimpleEnumerableGenericMethod", false); + Assert.IsNotNull(method); + method.Invoke(null, new object[] { class1, 1 }); + + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(IEnumerable), new object[] { "x" }, "SimpleEnumerableGenericMethod", false); + Assert.IsNull(method); + } + + [Test] + public void Find_Overloaded_Generic_Enumerable_Method() + { + MethodInfo method; + var class1 = Enumerable.Empty(); + + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(IEnumerable), new object[] { 1 }, "SimpleOverloadEnumerableGenericMethod", false); + Assert.IsNotNull(method); + method.Invoke(null, new object[] { class1, 1 }); + + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(IEnumerable), new object[] { "x" }, "SimpleOverloadEnumerableGenericMethod", false); + Assert.IsNotNull(method); + method.Invoke(null, new object[] { class1, "x" }); + } + + [Test] + public void Find_Method_With_Parameter_Match_With_Generic_Argument() + { + MethodInfo method; + + var genericTestClass = new TestClass(); + var nonGenericTestClass = new TestClass(); + + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(TestClass), new object[] { genericTestClass }, "GenericParameterMethod", false); + Assert.IsNotNull(method); + + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(TestClass), new object[] { nonGenericTestClass }, "GenericParameterMethod", false); + Assert.IsNotNull(method); + } } static class ExtensionMethodFinderTestsExtensions { - public static void TestMethod1(this ExtensionMethodFinderTests.Class1 source, int value) + public static void SimpleMethod(this ExtensionMethodFinderTests.TestClass source, int value) { } - public static void TestMethod2(this ExtensionMethodFinderTests.Class1 source, int value) + public static void SimpleOverloadMethod(this ExtensionMethodFinderTests.TestClass source, int value) { } - public static void TestMethod2(this ExtensionMethodFinderTests.Class1 source, string value) + public static void SimpleOverloadMethod(this ExtensionMethodFinderTests.TestClass source, string value) { } + + public static void SimpleEnumerableGenericMethod(this IEnumerable source, int value) + { } + + public static void SimpleOverloadEnumerableGenericMethod(this IEnumerable source, int value) + { } + + public static void SimpleOverloadEnumerableGenericMethod(this IEnumerable source, string value) + { } + + public static void GenericParameterMethod(this ExtensionMethodFinderTests.TestClass source, ExtensionMethodFinderTests.TestClass value) + { } + } } diff --git a/src/Umbraco.Web/Dynamics/ExpressionParser.cs b/src/Umbraco.Web/Dynamics/ExpressionParser.cs index d348682428..6ca357a996 100644 --- a/src/Umbraco.Web/Dynamics/ExpressionParser.cs +++ b/src/Umbraco.Web/Dynamics/ExpressionParser.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; using Umbraco.Core; +using Umbraco.Core.Cache; using Umbraco.Core.Dynamics; using Umbraco.Web.Models; @@ -907,10 +908,12 @@ namespace Umbraco.Web.Dynamics //SD: I have yet to see extension methods actually being called in the dynamic parsing... need to unit test these // scenarios and figure out why all this type checking occurs. + var runtimeCache = ApplicationContext.Current != null ? ApplicationContext.Current.ApplicationCache.RuntimeCache : new NullCacheProvider(); + if (type == typeof(string) && instanceAsString != null) { Expression[] newArgs = (new List() { Expression.Invoke(instanceAsString, instanceExpression) }).Concat(args).ToArray(); - mb = ExtensionMethodFinder.FindExtensionMethod(typeof(string), newArgs, id, true); + mb = ExtensionMethodFinder.FindExtensionMethod(runtimeCache, typeof(string), newArgs, id, true); if (mb != null) { return CallMethodOnDynamicNode(instance, newArgs, instanceAsString, instanceExpression, (MethodInfo)mb, true); @@ -919,7 +922,7 @@ namespace Umbraco.Web.Dynamics if (type == typeof(string) && instanceAsString == null && instance is MemberExpression) { Expression[] newArgs = (new List() { instance }).Concat(args).ToArray(); - mb = ExtensionMethodFinder.FindExtensionMethod(typeof(string), newArgs, id, true); + mb = ExtensionMethodFinder.FindExtensionMethod(runtimeCache, typeof(string), newArgs, id, true); if (mb != null) { return Expression.Call(null, (MethodInfo)mb, newArgs); @@ -994,7 +997,7 @@ namespace Umbraco.Web.Dynamics //e.g. uBlogsyPostDate.Date //SD: Removed the NonPublic accessor here because this will never work in medium trust, wondering why it is NonPublic vs Public ? Have changed to Public. //MethodInfo ReflectPropertyValue = this.GetType().GetMethod("ReflectPropertyValue", BindingFlags.NonPublic | BindingFlags.Static); - MethodInfo ReflectPropertyValue = this.GetType().GetMethod("ReflectPropertyValue", BindingFlags.Public | BindingFlags.Static); + MethodInfo reflectPropertyValue = this.GetType().GetMethod("ReflectPropertyValue", BindingFlags.Public | BindingFlags.Static); ParameterExpression convertDynamicNullToBooleanFalse = Expression.Parameter(typeof(bool), "convertDynamicNullToBooleanFalse"); ParameterExpression result = Expression.Parameter(typeof(object), "result"); ParameterExpression idParam = Expression.Parameter(typeof(string), "id"); @@ -1008,7 +1011,7 @@ namespace Umbraco.Web.Dynamics new[] { lambdaResult, result, idParam, convertDynamicNullToBooleanFalse }, Expression.Assign(convertDynamicNullToBooleanFalse, Expression.Constant(_flagConvertDynamicNullToBooleanFalse, typeof(bool))), Expression.Assign(lambdaResult, Expression.Invoke(instance, lambdaInstanceExpression)), - Expression.Assign(result, Expression.Call(ReflectPropertyValue, lambdaResult, Expression.Constant(id))), + Expression.Assign(result, Expression.Call(reflectPropertyValue, lambdaResult, Expression.Constant(id))), Expression.IfThen( Expression.AndAlso( Expression.TypeEqual(result, typeof(DynamicNull)), diff --git a/src/Umbraco.Web/GridTemplateExtensions.cs b/src/Umbraco.Web/GridTemplateExtensions.cs index d26fb1ffeb..e4af759c88 100644 --- a/src/Umbraco.Web/GridTemplateExtensions.cs +++ b/src/Umbraco.Web/GridTemplateExtensions.cs @@ -48,38 +48,30 @@ namespace Umbraco.Web var model = prop.Value; var asString = model as string; - if (asString.IsNullOrWhiteSpace()) return new MvcHtmlString(string.Empty); + if (asString != null && string.IsNullOrEmpty(asString)) return new MvcHtmlString(string.Empty); return html.Partial(view, model); } - - [Obsolete("This should not be used, GetGridHtml extensions on HtmlHelper should be used instead")] - public static MvcHtmlString GetGridHtml(this IPublishedProperty property, string framework = "bootstrap3") + public static MvcHtmlString GetGridHtml(this IPublishedProperty property, HtmlHelper html, string framework = "bootstrap3") { var asString = property.Value as string; if (asString.IsNullOrWhiteSpace()) return new MvcHtmlString(string.Empty); - + var view = "Grid/" + framework; - return new MvcHtmlString(RenderPartialViewToString(view, property.Value)); + return html.Partial(view, property.Value); } - - [Obsolete("This should not be used, GetGridHtml extensions on HtmlHelper should be used instead")] - public static MvcHtmlString GetGridHtml(this IPublishedContent contentItem) + public static MvcHtmlString GetGridHtml(this IPublishedContent contentItem, HtmlHelper html) { - return GetGridHtml(contentItem, "bodyText", "bootstrap3"); + return GetGridHtml(contentItem, html, "bodyText", "bootstrap3"); } - - [Obsolete("This should not be used, GetGridHtml extensions on HtmlHelper should be used instead")] - public static MvcHtmlString GetGridHtml(this IPublishedContent contentItem, string propertyAlias) + public static MvcHtmlString GetGridHtml(this IPublishedContent contentItem, HtmlHelper html, string propertyAlias) { Mandate.ParameterNotNullOrEmpty(propertyAlias, "propertyAlias"); - return GetGridHtml(contentItem, propertyAlias, "bootstrap3"); + return GetGridHtml(contentItem, html, propertyAlias, "bootstrap3"); } - - [Obsolete("This should not be used, GetGridHtml extensions on HtmlHelper should be used instead")] - public static MvcHtmlString GetGridHtml(this IPublishedContent contentItem, string propertyAlias, string framework) + public static MvcHtmlString GetGridHtml(this IPublishedContent contentItem, HtmlHelper html, string propertyAlias, string framework) { Mandate.ParameterNotNullOrEmpty(propertyAlias, "propertyAlias"); @@ -89,41 +81,68 @@ namespace Umbraco.Web var model = prop.Value; var asString = model as string; - if (asString.IsNullOrWhiteSpace()) return new MvcHtmlString(string.Empty); + if (asString != null && string.IsNullOrEmpty(asString)) return new MvcHtmlString(string.Empty); - return new MvcHtmlString(RenderPartialViewToString(view, model)); + return html.Partial(view, model); } - [Obsolete("This should not be used, GetGridHtml extensions on HtmlHelper should be used instead")] - private static string RenderPartialViewToString(string viewName, object model) + + //[Obsolete("This should not be used, GetGridHtml methods accepting HtmlHelper as a parameter or GetGridHtml extensions on HtmlHelper should be used instead")] + public static MvcHtmlString GetGridHtml(this IPublishedProperty property, string framework = "bootstrap3") { + var asString = property.Value as string; + if (asString.IsNullOrWhiteSpace()) return new MvcHtmlString(string.Empty); - using (var sw = new StringWriter()) + var htmlHelper = CreateHtmlHelper(property.Value); + return htmlHelper.GetGridHtml(property, framework); + } + + //[Obsolete("This should not be used, GetGridHtml methods accepting HtmlHelper as a parameter or GetGridHtml extensions on HtmlHelper should be used instead")] + public static MvcHtmlString GetGridHtml(this IPublishedContent contentItem) + { + return GetGridHtml(contentItem, "bodyText", "bootstrap3"); + } + + //[Obsolete("This should not be used, GetGridHtml methods accepting HtmlHelper as a parameter or GetGridHtml extensions on HtmlHelper should be used instead")] + public static MvcHtmlString GetGridHtml(this IPublishedContent contentItem, string propertyAlias) + { + Mandate.ParameterNotNullOrEmpty(propertyAlias, "propertyAlias"); + + return GetGridHtml(contentItem, propertyAlias, "bootstrap3"); + } + + //[Obsolete("This should not be used, GetGridHtml methods accepting HtmlHelper as a parameter or GetGridHtml extensions on HtmlHelper should be used instead")] + public static MvcHtmlString GetGridHtml(this IPublishedContent contentItem, string propertyAlias, string framework) + { + Mandate.ParameterNotNullOrEmpty(propertyAlias, "propertyAlias"); + + var prop = contentItem.GetProperty(propertyAlias); + if (prop == null) throw new NullReferenceException("No property type found with alias " + propertyAlias); + var model = prop.Value; + + var asString = model as string; + if (asString != null && string.IsNullOrEmpty(asString)) return new MvcHtmlString(string.Empty); + + var htmlHelper = CreateHtmlHelper(model); + return htmlHelper.GetGridHtml(contentItem, propertyAlias, framework); + } + + //[Obsolete("This shouldn't need to be used but because the obsolete extension methods above don't have access to the current HtmlHelper, we need to create a fake one, unfortunately however this will not pertain the current views viewdata, tempdata or model state so should not be used")] + private static HtmlHelper CreateHtmlHelper(object model) + { + var cc = new ControllerContext { - var cc = new ControllerContext - { - RequestContext = - new RequestContext( - UmbracoContext.Current.HttpContext, - new RouteData() { Route = RouteTable.Routes["Umbraco_default"] }) - }; + RequestContext = UmbracoContext.Current.HttpContext.Request.RequestContext + }; + var viewContext = new ViewContext(cc, new FakeView(), new ViewDataDictionary(model), new TempDataDictionary(), new StringWriter()); + var htmlHelper = new HtmlHelper(viewContext, new ViewPage()); + return htmlHelper; + } - var routeHandler = new RenderRouteHandler(ControllerBuilder.Current.GetControllerFactory(), UmbracoContext.Current); - var routeDef = routeHandler.GetUmbracoRouteDefinition(cc.RequestContext, UmbracoContext.Current.PublishedContentRequest); - cc.RequestContext.RouteData.Values.Add("action", routeDef.ActionName); - cc.RequestContext.RouteData.Values.Add("controller", routeDef.ControllerName); - - var partialView = ViewEngines.Engines.FindPartialView(cc, viewName); - var viewData = new ViewDataDictionary(); - var tempData = new TempDataDictionary(); - - viewData.Model = model; - - var viewContext = new ViewContext(cc, partialView.View, viewData, tempData, sw); - partialView.View.Render(viewContext, sw); - partialView.ViewEngine.ReleaseView(cc, partialView.View); - - return sw.GetStringBuilder().ToString(); + private class FakeView : IView + { + public void Render(ViewContext viewContext, TextWriter writer) + { } } } diff --git a/src/Umbraco.Web/Models/DynamicPublishedContent.cs b/src/Umbraco.Web/Models/DynamicPublishedContent.cs index 8832086a31..83874623b8 100644 --- a/src/Umbraco.Web/Models/DynamicPublishedContent.cs +++ b/src/Umbraco.Web/Models/DynamicPublishedContent.cs @@ -10,6 +10,7 @@ using System.Dynamic; using System.Linq; using System.Runtime.CompilerServices; using System.Web; +using Umbraco.Core.Cache; using Umbraco.Core.Dynamics; using Umbraco.Core.Models; using Umbraco.Core; @@ -57,7 +58,11 @@ namespace Umbraco.Web.Models // these two here have leaked in v6 and so we cannot remove them anymore // without breaking compatibility but... TODO: remove them in v7 + + [Obsolete("Will be removing in future versions")] public DynamicPublishedContentList ChildrenAsList { get { return Children; } } + + [Obsolete("Will be removing in future versions")] public int parentId { get { return PublishedContent.Parent.Id; } } #region DynamicObject @@ -73,7 +78,9 @@ namespace Umbraco.Web.Models /// public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) { - var attempt = DynamicInstanceHelper.TryInvokeMember(this, binder, args, new[] + var runtimeCache = ApplicationContext.Current != null ? ApplicationContext.Current.ApplicationCache.RuntimeCache : new NullCacheProvider(); + + var attempt = DynamicInstanceHelper.TryInvokeMember(runtimeCache, this, binder, args, new[] { typeof(DynamicPublishedContent) }); diff --git a/src/Umbraco.Web/Models/DynamicPublishedContentList.cs b/src/Umbraco.Web/Models/DynamicPublishedContentList.cs index 386f7c7032..ef794737a5 100644 --- a/src/Umbraco.Web/Models/DynamicPublishedContentList.cs +++ b/src/Umbraco.Web/Models/DynamicPublishedContentList.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Dynamic; using Umbraco.Core; +using Umbraco.Core.Cache; using Umbraco.Core.Dynamics; using System.Collections; using System.Reflection; @@ -284,8 +285,10 @@ namespace Umbraco.Web.Models return true; } + var runtimeCache = ApplicationContext.Current != null ? ApplicationContext.Current.ApplicationCache.RuntimeCache : new NullCacheProvider(); + //ok, now lets try to match by member, property, extensino method - var attempt = DynamicInstanceHelper.TryInvokeMember(this, binder, args, new[] + var attempt = DynamicInstanceHelper.TryInvokeMember(runtimeCache, this, binder, args, new[] { typeof (IEnumerable), typeof (DynamicPublishedContentList) diff --git a/src/umbraco.MacroEngines/RazorDynamicNode/ExtensionMethodFinder.cs b/src/umbraco.MacroEngines/RazorDynamicNode/ExtensionMethodFinder.cs index 9030cecde2..7265be6423 100644 --- a/src/umbraco.MacroEngines/RazorDynamicNode/ExtensionMethodFinder.cs +++ b/src/umbraco.MacroEngines/RazorDynamicNode/ExtensionMethodFinder.cs @@ -7,6 +7,8 @@ using System.Web.Compilation; using System.Runtime.CompilerServices; using System.Collections; using System.Linq.Expressions; +using Umbraco.Core; +using Umbraco.Core.Cache; namespace umbraco.MacroEngines { @@ -16,7 +18,9 @@ namespace umbraco.MacroEngines { public static MethodInfo FindExtensionMethod(Type thisType, object[] args, string name, bool argsContainsThis) { - return Umbraco.Core.Dynamics.ExtensionMethodFinder.FindExtensionMethod(thisType, args, name, argsContainsThis); + var runtimeCache = ApplicationContext.Current != null ? ApplicationContext.Current.ApplicationCache.RuntimeCache : new NullCacheProvider(); + + return Umbraco.Core.Dynamics.ExtensionMethodFinder.FindExtensionMethod(runtimeCache, thisType, args, name, argsContainsThis); } } }