Centralizes logic to validate complex editors, starts transitioning to new JSON error messages for complex editors, starts adding tests

This commit is contained in:
Shannon
2020-06-15 23:05:32 +10:00
parent ba43a43483
commit 32e3ebb6fb
27 changed files with 659 additions and 193 deletions

View File

@@ -3,8 +3,12 @@ using System.Collections.Generic;
namespace Umbraco.Core.Models.Blocks
{
// TODO: Rename this, we don't want to use the name "Helper"
// TODO: What is this? This requires code docs
// TODO: This is not used publicly at all - therefore it probably shouldn't be public
public interface IBlockEditorDataHelper
{
// TODO: Does this abstraction need a reference to JObject? Maybe it does but ideally it doesn't
IEnumerable<IBlockReference> GetBlockReferences(JObject layout);
bool IsEditorSpecificPropertyKey(string propertyKey);
}

View File

@@ -1,9 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Umbraco.Core.Collections;
using Umbraco.Core.Composing;
using Umbraco.Core.Models;
using Umbraco.Core.PropertyEditors;
@@ -15,19 +13,73 @@ namespace Umbraco.Core.Services
{
private readonly PropertyEditorCollection _propertyEditors;
private readonly IDataTypeService _dataTypeService;
private readonly ILocalizedTextService _textService;
public PropertyValidationService(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService)
public PropertyValidationService(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, ILocalizedTextService textService)
{
_propertyEditors = propertyEditors;
_dataTypeService = dataTypeService;
_textService = textService;
}
//TODO: Remove this method in favor of the overload specifying all dependencies
public PropertyValidationService()
: this(Current.PropertyEditors, Current.Services.DataTypeService)
: this(Current.PropertyEditors, Current.Services.DataTypeService, Current.Services.TextService)
{
}
public IEnumerable<ValidationResult> ValidatePropertyValue(
PropertyType propertyType,
object postedValue)
{
if (propertyType is null) throw new ArgumentNullException(nameof(propertyType));
var dataType = _dataTypeService.GetDataType(propertyType.DataTypeId);
if (dataType == null) throw new InvalidOperationException("No data type found by id " + propertyType.DataTypeId);
var editor = _propertyEditors[propertyType.PropertyEditorAlias];
if (editor == null) throw new InvalidOperationException("No property editor found by alias " + propertyType.PropertyEditorAlias); ;
return ValidatePropertyValue(_textService, editor, dataType, postedValue, propertyType.Mandatory, propertyType.ValidationRegExp, propertyType.MandatoryMessage, propertyType.ValidationRegExpMessage);
}
internal static IEnumerable<ValidationResult> ValidatePropertyValue(
ILocalizedTextService textService,
IDataEditor editor,
IDataType dataType,
object postedValue,
bool isRequired,
string validationRegExp,
string isRequiredMessage,
string validationRegExpMessage)
{
// Retrieve default messages used for required and regex validatation. We'll replace these
// if set with custom ones if they've been provided for a given property.
var requiredDefaultMessages = new[]
{
textService.Localize("validation", "invalidNull"),
textService.Localize("validation", "invalidEmpty")
};
var formatDefaultMessages = new[]
{
textService.Localize("validation", "invalidPattern"),
};
var valueEditor = editor.GetValueEditor(dataType.Configuration);
foreach (var validationResult in valueEditor.Validate(postedValue, isRequired, validationRegExp))
{
// If we've got custom error messages, we'll replace the default ones that will have been applied in the call to Validate().
if (isRequired && !string.IsNullOrWhiteSpace(isRequiredMessage) && requiredDefaultMessages.Contains(validationResult.ErrorMessage, StringComparer.OrdinalIgnoreCase))
{
validationResult.ErrorMessage = isRequiredMessage;
}
if (!string.IsNullOrWhiteSpace(validationRegExp) && !string.IsNullOrWhiteSpace(validationRegExpMessage) && formatDefaultMessages.Contains(validationResult.ErrorMessage, StringComparer.OrdinalIgnoreCase))
{
validationResult.ErrorMessage = validationRegExpMessage;
}
yield return validationResult;
}
}
/// <summary>
/// Validates the content item's properties pass validation rules
/// </summary>

View File

@@ -442,7 +442,10 @@ namespace Umbraco.Tests.Models
[Test]
public void ContentPublishValuesWithMixedPropertyTypeVariations()
{
var propertyValidationService = new PropertyValidationService(Current.Factory.GetInstance<PropertyEditorCollection>(), Current.Factory.GetInstance<ServiceContext>().DataTypeService);
var propertyValidationService = new PropertyValidationService(
Current.Factory.GetInstance<PropertyEditorCollection>(),
Current.Factory.GetInstance<ServiceContext>().DataTypeService,
Current.Factory.GetInstance<ServiceContext>().TextService);
const string langFr = "fr-FR";
// content type varies by Culture
@@ -574,7 +577,10 @@ namespace Umbraco.Tests.Models
prop.SetValue("a");
Assert.AreEqual("a", prop.GetValue());
Assert.IsNull(prop.GetValue(published: true));
var propertyValidationService = new PropertyValidationService(Current.Factory.GetInstance<PropertyEditorCollection>(), Current.Factory.GetInstance<ServiceContext>().DataTypeService);
var propertyValidationService = new PropertyValidationService(
Current.Factory.GetInstance<PropertyEditorCollection>(),
Current.Factory.GetInstance<ServiceContext>().DataTypeService,
Current.Factory.GetInstance<ServiceContext>().TextService);
Assert.IsTrue(propertyValidationService.IsPropertyValid(prop));

View File

@@ -1197,7 +1197,7 @@ namespace Umbraco.Tests.Services
Assert.IsFalse(content.HasIdentity);
// content cannot publish values because they are invalid
var propertyValidationService = new PropertyValidationService(Factory.GetInstance<PropertyEditorCollection>(), ServiceContext.DataTypeService);
var propertyValidationService = new PropertyValidationService(Factory.GetInstance<PropertyEditorCollection>(), ServiceContext.DataTypeService, ServiceContext.TextService);
var isValid = propertyValidationService.IsPropertyDataValid(content, out var invalidProperties, CultureImpact.Invariant);
Assert.IsFalse(isValid);
Assert.IsNotEmpty(invalidProperties);

View File

@@ -36,7 +36,7 @@ namespace Umbraco.Tests.Services
var propEditors = new PropertyEditorCollection(new DataEditorCollection(new[] { dataEditor }));
validationService = new PropertyValidationService(propEditors, dataTypeService.Object);
validationService = new PropertyValidationService(propEditors, dataTypeService.Object, Mock.Of<ILocalizedTextService>());
}
[Test]

View File

@@ -41,12 +41,12 @@ namespace Umbraco.Tests.TestHelpers.Entities
};
var contentCollection = new PropertyTypeCollection(true);
contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "title", Name = "Title", Description = "", Mandatory = false, SortOrder = 1, DataTypeId = -88 });
contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "bodyText", Name = "Body Text", Description = "", Mandatory = false, SortOrder = 2, DataTypeId = -87 });
contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "title", Name = "Title", Description = "", Mandatory = false, SortOrder = 1, DataTypeId = Constants.DataTypes.Textbox });
contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "bodyText", Name = "Body Text", Description = "", Mandatory = false, SortOrder = 2, DataTypeId = Constants.DataTypes.RichtextEditor });
var metaCollection = new PropertyTypeCollection(true);
metaCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "keywords", Name = "Meta Keywords", Description = "", Mandatory = false, SortOrder = 1, DataTypeId = -88 });
metaCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "description", Name = "Meta Description", Description = "", Mandatory = false, SortOrder = 2, DataTypeId = -89 });
metaCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "keywords", Name = "Meta Keywords", Description = "", Mandatory = false, SortOrder = 1, DataTypeId = Constants.DataTypes.Textbox });
metaCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "description", Name = "Meta Description", Description = "", Mandatory = false, SortOrder = 2, DataTypeId = Constants.DataTypes.Textarea });
contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 });
contentType.PropertyGroups.Add(new PropertyGroup(metaCollection) { Name = "Meta", SortOrder = 2 });

