diff --git a/src/Umbraco.Core/CoreBootManager.cs b/src/Umbraco.Core/CoreBootManager.cs index 76045b011e..cbc52e79de 100644 --- a/src/Umbraco.Core/CoreBootManager.cs +++ b/src/Umbraco.Core/CoreBootManager.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.ObjectResolution; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Mappers; @@ -282,6 +283,9 @@ namespace Umbraco.Core UrlSegmentProviderResolver.Current = new UrlSegmentProviderResolver( typeof (DefaultUrlSegmentProvider)); + + PublishedContentModelFactoryResolver.Current = new PublishedContentModelFactoryResolver( + new PublishedContentModelFactoryImpl()); } } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentModel.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentModel.cs new file mode 100644 index 0000000000..27c57ef3e9 --- /dev/null +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentModel.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Umbraco.Core.Models.PublishedContent +{ + /// + /// Represents a strongly-typed published content. + /// + /// Every strongly-typed published content class should inherit from PublishedContentModel + /// (or inherit from a class that inherits from... etc.) so they are picked by the factory. + public abstract class PublishedContentModel : PublishedContentExtended + { + /// + /// Initializes a new instance of the class with + /// an original instance. + /// + /// The original content. + protected PublishedContentModel(IPublishedContent content) + : base(content) + { } + } +} diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentModelAttribute.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentModelAttribute.cs new file mode 100644 index 0000000000..8eaebf6dd1 --- /dev/null +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentModelAttribute.cs @@ -0,0 +1,29 @@ +using System; + +namespace Umbraco.Core.Models.PublishedContent +{ + /// + /// Indicates that the class is a published content model for a specified content type. + /// + /// By default, the name of the class is assumed to be the content type alias. The + /// PublishedContentModelAttribute can be used to indicate a different alias. + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + public sealed class PublishedContentModelAttribute : Attribute + { + /// + /// Initializes a new instance of the class with a content type alias. + /// + /// The content type alias. + public PublishedContentModelAttribute(string contentTypeAlias) + { + if (string.IsNullOrWhiteSpace(contentTypeAlias)) + throw new ArgumentException("Argument cannot be null nor empty.", "contentTypeAlias"); + ContentTypeAlias = contentTypeAlias; + } + + /// + /// Gets or sets the content type alias. + /// + public string ContentTypeAlias { get; private set; } + } +} diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentModelFactoryImpl.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentModelFactoryImpl.cs new file mode 100644 index 0000000000..ecff5ef0ca --- /dev/null +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentModelFactoryImpl.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace Umbraco.Core.Models.PublishedContent +{ + /// + /// Implements a strongly typed content model factory + /// + internal class PublishedContentModelFactoryImpl : IPublishedContentModelFactory + { + //private readonly Dictionary _constructors + // = new Dictionary(); + + private readonly Dictionary> _constructors + = new Dictionary>(); + + public PublishedContentModelFactoryImpl() + { + var types = PluginManager.Current.ResolveTypes(); + var ctorArgTypes = new[] { typeof(IPublishedContent) }; + + 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)); + var attribute = type.GetCustomAttribute(false); + var typeName = attribute == null ? type.Name : attribute.ContentTypeAlias; + typeName = typeName.ToLowerInvariant(); + + if (_constructors.ContainsKey(typeName)) + throw new InvalidOperationException(string.Format("More that one type want to be a model for content type {0}.", typeName)); + + // should work everywhere, potentially slow? + //_constructors[typeName] = constructor; + + // note: would it be even faster with a dynamic method? + // here http://stackoverflow.com/questions/16363838/how-do-you-call-a-constructor-via-an-expression-tree-on-an-existing-object + // but MediumTrust issue? + + // fixme - must make sure that works in medium trust + // read http://boxbinary.com/2011/10/how-to-run-a-unit-test-in-medium-trust-with-nunitpart-three-umbraco-framework-testing/ + var exprArg = Expression.Parameter(typeof(IPublishedContent), "content"); + var exprNew = Expression.New(constructor, exprArg); + var expr = Expression.Lambda>(exprNew, exprArg); + var func = expr.Compile(); + _constructors[typeName] = func; + } + } + + public IPublishedContent CreateModel(IPublishedContent content) + { + // be case-insensitive + var contentTypeAlias = content.DocumentTypeAlias.ToLowerInvariant(); + + //ConstructorInfo constructor; + //return _constructors.TryGetValue(contentTypeAlias, out constructor) + // ? (IPublishedContent) constructor.Invoke(new object[] { content }) + // : content; + + Func constructor; + return _constructors.TryGetValue(contentTypeAlias, out constructor) + ? constructor(content) + : content; + } + } +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index a270070d9e..7846030fa3 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -184,7 +184,9 @@ + + @@ -216,6 +218,7 @@ + diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs index 4477410864..bc7a1b4ab8 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs @@ -1,6 +1,5 @@ using System.Linq; using System.Collections.ObjectModel; -using Lucene.Net.Documents; using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Models; @@ -36,7 +35,7 @@ namespace Umbraco.Tests.PublishedContent PropertyValueConvertersResolver.Current = new PropertyValueConvertersResolver(); PublishedContentModelFactoryResolver.Current = - new PublishedContentModelFactoryResolver(); + new PublishedContentModelFactoryResolver(new PublishedContentModelFactoryImpl()); Resolution.Freeze(); var caches = CreatePublishedContent(); @@ -122,6 +121,44 @@ namespace Umbraco.Tests.PublishedContent Assert.IsTrue(content.IsLast()); } + [Test] + public void OfType1() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot() + .OfType() + .Distinct() + .ToArray(); + Assert.AreEqual(2, content.Count()); + Assert.IsInstanceOf(content.First()); + var set = content.ToContentSet(); + Assert.IsInstanceOf(set.First()); + Assert.AreSame(set, set.First().ContentSet); + Assert.IsInstanceOf(set.First().Next()); + } + + [Test] + public void OfType2() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot() + .OfType() + .Distinct() + .ToArray(); + Assert.AreEqual(1, content.Count()); + Assert.IsInstanceOf(content.First()); + var set = content.ToContentSet(); + Assert.IsInstanceOf(set.First()); + } + + [Test] + public void OfType() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot() + .OfType() + .First(x => x.Prop1 == 1234); + Assert.AreEqual("Content 2", content.Name); + Assert.AreEqual(1234, content.Prop1); + } + [Test] public void Position() { @@ -138,6 +175,28 @@ namespace Umbraco.Tests.PublishedContent Assert.IsTrue(content.First().Next().Next().IsLast()); } + [Test] + public void Issue() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot() + .Distinct() + .OfType(); + + var where = content.Where(x => x.Prop1 == 1234); + var first = where.First(); + Assert.AreEqual(1234, first.Prop1); + + var content2 = UmbracoContext.Current.ContentCache.GetAtRoot() + .OfType() + .First(x => x.Prop1 == 1234); + Assert.AreEqual(1234, content2.Prop1); + + var content3 = UmbracoContext.Current.ContentCache.GetAtRoot() + .OfType() + .First(); + Assert.AreEqual(1234, content3.Prop1); + } + static SolidPublishedCaches CreatePublishedContent() { var caches = new SolidPublishedCaches(); diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTestElements.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTestElements.cs index a2cb4c5dac..0bd31838d2 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTestElements.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTestElements.cs @@ -227,6 +227,36 @@ namespace Umbraco.Tests.PublishedContent public object XPathValue { get; set; } } + [PublishedContentModel("ContentType2")] + public class ContentType2 : PublishedContentModel + { + #region Plumbing + + public ContentType2(IPublishedContent content) + : base(content) + { } + + #endregion + + // fast, if you know that the appropriate IPropertyEditorValueConverter is wired + public int Prop1 { get { return (int)this["prop1"]; } } + + // almost as fast, not sure I like it as much, though + //public int Prop1 { get { return this.GetPropertyValue("prop1"); } } + } + + [PublishedContentModel("ContentType2Sub")] + public class ContentType2Sub : ContentType2 + { + #region Plumbing + + public ContentType2Sub(IPublishedContent content) + : base(content) + { } + + #endregion + } + class PublishedContentStrong1 : PublishedContentExtended { public PublishedContentStrong1(IPublishedContent content) diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs index 06ac9dc824..e199e4df8b 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs @@ -23,6 +23,8 @@ namespace Umbraco.Tests.PublishedContent get { return DatabaseBehavior.NoDatabasePerFixture; } } + private PluginManager _pluginManager; + public override void Initialize() { // required so we can access property.Value @@ -30,6 +32,16 @@ namespace Umbraco.Tests.PublishedContent base.Initialize(); + // this is so the model factory looks into the test assembly + _pluginManager = PluginManager.Current; + PluginManager.Current = new PluginManager(false) + { + AssembliesToScan = _pluginManager.AssembliesToScan + .Union(new[] { typeof(PublishedContentTests).Assembly }) + }; + + ApplicationContext.Current = new ApplicationContext(false) { IsReady = true }; + // need to specify a custom callback for unit tests // AutoPublishedContentTypes generates properties automatically // when they are requested, but we must declare those that we @@ -48,6 +60,20 @@ namespace Umbraco.Tests.PublishedContent PublishedContentType.GetPublishedContentTypeCallback = (alias) => type; } + public override void TearDown() + { + PluginManager.Current = _pluginManager; + ApplicationContext.Current.DisposeIfDisposable(); + ApplicationContext.Current = null; + } + + protected override void FreezeResolution() + { + PublishedContentModelFactoryResolver.Current = new PublishedContentModelFactoryResolver( + new PublishedContentModelFactoryImpl()); + base.FreezeResolution(); + } + protected override string GetXmlContent(int templateId) { return @" @@ -186,6 +212,47 @@ namespace Umbraco.Tests.PublishedContent } } + [PublishedContentModel("Home")] + public class Home : PublishedContentModel + { + public Home(IPublishedContent content) + : base(content) + {} + } + + [Test] + public void Is_Last_From_Where_Filter2() + { + var doc = GetNode(1173); + + var items = doc.Children + .Select(PublishedContentModelFactory.CreateModel) // linq, returns IEnumerable + + // only way around this is to make sure every IEnumerable extension + // explicitely returns a PublishedContentSet, not an IEnumerable + + .OfType() // ours, return IEnumerable (actually a PublishedContentSet) + .Where(x => x.IsVisible()) // so, here it's linq again :-( + .ToContentSet() // so, we need that one for the test to pass + .ToArray(); + + Assert.AreEqual(1, items.Count()); + + foreach (var d in items) + { + switch (d.Id) + { + case 1174: + Assert.IsTrue(d.IsFirst()); + Assert.IsTrue(d.IsLast()); + break; + default: + Assert.Fail("Invalid id."); + break; + } + } + } + [Test] public void Is_Last_From_Take() {