diff --git a/src/Umbraco.Core/EnumerableExtensions.cs b/src/Umbraco.Core/EnumerableExtensions.cs index 156831670a..f5679e4f7d 100644 --- a/src/Umbraco.Core/EnumerableExtensions.cs +++ b/src/Umbraco.Core/EnumerableExtensions.cs @@ -347,5 +347,12 @@ namespace Umbraco.Core { return items ?? Enumerable.Empty(); } + + // the .OfType() filter is nice when there's only one type + // this is to support filtering with multiple types + public static IEnumerable OfTypes(this IEnumerable contents, params Type[] types) + { + return contents.Where(x => types.Contains(x.GetType())); + } } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtended.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtended.cs index d5b684d0bf..f024e084f1 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtended.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtended.cs @@ -19,61 +19,38 @@ namespace Umbraco.Core.Models.PublishedContent #region Extend + private static IPublishedContent Unwrap(IPublishedContent content) + { + if (content == null) return null; + + while (true) + { + var extended = content as PublishedContentExtended; + if (extended != null) + { + if (((IPublishedContentExtended)extended).HasAddedProperties) return extended; + content = extended.Unwrap(); + continue; + } + var wrapped = content as PublishedContentWrapped; + if (wrapped != null) + { + content = wrapped.Unwrap(); + continue; + } + return content; + } + } + internal static IPublishedContentExtended Extend(IPublishedContent content) { // first unwrap content down to the lowest possible level, ie either the deepest inner // IPublishedContent or the first extended that has added properties. this is to avoid // nesting extended objects as much as possible, so we try to re-extend that lowest - // object. + // object. Then extend. But do NOT create a model, else we would not be able to add + // properties. BEWARE means that whatever calls Extend MUST create the model. - var wrapped = content as PublishedContentExtended; - while (wrapped != null && ((IPublishedContentExtended)wrapped).HasAddedProperties == false) - wrapped = (content = wrapped.Unwrap()) as PublishedContentExtended; - - // if the factory returns something else than content it means it has created - // a model, and then that model has to inherit from PublishedContentExtended, - // => implements the internal IPublishedContentExtended. - - // here we assume that either the factory just created a model that implements - // IPublishedContentExtended and therefore does not need to be extended again, - // because it can carry the extra property - or that it did *not* create a - // model and therefore returned the original content unchanged. - - var model = content.CreateModel(); - IPublishedContent extended; - if (model == content) // == means the factory did not create a model - { - // so we have to extend - extended = new PublishedContentExtended(content); - } - else - { - // else we can use what the factory returned - extended = model; - } - - // so extended should always implement IPublishedContentExtended, however if - // by mistake the factory returned a different object that does not implement - // IPublishedContentExtended (which would be an error), throw. - // - // see also PublishedContentExtensionsForModels.CreateModel - - // NOTE - // could we lift that constraint and accept that models just be IPublishedContent? - // would then mean that we cannot assume a model is IPublishedContentExtended, so - // either it is, or we need to wrap it. so instead of having - // (Model:IPublishedContentExtended (IPublishedContent)) - // we'd have - // (PublishedContentExtended (Model (IPublishedContent))) - // and it is that bad? any other consequences? - // - // would also allow the factory to cache the model (though that should really - // be done by the content cache, not by the factory). - - var extended2 = extended as IPublishedContentExtended; - if (extended2 == null) - throw new Exception("Extended does not implement IPublishedContentExtended."); - return extended2; + return new PublishedContentExtended(Unwrap(content)); } #endregion diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtensionsForModels.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtensionsForModels.cs index f0b8ad92d6..08d04da1eb 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtensionsForModels.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtensionsForModels.cs @@ -26,18 +26,6 @@ namespace Umbraco.Core.Models.PublishedContent var model = PublishedContentModelFactoryResolver.Current.Factory.CreateModel(content); if (model == null) throw new Exception("IPublishedContentFactory returned null."); - if (ReferenceEquals(model, content)) - return content; - - // at the moment, other parts of our code assume that all models will - // somehow implement IPublishedContentExtended and not just be IPublishedContent, - // so we'd better check this here to fail as soon as we can. - // - // see also PublishedContentExtended.Extend - - var extended = model as IPublishedContentExtended; - if (extended == null) - throw new Exception("IPublishedContentFactory created an object that does not implement IPublishedContentModelExtended."); return model; } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentModel.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentModel.cs index fa5587813e..f1d2a5f208 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentModel.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentModel.cs @@ -1,16 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace Umbraco.Core.Models.PublishedContent +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 + public abstract class PublishedContentModel : PublishedContentWrapped { /// /// Initializes a new instance of the class with diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs index 7dd88d974f..2fb88e1002 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs @@ -36,6 +36,7 @@ namespace Umbraco.Core.Models.PublishedContent PropertyEditorAlias = propertyType.PropertyEditorAlias; } + /* /// /// Initializes a new instance of the class with an existing /// and a new property type alias. @@ -49,7 +50,7 @@ namespace Umbraco.Core.Models.PublishedContent internal PublishedPropertyType(string propertyTypeAlias, PublishedPropertyType propertyType) : this(propertyTypeAlias, propertyType.DataTypeId, propertyType.PropertyEditorAlias) { } - + */ /// /// Initializes a new instance of the class with a property type alias and a property editor alias. @@ -68,6 +69,7 @@ namespace Umbraco.Core.Models.PublishedContent : this(propertyTypeAlias, 0, propertyEditorAlias) { } + /* /// /// Initializes a new instance of the class with a property type alias and a datatype definition. /// @@ -79,6 +81,7 @@ namespace Umbraco.Core.Models.PublishedContent internal PublishedPropertyType(string propertyTypeAlias, IDataTypeDefinition dataTypeDefinition) : this(propertyTypeAlias, dataTypeDefinition.Id, dataTypeDefinition.PropertyEditorAlias) { } + */ /// /// Initializes a new instance of the class with a property type alias, diff --git a/src/Umbraco.Core/PropertyEditors/IPropertyValueConverter.cs b/src/Umbraco.Core/PropertyEditors/IPropertyValueConverter.cs index 8a2949d806..cd7658667d 100644 --- a/src/Umbraco.Core/PropertyEditors/IPropertyValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/IPropertyValueConverter.cs @@ -23,55 +23,60 @@ namespace Umbraco.Core.PropertyEditors Type GetPropertyValueType(PublishedPropertyType propertyType); /// - /// Gets the property cache level of a specified value. + /// Gets the property cache level. /// /// The property type. - /// The property cache level of the specified value. + /// The property cache level. PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType); /// - /// Converts a property Data value to a Source value. + /// Converts a property source value to an intermediate value. /// /// The property type. - /// The data value. + /// The source value. /// A value indicating whether conversion should take place in preview mode. /// The result of the conversion. /// - /// The converter should know how to convert a null raw value, meaning that no - /// value has been assigned to the property. The source value can be null. - /// With the XML cache, raw values come from the XML cache and therefore are strings. - /// With objects caches, raw values would come from the database and therefore be either - /// ints, DateTimes, or strings. + /// The converter should know how to convert a null source value, meaning that no + /// value has been assigned to the property. The intermediate value can be null. + /// With the XML cache, source values come from the XML cache and therefore are strings. + /// With objects caches, source values would come from the database and therefore be either + /// ints, DateTimes, decimals, or strings. /// The converter should be prepared to handle both situations. - /// When raw values are strings, the converter must handle empty strings, whitespace + /// When source values are strings, the converter must handle empty strings, whitespace /// strings, and xml-whitespace strings appropriately, ie it should know whether to preserve /// whitespaces. /// object ConvertSourceToInter(PublishedPropertyType propertyType, object source, bool preview); /// - /// Converts a property Source value to an Object value. + /// Converts a property intermediate value to an Object value. /// /// The property type. - /// fixme - /// The source value. - /// A value indicating whether conversion should take place in preview mode. - /// The result of the conversion. - /// The converter should know how to convert a null source value, or any source value - /// indicating that no value has been assigned to the property. It is up to the converter to determine - /// what to return in that case: either null, or the default value... - object ConvertInterToObject(PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview); - - /// - /// Converts a property Source value to an XPath value. - /// - /// The property type. - /// fixme - /// The source value. + /// The reference cache level. + /// The intermediate value. /// A value indicating whether conversion should take place in preview mode. /// The result of the conversion. /// - /// The converter should know how to convert a null source value, or any source value + /// The converter should know how to convert a null intermediate value, or any intermediate value + /// indicating that no value has been assigned to the property. It is up to the converter to determine + /// what to return in that case: either null, or the default value... + /// The is passed to the converter so that it can be, in turn, + /// passed to eg a PublishedFragment constructor. It is used by the fragment and the properties to manage + /// the cache levels of property values. It is not meant to be used by the converter. + /// + object ConvertInterToObject(PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview); + + /// + /// Converts a property intermediate value to an XPath value. + /// + /// The property type. + /// The reference cache level. + /// The intermediate value. + /// A value indicating whether conversion should take place in preview mode. + /// The result of the conversion. + /// + /// The converter should know how to convert a null intermediate value, or any intermediate value /// indicating that no value has been assigned to the property. It is up to the converter to determine /// what to return in that case: either null, or the default value... /// If successful, the result should be either null, a string, or an XPathNavigator @@ -79,6 +84,9 @@ namespace Umbraco.Core.PropertyEditors /// up to the converter. /// The converter may want to return an XML fragment that represent a part of the content tree, /// but should pay attention not to create infinite loops that would kill XPath and XSLT. + /// The is passed to the converter so that it can be, in turn, + /// passed to eg a PublishedFragment constructor. It is used by the fragment and the properties to manage + /// the cache levels of property values. It is not meant to be used by the converter. /// object ConvertInterToXPath(PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview); } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentDataTableTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentDataTableTests.cs index f7becfdc0e..019f6448dd 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentDataTableTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentDataTableTests.cs @@ -23,14 +23,14 @@ namespace Umbraco.Tests.PublishedContent base.Initialize(); // need to specify a different callback for testing - PublishedContentExtensions.GetPropertyAliasesAndNames = s => + PublishedContentExtensions.GetPropertyAliasesAndNames = (services, alias) => { var userFields = new Dictionary() { {"property1", "Property 1"}, {"property2", "Property 2"} }; - if (s == "Child") + if (alias == "Child") { userFields.Add("property4", "Property 4"); } @@ -73,7 +73,7 @@ namespace Umbraco.Tests.PublishedContent public void To_DataTable() { var doc = GetContent(true, 1); - var dt = doc.ChildrenAsTable(); + var dt = doc.ChildrenAsTable(ApplicationContext.Services); Assert.AreEqual(11, dt.Columns.Count); Assert.AreEqual(3, dt.Rows.Count); @@ -95,7 +95,7 @@ namespace Umbraco.Tests.PublishedContent //change a doc type alias ((TestPublishedContent) doc.Children.ElementAt(0)).DocumentTypeAlias = "DontMatch"; - var dt = doc.ChildrenAsTable("Child"); + var dt = doc.ChildrenAsTable(ApplicationContext.Services, "Child"); Assert.AreEqual(11, dt.Columns.Count); Assert.AreEqual(2, dt.Rows.Count); @@ -111,7 +111,7 @@ namespace Umbraco.Tests.PublishedContent public void To_DataTable_No_Rows() { var doc = GetContent(false, 1); - var dt = doc.ChildrenAsTable(); + var dt = doc.ChildrenAsTable(ApplicationContext.Services); //will return an empty data table Assert.AreEqual(0, dt.Columns.Count); Assert.AreEqual(0, dt.Rows.Count); diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTestElements.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTestElements.cs index b483e515bc..c6f4e2bcc8 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTestElements.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTestElements.cs @@ -27,11 +27,6 @@ namespace Umbraco.Tests.PublishedContent throw new NotImplementedException(); } - public IPublishedProperty CreateFragmentProperty(PublishedPropertyType propertyType, Guid itemKey, bool previewing, PropertyCacheLevel referenceCacheLevel, object sourceValue = null) - { - throw new NotImplementedException(); - } - public void Resync() { } } @@ -279,7 +274,7 @@ namespace Umbraco.Tests.PublishedContent #endregion } - class PublishedContentStrong1 : PublishedContentExtended + class PublishedContentStrong1 : PublishedContentModel { public PublishedContentStrong1(IPublishedContent content) : base(content) @@ -297,7 +292,7 @@ namespace Umbraco.Tests.PublishedContent public int AnotherValue => this.GetPropertyValue("anotherValue"); } - class PublishedContentStrong2 : PublishedContentExtended + class PublishedContentStrong2 : PublishedContentModel { public PublishedContentStrong2(IPublishedContent content) : base(content) diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs index 67727a3157..960963b265 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Web; @@ -5,8 +6,9 @@ using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Plugins; +using Umbraco.Core.PropertyEditors; using Umbraco.Web; -using Umbraco.Web.Models; +using Umbraco.Web.PublishedCache; namespace Umbraco.Tests.PublishedContent { @@ -293,7 +295,7 @@ namespace Umbraco.Tests.PublishedContent var rVal = doc.GetPropertyValue("testRecursive", true); var nullVal = doc.GetPropertyValue("DoNotFindThis", true); Assert.AreEqual("This is the recursive val", rVal); - Assert.AreEqual("", nullVal); + Assert.AreEqual(null, nullVal); } [Test] @@ -530,66 +532,60 @@ namespace Umbraco.Tests.PublishedContent Assert.AreEqual(1174, result.Id); } - [Test] - public void FragmentTest() - { - - } - [Test] - public void DetachedProperty1() + public void FragmentProperty() { - var type = new PublishedPropertyType("detached", Constants.PropertyEditors.IntegerAlias); - var prop = PublishedProperty.GetDetached(type.Detached(), "5548"); + var pt = new PublishedPropertyType("detached", Constants.PropertyEditors.IntegerAlias); + var ct = new PublishedContentType(0, "alias", new[] { pt }); + var prop = new PublishedFragmentProperty(pt, Guid.NewGuid(), false, PropertyCacheLevel.None, 5548); Assert.IsInstanceOf(prop.Value); Assert.AreEqual(5548, prop.Value); } - public void CreateDetachedContentSample() + public void Fragment1() { - bool previewing = false; - var t = ContentTypesCache.Get(PublishedItemType.Content, "detachedSomething"); + var type = ContentTypesCache.Get(PublishedItemType.Content, "detachedSomething"); var values = new Dictionary(); - var properties = t.PropertyTypes.Select(x => - { - object value; - if (values.TryGetValue(x.PropertyTypeAlias, out value) == false) value = null; - return PublishedProperty.GetDetached(x.Detached(), value, previewing); - }); - // and if you want some sort of "model" it's up to you really... - var c = new DetachedContent(properties); + var f = new PublishedFragment(type, Guid.NewGuid(), values, false); } - public void CreatedDetachedContentInConverterSample() + [Test] + public void Fragment2() { - // the converter args - PublishedPropertyType argPropertyType = null; - object argSource = null; - bool argPreview = false; - var pt1 = new PublishedPropertyType("legend", 0, Constants.PropertyEditors.TextboxAlias); var pt2 = new PublishedPropertyType("image", 0, Constants.PropertyEditors.MediaPickerAlias); - string val1 = ""; - int val2 = 0; + var pt3 = new PublishedPropertyType("size", 0, Constants.PropertyEditors.IntegerAlias); + const string val1 = "boom bam"; + const int val2 = 0; + const int val3 = 666; - var c = new ImageWithLegendModel( - PublishedProperty.GetDetached(pt1.Nested(argPropertyType), val1, argPreview), - PublishedProperty.GetDetached(pt2.Nested(argPropertyType), val2, argPreview)); + var guid = Guid.NewGuid(); + + var ct = new PublishedContentType(0, "alias", new[] { pt1, pt2, pt3 }); + + var c = new ImageWithLegendModel(ct, guid, new Dictionary + { + { "legend", val1 }, + { "image", val2 }, + { "size", val3 }, + }, false); + + Assert.AreEqual(val1, c.Legend); + Assert.AreEqual(val3, c.Size); } - class ImageWithLegendModel + class ImageWithLegendModel : PublishedFragment { - private readonly IPublishedProperty _legendProperty; - private readonly IPublishedProperty _imageProperty; + public ImageWithLegendModel(PublishedContentType contentType, Guid fragmentKey, Dictionary values, bool previewing) + : base(contentType, fragmentKey, values, previewing) + { } - public ImageWithLegendModel(IPublishedProperty legendProperty, IPublishedProperty imageProperty) - { - _legendProperty = legendProperty; - _imageProperty = imageProperty; - } - public string Legend => _legendProperty.GetValue(); - public IPublishedContent Image => _imageProperty.GetValue(); + public string Legend => this.GetPropertyValue("legend"); + + public IPublishedContent Image => this.GetPropertyValue("image"); + + public int Size => this.GetPropertyValue("size"); } } } \ No newline at end of file diff --git a/src/Umbraco.Web/ExamineExtensions.cs b/src/Umbraco.Web/ExamineExtensions.cs index f52b185d8b..2d16c9b345 100644 --- a/src/Umbraco.Web/ExamineExtensions.cs +++ b/src/Umbraco.Web/ExamineExtensions.cs @@ -16,9 +16,8 @@ namespace Umbraco.Web { internal static IEnumerable ConvertSearchResultToPublishedContent(this IEnumerable results, IPublishedCache cache) { - //TODO: The search result has already returned a result which SHOULD include all of the data to create an IPublishedContent, - // however this is currently not the case: - // http://examine.codeplex.com/workitem/10350 + // no need to think about creating the IPublishedContent from the Examine result + // content cache is fast and optimized - use it! var list = new List(); @@ -35,13 +34,13 @@ namespace Umbraco.Web // returned by the cache, in case the cache can create real types. // so we have to ask it to please extend itself. - //var extend = set.MapContent(content); var extend = PublishedContentExtended.Extend(content); - list.Add(extend); + list.Add(extend.CreateModel()); // take care, must create the model! var property = new PropertyResult("examineScore", result.Score, PropertyResultType.CustomProperty); + extend.AddProperty(property); } diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/TempValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/TempValueConverter.cs deleted file mode 100644 index 42a7955303..0000000000 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/TempValueConverter.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json; -using Umbraco.Core; -using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.PropertyEditors; -using Umbraco.Web.PublishedCache; -using Umbraco.Web.PublishedCache.NuCache; - -namespace Umbraco.Web.PropertyEditors.ValueConverters -{ - // fixme - // this is an experimental converter to work with nested etc. - internal class TempValueConverter : PropertyValueConverterBase - { - private readonly IFacadeAccessor _facadeAccessor; - - public TempValueConverter(IFacadeAccessor facadeAccessor) - { - _facadeAccessor = facadeAccessor; - } - - public override bool IsConverter(PublishedPropertyType propertyType) - { - return propertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.ContentPickerAlias); - } - - public override Type GetPropertyValueType(PublishedPropertyType propertyType) - { - return typeof (IPublishedFragment); - } - - public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) - { - return PropertyCacheLevel.Snapshot; - } - - public override object ConvertSourceToInter(PublishedPropertyType propertyType, object source, bool preview) - { - var json = source as string; - return json == null ? null : JsonConvert.DeserializeObject(json); - } - - public override object ConvertInterToObject(PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) - { - var data = (TempData) inter; - if (data == null) return null; - - var contentType = _facadeAccessor.Facade.ContentCache.GetContentType(data.ContentTypeAlias); - if (contentType == null) return null; - - // fixme - // note: if we wanted to returned a strongly-typed model here, we'd have to be explicit about it - // so that we can tell GetPropertyValueType what we return - just relying on a factory is not - // going to make it in any helpful way? - - return new PublishedFragment(contentType, _facadeAccessor, referenceCacheLevel, data.Key, data.Values, preview); - } - - public override object ConvertInterToXPath(PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) - { - return inter == null ? null : JsonConvert.SerializeObject((TempData) inter); - } - - public class TempData - { - public string ContentTypeAlias { get; set; } - public Guid Key { get; set; } - public Dictionary Values { get; set; } - } - } -} diff --git a/src/Umbraco.Web/PublishedCache/FacadeServiceBase.cs b/src/Umbraco.Web/PublishedCache/FacadeServiceBase.cs index b61eac3889..9384ad2e42 100644 --- a/src/Umbraco.Web/PublishedCache/FacadeServiceBase.cs +++ b/src/Umbraco.Web/PublishedCache/FacadeServiceBase.cs @@ -1,23 +1,27 @@ using System; using Umbraco.Core.Models.Membership; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors; using Umbraco.Web.Cache; namespace Umbraco.Web.PublishedCache { abstract class FacadeServiceBase : IFacadeService { - private IFacadeAccessor _facadeAccessor; - protected FacadeServiceBase(IFacadeAccessor facadeAccessor) { - _facadeAccessor = facadeAccessor; + FacadeAccessor = facadeAccessor; } + public IFacadeAccessor FacadeAccessor { get; } + // note: NOT setting _facadeAccessor.Facade here because it is the // responsibility of the caller to manage what the 'current' facade is public abstract IFacade CreateFacade(string previewToken); - protected IFacade CurrentFacade => _facadeAccessor.Facade; + protected IFacade CurrentFacade => FacadeAccessor.Facade; + + public abstract IPublishedProperty CreateFragmentProperty(PublishedPropertyType propertyType, Guid itemKey, bool previewing, PropertyCacheLevel referenceCacheLevel, object sourceValue = null); public abstract string EnterPreview(IUser user, int contentId); public abstract void RefreshPreview(string previewToken, int contentId); diff --git a/src/Umbraco.Web/PublishedCache/IFacade.cs b/src/Umbraco.Web/PublishedCache/IFacade.cs index 1837d4df67..9af439b58e 100644 --- a/src/Umbraco.Web/PublishedCache/IFacade.cs +++ b/src/Umbraco.Web/PublishedCache/IFacade.cs @@ -39,17 +39,5 @@ namespace Umbraco.Web.PublishedCache /// otherwise the facade keeps previewing according to whatever settings it is using already. /// Stops forcing preview when disposed. IDisposable ForcedPreview(bool preview, Action callback = null); - - // fixme - document - /// - /// Creates a fragment property. - /// - /// - /// - /// - /// - /// - /// - IPublishedProperty CreateFragmentProperty(PublishedPropertyType propertyType, Guid itemKey, bool previewing, PropertyCacheLevel referenceCacheLevel, object sourceValue = null); } } diff --git a/src/Umbraco.Web/PublishedCache/IFacadeAccessor.cs b/src/Umbraco.Web/PublishedCache/IFacadeAccessor.cs index 208e476edf..024d73a2d5 100644 --- a/src/Umbraco.Web/PublishedCache/IFacadeAccessor.cs +++ b/src/Umbraco.Web/PublishedCache/IFacadeAccessor.cs @@ -3,7 +3,7 @@ /// /// Provides access to IFacade. /// - interface IFacadeAccessor + public interface IFacadeAccessor { IFacade Facade { get; set; } } diff --git a/src/Umbraco.Web/PublishedCache/IFacadeService.cs b/src/Umbraco.Web/PublishedCache/IFacadeService.cs index 22d95a06a2..7f1b92e102 100644 --- a/src/Umbraco.Web/PublishedCache/IFacadeService.cs +++ b/src/Umbraco.Web/PublishedCache/IFacadeService.cs @@ -1,5 +1,7 @@ using System; using Umbraco.Core.Models.Membership; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors; using Umbraco.Web.Cache; namespace Umbraco.Web.PublishedCache @@ -9,7 +11,7 @@ namespace Umbraco.Web.PublishedCache /// public interface IFacadeService : IDisposable { - #region PublishedCaches + #region Facade /* Various places (such as Node) want to access the XML content, today as an XmlDocument * but to migrate to a new cache, they're migrating to an XPathNavigator. Still, they need @@ -37,6 +39,11 @@ namespace Umbraco.Web.PublishedCache /// A facade. IFacade CreateFacade(string previewToken); + /// + /// Gets the facade accessor. + /// + IFacadeAccessor FacadeAccessor { get; } + #endregion #region Preview @@ -136,5 +143,20 @@ namespace Umbraco.Web.PublishedCache void Notify(DomainCacheRefresher.JsonPayload[] payloads); #endregion + + #region Fragment + + /// + /// Creates a fragment property. + /// + /// The property type. + /// The fragment key. + /// A value indicating whether previewing. + /// The reference cache level. + /// The source value. + /// A fragment property. + IPublishedProperty CreateFragmentProperty(PublishedPropertyType propertyType, Guid itemKey, bool previewing, PropertyCacheLevel referenceCacheLevel, object sourceValue = null); + + #endregion } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/Facade.cs b/src/Umbraco.Web/PublishedCache/NuCache/Facade.cs index 4500a56ffe..8619983c04 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/Facade.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/Facade.cs @@ -96,11 +96,6 @@ namespace Umbraco.Web.PublishedCache.NuCache } } - public IPublishedProperty CreateFragmentProperty(PublishedPropertyType propertyType, Guid itemKey, bool previewing, PropertyCacheLevel referenceCacheLevel, object sourceValue = null) - { - return _service.CreateFragmentProperty(propertyType, itemKey, previewing, referenceCacheLevel, sourceValue); - } - #endregion #region IDisposable diff --git a/src/Umbraco.Web/PublishedCache/NuCache/FacadeService.cs b/src/Umbraco.Web/PublishedCache/NuCache/FacadeService.cs index 152f61311d..ff5157c5dc 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/FacadeService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/FacadeService.cs @@ -1482,7 +1482,7 @@ AND cmsContentNu.nodeId IS NULL #region Fragments - public IPublishedProperty CreateFragmentProperty(PublishedPropertyType propertyType, Guid itemKey, bool previewing, PropertyCacheLevel referenceCacheLevel, object sourceValue = null) + public override IPublishedProperty CreateFragmentProperty(PublishedPropertyType propertyType, Guid itemKey, bool previewing, PropertyCacheLevel referenceCacheLevel, object sourceValue = null) { return new PublishedFragmentProperty(_facadeAccessor, propertyType, itemKey, previewing, referenceCacheLevel, sourceValue); } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedFragmentProperty.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedFragmentProperty.cs index db4d6bb341..4e235c31a2 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedFragmentProperty.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedFragmentProperty.cs @@ -5,21 +5,21 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PublishedCache.NuCache { - internal class PublishedFragmentProperty : PublishedCache.PublishedFragmentProperty + internal class PublishedFragmentProperty : PublishedFragmentPropertyBase { private readonly IFacadeAccessor _facadeAccessor; private string _valuesCacheKey; // initializes a published item property - public PublishedFragmentProperty(IFacadeAccessor facadeAccessor, PublishedPropertyType propertyType, Guid itemKey, bool previewing, PropertyCacheLevel referenceCacheLevel, object sourceValue = null) - : base(propertyType, itemKey, previewing, referenceCacheLevel, sourceValue) + public PublishedFragmentProperty(IFacadeAccessor facadeAccessor, PublishedPropertyType propertyType, Guid fragmentKey, bool previewing, PropertyCacheLevel referenceCacheLevel, object sourceValue = null) + : base(propertyType, fragmentKey, previewing, referenceCacheLevel, sourceValue) { _facadeAccessor = facadeAccessor; } // used to cache the CacheValues of this property internal string ValuesCacheKey => _valuesCacheKey - ?? (_valuesCacheKey = CacheKeys.PropertyCacheValues(ItemUid, PropertyTypeAlias, IsPreviewing)); + ?? (_valuesCacheKey = CacheKeys.PropertyCacheValues(FragmentKey, PropertyTypeAlias, IsPreviewing)); protected override CacheValues GetSnapshotCacheValues() { diff --git a/src/Umbraco.Web/PublishedCache/PublishedFragment.cs b/src/Umbraco.Web/PublishedCache/PublishedFragment.cs index 9832f9782e..ebd76f3b20 100644 --- a/src/Umbraco.Web/PublishedCache/PublishedFragment.cs +++ b/src/Umbraco.Web/PublishedCache/PublishedFragment.cs @@ -6,36 +6,27 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PublishedCache { - // fixme document & review + // notes: + // a published fragment does NOT manage any tree-like elements, neither the + // original NestedContent (from Lee) nor the DetachedPublishedContent POC did. // - // these things should NOT manage any tree-like elements (fixme:discuss?) - // Lee's NestedContent package DetachedPublishedContent does NOT - // and yet we need to be able to figure out what happens when nesting things? - // - // how would we create MODELS for published items? should it be automatic - // or explicit when creating the items? probably cannot be automatic because - // then, how would we cast the returned object? - // - // note: could also have a totally detached one in Core? + // at the moment we do NOT support models for fragments - that would require + // an entirely new models factory + not even sure it makes sense at all since + // fragments are created manually // internal class PublishedFragment : IPublishedFragment { - private readonly IFacadeAccessor _facadeAccessor; - + // initializes a new instance of the PublishedFragment class + // within the context of a facade service (eg a published content property value) public PublishedFragment(PublishedContentType contentType, - IFacadeAccessor facadeAccessor, PropertyCacheLevel referenceCacheLevel, + IFacadeService facadeService, PropertyCacheLevel referenceCacheLevel, Guid key, Dictionary values, bool previewing) { ContentType = contentType; - _facadeAccessor = facadeAccessor; Key = key; - // ensure we ignore case for property aliases - var comparer = values.Comparer; - var ignoreCase = comparer == StringComparer.OrdinalIgnoreCase || comparer == StringComparer.InvariantCultureIgnoreCase || comparer == StringComparer.CurrentCultureIgnoreCase; - if (ignoreCase == false) - values = new Dictionary(values, StringComparer.OrdinalIgnoreCase); + values = GetCaseInsensitiveValueDictionary(values); _propertiesArray = contentType .PropertyTypes @@ -43,10 +34,42 @@ namespace Umbraco.Web.PublishedCache { object value; values.TryGetValue(propertyType.PropertyTypeAlias, out value); - return _facadeAccessor.Facade.CreateFragmentProperty(propertyType, Key, previewing, referenceCacheLevel, value); + return facadeService.CreateFragmentProperty(propertyType, Key, previewing, referenceCacheLevel, value); }) .ToArray(); + } + // initializes a new instance of the PublishedFragment class + // without any context, so it's purely 'standalone' and should NOT interfere with the facade service + public PublishedFragment(PublishedContentType contentType, Guid key, Dictionary values, bool previewing) + { + ContentType = contentType; + Key = key; + + values = GetCaseInsensitiveValueDictionary(values); + + // using an initial reference cache level of .None ensures that + // everything will be cached at .Content level + // that reference cache level will propagate to all properties + const PropertyCacheLevel cacheLevel = PropertyCacheLevel.None; + + _propertiesArray = contentType + .PropertyTypes + .Select(propertyType => + { + object value; + values.TryGetValue(propertyType.PropertyTypeAlias, out value); + return (IPublishedProperty) new PublishedFragmentProperty(propertyType, Key, previewing, cacheLevel, value); + }) + .ToArray(); + } + + private static Dictionary GetCaseInsensitiveValueDictionary(Dictionary values) + { + // ensure we ignore case for property aliases + var comparer = values.Comparer; + var ignoreCase = comparer == StringComparer.OrdinalIgnoreCase || comparer == StringComparer.InvariantCultureIgnoreCase || comparer == StringComparer.CurrentCultureIgnoreCase; + return ignoreCase ? values : new Dictionary(values, StringComparer.OrdinalIgnoreCase); } #region ContentType diff --git a/src/Umbraco.Web/PublishedCache/PublishedFragmentProperty.cs b/src/Umbraco.Web/PublishedCache/PublishedFragmentProperty.cs index a47a33ac86..353810caf5 100644 --- a/src/Umbraco.Web/PublishedCache/PublishedFragmentProperty.cs +++ b/src/Umbraco.Web/PublishedCache/PublishedFragmentProperty.cs @@ -4,162 +4,20 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PublishedCache { - internal abstract class PublishedFragmentProperty : PublishedPropertyBase + class PublishedFragmentProperty : PublishedFragmentPropertyBase { - private readonly object _locko = new object(); - private readonly object _sourceValue; + public PublishedFragmentProperty(PublishedPropertyType propertyType, Guid fragmentKey, bool previewing, PropertyCacheLevel cacheLevel, object sourceValue = null) + : base(propertyType, fragmentKey, previewing, cacheLevel, sourceValue) + { } - protected readonly Guid ItemUid; - protected readonly bool IsPreviewing; - protected readonly bool IsMember; - - private bool _interInitialized; - private object _interValue; - private CacheValues _cacheValues; - - // initializes a published item property - protected PublishedFragmentProperty(PublishedPropertyType propertyType, Guid itemKey, bool previewing, PropertyCacheLevel referenceCacheLevel, object sourceValue = null) - : base(propertyType, referenceCacheLevel) + protected override CacheValues GetSnapshotCacheValues() { - _sourceValue = sourceValue; - ItemUid = itemKey; - IsPreviewing = previewing; - IsMember = propertyType.ContentType.ItemType == PublishedItemType.Member; + throw new NotSupportedException(); } - public override bool HasValue => _sourceValue != null - && ((_sourceValue is string) == false || string.IsNullOrWhiteSpace((string)_sourceValue) == false); - - protected class CacheValues + protected override CacheValues GetFacadeCacheValues() { - public bool ObjectInitialized; - public object ObjectValue; - public bool XPathInitialized; - public object XPathValue; - } - - private static void ValidateCacheLevel(PropertyCacheLevel cacheLevel) - { - switch (cacheLevel) - { - case PropertyCacheLevel.Content: - case PropertyCacheLevel.Snapshot: - case PropertyCacheLevel.Facade: - case PropertyCacheLevel.None: - break; - default: - throw new Exception("Invalid cache level."); - } - } - - private void GetCacheLevels(out PropertyCacheLevel cacheLevel, out PropertyCacheLevel referenceCacheLevel) - { - // based upon the current reference cache level (ReferenceCacheLevel) and this property - // cache level (PropertyType.CacheLevel), determines both the actual cache level for the - // property, and the new reference cache level. - - // sanity checks - ValidateCacheLevel(ReferenceCacheLevel); - ValidateCacheLevel(PropertyType.CacheLevel); - - // if the property cache level is 'shorter-termed' that the reference - // then use it and it becomes the new reference, else use Content and - // don't change the reference. - // - // examples: - // currently (reference) caching at facade, property specifies - // snapshot, ok to use content. OTOH, currently caching at snapshot, - // property specifies facade, need to use facade. - // - if (PropertyType.CacheLevel > ReferenceCacheLevel) - { - cacheLevel = PropertyType.CacheLevel; - referenceCacheLevel = cacheLevel; - } - else - { - cacheLevel = PropertyCacheLevel.Content; - referenceCacheLevel = ReferenceCacheLevel; - } - } - - protected abstract CacheValues GetSnapshotCacheValues(); - protected abstract CacheValues GetFacadeCacheValues(); - - private CacheValues GetCacheValues(PropertyCacheLevel cacheLevel) - { - CacheValues cacheValues; - switch (cacheLevel) - { - case PropertyCacheLevel.None: - // never cache anything - cacheValues = new CacheValues(); - break; - case PropertyCacheLevel.Content: - // cache within the property object itself, ie within the content object - cacheValues = _cacheValues ?? (_cacheValues = new CacheValues()); - break; - case PropertyCacheLevel.Snapshot: - // cache within the snapshot cache, depending... - cacheValues = GetSnapshotCacheValues(); - break; - case PropertyCacheLevel.Facade: - // cache within the facade cache - cacheValues = GetFacadeCacheValues(); - break; - default: - throw new InvalidOperationException("Invalid cache level."); - } - return cacheValues; - } - - private object GetInterValue() - { - if (_interInitialized) return _interValue; - - _interValue = PropertyType.ConvertSourceToInter(_sourceValue, IsPreviewing); - _interInitialized = true; - return _interValue; - } - - public override object SourceValue => _sourceValue; - - public override object Value - { - get - { - lock (_locko) - { - PropertyCacheLevel cacheLevel, referenceCacheLevel; - GetCacheLevels(out cacheLevel, out referenceCacheLevel); - - var cacheValues = GetCacheValues(cacheLevel); - if (cacheValues.ObjectInitialized) return cacheValues.ObjectValue; - - cacheValues.ObjectValue = PropertyType.ConvertInterToObject(referenceCacheLevel, GetInterValue(), IsPreviewing); - cacheValues.ObjectInitialized = true; - return cacheValues.ObjectValue; - } - } - } - - public override object XPathValue - { - get - { - lock (_locko) - { - PropertyCacheLevel cacheLevel, referenceCacheLevel; - GetCacheLevels(out cacheLevel, out referenceCacheLevel); - - var cacheValues = GetCacheValues(cacheLevel); - if (cacheValues.XPathInitialized) return cacheValues.XPathValue; - - cacheValues.XPathValue = PropertyType.ConvertInterToXPath(referenceCacheLevel, GetInterValue(), IsPreviewing); - cacheValues.XPathInitialized = true; - return cacheValues.XPathValue; - } - } + throw new NotSupportedException(); } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/PublishedCache/PublishedFragmentPropertyBase.cs b/src/Umbraco.Web/PublishedCache/PublishedFragmentPropertyBase.cs new file mode 100644 index 0000000000..fd0d972c8b --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/PublishedFragmentPropertyBase.cs @@ -0,0 +1,165 @@ +using System; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PublishedCache +{ + internal abstract class PublishedFragmentPropertyBase : PublishedPropertyBase + { + private readonly object _locko = new object(); + private readonly object _sourceValue; + + protected readonly Guid FragmentKey; + protected readonly bool IsPreviewing; + protected readonly bool IsMember; + + private bool _interInitialized; + private object _interValue; + private CacheValues _cacheValues; + + // initializes a published item property + protected PublishedFragmentPropertyBase(PublishedPropertyType propertyType, Guid fragmentKey, bool previewing, PropertyCacheLevel referenceCacheLevel, object sourceValue = null) + : base(propertyType, referenceCacheLevel) + { + _sourceValue = sourceValue; + FragmentKey = fragmentKey; + IsPreviewing = previewing; + IsMember = propertyType.ContentType.ItemType == PublishedItemType.Member; + } + + public override bool HasValue => _sourceValue != null + && ((_sourceValue is string) == false || string.IsNullOrWhiteSpace((string)_sourceValue) == false); + + protected class CacheValues + { + public bool ObjectInitialized; + public object ObjectValue; + public bool XPathInitialized; + public object XPathValue; + } + + private static void ValidateCacheLevel(PropertyCacheLevel cacheLevel) + { + switch (cacheLevel) + { + case PropertyCacheLevel.Content: + case PropertyCacheLevel.Snapshot: + case PropertyCacheLevel.Facade: + case PropertyCacheLevel.None: + break; + default: + throw new Exception("Invalid cache level."); + } + } + + private void GetCacheLevels(out PropertyCacheLevel cacheLevel, out PropertyCacheLevel referenceCacheLevel) + { + // based upon the current reference cache level (ReferenceCacheLevel) and this property + // cache level (PropertyType.CacheLevel), determines both the actual cache level for the + // property, and the new reference cache level. + + // sanity checks + ValidateCacheLevel(ReferenceCacheLevel); + ValidateCacheLevel(PropertyType.CacheLevel); + + // if the property cache level is 'shorter-termed' that the reference + // then use it and it becomes the new reference, else use Content and + // don't change the reference. + // + // examples: + // currently (reference) caching at facade, property specifies + // snapshot, ok to use content. OTOH, currently caching at snapshot, + // property specifies facade, need to use facade. + // + if (PropertyType.CacheLevel > ReferenceCacheLevel) + { + cacheLevel = PropertyType.CacheLevel; + referenceCacheLevel = cacheLevel; + } + else + { + cacheLevel = PropertyCacheLevel.Content; + referenceCacheLevel = ReferenceCacheLevel; + } + } + + protected abstract CacheValues GetSnapshotCacheValues(); + protected abstract CacheValues GetFacadeCacheValues(); + + private CacheValues GetCacheValues(PropertyCacheLevel cacheLevel) + { + CacheValues cacheValues; + switch (cacheLevel) + { + case PropertyCacheLevel.None: + // never cache anything + cacheValues = new CacheValues(); + break; + case PropertyCacheLevel.Content: + // cache within the property object itself, ie within the content object + cacheValues = _cacheValues ?? (_cacheValues = new CacheValues()); + break; + case PropertyCacheLevel.Snapshot: + // cache within the snapshot cache, depending... + cacheValues = GetSnapshotCacheValues(); + break; + case PropertyCacheLevel.Facade: + // cache within the facade cache + cacheValues = GetFacadeCacheValues(); + break; + default: + throw new InvalidOperationException("Invalid cache level."); + } + return cacheValues; + } + + private object GetInterValue() + { + if (_interInitialized) return _interValue; + + _interValue = PropertyType.ConvertSourceToInter(_sourceValue, IsPreviewing); + _interInitialized = true; + return _interValue; + } + + public override object SourceValue => _sourceValue; + + public override object Value + { + get + { + lock (_locko) + { + PropertyCacheLevel cacheLevel, referenceCacheLevel; + GetCacheLevels(out cacheLevel, out referenceCacheLevel); + + var cacheValues = GetCacheValues(cacheLevel); + if (cacheValues.ObjectInitialized) return cacheValues.ObjectValue; + + cacheValues.ObjectValue = PropertyType.ConvertInterToObject(referenceCacheLevel, GetInterValue(), IsPreviewing); + cacheValues.ObjectInitialized = true; + return cacheValues.ObjectValue; + } + } + } + + public override object XPathValue + { + get + { + lock (_locko) + { + PropertyCacheLevel cacheLevel, referenceCacheLevel; + GetCacheLevels(out cacheLevel, out referenceCacheLevel); + + var cacheValues = GetCacheValues(cacheLevel); + if (cacheValues.XPathInitialized) return cacheValues.XPathValue; + + cacheValues.XPathValue = PropertyType.ConvertInterToXPath(referenceCacheLevel, GetInterValue(), IsPreviewing); + cacheValues.XPathInitialized = true; + return cacheValues.XPathValue; + } + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/Facade.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/Facade.cs index 533d9fd12a..59cb6e2d27 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/Facade.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/Facade.cs @@ -57,12 +57,5 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache protected override void DisposeResources() { } } - - public IPublishedProperty CreateFragmentProperty(PublishedPropertyType propertyType, Guid itemKey, bool previewing, PropertyCacheLevel referenceCacheLevel, object sourceValue = null) - { - // fixme - throw new NotImplementedException(); - } - } } diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/FacadeService.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/FacadeService.cs index f40f27f7b9..ffccc1b7c7 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/FacadeService.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/FacadeService.cs @@ -1,10 +1,13 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Persistence.UnitOfWork; +using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; using Umbraco.Core.Strings; using Umbraco.Web.Cache; @@ -200,5 +203,14 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache } #endregion + + #region Fragments + + public override IPublishedProperty CreateFragmentProperty(PublishedPropertyType propertyType, Guid itemKey, bool previewing, PropertyCacheLevel referenceCacheLevel, object sourceValue = null) + { + throw new NotImplementedException(); + } + + #endregion } } diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index a3307743b1..fb19fb00b9 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -9,202 +9,10 @@ using Umbraco.Core.Models.PublishedContent; using Umbraco.Web.Models; using Umbraco.Core; using Umbraco.Core.Models; +using Umbraco.Core.Services; namespace Umbraco.Web { - // fixme! + migrate a few more of them - /// - /// Provides extension methods for IPublishedItem. - /// - public static class PublishedItemExtensions - { - #region IsComposedOf - - /// - /// Gets a value indicating whether the content is of a content type composed of the given alias - /// - /// The content. - /// The content type alias. - /// A value indicating whether the content is of a content type composed of a content type identified by the alias. - public static bool IsComposedOf(this IPublishedFragment content, string alias) - { - return content.ContentType.CompositionAliases.Contains(alias); - } - - #endregion - - #region HasProperty - - /// - /// Gets a value indicating whether the content has a property identified by its alias. - /// - /// The content. - /// The property alias. - /// A value indicating whether the content has the property identified by the alias. - /// The content may have a property, and that property may not have a value. - public static bool HasProperty(this IPublishedFragment content, string alias) - { - return content.ContentType.GetPropertyType(alias) != null; - } - - #endregion - - #region HasValue - - /// - /// Gets a value indicating whether the content has a value for a property identified by its alias. - /// - /// The content. - /// The property alias. - /// A value indicating whether the content has a value for the property identified by the alias. - /// Returns true if GetProperty(alias) is not null and GetProperty(alias).HasValue is true. - public static bool HasValue(this IPublishedFragment content, string alias) - { - var prop = content.GetProperty(alias); - return prop != null && prop.HasValue; - } - - /// - /// Returns one of two strings depending on whether the content has a value for a property identified by its alias. - /// - /// The content. - /// The property alias. - /// The value to return if the content has a value for the property. - /// The value to return if the content has no value for the property. - /// Either or depending on whether the content - /// has a value for the property identified by the alias. - public static IHtmlString HasValue(this IPublishedFragment content, string alias, - string valueIfTrue, string valueIfFalse = null) - { - return content.HasValue(alias) - ? new HtmlString(valueIfTrue) - : new HtmlString(valueIfFalse ?? string.Empty); - } - - #endregion - - #region GetPropertyValue - - /// - /// Gets the value of a content's property identified by its alias. - /// - /// The content. - /// The property alias. - /// The value of the content's property identified by the alias. - /// - /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. - /// If no property with the specified alias exists, or if the property has no value, returns null. - /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. - /// The alias is case-insensitive. - /// - public static object GetPropertyValue(this IPublishedFragment content, string alias) - { - var property = content.GetProperty(alias); - return property?.Value; - } - - /// - /// Gets the value of a content's property identified by its alias, if it exists, otherwise a default value. - /// - /// The content. - /// The property alias. - /// The default value. - /// The value of the content's property identified by the alias, if it exists, otherwise a default value. - /// - /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. - /// If no property with the specified alias exists, or if the property has no value, returns . - /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. - /// The alias is case-insensitive. - /// - public static object GetPropertyValue(this IPublishedFragment content, string alias, string defaultValue) - { - var property = content.GetProperty(alias); - return property == null || property.HasValue == false ? defaultValue : property.Value; - } - - /// - /// Gets the value of a content's property identified by its alias, if it exists, otherwise a default value. - /// - /// The content. - /// The property alias. - /// The default value. - /// The value of the content's property identified by the alias, if it exists, otherwise a default value. - /// - /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. - /// If no property with the specified alias exists, or if the property has no value, returns . - /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. - /// The alias is case-insensitive. - /// - public static object GetPropertyValue(this IPublishedFragment content, string alias, object defaultValue) - { - var property = content.GetProperty(alias); - return property == null || property.HasValue == false ? defaultValue : property.Value; - } - - #endregion - - #region GetPropertyValue - - /// - /// Gets the value of a content's property identified by its alias, converted to a specified type. - /// - /// The target property type. - /// The content. - /// The property alias. - /// The value of the content's property identified by the alias, converted to the specified type. - /// - /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. - /// If no property with the specified alias exists, or if the property has no value, or if it could not be converted, returns default(T). - /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. - /// The alias is case-insensitive. - /// - public static T GetPropertyValue(this IPublishedFragment content, string alias) - { - return content.GetPropertyValue(alias, false, default(T)); - } - - /// - /// Gets the value of a content's property identified by its alias, converted to a specified type, if it exists, otherwise a default value. - /// - /// The target property type. - /// The content. - /// The property alias. - /// The default value. - /// The value of the content's property identified by the alias, converted to the specified type, if it exists, otherwise a default value. - /// - /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. - /// If no property with the specified alias exists, or if the property has no value, or if it could not be converted, returns . - /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. - /// The alias is case-insensitive. - /// - public static T GetPropertyValue(this IPublishedFragment content, string alias, T defaultValue) - { - return content.GetPropertyValue(alias, true, defaultValue); - } - - internal static T GetPropertyValue(this IPublishedFragment content, string alias, bool withDefaultValue, T defaultValue) - { - var property = content.GetProperty(alias); - if (property == null) return defaultValue; - - return property.GetValue(withDefaultValue, defaultValue); - } - - #endregion - - #region ToIndexedArray - - public static IndexedArrayItem[] ToIndexedArray(this IEnumerable source) - where TContent : class, IPublishedFragment - { - var set = source.Select((content, index) => new IndexedArrayItem(content, index)).ToArray(); - foreach (var setItem in set) setItem.TotalCount = set.Length; - return set; - } - - #endregion - } - /// /// Provides extension methods for IPublishedContent. /// @@ -494,19 +302,19 @@ namespace Umbraco.Web public static HtmlString Where(this IPublishedContent content, string predicate, string valueIfTrue) { - if (content == null) throw new ArgumentNullException("content"); + if (content == null) throw new ArgumentNullException(nameof(content)); return content.Where(predicate, valueIfTrue, string.Empty); } public static HtmlString Where(this IPublishedContent content, string predicate, string valueIfTrue, string valueIfFalse) { - if (content == null) throw new ArgumentNullException("content"); + if (content == null) throw new ArgumentNullException(nameof(content)); return new HtmlString(content.Where(predicate) ? valueIfTrue : valueIfFalse); } public static bool Where(this IPublishedContent content, string predicate) { - if (content == null) throw new ArgumentNullException("content"); + if (content == null) throw new ArgumentNullException(nameof(content)); var dynamicDocumentList = new DynamicPublishedContentList { content.AsDynamicOrNull() }; var filtered = dynamicDocumentList.Where(predicate); return filtered.Count() == 1; @@ -521,7 +329,7 @@ namespace Umbraco.Web // content should NOT be null public static dynamic AsDynamic(this IPublishedContent content) { - if (content == null) throw new ArgumentNullException("content"); + if (content == null) throw new ArgumentNullException(nameof(content)); return new DynamicPublishedContent(content); } @@ -575,9 +383,7 @@ namespace Umbraco.Web if (content.IsDocumentType(docTypeAlias)) return true; - if (recursive) - return IsDocumentTypeRecursive(content, docTypeAlias); - return false; + return recursive && IsDocumentTypeRecursive(content, docTypeAlias); } private static bool IsDocumentTypeRecursive(IPublishedContent content, string docTypeAlias) @@ -996,7 +802,7 @@ namespace Umbraco.Web /// Enumerates bottom-up ie walking up the tree (parent, grand-parent, etc). internal static IEnumerable EnumerateAncestors(this IPublishedContent content, bool orSelf) { - if (content == null) throw new ArgumentNullException("content"); + if (content == null) throw new ArgumentNullException(nameof(content)); if (orSelf) yield return content; while ((content = content.Parent) != null) yield return content; @@ -1169,21 +975,19 @@ namespace Umbraco.Web internal static IEnumerable EnumerateDescendants(this IPublishedContent content, bool orSelf) { - if (content == null) throw new ArgumentNullException("content"); + if (content == null) throw new ArgumentNullException(nameof(content)); if (orSelf) yield return content; - foreach (var child in content.Children) - foreach (var child2 in child.EnumerateDescendants()) - yield return child2; + foreach (var desc in content.Children.SelectMany(x => x.EnumerateDescendants())) + yield return desc; } internal static IEnumerable EnumerateDescendants(this IPublishedContent content) { yield return content; - foreach (var child in content.Children) - foreach (var child2 in child.EnumerateDescendants()) - yield return child2; + foreach (var desc in content.Children.SelectMany(x => x.EnumerateDescendants())) + yield return desc; } #endregion @@ -1202,7 +1006,7 @@ namespace Umbraco.Web public static IPublishedContent Up(this IPublishedContent content, int number) { if (number < 0) - throw new ArgumentOutOfRangeException("number", "Must be greater than, or equal to, zero."); + throw new ArgumentOutOfRangeException(nameof(number), "Must be greater than, or equal to, zero."); return number == 0 ? content : content.EnumerateAncestors(false).Skip(number).FirstOrDefault(); } @@ -1225,7 +1029,7 @@ namespace Umbraco.Web public static IPublishedContent Down(this IPublishedContent content, int number) { if (number < 0) - throw new ArgumentOutOfRangeException("number", "Must be greater than, or equal to, zero."); + throw new ArgumentOutOfRangeException(nameof(number), "Must be greater than, or equal to, zero."); if (number == 0) return content; content = content.Children.FirstOrDefault(); @@ -1260,7 +1064,7 @@ namespace Umbraco.Web public static T Parent(this IPublishedContent content) where T : class, IPublishedContent { - if (content == null) throw new ArgumentNullException("content"); + if (content == null) throw new ArgumentNullException(nameof(content)); return content.Parent as T; } @@ -1279,7 +1083,7 @@ namespace Umbraco.Web /// public static IEnumerable Children(this IPublishedContent content) { - if (content == null) throw new ArgumentNullException("content"); + if (content == null) throw new ArgumentNullException(nameof(content)); return content.Children; } @@ -1328,24 +1132,26 @@ namespace Umbraco.Web return content.Children().FirstOrDefault(); } - /// - /// Gets the children of the content in a DataTable. - /// - /// The content. - /// An optional content type alias. - /// The children of the content. - public static DataTable ChildrenAsTable(this IPublishedContent content, string contentTypeAliasFilter = "") - { - return GenerateDataTable(content, contentTypeAliasFilter); - } - - /// + /// /// Gets the children of the content in a DataTable. /// /// The content. + /// A service context. /// An optional content type alias. /// The children of the content. - private static DataTable GenerateDataTable(IPublishedContent content, string contentTypeAliasFilter = "") + public static DataTable ChildrenAsTable(this IPublishedContent content, ServiceContext services, string contentTypeAliasFilter = "") + { + return GenerateDataTable(content, services, contentTypeAliasFilter); + } + + /// + /// Gets the children of the content in a DataTable. + /// + /// The content. + /// A service context. + /// An optional content type alias. + /// The children of the content. + private static DataTable GenerateDataTable(IPublishedContent content, ServiceContext services, string contentTypeAliasFilter = "") { var firstNode = contentTypeAliasFilter.IsNullOrWhiteSpace() ? content.Children.Any() @@ -1360,7 +1166,7 @@ namespace Umbraco.Web //pass in the alias of the first child node since this is the node type we're rendering headers for firstNode.DocumentTypeAlias, //pass in the callback to extract the Dictionary of all defined aliases to their names - alias => GetPropertyAliasesAndNames(alias), + alias => GetPropertyAliasesAndNames(services, alias), //pass in a callback to populate the datatable, yup its a bit ugly but it's already legacy and we just want to maintain code in one place. () => { @@ -1404,72 +1210,45 @@ namespace Umbraco.Web #endregion - #region OfTypes - - // the .OfType() filter is nice when there's only one type - // this is to support filtering with multiple types - - public static IEnumerable OfTypes(this IEnumerable contents, params Type[] types) - { - return contents.Where(x => types.Contains(x.GetType())); - } - - public static IEnumerable OfTypes(this IEnumerable contents, params string[] types) - { - types = types.Select(x => x.ToLowerInvariant()).ToArray(); - return contents.Where(x => types.Contains(x.DocumentTypeAlias.ToLowerInvariant())); - } - - public static T OfType(this IPublishedContent content) - where T : class, IPublishedContent - { - return content as T; - } - - #endregion - #region PropertyAliasesAndNames - private static Func> _getPropertyAliasesAndNames; + private static Func> _getPropertyAliasesAndNames; /// /// This is used only for unit tests to set the delegate to look up aliases/names dictionary of a content type /// - internal static Func> GetPropertyAliasesAndNames + internal static Func> GetPropertyAliasesAndNames { - get - { - return _getPropertyAliasesAndNames ?? (_getPropertyAliasesAndNames = alias => - { - var userFields = GetAliasesAndNames(alias); - //ensure the standard fields are there - var allFields = new Dictionary - { - {"Id", "Id"}, - {"NodeName", "NodeName"}, - {"NodeTypeAlias", "NodeTypeAlias"}, - {"CreateDate", "CreateDate"}, - {"UpdateDate", "UpdateDate"}, - {"CreatorName", "CreatorName"}, - {"WriterName", "WriterName"}, - {"Url", "Url"} - }; - foreach (var f in userFields.Where(f => allFields.ContainsKey(f.Key) == false)) - { - allFields.Add(f.Key, f.Value); - } - return allFields; - }); - } + get { return _getPropertyAliasesAndNames ?? GetAliasesAndNames; } set { _getPropertyAliasesAndNames = value; } } - // fixme - temp, issue w/singleton, but removes dep on legacy - // should it work for media/members too? - private static Dictionary GetAliasesAndNames(string alias) + private static Dictionary GetAliasesAndNames(ServiceContext services, string alias) { - var type = ApplicationContext.Current.Services.ContentTypeService.Get(alias); - return GetAliasesAndNames(type); + var type = services.ContentTypeService.Get(alias) + ?? services.MediaTypeService.Get(alias) + ?? (IContentTypeBase)services.MemberTypeService.Get(alias); + var fields = GetAliasesAndNames(type); + + // ensure the standard fields are there + var stdFields = new Dictionary + { + {"Id", "Id"}, + {"NodeName", "NodeName"}, + {"NodeTypeAlias", "NodeTypeAlias"}, + {"CreateDate", "CreateDate"}, + {"UpdateDate", "UpdateDate"}, + {"CreatorName", "CreatorName"}, + {"WriterName", "WriterName"}, + {"Url", "Url"} + }; + + foreach (var field in stdFields.Where(x => fields.ContainsKey(x.Key) == false)) + { + fields[field.Key] = field.Value; + } + + return fields; } private static Dictionary GetAliasesAndNames(IContentTypeBase contentType) diff --git a/src/Umbraco.Web/PublishedFragmentExtensions.cs b/src/Umbraco.Web/PublishedFragmentExtensions.cs new file mode 100644 index 0000000000..cb2c2a4619 --- /dev/null +++ b/src/Umbraco.Web/PublishedFragmentExtensions.cs @@ -0,0 +1,212 @@ +using System.Collections.Generic; +using System.Linq; +using System.Web; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Web +{ + /// + /// Provides extension methods for IPublishedItem. + /// + public static class PublishedFragmentExtensions + { + #region IsComposedOf + + /// + /// Gets a value indicating whether the content is of a content type composed of the given alias + /// + /// The content. + /// The content type alias. + /// A value indicating whether the content is of a content type composed of a content type identified by the alias. + public static bool IsComposedOf(this IPublishedFragment content, string alias) + { + return content.ContentType.CompositionAliases.Contains(alias); + } + + #endregion + + #region HasProperty + + /// + /// Gets a value indicating whether the content has a property identified by its alias. + /// + /// The content. + /// The property alias. + /// A value indicating whether the content has the property identified by the alias. + /// The content may have a property, and that property may not have a value. + public static bool HasProperty(this IPublishedFragment content, string alias) + { + return content.ContentType.GetPropertyType(alias) != null; + } + + #endregion + + #region HasValue + + /// + /// Gets a value indicating whether the content has a value for a property identified by its alias. + /// + /// The content. + /// The property alias. + /// A value indicating whether the content has a value for the property identified by the alias. + /// Returns true if GetProperty(alias) is not null and GetProperty(alias).HasValue is true. + public static bool HasValue(this IPublishedFragment content, string alias) + { + var prop = content.GetProperty(alias); + return prop != null && prop.HasValue; + } + + /// + /// Returns one of two strings depending on whether the content has a value for a property identified by its alias. + /// + /// The content. + /// The property alias. + /// The value to return if the content has a value for the property. + /// The value to return if the content has no value for the property. + /// Either or depending on whether the content + /// has a value for the property identified by the alias. + public static IHtmlString HasValue(this IPublishedFragment content, string alias, + string valueIfTrue, string valueIfFalse = null) + { + return content.HasValue(alias) + ? new HtmlString(valueIfTrue) + : new HtmlString(valueIfFalse ?? string.Empty); + } + + #endregion + + #region GetPropertyValue + + /// + /// Gets the value of a content's property identified by its alias. + /// + /// The content. + /// The property alias. + /// The value of the content's property identified by the alias. + /// + /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. + /// If no property with the specified alias exists, or if the property has no value, returns null. + /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. + /// The alias is case-insensitive. + /// + public static object GetPropertyValue(this IPublishedFragment content, string alias) + { + var property = content.GetProperty(alias); + return property?.Value; + } + + /// + /// Gets the value of a content's property identified by its alias, if it exists, otherwise a default value. + /// + /// The content. + /// The property alias. + /// The default value. + /// The value of the content's property identified by the alias, if it exists, otherwise a default value. + /// + /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. + /// If no property with the specified alias exists, or if the property has no value, returns . + /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. + /// The alias is case-insensitive. + /// + public static object GetPropertyValue(this IPublishedFragment content, string alias, string defaultValue) + { + var property = content.GetProperty(alias); + return property == null || property.HasValue == false ? defaultValue : property.Value; + } + + /// + /// Gets the value of a content's property identified by its alias, if it exists, otherwise a default value. + /// + /// The content. + /// The property alias. + /// The default value. + /// The value of the content's property identified by the alias, if it exists, otherwise a default value. + /// + /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. + /// If no property with the specified alias exists, or if the property has no value, returns . + /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. + /// The alias is case-insensitive. + /// + public static object GetPropertyValue(this IPublishedFragment content, string alias, object defaultValue) + { + var property = content.GetProperty(alias); + return property == null || property.HasValue == false ? defaultValue : property.Value; + } + + #endregion + + #region GetPropertyValue + + /// + /// Gets the value of a content's property identified by its alias, converted to a specified type. + /// + /// The target property type. + /// The content. + /// The property alias. + /// The value of the content's property identified by the alias, converted to the specified type. + /// + /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. + /// If no property with the specified alias exists, or if the property has no value, or if it could not be converted, returns default(T). + /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. + /// The alias is case-insensitive. + /// + public static T GetPropertyValue(this IPublishedFragment content, string alias) + { + return content.GetPropertyValue(alias, false, default(T)); + } + + /// + /// Gets the value of a content's property identified by its alias, converted to a specified type, if it exists, otherwise a default value. + /// + /// The target property type. + /// The content. + /// The property alias. + /// The default value. + /// The value of the content's property identified by the alias, converted to the specified type, if it exists, otherwise a default value. + /// + /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. + /// If no property with the specified alias exists, or if the property has no value, or if it could not be converted, returns . + /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. + /// The alias is case-insensitive. + /// + public static T GetPropertyValue(this IPublishedFragment content, string alias, T defaultValue) + { + return content.GetPropertyValue(alias, true, defaultValue); + } + + internal static T GetPropertyValue(this IPublishedFragment content, string alias, bool withDefaultValue, T defaultValue) + { + var property = content.GetProperty(alias); + if (property == null) return defaultValue; + + return property.GetValue(withDefaultValue, defaultValue); + } + + #endregion + + #region ToIndexedArray + + public static IndexedArrayItem[] ToIndexedArray(this IEnumerable source) + where TContent : class, IPublishedFragment + { + var set = source.Select((content, index) => new IndexedArrayItem(content, index)).ToArray(); + foreach (var setItem in set) setItem.TotalCount = set.Length; + return set; + } + + #endregion + + #region OfTypes + + // the .OfType() filter is nice when there's only one type + // this is to support filtering with multiple types + public static IEnumerable OfTypes(this IEnumerable contents, params string[] types) + where T : IPublishedFragment + { + types = types.Select(x => x.ToLowerInvariant()).ToArray(); + return contents.Where(x => types.Contains(x.ContentType.Alias.ToLowerInvariant())); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 9b9eac31e0..64697b5e45 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -213,9 +213,9 @@ + - @@ -258,6 +258,7 @@ +