Retreat Amazing Nested Content Work

This commit is contained in:
Stephan
2017-06-04 16:22:43 +02:00
committed by Shannon
parent 53357b0582
commit 942d010264
26 changed files with 1991 additions and 71 deletions

View File

@@ -0,0 +1,166 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent;
namespace Umbraco.Web.Models
{
public class DetachedPublishedContent : PublishedContentWithKeyBase
{
private readonly Guid _key;
private readonly string _name;
private readonly PublishedContentType _contentType;
private readonly IEnumerable<IPublishedProperty> _properties;
private readonly int _sortOrder;
private readonly bool _isPreviewing;
private readonly IPublishedContent _containerNode;
public DetachedPublishedContent(
Guid key,
string name,
PublishedContentType contentType,
IEnumerable<IPublishedProperty> properties,
IPublishedContent containerNode = null,
int sortOrder = 0,
bool isPreviewing = false)
{
_key = key;
_name = name;
_contentType = contentType;
_properties = properties;
_sortOrder = sortOrder;
_isPreviewing = isPreviewing;
_containerNode = containerNode;
}
public override Guid Key
{
get { return _key; }
}
public override int Id
{
get { return 0; }
}
public override string Name
{
get { return _name; }
}
public override bool IsDraft
{
get { return _isPreviewing; }
}
public override PublishedItemType ItemType
{
get { return PublishedItemType.Content; }
}
public override PublishedContentType ContentType
{
get { return _contentType; }
}
public override string DocumentTypeAlias
{
get { return _contentType.Alias; }
}
public override int DocumentTypeId
{
get { return _contentType.Id; }
}
public override ICollection<IPublishedProperty> Properties
{
get { return _properties.ToArray(); }
}
public override IPublishedProperty GetProperty(string alias)
{
return _properties.FirstOrDefault(x => x.PropertyTypeAlias.InvariantEquals(alias));
}
public override IPublishedProperty GetProperty(string alias, bool recurse)
{
if (recurse)
throw new NotSupportedException();
return GetProperty(alias);
}
public override IPublishedContent Parent
{
get { return null; }
}
public override IEnumerable<IPublishedContent> Children
{
get { return Enumerable.Empty<IPublishedContent>(); }
}
public override int TemplateId
{
get { return 0; }
}
public override int SortOrder
{
get { return _sortOrder; }
}
public override string UrlName
{
get { return null; }
}
public override string WriterName
{
get { return _containerNode != null ? _containerNode.WriterName : null; }
}
public override string CreatorName
{
get { return _containerNode != null ? _containerNode.CreatorName : null; }
}
public override int WriterId
{
get { return _containerNode != null ? _containerNode.WriterId : 0; }
}
public override int CreatorId
{
get { return _containerNode != null ? _containerNode.CreatorId : 0; }
}
public override string Path
{
get { return null; }
}
public override DateTime CreateDate
{
get { return _containerNode != null ? _containerNode.CreateDate : DateTime.MinValue; }
}
public override DateTime UpdateDate
{
get { return _containerNode != null ? _containerNode.UpdateDate : DateTime.MinValue; }
}
public override Guid Version
{
get { return _containerNode != null ? _containerNode.Version : Guid.Empty; }
}
public override int Level
{
get { return 0; }
}
}
}

View File

@@ -0,0 +1,52 @@
using System;
using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent;
namespace Umbraco.Web.Models
{
internal class DetachedPublishedProperty : IPublishedProperty
{
private readonly PublishedPropertyType _propertyType;
private readonly object _rawValue;
private readonly Lazy<object> _sourceValue;
private readonly Lazy<object> _objectValue;
private readonly Lazy<object> _xpathValue;
private readonly bool _isPreview;
public DetachedPublishedProperty(PublishedPropertyType propertyType, object value)
: this(propertyType, value, false)
{
}
public DetachedPublishedProperty(PublishedPropertyType propertyType, object value, bool isPreview)
{
_propertyType = propertyType;
_isPreview = isPreview;
_rawValue = value;
_sourceValue = new Lazy<object>(() => _propertyType.ConvertDataToSource(_rawValue, _isPreview));
_objectValue = new Lazy<object>(() => _propertyType.ConvertSourceToObject(_sourceValue.Value, _isPreview));
_xpathValue = new Lazy<object>(() => _propertyType.ConvertSourceToXPath(_sourceValue.Value, _isPreview));
}
public string PropertyTypeAlias
{
get
{
return _propertyType.PropertyTypeAlias;
}
}
public bool HasValue
{
get { return DataValue != null && DataValue.ToString().Trim().Length > 0; }
}
public object DataValue { get { return _rawValue; } }
public object Value { get { return _objectValue.Value; } }
public object XPathValue { get { return _xpathValue.Value; } }
}
}