View File

@@ -266,7 +266,7 @@
<Compile Include="Web\HttpCookieExtensionsTests.cs" />
<Compile Include="Templates\HtmlImageSourceParserTests.cs" />
<Compile Include="Web\Mvc\HtmlStringUtilitiesTests.cs" />
<Compile Include="Web\ModelStateExtensionsTests.cs" />
<Compile Include="Web\Validation\ModelStateExtensionsTests.cs" />
<Compile Include="Web\Mvc\RenderIndexActionSelectorAttributeTests.cs" />
<Compile Include="Persistence\NPocoTests\PetaPocoCachesTest.cs" />
<Compile Include="Persistence\Repositories\AuditRepositoryTest.cs" />
@@ -513,6 +513,7 @@
<Compile Include="CoreThings\VersionExtensionTests.cs" />
<Compile Include="Web\TemplateUtilitiesTests.cs" />
<Compile Include="Web\UrlHelperExtensionTests.cs" />
<Compile Include="Web\Validation\ContentModelValidatorTests.cs" />
<Compile Include="Web\WebExtensionMethodTests.cs" />
<Compile Include="CoreThings\XmlExtensionsTests.cs" />
<Compile Include="Misc\XmlHelperTests.cs" />

View File

@@ -0,0 +1,254 @@
using Moq;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Umbraco.Core.Services;
using Umbraco.Core.Logging;
using Umbraco.Web;
using Umbraco.Web.Editors.Filters;
using Umbraco.Tests.TestHelpers.Entities;
using Umbraco.Core.Models;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Editors.Binders;
using Umbraco.Core;
using Umbraco.Tests.Testing;
using Umbraco.Core.Mapping;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Composing;
using System.Web.Http.ModelBinding;
using Umbraco.Web.PropertyEditors;
using System.ComponentModel.DataAnnotations;
using Umbraco.Tests.TestHelpers;
using System.Globalization;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Umbraco.Tests.Web.Validation
{
[UmbracoTest(Mapper = true, WithApplication = true, Logger = UmbracoTestOptions.Logger.Console)]
[TestFixture]
public class ContentModelValidatorTests : UmbracoTestBase
{
private const int ComplexDataTypeId = 9999;
private const string ContentTypeAlias = "textPage";
private ContentType _contentType;
public override void SetUp()
{
base.SetUp();
_contentType = MockedContentTypes.CreateTextPageContentType(ContentTypeAlias);
// add complex editor
_contentType.AddPropertyType(
new PropertyType("complexTest", ValueStorageType.Ntext) { Alias = "complex", Name = "Meta Keywords", Description = "", Mandatory = false, SortOrder = 1, DataTypeId = ComplexDataTypeId },
"Content");
// make them all validate with a regex rule that will not pass
foreach (var prop in _contentType.PropertyTypes)
{
prop.ValidationRegExp = "^donotmatch$";
prop.ValidationRegExpMessage = "Does not match!";
}
}
protected override void Compose()
{
base.Compose();
var complexEditorConfig = new NestedContentConfiguration
{
ContentTypes = new[]
{
new NestedContentConfiguration.ContentType { Alias = "feature" }
}
};
var dataTypeService = new Mock<IDataTypeService>();
dataTypeService.Setup(x => x.GetDataType(It.IsAny<int>()))
.Returns((int id) => id == ComplexDataTypeId
? Mock.Of<IDataType>(x => x.Configuration == complexEditorConfig)
: Mock.Of<IDataType>());
var contentTypeService = new Mock<IContentTypeService>();
contentTypeService.Setup(x => x.GetAll(It.IsAny<int[]>()))
.Returns(() => new List<IContentType>
{
_contentType
});
var textService = new Mock<ILocalizedTextService>();
textService.Setup(x => x.Localize("validation/invalidPattern", It.IsAny<CultureInfo>(), It.IsAny<IDictionary<string, string>>())).Returns(() => "invalidPattern");
textService.Setup(x => x.Localize("validation/invalidNull", It.IsAny<CultureInfo>(), It.IsAny<IDictionary<string, string>>())).Returns("invalidNull");
textService.Setup(x => x.Localize("validation/invalidEmpty", It.IsAny<CultureInfo>(), It.IsAny<IDictionary<string, string>>())).Returns("invalidEmpty");
Composition.RegisterUnique(x => Mock.Of<IDataTypeService>(x => x.GetDataType(It.IsAny<int>()) == Mock.Of<IDataType>()));
Composition.RegisterUnique(x => dataTypeService.Object);
Composition.RegisterUnique(x => contentTypeService.Object);
Composition.RegisterUnique(x => textService.Object);
Composition.WithCollectionBuilder<DataEditorCollectionBuilder>()
.Add<TestEditor>()
.Add<ComplexTestEditor>();
}
[Test]
public void Test()
{
var validator = new ContentSaveModelValidator(
Factory.GetInstance<ILogger>(),
Mock.Of<IUmbracoContextAccessor>(),
Factory.GetInstance<ILocalizedTextService>());
var content = MockedContent.CreateTextpageContent(_contentType, "test", -1);
const string complexValue = @"[{
""key"": ""c8df5136-d606-41f0-9134-dea6ae0c2fd9"",
""name"": ""Hello world"",
""ncContentTypeAlias"": """ + ContentTypeAlias + @""",
""title"": ""Hello world""
}, {
""key"": ""f916104a-4082-48b2-a515-5c4bf2230f38"",
""name"": ""Hello worldsss ddf"",
""ncContentTypeAlias"": """ + ContentTypeAlias + @""",
""title"": ""Hello worldsss ddf""
}
]";
content.SetValue("complex", complexValue);
// map the persisted properties to a model representing properties to save
//var saveProperties = content.Properties.Select(x => Mapper.Map<ContentPropertyBasic>(x)).ToList();
var saveProperties = content.Properties.Select(x =>
{
return new ContentPropertyBasic
{
Alias = x.Alias,
Id = x.Id,
Value = x.GetValue()
};
}).ToList();
var saveVariants = new List<ContentVariantSave>
{
new ContentVariantSave
{
Culture = string.Empty,
Segment = string.Empty,
Name = content.Name,
Save = true,
Properties = saveProperties
}
};
var save = new ContentItemSave
{
Id = content.Id,
Action = ContentSaveAction.Save,
ContentTypeAlias = _contentType.Alias,
ParentId = -1,
PersistedContent = content,
TemplateAlias = null,
Variants = saveVariants
};
// This will map the ContentItemSave.Variants.PropertyCollectionDto and then map the values in the saved model
// back onto the persisted IContent model.
ContentItemBinder.BindModel(save, content);
var modelState = new ModelStateDictionary();
var isValid = validator.ValidatePropertiesData(save, saveVariants[0], saveVariants[0].PropertyCollectionDto, modelState);
// list results for debugging
foreach (var state in modelState)
{
Console.WriteLine(state.Key);
foreach (var error in state.Value.Errors)
{
Console.WriteLine("\t" + error.ErrorMessage);
}
}
// assert
Assert.IsFalse(isValid);
Assert.AreEqual(5, modelState.Keys.Count);
const string complexPropertyKey = "_Properties.complex.invariant.null";
Assert.IsTrue(modelState.Keys.Contains(complexPropertyKey));
foreach(var state in modelState.Where(x => x.Key != complexPropertyKey))
{
foreach (var error in state.Value.Errors)
{
Assert.IsTrue(error.ErrorMessage.DetectIsJson());
var json = JsonConvert.DeserializeObject<JObject>(error.ErrorMessage);
Assert.IsNotEmpty(json["errorMessage"].Value<string>());
Assert.AreEqual(1, json["memberNames"].Value<JArray>().Count);
}
}
var complexEditorErrors = modelState.Single(x => x.Key == complexPropertyKey).Value.Errors;
Assert.AreEqual(3, complexEditorErrors.Count);
var nestedError = complexEditorErrors.Single(x => x.ErrorMessage.Contains("nestedValidation"));
var jsonNestedError = JsonConvert.DeserializeObject<JObject>(nestedError.ErrorMessage);
Assert.AreEqual(JTokenType.Array, jsonNestedError["nestedValidation"].Type);
var nestedValidation = (JArray)jsonNestedError["nestedValidation"];
Assert.AreEqual(2, nestedValidation.Count); // there are 2 because there are 2 nested content rows
foreach(var rowErrors in nestedValidation)
{
var elementTypeErrors = (JArray)rowErrors; // this is an array of errors for the nested content row (element type)
Assert.AreEqual(2, elementTypeErrors.Count);
foreach(var elementTypeErr in elementTypeErrors)
{
Assert.IsNotEmpty(elementTypeErr["errorMessage"].Value<string>());
Assert.AreEqual(1, elementTypeErr["memberNames"].Value<JArray>().Count);
}
}
}
[HideFromTypeFinder]
[DataEditor("complexTest", "test", "test")]
public class ComplexTestEditor : NestedContentPropertyEditor
{
public ComplexTestEditor(ILogger logger, Lazy<PropertyEditorCollection> propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService) : base(logger, propertyEditors, dataTypeService, contentTypeService)
{
}
protected override IDataValueEditor CreateValueEditor()
{
var editor = base.CreateValueEditor();
editor.Validators.Add(new NeverValidateValidator());
return editor;
}
}
[HideFromTypeFinder]
[DataEditor("test", "test", "test")] // This alias aligns with the prop editor alias for all properties created from MockedContentTypes.CreateTextPageContentType
public class TestEditor : DataEditor
{
public TestEditor(ILogger logger)
: base(logger)
{
}
protected override IDataValueEditor CreateValueEditor() => new TestValueEditor(Attribute);
private class TestValueEditor : DataValueEditor
{
public TestValueEditor(DataEditorAttribute attribute)
: base(attribute)
{
Validators.Add(new NeverValidateValidator());
}
}
}
public class NeverValidateValidator : IValueValidator
{
public IEnumerable<ValidationResult> Validate(object value, string valueType, object dataTypeConfiguration)
{
yield return new ValidationResult("WRONG!", new[] { "innerFieldId" });
}
}
}
}

