From 2721c5cb12d38267d68b9eb6d04d15212fff9650 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 5 Feb 2016 13:51:43 +0100 Subject: [PATCH] adds dynamic caseinsensitive support for the image cropper model --- .../Dynamics/CaseInsensitiveDynamicObject.cs | 93 +++++++++++++++++++ src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../PropertyEditors/ImageCropperTest.cs | 86 ++++++++++++++++- .../Models/ImageCropCoordinates.cs | 3 +- src/Umbraco.Web/Models/ImageCropData.cs | 3 +- src/Umbraco.Web/Models/ImageCropDataSet.cs | 11 ++- src/Umbraco.Web/Models/ImageCropFocalPoint.cs | 3 +- 7 files changed, 193 insertions(+), 7 deletions(-) create mode 100644 src/Umbraco.Core/Dynamics/CaseInsensitiveDynamicObject.cs diff --git a/src/Umbraco.Core/Dynamics/CaseInsensitiveDynamicObject.cs b/src/Umbraco.Core/Dynamics/CaseInsensitiveDynamicObject.cs new file mode 100644 index 0000000000..43d7adc647 --- /dev/null +++ b/src/Umbraco.Core/Dynamics/CaseInsensitiveDynamicObject.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.Linq; +using System.Reflection; + +namespace Umbraco.Core.Dynamics +{ + /// + /// This will check enable dynamic access to properties and methods in a case insensitive manner + /// + /// + /// + /// This works by using reflection on the type - the reflection lookup is lazy so it will not execute unless a dynamic method needs to be accessed + /// + public abstract class CaseInsensitiveDynamicObject : DynamicObject + where T: class + { + /// + /// Used for dynamic access for case insensitive property access + /// ` + private static readonly Lazy>> CaseInsensitivePropertyAccess = new Lazy>>(() => + { + var props = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public) + .DistinctBy(x => x.Name); + return props.Select(propInfo => + { + var name = propInfo.Name.ToLowerInvariant(); + Func getVal = propInfo.GetValue; + return new KeyValuePair>(name, getVal); + + }).ToDictionary(x => x.Key, x => x.Value); + }); + + /// + /// Used for dynamic access for case insensitive property access + /// + private static readonly Lazy>>> CaseInsensitiveMethodAccess + = new Lazy>>>(() => + { + var props = typeof(T).GetMethods(BindingFlags.Instance | BindingFlags.Public) + .Where(x => x.IsSpecialName == false && x.IsVirtual == false) + .DistinctBy(x => x.Name); + return props.Select(methodInfo => + { + var name = methodInfo.Name.ToLowerInvariant(); + Func getVal = methodInfo.Invoke; + var val = new Tuple>(methodInfo.GetParameters(), getVal); + return new KeyValuePair>>(name, val); + + }).ToDictionary(x => x.Key, x => x.Value); + }); + + public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) + { + var name = binder.Name.ToLowerInvariant(); + if (CaseInsensitiveMethodAccess.Value.ContainsKey(name) == false) + return base.TryInvokeMember(binder, args, out result); + + var val = CaseInsensitiveMethodAccess.Value[name]; + var parameters = val.Item1; + var callback = val.Item2; + var fullArgs = new List(args); + if (args.Length <= parameters.Length) + { + //need to fill them up if they're optional + for (var i = args.Length; i < parameters.Length; i++) + { + if (parameters[i].IsOptional) + { + fullArgs.Add(parameters[i].DefaultValue); + } + } + if (fullArgs.Count == parameters.Length) + { + result = callback((T)(object)this, fullArgs.ToArray()); + return true; + } + } + return base.TryInvokeMember(binder, args, out result); + } + + public override bool TryGetMember(GetMemberBinder binder, out object result) + { + var name = binder.Name.ToLowerInvariant(); + if (CaseInsensitivePropertyAccess.Value.ContainsKey(name) == false) + return base.TryGetMember(binder, out result); + + result = CaseInsensitivePropertyAccess.Value[name]((T)(object)this); + return true; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index ba14fa044a..ef45bb08ab 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -309,6 +309,7 @@ + diff --git a/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs b/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs index 41ba665b4a..8a12b8a651 100644 --- a/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs +++ b/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Linq; using Moq; using Newtonsoft.Json; @@ -24,7 +25,86 @@ namespace Umbraco.Tests.PropertyEditors private const string cropperJson2 = "{\"focalPoint\": {\"left\": 0.98,\"top\": 0.80827067669172936},\"src\": \"/media/1005/img_0672.jpg\",\"crops\": [{\"alias\":\"thumb\",\"width\": 100,\"height\": 100,\"coordinates\": {\"x1\": 0.58729977382575338,\"y1\": 0.055768992440203169,\"x2\": 0,\"y2\": 0.32457553600198386}}]}"; private const string cropperJson3 = "{\"focalPoint\": {\"left\": 0.98,\"top\": 0.80827067669172936},\"src\": \"/media/1005/img_0672.jpg\",\"crops\": []}"; private const string mediaPath = "/media/1005/img_0671.jpg"; - + + [Test] + public void ImageCropData_Properties_As_Dynamic() + { + var sourceObj = cropperJson1.SerializeToCropDataSet(); + dynamic d = sourceObj; + + var index = 0; + foreach (var crop in d.crops) + { + var realObjCrop = sourceObj.Crops.ElementAt(index); + Assert.AreEqual(realObjCrop.Alias, crop.Alias); + Assert.AreEqual(realObjCrop.Alias, crop.alias); + + Assert.AreEqual(realObjCrop.Height, crop.Height); + Assert.AreEqual(realObjCrop.Height, crop.height); + + Assert.AreEqual(realObjCrop.Coordinates.X1, crop.Coordinates.X1); + Assert.AreEqual(realObjCrop.Coordinates.X1, crop.coordinates.x1); + + Assert.AreEqual(realObjCrop.Coordinates.X2, crop.Coordinates.X2); + Assert.AreEqual(realObjCrop.Coordinates.X2, crop.coordinates.x2); + + Assert.AreEqual(realObjCrop.Coordinates.Y1, crop.Coordinates.Y1); + Assert.AreEqual(realObjCrop.Coordinates.Y1, crop.coordinates.y1); + + Assert.AreEqual(realObjCrop.Coordinates.Y2, crop.Coordinates.Y2); + Assert.AreEqual(realObjCrop.Coordinates.Y2, crop.coordinates.y2); + index++; + } + + Assert.AreEqual(index, 1); + } + + [Test] + public void ImageCropFocalPoint_Properties_As_Dynamic() + { + var sourceObj = cropperJson1.SerializeToCropDataSet(); + dynamic d = sourceObj; + + Assert.AreEqual(sourceObj.FocalPoint.Left, d.FocalPoint.Left); + Assert.AreEqual(sourceObj.FocalPoint.Left, d.focalPoint.left); + + Assert.AreEqual(sourceObj.FocalPoint.Top, d.FocalPoint.Top); + Assert.AreEqual(sourceObj.FocalPoint.Top, d.focalPoint.top); + } + + [Test] + public void ImageCropDataSet_Properties_As_Dynamic() + { + var sourceObj = cropperJson1.SerializeToCropDataSet(); + dynamic d = sourceObj; + + Assert.AreEqual(sourceObj.Src, d.Src); + Assert.AreEqual(sourceObj.Src, d.src); + + Assert.AreEqual(sourceObj.FocalPoint, d.FocalPoint); + Assert.AreEqual(sourceObj.FocalPoint, d.focalPoint); + + Assert.AreEqual(sourceObj.Crops, d.Crops); + Assert.AreEqual(sourceObj.Crops, d.crops); + } + + [Test] + public void ImageCropDataSet_Methods_As_Dynamic() + { + var sourceObj = cropperJson1.SerializeToCropDataSet(); + dynamic d = sourceObj; + + Assert.AreEqual(sourceObj.HasCrop("thumb"), d.HasCrop("thumb")); + Assert.AreEqual(sourceObj.HasCrop("thumb"), d.hasCrop("thumb")); + + Assert.AreEqual(sourceObj.GetCropUrl("thumb"), d.GetCropUrl("thumb")); + Assert.AreEqual(sourceObj.GetCropUrl("thumb"), d.getCropUrl("thumb")); + + Assert.AreEqual(sourceObj.HasFocalPoint(), d.HasFocalPoint()); + Assert.AreEqual(sourceObj.HasFocalPoint(), d.hasFocalPoint()); + } + + [Test] public void CanConvertImageCropperDataSetSrcToString() { //cropperJson3 - has not crops @@ -34,6 +114,7 @@ namespace Umbraco.Tests.PropertyEditors Assert.AreEqual(destObj.Result, "/media/1005/img_0672.jpg"); } + [Test] public void CanConvertImageCropperDataSetJObject() { //cropperJson3 - has not crops @@ -43,13 +124,14 @@ namespace Umbraco.Tests.PropertyEditors Assert.AreEqual(sourceObj, destObj.Result.ToObject()); } + [Test] public void CanConvertImageCropperDataSetJsonToString() { var sourceObj = cropperJson1.SerializeToCropDataSet(); var destObj = sourceObj.TryConvertTo(); Assert.IsTrue(destObj.Success); Assert.IsTrue(destObj.Result.DetectIsJson()); - var obj = JsonConvert.DeserializeObject(cropperJson1); + var obj = JsonConvert.DeserializeObject(cropperJson1, new JsonSerializerSettings {Culture = CultureInfo.InvariantCulture, FloatParseHandling = FloatParseHandling.Decimal}); Assert.AreEqual(sourceObj, obj); } diff --git a/src/Umbraco.Web/Models/ImageCropCoordinates.cs b/src/Umbraco.Web/Models/ImageCropCoordinates.cs index 69a2daee95..4a480b4beb 100644 --- a/src/Umbraco.Web/Models/ImageCropCoordinates.cs +++ b/src/Umbraco.Web/Models/ImageCropCoordinates.cs @@ -1,10 +1,11 @@ using System; using System.Runtime.Serialization; +using Umbraco.Core.Dynamics; namespace Umbraco.Web.Models { [DataContract(Name = "imageCropCoordinates")] - public class ImageCropCoordinates : IEquatable + public class ImageCropCoordinates : CaseInsensitiveDynamicObject, IEquatable { [DataMember(Name = "x1")] public decimal X1 { get; set; } diff --git a/src/Umbraco.Web/Models/ImageCropData.cs b/src/Umbraco.Web/Models/ImageCropData.cs index 5992910c5b..fb1b3c7424 100644 --- a/src/Umbraco.Web/Models/ImageCropData.cs +++ b/src/Umbraco.Web/Models/ImageCropData.cs @@ -1,11 +1,12 @@ using System; using System.Runtime.Serialization; using System.Threading.Tasks; +using Umbraco.Core.Dynamics; namespace Umbraco.Web.Models { [DataContract(Name = "imageCropData")] - public class ImageCropData : IEquatable + public class ImageCropData : CaseInsensitiveDynamicObject, IEquatable { [DataMember(Name = "alias")] public string Alias { get; set; } diff --git a/src/Umbraco.Web/Models/ImageCropDataSet.cs b/src/Umbraco.Web/Models/ImageCropDataSet.cs index 174d52d06f..4c5a68a6b9 100644 --- a/src/Umbraco.Web/Models/ImageCropDataSet.cs +++ b/src/Umbraco.Web/Models/ImageCropDataSet.cs @@ -1,11 +1,16 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Dynamic; using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Text; using System.Web; using Newtonsoft.Json; +using Umbraco.Core; +using Umbraco.Core.Dynamics; using Umbraco.Core.Serialization; using Umbraco.Web.PropertyEditors.ValueConverters; @@ -14,8 +19,9 @@ namespace Umbraco.Web.Models [JsonConverter(typeof(NoTypeConverterJsonConverter))] [TypeConverter(typeof(ImageCropDataSetConverter))] [DataContract(Name="imageCropDataSet")] - public class ImageCropDataSet : IHtmlString, IEquatable - { + public class ImageCropDataSet : CaseInsensitiveDynamicObject, IHtmlString, IEquatable + { + [DataMember(Name="src")] public string Src { get; set;} @@ -88,6 +94,7 @@ namespace Umbraco.Web.Models { return Crops.Any() ? JsonConvert.SerializeObject(this) : Src; } + /// /// Indicates whether the current object is equal to another object of the same type. diff --git a/src/Umbraco.Web/Models/ImageCropFocalPoint.cs b/src/Umbraco.Web/Models/ImageCropFocalPoint.cs index d9a0984167..e47bab5b7a 100644 --- a/src/Umbraco.Web/Models/ImageCropFocalPoint.cs +++ b/src/Umbraco.Web/Models/ImageCropFocalPoint.cs @@ -1,10 +1,11 @@ using System; using System.Runtime.Serialization; +using Umbraco.Core.Dynamics; namespace Umbraco.Web.Models { [DataContract(Name = "imageCropFocalPoint")] - public class ImageCropFocalPoint : IEquatable + public class ImageCropFocalPoint : CaseInsensitiveDynamicObject, IEquatable { [DataMember(Name = "left")] public decimal Left { get; set; }