View File

@@ -0,0 +1,27 @@
using System.Collections.Generic;
using System.Linq;
using Umbraco.Web.Editors;
using Umbraco.Web.Mvc;
namespace Umbraco.Web.PropertyEditors
{
[PluginController("UmbracoApi")]
public class NestedContentController : UmbracoAuthorizedJsonController
{
[System.Web.Http.HttpGet]
public IEnumerable<object> GetContentTypes()
{
return Services.ContentTypeService.GetAllContentTypes()
.OrderBy(x => x.SortOrder)
.Select(x => new
{
id = x.Id,
guid = x.Key,
name = x.Name,
alias = x.Alias,
icon = x.Icon,
tabs = x.CompositionPropertyGroups.Select(y => y.Name).Distinct()
});
}
}
}

View File

@@ -0,0 +1,131 @@
using System;
using System.Linq;
using Newtonsoft.Json.Linq;
using Umbraco.Core;
using Umbraco.Core.Models;
namespace Umbraco.Web.PropertyEditors
{
internal static class NestedContentHelper
{
private const string CacheKeyPrefix = "Umbraco.Web.PropertyEditors.NestedContent.GetPreValuesCollectionByDataTypeId_";
public static PreValueCollection GetPreValuesCollectionByDataTypeId(int dtdId)
{
var preValueCollection = (PreValueCollection)ApplicationContext.Current.ApplicationCache.RuntimeCache.GetCacheItem(
string.Concat(CacheKeyPrefix, dtdId),
() => ApplicationContext.Current.Services.DataTypeService.GetPreValuesCollectionByDataTypeId(dtdId));
return preValueCollection;
}
public static void ClearCache(int id)
{
ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(
string.Concat(CacheKeyPrefix, id));
}
public static string GetContentTypeAliasFromItem(JObject item)
{
var contentTypeAliasProperty = item[NestedContentPropertyEditor.ContentTypeAliasPropertyKey];
if (contentTypeAliasProperty == null)
{
return null;
}
return contentTypeAliasProperty.ToObject<string>();
}
public static IContentType GetContentTypeFromItem(JObject item)
{
var contentTypeAlias = GetContentTypeAliasFromItem(item);
if (string.IsNullOrEmpty(contentTypeAlias))
{
return null;
}
return ApplicationContext.Current.Services.ContentTypeService.GetContentType(contentTypeAlias);
}
#region Conversion from v0.1.1 data formats
public static void ConvertItemValueFromV011(JObject item, int dtdId, ref PreValueCollection preValues)
{
var contentTypeAlias = GetContentTypeAliasFromItem(item);
if (contentTypeAlias != null)
{
// the item is already in >v0.1.1 format
return;
}
// old style (v0.1.1) data, let's attempt a conversion
// - get the prevalues (if they're not loaded already)
preValues = preValues ?? GetPreValuesCollectionByDataTypeId(dtdId);
// - convert the prevalues (if necessary)
ConvertPreValueCollectionFromV011(preValues);
// - get the content types prevalue as JArray
var preValuesAsDictionary = preValues.PreValuesAsDictionary.ToDictionary(x => x.Key, x => x.Value.Value);
if (!preValuesAsDictionary.ContainsKey(ContentTypesPreValueKey) || string.IsNullOrEmpty(preValuesAsDictionary[ContentTypesPreValueKey]) != false)
{
return;
}
var preValueContentTypes = JArray.Parse(preValuesAsDictionary[ContentTypesPreValueKey]);
if (preValueContentTypes.Any())
{
// the only thing we can really do is assume that the item is the first available content type
item[NestedContentPropertyEditor.ContentTypeAliasPropertyKey] = preValueContentTypes.First().Value<string>("ncAlias");
}
}
public static void ConvertPreValueCollectionFromV011(PreValueCollection preValueCollection)
{
if (preValueCollection == null)
{
return;
}
var persistedPreValuesAsDictionary = preValueCollection.PreValuesAsDictionary.ToDictionary(x => x.Key, x => x.Value.Value);
// do we have a "docTypeGuid" prevalue and no "contentTypes" prevalue?
if (persistedPreValuesAsDictionary.ContainsKey("docTypeGuid") == false || persistedPreValuesAsDictionary.ContainsKey(ContentTypesPreValueKey))
{
// the prevalues are already in >v0.1.1 format
return;
}
// attempt to parse the doc type guid
Guid guid;
if (Guid.TryParse(persistedPreValuesAsDictionary["docTypeGuid"], out guid) == false)
{
// this shouldn't happen... but just in case.
return;
}
// find the content type
var contentType = ApplicationContext.Current.Services.ContentTypeService.GetAllContentTypes().FirstOrDefault(c => c.Key == guid);
if (contentType == null)
{
return;
}
// add a prevalue in the format expected by the new (>0.1.1) content type picker/configurator
preValueCollection.PreValuesAsDictionary[ContentTypesPreValueKey] = new PreValue(
string.Format(@"[{{""ncAlias"": ""{0}"", ""ncTabAlias"": ""{1}"", ""nameTemplate"": ""{2}"", }}]",
contentType.Alias,
persistedPreValuesAsDictionary["tabAlias"],
persistedPreValuesAsDictionary["nameTemplate"]
)
);
}
private static string ContentTypesPreValueKey
{
get { return NestedContentPropertyEditor.NestedContentPreValueEditor.ContentTypesPreValueKey; }
}
#endregion
}
}