View File

@@ -6,7 +6,7 @@ using NUnit.Framework;
using Umbraco.Core.Services;
using Umbraco.Web;
namespace Umbraco.Tests.Web
namespace Umbraco.Tests.Web.Validation
{
[TestFixture]
public class ModelStateExtensionsTests

View File

@@ -11,8 +11,8 @@ namespace Umbraco.Web.Editors.Binders
{
}
public BlueprintItemBinder(ILogger logger, ServiceContext services, IUmbracoContextAccessor umbracoContextAccessor)
: base(logger, services, umbracoContextAccessor)
public BlueprintItemBinder(ServiceContext services)
: base(services)
{
}

View File

@@ -17,16 +17,13 @@ namespace Umbraco.Web.Editors.Binders
/// </summary>
internal class ContentItemBinder : IModelBinder
{
private readonly ContentModelBinderHelper _modelBinderHelper;
public ContentItemBinder() : this(Current.Logger, Current.Services, Current.UmbracoContextAccessor)
public ContentItemBinder() : this(Current.Services)
{
}
public ContentItemBinder(ILogger logger, ServiceContext services, IUmbracoContextAccessor umbracoContextAccessor)
public ContentItemBinder(ServiceContext services)
{
Services = services;
_modelBinderHelper = new ContentModelBinderHelper();
}
protected ServiceContext Services { get; }
@@ -39,10 +36,20 @@ namespace Umbraco.Web.Editors.Binders
/// <returns></returns>
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
var model = _modelBinderHelper.BindModelFromMultipartRequest<ContentItemSave>(actionContext, bindingContext);
var model = ContentModelBinderHelper.BindModelFromMultipartRequest<ContentItemSave>(actionContext, bindingContext);
if (model == null) return false;
model.PersistedContent = ContentControllerBase.IsCreatingAction(model.Action) ? CreateNew(model) : GetExisting(model);
BindModel(model, ContentControllerBase.IsCreatingAction(model.Action) ? CreateNew(model) : GetExisting(model));
return true;
}
internal static void BindModel(ContentItemSave model, IContent persistedContent)
{
if (model is null) throw new ArgumentNullException(nameof(model));
if (persistedContent is null) throw new ArgumentNullException(nameof(persistedContent));
model.PersistedContent = persistedContent;
//create the dto from the persisted model
if (model.PersistedContent != null)
@@ -60,11 +67,9 @@ namespace Umbraco.Web.Editors.Binders
});
//now map all of the saved values to the dto
_modelBinderHelper.MapPropertyValuesFromSaved(variant, variant.PropertyCollectionDto);
ContentModelBinderHelper.MapPropertyValuesFromSaved(variant, variant.PropertyCollectionDto);
}
}
return true;
}
protected virtual IContent GetExisting(ContentItemSave model)

