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()
{