View File

@@ -0,0 +1,413 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Editors;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
namespace Umbraco.Web.PropertyEditors
{
[PropertyEditor(Constants.PropertyEditors.NestedContentAlias, "Nested Content", "nestedcontent", ValueType = "JSON")]
public class NestedContentPropertyEditor : PropertyEditor
{
internal const string ContentTypeAliasPropertyKey = "ncContentTypeAlias";
private IDictionary<string, object> _defaultPreValues;
public override IDictionary<string, object> DefaultPreValues
{
get { return _defaultPreValues; }
set { _defaultPreValues = value; }
}
public NestedContentPropertyEditor()
{
// Setup default values
_defaultPreValues = new Dictionary<string, object>
{
{NestedContentPreValueEditor.ContentTypesPreValueKey, ""},
{"minItems", 0},
{"maxItems", 0},
{"confirmDeletes", "1"},
{"showIcons", "1"}
};
}
#region Pre Value Editor
protected override PreValueEditor CreatePreValueEditor()
{
return new NestedContentPreValueEditor();
}
internal class NestedContentPreValueEditor : PreValueEditor
{
internal const string ContentTypesPreValueKey = "contentTypes";
[PreValueField(ContentTypesPreValueKey, "Doc Types", "views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html", Description = "Select the doc types to use as the data blueprint.")]
public string[] ContentTypes { get; set; }
[PreValueField("minItems", "Min Items", "number", Description = "Set the minimum number of items allowed.")]
public string MinItems { get; set; }
[PreValueField("maxItems", "Max Items", "number", Description = "Set the maximum number of items allowed.")]
public string MaxItems { get; set; }
[PreValueField("confirmDeletes", "Confirm Deletes", "boolean", Description = "Set whether item deletions should require confirming.")]
public string ConfirmDeletes { get; set; }
[PreValueField("showIcons", "Show Icons", "boolean", Description = "Set whether to show the items doc type icon in the list.")]
public string ShowIcons { get; set; }
[PreValueField("hideLabel", "Hide Label", "boolean", Description = "Set whether to hide the editor label and have the list take up the full width of the editor window.")]
public string HideLabel { get; set; }
public override IDictionary<string, object> ConvertDbToEditor(IDictionary<string, object> defaultPreVals, PreValueCollection persistedPreVals)
{
// re-format old style (v0.1.1) pre values if necessary
NestedContentHelper.ConvertPreValueCollectionFromV011(persistedPreVals);
return base.ConvertDbToEditor(defaultPreVals, persistedPreVals);
}
}
#endregion
#region Value Editor
protected override PropertyValueEditor CreateValueEditor()
{
return new NestedContentPropertyValueEditor(base.CreateValueEditor());
}
internal class NestedContentPropertyValueEditor : PropertyValueEditorWrapper
{
public NestedContentPropertyValueEditor(PropertyValueEditor wrapped)
: base(wrapped)
{
Validators.Add(new NestedContentValidator());
}
internal ServiceContext Services
{
get { return ApplicationContext.Current.Services; }
}
public override void ConfigureForDisplay(PreValueCollection preValues)
{
base.ConfigureForDisplay(preValues);
var asDictionary = preValues.PreValuesAsDictionary.ToDictionary(x => x.Key, x => x.Value.Value);
if (asDictionary.ContainsKey("hideLabel"))
{
var boolAttempt = asDictionary["hideLabel"].TryConvertTo<bool>();
if (boolAttempt.Success)
{
HideLabel = boolAttempt.Result;
}
}
}
#region DB to String
public override string ConvertDbToString(Property property, PropertyType propertyType, IDataTypeService dataTypeService)
{
// Convert / validate value
if (property.Value == null || string.IsNullOrWhiteSpace(property.Value.ToString()))
return string.Empty;
var value = JsonConvert.DeserializeObject<List<object>>(property.Value.ToString());
if (value == null)
return string.Empty;
// Process value
PreValueCollection preValues = null;
for (var i = 0; i < value.Count; i++)
{
var o = value[i];
var propValues = ((JObject)o);
// convert from old style (v0.1.1) data format if necessary
NestedContentHelper.ConvertItemValueFromV011(propValues, propertyType.DataTypeDefinitionId, ref preValues);
var contentType = NestedContentHelper.GetContentTypeFromItem(propValues);
if (contentType == null)
{
continue;
}
var propValueKeys = propValues.Properties().Select(x => x.Name).ToArray();
foreach (var propKey in propValueKeys)
{
var propType = contentType.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == propKey);
if (propType == null)
{
if (IsSystemPropertyKey(propKey) == false)
{
// Property missing so just delete the value
propValues[propKey] = null;
}
}
else
{
try
{
// Create a fake property using the property abd stored value
var prop = new Property(propType, propValues[propKey] == null ? null : propValues[propKey].ToString());
// Lookup the property editor
var propEditor = PropertyEditorResolver.Current.GetByAlias(propType.PropertyEditorAlias);
// Get the editor to do it's conversion, and store it back
propValues[propKey] = propEditor.ValueEditor.ConvertDbToString(prop, propType, dataTypeService);
}
catch (InvalidOperationException)
{
// https://github.com/umco/umbraco-nested-content/issues/111
// Catch any invalid cast operations as likely means courier failed due to missing
// or trashed item so couldn't convert a guid back to an int
propValues[propKey] = null;
}
}
}
}
// Update the value on the property
property.Value = JsonConvert.SerializeObject(value);
// Pass the call down
return base.ConvertDbToString(property, propertyType, dataTypeService);
}
#endregion
#region DB to Editor
public override object ConvertDbToEditor(Property property, PropertyType propertyType, IDataTypeService dataTypeService)
{
if (property.Value == null || string.IsNullOrWhiteSpace(property.Value.ToString()))
return string.Empty;
var value = JsonConvert.DeserializeObject<List<object>>(property.Value.ToString());
if (value == null)
return string.Empty;
// Process value
PreValueCollection preValues = null;
for (var i = 0; i < value.Count; i++)
{
var o = value[i];
var propValues = ((JObject)o);
// convert from old style (v0.1.1) data format if necessary
NestedContentHelper.ConvertItemValueFromV011(propValues, propertyType.DataTypeDefinitionId, ref preValues);
var contentType = NestedContentHelper.GetContentTypeFromItem(propValues);
if (contentType == null)
{
continue;
}
var propValueKeys = propValues.Properties().Select(x => x.Name).ToArray();
foreach (var propKey in propValueKeys)
{
var propType = contentType.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == propKey);
if (propType == null)
{
if (IsSystemPropertyKey(propKey) == false)
{
// Property missing so just delete the value
propValues[propKey] = null;
}
}
else
{
try
{
// Create a fake property using the property and stored value
var prop = new Property(propType, propValues[propKey] == null ? null : propValues[propKey].ToString());
// Lookup the property editor
var propEditor = PropertyEditorResolver.Current.GetByAlias(propType.PropertyEditorAlias);
// Get the editor to do it's conversion
var newValue = propEditor.ValueEditor.ConvertDbToEditor(prop, propType, dataTypeService);
// Store the value back
propValues[propKey] = (newValue == null) ? null : JToken.FromObject(newValue);
}
catch (InvalidOperationException)
{
// https://github.com/umco/umbraco-nested-content/issues/111
// Catch any invalid cast operations as likely means courier failed due to missing
// or trashed item so couldn't convert a guid back to an int
propValues[propKey] = null;
}
}
}
}
// Update the value on the property
property.Value = JsonConvert.SerializeObject(value);
// Pass the call down
return base.ConvertDbToEditor(property, propertyType, dataTypeService);
}
#endregion
#region Editor to DB
public override object ConvertEditorToDb(ContentPropertyData editorValue, object currentValue)
{
if (editorValue.Value == null || string.IsNullOrWhiteSpace(editorValue.Value.ToString()))
return null;
var value = JsonConvert.DeserializeObject<List<object>>(editorValue.Value.ToString());
if (value == null)
return null;
// Issue #38 - Keep recursive property lookups working
if (!value.Any())
return null;
// Process value
for (var i = 0; i < value.Count; i++)
{
var o = value[i];
var propValues = ((JObject)o);
var contentType = NestedContentHelper.GetContentTypeFromItem(propValues);
if (contentType == null)
{
continue;
}
var propValueKeys = propValues.Properties().Select(x => x.Name).ToArray();
foreach (var propKey in propValueKeys)
{
var propType = contentType.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == propKey);
if (propType == null)
{
if (IsSystemPropertyKey(propKey) == false)
{
// Property missing so just delete the value
propValues[propKey] = null;
}
}
else
{
// Fetch the property types prevalue
var propPreValues = Services.DataTypeService.GetPreValuesCollectionByDataTypeId(
propType.DataTypeDefinitionId);
// Lookup the property editor
var propEditor = PropertyEditorResolver.Current.GetByAlias(propType.PropertyEditorAlias);
// Create a fake content property data object
var contentPropData = new ContentPropertyData(
propValues[propKey], propPreValues,
new Dictionary<string, object>());
// Get the property editor to do it's conversion
var newValue = propEditor.ValueEditor.ConvertEditorToDb(contentPropData, propValues[propKey]);
// Store the value back
propValues[propKey] = (newValue == null) ? null : JToken.FromObject(newValue);
}
}
}
return JsonConvert.SerializeObject(value);
}
#endregion
}
internal class NestedContentValidator : IPropertyValidator
{
public IEnumerable<ValidationResult> Validate(object rawValue, PreValueCollection preValues, PropertyEditor editor)
{
var value = JsonConvert.DeserializeObject<List<object>>(rawValue.ToString());
if (value == null)
yield break;
IDataTypeService dataTypeService = ApplicationContext.Current.Services.DataTypeService;
for (var i = 0; i < value.Count; i++)
{
var o = value[i];
var propValues = ((JObject)o);
var contentType = NestedContentHelper.GetContentTypeFromItem(propValues);
if (contentType == null)
{
continue;
}
var propValueKeys = propValues.Properties().Select(x => x.Name).ToArray();
foreach (var propKey in propValueKeys)
{
var propType = contentType.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == propKey);
if (propType != null)
{
PreValueCollection propPrevalues = dataTypeService.GetPreValuesCollectionByDataTypeId(propType.DataTypeDefinitionId);
PropertyEditor propertyEditor = PropertyEditorResolver.Current.GetByAlias(propType.PropertyEditorAlias);
foreach (IPropertyValidator validator in propertyEditor.ValueEditor.Validators)
{
foreach (ValidationResult result in validator.Validate(propValues[propKey], propPrevalues, propertyEditor))
{
result.ErrorMessage = "Item " + (i + 1) + " '" + propType.Name + "' " + result.ErrorMessage;
yield return result;
}
}
// Check mandatory
if (propType.Mandatory)
{
if (propValues[propKey] == null)
yield return new ValidationResult("Item " + (i + 1) + " '" + propType.Name + "' cannot be null", new[] { propKey });
else if (propValues[propKey].ToString().IsNullOrWhiteSpace())
yield return new ValidationResult("Item " + (i + 1) + " '" + propType.Name + "' cannot be empty", new[] { propKey });
}
// Check regex
if (!propType.ValidationRegExp.IsNullOrWhiteSpace()
&& propValues[propKey] != null && !propValues[propKey].ToString().IsNullOrWhiteSpace())
{
var regex = new Regex(propType.ValidationRegExp);
if (!regex.IsMatch(propValues[propKey].ToString()))
{
yield return new ValidationResult("Item " + (i + 1) + " '" + propType.Name + "' is invalid, it does not match the correct pattern", new[] { propKey });
}
}
}
}
}
}
}
#endregion
private static bool IsSystemPropertyKey(string propKey)
{
return propKey == "name" || propKey == "key" || propKey == ContentTypeAliasPropertyKey;
}
}
}