View File

@@ -15,9 +15,9 @@ namespace Umbraco.Web.Editors.Binders
/// <summary>
/// Helper methods to bind media/member models
/// </summary>
internal class ContentModelBinderHelper
internal static class ContentModelBinderHelper
{
public TModelSave BindModelFromMultipartRequest<TModelSave>(HttpActionContext actionContext, ModelBindingContext bindingContext)
public static TModelSave BindModelFromMultipartRequest<TModelSave>(HttpActionContext actionContext, ModelBindingContext bindingContext)
where TModelSave : IHaveUploadedFiles
{
var result = actionContext.ReadAsMultipart(SystemDirectories.TempFileUploads);
@@ -86,7 +86,7 @@ namespace Umbraco.Web.Editors.Binders
/// </summary>
/// <param name="saveModel"></param>
/// <param name="dto"></param>
public void MapPropertyValuesFromSaved(IContentProperties<ContentPropertyBasic> saveModel, ContentPropertyCollectionDto dto)
public static void MapPropertyValuesFromSaved(IContentProperties<ContentPropertyBasic> saveModel, ContentPropertyCollectionDto dto)
{
//NOTE: Don't convert this to linq, this is much quicker
foreach (var p in saveModel.Properties)

View File

@@ -13,7 +13,6 @@ namespace Umbraco.Web.Editors.Binders
/// </summary>
internal class MediaItemBinder : IModelBinder
{
private readonly ContentModelBinderHelper _modelBinderHelper;
private readonly ServiceContext _services;
public MediaItemBinder() : this(Current.Services)
@@ -23,7 +22,6 @@ namespace Umbraco.Web.Editors.Binders
public MediaItemBinder(ServiceContext services)
{
_services = services;
_modelBinderHelper = new ContentModelBinderHelper();
}
/// <summary>
@@ -34,7 +32,7 @@ namespace Umbraco.Web.Editors.Binders
/// <returns></returns>
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
var model = _modelBinderHelper.BindModelFromMultipartRequest<MediaItemSave>(actionContext, bindingContext);
var model = ContentModelBinderHelper.BindModelFromMultipartRequest<MediaItemSave>(actionContext, bindingContext);
if (model == null) return false;
model.PersistedContent = ContentControllerBase.IsCreatingAction(model.Action) ? CreateNew(model) : GetExisting(model);
@@ -44,7 +42,7 @@ namespace Umbraco.Web.Editors.Binders
{
model.PropertyCollectionDto = Current.Mapper.Map<IMedia, ContentPropertyCollectionDto>(model.PersistedContent);
//now map all of the saved values to the dto
_modelBinderHelper.MapPropertyValuesFromSaved(model, model.PropertyCollectionDto);
ContentModelBinderHelper.MapPropertyValuesFromSaved(model, model.PropertyCollectionDto);
}
model.Name = model.Name.Trim();

View File

@@ -20,7 +20,6 @@ namespace Umbraco.Web.Editors.Binders
/// </summary>
internal class MemberBinder : IModelBinder
{
private readonly ContentModelBinderHelper _modelBinderHelper;
private readonly ServiceContext _services;
public MemberBinder() : this(Current.Services)
@@ -30,7 +29,6 @@ namespace Umbraco.Web.Editors.Binders
public MemberBinder(ServiceContext services)
{
_services = services;
_modelBinderHelper = new ContentModelBinderHelper();
}
/// <summary>
@@ -41,7 +39,7 @@ namespace Umbraco.Web.Editors.Binders
/// <returns></returns>
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
var model = _modelBinderHelper.BindModelFromMultipartRequest<MemberSave>(actionContext, bindingContext);
var model = ContentModelBinderHelper.BindModelFromMultipartRequest<MemberSave>(actionContext, bindingContext);
if (model == null) return false;
model.PersistedContent = ContentControllerBase.IsCreatingAction(model.Action) ? CreateNew(model) : GetExisting(model);
@@ -51,7 +49,7 @@ namespace Umbraco.Web.Editors.Binders
{
model.PropertyCollectionDto = Current.Mapper.Map<IMember, ContentPropertyCollectionDto>(model.PersistedContent);
//now map all of the saved values to the dto
_modelBinderHelper.MapPropertyValuesFromSaved(model, model.PropertyCollectionDto);
ContentModelBinderHelper.MapPropertyValuesFromSaved(model, model.PropertyCollectionDto);
}
model.Name = model.Name.Trim();

View File

@@ -1,15 +1,19 @@
using System;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.ModelBinding;
using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.PropertyEditors.Validation;
namespace Umbraco.Web.Editors.Filters
{
@@ -125,18 +129,6 @@ namespace Umbraco.Web.Editors.Filters
{
var properties = modelWithProperties.Properties.ToDictionary(x => x.Alias, x => x);
// Retrieve default messages used for required and regex validatation. We'll replace these
// if set with custom ones if they've been provided for a given property.
var requiredDefaultMessages = new[]
{
_textService.Localize("validation", "invalidNull"),
_textService.Localize("validation", "invalidEmpty")
};
var formatDefaultMessages = new[]
{
_textService.Localize("validation", "invalidPattern"),
};
foreach (var p in dto.Properties)
{
var editor = p.PropertyEditor;
@@ -156,7 +148,7 @@ namespace Umbraco.Web.Editors.Filters
var postedValue = postedProp.Value;
ValidatePropertyValue(model, modelWithProperties, editor, p, postedValue, modelState, requiredDefaultMessages, formatDefaultMessages);
ValidatePropertyValue(model, modelWithProperties, editor, p, postedValue, modelState);
}
@@ -180,26 +172,29 @@ namespace Umbraco.Web.Editors.Filters
IDataEditor editor,
ContentPropertyDto property,
object postedValue,
ModelStateDictionary modelState,
string[] requiredDefaultMessages,
string[] formatDefaultMessages)
ModelStateDictionary modelState)
{
var valueEditor = editor.GetValueEditor(property.DataType.Configuration);
foreach (var r in valueEditor.Validate(postedValue, property.IsRequired, property.ValidationRegExp))
if (property is null) throw new ArgumentNullException(nameof(property));
if (property.DataType is null) throw new InvalidOperationException($"{nameof(property)}.{nameof(property.DataType)} cannot be null");
foreach (var validationResult in PropertyValidationService.ValidatePropertyValue(
_textService, editor, property.DataType, postedValue, property.IsRequired,
property.ValidationRegExp, property.IsRequiredMessage, property.ValidationRegExpMessage))
{
// If we've got custom error messages, we'll replace the default ones that will have been applied in the call to Validate().
if (property.IsRequired && !string.IsNullOrWhiteSpace(property.IsRequiredMessage) && requiredDefaultMessages.Contains(r.ErrorMessage, StringComparer.OrdinalIgnoreCase))
{
r.ErrorMessage = property.IsRequiredMessage;
}
if (!string.IsNullOrWhiteSpace(property.ValidationRegExp) && !string.IsNullOrWhiteSpace(property.ValidationRegExpMessage) && formatDefaultMessages.Contains(r.ErrorMessage, StringComparer.OrdinalIgnoreCase))
{
r.ErrorMessage = property.ValidationRegExpMessage;
}
modelState.AddPropertyError(r, property.Alias, property.Culture, property.Segment);
AddPropertyError(model, modelWithProperties, editor, property, validationResult, modelState);
}
}
protected virtual void AddPropertyError(
TModelSave model,
TModelWithProperties modelWithProperties,
IDataEditor editor,
ContentPropertyDto property,
ValidationResult validationResult,
ModelStateDictionary modelState)
{
modelState.AddPropertyError(validationResult, property.Alias, property.Culture, property.Segment);
}
}
}

View File

@@ -1,5 +1,8 @@
using Umbraco.Core.Logging;
using System.ComponentModel.DataAnnotations;
using System.Web.Http.ModelBinding;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
using Umbraco.Web.Models.ContentEditing;
@@ -13,5 +16,30 @@ namespace Umbraco.Web.Editors.Filters
public ContentSaveModelValidator(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, ILocalizedTextService textService) : base(logger, umbracoContextAccessor, textService)
{
}
protected override void AddPropertyError(ContentItemSave model, ContentVariantSave modelWithProperties, IDataEditor editor, ContentPropertyDto property, ValidationResult validationResult, ModelStateDictionary modelState)
{
// Original idea: See if we can build up the JSON + JSON Path
// SD: I'm just keeping these notes here for later just to remind myself that we might want to take into account the tab number in validation
// which we might be able to get in the PropertyValidationService anyways?
// Create a JSON + JSON Path key, see https://gist.github.com/Shazwazza/ad9fcbdb0fdacff1179a9eed88393aa6
//var json = new PropertyError
//{
// Culture = property.Culture,
// Segment = property.Segment
//};
// TODO: Hrm, we can't get the tab index without a reference to the content type itself! the IContent doesn't contain a reference to groups/indexes
// BUT! I think it contains a reference to the group alias so we could use JSON Path for a group alias instead of index like:
// .tabs[?(@.alias=='Content')]
//var tabIndex = ??
//var jsonPath = "$.variants[0].tabs[0].properties[?(@.alias=='title')].value[0]";
base.AddPropertyError(model, modelWithProperties, editor, property, validationResult, modelState);
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
@@ -6,6 +7,7 @@ using System.Web.Mvc;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Core.Services;
using Umbraco.Web.PropertyEditors.Validation;
namespace Umbraco.Web
{
@@ -51,13 +53,26 @@ namespace Umbraco.Web
internal static void AddPropertyError(this System.Web.Http.ModelBinding.ModelStateDictionary modelState,
ValidationResult result, string propertyAlias, string culture = "", string segment = "")
{
if (culture == null)
culture = "";
modelState.AddValidationError(result, "_Properties", propertyAlias,
var propValidationResult = new PropertyValidationResult(result);
var keyParts = new[]
{
"_Properties",
propertyAlias,
//if the culture is null, we'll add the term 'invariant' as part of the key
culture.IsNullOrWhiteSpace() ? "invariant" : culture,
// if the segment is null, we'll add the term 'null' as part of the key
segment.IsNullOrWhiteSpace() ? "null" : segment);
segment.IsNullOrWhiteSpace() ? "null" : segment
};
var key = string.Join(".", keyParts);
modelState.AddModelError(
key,
JsonConvert.SerializeObject(
propValidationResult,
new ValidationResultConverter()));
}
/// <summary>
@@ -236,5 +251,6 @@ namespace Umbraco.Web
};
}
}
}

View File

@@ -14,7 +14,7 @@ namespace Umbraco.Web.Models.ContentEditing
[DataContract(Name = "content", Namespace = "")]
public class ContentItemSave : IContentSave<IContent>
{
protected ContentItemSave()
public ContentItemSave()
{
UploadedFiles = new List<ContentPropertyFile>();
Variants = new List<ContentVariantSave>();

View File

@@ -44,6 +44,9 @@ namespace Umbraco.Web.Models.Mapping
property.PropertyType.PropertyEditorAlias);
editor = _propertyEditors[Constants.PropertyEditors.Aliases.Label];
if (editor == null)
throw new InvalidOperationException($"Could not resolve the property editor {Constants.PropertyEditors.Aliases.Label}");
}
dest.Id = property.Id;

View File

@@ -86,75 +86,24 @@ namespace Umbraco.Web.PropertyEditors
}
}
internal class BlockEditorValidator : IValueValidator
internal class BlockEditorValidator : ComplexEditorValidator
{
private readonly PropertyEditorCollection _propertyEditors;
private readonly IDataTypeService _dataTypeService;
private readonly BlockEditorValues _blockEditorValues;
public BlockEditorValidator(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, BlockEditorValues blockEditorValues)
public BlockEditorValidator(BlockEditorValues blockEditorValues, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, ILocalizedTextService textService) : base(propertyEditors, dataTypeService, textService)
{
_propertyEditors = propertyEditors;
_dataTypeService = dataTypeService;
_blockEditorValues = blockEditorValues;
}
public IEnumerable<ValidationResult> Validate(object rawValue, string valueType, object dataTypeConfiguration)
protected override IEnumerable<ElementTypeValidationModel> GetElementsFromValue(object value)
{
var validationResults = new List<ValidationResult>();
foreach (var row in _blockEditorValues.GetPropertyValues(rawValue, out _))
foreach (var row in _blockEditorValues.GetPropertyValues(value, out _))
{
if (row.PropType == null) continue;
var config = _dataTypeService.GetDataType(row.PropType.DataTypeId).Configuration;
var propertyEditor = _propertyEditors[row.PropType.PropertyEditorAlias];
if (propertyEditor == null) continue;
foreach (var validator in propertyEditor.GetValueEditor().Validators)
{
foreach (var result in validator.Validate(row.JsonRowValue[row.PropKey], propertyEditor.GetValueEditor().ValueType, config))
{
result.ErrorMessage = "Item " + (row.RowIndex + 1) + " '" + row.PropType.Name + "' " + result.ErrorMessage;
validationResults.Add(result);
}
}
// Check mandatory
if (row.PropType.Mandatory)
{
if (row.JsonRowValue[row.PropKey] == null)
{
var message = string.IsNullOrWhiteSpace(row.PropType.MandatoryMessage)
? $"'{row.PropType.Name}' cannot be null"
: row.PropType.MandatoryMessage;
validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey }));
}
else if (row.JsonRowValue[row.PropKey].ToString().IsNullOrWhiteSpace() || (row.JsonRowValue[row.PropKey].Type == JTokenType.Array && !row.JsonRowValue[row.PropKey].HasValues))
{
var message = string.IsNullOrWhiteSpace(row.PropType.MandatoryMessage)
? $"'{row.PropType.Name}' cannot be empty"
: row.PropType.MandatoryMessage;
validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey }));
}
}
// Check regex
if (!row.PropType.ValidationRegExp.IsNullOrWhiteSpace()
&& row.JsonRowValue[row.PropKey] != null && !row.JsonRowValue[row.PropKey].ToString().IsNullOrWhiteSpace())
{
var regex = new Regex(row.PropType.ValidationRegExp);
if (!regex.IsMatch(row.JsonRowValue[row.PropKey].ToString()))
{
var message = string.IsNullOrWhiteSpace(row.PropType.ValidationRegExpMessage)
? $"'{row.PropType.Name}' is invalid, it does not match the correct pattern"
: row.PropType.ValidationRegExpMessage;
validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey }));
}
}
var val = row.JsonRowValue[row.PropKey];
yield return new ElementTypeValidationModel(val, row.PropType);
}
return validationResults;
}
}

