From 291f5263684907990c3fe5d4115b644f42b889d3 Mon Sep 17 00:00:00 2001 From: Jeremy Pyne Date: Fri, 18 Oct 2013 11:37:06 -0400 Subject: [PATCH 1/8] Fix to properly read actions from trees.config. --- .../umbraco.presentation/umbraco/Trees/XmlTree.cs | 9 ++++++--- .../umbraco.presentation/umbraco/Trees/loadDictionary.cs | 1 - 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/XmlTree.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/XmlTree.cs index 5d71a2f606..fece08b74b 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/XmlTree.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/XmlTree.cs @@ -199,9 +199,12 @@ namespace umbraco.cms.presentation.Trees xNode.Menu = bTree.RootNodeActions.FindAll(delegate(IAction a) { return true; }); //return a duplicate copy of the list xNode.NodeType = bTree.TreeAlias; xNode.Text = BaseTree.GetTreeHeader(bTree.TreeAlias); - //by default, all root nodes will open the dashboard to their application - xNode.Action = "javascript:" + ClientTools.Scripts.OpenDashboard(bTree.app); - xNode.IsRoot = true; + + // By default the action from the trees.config will be used, if none is specified then the apps dashboard will be used. + var appTreeItem = umbraco.BusinessLogic.ApplicationTree.getByAlias(bTree.TreeAlias); + xNode.Action = appTreeItem == null || String.IsNullOrEmpty(appTreeItem.Action) ? "javascript:" + ClientTools.Scripts.OpenDashboard(bTree.app) : "javascript:" + appTreeItem.Action; + + xNode.IsRoot = true; //generally the tree type and node type are the same but in some cased they are not. xNode.m_treeType = bTree.TreeAlias; return xNode; diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadDictionary.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadDictionary.cs index 33f522ba44..250d07c371 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadDictionary.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadDictionary.cs @@ -38,7 +38,6 @@ namespace umbraco { rootNode.NodeType = "init" + TreeAlias; rootNode.NodeID = "init"; - rootNode.Action = "javascript:openDictionary()"; } protected override void CreateAllowedActions(ref List actions) From 4487e686d792eaf2f9f0bb92498140b6febf093d Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 24 Oct 2013 20:53:01 +0200 Subject: [PATCH 2/8] Core.TypeExtensions - add methods --- src/Umbraco.Core/TypeExtensions.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Umbraco.Core/TypeExtensions.cs b/src/Umbraco.Core/TypeExtensions.cs index c8da74f067..281901504f 100644 --- a/src/Umbraco.Core/TypeExtensions.cs +++ b/src/Umbraco.Core/TypeExtensions.cs @@ -278,6 +278,11 @@ namespace Umbraco.Core return typeof (TBase).IsAssignableFrom(type); } + public static bool Inherits(this Type type, Type tbase) + { + return tbase.IsAssignableFrom(type); + } + public static bool Implements(this Type type) { return typeof (TInterface).IsAssignableFrom(type); From 128a08f169bb01a4c5831ecaabfd65686336bbed Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 23 Oct 2013 10:13:54 +0200 Subject: [PATCH 3/8] Web.Mvc - Refactor UmbracoViewPage, UmbracoTemplatePage, support strongly-typed content Conflicts: src/Umbraco.Web/Umbraco.Web.csproj --- src/Umbraco.Tests/Mvc/UmbracoViewPageTests.cs | 435 ++++++++++++++++++ src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + src/Umbraco.Web/Models/IRenderModel.cs | 9 + src/Umbraco.Web/Models/RenderModel.cs | 7 +- .../Models/RenderModelOfTContent.cs | 36 ++ src/Umbraco.Web/Mvc/UmbracoTemplatePage.cs | 62 +-- .../Mvc/UmbracoTemplatePageOfTContent.cs | 26 ++ src/Umbraco.Web/Mvc/UmbracoViewPage.cs | 191 +------- .../Mvc/UmbracoViewPageOfTModel.cs | 282 ++++++++++++ src/Umbraco.Web/Umbraco.Web.csproj | 6 +- 10 files changed, 812 insertions(+), 243 deletions(-) create mode 100644 src/Umbraco.Tests/Mvc/UmbracoViewPageTests.cs create mode 100644 src/Umbraco.Web/Models/IRenderModel.cs create mode 100644 src/Umbraco.Web/Models/RenderModelOfTContent.cs create mode 100644 src/Umbraco.Web/Mvc/UmbracoTemplatePageOfTContent.cs create mode 100644 src/Umbraco.Web/Mvc/UmbracoViewPageOfTModel.cs diff --git a/src/Umbraco.Tests/Mvc/UmbracoViewPageTests.cs b/src/Umbraco.Tests/Mvc/UmbracoViewPageTests.cs new file mode 100644 index 0000000000..462936c26e --- /dev/null +++ b/src/Umbraco.Tests/Mvc/UmbracoViewPageTests.cs @@ -0,0 +1,435 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Web.Mvc; +using System.Web.Routing; +using System.Xml; +using NUnit.Framework; +using umbraco.BusinessLogic; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Tests.PublishedContent; +using Umbraco.Tests.TestHelpers; +using Umbraco.Tests.TestHelpers.Stubs; +using Umbraco.Web; +using Umbraco.Web.Models; +using Umbraco.Web.Mvc; +using Umbraco.Web.PublishedCache; +using Umbraco.Web.PublishedCache.XmlPublishedCache; +using Umbraco.Web.Routing; + +namespace Umbraco.Tests.Mvc +{ + [TestFixture] + public class UmbracoViewPageTests + { + #region RenderModel To ... + + [Test] + public void RenderModel_To_RenderModel() + { + var content = new ContentType1(null); + var model = new RenderModel(content, CultureInfo.InvariantCulture); + var view = new RenderModelTestPage(); + var viewData = new ViewDataDictionary(model); + + view.ViewContext = GetViewContext(); + view.SetViewDataX(viewData); + + Assert.AreSame(model, view.Model); + } + + [Test] + public void RenderModel_ContentType1_To_ContentType1() + { + var content = new ContentType1(null); + var model = new RenderModel(content, CultureInfo.InvariantCulture); + var view = new ContentType1TestPage(); + var viewData = new ViewDataDictionary(model); + + view.ViewContext = GetViewContext(); + view.SetViewDataX(viewData); + + Assert.IsInstanceOf(view.Model); + } + + [Test] + public void RenderModel_ContentType2_To_ContentType1() + { + var content = new ContentType2(null); + var model = new RenderModel(content, CultureInfo.InvariantCulture); + var view = new ContentType1TestPage(); + var viewData = new ViewDataDictionary(model); + + view.ViewContext = GetViewContext(); + view.SetViewDataX(viewData); + + Assert.IsInstanceOf(view.Model); + } + + [Test] + public void RenderModel_ContentType1_To_ContentType2() + { + var content = new ContentType1(null); + var model = new RenderModel(content, CultureInfo.InvariantCulture); + var view = new ContentType2TestPage(); + var viewData = new ViewDataDictionary(model); + + view.ViewContext = GetViewContext(); + + Assert.Throws(() => view.SetViewDataX(viewData)); + } + + [Test] + public void RenderModel_ContentType1_To_RenderModelOf_ContentType1() + { + var content = new ContentType1(null); + var model = new RenderModel(content, CultureInfo.InvariantCulture); + var view = new RenderModelOfContentType1TestPage(); + var viewData = new ViewDataDictionary(model); + + view.ViewContext = GetViewContext(); + view.SetViewDataX(viewData); + + Assert.IsInstanceOf>(view.Model); + Assert.IsInstanceOf(view.Model.Content); + } + + [Test] + public void RenderModel_ContentType2_To_RenderModelOf_ContentType1() + { + var content = new ContentType2(null); + var model = new RenderModel(content, CultureInfo.InvariantCulture); + var view = new RenderModelOfContentType1TestPage(); + var viewData = new ViewDataDictionary(model); + + view.ViewContext = GetViewContext(); + view.SetViewDataX(viewData); + + Assert.IsInstanceOf>(view.Model); + Assert.IsInstanceOf(view.Model.Content); + } + + [Test] + public void RenderModel_ContentType1_To_RenderModelOf_ContentType2() + { + var content = new ContentType1(null); + var model = new RenderModel(content, CultureInfo.InvariantCulture); + var view = new RenderModelOfContentType2TestPage(); + var viewData = new ViewDataDictionary(model); + + view.ViewContext = GetViewContext(); + + Assert.Throws(() => view.SetViewDataX(viewData)); + } + + #endregion + + #region RenderModelOf To ... + + [Test] + public void RenderModelOf_ContentType1_To_RenderModel() + { + var content = new ContentType1(null); + var model = new RenderModel(content, CultureInfo.InvariantCulture); + var view = new RenderModelTestPage(); + var viewData = new ViewDataDictionary(model); + + view.ViewContext = GetViewContext(); + view.SetViewDataX(viewData); + + Assert.AreSame(model, view.Model); + } + + [Test] + public void RenderModelOf_ContentType1_To_ContentType1() + { + var content = new ContentType1(null); + var model = new RenderModel(content, CultureInfo.InvariantCulture); + var view = new ContentType1TestPage(); + var viewData = new ViewDataDictionary(model); + + view.ViewContext = GetViewContext(); + view.SetViewDataX(viewData); + + Assert.IsInstanceOf(view.Model); + } + + [Test] + public void RenderModelOf_ContentType2_To_ContentType1() + { + var content = new ContentType2(null); + var model = new RenderModel(content, CultureInfo.InvariantCulture); + var view = new ContentType1TestPage(); + var viewData = new ViewDataDictionary(model); + + view.ViewContext = GetViewContext(); + view.SetViewDataX(viewData); + + Assert.IsInstanceOf(view.Model); + } + + [Test] + public void RenderModelOf_ContentType1_To_ContentType2() + { + var content = new ContentType1(null); + var model = new RenderModel(content, CultureInfo.InvariantCulture); + var view = new ContentType2TestPage(); + var viewData = new ViewDataDictionary(model); + + view.ViewContext = GetViewContext(); + Assert.Throws(() => view.SetViewDataX(viewData)); + } + + [Test] + public void RenderModelOf_ContentType1_To_RenderModelOf_ContentType1() + { + var content = new ContentType1(null); + var model = new RenderModel(content, CultureInfo.InvariantCulture); + var view = new RenderModelOfContentType1TestPage(); + var viewData = new ViewDataDictionary(model); + + view.ViewContext = GetViewContext(); + view.SetViewDataX(viewData); + + Assert.IsInstanceOf>(view.Model); + Assert.IsInstanceOf(view.Model.Content); + } + + [Test] + public void RenderModelOf_ContentType2_To_RenderModelOf_ContentType1() + { + var content = new ContentType2(null); + var model = new RenderModel(content, CultureInfo.InvariantCulture); + var view = new RenderModelOfContentType1TestPage(); + var viewData = new ViewDataDictionary(model); + + view.ViewContext = GetViewContext(); + view.SetViewDataX(viewData); + + Assert.IsInstanceOf>(view.Model); + Assert.IsInstanceOf(view.Model.Content); + } + + [Test] + public void RenderModelOf_ContentType1_To_RenderModelOf_ContentType2() + { + var content = new ContentType1(null); + var model = new RenderModel(content, CultureInfo.InvariantCulture); + var view = new RenderModelOfContentType2TestPage(); + var viewData = new ViewDataDictionary(model); + + view.ViewContext = GetViewContext(); + Assert.Throws(() => view.SetViewDataX(viewData)); + } + + #endregion + + #region ContentType To ... + + [Test] + public void ContentType1_To_RenderModel() + { + var content = new ContentType1(null); + var view = new RenderModelTestPage(); + var viewData = new ViewDataDictionary(content); + + view.ViewContext = GetViewContext(); + view.SetViewDataX(viewData); + + Assert.IsInstanceOf(view.Model); + } + + [Test] + public void ContentType1_To_RenderModelOf_ContentType1() + { + var content = new ContentType1(null); + var view = new RenderModelOfContentType1TestPage(); + var viewData = new ViewDataDictionary(content); + + view.ViewContext = GetViewContext(); + view.SetViewDataX(viewData); + + Assert.IsInstanceOf>(view.Model); + Assert.IsInstanceOf(view.Model.Content); + } + + [Test] + public void ContentType2_To_RenderModelOf_ContentType1() + { + var content = new ContentType2(null); + var view = new RenderModelOfContentType1TestPage(); + var viewData = new ViewDataDictionary(content); + + view.ViewContext = GetViewContext(); + view.SetViewDataX(viewData); + + Assert.IsInstanceOf>(view.Model); + Assert.IsInstanceOf(view.Model.Content); + } + + [Test] + public void ContentType1_To_RenderModelOf_ContentType2() + { + var content = new ContentType1(null); + var view = new RenderModelOfContentType2TestPage(); + var viewData = new ViewDataDictionary(content); + + view.ViewContext = GetViewContext(); + Assert.Throws(() =>view.SetViewDataX(viewData)); + } + + [Test] + public void ContentType1_To_ContentType1() + { + var content = new ContentType1(null); + var view = new ContentType1TestPage(); + var viewData = new ViewDataDictionary(content); + + view.ViewContext = GetViewContext(); + view.SetViewDataX(viewData); + + Assert.IsInstanceOf(view.Model); + } + + [Test] + public void ContentType1_To_ContentType2() + { + var content = new ContentType1(null); + var view = new ContentType2TestPage(); + var viewData = new ViewDataDictionary(content); + + view.ViewContext = GetViewContext(); + Assert.Throws(() => view.SetViewDataX(viewData)); + } + + [Test] + public void ContentType2_To_ContentType1() + { + var content = new ContentType2(null); + var view = new ContentType1TestPage(); + var viewData = new ViewDataDictionary(content); + + view.ViewContext = GetViewContext(); + view.SetViewDataX(viewData); + + Assert.IsInstanceOf(view.Model); + } + + #endregion + + #region Test elements + + public class TestPage : UmbracoViewPage + { + public override void Execute() + { + throw new NotImplementedException(); + } + + public void SetViewDataX(ViewDataDictionary viewData) + { + SetViewData(viewData); + } + } + + public class RenderModelTestPage : TestPage + { } + + public class RenderModelOfContentType1TestPage : TestPage> + { } + + public class RenderModelOfContentType2TestPage : TestPage> + { } + + public class ContentType1TestPage : TestPage + { } + + public class ContentType2TestPage : TestPage + { } + + public class ContentType1 : PublishedContentWrapped + { + public ContentType1(IPublishedContent content) : base(content) {} + } + + public class ContentType2 : ContentType1 + { + public ContentType2(IPublishedContent content) : base(content) { } + } + + #endregion + + #region Test helpers + + ViewContext GetViewContext() + { + var umbracoContext = GetUmbracoContext("/dang", 0); + + var urlProvider = new UrlProvider(umbracoContext, new IUrlProvider[] { new DefaultUrlProvider() }); + var routingContext = new RoutingContext( + umbracoContext, + Enumerable.Empty(), + new FakeLastChanceFinder(), + urlProvider); + umbracoContext.RoutingContext = routingContext; + + var request = new PublishedContentRequest(new Uri("http://localhost/dang"), routingContext); + request.Culture = CultureInfo.InvariantCulture; + umbracoContext.PublishedContentRequest = request; + + var context = new ViewContext(); + context.RouteData = new RouteData(); + context.RouteData.DataTokens.Add("umbraco-context", umbracoContext); + + return context; + } + + protected UmbracoContext GetUmbracoContext(string url, int templateId, RouteData routeData = null, bool setSingleton = false) + { + var cache = new PublishedContentCache(); + + //cache.GetXmlDelegate = (context, preview) => + //{ + // var doc = new XmlDocument(); + // doc.LoadXml(GetXmlContent(templateId)); + // return doc; + //}; + + //PublishedContentCache.UnitTesting = true; + + // ApplicationContext.Current = new ApplicationContext(false) { IsReady = true }; + var appCtx = new ApplicationContext(false) { IsReady = true }; + + var ctx = new UmbracoContext( + GetHttpContextFactory(url, routeData).HttpContext, + appCtx, + new PublishedCaches(cache, new PublishedMediaCache())); + + //if (setSingleton) + //{ + // UmbracoContext.Current = ctx; + //} + + return ctx; + } + + protected FakeHttpContextFactory GetHttpContextFactory(string url, RouteData routeData = null) + { + var factory = routeData != null + ? new FakeHttpContextFactory(url, routeData) + : new FakeHttpContextFactory(url); + + + //set the state helper + StateHelper.HttpContext = factory.HttpContext; + + return factory; + } + + #endregion + } +} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index f1fb84e71c..af40388b00 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -145,6 +145,7 @@ + diff --git a/src/Umbraco.Web/Models/IRenderModel.cs b/src/Umbraco.Web/Models/IRenderModel.cs new file mode 100644 index 0000000000..21e3f765ac --- /dev/null +++ b/src/Umbraco.Web/Models/IRenderModel.cs @@ -0,0 +1,9 @@ +using Umbraco.Core.Models; + +namespace Umbraco.Web.Models +{ + public interface IRenderModel + { + IPublishedContent Content { get; } + } +} diff --git a/src/Umbraco.Web/Models/RenderModel.cs b/src/Umbraco.Web/Models/RenderModel.cs index f88bcf1ad5..ff2ea8a674 100644 --- a/src/Umbraco.Web/Models/RenderModel.cs +++ b/src/Umbraco.Web/Models/RenderModel.cs @@ -1,13 +1,14 @@ using System; using System.Globalization; using Umbraco.Core.Models; +using Umbraco.Web.Mvc; namespace Umbraco.Web.Models { /// /// Represents the model for the current rendering page in Umbraco /// - public class RenderModel + public class RenderModel : IRenderModel { /// /// Constructor specifying both the IPublishedContent and the CultureInfo @@ -16,7 +17,7 @@ namespace Umbraco.Web.Models /// public RenderModel(IPublishedContent content, CultureInfo culture) { - if (content == null) throw new ArgumentNullException("content"); + if (content == null) throw new ArgumentNullException("content"); if (culture == null) throw new ArgumentNullException("culture"); Content = content; CurrentCulture = culture; @@ -28,7 +29,7 @@ namespace Umbraco.Web.Models /// public RenderModel(IPublishedContent content) { - if (content == null) throw new ArgumentNullException("content"); + if (content == null) throw new ArgumentNullException("content"); if (UmbracoContext.Current == null) { throw new InvalidOperationException("Cannot construct a RenderModel without specifying a CultureInfo when no UmbracoContext has been initialized"); diff --git a/src/Umbraco.Web/Models/RenderModelOfTContent.cs b/src/Umbraco.Web/Models/RenderModelOfTContent.cs new file mode 100644 index 0000000000..9645c82f14 --- /dev/null +++ b/src/Umbraco.Web/Models/RenderModelOfTContent.cs @@ -0,0 +1,36 @@ +using System.Globalization; +using Umbraco.Core.Models; +using Umbraco.Web.Mvc; + +namespace Umbraco.Web.Models +{ + public class RenderModel : RenderModel + where TContent : class, IPublishedContent + { + /// + /// Constructor specifying both the IPublishedContent and the CultureInfo + /// + /// + /// + public RenderModel(TContent content, CultureInfo culture) + : base(content, culture) + { + Content = content; + } + + /// + /// Constructor to set the IPublishedContent and the CurrentCulture is set by the UmbracoContext + /// + /// + public RenderModel(TContent content) + : base(content) + { + Content = content; + } + + /// + /// Returns the current IPublishedContent object + /// + public new TContent Content { get; private set; } + } +} diff --git a/src/Umbraco.Web/Mvc/UmbracoTemplatePage.cs b/src/Umbraco.Web/Mvc/UmbracoTemplatePage.cs index 231c6e12b2..b9f9f4690f 100644 --- a/src/Umbraco.Web/Mvc/UmbracoTemplatePage.cs +++ b/src/Umbraco.Web/Mvc/UmbracoTemplatePage.cs @@ -1,13 +1,4 @@ -using System; -using StackExchange.Profiling; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Dictionary; using Umbraco.Core.Dynamics; -using Umbraco.Core.IO; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.Profiling; using Umbraco.Web.Models; namespace Umbraco.Web.Mvc @@ -17,58 +8,19 @@ namespace Umbraco.Web.Mvc /// public abstract class UmbracoTemplatePage : UmbracoViewPage { - protected UmbracoTemplatePage() - { - - } - - protected override void InitializePage() - { - base.InitializePage(); - //set the model to the current node if it is not set, this is generally not the case - if (Model != null) - { - ////this.ViewData.Model = Model; - //var backingItem = new DynamicBackingItem(Model.CurrentNode); - var dynamicNode = new DynamicPublishedContent(Model.Content); - CurrentPage = dynamicNode.AsDynamic(); - } - } - - protected override void SetViewData(System.Web.Mvc.ViewDataDictionary viewData) - { - //Here we're going to check if the viewData's model is of IPublishedContent, this is basically just a helper for - //syntax on the front-end so we can just pass in an IPublishedContent object to partial views that inherit from - //UmbracoTemplatePage. Then we're going to manually contruct a RenderViewModel to pass back in to SetViewData - if (viewData.Model is IPublishedContent) - { - //change the model to a RenderModel and auto set the culture - viewData.Model = new RenderModel((IPublishedContent)viewData.Model, UmbracoContext.PublishedContentRequest.Culture); - } - - base.SetViewData(viewData); - } + private object _currentPage; /// - /// Returns the a DynamicPublishedContent object + /// Returns the content as a dynamic object /// - public dynamic CurrentPage { get; private set; } - - private UmbracoHelper _helper; - - /// - /// Gets an UmbracoHelper - /// - /// - /// This ensures that the UmbracoHelper is constructed with the content model of this view - /// - public override UmbracoHelper Umbraco + public dynamic CurrentPage { get { - return _helper ?? (_helper = Model == null - ? new UmbracoHelper(UmbracoContext) - : new UmbracoHelper(UmbracoContext, Model.Content)); + // it's invalid to create a DynamicPublishedContent around a null content anyway + // fixme - should we return null or DynamicNull.Null? + if (Model == null || Model.Content == null) return null; + return _currentPage ?? (_currentPage = Model.Content.AsDynamic()); } } } diff --git a/src/Umbraco.Web/Mvc/UmbracoTemplatePageOfTContent.cs b/src/Umbraco.Web/Mvc/UmbracoTemplatePageOfTContent.cs new file mode 100644 index 0000000000..d25fdb9141 --- /dev/null +++ b/src/Umbraco.Web/Mvc/UmbracoTemplatePageOfTContent.cs @@ -0,0 +1,26 @@ +using Umbraco.Core.Dynamics; +using Umbraco.Core.Models; +using Umbraco.Web.Models; + +namespace Umbraco.Web.Mvc +{ + public abstract class UmbracoTemplatePage : UmbracoViewPage> + where TContent : class, IPublishedContent + { + private object _currentPage; + + /// + /// Returns the content as a dynamic object + /// + public dynamic CurrentPage + { + get + { + // it's invalid to create a DynamicPublishedContent around a null content anyway + // fixme - should we return null or DynamicNull.Null? + if (Model == null || Model.Content == null) return null; + return _currentPage ?? (_currentPage = Model.Content.AsDynamic()); + } + } + } +} diff --git a/src/Umbraco.Web/Mvc/UmbracoViewPage.cs b/src/Umbraco.Web/Mvc/UmbracoViewPage.cs index 0f56fe6e55..4e067c2c82 100644 --- a/src/Umbraco.Web/Mvc/UmbracoViewPage.cs +++ b/src/Umbraco.Web/Mvc/UmbracoViewPage.cs @@ -1,188 +1,11 @@ -using System; +using System; +using System.Collections.Generic; +using System.Linq; using System.Text; -using System.Web; -using System.Web.Mvc; -using System.Web.WebPages; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.IO; -using Umbraco.Web.Routing; +using Umbraco.Core.Models; namespace Umbraco.Web.Mvc { - /// - /// The View that umbraco front-end views inherit from - /// - public abstract class UmbracoViewPage : WebViewPage - { - protected UmbracoViewPage() - { - - } - - /// - /// Returns the current UmbracoContext - /// - public UmbracoContext UmbracoContext - { - get - { - //we should always try to return the context from the data tokens just in case its a custom context and not - //using the UmbracoContext.Current. - //we will fallback to the singleton if necessary. - if (ViewContext.RouteData.DataTokens.ContainsKey("umbraco-context")) - { - return (UmbracoContext)ViewContext.RouteData.DataTokens.GetRequiredObject("umbraco-context"); - } - //next check if it is a child action and see if the parent has it set in data tokens - if (ViewContext.IsChildAction) - { - if (ViewContext.ParentActionViewContext.RouteData.DataTokens.ContainsKey("umbraco-context")) - { - return (UmbracoContext)ViewContext.ParentActionViewContext.RouteData.DataTokens.GetRequiredObject("umbraco-context"); - } - } - - //lastly, we will use the singleton, the only reason this should ever happen is is someone is rendering a page that inherits from this - //class and are rendering it outside of the normal Umbraco routing process. Very unlikely. - return UmbracoContext.Current; - } - } - - /// - /// Returns the current ApplicationContext - /// - public ApplicationContext ApplicationContext - { - get { return UmbracoContext.Application; } - } - - /// - /// Returns the current PublishedContentRequest - /// - internal PublishedContentRequest PublishedContentRequest - { - get - { - //we should always try to return the object from the data tokens just in case its a custom object and not - //using the UmbracoContext.Current. - //we will fallback to the singleton if necessary. - if (ViewContext.RouteData.DataTokens.ContainsKey("umbraco-doc-request")) - { - return (PublishedContentRequest)ViewContext.RouteData.DataTokens.GetRequiredObject("umbraco-doc-request"); - } - //next check if it is a child action and see if the parent has it set in data tokens - if (ViewContext.IsChildAction) - { - if (ViewContext.ParentActionViewContext.RouteData.DataTokens.ContainsKey("umbraco-doc-request")) - { - return (PublishedContentRequest)ViewContext.ParentActionViewContext.RouteData.DataTokens.GetRequiredObject("umbraco-doc-request"); - } - } - - //lastly, we will use the singleton, the only reason this should ever happen is is someone is rendering a page that inherits from this - //class and are rendering it outside of the normal Umbraco routing process. Very unlikely. - return UmbracoContext.Current.PublishedContentRequest; - } - } - - private UmbracoHelper _helper; - - /// - /// Gets an UmbracoHelper - /// - /// - /// This constructs the UmbracoHelper with the content model of the page routed to - /// - public virtual UmbracoHelper Umbraco - { - get { return _helper ?? (_helper = new UmbracoHelper(UmbracoContext)); } - } - - /// - /// Ensure that the current view context is added to the route data tokens so we can extract it if we like - /// - /// - /// Currently this is required by mvc macro engines - /// - protected override void InitializePage() - { - base.InitializePage(); - if (!ViewContext.IsChildAction) - { - if (!ViewContext.RouteData.DataTokens.ContainsKey(Constants.DataTokenCurrentViewContext)) - { - ViewContext.RouteData.DataTokens.Add(Constants.DataTokenCurrentViewContext, this.ViewContext); - } - } - - } - - /// - /// This will detect the end /body tag and insert the preview badge if in preview mode - /// - /// - public override void WriteLiteral(object value) - { - // filter / add preview banner - if (Response.ContentType.InvariantEquals("text/html")) // ASP.NET default value - { - if (UmbracoContext.Current.IsDebug || UmbracoContext.Current.InPreviewMode) - { - var text = value.ToString().ToLowerInvariant(); - var pos = text.IndexOf("", StringComparison.InvariantCultureIgnoreCase); - - if (pos > -1) - { - string markupToInject; - - if (UmbracoContext.Current.InPreviewMode) - { - // creating previewBadge markup - markupToInject = - String.Format(UmbracoSettings.PreviewBadge, - IOHelper.ResolveUrl(SystemDirectories.Umbraco), - IOHelper.ResolveUrl(SystemDirectories.UmbracoClient), - Server.UrlEncode(UmbracoContext.Current.HttpContext.Request.Path)); - } - else - { - // creating mini-profiler markup - markupToInject = Html.RenderProfiler().ToHtmlString(); - } - - var sb = new StringBuilder(text); - sb.Insert(pos, markupToInject); - - base.WriteLiteral(sb.ToString()); - return; - } - } - } - - base.WriteLiteral(value); - - - } - - public HelperResult RenderSection(string name, Func defaultContents) - { - return WebViewPageExtensions.RenderSection(this, name, defaultContents); - } - - public HelperResult RenderSection(string name, HelperResult defaultContents) - { - return WebViewPageExtensions.RenderSection(this, name, defaultContents); - } - - public HelperResult RenderSection(string name, string defaultContents) - { - return WebViewPageExtensions.RenderSection(this, name, defaultContents); - } - - public HelperResult RenderSection(string name, IHtmlString defaultContents) - { - return WebViewPageExtensions.RenderSection(this, name, defaultContents); - } - } -} \ No newline at end of file + public abstract class UmbracoViewPage : UmbracoViewPage + { } +} diff --git a/src/Umbraco.Web/Mvc/UmbracoViewPageOfTModel.cs b/src/Umbraco.Web/Mvc/UmbracoViewPageOfTModel.cs new file mode 100644 index 0000000000..f13c8b32ef --- /dev/null +++ b/src/Umbraco.Web/Mvc/UmbracoViewPageOfTModel.cs @@ -0,0 +1,282 @@ +using System; +using System.Text; +using System.Web; +using System.Web.Mvc; +using System.Web.WebPages; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.IO; +using Umbraco.Core.Models; +using Umbraco.Web.Models; +using Umbraco.Web.Routing; + +namespace Umbraco.Web.Mvc +{ + /// + /// The View that umbraco front-end views inherit from + /// + public abstract class UmbracoViewPage : WebViewPage + { + /// + /// Returns the current UmbracoContext + /// + public UmbracoContext UmbracoContext + { + get + { + //we should always try to return the context from the data tokens just in case its a custom context and not + //using the UmbracoContext.Current. + //we will fallback to the singleton if necessary. + if (ViewContext.RouteData.DataTokens.ContainsKey("umbraco-context")) + { + return (UmbracoContext)ViewContext.RouteData.DataTokens.GetRequiredObject("umbraco-context"); + } + //next check if it is a child action and see if the parent has it set in data tokens + if (ViewContext.IsChildAction) + { + if (ViewContext.ParentActionViewContext.RouteData.DataTokens.ContainsKey("umbraco-context")) + { + return (UmbracoContext)ViewContext.ParentActionViewContext.RouteData.DataTokens.GetRequiredObject("umbraco-context"); + } + } + + //lastly, we will use the singleton, the only reason this should ever happen is is someone is rendering a page that inherits from this + //class and are rendering it outside of the normal Umbraco routing process. Very unlikely. + return UmbracoContext.Current; + } + } + + /// + /// Returns the current ApplicationContext + /// + public ApplicationContext ApplicationContext + { + get { return UmbracoContext.Application; } + } + + /// + /// Returns the current PublishedContentRequest + /// + internal PublishedContentRequest PublishedContentRequest + { + get + { + //we should always try to return the object from the data tokens just in case its a custom object and not + //using the UmbracoContext.Current. + //we will fallback to the singleton if necessary. + if (ViewContext.RouteData.DataTokens.ContainsKey("umbraco-doc-request")) + { + return (PublishedContentRequest)ViewContext.RouteData.DataTokens.GetRequiredObject("umbraco-doc-request"); + } + //next check if it is a child action and see if the parent has it set in data tokens + if (ViewContext.IsChildAction) + { + if (ViewContext.ParentActionViewContext.RouteData.DataTokens.ContainsKey("umbraco-doc-request")) + { + return (PublishedContentRequest)ViewContext.ParentActionViewContext.RouteData.DataTokens.GetRequiredObject("umbraco-doc-request"); + } + } + + //lastly, we will use the singleton, the only reason this should ever happen is is someone is rendering a page that inherits from this + //class and are rendering it outside of the normal Umbraco routing process. Very unlikely. + return UmbracoContext.Current.PublishedContentRequest; + } + } + + private UmbracoHelper _helper; + + /// + /// Gets an UmbracoHelper + /// + /// + /// This constructs the UmbracoHelper with the content model of the page routed to + /// + public virtual UmbracoHelper Umbraco + { + get + { + if (_helper == null) + { + var model = ViewData.Model; + var content = model as IPublishedContent; + if (content == null && model is IRenderModel) + content = ((IRenderModel) model).Content; + _helper = content == null + ? new UmbracoHelper(UmbracoContext) + : new UmbracoHelper(UmbracoContext, content); + } + return _helper; + } + } + + /// + /// Ensure that the current view context is added to the route data tokens so we can extract it if we like + /// + /// + /// Currently this is required by mvc macro engines + /// + protected override void InitializePage() + { + base.InitializePage(); + if (ViewContext.IsChildAction == false) + { + if (ViewContext.RouteData.DataTokens.ContainsKey(Constants.DataTokenCurrentViewContext) == false) + { + ViewContext.RouteData.DataTokens.Add(Constants.DataTokenCurrentViewContext, ViewContext); + } + } + + } + + // maps model + protected override void SetViewData(ViewDataDictionary viewData) + { + var source = viewData.Model; + if (source == null) + { + base.SetViewData(viewData); + return; + } + + var sourceType = source.GetType(); + var targetType = typeof (TModel); + + // it types already match, nothing to do + if (sourceType.Inherits()) // includes == + { + base.SetViewData(viewData); + return; + } + + // try to grab the content + // if no content is found, return, nothing we can do + var sourceContent = source as IPublishedContent; + if (sourceContent == null && sourceType.Implements()) + { + sourceContent = ((IRenderModel)source).Content; + } + if (sourceContent == null) + { + var attempt = source.TryConvertTo(); + if (attempt.Success) sourceContent = attempt.Result; + } + + var ok = sourceContent != null; + if (sourceContent != null) + { + // try to grab the culture + // using context's culture by default + var culture = UmbracoContext.PublishedContentRequest.Culture; + var sourceRenderModel = source as RenderModel; + if (sourceRenderModel != null) + culture = sourceRenderModel.CurrentCulture; + + // reassign the model depending on its type + if (targetType.Implements()) + { + // it TModel implements IPublishedContent then use the content + // provided that the content is of the proper type + if ((sourceContent is TModel) == false) + throw new InvalidCastException(string.Format("Cannot cast source content type {0} to view model type {1}.", + sourceContent.GetType(), targetType)); + viewData.Model = sourceContent; + } + else if (targetType == typeof(RenderModel)) + { + // if TModel is a basic RenderModel just create it + viewData.Model = new RenderModel(sourceContent, culture); + } + else if (targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(RenderModel<>)) + { + // if TModel is a strongly-typed RenderModel<> then create it + // provided that the content is of the proper type + var targetContentType = targetType.GetGenericArguments()[0]; + if ((sourceContent.GetType().Inherits(targetContentType)) == false) + throw new InvalidCastException(string.Format("Cannot cast source content type {0} to view model content type {1}.", + sourceContent.GetType(), targetContentType)); + viewData.Model = Activator.CreateInstance(targetType, sourceContent, culture); + } + else + { + ok = false; + } + } + + if (ok == false) + { + // last chance : try to convert + var attempt = source.TryConvertTo(); + if (attempt.Success) viewData.Model = attempt.Result; + } + + base.SetViewData(viewData); + } + + /// + /// This will detect the end /body tag and insert the preview badge if in preview mode + /// + /// + public override void WriteLiteral(object value) + { + // filter / add preview banner + if (Response.ContentType.InvariantEquals("text/html")) // ASP.NET default value + { + if (UmbracoContext.Current.IsDebug || UmbracoContext.Current.InPreviewMode) + { + var text = value.ToString().ToLowerInvariant(); + var pos = text.IndexOf("", StringComparison.InvariantCultureIgnoreCase); + + if (pos > -1) + { + string markupToInject; + + if (UmbracoContext.Current.InPreviewMode) + { + // creating previewBadge markup + markupToInject = + String.Format(UmbracoSettings.PreviewBadge, + IOHelper.ResolveUrl(SystemDirectories.Umbraco), + IOHelper.ResolveUrl(SystemDirectories.UmbracoClient), + Server.UrlEncode(UmbracoContext.Current.HttpContext.Request.Path)); + } + else + { + // creating mini-profiler markup + markupToInject = Html.RenderProfiler().ToHtmlString(); + } + + var sb = new StringBuilder(text); + sb.Insert(pos, markupToInject); + + base.WriteLiteral(sb.ToString()); + return; + } + } + } + + base.WriteLiteral(value); + + + } + + public HelperResult RenderSection(string name, Func defaultContents) + { + return WebViewPageExtensions.RenderSection(this, name, defaultContents); + } + + public HelperResult RenderSection(string name, HelperResult defaultContents) + { + return WebViewPageExtensions.RenderSection(this, name, defaultContents); + } + + public HelperResult RenderSection(string name, string defaultContents) + { + return WebViewPageExtensions.RenderSection(this, name, defaultContents); + } + + public HelperResult RenderSection(string name, IHtmlString defaultContents) + { + return WebViewPageExtensions.RenderSection(this, name, defaultContents); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 0f2e5e5ce2..85dc520e4f 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -294,6 +294,10 @@ + + + + @@ -421,7 +425,7 @@ - + From 9a3b70149cd2aa3f6f8a78b53956fb58e9642b72 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 28 Oct 2013 11:00:14 +1100 Subject: [PATCH 4/8] Fixes packaging service to export data type's property with the ID as the property editor ID --- src/Umbraco.Core/Services/PackagingService.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Services/PackagingService.cs b/src/Umbraco.Core/Services/PackagingService.cs index 0199a76180..efa6ae9fe6 100644 --- a/src/Umbraco.Core/Services/PackagingService.cs +++ b/src/Umbraco.Core/Services/PackagingService.cs @@ -720,7 +720,8 @@ namespace Umbraco.Core.Services var xml = new XElement("DataType", prevalues); xml.Add(new XAttribute("Name", dataTypeDefinition.Name)); - xml.Add(new XAttribute("Id", dataTypeDefinition.Id)); + //The 'ID' when exporting is actually the property editor alias (in pre v7 it was the IDataType GUID id) + xml.Add(new XAttribute("Id", dataTypeDefinition.ControlId)); xml.Add(new XAttribute("Definition", dataTypeDefinition.Key)); xml.Add(new XAttribute("DatabaseType", dataTypeDefinition.DatabaseType.ToString())); From c66d49dbfe7eec1433a56eb6a9640a0e6b056a7c Mon Sep 17 00:00:00 2001 From: Stephan Date: Mon, 28 Oct 2013 01:00:29 +0100 Subject: [PATCH 5/8] PublishedContent - remove useles code in model factory --- .../Models/PublishedContent/PublishedContentModelFactoryImpl.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentModelFactoryImpl.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentModelFactoryImpl.cs index af2bdd6859..7bc0343add 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentModelFactoryImpl.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentModelFactoryImpl.cs @@ -22,8 +22,6 @@ namespace Umbraco.Core.Models.PublishedContent foreach (var type in types) { - if (type.Inherits() == false) - throw new InvalidOperationException(string.Format("Type {0} is marked with PublishedContentModel attribute but does not inherit from PublishedContentExtended.", type.FullName)); var constructor = type.GetConstructor(ctorArgTypes); if (constructor == null) throw new InvalidOperationException(string.Format("Type {0} is missing a public constructor with one argument of type IPublishedContent.", type.FullName)); From 73b7729b82e5819395f4812cab65ac32aac09aac Mon Sep 17 00:00:00 2001 From: Stephan Date: Mon, 28 Oct 2013 01:01:20 +0100 Subject: [PATCH 6/8] Standalone - fix so it works again --- .../Standalone/StandaloneApplication.cs | 26 ++++++++++++++++++- .../Standalone/StandaloneBootManager.cs | 2 +- .../Standalone/StandaloneHttpContext.cs | 5 +++- src/Umbraco.Web/UmbracoContext.cs | 12 ++++++--- 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web/Standalone/StandaloneApplication.cs b/src/Umbraco.Web/Standalone/StandaloneApplication.cs index cdaf8718c1..64e2975dc2 100644 --- a/src/Umbraco.Web/Standalone/StandaloneApplication.cs +++ b/src/Umbraco.Web/Standalone/StandaloneApplication.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using Umbraco.Core; +using Umbraco.Core.ObjectResolution; namespace Umbraco.Web.Standalone { @@ -49,17 +50,40 @@ namespace Umbraco.Web.Standalone /// /// Starts the application. /// - public void Start() + public void Start(bool noerr = false) { lock (AppLock) { if (_started) + { + if (noerr) return; throw new InvalidOperationException("Application has already started."); + } Application_Start(this, EventArgs.Empty); _started = true; } } + public void Terminate(bool noerr = false) + { + lock (AppLock) + { + if (_started == false) + { + if (noerr) return; + throw new InvalidOperationException("Application has already been terminated."); + } + + ApplicationContext.Current.DisposeIfDisposable(); // should reset resolution, clear caches & resolvers... + ApplicationContext.Current = null; + UmbracoContext.Current.DisposeIfDisposable(); // dunno + UmbracoContext.Current = null; + + _started = false; + _application = null; + } + } + #endregion #region IApplicationEventHandler management diff --git a/src/Umbraco.Web/Standalone/StandaloneBootManager.cs b/src/Umbraco.Web/Standalone/StandaloneBootManager.cs index 8e69797a4d..b97c06ca68 100644 --- a/src/Umbraco.Web/Standalone/StandaloneBootManager.cs +++ b/src/Umbraco.Web/Standalone/StandaloneBootManager.cs @@ -80,7 +80,7 @@ namespace Umbraco.Web.Standalone base.FreezeResolution(); var httpContext = new StandaloneHttpContext(); - UmbracoContext.EnsureContext(httpContext, ApplicationContext.Current); + UmbracoContext.EnsureContext(httpContext, ApplicationContext.Current, false, false); } } } diff --git a/src/Umbraco.Web/Standalone/StandaloneHttpContext.cs b/src/Umbraco.Web/Standalone/StandaloneHttpContext.cs index ac34e405e5..dec95dd372 100644 --- a/src/Umbraco.Web/Standalone/StandaloneHttpContext.cs +++ b/src/Umbraco.Web/Standalone/StandaloneHttpContext.cs @@ -65,6 +65,9 @@ namespace Umbraco.Web.Standalone internal class StandaloneHttpRequest : HttpRequestBase { - + public override Uri Url + { + get { return new Uri("http://localhost"); } + } } } diff --git a/src/Umbraco.Web/UmbracoContext.cs b/src/Umbraco.Web/UmbracoContext.cs index bffbf76930..47c55de9e0 100644 --- a/src/Umbraco.Web/UmbracoContext.cs +++ b/src/Umbraco.Web/UmbracoContext.cs @@ -54,7 +54,12 @@ namespace Umbraco.Web /// public static UmbracoContext EnsureContext(HttpContextBase httpContext, ApplicationContext applicationContext) { - return EnsureContext(httpContext, applicationContext, false); + return EnsureContext(httpContext, applicationContext, false, null); + } + + public static UmbracoContext EnsureContext(HttpContextBase httpContext, ApplicationContext applicationContext, bool replaceContext) + { + return EnsureContext(httpContext, applicationContext, replaceContext, null); } /// @@ -77,7 +82,7 @@ namespace Umbraco.Web /// during the startup process as well. /// See: http://issues.umbraco.org/issue/U4-1890, http://issues.umbraco.org/issue/U4-1717 /// - public static UmbracoContext EnsureContext(HttpContextBase httpContext, ApplicationContext applicationContext, bool replaceContext) + public static UmbracoContext EnsureContext(HttpContextBase httpContext, ApplicationContext applicationContext, bool replaceContext, bool? preview) { if (UmbracoContext.Current != null) { @@ -89,7 +94,8 @@ namespace Umbraco.Web var umbracoContext = new UmbracoContext( httpContext, applicationContext, - PublishedCachesResolver.Current.Caches); + PublishedCachesResolver.Current.Caches, + preview); // create the nice urls provider // there's one per request because there are some behavior parameters that can be changed From 6cfb864055d3f6222d550fb3474fd86bb906e919 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 28 Oct 2013 11:23:09 +1100 Subject: [PATCH 7/8] Fixes DataTypeDefinitionRepository to properly delete a data type including it's pre-values --- .../Persistence/Repositories/DataTypeDefinitionRepository.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs index 7abcc8e4b0..7cd7069751 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs @@ -204,6 +204,9 @@ namespace Umbraco.Core.Persistence.Repositories Database.Delete("WHERE id = @Id", new { Id = dto.Id }); } + //Delete the pre-values + Database.Delete("WHERE datatypeNodeId = @Id", new {Id = entity.Id}); + //Delete Content specific data Database.Delete("WHERE nodeId = @Id", new { Id = entity.Id }); From 49d5791825bfb4293e9daafdb378cc463e49aa5f Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 28 Oct 2013 12:35:50 +1100 Subject: [PATCH 8/8] Adds DescendantsOrSelf extensions on ienumerable to published content extensions --- src/Umbraco.Web/PublishedContentExtensions.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 8de72a48bb..b4d07b8baa 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -1065,6 +1065,35 @@ namespace Umbraco.Web #region Axes: descendants, descendants-or-self + /// + /// Returns all DescendantsOrSelf of all content referenced + /// + /// + /// + /// + /// + /// This can be useful in order to return all nodes in an entire site by a type when combined with TypedContentAtRoot + /// + public static IEnumerable DescendantsOrSelf(this IEnumerable parentNodes, string docTypeAlias) + { + return parentNodes.SelectMany(x => x.DescendantsOrSelf(docTypeAlias)); + } + + /// + /// Returns all DescendantsOrSelf of all content referenced + /// + /// + /// + /// + /// This can be useful in order to return all nodes in an entire site by a type when combined with TypedContentAtRoot + /// + public static IEnumerable DescendantsOrSelf(this IEnumerable parentNodes) + where T : class, IPublishedContent + { + return parentNodes.SelectMany(x => x.DescendantsOrSelf()); + } + + // as per XPath 1.0 specs §2.2, // - the descendant axis contains the descendants of the context node; a descendant is a child or a child of a child and so on; thus // the descendant axis never contains attribute or namespace nodes.