View File

@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.PropertyEditors;
namespace Umbraco.Web.PropertyEditors.ValueConverters
{
public class NestedContentManyValueConverter : PropertyValueConverterBase, IPropertyValueConverterMeta
{
public override bool IsConverter(PublishedPropertyType propertyType)
{
return propertyType.IsNestedContentProperty() && !propertyType.IsSingleNestedContentProperty();
}
public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview)
{
try
{
return propertyType.ConvertPropertyToNestedContent(source, preview);
}
catch (Exception e)
{
LogHelper.Error<NestedContentManyValueConverter>("Error converting value", e);
}
return null;
}
public virtual Type GetPropertyValueType(PublishedPropertyType propertyType)
{
return typeof(IEnumerable<IPublishedContent>);
}
public virtual PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType, PropertyCacheValue cacheValue)
{
return PropertyCacheLevel.Content;
}
}
}

View File

@@ -0,0 +1,133 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Web.Models;
namespace Umbraco.Web.PropertyEditors.ValueConverters
{
internal static class NestedContentPublishedPropertyTypeExtensions
{
public static bool IsNestedContentProperty(this PublishedPropertyType publishedProperty)
{
return publishedProperty.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.NestedContentAlias);
}
public static bool IsSingleNestedContentProperty(this PublishedPropertyType publishedProperty)
{
if (!publishedProperty.IsNestedContentProperty())
{
return false;
}
var preValueCollection = NestedContentHelper.GetPreValuesCollectionByDataTypeId(publishedProperty.DataTypeId);
var preValueDictionary = preValueCollection.PreValuesAsDictionary.ToDictionary(x => x.Key, x => x.Value.Value);
int minItems, maxItems;
return preValueDictionary.ContainsKey("minItems") &&
int.TryParse(preValueDictionary["minItems"], out minItems) && minItems == 1
&& preValueDictionary.ContainsKey("maxItems") &&
int.TryParse(preValueDictionary["maxItems"], out maxItems) && maxItems == 1;
}
public static object ConvertPropertyToNestedContent(this PublishedPropertyType propertyType, object source, bool preview)
{
using (DisposableTimer.DebugDuration<PublishedPropertyType>(string.Format("ConvertPropertyToNestedContent ({0})", propertyType.DataTypeId)))
{
if (source != null && !source.ToString().IsNullOrWhiteSpace())
{
var rawValue = JsonConvert.DeserializeObject<List<object>>(source.ToString());
var processedValue = new List<IPublishedContent>();
var preValueCollection = NestedContentHelper.GetPreValuesCollectionByDataTypeId(propertyType.DataTypeId);
var preValueDictionary = preValueCollection.PreValuesAsDictionary.ToDictionary(x => x.Key, x => x.Value.Value);
for (var i = 0; i < rawValue.Count; i++)
{
var item = (JObject)rawValue[i];
// Convert from old style (v.0.1.1) data format if necessary
// - Please note: This call has virtually no impact on rendering performance for new style (>v0.1.1).
// Even so, this should be removed eventually, when it's safe to assume that there is
// no longer any need for conversion.
NestedContentHelper.ConvertItemValueFromV011(item, propertyType.DataTypeId, ref preValueCollection);
var contentTypeAlias = NestedContentHelper.GetContentTypeAliasFromItem(item);
if (string.IsNullOrEmpty(contentTypeAlias))
{
continue;
}
var publishedContentType = PublishedContentType.Get(PublishedItemType.Content, contentTypeAlias);
if (publishedContentType == null)
{
continue;
}
var propValues = item.ToObject<Dictionary<string, object>>();
var properties = new List<IPublishedProperty>();
foreach (var jProp in propValues)
{
var propType = publishedContentType.GetPropertyType(jProp.Key);
if (propType != null)
{
properties.Add(new DetachedPublishedProperty(propType, jProp.Value, preview));
}
}
// Parse out the name manually
object nameObj = null;
if (propValues.TryGetValue("name", out nameObj))
{
// Do nothing, we just want to parse out the name if we can
}
object keyObj;
var key = Guid.Empty;
if (propValues.TryGetValue("key", out keyObj))
{
key = Guid.Parse(keyObj.ToString());
}
// Get the current request node we are embedded in
var pcr = UmbracoContext.Current == null ? null : UmbracoContext.Current.PublishedContentRequest;
var containerNode = pcr != null && pcr.HasPublishedContent ? pcr.PublishedContent : null;
// Create the model based on our implementation of IPublishedContent
IPublishedContent content = new DetachedPublishedContent(
key,
nameObj == null ? null : nameObj.ToString(),
publishedContentType,
properties.ToArray(),
containerNode,
i,
preview);
if (PublishedContentModelFactoryResolver.HasCurrent)
{
// Let the current model factory create a typed model to wrap our model
content = PublishedContentModelFactoryResolver.Current.Factory.CreateModel(content);
}
// Add the (typed) model as a result
processedValue.Add(content);
}
if (propertyType.IsSingleNestedContentProperty())
{
return processedValue.FirstOrDefault();
}
return processedValue;
}
}
return null;
}
}
}