View File

@@ -34,6 +34,7 @@ namespace Umbraco.Web.PropertyEditors
#region IBlockEditorDataHelper
// TODO: Rename this we don't want to use the name "Helper"
private class DataHelper : IBlockEditorDataHelper
{
public IEnumerable<IBlockReference> GetBlockReferences(JObject layout)

View File

@@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
using Umbraco.Web.PropertyEditors.Validation;
namespace Umbraco.Web.PropertyEditors
{
/// <summary>
/// Used to validate complex editors that contain nested editors
/// </summary>
public abstract class ComplexEditorValidator : IValueValidator
{
private readonly PropertyValidationService _propertyValidationService;
public ComplexEditorValidator(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, ILocalizedTextService textService)
{
_propertyValidationService = new PropertyValidationService(propertyEditors, dataTypeService, textService);
}
public IEnumerable<ValidationResult> Validate(object value, string valueType, object dataTypeConfiguration)
{
var elements = GetElementsFromValue(value);
var rowResults = GetNestedValidationResults(elements).ToList();
return rowResults.Count > 0
? new NestedValidationResults(rowResults).Yield()
: Enumerable.Empty<ValidationResult>();
}
protected abstract IEnumerable<ElementTypeValidationModel> GetElementsFromValue(object value);
/// <summary>
/// Return a nested validation result per row
/// </summary>
/// <param name="rawValue"></param>
/// <returns></returns>
protected IEnumerable<NestedValidationResults> GetNestedValidationResults(IEnumerable<ElementTypeValidationModel> elements)
{
foreach (var row in elements)
{
var nestedValidation = new List<ValidationResult>();
foreach(var validationResult in _propertyValidationService.ValidatePropertyValue(row.PropertyType, row.PostedValue))
{
nestedValidation.Add(validationResult);
}
if (nestedValidation.Count > 0)
{
yield return new NestedValidationResults(nestedValidation);
}
}
}
public class ElementTypeValidationModel
{
public ElementTypeValidationModel(object postedValue, PropertyType propertyType)
{
PostedValue = postedValue ?? throw new ArgumentNullException(nameof(postedValue));
PropertyType = propertyType ?? throw new ArgumentNullException(nameof(propertyType));
}
public object PostedValue { get; }
public PropertyType PropertyType { get; }
}
}
}

View File

@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Core;
@@ -13,9 +14,11 @@ using Umbraco.Core.Models;
using Umbraco.Core.Models.Editors;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
using Umbraco.Web.PropertyEditors.Validation;
namespace Umbraco.Web.PropertyEditors
{
/// <summary>
/// Represents a nested content property editor.
/// </summary>
@@ -31,14 +34,20 @@ namespace Umbraco.Web.PropertyEditors
private readonly Lazy<PropertyEditorCollection> _propertyEditors;
private readonly IDataTypeService _dataTypeService;
private readonly IContentTypeService _contentTypeService;
private readonly ILocalizedTextService _localizedTextService;
internal const string ContentTypeAliasPropertyKey = "ncContentTypeAlias";
[Obsolete("Use the constructor specifying all parameters instead")]
public NestedContentPropertyEditor(ILogger logger, Lazy<PropertyEditorCollection> propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService)
: this(logger, propertyEditors, dataTypeService, contentTypeService, Current.Services.TextService) { }
public NestedContentPropertyEditor(ILogger logger, Lazy<PropertyEditorCollection> propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService, ILocalizedTextService localizedTextService)
: base (logger)
{
_propertyEditors = propertyEditors;
_dataTypeService = dataTypeService;
_contentTypeService = contentTypeService;
_localizedTextService = localizedTextService;
}
// has to be lazy else circular dep in ctor
@@ -52,7 +61,7 @@ namespace Umbraco.Web.PropertyEditors
#region Value Editor
protected override IDataValueEditor CreateValueEditor() => new NestedContentPropertyValueEditor(Attribute, PropertyEditors, _dataTypeService, _contentTypeService);
protected override IDataValueEditor CreateValueEditor() => new NestedContentPropertyValueEditor(Attribute, PropertyEditors, _dataTypeService, _contentTypeService, _localizedTextService);
internal class NestedContentPropertyValueEditor : DataValueEditor, IDataValueReference
{
@@ -60,13 +69,13 @@ namespace Umbraco.Web.PropertyEditors
private readonly IDataTypeService _dataTypeService;
private readonly NestedContentValues _nestedContentValues;
public NestedContentPropertyValueEditor(DataEditorAttribute attribute, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService)
public NestedContentPropertyValueEditor(DataEditorAttribute attribute, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService, ILocalizedTextService textService)
: base(attribute)
{
_propertyEditors = propertyEditors;
_dataTypeService = dataTypeService;
_nestedContentValues = new NestedContentValues(contentTypeService);
Validators.Add(new NestedContentValidator(propertyEditors, dataTypeService, _nestedContentValues));
Validators.Add(new NestedContentValidator(_nestedContentValues, propertyEditors, dataTypeService, textService));
}
/// <inheritdoc />
@@ -255,75 +264,25 @@ namespace Umbraco.Web.PropertyEditors
}
}
internal class NestedContentValidator : IValueValidator
internal class NestedContentValidator : ComplexEditorValidator
{
private readonly PropertyEditorCollection _propertyEditors;
private readonly IDataTypeService _dataTypeService;
private readonly NestedContentValues _nestedContentValues;
public NestedContentValidator(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, NestedContentValues nestedContentValues)
public NestedContentValidator(NestedContentValues nestedContentValues, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, ILocalizedTextService textService)
: base(propertyEditors, dataTypeService, textService)
{
_propertyEditors = propertyEditors;
_dataTypeService = dataTypeService;
_nestedContentValues = nestedContentValues;
}
public IEnumerable<ValidationResult> Validate(object rawValue, string valueType, object dataTypeConfiguration)
protected override IEnumerable<ElementTypeValidationModel> GetElementsFromValue(object value)
{
var validationResults = new List<ValidationResult>();
foreach(var row in _nestedContentValues.GetPropertyValues(rawValue, out _))
foreach (var row in _nestedContentValues.GetPropertyValues(value, out _))
{
if (row.PropType == null) continue;
var config = _dataTypeService.GetDataType(row.PropType.DataTypeId).Configuration;
var propertyEditor = _propertyEditors[row.PropType.PropertyEditorAlias];
if (propertyEditor == null) continue;
foreach (var validator in propertyEditor.GetValueEditor().Validators)
{
foreach (var result in validator.Validate(row.JsonRowValue[row.PropKey], propertyEditor.GetValueEditor().ValueType, config))
{
result.ErrorMessage = "Item " + (row.RowIndex + 1) + " '" + row.PropType.Name + "' " + result.ErrorMessage;
validationResults.Add(result);
}
}
// Check mandatory
if (row.PropType.Mandatory)
{
if (row.JsonRowValue[row.PropKey] == null)
{
var message = string.IsNullOrWhiteSpace(row.PropType.MandatoryMessage)
? $"'{row.PropType.Name}' cannot be null"
: row.PropType.MandatoryMessage;
validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey }));
}
else if (row.JsonRowValue[row.PropKey].ToString().IsNullOrWhiteSpace() || (row.JsonRowValue[row.PropKey].Type == JTokenType.Array && !row.JsonRowValue[row.PropKey].HasValues))
{
var message = string.IsNullOrWhiteSpace(row.PropType.MandatoryMessage)
? $"'{row.PropType.Name}' cannot be empty"
: row.PropType.MandatoryMessage;
validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey }));
}
}
// Check regex
if (!row.PropType.ValidationRegExp.IsNullOrWhiteSpace()
&& row.JsonRowValue[row.PropKey] != null && !row.JsonRowValue[row.PropKey].ToString().IsNullOrWhiteSpace())
{
var regex = new Regex(row.PropType.ValidationRegExp);
if (!regex.IsMatch(row.JsonRowValue[row.PropKey].ToString()))
{
var message = string.IsNullOrWhiteSpace(row.PropType.ValidationRegExpMessage)
? $"'{row.PropType.Name}' is invalid, it does not match the correct pattern"
: row.PropType.ValidationRegExpMessage;
validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey }));
}
}
var val = row.JsonRowValue[row.PropKey];
yield return new ElementTypeValidationModel(val, row.PropType);
}
return validationResults;
}
}

