diff --git a/src/Umbraco.Core/Dynamics/DynamicInstanceHelper.cs b/src/Umbraco.Core/Dynamics/DynamicInstanceHelper.cs index 95ed389a7a..94394abb3e 100644 --- a/src/Umbraco.Core/Dynamics/DynamicInstanceHelper.cs +++ b/src/Umbraco.Core/Dynamics/DynamicInstanceHelper.cs @@ -5,6 +5,7 @@ using System.Dynamic; using System.Linq; using System.Reflection; using System.Text; +using Umbraco.Core.Cache; using Umbraco.Core.Logging; namespace Umbraco.Core.Dynamics @@ -34,38 +35,42 @@ namespace Umbraco.Core.Dynamics FoundExtensionMethod } - /// - /// Attempts to invoke a member based on the dynamic instance - /// - /// - /// The object instance to invoke the extension method for - /// - /// - /// - /// - /// First tries to find a property with the binder name, if that fails it will try to find a static or instance method - /// on the object that matches the binder name - /// - public static Attempt TryInvokeMember(T thisObject, InvokeMemberBinder binder, object[] args) + /// + /// Attempts to invoke a member based on the dynamic instance + /// + /// + /// + /// The object instance to invoke the extension method for + /// + /// + /// + /// + /// First tries to find a property with the binder name, if that fails it will try to find a static or instance method + /// on the object that matches the binder name + /// + public static Attempt TryInvokeMember(IRuntimeCacheProvider runtimeCache, T thisObject, InvokeMemberBinder binder, object[] args) { - return TryInvokeMember(thisObject, binder, args, null); + return TryInvokeMember(runtimeCache, thisObject, binder, args, null); } - /// - /// Attempts to invoke a member based on the dynamic instance - /// - /// - /// The object instance to invoke the extension method for - /// - /// - /// The types to scan for extension methods - /// - /// - /// First tries to find a property with the binder name, if that fails it will try to find a static or instance method - /// on the object that matches the binder name, if that fails it will then attempt to invoke an extension method - /// based on the binder name and the extension method types to scan. - /// - public static Attempt TryInvokeMember(T thisObject, + /// + /// Attempts to invoke a member based on the dynamic instance + /// + /// + /// + /// The object instance to invoke the extension method for + /// + /// + /// The types to scan for extension methods + /// + /// + /// First tries to find a property with the binder name, if that fails it will try to find a static or instance method + /// on the object that matches the binder name, if that fails it will then attempt to invoke an extension method + /// based on the binder name and the extension method types to scan. + /// + public static Attempt TryInvokeMember( + IRuntimeCacheProvider runtimeCache, + T thisObject, InvokeMemberBinder binder, object[] args, IEnumerable findExtensionMethodsOnTypes) @@ -76,9 +81,9 @@ namespace Umbraco.Core.Dynamics { //Property? result = typeof(T).InvokeMember(binder.Name, - System.Reflection.BindingFlags.Instance | - System.Reflection.BindingFlags.Public | - System.Reflection.BindingFlags.GetProperty, + BindingFlags.Instance | + BindingFlags.Public | + BindingFlags.GetProperty, null, thisObject, args); @@ -90,10 +95,10 @@ namespace Umbraco.Core.Dynamics { //Static or Instance Method? result = typeof(T).InvokeMember(binder.Name, - System.Reflection.BindingFlags.Instance | - System.Reflection.BindingFlags.Public | - System.Reflection.BindingFlags.Static | - System.Reflection.BindingFlags.InvokeMethod, + BindingFlags.Instance | + BindingFlags.Public | + BindingFlags.Static | + BindingFlags.InvokeMethod, null, thisObject, args); @@ -105,7 +110,7 @@ namespace Umbraco.Core.Dynamics { try { - result = FindAndExecuteExtensionMethod(thisObject, args, binder.Name, findExtensionMethodsOnTypes); + result = FindAndExecuteExtensionMethod(runtimeCache, thisObject, args, binder.Name, findExtensionMethodsOnTypes); return Attempt.Succeed(new TryInvokeMemberResult(result, TryInvokeMemberSuccessReason.FoundExtensionMethod)); } catch (TargetInvocationException ext) @@ -138,16 +143,19 @@ namespace Umbraco.Core.Dynamics } } - /// - /// Attempts to find an extension method that matches the name and arguments based on scanning the Type's passed in - /// to the findMethodsOnTypes parameter - /// - /// The instance object to execute the extension method for - /// - /// - /// - /// - internal static object FindAndExecuteExtensionMethod(T thisObject, + /// + /// Attempts to find an extension method that matches the name and arguments based on scanning the Type's passed in + /// to the findMethodsOnTypes parameter + /// + /// + /// The instance object to execute the extension method for + /// + /// + /// + /// + internal static object FindAndExecuteExtensionMethod( + IRuntimeCacheProvider runtimeCache, + T thisObject, object[] args, string name, IEnumerable findMethodsOnTypes) @@ -158,7 +166,7 @@ namespace Umbraco.Core.Dynamics MethodInfo toExecute = null; foreach (var t in findMethodsOnTypes) { - toExecute = ExtensionMethodFinder.FindExtensionMethod(t, args, name, false); + toExecute = ExtensionMethodFinder.FindExtensionMethod(runtimeCache, t, args, name, false); if (toExecute != null) break; } diff --git a/src/Umbraco.Core/Dynamics/DynamicXml.cs b/src/Umbraco.Core/Dynamics/DynamicXml.cs index 7a4d714fbe..5fd97fc778 100644 --- a/src/Umbraco.Core/Dynamics/DynamicXml.cs +++ b/src/Umbraco.Core/Dynamics/DynamicXml.cs @@ -10,6 +10,7 @@ using System.Collections; using System.IO; using System.Web; using Umbraco.Core; +using Umbraco.Core.Cache; namespace Umbraco.Core.Dynamics { @@ -162,8 +163,10 @@ namespace Umbraco.Core.Dynamics return true; //anyway } + var runtimeCache = ApplicationContext.Current != null ? ApplicationContext.Current.ApplicationCache.RuntimeCache : new NullCacheProvider(); + //ok, now lets try to match by member, property, extensino method - var attempt = DynamicInstanceHelper.TryInvokeMember(this, binder, args, new[] + var attempt = DynamicInstanceHelper.TryInvokeMember(runtimeCache, this, binder, args, new[] { typeof (IEnumerable), typeof (IEnumerable), diff --git a/src/Umbraco.Core/Dynamics/ExtensionMethodFinder.cs b/src/Umbraco.Core/Dynamics/ExtensionMethodFinder.cs index cfc8a92ddf..fbfa933e83 100644 --- a/src/Umbraco.Core/Dynamics/ExtensionMethodFinder.cs +++ b/src/Umbraco.Core/Dynamics/ExtensionMethodFinder.cs @@ -1,11 +1,11 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Web.Compilation; using System.Runtime.CompilerServices; -using System.Collections; using System.Linq.Expressions; +using Umbraco.Core.Cache; namespace Umbraco.Core.Dynamics { @@ -14,148 +14,89 @@ namespace Umbraco.Core.Dynamics /// internal static class ExtensionMethodFinder { - private static readonly MethodInfo[] AllExtensionMethods; + /// + /// The static cache for extension methods found that match the criteria that we are looking for + /// + private static readonly ConcurrentDictionary, MethodInfo[]> MethodCache = new ConcurrentDictionary, MethodInfo[]>(); - static ExtensionMethodFinder() - { - AllExtensionMethods = TypeFinder.GetAssembliesWithKnownExclusions() + /// + /// Returns the enumerable of all extension method info's in the app domain = USE SPARINGLY!!! + /// + /// + /// + /// We cache this as a sliding 5 minute exiration, in unit tests there's over 1100 methods found, surely that will eat up a bit of memory so we want + /// to make sure we give it back. + /// + private static IEnumerable GetAllExtensionMethodsInAppDomain(IRuntimeCacheProvider runtimeCacheProvider) + { + if (runtimeCacheProvider == null) throw new ArgumentNullException("runtimeCacheProvider"); + + return runtimeCacheProvider.GetCacheItem(typeof (ExtensionMethodFinder).Name, () => TypeFinder.GetAssembliesWithKnownExclusions() // assemblies that contain extension methods - .Where(a => a.IsDefined(typeof(ExtensionAttribute), false)) + .Where(a => a.IsDefined(typeof (ExtensionAttribute), false)) // types that contain extension methods .SelectMany(a => a.GetTypes() - .Where(t => t.IsDefined(typeof(ExtensionAttribute), false) && t.IsSealed && t.IsGenericType == false && t.IsNested == false)) + .Where(t => t.IsDefined(typeof (ExtensionAttribute), false) && t.IsSealed && t.IsGenericType == false && t.IsNested == false)) // actual extension methods .SelectMany(t => t.GetMethods(BindingFlags.Static | BindingFlags.Public) - .Where(m => m.IsDefined(typeof(ExtensionAttribute), false))) + .Where(m => m.IsDefined(typeof (ExtensionAttribute), false))) // and also IEnumerable extension methods - because the assembly is excluded - .Concat(typeof(Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public)) - .ToArray(); - } + .Concat(typeof (Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public)) + //If we don't do this then we'll be scanning all assemblies each time! + .ToArray(), + + //only cache for 5 minutes + timeout: TimeSpan.FromMinutes(5), - // ORIGINAL CODE IS NOT COMPLETE, DOES NOT HANDLE GENERICS, ETC... - - // so this is an attempt at fixing things, but it's not done yet - // and do we really want to do this? extension methods are not supported on dynamics, period - // we should use strongly typed content instead of dynamics. - - /* - - // get all extension methods for type thisType, with name name, - // accepting argsCount arguments (not counting the instance of thisType). - private static IEnumerable GetExtensionMethods(Type thisType, string name, int argsCount) - { - var key = string.Format("{0}.{1}::{2}", thisType.FullName, name, argsCount); - - var types = thisType.GetBaseTypes(true); // either do this OR have MatchFirstParameter handle the stuff... F*XME - - var methods = AllExtensionMethods - .Where(m => m.Name == name) - .Where(m => m.GetParameters().Length == argsCount) - .Where(m => MatchFirstParameter(thisType, m.GetParameters()[0].ParameterType)); - - // f*xme - is this what we should cache? - return methods; + //each time this is accessed it will be for 5 minutes longer + isSliding:true); } - // find out whether the first parameter is a match for thisType - private static bool MatchFirstParameter(Type thisType, Type firstParameterType) - { - return MethodArgZeroHasCorrectTargetType(null, firstParameterType, thisType); - } + /// + /// Returns all extension methods found matching the definition + /// + /// + /// The runtime cache is used to temporarily cache all extension methods found in the app domain so that + /// while we search for individual extension methods, the process will be reasonably 'quick'. We then statically + /// cache the MethodInfo's that we are looking for and then the runtime cache will expire and give back all that memory. + /// + /// + /// + /// + /// The arguments EXCLUDING the 'this' argument in an extension method + /// + /// + /// + /// NOTE: This will be an intensive method to call! Results will be cached based on the key (args) of this method + /// + internal static IEnumerable GetAllExtensionMethods(IRuntimeCacheProvider runtimeCache, Type thisType, string name, int argumentCount) + { + var key = new Tuple(thisType, name, argumentCount); - // get the single extension method for type thisType, with name name, - // that accepts the arguments in args (which does not contain the instance of thisType). - public static MethodInfo GetExtensionMethod(Type thisType, string name, object[] args) - { - MethodInfo result = null; - foreach (var method in GetExtensionMethods(thisType, name, args.Length).Where(m => MatchParameters(m, args))) - { - if (result == null) - result = method; - else - throw new AmbiguousMatchException("More than one matching extension method was found."); - } - return result; - } + return MethodCache.GetOrAdd(key, tuple => + { + var candidates = GetAllExtensionMethodsInAppDomain(runtimeCache); - // find out whether the method can accept the arguments - private static bool MatchParameters(MethodInfo method, IList args) - { - var parameters = method.GetParameters(); - var i = 0; - for (; i < parameters.Length; ++i) - { - if (MatchParameter(parameters[i].ParameterType, args[i].GetType()) == false) - break; - } - return (i == parameters.Length); - } + //filter by name + var methodsByName = candidates.Where(m => m.Name == name); - internal static bool MatchParameter(Type parameterType, Type argumentType) - { - // public static int DoSomething(Foo foo, T t1, T t2) - // DoSomething(foo, t1, t2) => how can we match?! - return parameterType == argumentType; // f*xme of course! - } - * - */ + //ensure we add + 1 to the arg count because the 'this' arg is not included in the count above! + var isGenericAndRightParamCount = methodsByName.Where(m => m.GetParameters().Length == argumentCount + 1); - // BELOW IS THE ORIGINAL CODE... + //find the right overload that can take genericParameterType + //which will be either DynamicNodeList or List which is IEnumerable` - /// - /// Returns all extension methods found matching the definition - /// - /// - /// - /// - /// - /// - /// - /// TODO: NOTE: This will be an intensive method to call!! Results should be cached! - /// - private static IEnumerable GetAllExtensionMethods(Type thisType, string name, int argumentCount, bool argsContainsThis) - { - // at *least* we can cache the extension methods discovery - var candidates = AllExtensionMethods; + var withGenericParameterType = isGenericAndRightParamCount.Select(m => new { m, t = FirstParameterType(m) }); - /* - //only scan assemblies we know to contain extension methods (user assemblies) - var assembliesToScan = TypeFinder.GetAssembliesWithKnownExclusions(); + var methodsWhereArgZeroIsTargetType = (from method in withGenericParameterType + where + method.t != null && MethodArgZeroHasCorrectTargetType(method.m, method.t, thisType) + select method); - //get extension methods from runtime - var candidates = ( - from assembly in assembliesToScan - where assembly.IsDefined(typeof(ExtensionAttribute), false) - from type in assembly.GetTypes() - where (type.IsDefined(typeof(ExtensionAttribute), false) - && type.IsSealed && !type.IsGenericType && !type.IsNested) - from method in type.GetMethods(BindingFlags.Static | BindingFlags.Public) - // this filters extension methods - where method.IsDefined(typeof(ExtensionAttribute), false) - select method - ); - - //add the extension methods defined in IEnumerable - candidates = candidates.Concat(typeof(Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public)); - */ - - //filter by name - var methodsByName = candidates.Where(m => m.Name == name); - - var isGenericAndRightParamCount = methodsByName.Where(m => m.GetParameters().Length == argumentCount + (argsContainsThis ? 0 : 1)); - - //find the right overload that can take genericParameterType - //which will be either DynamicNodeList or List which is IEnumerable` - - var withGenericParameterType = isGenericAndRightParamCount.Select(m => new { m, t = FirstParameterType(m) }); - - var methodsWhereArgZeroIsTargetType = (from method in withGenericParameterType - where - method.t != null && MethodArgZeroHasCorrectTargetType(method.m, method.t, thisType) - select method); - - return methodsWhereArgZeroIsTargetType.Select(mt => mt.m); + return methodsWhereArgZeroIsTargetType.Select(mt => mt.m).ToArray(); + }); + } private static bool MethodArgZeroHasCorrectTargetType(MethodInfo method, Type firstArgumentType, Type thisType) @@ -182,36 +123,36 @@ namespace Umbraco.Core.Dynamics private static bool MethodArgZeroHasCorrectTargetTypeShareACommonInterface(MethodInfo method, Type firstArgumentType, Type thisType) { - Type[] interfaces = firstArgumentType.GetInterfaces(); + var interfaces = firstArgumentType.GetInterfaces(); if (interfaces.Length == 0) { return false; } - bool result = interfaces.All(i => thisType.GetInterfaces().Contains(i)); + var result = interfaces.All(i => thisType.GetInterfaces().Contains(i)); return result; } private static bool MethodArgZeroHasCorrectTargetTypeIsASubclassOf(MethodInfo method, Type firstArgumentType, Type thisType) { - bool result = thisType.IsSubclassOf(firstArgumentType); + var result = thisType.IsSubclassOf(firstArgumentType); return result; } private static bool MethodArgZeroHasCorrectTargetTypeAnInterfaceMatches(MethodInfo method, Type firstArgumentType, Type thisType) { - bool result = thisType.GetInterfaces().Contains(firstArgumentType); + var result = thisType.GetInterfaces().Contains(firstArgumentType); return result; } private static bool MethodArgZeroHasCorrectTargetTypeTypeMatchesExactly(MethodInfo method, Type firstArgumentType, Type thisType) { - bool result = (thisType == firstArgumentType); + var result = (thisType == firstArgumentType); return result; } private static Type FirstParameterType(MethodInfo m) { - ParameterInfo[] p = m.GetParameters(); + var p = m.GetParameters(); if (p.Any()) { return p.First().ParameterType; @@ -219,74 +160,62 @@ namespace Umbraco.Core.Dynamics return null; } - private static MethodInfo DetermineMethodFromParams(IEnumerable methods, Type genericType, IEnumerable args) + private static MethodInfo DetermineMethodFromParams(IEnumerable methods, Type genericType, IEnumerable args) { - if (!methods.Any()) - { - return null; - } MethodInfo methodToExecute = null; - if (methods.Count() > 1) - { - //Given the args, lets get the types and compare the type sequence to try and find the correct overload - var argTypes = args.ToList().ConvertAll(o => - { - var oe = (o as Expression); - if (oe != null) - { - return oe.Type.FullName; - } - return o.GetType().FullName; - }); - var methodsWithArgTypes = methods.Select(method => new { method, types = method.GetParameters().Select(pi => pi.ParameterType.FullName) }); - var firstMatchingOverload = methodsWithArgTypes.FirstOrDefault(m => m.types.SequenceEqual(argTypes)); - if (firstMatchingOverload != null) - { - methodToExecute = firstMatchingOverload.method; - } - } - if (methodToExecute == null) - { - var firstMethod = methods.FirstOrDefault(); - // NH: this is to ensure that it's always the correct one being chosen when using the LINQ extension methods - if (methods.Count() > 1) - { - var firstGenericMethod = methods.FirstOrDefault(x => x.IsGenericMethodDefinition); - if (firstGenericMethod != null) - { - firstMethod = firstGenericMethod; - } - } + //Given the args, lets get the types and compare the type sequence to try and find the correct overload + var argTypes = args.Select(o => + { + var oe = (o as Expression); + return oe != null ? oe.Type : o.GetType(); + }); + + var methodsWithArgTypes = methods.Select(method => new + { + method, + //skip the first arg because that is the extension method type ('this') that we are looking for + types = method.GetParameters().Select(pi => pi.ParameterType).Skip(1) + }); + + //This type comparer will check + var typeComparer = new DelegateEqualityComparer( + //Checks if the argument type passed in can be assigned from the parameter type in the method. For + // example, if the argument type is HtmlHelper but the method parameter type is HtmlHelper then + // it will match because the argument is assignable to that parameter type and will be able to execute + TypeHelper.IsTypeAssignableFrom, + //This will not ever execute but if it does we need to get the hash code of the string because the hash + // code of a type is random + type => type.FullName.GetHashCode()); + + var firstMatchingOverload = methodsWithArgTypes + .FirstOrDefault(m => m.types.SequenceEqual(argTypes, typeComparer)); + + if (firstMatchingOverload != null) + { + methodToExecute = firstMatchingOverload.method; + } - if (firstMethod != null) - { - if (firstMethod.IsGenericMethodDefinition) - { - if (genericType != null) - { - methodToExecute = firstMethod.MakeGenericMethod(genericType); - } - } - else - { - methodToExecute = firstMethod; - } - } - } return methodToExecute; } - public static MethodInfo FindExtensionMethod(Type thisType, object[] args, string name, bool argsContainsThis) + public static MethodInfo FindExtensionMethod(IRuntimeCacheProvider runtimeCache, Type thisType, object[] args, string name, bool argsContainsThis) { Type genericType = null; if (thisType.IsGenericType) { genericType = thisType.GetGenericArguments()[0]; - } + } - var methods = GetAllExtensionMethods(thisType, name, args.Length, argsContainsThis).ToArray(); - return DetermineMethodFromParams(methods, genericType, args); + args = args + //if the args contains 'this', remove the first one since that is 'this' and we don't want to use + //that in the method searching + .Skip(argsContainsThis ? 1 : 0) + .ToArray(); + + var methods = GetAllExtensionMethods(runtimeCache, thisType, name, args.Length).ToArray(); + + return DetermineMethodFromParams(methods, genericType, args); } } } diff --git a/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs b/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs index 3af35db44c..265948a073 100644 --- a/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs +++ b/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs @@ -15,6 +15,95 @@ namespace Umbraco.Core.Persistence { + /// + /// This will handle the issue of inserting data into a table when there can be a violation of a primary key or unique constraint which + /// can occur when two threads are trying to insert data at the exact same time when the data violates this constraint. + /// + /// + /// + /// + /// Returns the action that executed, either an insert or an update + /// + /// NOTE: If an insert occurred and a PK value got generated, the poco object passed in will contain the updated value. + /// + /// + /// In different databases, there are a few raw SQL options like MySql's ON DUPLICATE KEY UPDATE or MSSQL's MERGE WHEN MATCHED, but since we are + /// also supporting SQLCE for which this doesn't exist we cannot simply rely on the underlying database to help us here. So we'll actually need to + /// try to be as proficient as possible when we know this can occur and manually handle the issue. + /// + /// We do this by first trying to Update the record, this will return the number of rows affected. If it is zero then we insert, if it is one, then + /// we know the update was successful and the row was already inserted by another thread. If the rowcount is zero and we insert and get an exception, + /// that's due to a race condition, in which case we need to retry and update. + /// + internal static RecordPersistenceType InsertOrUpdate(this Database db, T poco) + where T : class + { + return db.InsertOrUpdate(poco, null, null); + } + + /// + /// This will handle the issue of inserting data into a table when there can be a violation of a primary key or unique constraint which + /// can occur when two threads are trying to insert data at the exact same time when the data violates this constraint. + /// + /// + /// + /// + /// If the entity has a composite key they you need to specify the update command explicitly + /// + /// Returns the action that executed, either an insert or an update + /// + /// NOTE: If an insert occurred and a PK value got generated, the poco object passed in will contain the updated value. + /// + /// + /// In different databases, there are a few raw SQL options like MySql's ON DUPLICATE KEY UPDATE or MSSQL's MERGE WHEN MATCHED, but since we are + /// also supporting SQLCE for which this doesn't exist we cannot simply rely on the underlying database to help us here. So we'll actually need to + /// try to be as proficient as possible when we know this can occur and manually handle the issue. + /// + /// We do this by first trying to Update the record, this will return the number of rows affected. If it is zero then we insert, if it is one, then + /// we know the update was successful and the row was already inserted by another thread. If the rowcount is zero and we insert and get an exception, + /// that's due to a race condition, in which case we need to retry and update. + /// + internal static RecordPersistenceType InsertOrUpdate(this Database db, + T poco, + string updateCommand, + object updateArgs) + where T : class + { + if (poco == null) throw new ArgumentNullException("poco"); + + var rowCount = updateCommand.IsNullOrWhiteSpace() + ? db.Update(poco) + : db.Update(updateCommand, updateArgs); + + if (rowCount > 0) return RecordPersistenceType.Update; + + try + { + db.Insert(poco); + return RecordPersistenceType.Insert; + } + //TODO: Need to find out if this is the same exception that will occur for all databases... pretty sure it will be + catch (SqlException ex) + { + //This will occur if the constraint was violated and this record was already inserted by another thread, + //at this exact same time, in this case we need to do an update + + rowCount = updateCommand.IsNullOrWhiteSpace() + ? db.Update(poco) + : db.Update(updateCommand, updateArgs); + + if (rowCount == 0) + { + //this would be strange! in this case the only circumstance would be that at the exact same time, 3 threads executed, one + // did the insert and the other somehow managed to do a delete precisely before this update was executed... now that would + // be real crazy. In that case we need to throw an exception. + throw new DataException("Record could not be inserted or updated"); + } + + return RecordPersistenceType.Update; + } + } + /// /// This will handle the issue of inserting data into a table when there can be a violation of a primary key or unique constraint which /// can occur when two threads are trying to insert data at the exact same time when the data violates this constraint. diff --git a/src/Umbraco.Tests/DynamicsAndReflection/ExtensionMethodFinderTests.cs b/src/Umbraco.Tests/DynamicsAndReflection/ExtensionMethodFinderTests.cs index 56fc36a123..37b0a63866 100644 --- a/src/Umbraco.Tests/DynamicsAndReflection/ExtensionMethodFinderTests.cs +++ b/src/Umbraco.Tests/DynamicsAndReflection/ExtensionMethodFinderTests.cs @@ -3,8 +3,10 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; +using System.Web.Mvc; using NUnit.Framework; using Umbraco.Core; +using Umbraco.Core.Cache; using Umbraco.Core.Dynamics; namespace Umbraco.Tests.DynamicsAndReflection @@ -27,8 +29,8 @@ namespace Umbraco.Tests.DynamicsAndReflection // // Eric Lippert, http://stackoverflow.com/questions/5311465/extension-method-and-dynamic-object-in-c-sharp + [Ignore("This is just testing the below GetMethodForArguments method - Stephen was working on this but it's not used in the core")] [Test] - [Ignore("fails")] public void TypesTests() { Assert.IsTrue(typeof(int[]).Inherits()); @@ -84,7 +86,9 @@ namespace Umbraco.Tests.DynamicsAndReflection Assert.IsNotNull(m5A); // note - should we also handle "ref" and "out" parameters? + // SD: NO, lets not make this more complicated than it already is // note - should we pay attention to array types? + // SD: NO, lets not make this more complicated than it already is } public void TestMethod1(int value) {} @@ -157,42 +161,122 @@ namespace Umbraco.Tests.DynamicsAndReflection : method.MakeGenericMethod(genericArgumentTypes); } - public class Class1 + public class TestClass {} + public class TestClass : TestClass { } + [Test] - [Ignore("fails")] - public void FinderTests() + public void Find_Non_Overloaded_Method() { MethodInfo method; - var class1 = new Class1(); + var class1 = new TestClass(); - method = ExtensionMethodFinder.FindExtensionMethod(typeof (Class1), new object[] {1}, "TestMethod1", false); + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(TestClass), new object[] { 1 }, "SimpleMethod", false); Assert.IsNotNull(method); method.Invoke(null, new object[] { class1, 1 }); - method = ExtensionMethodFinder.FindExtensionMethod(typeof(Class1), new object[] { "x" }, "TestMethod1", false); - Assert.IsNull(method); // note - fails, return TestMethod1! + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(TestClass), new object[] { "x" }, "SimpleMethod", false); + Assert.IsNull(method); + } - method = ExtensionMethodFinder.FindExtensionMethod(typeof(Class1), new object[] { 1 }, "TestMethod2", false); + [Test] + public void Find_Overloaded_Method() + { + MethodInfo method; + var class1 = new TestClass(); + + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(TestClass), new object[] { 1 }, "SimpleOverloadMethod", false); Assert.IsNotNull(method); - method.Invoke(null, new object[] { class1, "1" }); + method.Invoke(null, new object[] { class1, 1 }); - method = ExtensionMethodFinder.FindExtensionMethod(typeof(Class1), new object[] { "x" }, "TestMethod2", false); + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(TestClass), new object[] { "x" }, "SimpleOverloadMethod", false); Assert.IsNotNull(method); method.Invoke(null, new object[] { class1, "x" }); } + + [Test] + public void Find_Overloaded_Method_With_Args_Containing_This() + { + MethodInfo method; + var class1 = new TestClass(); + + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(TestClass), new object[] { class1, 1 }, "SimpleOverloadMethod", true); + Assert.IsNotNull(method); + method.Invoke(null, new object[] { class1, 1 }); + + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(TestClass), new object[] { class1, "x" }, "SimpleOverloadMethod", true); + Assert.IsNotNull(method); + method.Invoke(null, new object[] { class1, "x" }); + } + + [Test] + public void Find_Non_Overloaded_Generic_Enumerable_Method() + { + MethodInfo method; + var class1 = Enumerable.Empty(); + + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(IEnumerable), new object[] { 1 }, "SimpleEnumerableGenericMethod", false); + Assert.IsNotNull(method); + method.Invoke(null, new object[] { class1, 1 }); + + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(IEnumerable), new object[] { "x" }, "SimpleEnumerableGenericMethod", false); + Assert.IsNull(method); + } + + [Test] + public void Find_Overloaded_Generic_Enumerable_Method() + { + MethodInfo method; + var class1 = Enumerable.Empty(); + + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(IEnumerable), new object[] { 1 }, "SimpleOverloadEnumerableGenericMethod", false); + Assert.IsNotNull(method); + method.Invoke(null, new object[] { class1, 1 }); + + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(IEnumerable), new object[] { "x" }, "SimpleOverloadEnumerableGenericMethod", false); + Assert.IsNotNull(method); + method.Invoke(null, new object[] { class1, "x" }); + } + + [Test] + public void Find_Method_With_Parameter_Match_With_Generic_Argument() + { + MethodInfo method; + + var genericTestClass = new TestClass(); + var nonGenericTestClass = new TestClass(); + + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(TestClass), new object[] { genericTestClass }, "GenericParameterMethod", false); + Assert.IsNotNull(method); + + method = ExtensionMethodFinder.FindExtensionMethod(new NullCacheProvider(), typeof(TestClass), new object[] { nonGenericTestClass }, "GenericParameterMethod", false); + Assert.IsNotNull(method); + } } static class ExtensionMethodFinderTestsExtensions { - public static void TestMethod1(this ExtensionMethodFinderTests.Class1 source, int value) + public static void SimpleMethod(this ExtensionMethodFinderTests.TestClass source, int value) { } - public static void TestMethod2(this ExtensionMethodFinderTests.Class1 source, int value) + public static void SimpleOverloadMethod(this ExtensionMethodFinderTests.TestClass source, int value) { } - public static void TestMethod2(this ExtensionMethodFinderTests.Class1 source, string value) + public static void SimpleOverloadMethod(this ExtensionMethodFinderTests.TestClass source, string value) { } + + public static void SimpleEnumerableGenericMethod(this IEnumerable source, int value) + { } + + public static void SimpleOverloadEnumerableGenericMethod(this IEnumerable source, int value) + { } + + public static void SimpleOverloadEnumerableGenericMethod(this IEnumerable source, string value) + { } + + public static void GenericParameterMethod(this ExtensionMethodFinderTests.TestClass source, ExtensionMethodFinderTests.TestClass value) + { } + } } diff --git a/src/Umbraco.Web.UI.Client/src/less/gridview.less b/src/Umbraco.Web.UI.Client/src/less/gridview.less index b832f4dd18..cf4b07ee25 100644 --- a/src/Umbraco.Web.UI.Client/src/less/gridview.less +++ b/src/Umbraco.Web.UI.Client/src/less/gridview.less @@ -151,11 +151,15 @@ IFRAME {overflow:hidden;} bottom: 0; top: 0; right: 0; - width: 150px; + width: 50px; opacity: 0.3; z-index: 50; } +.usky-grid .cell-tools.with-prompt { + width:200px; +} + .usky-grid .cell-tools:hover{ opacity: 1; } @@ -343,8 +347,20 @@ IFRAME {overflow:hidden;} border-radius: 200px; background: rgba(255,255,255, 1); border:1px solid rgb(182, 182, 182); - margin: 2px; + margin: 2px; } + + .usky-grid .iconBox span.prompt { + display:block; + min-width: 150px; + text-align:center; + + } + + .usky-grid .iconBox span.prompt > a { + text-decoration:underline; + } + .usky-grid .iconBox:hover, .usky-grid .iconBox:hover *{ background: @blue !important; color: white !important; diff --git a/src/Umbraco.Web.UI.Client/src/less/panel.less b/src/Umbraco.Web.UI.Client/src/less/panel.less index 7fc2d061ea..d9766e6f67 100644 --- a/src/Umbraco.Web.UI.Client/src/less/panel.less +++ b/src/Umbraco.Web.UI.Client/src/less/panel.less @@ -160,7 +160,8 @@ border-top: 1px solid @grayLighter; padding: 10px 0 10px 0; - + + margin-bottom: 17px; position: fixed; bottom: 0px; left: 100px; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/mediapicker.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/mediapicker.html index 7500c0ae42..a044e91e2d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/mediapicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/mediapicker.html @@ -78,6 +78,7 @@ data-file-upload="options" data-file-upload-progress="" data-ng-class="{'fileupl + Upload diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js index c74c7c90b9..fee0df5745 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js @@ -32,13 +32,14 @@ function ContentEditController($scope, $rootScope, $routeParams, $q, $timeout, $ editorState.set($scope.content); - //We fetch all ancestors of the node to generate the footer breadcrump navigation + //We fetch all ancestors of the node to generate the footer breadcrumb navigation if (!$routeParams.create) { - entityResource.getAncestors(content.id, "document") - .then(function (anc) { - anc.pop(); - $scope.ancestors = anc; - }); + if (content.parentId && content.parentId != -1) { + entityResource.getAncestors(content.id, "document") + .then(function (anc) { + $scope.ancestors = anc.reverse(); + }); + } } } diff --git a/src/Umbraco.Web.UI.Client/src/views/content/edit.html b/src/Umbraco.Web.UI.Client/src/views/content/edit.html index 29c646a092..b6ad9cb934 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/edit.html @@ -4,42 +4,40 @@ ng-submit="save()" val-form-manager> - +
- +
-
+
- + + current-node="currentNode" + current-section="{{currentSection}}"> -
-
+
+
-