View File

@@ -0,0 +1,40 @@
using System;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.PropertyEditors;
namespace Umbraco.Web.PropertyEditors.ValueConverters
{
public class NestedContentSingleValueConverter : PropertyValueConverterBase, IPropertyValueConverterMeta
{
public override bool IsConverter(PublishedPropertyType propertyType)
{
return propertyType.IsSingleNestedContentProperty();
}
public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview)
{
try
{
return propertyType.ConvertPropertyToNestedContent(source, preview);
}
catch (Exception e)
{
LogHelper.Error<NestedContentSingleValueConverter>("Error converting value", e);
}
return null;
}
public virtual Type GetPropertyValueType(PublishedPropertyType propertyType)
{
return typeof(IPublishedContent);
}
public virtual PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType, PropertyCacheValue cacheValue)
{
return PropertyCacheLevel.Content;
}
}
}

View File

@@ -24,6 +24,12 @@ namespace Umbraco.Web
public static Guid GetKey(this IPublishedContent content)
{
var wrapped = content as PublishedContentWrapped;
while (wrapped != null)
{
content = wrapped.Unwrap();
wrapped = content as PublishedContentWrapped;
}
var contentWithKey = content as IPublishedContentWithKey;
return contentWithKey == null ? Guid.Empty : contentWithKey.Key;
}