View File

@@ -0,0 +1,22 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Umbraco.Web.PropertyEditors.Validation
{
/// <summary>
/// Custom <see cref="ValidationResult"/> that contains a list of nested validation results
/// </summary>
/// <remarks>
/// For example, each <see cref="NestedValidationResults"/> represents validation results for a row in Nested Content
/// </remarks>
public class NestedValidationResults : ValidationResult
{
public NestedValidationResults(IEnumerable<ValidationResult> nested)
: base(string.Empty)
{
ValidationResults = new List<ValidationResult>(nested);
}
public IList<ValidationResult> ValidationResults { get; }
}
}

View File

@@ -0,0 +1,27 @@
using System.ComponentModel.DataAnnotations;
namespace Umbraco.Web.PropertyEditors.Validation
{
/// <summary>
/// Custom <see cref="ValidationResult"/> for content properties
/// </summary>
/// <remarks>
/// This clones the original result and then ensures the nested result if it's the correct type
/// </remarks>
public class PropertyValidationResult : ValidationResult
{
public PropertyValidationResult(ValidationResult nested)
: base(nested.ErrorMessage, nested.MemberNames)
{
NestedResuls = nested as NestedValidationResults;
}
/// <summary>
/// Nested validation results for the content property
/// </summary>
/// <remarks>
/// There can be nested results for complex editors that contain other editors
/// </remarks>
public NestedValidationResults NestedResuls { get; }
}
}

View File

@@ -0,0 +1,71 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Umbraco.Core;
namespace Umbraco.Web.PropertyEditors.Validation
{
/// <summary>
/// Custom json converter for <see cref="ValidationResult"/> and <see cref="PropertyValidationResult"/>
/// </summary>
internal class ValidationResultConverter : JsonConverter
{
public override bool CanConvert(Type objectType) => typeof(ValidationResult).IsAssignableFrom(objectType);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var camelCaseSerializer = new JsonSerializer
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
DefaultValueHandling = DefaultValueHandling.Ignore
};
foreach (var c in serializer.Converters)
camelCaseSerializer.Converters.Add(c);
var validationResult = (ValidationResult)value;
if (validationResult is NestedValidationResults nestedResult)
{
if (nestedResult.ValidationResults.Count > 0)
{
var validationItems = JToken.FromObject(nestedResult.ValidationResults, camelCaseSerializer);
validationItems.WriteTo(writer);
}
}
else
{
var jo = new JObject();
if (!validationResult.ErrorMessage.IsNullOrWhiteSpace())
{
jo.Add("errorMessage", JToken.FromObject(validationResult.ErrorMessage, camelCaseSerializer));
}
if (validationResult.MemberNames.Any())
{
jo.Add("memberNames", JToken.FromObject(validationResult.MemberNames, camelCaseSerializer));
}
if (validationResult is PropertyValidationResult propertyValidationResult)
{
if (propertyValidationResult.NestedResuls?.ValidationResults.Count > 0)
{
jo.Add("nestedValidation", JToken.FromObject(propertyValidationResult.NestedResuls, camelCaseSerializer));
}
}
jo.WriteTo(writer);
}
}
}
}

View File

@@ -245,8 +245,12 @@
<Compile Include="PropertyEditors\BlockListConfigurationEditor.cs" />
<Compile Include="PropertyEditors\BlockListPropertyEditor.cs" />
<Compile Include="Compose\NestedContentPropertyComposer.cs" />
<Compile Include="PropertyEditors\ComplexEditorValidator.cs" />
<Compile Include="PropertyEditors\ParameterEditors\MultipleMediaPickerParameterEditor.cs" />
<Compile Include="PropertyEditors\RichTextEditorPastedImages.cs" />
<Compile Include="PropertyEditors\Validation\NestedValidationResults.cs" />
<Compile Include="PropertyEditors\Validation\PropertyValidationResult.cs" />
<Compile Include="PropertyEditors\Validation\ValidationResultConverter.cs" />
<Compile Include="PropertyEditors\ValueConverters\BlockEditorConverter.cs" />
<Compile Include="PropertyEditors\ValueConverters\BlockListPropertyValueConverter.cs" />
<Compile Include="PublishedCache\NuCache\PublishedSnapshotServiceOptions.cs" />