View File

@@ -304,7 +304,30 @@
<Compile Include="ApplicationContextExtensions.cs" />
<Compile Include="AreaRegistrationContextExtensions.cs" />
<Compile Include="BatchedDatabaseServerMessengerStartup.cs" />
<Compile Include="Cache\ApplicationCacheRefresher.cs" />
<Compile Include="Cache\ApplicationTreeCacheRefresher.cs" />
<Compile Include="Cache\CacheRefresherEventHandler.cs" />
<Compile Include="Cache\ContentTypeCacheRefresher.cs" />
<Compile Include="Cache\DataTypeCacheRefresher.cs" />
<Compile Include="Cache\DictionaryCacheRefresher.cs" />
<Compile Include="Cache\DistributedCache.cs" />
<Compile Include="Cache\DistributedCacheExtensions.cs" />
<Compile Include="Cache\DomainCacheRefresher.cs" />
<Compile Include="Cache\LanguageCacheRefresher.cs" />
<Compile Include="Cache\MacroCacheRefresher.cs" />
<Compile Include="Cache\MediaCacheRefresher.cs" />
<Compile Include="Cache\MemberCacheRefresher.cs" />
<Compile Include="Cache\MemberGroupCacheRefresher.cs" />
<Compile Include="Cache\PageCacheRefresher.cs" />
<Compile Include="Cache\PublicAccessCacheRefresher.cs" />
<Compile Include="Cache\RelationTypeCacheRefresher.cs" />
<Compile Include="Cache\StylesheetCacheRefresher.cs" />
<Compile Include="Cache\StylesheetPropertyCacheRefresher.cs" />
<Compile Include="Cache\TemplateCacheRefresher.cs" />
<Compile Include="Cache\UnpublishedPageCacheRefresher.cs" />
<Compile Include="Cache\UserCacheRefresher.cs" />
<Compile Include="Cache\UserPermissionsCacheRefresher.cs" />
<Compile Include="Cache\UserTypeCacheRefresher.cs" />
<Compile Include="Editors\BackOfficeNotificationsController.cs" />
<Compile Include="Editors\EditorValidationResolver.cs" />
<Compile Include="Editors\EditorValidator.cs" />
@@ -366,6 +389,8 @@
<Compile Include="Models\ContentEditing\SimpleNotificationModel.cs" />
<Compile Include="Models\ContentEditing\SnippetDisplay.cs" />
<Compile Include="Models\ContentEditing\TemplateDisplay.cs" />
<Compile Include="Models\DetachedPublishedContent.cs" />
<Compile Include="Models\DetachedPublishedProperty.cs" />
<Compile Include="Models\LocalPackageInstallModel.cs" />
<Compile Include="Models\Mapping\CodeFileDisplayMapper.cs" />
<Compile Include="Models\Mapping\ContentTypeModelMapperExtensions.cs" />
@@ -402,6 +427,9 @@
<Compile Include="PropertyEditors\MediaPicker2PropertyEditor.cs" />
<Compile Include="PropertyEditors\MemberPicker2PropertyEditor.cs" />
<Compile Include="PropertyEditors\MultiNodeTreePicker2PropertyEditor.cs" />
<Compile Include="PropertyEditors\NestedContentController.cs" />
<Compile Include="PropertyEditors\NestedContentHelper.cs" />
<Compile Include="PropertyEditors\NestedContentPropertyEditor.cs" />
<Compile Include="PropertyEditors\RelatedLinks2PropertyEditor.cs" />
<Compile Include="PropertyEditors\ValueConverters\ContentPickerPropertyConverter.cs" />
<Compile Include="PropertyEditors\ParameterEditors\MultipleContentPickerParameterEditor.cs" />
@@ -412,6 +440,9 @@
<Compile Include="PropertyEditors\ValueConverters\MemberPickerPropertyConverter.cs" />
<Compile Include="PropertyEditors\ValueConverters\MultiNodeTreePickerPropertyConverter.cs" />
<Compile Include="PropertyEditors\ValueConverters\MultipleMediaPickerPropertyConverter.cs" />
<Compile Include="PropertyEditors\ValueConverters\NestedContentSingleValueConverter.cs" />
<Compile Include="PropertyEditors\ValueConverters\NestedContentManyValueConverter.cs" />
<Compile Include="PropertyEditors\ValueConverters\NestedContentPublishedPropertyTypeExtensions.cs" />
<Compile Include="PublishedContentQueryExtensions.cs" />
<Compile Include="Routing\RedirectTrackingEventHandler.cs" />
<Compile Include="Editors\RedirectUrlManagementController.cs" />
@@ -470,29 +501,6 @@
<Compile Include="BatchedDatabaseServerMessenger.cs" />
<Compile Include="BatchedWebServiceServerMessenger.cs" />
<Compile Include="CacheHelperExtensions.cs" />
<Compile Include="Cache\ApplicationCacheRefresher.cs" />
<Compile Include="Cache\ApplicationTreeCacheRefresher.cs" />
<Compile Include="Cache\ContentTypeCacheRefresher.cs" />
<Compile Include="Cache\DataTypeCacheRefresher.cs" />
<Compile Include="Cache\DictionaryCacheRefresher.cs" />
<Compile Include="Cache\DistributedCache.cs" />
<Compile Include="Cache\DistributedCacheExtensions.cs" />
<Compile Include="Cache\CacheRefresherEventHandler.cs" />
<Compile Include="Cache\DomainCacheRefresher.cs" />
<Compile Include="Cache\LanguageCacheRefresher.cs" />
<Compile Include="Cache\MacroCacheRefresher.cs" />
<Compile Include="Cache\MediaCacheRefresher.cs" />
<Compile Include="Cache\MemberCacheRefresher.cs" />
<Compile Include="Cache\MemberGroupCacheRefresher.cs" />
<Compile Include="Cache\PageCacheRefresher.cs" />
<Compile Include="Cache\PublicAccessCacheRefresher.cs" />
<Compile Include="Cache\StylesheetCacheRefresher.cs" />
<Compile Include="Cache\StylesheetPropertyCacheRefresher.cs" />
<Compile Include="Cache\TemplateCacheRefresher.cs" />
<Compile Include="Cache\UnpublishedPageCacheRefresher.cs" />
<Compile Include="Cache\UserCacheRefresher.cs" />
<Compile Include="Cache\UserPermissionsCacheRefresher.cs" />
<Compile Include="Cache\UserTypeCacheRefresher.cs" />
<Compile Include="Editors\AuthenticationController.cs" />
<Compile Include="Controllers\UmbProfileController.cs" />
<Compile Include="Editors\ContentController.cs" />