diff options
author | bradwilson <bradwils@microsoft.com> | 2012-03-11 21:17:56 +0400 |
---|---|---|
committer | bradwilson <bradwils@microsoft.com> | 2012-03-11 21:17:56 +0400 |
commit | 0f8c45fe03e71446fd8287115a1774b549a72314 (patch) | |
tree | e26a39eb9ce385aa6993677f44a27446d089c2ff /src/System.Web.Mvc |
Initial revision.
Diffstat (limited to 'src/System.Web.Mvc')
350 files changed, 28601 insertions, 0 deletions
diff --git a/src/System.Web.Mvc/AcceptVerbsAttribute.cs b/src/System.Web.Mvc/AcceptVerbsAttribute.cs new file mode 100644 index 00000000..1783cdfe --- /dev/null +++ b/src/System.Web.Mvc/AcceptVerbsAttribute.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + [SuppressMessage("Microsoft.Design", "CA1019:DefineAccessorsForAttributeArguments", Justification = "The accessor is exposed as an ICollection<string>.")] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public sealed class AcceptVerbsAttribute : ActionMethodSelectorAttribute + { + public AcceptVerbsAttribute(HttpVerbs verbs) + : this(EnumToArray(verbs)) + { + } + + public AcceptVerbsAttribute(params string[] verbs) + { + if (verbs == null || verbs.Length == 0) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "verbs"); + } + + Verbs = new ReadOnlyCollection<string>(verbs); + } + + public ICollection<string> Verbs { get; private set; } + + private static void AddEntryToList(HttpVerbs verbs, HttpVerbs match, List<string> verbList, string entryText) + { + if ((verbs & match) != 0) + { + verbList.Add(entryText); + } + } + + internal static string[] EnumToArray(HttpVerbs verbs) + { + List<string> verbList = new List<string>(); + + AddEntryToList(verbs, HttpVerbs.Get, verbList, "GET"); + AddEntryToList(verbs, HttpVerbs.Post, verbList, "POST"); + AddEntryToList(verbs, HttpVerbs.Put, verbList, "PUT"); + AddEntryToList(verbs, HttpVerbs.Delete, verbList, "DELETE"); + AddEntryToList(verbs, HttpVerbs.Head, verbList, "HEAD"); + + return verbList.ToArray(); + } + + public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo) + { + if (controllerContext == null) + { + throw new ArgumentNullException("controllerContext"); + } + + string incomingVerb = controllerContext.HttpContext.Request.GetHttpMethodOverride(); + + return Verbs.Contains(incomingVerb, StringComparer.OrdinalIgnoreCase); + } + } +} diff --git a/src/System.Web.Mvc/ActionDescriptor.cs b/src/System.Web.Mvc/ActionDescriptor.cs new file mode 100644 index 00000000..7175bdc4 --- /dev/null +++ b/src/System.Web.Mvc/ActionDescriptor.cs @@ -0,0 +1,196 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + public abstract class ActionDescriptor : ICustomAttributeProvider, IUniquelyIdentifiable + { + private static readonly ActionMethodDispatcherCache _staticDispatcherCache = new ActionMethodDispatcherCache(); + + private static readonly ActionSelector[] _emptySelectors = new ActionSelector[0]; + private readonly Lazy<string> _uniqueId; + private ActionMethodDispatcherCache _instanceDispatcherCache; + + protected ActionDescriptor() + { + _uniqueId = new Lazy<string>(CreateUniqueId); + } + + public abstract string ActionName { get; } + + public abstract ControllerDescriptor ControllerDescriptor { get; } + + internal ActionMethodDispatcherCache DispatcherCache + { + get + { + if (_instanceDispatcherCache == null) + { + _instanceDispatcherCache = _staticDispatcherCache; + } + return _instanceDispatcherCache; + } + set { _instanceDispatcherCache = value; } + } + + [SuppressMessage("Microsoft.Security", "CA2119:SealMethodsThatSatisfyPrivateInterfaces", Justification = "This is overridden elsewhere in System.Web.Mvc")] + public virtual string UniqueId + { + get { return _uniqueId.Value; } + } + + private string CreateUniqueId() + { + return DescriptorUtil.CreateUniqueId(GetType(), ControllerDescriptor, ActionName); + } + + public abstract object Execute(ControllerContext controllerContext, IDictionary<string, object> parameters); + + internal static object ExtractParameterFromDictionary(ParameterInfo parameterInfo, IDictionary<string, object> parameters, MethodInfo methodInfo) + { + object value; + + if (!parameters.TryGetValue(parameterInfo.Name, out value)) + { + // the key should always be present, even if the parameter value is null + string message = String.Format(CultureInfo.CurrentCulture, MvcResources.ReflectedActionDescriptor_ParameterNotInDictionary, + parameterInfo.Name, parameterInfo.ParameterType, methodInfo, methodInfo.DeclaringType); + throw new ArgumentException(message, "parameters"); + } + + if (value == null && !TypeHelpers.TypeAllowsNullValue(parameterInfo.ParameterType)) + { + // tried to pass a null value for a non-nullable parameter type + string message = String.Format(CultureInfo.CurrentCulture, MvcResources.ReflectedActionDescriptor_ParameterCannotBeNull, + parameterInfo.Name, parameterInfo.ParameterType, methodInfo, methodInfo.DeclaringType); + throw new ArgumentException(message, "parameters"); + } + + if (value != null && !parameterInfo.ParameterType.IsInstanceOfType(value)) + { + // value was supplied but is not of the proper type + string message = String.Format(CultureInfo.CurrentCulture, MvcResources.ReflectedActionDescriptor_ParameterValueHasWrongType, + parameterInfo.Name, methodInfo, methodInfo.DeclaringType, value.GetType(), parameterInfo.ParameterType); + throw new ArgumentException(message, "parameters"); + } + + return value; + } + + internal static object ExtractParameterOrDefaultFromDictionary(ParameterInfo parameterInfo, IDictionary<string, object> parameters) + { + Type parameterType = parameterInfo.ParameterType; + + object value; + parameters.TryGetValue(parameterInfo.Name, out value); + + // if wrong type, replace with default instance + if (parameterType.IsInstanceOfType(value)) + { + return value; + } + else + { + object defaultValue; + if (ParameterInfoUtil.TryGetDefaultValue(parameterInfo, out defaultValue)) + { + return defaultValue; + } + else + { + return TypeHelpers.GetDefaultValue(parameterType); + } + } + } + + public virtual object[] GetCustomAttributes(bool inherit) + { + return GetCustomAttributes(typeof(object), inherit); + } + + public virtual object[] GetCustomAttributes(Type attributeType, bool inherit) + { + if (attributeType == null) + { + throw new ArgumentNullException("attributeType"); + } + + return (object[])Array.CreateInstance(attributeType, 0); + } + + public virtual IEnumerable<FilterAttribute> GetFilterAttributes(bool useCache) + { + return GetCustomAttributes(typeof(FilterAttribute), inherit: true).Cast<FilterAttribute>(); + } + + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Please call System.Web.Mvc.FilterProviders.Providers.GetFilters() now.", true)] + public virtual FilterInfo GetFilters() + { + return new FilterInfo(); + } + + public abstract ParameterDescriptor[] GetParameters(); + + [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This method may perform non-trivial work.")] + public virtual ICollection<ActionSelector> GetSelectors() + { + return _emptySelectors; + } + + public virtual bool IsDefined(Type attributeType, bool inherit) + { + if (attributeType == null) + { + throw new ArgumentNullException("attributeType"); + } + + return false; + } + + internal static string VerifyActionMethodIsCallable(MethodInfo methodInfo) + { + // we can't call static methods + if (methodInfo.IsStatic) + { + return String.Format(CultureInfo.CurrentCulture, + MvcResources.ReflectedActionDescriptor_CannotCallStaticMethod, + methodInfo, + methodInfo.ReflectedType.FullName); + } + + // we can't call instance methods where the 'this' parameter is a type other than ControllerBase + if (!typeof(ControllerBase).IsAssignableFrom(methodInfo.ReflectedType)) + { + return String.Format(CultureInfo.CurrentCulture, MvcResources.ReflectedActionDescriptor_CannotCallInstanceMethodOnNonControllerType, + methodInfo, methodInfo.ReflectedType.FullName); + } + + // we can't call methods with open generic type parameters + if (methodInfo.ContainsGenericParameters) + { + return String.Format(CultureInfo.CurrentCulture, MvcResources.ReflectedActionDescriptor_CannotCallOpenGenericMethods, + methodInfo, methodInfo.ReflectedType.FullName); + } + + // we can't call methods with ref/out parameters + ParameterInfo[] parameterInfos = methodInfo.GetParameters(); + foreach (ParameterInfo parameterInfo in parameterInfos) + { + if (parameterInfo.IsOut || parameterInfo.ParameterType.IsByRef) + { + return String.Format(CultureInfo.CurrentCulture, MvcResources.ReflectedActionDescriptor_CannotCallMethodsWithOutOrRefParameters, + methodInfo, methodInfo.ReflectedType.FullName, parameterInfo); + } + } + + // we can call this method + return null; + } + } +} diff --git a/src/System.Web.Mvc/ActionDescriptorHelper.cs b/src/System.Web.Mvc/ActionDescriptorHelper.cs new file mode 100644 index 00000000..1d659c0e --- /dev/null +++ b/src/System.Web.Mvc/ActionDescriptorHelper.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using System.Reflection; + +namespace System.Web.Mvc +{ + internal static class ActionDescriptorHelper + { + public static ICollection<ActionSelector> GetSelectors(MethodInfo methodInfo) + { + ActionMethodSelectorAttribute[] attrs = (ActionMethodSelectorAttribute[])methodInfo.GetCustomAttributes(typeof(ActionMethodSelectorAttribute), inherit: true); + ActionSelector[] selectors = Array.ConvertAll(attrs, attr => (ActionSelector)(controllerContext => attr.IsValidForRequest(controllerContext, methodInfo))); + return selectors; + } + + public static bool IsDefined(MemberInfo methodInfo, Type attributeType, bool inherit) + { + return methodInfo.IsDefined(attributeType, inherit); + } + + public static object[] GetCustomAttributes(MemberInfo methodInfo, bool inherit) + { + return methodInfo.GetCustomAttributes(inherit); + } + + public static object[] GetCustomAttributes(MemberInfo methodInfo, Type attributeType, bool inherit) + { + return methodInfo.GetCustomAttributes(attributeType, inherit); + } + + public static ParameterDescriptor[] GetParameters(ActionDescriptor actionDescriptor, MethodInfo methodInfo, ref ParameterDescriptor[] parametersCache) + { + ParameterDescriptor[] parameters = LazilyFetchParametersCollection(actionDescriptor, methodInfo, ref parametersCache); + + // need to clone array so that user modifications aren't accidentally stored + return (ParameterDescriptor[])parameters.Clone(); + } + + private static ParameterDescriptor[] LazilyFetchParametersCollection(ActionDescriptor actionDescriptor, MethodInfo methodInfo, ref ParameterDescriptor[] parametersCache) + { + return DescriptorUtil.LazilyFetchOrCreateDescriptors<ParameterInfo, ParameterDescriptor>( + cacheLocation: ref parametersCache, + initializer: methodInfo.GetParameters, + converter: parameterInfo => new ReflectedParameterDescriptor(parameterInfo, actionDescriptor)); + } + } +} diff --git a/src/System.Web.Mvc/ActionExecutedContext.cs b/src/System.Web.Mvc/ActionExecutedContext.cs new file mode 100644 index 00000000..0ed6b178 --- /dev/null +++ b/src/System.Web.Mvc/ActionExecutedContext.cs @@ -0,0 +1,42 @@ +using System.Diagnostics.CodeAnalysis; + +namespace System.Web.Mvc +{ + public class ActionExecutedContext : ControllerContext + { + private ActionResult _result; + + // parameterless constructor used for mocking + public ActionExecutedContext() + { + } + + [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "The virtual property setters are only to support mocking frameworks, in which case this constructor shouldn't be called anyway.")] + public ActionExecutedContext(ControllerContext controllerContext, ActionDescriptor actionDescriptor, bool canceled, Exception exception) + : base(controllerContext) + { + if (actionDescriptor == null) + { + throw new ArgumentNullException("actionDescriptor"); + } + + ActionDescriptor = actionDescriptor; + Canceled = canceled; + Exception = exception; + } + + public virtual ActionDescriptor ActionDescriptor { get; set; } + + public virtual bool Canceled { get; set; } + + public virtual Exception Exception { get; set; } + + public bool ExceptionHandled { get; set; } + + public ActionResult Result + { + get { return _result ?? EmptyResult.Instance; } + set { _result = value; } + } + } +} diff --git a/src/System.Web.Mvc/ActionExecutingContext.cs b/src/System.Web.Mvc/ActionExecutingContext.cs new file mode 100644 index 00000000..1a66c62c --- /dev/null +++ b/src/System.Web.Mvc/ActionExecutingContext.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace System.Web.Mvc +{ + public class ActionExecutingContext : ControllerContext + { + // parameterless constructor used for mocking + public ActionExecutingContext() + { + } + + [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "The virtual property setters are only to support mocking frameworks, in which case this constructor shouldn't be called anyway.")] + public ActionExecutingContext(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary<string, object> actionParameters) + : base(controllerContext) + { + if (actionDescriptor == null) + { + throw new ArgumentNullException("actionDescriptor"); + } + if (actionParameters == null) + { + throw new ArgumentNullException("actionParameters"); + } + + ActionDescriptor = actionDescriptor; + ActionParameters = actionParameters; + } + + public virtual ActionDescriptor ActionDescriptor { get; set; } + + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "The property setter is only here to support mocking this type and should not be called at runtime.")] + public virtual IDictionary<string, object> ActionParameters { get; set; } + + public ActionResult Result { get; set; } + } +} diff --git a/src/System.Web.Mvc/ActionFilterAttribute.cs b/src/System.Web.Mvc/ActionFilterAttribute.cs new file mode 100644 index 00000000..a27c5fa3 --- /dev/null +++ b/src/System.Web.Mvc/ActionFilterAttribute.cs @@ -0,0 +1,25 @@ +namespace System.Web.Mvc +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)] + public abstract class ActionFilterAttribute : FilterAttribute, IActionFilter, IResultFilter + { + // The OnXxx() methods are virtual rather than abstract so that a developer need override + // only the ones that interest him. + + public virtual void OnActionExecuting(ActionExecutingContext filterContext) + { + } + + public virtual void OnActionExecuted(ActionExecutedContext filterContext) + { + } + + public virtual void OnResultExecuting(ResultExecutingContext filterContext) + { + } + + public virtual void OnResultExecuted(ResultExecutedContext filterContext) + { + } + } +} diff --git a/src/System.Web.Mvc/ActionMethodDispatcher.cs b/src/System.Web.Mvc/ActionMethodDispatcher.cs new file mode 100644 index 00000000..3fc3a0b9 --- /dev/null +++ b/src/System.Web.Mvc/ActionMethodDispatcher.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; + +namespace System.Web.Mvc +{ + // The methods in this class don't perform error checking; that is the responsibility of the + // caller. + internal sealed class ActionMethodDispatcher + { + private ActionExecutor _executor; + + public ActionMethodDispatcher(MethodInfo methodInfo) + { + _executor = GetExecutor(methodInfo); + MethodInfo = methodInfo; + } + + private delegate object ActionExecutor(ControllerBase controller, object[] parameters); + + private delegate void VoidActionExecutor(ControllerBase controller, object[] parameters); + + public MethodInfo MethodInfo { get; private set; } + + public object Execute(ControllerBase controller, object[] parameters) + { + return _executor(controller, parameters); + } + + private static ActionExecutor GetExecutor(MethodInfo methodInfo) + { + // Parameters to executor + ParameterExpression controllerParameter = Expression.Parameter(typeof(ControllerBase), "controller"); + ParameterExpression parametersParameter = Expression.Parameter(typeof(object[]), "parameters"); + + // Build parameter list + List<Expression> parameters = new List<Expression>(); + ParameterInfo[] paramInfos = methodInfo.GetParameters(); + for (int i = 0; i < paramInfos.Length; i++) + { + ParameterInfo paramInfo = paramInfos[i]; + BinaryExpression valueObj = Expression.ArrayIndex(parametersParameter, Expression.Constant(i)); + UnaryExpression valueCast = Expression.Convert(valueObj, paramInfo.ParameterType); + + // valueCast is "(Ti) parameters[i]" + parameters.Add(valueCast); + } + + // Call method + UnaryExpression instanceCast = (!methodInfo.IsStatic) ? Expression.Convert(controllerParameter, methodInfo.ReflectedType) : null; + MethodCallExpression methodCall = methodCall = Expression.Call(instanceCast, methodInfo, parameters); + + // methodCall is "((TController) controller) method((T0) parameters[0], (T1) parameters[1], ...)" + // Create function + if (methodCall.Type == typeof(void)) + { + Expression<VoidActionExecutor> lambda = Expression.Lambda<VoidActionExecutor>(methodCall, controllerParameter, parametersParameter); + VoidActionExecutor voidExecutor = lambda.Compile(); + return WrapVoidAction(voidExecutor); + } + else + { + // must coerce methodCall to match ActionExecutor signature + UnaryExpression castMethodCall = Expression.Convert(methodCall, typeof(object)); + Expression<ActionExecutor> lambda = Expression.Lambda<ActionExecutor>(castMethodCall, controllerParameter, parametersParameter); + return lambda.Compile(); + } + } + + private static ActionExecutor WrapVoidAction(VoidActionExecutor executor) + { + return delegate(ControllerBase controller, object[] parameters) + { + executor(controller, parameters); + return null; + }; + } + } +} diff --git a/src/System.Web.Mvc/ActionMethodDispatcherCache.cs b/src/System.Web.Mvc/ActionMethodDispatcherCache.cs new file mode 100644 index 00000000..cdfd57af --- /dev/null +++ b/src/System.Web.Mvc/ActionMethodDispatcherCache.cs @@ -0,0 +1,16 @@ +using System.Reflection; + +namespace System.Web.Mvc +{ + internal sealed class ActionMethodDispatcherCache : ReaderWriterCache<MethodInfo, ActionMethodDispatcher> + { + public ActionMethodDispatcherCache() + { + } + + public ActionMethodDispatcher GetDispatcher(MethodInfo methodInfo) + { + return FetchOrCreateItem(methodInfo, () => new ActionMethodDispatcher(methodInfo)); + } + } +} diff --git a/src/System.Web.Mvc/ActionMethodSelector.cs b/src/System.Web.Mvc/ActionMethodSelector.cs new file mode 100644 index 00000000..834b296a --- /dev/null +++ b/src/System.Web.Mvc/ActionMethodSelector.cs @@ -0,0 +1,116 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + internal sealed class ActionMethodSelector + { + public ActionMethodSelector(Type controllerType) + { + ControllerType = controllerType; + PopulateLookupTables(); + } + + public Type ControllerType { get; private set; } + + public MethodInfo[] AliasedMethods { get; private set; } + + public ILookup<string, MethodInfo> NonAliasedMethods { get; private set; } + + private AmbiguousMatchException CreateAmbiguousMatchException(List<MethodInfo> ambiguousMethods, string actionName) + { + StringBuilder exceptionMessageBuilder = new StringBuilder(); + foreach (MethodInfo methodInfo in ambiguousMethods) + { + string controllerAction = Convert.ToString(methodInfo, CultureInfo.CurrentCulture); + string controllerType = methodInfo.DeclaringType.FullName; + exceptionMessageBuilder.AppendLine(); + exceptionMessageBuilder.AppendFormat(CultureInfo.CurrentCulture, MvcResources.ActionMethodSelector_AmbiguousMatchType, controllerAction, controllerType); + } + string message = String.Format(CultureInfo.CurrentCulture, MvcResources.ActionMethodSelector_AmbiguousMatch, + actionName, ControllerType.Name, exceptionMessageBuilder); + return new AmbiguousMatchException(message); + } + + public MethodInfo FindActionMethod(ControllerContext controllerContext, string actionName) + { + List<MethodInfo> methodsMatchingName = GetMatchingAliasedMethods(controllerContext, actionName); + methodsMatchingName.AddRange(NonAliasedMethods[actionName]); + List<MethodInfo> finalMethods = RunSelectionFilters(controllerContext, methodsMatchingName); + + switch (finalMethods.Count) + { + case 0: + return null; + + case 1: + return finalMethods[0]; + + default: + throw CreateAmbiguousMatchException(finalMethods, actionName); + } + } + + internal List<MethodInfo> GetMatchingAliasedMethods(ControllerContext controllerContext, string actionName) + { + // find all aliased methods which are opting in to this request + // to opt in, all attributes defined on the method must return true + + var methods = from methodInfo in AliasedMethods + let attrs = ReflectedAttributeCache.GetActionNameSelectorAttributes(methodInfo) + where attrs.All(attr => attr.IsValidName(controllerContext, actionName, methodInfo)) + select methodInfo; + return methods.ToList(); + } + + private static bool IsMethodDecoratedWithAliasingAttribute(MethodInfo methodInfo) + { + return methodInfo.IsDefined(typeof(ActionNameSelectorAttribute), true /* inherit */); + } + + private static bool IsValidActionMethod(MethodInfo methodInfo) + { + return !(methodInfo.IsSpecialName || + methodInfo.GetBaseDefinition().DeclaringType.IsAssignableFrom(typeof(Controller))); + } + + private void PopulateLookupTables() + { + MethodInfo[] allMethods = ControllerType.GetMethods(BindingFlags.InvokeMethod | BindingFlags.Instance | BindingFlags.Public); + MethodInfo[] actionMethods = Array.FindAll(allMethods, IsValidActionMethod); + + AliasedMethods = Array.FindAll(actionMethods, IsMethodDecoratedWithAliasingAttribute); + NonAliasedMethods = actionMethods.Except(AliasedMethods).ToLookup(method => method.Name, StringComparer.OrdinalIgnoreCase); + } + + private static List<MethodInfo> RunSelectionFilters(ControllerContext controllerContext, List<MethodInfo> methodInfos) + { + // remove all methods which are opting out of this request + // to opt out, at least one attribute defined on the method must return false + + List<MethodInfo> matchesWithSelectionAttributes = new List<MethodInfo>(); + List<MethodInfo> matchesWithoutSelectionAttributes = new List<MethodInfo>(); + + foreach (MethodInfo methodInfo in methodInfos) + { + ICollection<ActionMethodSelectorAttribute> attrs = ReflectedAttributeCache.GetActionMethodSelectorAttributes(methodInfo); + if (attrs.Count == 0) + { + matchesWithoutSelectionAttributes.Add(methodInfo); + } + else if (attrs.All(attr => attr.IsValidForRequest(controllerContext, methodInfo))) + { + matchesWithSelectionAttributes.Add(methodInfo); + } + } + + // if a matching action method had a selection attribute, consider it more specific than a matching action method + // without a selection attribute + return (matchesWithSelectionAttributes.Count > 0) ? matchesWithSelectionAttributes : matchesWithoutSelectionAttributes; + } + } +} diff --git a/src/System.Web.Mvc/ActionMethodSelectorAttribute.cs b/src/System.Web.Mvc/ActionMethodSelectorAttribute.cs new file mode 100644 index 00000000..9dc3c8c2 --- /dev/null +++ b/src/System.Web.Mvc/ActionMethodSelectorAttribute.cs @@ -0,0 +1,10 @@ +using System.Reflection; + +namespace System.Web.Mvc +{ + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public abstract class ActionMethodSelectorAttribute : Attribute + { + public abstract bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo); + } +} diff --git a/src/System.Web.Mvc/ActionNameAttribute.cs b/src/System.Web.Mvc/ActionNameAttribute.cs new file mode 100644 index 00000000..087ef06e --- /dev/null +++ b/src/System.Web.Mvc/ActionNameAttribute.cs @@ -0,0 +1,26 @@ +using System.Reflection; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public sealed class ActionNameAttribute : ActionNameSelectorAttribute + { + public ActionNameAttribute(string name) + { + if (String.IsNullOrEmpty(name)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "name"); + } + + Name = name; + } + + public string Name { get; private set; } + + public override bool IsValidName(ControllerContext controllerContext, string actionName, MethodInfo methodInfo) + { + return String.Equals(actionName, Name, StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/src/System.Web.Mvc/ActionNameSelectorAttribute.cs b/src/System.Web.Mvc/ActionNameSelectorAttribute.cs new file mode 100644 index 00000000..8b2952fc --- /dev/null +++ b/src/System.Web.Mvc/ActionNameSelectorAttribute.cs @@ -0,0 +1,10 @@ +using System.Reflection; + +namespace System.Web.Mvc +{ + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public abstract class ActionNameSelectorAttribute : Attribute + { + public abstract bool IsValidName(ControllerContext controllerContext, string actionName, MethodInfo methodInfo); + } +} diff --git a/src/System.Web.Mvc/ActionResult.cs b/src/System.Web.Mvc/ActionResult.cs new file mode 100644 index 00000000..2dc190c0 --- /dev/null +++ b/src/System.Web.Mvc/ActionResult.cs @@ -0,0 +1,7 @@ +namespace System.Web.Mvc +{ + public abstract class ActionResult + { + public abstract void ExecuteResult(ControllerContext context); + } +} diff --git a/src/System.Web.Mvc/ActionSelector.cs b/src/System.Web.Mvc/ActionSelector.cs new file mode 100644 index 00000000..ee790b43 --- /dev/null +++ b/src/System.Web.Mvc/ActionSelector.cs @@ -0,0 +1,4 @@ +namespace System.Web.Mvc +{ + public delegate bool ActionSelector(ControllerContext controllerContext); +} diff --git a/src/System.Web.Mvc/AdditionalMetaDataAttribute.cs b/src/System.Web.Mvc/AdditionalMetaDataAttribute.cs new file mode 100644 index 00000000..0d131f83 --- /dev/null +++ b/src/System.Web.Mvc/AdditionalMetaDataAttribute.cs @@ -0,0 +1,38 @@ +namespace System.Web.Mvc +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Property, AllowMultiple = true)] + public sealed class AdditionalMetadataAttribute : Attribute, IMetadataAware + { + private object _typeId = new object(); + + public AdditionalMetadataAttribute(string name, object value) + { + if (name == null) + { + throw new ArgumentNullException("name"); + } + + Name = name; + Value = value; + } + + public override object TypeId + { + get { return _typeId; } + } + + public string Name { get; private set; } + + public object Value { get; private set; } + + public void OnMetadataCreated(ModelMetadata metadata) + { + if (metadata == null) + { + throw new ArgumentNullException("metadata"); + } + + metadata.AdditionalValues[Name] = Value; + } + } +} diff --git a/src/System.Web.Mvc/Ajax/AjaxExtensions.cs b/src/System.Web.Mvc/Ajax/AjaxExtensions.cs new file mode 100644 index 00000000..d2889fa9 --- /dev/null +++ b/src/System.Web.Mvc/Ajax/AjaxExtensions.cs @@ -0,0 +1,358 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Web.Mvc.Html; +using System.Web.Mvc.Properties; +using System.Web.Routing; + +namespace System.Web.Mvc.Ajax +{ + public static class AjaxExtensions + { + private const string LinkOnClickFormat = "Sys.Mvc.AsyncHyperlink.handleClick(this, new Sys.UI.DomEvent(event), {0});"; + private const string FormOnClickValue = "Sys.Mvc.AsyncForm.handleClick(this, new Sys.UI.DomEvent(event));"; + private const string FormOnSubmitFormat = "Sys.Mvc.AsyncForm.handleSubmit(this, new Sys.UI.DomEvent(event), {0});"; + + public static MvcHtmlString ActionLink(this AjaxHelper ajaxHelper, string linkText, string actionName, AjaxOptions ajaxOptions) + { + return ActionLink(ajaxHelper, linkText, actionName, (string)null /* controllerName */, ajaxOptions); + } + + public static MvcHtmlString ActionLink(this AjaxHelper ajaxHelper, string linkText, string actionName, object routeValues, AjaxOptions ajaxOptions) + { + return ActionLink(ajaxHelper, linkText, actionName, (string)null /* controllerName */, routeValues, ajaxOptions); + } + + public static MvcHtmlString ActionLink(this AjaxHelper ajaxHelper, string linkText, string actionName, object routeValues, AjaxOptions ajaxOptions, object htmlAttributes) + { + return ActionLink(ajaxHelper, linkText, actionName, (string)null /* controllerName */, routeValues, ajaxOptions, htmlAttributes); + } + + public static MvcHtmlString ActionLink(this AjaxHelper ajaxHelper, string linkText, string actionName, RouteValueDictionary routeValues, AjaxOptions ajaxOptions) + { + return ActionLink(ajaxHelper, linkText, actionName, (string)null /* controllerName */, routeValues, ajaxOptions); + } + + public static MvcHtmlString ActionLink(this AjaxHelper ajaxHelper, string linkText, string actionName, RouteValueDictionary routeValues, AjaxOptions ajaxOptions, IDictionary<string, object> htmlAttributes) + { + return ActionLink(ajaxHelper, linkText, actionName, (string)null /* controllerName */, routeValues, ajaxOptions, htmlAttributes); + } + + public static MvcHtmlString ActionLink(this AjaxHelper ajaxHelper, string linkText, string actionName, string controllerName, AjaxOptions ajaxOptions) + { + return ActionLink(ajaxHelper, linkText, actionName, controllerName, null /* values */, ajaxOptions, null /* htmlAttributes */); + } + + public static MvcHtmlString ActionLink(this AjaxHelper ajaxHelper, string linkText, string actionName, string controllerName, object routeValues, AjaxOptions ajaxOptions) + { + return ActionLink(ajaxHelper, linkText, actionName, controllerName, routeValues, ajaxOptions, null /* htmlAttributes */); + } + + public static MvcHtmlString ActionLink(this AjaxHelper ajaxHelper, string linkText, string actionName, string controllerName, object routeValues, AjaxOptions ajaxOptions, object htmlAttributes) + { + RouteValueDictionary newValues = new RouteValueDictionary(routeValues); + RouteValueDictionary newAttributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes); + return ActionLink(ajaxHelper, linkText, actionName, controllerName, newValues, ajaxOptions, newAttributes); + } + + public static MvcHtmlString ActionLink(this AjaxHelper ajaxHelper, string linkText, string actionName, string controllerName, RouteValueDictionary routeValues, AjaxOptions ajaxOptions) + { + return ActionLink(ajaxHelper, linkText, actionName, controllerName, routeValues, ajaxOptions, null /* htmlAttributes */); + } + + public static MvcHtmlString ActionLink(this AjaxHelper ajaxHelper, string linkText, string actionName, string controllerName, RouteValueDictionary routeValues, AjaxOptions ajaxOptions, IDictionary<string, object> htmlAttributes) + { + if (String.IsNullOrEmpty(linkText)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "linkText"); + } + + string targetUrl = UrlHelper.GenerateUrl(null, actionName, controllerName, routeValues, ajaxHelper.RouteCollection, ajaxHelper.ViewContext.RequestContext, true /* includeImplicitMvcValues */); + + return MvcHtmlString.Create(GenerateLink(ajaxHelper, linkText, targetUrl, GetAjaxOptions(ajaxOptions), htmlAttributes)); + } + + public static MvcHtmlString ActionLink(this AjaxHelper ajaxHelper, string linkText, string actionName, string controllerName, string protocol, string hostName, string fragment, object routeValues, AjaxOptions ajaxOptions, object htmlAttributes) + { + RouteValueDictionary newValues = new RouteValueDictionary(routeValues); + RouteValueDictionary newAttributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes); + return ActionLink(ajaxHelper, linkText, actionName, controllerName, protocol, hostName, fragment, newValues, ajaxOptions, newAttributes); + } + + public static MvcHtmlString ActionLink(this AjaxHelper ajaxHelper, string linkText, string actionName, string controllerName, string protocol, string hostName, string fragment, RouteValueDictionary routeValues, AjaxOptions ajaxOptions, IDictionary<string, object> htmlAttributes) + { + if (String.IsNullOrEmpty(linkText)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "linkText"); + } + + string targetUrl = UrlHelper.GenerateUrl(null /* routeName */, actionName, controllerName, protocol, hostName, fragment, routeValues, ajaxHelper.RouteCollection, ajaxHelper.ViewContext.RequestContext, true /* includeImplicitMvcValues */); + + return MvcHtmlString.Create(GenerateLink(ajaxHelper, linkText, targetUrl, ajaxOptions, htmlAttributes)); + } + + public static MvcForm BeginForm(this AjaxHelper ajaxHelper, AjaxOptions ajaxOptions) + { + string formAction = ajaxHelper.ViewContext.HttpContext.Request.RawUrl; + return FormHelper(ajaxHelper, formAction, ajaxOptions, new RouteValueDictionary()); + } + + public static MvcForm BeginForm(this AjaxHelper ajaxHelper, string actionName, AjaxOptions ajaxOptions) + { + return BeginForm(ajaxHelper, actionName, (string)null /* controllerName */, ajaxOptions); + } + + public static MvcForm BeginForm(this AjaxHelper ajaxHelper, string actionName, object routeValues, AjaxOptions ajaxOptions) + { + return BeginForm(ajaxHelper, actionName, (string)null /* controllerName */, routeValues, ajaxOptions); + } + + public static MvcForm BeginForm(this AjaxHelper ajaxHelper, string actionName, object routeValues, AjaxOptions ajaxOptions, object htmlAttributes) + { + return BeginForm(ajaxHelper, actionName, (string)null /* controllerName */, routeValues, ajaxOptions, htmlAttributes); + } + + public static MvcForm BeginForm(this AjaxHelper ajaxHelper, string actionName, RouteValueDictionary routeValues, AjaxOptions ajaxOptions) + { + return BeginForm(ajaxHelper, actionName, (string)null /* controllerName */, routeValues, ajaxOptions); + } + + public static MvcForm BeginForm(this AjaxHelper ajaxHelper, string actionName, RouteValueDictionary routeValues, AjaxOptions ajaxOptions, IDictionary<string, object> htmlAttributes) + { + return BeginForm(ajaxHelper, actionName, (string)null /* controllerName */, routeValues, ajaxOptions, htmlAttributes); + } + + public static MvcForm BeginForm(this AjaxHelper ajaxHelper, string actionName, string controllerName, AjaxOptions ajaxOptions) + { + return BeginForm(ajaxHelper, actionName, controllerName, null /* values */, ajaxOptions, null /* htmlAttributes */); + } + + public static MvcForm BeginForm(this AjaxHelper ajaxHelper, string actionName, string controllerName, object routeValues, AjaxOptions ajaxOptions) + { + return BeginForm(ajaxHelper, actionName, controllerName, routeValues, ajaxOptions, null /* htmlAttributes */); + } + + public static MvcForm BeginForm(this AjaxHelper ajaxHelper, string actionName, string controllerName, object routeValues, AjaxOptions ajaxOptions, object htmlAttributes) + { + RouteValueDictionary newValues = new RouteValueDictionary(routeValues); + RouteValueDictionary newAttributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes); + return BeginForm(ajaxHelper, actionName, controllerName, newValues, ajaxOptions, newAttributes); + } + + public static MvcForm BeginForm(this AjaxHelper ajaxHelper, string actionName, string controllerName, RouteValueDictionary routeValues, AjaxOptions ajaxOptions) + { + return BeginForm(ajaxHelper, actionName, controllerName, routeValues, ajaxOptions, null /* htmlAttributes */); + } + + public static MvcForm BeginForm(this AjaxHelper ajaxHelper, string actionName, string controllerName, RouteValueDictionary routeValues, AjaxOptions ajaxOptions, IDictionary<string, object> htmlAttributes) + { + // get target URL + string formAction = UrlHelper.GenerateUrl(null, actionName, controllerName, routeValues ?? new RouteValueDictionary(), ajaxHelper.RouteCollection, ajaxHelper.ViewContext.RequestContext, true /* includeImplicitMvcValues */); + return FormHelper(ajaxHelper, formAction, ajaxOptions, htmlAttributes); + } + + public static MvcForm BeginRouteForm(this AjaxHelper ajaxHelper, string routeName, AjaxOptions ajaxOptions) + { + return BeginRouteForm(ajaxHelper, routeName, null /* routeValues */, ajaxOptions, null /* htmlAttributes */); + } + + public static MvcForm BeginRouteForm(this AjaxHelper ajaxHelper, string routeName, object routeValues, AjaxOptions ajaxOptions) + { + return BeginRouteForm(ajaxHelper, routeName, (object)routeValues, ajaxOptions, null /* htmlAttributes */); + } + + public static MvcForm BeginRouteForm(this AjaxHelper ajaxHelper, string routeName, object routeValues, AjaxOptions ajaxOptions, object htmlAttributes) + { + RouteValueDictionary newAttributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes); + return BeginRouteForm(ajaxHelper, routeName, new RouteValueDictionary(routeValues), ajaxOptions, newAttributes); + } + + public static MvcForm BeginRouteForm(this AjaxHelper ajaxHelper, string routeName, RouteValueDictionary routeValues, AjaxOptions ajaxOptions) + { + return BeginRouteForm(ajaxHelper, routeName, routeValues, ajaxOptions, null /* htmlAttributes */); + } + + public static MvcForm BeginRouteForm(this AjaxHelper ajaxHelper, string routeName, RouteValueDictionary routeValues, AjaxOptions ajaxOptions, IDictionary<string, object> htmlAttributes) + { + string formAction = UrlHelper.GenerateUrl(routeName, null /* actionName */, null /* controllerName */, routeValues ?? new RouteValueDictionary(), ajaxHelper.RouteCollection, ajaxHelper.ViewContext.RequestContext, false /* includeImplicitMvcValues */); + return FormHelper(ajaxHelper, formAction, ajaxOptions, htmlAttributes); + } + + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "You don't want to dispose of this object unless you intend to write to the response")] + private static MvcForm FormHelper(this AjaxHelper ajaxHelper, string formAction, AjaxOptions ajaxOptions, IDictionary<string, object> htmlAttributes) + { + TagBuilder builder = new TagBuilder("form"); + builder.MergeAttributes(htmlAttributes); + builder.MergeAttribute("action", formAction); + builder.MergeAttribute("method", "post"); + + ajaxOptions = GetAjaxOptions(ajaxOptions); + + if (ajaxHelper.ViewContext.UnobtrusiveJavaScriptEnabled) + { + builder.MergeAttributes(ajaxOptions.ToUnobtrusiveHtmlAttributes()); + } + else + { + builder.MergeAttribute("onclick", FormOnClickValue); + builder.MergeAttribute("onsubmit", GenerateAjaxScript(ajaxOptions, FormOnSubmitFormat)); + } + + if (ajaxHelper.ViewContext.ClientValidationEnabled) + { + // forms must have an ID for client validation + builder.GenerateId(ajaxHelper.ViewContext.FormIdGenerator()); + } + + ajaxHelper.ViewContext.Writer.Write(builder.ToString(TagRenderMode.StartTag)); + MvcForm theForm = new MvcForm(ajaxHelper.ViewContext); + + if (ajaxHelper.ViewContext.ClientValidationEnabled) + { + ajaxHelper.ViewContext.FormContext.FormId = builder.Attributes["id"]; + } + + return theForm; + } + + public static MvcHtmlString GlobalizationScript(this AjaxHelper ajaxHelper) + { + return GlobalizationScript(ajaxHelper, CultureInfo.CurrentCulture); + } + + [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "ajaxHelper", Justification = "This is an extension method")] + public static MvcHtmlString GlobalizationScript(this AjaxHelper ajaxHelper, CultureInfo cultureInfo) + { + return GlobalizationScriptHelper(AjaxHelper.GlobalizationScriptPath, cultureInfo); + } + + internal static MvcHtmlString GlobalizationScriptHelper(string scriptPath, CultureInfo cultureInfo) + { + if (cultureInfo == null) + { + throw new ArgumentNullException("cultureInfo"); + } + + TagBuilder tagBuilder = new TagBuilder("script"); + tagBuilder.MergeAttribute("type", "text/javascript"); + + string src = VirtualPathUtility.AppendTrailingSlash(scriptPath) + HttpUtility.UrlEncode(cultureInfo.Name) + ".js"; + tagBuilder.MergeAttribute("src", src); + + return tagBuilder.ToMvcHtmlString(TagRenderMode.Normal); + } + + public static MvcHtmlString RouteLink(this AjaxHelper ajaxHelper, string linkText, object routeValues, AjaxOptions ajaxOptions) + { + return RouteLink(ajaxHelper, linkText, null /* routeName */, new RouteValueDictionary(routeValues), ajaxOptions, + new Dictionary<string, object>()); + } + + public static MvcHtmlString RouteLink(this AjaxHelper ajaxHelper, string linkText, object routeValues, AjaxOptions ajaxOptions, object htmlAttributes) + { + return RouteLink(ajaxHelper, linkText, null /* routeName */, new RouteValueDictionary(routeValues), ajaxOptions, + HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static MvcHtmlString RouteLink(this AjaxHelper ajaxHelper, string linkText, RouteValueDictionary routeValues, AjaxOptions ajaxOptions) + { + return RouteLink(ajaxHelper, linkText, null /* routeName */, routeValues, ajaxOptions, + new Dictionary<string, object>()); + } + + public static MvcHtmlString RouteLink(this AjaxHelper ajaxHelper, string linkText, RouteValueDictionary routeValues, AjaxOptions ajaxOptions, IDictionary<string, object> htmlAttributes) + { + return RouteLink(ajaxHelper, linkText, null /* routeName */, routeValues, ajaxOptions, htmlAttributes); + } + + public static MvcHtmlString RouteLink(this AjaxHelper ajaxHelper, string linkText, string routeName, AjaxOptions ajaxOptions) + { + return RouteLink(ajaxHelper, linkText, routeName, new RouteValueDictionary(), ajaxOptions, + new Dictionary<string, object>()); + } + + public static MvcHtmlString RouteLink(this AjaxHelper ajaxHelper, string linkText, string routeName, AjaxOptions ajaxOptions, object htmlAttributes) + { + return RouteLink(ajaxHelper, linkText, routeName, new RouteValueDictionary(), ajaxOptions, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static MvcHtmlString RouteLink(this AjaxHelper ajaxHelper, string linkText, string routeName, AjaxOptions ajaxOptions, IDictionary<string, object> htmlAttributes) + { + return RouteLink(ajaxHelper, linkText, routeName, new RouteValueDictionary(), ajaxOptions, htmlAttributes); + } + + public static MvcHtmlString RouteLink(this AjaxHelper ajaxHelper, string linkText, string routeName, object routeValues, AjaxOptions ajaxOptions) + { + return RouteLink(ajaxHelper, linkText, routeName, new RouteValueDictionary(routeValues), ajaxOptions, + new Dictionary<string, object>()); + } + + public static MvcHtmlString RouteLink(this AjaxHelper ajaxHelper, string linkText, string routeName, object routeValues, AjaxOptions ajaxOptions, object htmlAttributes) + { + return RouteLink(ajaxHelper, linkText, routeName, new RouteValueDictionary(routeValues), ajaxOptions, + HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static MvcHtmlString RouteLink(this AjaxHelper ajaxHelper, string linkText, string routeName, RouteValueDictionary routeValues, AjaxOptions ajaxOptions) + { + return RouteLink(ajaxHelper, linkText, routeName, routeValues, ajaxOptions, new Dictionary<string, object>()); + } + + public static MvcHtmlString RouteLink(this AjaxHelper ajaxHelper, string linkText, string routeName, RouteValueDictionary routeValues, AjaxOptions ajaxOptions, IDictionary<string, object> htmlAttributes) + { + if (String.IsNullOrEmpty(linkText)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "linkText"); + } + + string targetUrl = UrlHelper.GenerateUrl(routeName, null /* actionName */, null /* controllerName */, routeValues ?? new RouteValueDictionary(), ajaxHelper.RouteCollection, ajaxHelper.ViewContext.RequestContext, false /* includeImplicitMvcValues */); + + return MvcHtmlString.Create(GenerateLink(ajaxHelper, linkText, targetUrl, GetAjaxOptions(ajaxOptions), htmlAttributes)); + } + + public static MvcHtmlString RouteLink(this AjaxHelper ajaxHelper, string linkText, string routeName, string protocol, string hostName, string fragment, RouteValueDictionary routeValues, AjaxOptions ajaxOptions, IDictionary<string, object> htmlAttributes) + { + if (String.IsNullOrEmpty(linkText)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "linkText"); + } + + string targetUrl = UrlHelper.GenerateUrl(routeName, null /* actionName */, null /* controllerName */, protocol, hostName, fragment, routeValues ?? new RouteValueDictionary(), ajaxHelper.RouteCollection, ajaxHelper.ViewContext.RequestContext, false /* includeImplicitMvcValues */); + + return MvcHtmlString.Create(GenerateLink(ajaxHelper, linkText, targetUrl, GetAjaxOptions(ajaxOptions), htmlAttributes)); + } + + private static string GenerateLink(AjaxHelper ajaxHelper, string linkText, string targetUrl, AjaxOptions ajaxOptions, IDictionary<string, object> htmlAttributes) + { + TagBuilder tag = new TagBuilder("a") + { + InnerHtml = HttpUtility.HtmlEncode(linkText) + }; + + tag.MergeAttributes(htmlAttributes); + tag.MergeAttribute("href", targetUrl); + + if (ajaxHelper.ViewContext.UnobtrusiveJavaScriptEnabled) + { + tag.MergeAttributes(ajaxOptions.ToUnobtrusiveHtmlAttributes()); + } + else + { + tag.MergeAttribute("onclick", GenerateAjaxScript(ajaxOptions, LinkOnClickFormat)); + } + + return tag.ToString(TagRenderMode.Normal); + } + + private static string GenerateAjaxScript(AjaxOptions ajaxOptions, string scriptFormat) + { + string optionsString = ajaxOptions.ToJavascriptString(); + return String.Format(CultureInfo.InvariantCulture, scriptFormat, optionsString); + } + + private static AjaxOptions GetAjaxOptions(AjaxOptions ajaxOptions) + { + return (ajaxOptions != null) ? ajaxOptions : new AjaxOptions(); + } + } +} diff --git a/src/System.Web.Mvc/Ajax/AjaxOptions.cs b/src/System.Web.Mvc/Ajax/AjaxOptions.cs new file mode 100644 index 00000000..73b8f712 --- /dev/null +++ b/src/System.Web.Mvc/Ajax/AjaxOptions.cs @@ -0,0 +1,216 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Text; + +namespace System.Web.Mvc.Ajax +{ + public class AjaxOptions + { + private string _confirm; + private string _httpMethod; + private InsertionMode _insertionMode = InsertionMode.Replace; + private string _loadingElementId; + private string _onBegin; + private string _onComplete; + private string _onFailure; + private string _onSuccess; + private string _updateTargetId; + private string _url; + + public string Confirm + { + get { return _confirm ?? String.Empty; } + set { _confirm = value; } + } + + public string HttpMethod + { + get { return _httpMethod ?? String.Empty; } + set { _httpMethod = value; } + } + + public InsertionMode InsertionMode + { + get { return _insertionMode; } + set + { + switch (value) + { + case InsertionMode.Replace: + case InsertionMode.InsertAfter: + case InsertionMode.InsertBefore: + _insertionMode = value; + return; + + default: + throw new ArgumentOutOfRangeException("value"); + } + } + } + + internal string InsertionModeString + { + get + { + switch (InsertionMode) + { + case InsertionMode.Replace: + return "Sys.Mvc.InsertionMode.replace"; + case InsertionMode.InsertBefore: + return "Sys.Mvc.InsertionMode.insertBefore"; + case InsertionMode.InsertAfter: + return "Sys.Mvc.InsertionMode.insertAfter"; + default: + return ((int)InsertionMode).ToString(CultureInfo.InvariantCulture); + } + } + } + + internal string InsertionModeUnobtrusive + { + get + { + switch (InsertionMode) + { + case InsertionMode.Replace: + return "replace"; + case InsertionMode.InsertBefore: + return "before"; + case InsertionMode.InsertAfter: + return "after"; + default: + return ((int)InsertionMode).ToString(CultureInfo.InvariantCulture); + } + } + } + + public int LoadingElementDuration { get; set; } + + public string LoadingElementId + { + get { return _loadingElementId ?? String.Empty; } + set { _loadingElementId = value; } + } + + public string OnBegin + { + get { return _onBegin ?? String.Empty; } + set { _onBegin = value; } + } + + public string OnComplete + { + get { return _onComplete ?? String.Empty; } + set { _onComplete = value; } + } + + public string OnFailure + { + get { return _onFailure ?? String.Empty; } + set { _onFailure = value; } + } + + public string OnSuccess + { + get { return _onSuccess ?? String.Empty; } + set { _onSuccess = value; } + } + + public string UpdateTargetId + { + get { return _updateTargetId ?? String.Empty; } + set { _updateTargetId = value; } + } + + [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "This property is used by the optionsBuilder which always accepts a string.")] + public string Url + { + get { return _url ?? String.Empty; } + set { _url = value; } + } + + internal string ToJavascriptString() + { + // creates a string of the form { key1: value1, key2 : value2, ... } + StringBuilder optionsBuilder = new StringBuilder("{"); + optionsBuilder.Append(String.Format(CultureInfo.InvariantCulture, " insertionMode: {0},", InsertionModeString)); + optionsBuilder.Append(PropertyStringIfSpecified("confirm", Confirm)); + optionsBuilder.Append(PropertyStringIfSpecified("httpMethod", HttpMethod)); + optionsBuilder.Append(PropertyStringIfSpecified("loadingElementId", LoadingElementId)); + optionsBuilder.Append(PropertyStringIfSpecified("updateTargetId", UpdateTargetId)); + optionsBuilder.Append(PropertyStringIfSpecified("url", Url)); + optionsBuilder.Append(EventStringIfSpecified("onBegin", OnBegin)); + optionsBuilder.Append(EventStringIfSpecified("onComplete", OnComplete)); + optionsBuilder.Append(EventStringIfSpecified("onFailure", OnFailure)); + optionsBuilder.Append(EventStringIfSpecified("onSuccess", OnSuccess)); + optionsBuilder.Length--; + optionsBuilder.Append(" }"); + return optionsBuilder.ToString(); + } + + public IDictionary<string, object> ToUnobtrusiveHtmlAttributes() + { + var result = new Dictionary<string, object> + { + { "data-ajax", "true" }, + }; + + AddToDictionaryIfSpecified(result, "data-ajax-url", Url); + AddToDictionaryIfSpecified(result, "data-ajax-method", HttpMethod); + AddToDictionaryIfSpecified(result, "data-ajax-confirm", Confirm); + + AddToDictionaryIfSpecified(result, "data-ajax-begin", OnBegin); + AddToDictionaryIfSpecified(result, "data-ajax-complete", OnComplete); + AddToDictionaryIfSpecified(result, "data-ajax-failure", OnFailure); + AddToDictionaryIfSpecified(result, "data-ajax-success", OnSuccess); + + if (!String.IsNullOrWhiteSpace(LoadingElementId)) + { + result.Add("data-ajax-loading", "#" + LoadingElementId); + + if (LoadingElementDuration > 0) + { + result.Add("data-ajax-loading-duration", LoadingElementDuration); + } + } + + if (!String.IsNullOrWhiteSpace(UpdateTargetId)) + { + result.Add("data-ajax-update", "#" + UpdateTargetId); + result.Add("data-ajax-mode", InsertionModeUnobtrusive); + } + + return result; + } + + // Helpers + + private static void AddToDictionaryIfSpecified(IDictionary<string, object> dictionary, string name, string value) + { + if (!String.IsNullOrWhiteSpace(value)) + { + dictionary.Add(name, value); + } + } + + private static string EventStringIfSpecified(string propertyName, string handler) + { + if (!String.IsNullOrEmpty(handler)) + { + return String.Format(CultureInfo.InvariantCulture, " {0}: Function.createDelegate(this, {1}),", propertyName, handler.ToString()); + } + return String.Empty; + } + + private static string PropertyStringIfSpecified(string propertyName, string propertyValue) + { + if (!String.IsNullOrEmpty(propertyValue)) + { + string escapedPropertyValue = propertyValue.Replace("'", @"\'"); + return String.Format(CultureInfo.InvariantCulture, " {0}: '{1}',", propertyName, escapedPropertyValue); + } + return String.Empty; + } + } +} diff --git a/src/System.Web.Mvc/Ajax/InsertionMode.cs b/src/System.Web.Mvc/Ajax/InsertionMode.cs new file mode 100644 index 00000000..3e0affc9 --- /dev/null +++ b/src/System.Web.Mvc/Ajax/InsertionMode.cs @@ -0,0 +1,9 @@ +namespace System.Web.Mvc.Ajax +{ + public enum InsertionMode + { + Replace = 0, + InsertBefore = 1, + InsertAfter = 2 + } +} diff --git a/src/System.Web.Mvc/AjaxHelper.cs b/src/System.Web.Mvc/AjaxHelper.cs new file mode 100644 index 00000000..ab2d82cb --- /dev/null +++ b/src/System.Web.Mvc/AjaxHelper.cs @@ -0,0 +1,83 @@ +using System.Diagnostics.CodeAnalysis; +using System.Web.Routing; + +namespace System.Web.Mvc +{ + public class AjaxHelper + { + private static string _globalizationScriptPath; + + private DynamicViewDataDictionary _dynamicViewDataDictionary; + + public AjaxHelper(ViewContext viewContext, IViewDataContainer viewDataContainer) + : this(viewContext, viewDataContainer, RouteTable.Routes) + { + } + + public AjaxHelper(ViewContext viewContext, IViewDataContainer viewDataContainer, RouteCollection routeCollection) + { + if (viewContext == null) + { + throw new ArgumentNullException("viewContext"); + } + if (viewDataContainer == null) + { + throw new ArgumentNullException("viewDataContainer"); + } + if (routeCollection == null) + { + throw new ArgumentNullException("routeCollection"); + } + ViewContext = viewContext; + ViewDataContainer = viewDataContainer; + RouteCollection = routeCollection; + } + + public static string GlobalizationScriptPath + { + get + { + if (String.IsNullOrEmpty(_globalizationScriptPath)) + { + _globalizationScriptPath = "~/Scripts/Globalization"; + } + return _globalizationScriptPath; + } + set { _globalizationScriptPath = value; } + } + + public RouteCollection RouteCollection { get; private set; } + + public dynamic ViewBag + { + get + { + if (_dynamicViewDataDictionary == null) + { + _dynamicViewDataDictionary = new DynamicViewDataDictionary(() => ViewData); + } + return _dynamicViewDataDictionary; + } + } + + public ViewContext ViewContext { get; private set; } + + public ViewDataDictionary ViewData + { + get { return ViewDataContainer.ViewData; } + } + + public IViewDataContainer ViewDataContainer { get; internal set; } + + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Instance method for consistency with other helpers.")] + public string JavaScriptStringEncode(string message) + { + if (String.IsNullOrEmpty(message)) + { + return message; + } + + return HttpUtility.JavaScriptStringEncode(message); + } + } +} diff --git a/src/System.Web.Mvc/AjaxHelper`1.cs b/src/System.Web.Mvc/AjaxHelper`1.cs new file mode 100644 index 00000000..f579b9f1 --- /dev/null +++ b/src/System.Web.Mvc/AjaxHelper`1.cs @@ -0,0 +1,39 @@ +using System.Web.Routing; + +namespace System.Web.Mvc +{ + public class AjaxHelper<TModel> : AjaxHelper + { + private DynamicViewDataDictionary _dynamicViewDataDictionary; + private ViewDataDictionary<TModel> _viewData; + + public AjaxHelper(ViewContext viewContext, IViewDataContainer viewDataContainer) + : this(viewContext, viewDataContainer, RouteTable.Routes) + { + } + + public AjaxHelper(ViewContext viewContext, IViewDataContainer viewDataContainer, RouteCollection routeCollection) + : base(viewContext, viewDataContainer, routeCollection) + { + _viewData = new ViewDataDictionary<TModel>(viewDataContainer.ViewData); + } + + public new dynamic ViewBag + { + get + { + if (_dynamicViewDataDictionary == null) + { + _dynamicViewDataDictionary = new DynamicViewDataDictionary(() => ViewData); + } + + return _dynamicViewDataDictionary; + } + } + + public new ViewDataDictionary<TModel> ViewData + { + get { return _viewData; } + } + } +} diff --git a/src/System.Web.Mvc/AjaxRequestExtensions.cs b/src/System.Web.Mvc/AjaxRequestExtensions.cs new file mode 100644 index 00000000..e890fdcc --- /dev/null +++ b/src/System.Web.Mvc/AjaxRequestExtensions.cs @@ -0,0 +1,15 @@ +namespace System.Web.Mvc +{ + public static class AjaxRequestExtensions + { + public static bool IsAjaxRequest(this HttpRequestBase request) + { + if (request == null) + { + throw new ArgumentNullException("request"); + } + + return (request["X-Requested-With"] == "XMLHttpRequest") || ((request.Headers != null) && (request.Headers["X-Requested-With"] == "XMLHttpRequest")); + } + } +} diff --git a/src/System.Web.Mvc/AllowAnonymousAttribute.cs b/src/System.Web.Mvc/AllowAnonymousAttribute.cs new file mode 100644 index 00000000..780e7c51 --- /dev/null +++ b/src/System.Web.Mvc/AllowAnonymousAttribute.cs @@ -0,0 +1,11 @@ +namespace System.Web.Mvc +{ + /// <summary> + /// Actions and controllers with the AllowAnonymous attribute are skipped by the Authorize attribute + /// on authorization. See AccountController.cs in the project template for an example. + /// </summary> + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public sealed class AllowAnonymousAttribute : Attribute + { + } +} diff --git a/src/System.Web.Mvc/AllowHtmlAttribute.cs b/src/System.Web.Mvc/AllowHtmlAttribute.cs new file mode 100644 index 00000000..66abb499 --- /dev/null +++ b/src/System.Web.Mvc/AllowHtmlAttribute.cs @@ -0,0 +1,19 @@ +namespace System.Web.Mvc +{ + // This attribute can be applied to a model property to specify that the particular property to + // which it is applied should not go through request validation. + + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public sealed class AllowHtmlAttribute : Attribute, IMetadataAware + { + public void OnMetadataCreated(ModelMetadata metadata) + { + if (metadata == null) + { + throw new ArgumentNullException("metadata"); + } + + metadata.RequestValidationEnabled = false; + } + } +} diff --git a/src/System.Web.Mvc/AreaHelpers.cs b/src/System.Web.Mvc/AreaHelpers.cs new file mode 100644 index 00000000..e8deaed0 --- /dev/null +++ b/src/System.Web.Mvc/AreaHelpers.cs @@ -0,0 +1,35 @@ +using System.Web.Routing; + +namespace System.Web.Mvc +{ + internal static class AreaHelpers + { + public static string GetAreaName(RouteBase route) + { + IRouteWithArea routeWithArea = route as IRouteWithArea; + if (routeWithArea != null) + { + return routeWithArea.Area; + } + + Route castRoute = route as Route; + if (castRoute != null && castRoute.DataTokens != null) + { + return castRoute.DataTokens["area"] as string; + } + + return null; + } + + public static string GetAreaName(RouteData routeData) + { + object area; + if (routeData.DataTokens.TryGetValue("area", out area)) + { + return area as string; + } + + return GetAreaName(routeData.Route); + } + } +} diff --git a/src/System.Web.Mvc/AreaRegistration.cs b/src/System.Web.Mvc/AreaRegistration.cs new file mode 100644 index 00000000..6b80fe7e --- /dev/null +++ b/src/System.Web.Mvc/AreaRegistration.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Web.Routing; + +namespace System.Web.Mvc +{ + public abstract class AreaRegistration + { + private const string TypeCacheName = "MVC-AreaRegistrationTypeCache.xml"; + + public abstract string AreaName { get; } + + internal void CreateContextAndRegister(RouteCollection routes, object state) + { + AreaRegistrationContext context = new AreaRegistrationContext(AreaName, routes, state); + + string thisNamespace = GetType().Namespace; + if (thisNamespace != null) + { + context.Namespaces.Add(thisNamespace + ".*"); + } + + RegisterArea(context); + } + + private static bool IsAreaRegistrationType(Type type) + { + return + typeof(AreaRegistration).IsAssignableFrom(type) && + type.GetConstructor(Type.EmptyTypes) != null; + } + + public static void RegisterAllAreas() + { + RegisterAllAreas(null); + } + + public static void RegisterAllAreas(object state) + { + RegisterAllAreas(RouteTable.Routes, new BuildManagerWrapper(), state); + } + + internal static void RegisterAllAreas(RouteCollection routes, IBuildManager buildManager, object state) + { + List<Type> areaRegistrationTypes = TypeCacheUtil.GetFilteredTypesFromAssemblies(TypeCacheName, IsAreaRegistrationType, buildManager); + foreach (Type areaRegistrationType in areaRegistrationTypes) + { + AreaRegistration registration = (AreaRegistration)Activator.CreateInstance(areaRegistrationType); + registration.CreateContextAndRegister(routes, state); + } + } + + public abstract void RegisterArea(AreaRegistrationContext context); + } +} diff --git a/src/System.Web.Mvc/AreaRegistrationContext.cs b/src/System.Web.Mvc/AreaRegistrationContext.cs new file mode 100644 index 00000000..44a9a44f --- /dev/null +++ b/src/System.Web.Mvc/AreaRegistrationContext.cs @@ -0,0 +1,93 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Web.Routing; + +namespace System.Web.Mvc +{ + public class AreaRegistrationContext + { + private readonly HashSet<string> _namespaces = new HashSet<string>(StringComparer.OrdinalIgnoreCase); + + public AreaRegistrationContext(string areaName, RouteCollection routes) + : this(areaName, routes, null) + { + } + + public AreaRegistrationContext(string areaName, RouteCollection routes, object state) + { + if (String.IsNullOrEmpty(areaName)) + { + throw Error.ParameterCannotBeNullOrEmpty("areaName"); + } + if (routes == null) + { + throw new ArgumentNullException("routes"); + } + + AreaName = areaName; + Routes = routes; + State = state; + } + + public string AreaName { get; private set; } + + public ICollection<string> Namespaces + { + get { return _namespaces; } + } + + public RouteCollection Routes { get; private set; } + + public object State { get; private set; } + + [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "This is not a regular URL as it may contain special routing characters.")] + public Route MapRoute(string name, string url) + { + return MapRoute(name, url, (object)null /* defaults */); + } + + [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "This is not a regular URL as it may contain special routing characters.")] + public Route MapRoute(string name, string url, object defaults) + { + return MapRoute(name, url, defaults, (object)null /* constraints */); + } + + [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "This is not a regular URL as it may contain special routing characters.")] + public Route MapRoute(string name, string url, object defaults, object constraints) + { + return MapRoute(name, url, defaults, constraints, null /* namespaces */); + } + + [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "This is not a regular URL as it may contain special routing characters.")] + public Route MapRoute(string name, string url, string[] namespaces) + { + return MapRoute(name, url, (object)null /* defaults */, namespaces); + } + + [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "This is not a regular URL as it may contain special routing characters.")] + public Route MapRoute(string name, string url, object defaults, string[] namespaces) + { + return MapRoute(name, url, defaults, null /* constraints */, namespaces); + } + + [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "This is not a regular URL as it may contain special routing characters.")] + public Route MapRoute(string name, string url, object defaults, object constraints, string[] namespaces) + { + if (namespaces == null && Namespaces != null) + { + namespaces = Namespaces.ToArray(); + } + + Route route = Routes.MapRoute(name, url, defaults, constraints, namespaces); + route.DataTokens["area"] = AreaName; + + // disabling the namespace lookup fallback mechanism keeps this areas from accidentally picking up + // controllers belonging to other areas + bool useNamespaceFallback = (namespaces == null || namespaces.Length == 0); + route.DataTokens["UseNamespaceFallback"] = useNamespaceFallback; + + return route; + } + } +} diff --git a/src/System.Web.Mvc/AssociatedMetadataProvider.cs b/src/System.Web.Mvc/AssociatedMetadataProvider.cs new file mode 100644 index 00000000..1d1b7617 --- /dev/null +++ b/src/System.Web.Mvc/AssociatedMetadataProvider.cs @@ -0,0 +1,110 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + // This class provides a good implementation of ModelMetadataProvider for people who will be + // using traditional classes with properties. It uses the buddy class support from + // DataAnnotations, and consolidates the three operations down to a single override + // for reading the attribute values and creating the metadata class. + public abstract class AssociatedMetadataProvider : ModelMetadataProvider + { + private static void ApplyMetadataAwareAttributes(IEnumerable<Attribute> attributes, ModelMetadata result) + { + foreach (IMetadataAware awareAttribute in attributes.OfType<IMetadataAware>()) + { + awareAttribute.OnMetadataCreated(result); + } + } + + protected abstract ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName); + + protected virtual IEnumerable<Attribute> FilterAttributes(Type containerType, PropertyDescriptor propertyDescriptor, IEnumerable<Attribute> attributes) + { + if (typeof(ViewPage).IsAssignableFrom(containerType) || typeof(ViewUserControl).IsAssignableFrom(containerType)) + { + return attributes.Where(a => !(a is ReadOnlyAttribute)); + } + + return attributes; + } + + public override IEnumerable<ModelMetadata> GetMetadataForProperties(object container, Type containerType) + { + if (containerType == null) + { + throw new ArgumentNullException("containerType"); + } + + return GetMetadataForPropertiesImpl(container, containerType); + } + + private IEnumerable<ModelMetadata> GetMetadataForPropertiesImpl(object container, Type containerType) + { + foreach (PropertyDescriptor property in GetTypeDescriptor(containerType).GetProperties()) + { + Func<object> modelAccessor = container == null ? null : GetPropertyValueAccessor(container, property); + yield return GetMetadataForProperty(modelAccessor, containerType, property); + } + } + + public override ModelMetadata GetMetadataForProperty(Func<object> modelAccessor, Type containerType, string propertyName) + { + if (containerType == null) + { + throw new ArgumentNullException("containerType"); + } + if (String.IsNullOrEmpty(propertyName)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "propertyName"); + } + + ICustomTypeDescriptor typeDescriptor = GetTypeDescriptor(containerType); + PropertyDescriptor property = typeDescriptor.GetProperties().Find(propertyName, true); + if (property == null) + { + throw new ArgumentException( + String.Format( + CultureInfo.CurrentCulture, + MvcResources.Common_PropertyNotFound, + containerType.FullName, propertyName)); + } + + return GetMetadataForProperty(modelAccessor, containerType, property); + } + + protected virtual ModelMetadata GetMetadataForProperty(Func<object> modelAccessor, Type containerType, PropertyDescriptor propertyDescriptor) + { + IEnumerable<Attribute> attributes = FilterAttributes(containerType, propertyDescriptor, propertyDescriptor.Attributes.Cast<Attribute>()); + ModelMetadata result = CreateMetadata(attributes, containerType, modelAccessor, propertyDescriptor.PropertyType, propertyDescriptor.Name); + ApplyMetadataAwareAttributes(attributes, result); + return result; + } + + public override ModelMetadata GetMetadataForType(Func<object> modelAccessor, Type modelType) + { + if (modelType == null) + { + throw new ArgumentNullException("modelType"); + } + + IEnumerable<Attribute> attributes = GetTypeDescriptor(modelType).GetAttributes().Cast<Attribute>(); + ModelMetadata result = CreateMetadata(attributes, null /* containerType */, modelAccessor, modelType, null /* propertyName */); + ApplyMetadataAwareAttributes(attributes, result); + return result; + } + + private static Func<object> GetPropertyValueAccessor(object container, PropertyDescriptor property) + { + return () => property.GetValue(container); + } + + protected virtual ICustomTypeDescriptor GetTypeDescriptor(Type type) + { + return TypeDescriptorHelper.Get(type); + } + } +} diff --git a/src/System.Web.Mvc/AssociatedValidatorProvider.cs b/src/System.Web.Mvc/AssociatedValidatorProvider.cs new file mode 100644 index 00000000..b91fade1 --- /dev/null +++ b/src/System.Web.Mvc/AssociatedValidatorProvider.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + public abstract class AssociatedValidatorProvider : ModelValidatorProvider + { + protected virtual ICustomTypeDescriptor GetTypeDescriptor(Type type) + { + return TypeDescriptorHelper.Get(type); + } + + public sealed override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context) + { + if (metadata == null) + { + throw new ArgumentNullException("metadata"); + } + if (context == null) + { + throw new ArgumentNullException("context"); + } + + if (metadata.ContainerType != null && !String.IsNullOrEmpty(metadata.PropertyName)) + { + return GetValidatorsForProperty(metadata, context); + } + + return GetValidatorsForType(metadata, context); + } + + protected abstract IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes); + + private IEnumerable<ModelValidator> GetValidatorsForProperty(ModelMetadata metadata, ControllerContext context) + { + ICustomTypeDescriptor typeDescriptor = GetTypeDescriptor(metadata.ContainerType); + PropertyDescriptor property = typeDescriptor.GetProperties().Find(metadata.PropertyName, true); + if (property == null) + { + throw new ArgumentException( + String.Format( + CultureInfo.CurrentCulture, + MvcResources.Common_PropertyNotFound, + metadata.ContainerType.FullName, metadata.PropertyName), + "metadata"); + } + + return GetValidators(metadata, context, property.Attributes.OfType<Attribute>()); + } + + private IEnumerable<ModelValidator> GetValidatorsForType(ModelMetadata metadata, ControllerContext context) + { + return GetValidators(metadata, context, GetTypeDescriptor(metadata.ModelType).GetAttributes().Cast<Attribute>()); + } + } +} diff --git a/src/System.Web.Mvc/Async/ActionDescriptorCreator.cs b/src/System.Web.Mvc/Async/ActionDescriptorCreator.cs new file mode 100644 index 00000000..0ecd2fc1 --- /dev/null +++ b/src/System.Web.Mvc/Async/ActionDescriptorCreator.cs @@ -0,0 +1,4 @@ +namespace System.Web.Mvc.Async +{ + internal delegate ActionDescriptor ActionDescriptorCreator(string actionName, ControllerDescriptor controllerDescriptor); +} diff --git a/src/System.Web.Mvc/Async/AsyncActionDescriptor.cs b/src/System.Web.Mvc/Async/AsyncActionDescriptor.cs new file mode 100644 index 00000000..8fd7567e --- /dev/null +++ b/src/System.Web.Mvc/Async/AsyncActionDescriptor.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc.Async +{ + public abstract class AsyncActionDescriptor : ActionDescriptor + { + public abstract IAsyncResult BeginExecute(ControllerContext controllerContext, IDictionary<string, object> parameters, AsyncCallback callback, object state); + + public abstract object EndExecute(IAsyncResult asyncResult); + + public override object Execute(ControllerContext controllerContext, IDictionary<string, object> parameters) + { + string errorMessage = String.Format(CultureInfo.CurrentCulture, MvcResources.AsyncActionDescriptor_CannotExecuteSynchronously, + ActionName); + + throw new InvalidOperationException(errorMessage); + } + + internal static AsyncManager GetAsyncManager(ControllerBase controller) + { + IAsyncManagerContainer helperContainer = controller as IAsyncManagerContainer; + if (helperContainer == null) + { + throw Error.AsyncCommon_ControllerMustImplementIAsyncManagerContainer(controller.GetType()); + } + + return helperContainer.AsyncManager; + } + } +} diff --git a/src/System.Web.Mvc/Async/AsyncActionMethodSelector.cs b/src/System.Web.Mvc/Async/AsyncActionMethodSelector.cs new file mode 100644 index 00000000..0d5d0d55 --- /dev/null +++ b/src/System.Web.Mvc/Async/AsyncActionMethodSelector.cs @@ -0,0 +1,227 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc.Async +{ + internal sealed class AsyncActionMethodSelector + { + // This flag controls async action binding for backwards compat since Controller now supports async. + // Set to true for classes that derive from AsyncController. In this case, FooAsync/FooCompleted is + // bound as a single async action pair "Foo". If false, they're bound as 2 separate sync actions. + // Practically, if this is false, then IsAsyncSuffixedMethod and IsCompeltedSuffixedMethod return false. + private bool _allowLegacyAsyncActions; + + public AsyncActionMethodSelector(Type controllerType, bool allowLegacyAsyncActions = true) + { + _allowLegacyAsyncActions = allowLegacyAsyncActions; + ControllerType = controllerType; + PopulateLookupTables(); + } + + public Type ControllerType { get; private set; } + + public MethodInfo[] AliasedMethods { get; private set; } + + public ILookup<string, MethodInfo> NonAliasedMethods { get; private set; } + + private AmbiguousMatchException CreateAmbiguousActionMatchException(IEnumerable<MethodInfo> ambiguousMethods, string actionName) + { + string ambiguityList = CreateAmbiguousMatchList(ambiguousMethods); + string message = String.Format(CultureInfo.CurrentCulture, MvcResources.ActionMethodSelector_AmbiguousMatch, + actionName, ControllerType.Name, ambiguityList); + return new AmbiguousMatchException(message); + } + + private AmbiguousMatchException CreateAmbiguousMethodMatchException(IEnumerable<MethodInfo> ambiguousMethods, string methodName) + { + string ambiguityList = CreateAmbiguousMatchList(ambiguousMethods); + string message = String.Format(CultureInfo.CurrentCulture, MvcResources.AsyncActionMethodSelector_AmbiguousMethodMatch, + methodName, ControllerType.Name, ambiguityList); + return new AmbiguousMatchException(message); + } + + private static string CreateAmbiguousMatchList(IEnumerable<MethodInfo> ambiguousMethods) + { + StringBuilder exceptionMessageBuilder = new StringBuilder(); + foreach (MethodInfo methodInfo in ambiguousMethods) + { + exceptionMessageBuilder.AppendLine(); + exceptionMessageBuilder.AppendFormat(CultureInfo.CurrentCulture, MvcResources.ActionMethodSelector_AmbiguousMatchType, methodInfo, methodInfo.DeclaringType.FullName); + } + + return exceptionMessageBuilder.ToString(); + } + + public ActionDescriptorCreator FindAction(ControllerContext controllerContext, string actionName) + { + List<MethodInfo> methodsMatchingName = GetMatchingAliasedMethods(controllerContext, actionName); + methodsMatchingName.AddRange(NonAliasedMethods[actionName]); + List<MethodInfo> finalMethods = RunSelectionFilters(controllerContext, methodsMatchingName); + + switch (finalMethods.Count) + { + case 0: + return null; + + case 1: + MethodInfo entryMethod = finalMethods[0]; + return GetActionDescriptorDelegate(entryMethod); + + default: + throw CreateAmbiguousActionMatchException(finalMethods, actionName); + } + } + + private ActionDescriptorCreator GetActionDescriptorDelegate(MethodInfo entryMethod) + { + // Does the action return a Task? + if (entryMethod.ReturnType != null && typeof(Task).IsAssignableFrom(entryMethod.ReturnType)) + { + return (actionName, controllerDescriptor) => new TaskAsyncActionDescriptor(entryMethod, actionName, controllerDescriptor); + } + + // Is this the FooAsync() / FooCompleted() pattern? + if (IsAsyncSuffixedMethod(entryMethod)) + { + string completionMethodName = entryMethod.Name.Substring(0, entryMethod.Name.Length - "Async".Length) + "Completed"; + MethodInfo completionMethod = GetMethodByName(completionMethodName); + if (completionMethod != null) + { + return (actionName, controllerDescriptor) => new ReflectedAsyncActionDescriptor(entryMethod, completionMethod, actionName, controllerDescriptor); + } + else + { + throw Error.AsyncActionMethodSelector_CouldNotFindMethod(completionMethodName, ControllerType); + } + } + + // Fallback to synchronous method + return (actionName, controllerDescriptor) => new ReflectedActionDescriptor(entryMethod, actionName, controllerDescriptor); + } + + private string GetCanonicalMethodName(MethodInfo methodInfo) + { + string methodName = methodInfo.Name; + return (IsAsyncSuffixedMethod(methodInfo)) + ? methodName.Substring(0, methodName.Length - "Async".Length) + : methodName; + } + + internal List<MethodInfo> GetMatchingAliasedMethods(ControllerContext controllerContext, string actionName) + { + // find all aliased methods which are opting in to this request + // to opt in, all attributes defined on the method must return true + + var methods = from methodInfo in AliasedMethods + let attrs = ReflectedAttributeCache.GetActionNameSelectorAttributes(methodInfo) + where attrs.All(attr => attr.IsValidName(controllerContext, actionName, methodInfo)) + select methodInfo; + return methods.ToList(); + } + + private bool IsAsyncSuffixedMethod(MethodInfo methodInfo) + { + return _allowLegacyAsyncActions && methodInfo.Name.EndsWith("Async", StringComparison.OrdinalIgnoreCase); + } + + private bool IsCompletedSuffixedMethod(MethodInfo methodInfo) + { + return _allowLegacyAsyncActions && methodInfo.Name.EndsWith("Completed", StringComparison.OrdinalIgnoreCase); + } + + private static bool IsMethodDecoratedWithAliasingAttribute(MethodInfo methodInfo) + { + return methodInfo.IsDefined(typeof(ActionNameSelectorAttribute), true /* inherit */); + } + + private MethodInfo GetMethodByName(string methodName) + { + List<MethodInfo> methods = (from MethodInfo methodInfo in ControllerType.GetMember(methodName, MemberTypes.Method, BindingFlags.Instance | BindingFlags.Public | BindingFlags.InvokeMethod | BindingFlags.IgnoreCase) + where IsValidActionMethod(methodInfo, false /* stripInfrastructureMethods */) + select methodInfo).ToList(); + + switch (methods.Count) + { + case 0: + return null; + + case 1: + return methods[0]; + + default: + throw CreateAmbiguousMethodMatchException(methods, methodName); + } + } + + private bool IsValidActionMethod(MethodInfo methodInfo) + { + return IsValidActionMethod(methodInfo, true /* stripInfrastructureMethods */); + } + + private bool IsValidActionMethod(MethodInfo methodInfo, bool stripInfrastructureMethods) + { + if (methodInfo.IsSpecialName) + { + // not a normal method, e.g. a constructor or an event + return false; + } + + if (methodInfo.GetBaseDefinition().DeclaringType.IsAssignableFrom(typeof(AsyncController))) + { + // is a method on Object, ControllerBase, Controller, or AsyncController + return false; + } + + if (stripInfrastructureMethods) + { + if (IsCompletedSuffixedMethod(methodInfo)) + { + // do not match FooCompleted() methods, as these are infrastructure methods + return false; + } + } + + return true; + } + + private void PopulateLookupTables() + { + MethodInfo[] allMethods = ControllerType.GetMethods(BindingFlags.InvokeMethod | BindingFlags.Instance | BindingFlags.Public); + MethodInfo[] actionMethods = Array.FindAll(allMethods, IsValidActionMethod); + + AliasedMethods = Array.FindAll(actionMethods, IsMethodDecoratedWithAliasingAttribute); + NonAliasedMethods = actionMethods.Except(AliasedMethods).ToLookup(GetCanonicalMethodName, StringComparer.OrdinalIgnoreCase); + } + + private static List<MethodInfo> RunSelectionFilters(ControllerContext controllerContext, List<MethodInfo> methodInfos) + { + // remove all methods which are opting out of this request + // to opt out, at least one attribute defined on the method must return false + + List<MethodInfo> matchesWithSelectionAttributes = new List<MethodInfo>(); + List<MethodInfo> matchesWithoutSelectionAttributes = new List<MethodInfo>(); + + foreach (MethodInfo methodInfo in methodInfos) + { + ICollection<ActionMethodSelectorAttribute> attrs = ReflectedAttributeCache.GetActionMethodSelectorAttributes(methodInfo); + if (attrs.Count == 0) + { + matchesWithoutSelectionAttributes.Add(methodInfo); + } + else if (attrs.All(attr => attr.IsValidForRequest(controllerContext, methodInfo))) + { + matchesWithSelectionAttributes.Add(methodInfo); + } + } + + // if a matching action method had a selection attribute, consider it more specific than a matching action method + // without a selection attribute + return (matchesWithSelectionAttributes.Count > 0) ? matchesWithSelectionAttributes : matchesWithoutSelectionAttributes; + } + } +} diff --git a/src/System.Web.Mvc/Async/AsyncControllerActionInvoker.cs b/src/System.Web.Mvc/Async/AsyncControllerActionInvoker.cs new file mode 100644 index 00000000..0d62065c --- /dev/null +++ b/src/System.Web.Mvc/Async/AsyncControllerActionInvoker.cs @@ -0,0 +1,324 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace System.Web.Mvc.Async +{ + public class AsyncControllerActionInvoker : ControllerActionInvoker, IAsyncActionInvoker + { + private static readonly object _invokeActionTag = new object(); + private static readonly object _invokeActionMethodTag = new object(); + private static readonly object _invokeActionMethodWithFiltersTag = new object(); + + public virtual IAsyncResult BeginInvokeAction(ControllerContext controllerContext, string actionName, AsyncCallback callback, object state) + { + if (controllerContext == null) + { + throw new ArgumentNullException("controllerContext"); + } + if (String.IsNullOrEmpty(actionName)) + { + throw Error.ParameterCannotBeNullOrEmpty("actionName"); + } + + ControllerDescriptor controllerDescriptor = GetControllerDescriptor(controllerContext); + ActionDescriptor actionDescriptor = FindAction(controllerContext, controllerDescriptor, actionName); + if (actionDescriptor != null) + { + FilterInfo filterInfo = GetFilters(controllerContext, actionDescriptor); + Action continuation = null; + + BeginInvokeDelegate beginDelegate = delegate(AsyncCallback asyncCallback, object asyncState) + { + try + { + AuthorizationContext authContext = InvokeAuthorizationFilters(controllerContext, filterInfo.AuthorizationFilters, actionDescriptor); + if (authContext.Result != null) + { + // the auth filter signaled that we should let it short-circuit the request + continuation = () => InvokeActionResult(controllerContext, authContext.Result); + } + else + { + if (controllerContext.Controller.ValidateRequest) + { + ValidateRequest(controllerContext); + } + + IDictionary<string, object> parameters = GetParameterValues(controllerContext, actionDescriptor); + IAsyncResult asyncResult = BeginInvokeActionMethodWithFilters(controllerContext, filterInfo.ActionFilters, actionDescriptor, parameters, asyncCallback, asyncState); + continuation = () => + { + ActionExecutedContext postActionContext = EndInvokeActionMethodWithFilters(asyncResult); + InvokeActionResultWithFilters(controllerContext, filterInfo.ResultFilters, postActionContext.Result); + }; + return asyncResult; + } + } + catch (ThreadAbortException) + { + // This type of exception occurs as a result of Response.Redirect(), but we special-case so that + // the filters don't see this as an error. + throw; + } + catch (Exception ex) + { + // something blew up, so execute the exception filters + ExceptionContext exceptionContext = InvokeExceptionFilters(controllerContext, filterInfo.ExceptionFilters, ex); + if (!exceptionContext.ExceptionHandled) + { + throw; + } + + continuation = () => InvokeActionResult(controllerContext, exceptionContext.Result); + } + + return BeginInvokeAction_MakeSynchronousAsyncResult(asyncCallback, asyncState); + }; + + EndInvokeDelegate<bool> endDelegate = delegate(IAsyncResult asyncResult) + { + try + { + continuation(); + } + catch (ThreadAbortException) + { + // This type of exception occurs as a result of Response.Redirect(), but we special-case so that + // the filters don't see this as an error. + throw; + } + catch (Exception ex) + { + // something blew up, so execute the exception filters + ExceptionContext exceptionContext = InvokeExceptionFilters(controllerContext, filterInfo.ExceptionFilters, ex); + if (!exceptionContext.ExceptionHandled) + { + throw; + } + InvokeActionResult(controllerContext, exceptionContext.Result); + } + + return true; + }; + + return AsyncResultWrapper.Begin(callback, state, beginDelegate, endDelegate, _invokeActionTag); + } + else + { + // Notify the controller that no action was found. + return BeginInvokeAction_ActionNotFound(callback, state); + } + } + + private static IAsyncResult BeginInvokeAction_ActionNotFound(AsyncCallback callback, object state) + { + BeginInvokeDelegate beginDelegate = BeginInvokeAction_MakeSynchronousAsyncResult; + + EndInvokeDelegate<bool> endDelegate = delegate(IAsyncResult asyncResult) + { + return false; + }; + + return AsyncResultWrapper.Begin(callback, state, beginDelegate, endDelegate, _invokeActionTag); + } + + private static IAsyncResult BeginInvokeAction_MakeSynchronousAsyncResult(AsyncCallback callback, object state) + { + SimpleAsyncResult asyncResult = new SimpleAsyncResult(state); + asyncResult.MarkCompleted(true /* completedSynchronously */, callback); + return asyncResult; + } + + protected internal virtual IAsyncResult BeginInvokeActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary<string, object> parameters, AsyncCallback callback, object state) + { + AsyncActionDescriptor asyncActionDescriptor = actionDescriptor as AsyncActionDescriptor; + if (asyncActionDescriptor != null) + { + return BeginInvokeAsynchronousActionMethod(controllerContext, asyncActionDescriptor, parameters, callback, state); + } + else + { + return BeginInvokeSynchronousActionMethod(controllerContext, actionDescriptor, parameters, callback, state); + } + } + + protected internal virtual IAsyncResult BeginInvokeActionMethodWithFilters(ControllerContext controllerContext, IList<IActionFilter> filters, ActionDescriptor actionDescriptor, IDictionary<string, object> parameters, AsyncCallback callback, object state) + { + Func<ActionExecutedContext> endContinuation = null; + + BeginInvokeDelegate beginDelegate = delegate(AsyncCallback asyncCallback, object asyncState) + { + ActionExecutingContext preContext = new ActionExecutingContext(controllerContext, actionDescriptor, parameters); + IAsyncResult innerAsyncResult = null; + + Func<Func<ActionExecutedContext>> beginContinuation = () => + { + innerAsyncResult = BeginInvokeActionMethod(controllerContext, actionDescriptor, parameters, asyncCallback, asyncState); + return () => + new ActionExecutedContext(controllerContext, actionDescriptor, false /* canceled */, null /* exception */) + { + Result = EndInvokeActionMethod(innerAsyncResult) + }; + }; + + // need to reverse the filter list because the continuations are built up backward + Func<Func<ActionExecutedContext>> thunk = filters.Reverse().Aggregate(beginContinuation, + (next, filter) => () => InvokeActionMethodFilterAsynchronously(filter, preContext, next)); + endContinuation = thunk(); + + if (innerAsyncResult != null) + { + // we're just waiting for the inner result to complete + return innerAsyncResult; + } + else + { + // something was short-circuited and the action was not called, so this was a synchronous operation + SimpleAsyncResult newAsyncResult = new SimpleAsyncResult(asyncState); + newAsyncResult.MarkCompleted(true /* completedSynchronously */, asyncCallback); + return newAsyncResult; + } + }; + + EndInvokeDelegate<ActionExecutedContext> endDelegate = delegate(IAsyncResult asyncResult) + { + return endContinuation(); + }; + + return AsyncResultWrapper.Begin(callback, state, beginDelegate, endDelegate, _invokeActionMethodWithFiltersTag); + } + + private IAsyncResult BeginInvokeAsynchronousActionMethod(ControllerContext controllerContext, AsyncActionDescriptor actionDescriptor, IDictionary<string, object> parameters, AsyncCallback callback, object state) + { + BeginInvokeDelegate beginDelegate = delegate(AsyncCallback asyncCallback, object asyncState) + { + return actionDescriptor.BeginExecute(controllerContext, parameters, asyncCallback, asyncState); + }; + + EndInvokeDelegate<ActionResult> endDelegate = delegate(IAsyncResult asyncResult) + { + object returnValue = actionDescriptor.EndExecute(asyncResult); + ActionResult result = CreateActionResult(controllerContext, actionDescriptor, returnValue); + return result; + }; + + return AsyncResultWrapper.Begin(callback, state, beginDelegate, endDelegate, _invokeActionMethodTag); + } + + private IAsyncResult BeginInvokeSynchronousActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary<string, object> parameters, AsyncCallback callback, object state) + { + return AsyncResultWrapper.BeginSynchronous(callback, state, + () => InvokeSynchronousActionMethod(controllerContext, actionDescriptor, parameters), + _invokeActionMethodTag); + } + + public virtual bool EndInvokeAction(IAsyncResult asyncResult) + { + return AsyncResultWrapper.End<bool>(asyncResult, _invokeActionTag); + } + + protected internal virtual ActionResult EndInvokeActionMethod(IAsyncResult asyncResult) + { + return AsyncResultWrapper.End<ActionResult>(asyncResult, _invokeActionMethodTag); + } + + protected internal virtual ActionExecutedContext EndInvokeActionMethodWithFilters(IAsyncResult asyncResult) + { + return AsyncResultWrapper.End<ActionExecutedContext>(asyncResult, _invokeActionMethodWithFiltersTag); + } + + protected override ControllerDescriptor GetControllerDescriptor(ControllerContext controllerContext) + { + Type controllerType = controllerContext.Controller.GetType(); + ControllerDescriptor controllerDescriptor = DescriptorCache.GetDescriptor(controllerType, () => new ReflectedAsyncControllerDescriptor(controllerType)); + return controllerDescriptor; + } + + internal static Func<ActionExecutedContext> InvokeActionMethodFilterAsynchronously(IActionFilter filter, ActionExecutingContext preContext, Func<Func<ActionExecutedContext>> nextInChain) + { + filter.OnActionExecuting(preContext); + if (preContext.Result != null) + { + ActionExecutedContext shortCircuitedPostContext = new ActionExecutedContext(preContext, preContext.ActionDescriptor, true /* canceled */, null /* exception */) + { + Result = preContext.Result + }; + return () => shortCircuitedPostContext; + } + + // There is a nested try / catch block here that contains much the same logic as the outer block. + // Since an exception can occur on either side of the asynchronous invocation, we need guards on + // on both sides. In the code below, the second side is represented by the nested delegate. This + // is really just a parallel of the synchronous ControllerActionInvoker.InvokeActionMethodFilter() + // method. + + try + { + Func<ActionExecutedContext> continuation = nextInChain(); + + // add our own continuation, then return the new function + return () => + { + ActionExecutedContext postContext; + bool wasError = true; + + try + { + postContext = continuation(); + wasError = false; + } + catch (ThreadAbortException) + { + // This type of exception occurs as a result of Response.Redirect(), but we special-case so that + // the filters don't see this as an error. + postContext = new ActionExecutedContext(preContext, preContext.ActionDescriptor, false /* canceled */, null /* exception */); + filter.OnActionExecuted(postContext); + throw; + } + catch (Exception ex) + { + postContext = new ActionExecutedContext(preContext, preContext.ActionDescriptor, false /* canceled */, ex); + filter.OnActionExecuted(postContext); + if (!postContext.ExceptionHandled) + { + throw; + } + } + if (!wasError) + { + filter.OnActionExecuted(postContext); + } + + return postContext; + }; + } + catch (ThreadAbortException) + { + // This type of exception occurs as a result of Response.Redirect(), but we special-case so that + // the filters don't see this as an error. + ActionExecutedContext postContext = new ActionExecutedContext(preContext, preContext.ActionDescriptor, false /* canceled */, null /* exception */); + filter.OnActionExecuted(postContext); + throw; + } + catch (Exception ex) + { + ActionExecutedContext postContext = new ActionExecutedContext(preContext, preContext.ActionDescriptor, false /* canceled */, ex); + filter.OnActionExecuted(postContext); + if (postContext.ExceptionHandled) + { + return () => postContext; + } + else + { + throw; + } + } + } + + private ActionResult InvokeSynchronousActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary<string, object> parameters) + { + return InvokeActionMethod(controllerContext, actionDescriptor, parameters); + } + } +} diff --git a/src/System.Web.Mvc/Async/AsyncManager.cs b/src/System.Web.Mvc/Async/AsyncManager.cs new file mode 100644 index 00000000..0e4869aa --- /dev/null +++ b/src/System.Web.Mvc/Async/AsyncManager.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; +using System.Threading; + +namespace System.Web.Mvc.Async +{ + public class AsyncManager + { + private readonly SynchronizationContext _syncContext; + + /// <summary> + /// default timeout is 45 sec + /// </summary> + /// <remarks> + /// from: http://msdn.microsoft.com/en-us/library/system.web.ui.page.asynctimeout.aspx + /// </remarks> + private int _timeout = 45 * 1000; + + public AsyncManager() + : this(null /* syncContext */) + { + } + + public AsyncManager(SynchronizationContext syncContext) + { + _syncContext = syncContext ?? SynchronizationContextUtil.GetSynchronizationContext(); + + OutstandingOperations = new OperationCounter(); + OutstandingOperations.Completed += delegate + { + Finish(); + }; + + Parameters = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); + } + + public event EventHandler Finished; + + public OperationCounter OutstandingOperations { get; private set; } + + public IDictionary<string, object> Parameters { get; private set; } + + /// <summary> + /// Measured in milliseconds, Timeout.Infinite means 'no timeout' + /// </summary> + public int Timeout + { + get { return _timeout; } + set + { + if (value < -1) + { + throw Error.AsyncCommon_InvalidTimeout("value"); + } + _timeout = value; + } + } + + /// <summary> + /// The developer may call this function to signal that all operations are complete instead of + /// waiting for the operation counter to reach zero. + /// </summary> + public virtual void Finish() + { + EventHandler handler = Finished; + if (handler != null) + { + handler(this, EventArgs.Empty); + } + } + + /// <summary> + /// Executes a callback in the current synchronization context, which gives access to HttpContext and related items. + /// </summary> + /// <param name="action"></param> + public virtual void Sync(Action action) + { + _syncContext.Sync(action); + } + } +} diff --git a/src/System.Web.Mvc/Async/AsyncResultWrapper.cs b/src/System.Web.Mvc/Async/AsyncResultWrapper.cs new file mode 100644 index 00000000..8cc54b7c --- /dev/null +++ b/src/System.Web.Mvc/Async/AsyncResultWrapper.cs @@ -0,0 +1,303 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; + +namespace System.Web.Mvc.Async +{ + // This class is used for the following pattern: + + // public IAsyncResult BeginInner(..., callback, state); + // public TInnerResult EndInner(asyncResult); + // public IAsyncResult BeginOuter(..., callback, state); + // public TOuterResult EndOuter(asyncResult); + + // That is, Begin/EndOuter() wrap Begin/EndInner(), potentially with pre- and post-processing. + + [DebuggerNonUserCode] + internal static class AsyncResultWrapper + { + // helper methods + + private static Func<AsyncVoid> MakeVoidDelegate(Action action) + { + return () => + { + action(); + return default(AsyncVoid); + }; + } + + private static EndInvokeDelegate<AsyncVoid> MakeVoidDelegate(EndInvokeDelegate endDelegate) + { + return ar => + { + endDelegate(ar); + return default(AsyncVoid); + }; + } + + // kicks off an asynchronous operation + + public static IAsyncResult Begin<TResult>(AsyncCallback callback, object state, BeginInvokeDelegate beginDelegate, EndInvokeDelegate<TResult> endDelegate) + { + return Begin<TResult>(callback, state, beginDelegate, endDelegate, tag: null); + } + + public static IAsyncResult Begin<TResult>(AsyncCallback callback, object state, BeginInvokeDelegate beginDelegate, EndInvokeDelegate<TResult> endDelegate, object tag) + { + return Begin<TResult>(callback, state, beginDelegate, endDelegate, tag, Timeout.Infinite); + } + + public static IAsyncResult Begin<TResult>(AsyncCallback callback, object state, BeginInvokeDelegate beginDelegate, EndInvokeDelegate<TResult> endDelegate, object tag, int timeout) + { + WrappedAsyncResult<TResult> asyncResult = new WrappedAsyncResult<TResult>(beginDelegate, endDelegate, tag); + asyncResult.Begin(callback, state, timeout); + return asyncResult; + } + + public static IAsyncResult Begin(AsyncCallback callback, object state, BeginInvokeDelegate beginDelegate, EndInvokeDelegate endDelegate) + { + return Begin(callback, state, beginDelegate, endDelegate, tag: null); + } + + public static IAsyncResult Begin(AsyncCallback callback, object state, BeginInvokeDelegate beginDelegate, EndInvokeDelegate endDelegate, object tag) + { + return Begin(callback, state, beginDelegate, endDelegate, tag, Timeout.Infinite); + } + + public static IAsyncResult Begin(AsyncCallback callback, object state, BeginInvokeDelegate beginDelegate, EndInvokeDelegate endDelegate, object tag, int timeout) + { + return Begin<AsyncVoid>(callback, state, beginDelegate, MakeVoidDelegate(endDelegate), tag, timeout); + } + + // wraps a synchronous operation in an asynchronous wrapper, but still completes synchronously + + public static IAsyncResult BeginSynchronous<TResult>(AsyncCallback callback, object state, Func<TResult> func) + { + return BeginSynchronous<TResult>(callback, state, func, tag: null); + } + + public static IAsyncResult BeginSynchronous<TResult>(AsyncCallback callback, object state, Func<TResult> func, object tag) + { + // Begin() doesn't perform any work on its own and returns immediately. + BeginInvokeDelegate beginDelegate = (asyncCallback, asyncState) => + { + SimpleAsyncResult innerAsyncResult = new SimpleAsyncResult(asyncState); + innerAsyncResult.MarkCompleted(completedSynchronously: true, callback: asyncCallback); + return innerAsyncResult; + }; + + // The End() method blocks. + EndInvokeDelegate<TResult> endDelegate = _ => + { + return func(); + }; + + WrappedAsyncResult<TResult> asyncResult = new WrappedAsyncResult<TResult>(beginDelegate, endDelegate, tag); + asyncResult.Begin(callback, state, Timeout.Infinite); + return asyncResult; + } + + public static IAsyncResult BeginSynchronous(AsyncCallback callback, object state, Action action) + { + return BeginSynchronous(callback, state, action, tag: null); + } + + public static IAsyncResult BeginSynchronous(AsyncCallback callback, object state, Action action, object tag) + { + return BeginSynchronous<AsyncVoid>(callback, state, MakeVoidDelegate(action), tag); + } + + // completes an asynchronous operation + + public static TResult End<TResult>(IAsyncResult asyncResult) + { + return End<TResult>(asyncResult, tag: null); + } + + public static TResult End<TResult>(IAsyncResult asyncResult, object tag) + { + return WrappedAsyncResult<TResult>.Cast(asyncResult, tag).End(); + } + + public static void End(IAsyncResult asyncResult) + { + End(asyncResult, tag: null); + } + + public static void End(IAsyncResult asyncResult, object tag) + { + End<AsyncVoid>(asyncResult, tag); + } + + [DebuggerNonUserCode] + [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "The Timer will be disposed of either when it fires or when the operation completes successfully.")] + private sealed class WrappedAsyncResult<TResult> : IAsyncResult + { + private const int AsyncStateNone = 0; + private const int AsyncStateBeginUnwound = 1; + private const int AsyncStateCallbackFired = 2; + + private int _asyncState; + private readonly BeginInvokeDelegate _beginDelegate; + private readonly object _beginDelegateLockObj = new object(); + private readonly EndInvokeDelegate<TResult> _endDelegate; + private readonly SingleEntryGate _endExecutedGate = new SingleEntryGate(); // prevent End() from being called twice + private readonly SingleEntryGate _handleCallbackGate = new SingleEntryGate(); // prevent callback from being handled multiple times + private readonly object _tag; // prevent an instance of this type from being passed to the wrong End() method + private IAsyncResult _innerAsyncResult; + private AsyncCallback _originalCallback; + private volatile bool _timedOut; + private Timer _timer; + + public WrappedAsyncResult(BeginInvokeDelegate beginDelegate, EndInvokeDelegate<TResult> endDelegate, object tag) + { + _beginDelegate = beginDelegate; + _endDelegate = endDelegate; + _tag = tag; + } + + public object AsyncState + { + get { return _innerAsyncResult.AsyncState; } + } + + public WaitHandle AsyncWaitHandle + { + get { return _innerAsyncResult.AsyncWaitHandle; } + } + + public bool CompletedSynchronously { get; private set; } + + public bool IsCompleted + { + get { return _innerAsyncResult.IsCompleted; } + } + + // kicks off the process, instantiates a timer if requested + public void Begin(AsyncCallback callback, object state, int timeout) + { + _originalCallback = callback; + + // Force the target Begin() operation to complete before the callback can continue, + // since the target operation might perform post-processing of the data. + lock (_beginDelegateLockObj) + { + _innerAsyncResult = _beginDelegate(HandleAsynchronousCompletion, state); + + // If the callback has already fired, then the completion routine has no-oped and we + // can just treat this as if it were a normal synchronous completion. + int originalState = Interlocked.Exchange(ref _asyncState, AsyncStateBeginUnwound); + bool callbackAlreadyFired = (originalState == AsyncStateCallbackFired); + + CompletedSynchronously = callbackAlreadyFired || _innerAsyncResult.CompletedSynchronously; + + if (!CompletedSynchronously) + { + if (timeout > Timeout.Infinite) + { + CreateTimer(timeout); + } + } + } + + if (CompletedSynchronously) + { + if (callback != null) + { + callback(this); + } + } + } + + public static WrappedAsyncResult<TResult> Cast(IAsyncResult asyncResult, object tag) + { + if (asyncResult == null) + { + throw new ArgumentNullException("asyncResult"); + } + + WrappedAsyncResult<TResult> castResult = asyncResult as WrappedAsyncResult<TResult>; + if (castResult != null && Equals(castResult._tag, tag)) + { + return castResult; + } + else + { + throw Error.AsyncCommon_InvalidAsyncResult("asyncResult"); + } + } + + private void CreateTimer(int timeout) + { + // this method should be called within a lock(_beginDelegateLockObj) + _timer = new Timer(HandleTimeout, null, timeout, Timeout.Infinite /* disable periodic signaling */); + } + + public TResult End() + { + if (!_endExecutedGate.TryEnter()) + { + throw Error.AsyncCommon_AsyncResultAlreadyConsumed(); + } + + if (_timedOut) + { + throw new TimeoutException(); + } + WaitForBeginToCompleteAndDestroyTimer(); + + return _endDelegate(_innerAsyncResult); + } + + private void ExecuteAsynchronousCallback(bool timedOut) + { + WaitForBeginToCompleteAndDestroyTimer(); + + if (_handleCallbackGate.TryEnter()) + { + _timedOut = timedOut; + if (_originalCallback != null) + { + _originalCallback(this); + } + } + } + + private void HandleAsynchronousCompletion(IAsyncResult asyncResult) + { + // Transition the async state to CALLBACK_FIRED. If the Begin* method hasn't yet unwound, + // then we can no-op here since the Begin method will query the _asyncState field and + // treat this as a regular synchronous completion. + int originalState = Interlocked.Exchange(ref _asyncState, AsyncStateCallbackFired); + if (originalState != AsyncStateBeginUnwound) + { + return; + } + + ExecuteAsynchronousCallback(timedOut: false); + } + + private void HandleTimeout(object state) + { + ExecuteAsynchronousCallback(timedOut: true); + } + + private void WaitForBeginToCompleteAndDestroyTimer() + { + lock (_beginDelegateLockObj) + { + // Wait for the target Begin() method to complete, as it might be performing + // post-processing. This also forces a memory barrier, so _innerAsyncResult + // is guaranteed to be non-null at this point. + + if (_timer != null) + { + _timer.Dispose(); + } + _timer = null; + } + } + } + } +} diff --git a/src/System.Web.Mvc/Async/AsyncUtil.cs b/src/System.Web.Mvc/Async/AsyncUtil.cs new file mode 100644 index 00000000..4c75be0b --- /dev/null +++ b/src/System.Web.Mvc/Async/AsyncUtil.cs @@ -0,0 +1,31 @@ +using System.Threading; + +namespace System.Web.Mvc.Async +{ + internal static class AsyncUtil + { + public static AsyncCallback WrapCallbackForSynchronizedExecution(AsyncCallback callback, SynchronizationContext syncContext) + { + if (callback == null || syncContext == null) + { + return callback; + } + + AsyncCallback newCallback = delegate(IAsyncResult asyncResult) + { + if (asyncResult.CompletedSynchronously) + { + callback(asyncResult); + } + else + { + // Only take the application lock if this request completed asynchronously, + // else we might end up in a deadlock situation. + syncContext.Sync(() => callback(asyncResult)); + } + }; + + return newCallback; + } + } +} diff --git a/src/System.Web.Mvc/Async/AsyncVoid.cs b/src/System.Web.Mvc/Async/AsyncVoid.cs new file mode 100644 index 00000000..697438cc --- /dev/null +++ b/src/System.Web.Mvc/Async/AsyncVoid.cs @@ -0,0 +1,7 @@ +namespace System.Web.Mvc.Async +{ + // Dummy type used for passing something resembling 'void' to the async delegate functions + internal struct AsyncVoid + { + } +} diff --git a/src/System.Web.Mvc/Async/BeginInvokeDelegate.cs b/src/System.Web.Mvc/Async/BeginInvokeDelegate.cs new file mode 100644 index 00000000..4f119f4a --- /dev/null +++ b/src/System.Web.Mvc/Async/BeginInvokeDelegate.cs @@ -0,0 +1,4 @@ +namespace System.Web.Mvc.Async +{ + internal delegate IAsyncResult BeginInvokeDelegate(AsyncCallback callback, object state); +} diff --git a/src/System.Web.Mvc/Async/EndInvokeDelegate.cs b/src/System.Web.Mvc/Async/EndInvokeDelegate.cs new file mode 100644 index 00000000..fd037258 --- /dev/null +++ b/src/System.Web.Mvc/Async/EndInvokeDelegate.cs @@ -0,0 +1,4 @@ +namespace System.Web.Mvc.Async +{ + internal delegate void EndInvokeDelegate(IAsyncResult asyncResult); +} diff --git a/src/System.Web.Mvc/Async/EndInvokeDelegate`1.cs b/src/System.Web.Mvc/Async/EndInvokeDelegate`1.cs new file mode 100644 index 00000000..e2650680 --- /dev/null +++ b/src/System.Web.Mvc/Async/EndInvokeDelegate`1.cs @@ -0,0 +1,4 @@ +namespace System.Web.Mvc.Async +{ + internal delegate TResult EndInvokeDelegate<TResult>(IAsyncResult asyncResult); +} diff --git a/src/System.Web.Mvc/Async/IAsyncActionInvoker.cs b/src/System.Web.Mvc/Async/IAsyncActionInvoker.cs new file mode 100644 index 00000000..c9ee72c3 --- /dev/null +++ b/src/System.Web.Mvc/Async/IAsyncActionInvoker.cs @@ -0,0 +1,8 @@ +namespace System.Web.Mvc.Async +{ + public interface IAsyncActionInvoker : IActionInvoker + { + IAsyncResult BeginInvokeAction(ControllerContext controllerContext, string actionName, AsyncCallback callback, object state); + bool EndInvokeAction(IAsyncResult asyncResult); + } +} diff --git a/src/System.Web.Mvc/Async/IAsyncController.cs b/src/System.Web.Mvc/Async/IAsyncController.cs new file mode 100644 index 00000000..ce46fce4 --- /dev/null +++ b/src/System.Web.Mvc/Async/IAsyncController.cs @@ -0,0 +1,10 @@ +using System.Web.Routing; + +namespace System.Web.Mvc.Async +{ + public interface IAsyncController : IController + { + IAsyncResult BeginExecute(RequestContext requestContext, AsyncCallback callback, object state); + void EndExecute(IAsyncResult asyncResult); + } +} diff --git a/src/System.Web.Mvc/Async/IAsyncManagerContainer.cs b/src/System.Web.Mvc/Async/IAsyncManagerContainer.cs new file mode 100644 index 00000000..9e53fd12 --- /dev/null +++ b/src/System.Web.Mvc/Async/IAsyncManagerContainer.cs @@ -0,0 +1,7 @@ +namespace System.Web.Mvc.Async +{ + public interface IAsyncManagerContainer + { + AsyncManager AsyncManager { get; } + } +} diff --git a/src/System.Web.Mvc/Async/OperationCounter.cs b/src/System.Web.Mvc/Async/OperationCounter.cs new file mode 100644 index 00000000..9dbb63b5 --- /dev/null +++ b/src/System.Web.Mvc/Async/OperationCounter.cs @@ -0,0 +1,56 @@ +using System.Threading; + +namespace System.Web.Mvc.Async +{ + public sealed class OperationCounter + { + private int _count; + + public event EventHandler Completed; + + public int Count + { + get { return Thread.VolatileRead(ref _count); } + } + + private int AddAndExecuteCallbackIfCompleted(int value) + { + int newCount = Interlocked.Add(ref _count, value); + if (newCount == 0) + { + OnCompleted(); + } + + return newCount; + } + + public int Decrement() + { + return AddAndExecuteCallbackIfCompleted(-1); + } + + public int Decrement(int value) + { + return AddAndExecuteCallbackIfCompleted(-value); + } + + public int Increment() + { + return AddAndExecuteCallbackIfCompleted(1); + } + + public int Increment(int value) + { + return AddAndExecuteCallbackIfCompleted(value); + } + + private void OnCompleted() + { + EventHandler handler = Completed; + if (handler != null) + { + handler(this, EventArgs.Empty); + } + } + } +} diff --git a/src/System.Web.Mvc/Async/ReflectedAsyncActionDescriptor.cs b/src/System.Web.Mvc/Async/ReflectedAsyncActionDescriptor.cs new file mode 100644 index 00000000..b1380e84 --- /dev/null +++ b/src/System.Web.Mvc/Async/ReflectedAsyncActionDescriptor.cs @@ -0,0 +1,188 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading; + +namespace System.Web.Mvc.Async +{ + public class ReflectedAsyncActionDescriptor : AsyncActionDescriptor + { + private readonly object _executeTag = new object(); + + private readonly string _actionName; + private readonly ControllerDescriptor _controllerDescriptor; + private readonly Lazy<string> _uniqueId; + private ParameterDescriptor[] _parametersCache; + + public ReflectedAsyncActionDescriptor(MethodInfo asyncMethodInfo, MethodInfo completedMethodInfo, string actionName, ControllerDescriptor controllerDescriptor) + : this(asyncMethodInfo, completedMethodInfo, actionName, controllerDescriptor, true /* validateMethods */) + { + } + + internal ReflectedAsyncActionDescriptor(MethodInfo asyncMethodInfo, MethodInfo completedMethodInfo, string actionName, ControllerDescriptor controllerDescriptor, bool validateMethods) + { + if (asyncMethodInfo == null) + { + throw new ArgumentNullException("asyncMethodInfo"); + } + if (completedMethodInfo == null) + { + throw new ArgumentNullException("completedMethodInfo"); + } + if (String.IsNullOrEmpty(actionName)) + { + throw Error.ParameterCannotBeNullOrEmpty("actionName"); + } + if (controllerDescriptor == null) + { + throw new ArgumentNullException("controllerDescriptor"); + } + + if (validateMethods) + { + string asyncFailedMessage = VerifyActionMethodIsCallable(asyncMethodInfo); + if (asyncFailedMessage != null) + { + throw new ArgumentException(asyncFailedMessage, "asyncMethodInfo"); + } + + string completedFailedMessage = VerifyActionMethodIsCallable(completedMethodInfo); + if (completedFailedMessage != null) + { + throw new ArgumentException(completedFailedMessage, "completedMethodInfo"); + } + } + + AsyncMethodInfo = asyncMethodInfo; + CompletedMethodInfo = completedMethodInfo; + _actionName = actionName; + _controllerDescriptor = controllerDescriptor; + _uniqueId = new Lazy<string>(CreateUniqueId); + } + + public override string ActionName + { + get { return _actionName; } + } + + public MethodInfo AsyncMethodInfo { get; private set; } + + public MethodInfo CompletedMethodInfo { get; private set; } + + public override ControllerDescriptor ControllerDescriptor + { + get { return _controllerDescriptor; } + } + + public override string UniqueId + { + get { return _uniqueId.Value; } + } + + public override IAsyncResult BeginExecute(ControllerContext controllerContext, IDictionary<string, object> parameters, AsyncCallback callback, object state) + { + if (controllerContext == null) + { + throw new ArgumentNullException("controllerContext"); + } + if (parameters == null) + { + throw new ArgumentNullException("parameters"); + } + + AsyncManager asyncManager = GetAsyncManager(controllerContext.Controller); + + BeginInvokeDelegate beginDelegate = delegate(AsyncCallback asyncCallback, object asyncState) + { + // call the XxxAsync() method + ParameterInfo[] parameterInfos = AsyncMethodInfo.GetParameters(); + var rawParameterValues = from parameterInfo in parameterInfos + select ExtractParameterFromDictionary(parameterInfo, parameters, AsyncMethodInfo); + object[] parametersArray = rawParameterValues.ToArray(); + + TriggerListener listener = new TriggerListener(); + SimpleAsyncResult asyncResult = new SimpleAsyncResult(asyncState); + + // hook the Finished event to notify us upon completion + Trigger finishTrigger = listener.CreateTrigger(); + asyncManager.Finished += delegate + { + finishTrigger.Fire(); + }; + asyncManager.OutstandingOperations.Increment(); + + // to simplify the logic, force the rest of the pipeline to execute in an asynchronous callback + listener.SetContinuation(() => ThreadPool.QueueUserWorkItem(_ => asyncResult.MarkCompleted(false /* completedSynchronously */, asyncCallback))); + + // the inner operation might complete synchronously, so all setup work has to be done before this point + ActionMethodDispatcher dispatcher = DispatcherCache.GetDispatcher(AsyncMethodInfo); + dispatcher.Execute(controllerContext.Controller, parametersArray); // ignore return value from this method + + // now that the XxxAsync() method has completed, kick off any pending operations + asyncManager.OutstandingOperations.Decrement(); + listener.Activate(); + return asyncResult; + }; + + EndInvokeDelegate<object> endDelegate = delegate(IAsyncResult asyncResult) + { + // call the XxxCompleted() method + ParameterInfo[] completionParametersInfos = CompletedMethodInfo.GetParameters(); + var rawCompletionParameterValues = from parameterInfo in completionParametersInfos + select ExtractParameterOrDefaultFromDictionary(parameterInfo, asyncManager.Parameters); + object[] completionParametersArray = rawCompletionParameterValues.ToArray(); + + ActionMethodDispatcher dispatcher = DispatcherCache.GetDispatcher(CompletedMethodInfo); + object actionReturnValue = dispatcher.Execute(controllerContext.Controller, completionParametersArray); + return actionReturnValue; + }; + + return AsyncResultWrapper.Begin(callback, state, beginDelegate, endDelegate, _executeTag, asyncManager.Timeout); + } + + private string CreateUniqueId() + { + return base.UniqueId + DescriptorUtil.CreateUniqueId(AsyncMethodInfo, CompletedMethodInfo); + } + + public override object EndExecute(IAsyncResult asyncResult) + { + return AsyncResultWrapper.End<object>(asyncResult, _executeTag); + } + + public override object[] GetCustomAttributes(bool inherit) + { + return ActionDescriptorHelper.GetCustomAttributes(AsyncMethodInfo, inherit); + } + + public override object[] GetCustomAttributes(Type attributeType, bool inherit) + { + return ActionDescriptorHelper.GetCustomAttributes(AsyncMethodInfo, attributeType, inherit); + } + + public override IEnumerable<FilterAttribute> GetFilterAttributes(bool useCache) + { + if (useCache && GetType() == typeof(ReflectedAsyncActionDescriptor)) + { + // Do not look at cache in types derived from this type because they might incorrectly implement GetCustomAttributes + return ReflectedAttributeCache.GetMethodFilterAttributes(AsyncMethodInfo); + } + return base.GetFilterAttributes(useCache); + } + + public override ParameterDescriptor[] GetParameters() + { + return ActionDescriptorHelper.GetParameters(this, AsyncMethodInfo, ref _parametersCache); + } + + public override ICollection<ActionSelector> GetSelectors() + { + return ActionDescriptorHelper.GetSelectors(AsyncMethodInfo); + } + + public override bool IsDefined(Type attributeType, bool inherit) + { + return ActionDescriptorHelper.IsDefined(AsyncMethodInfo, attributeType, inherit); + } + } +} diff --git a/src/System.Web.Mvc/Async/ReflectedAsyncControllerDescriptor.cs b/src/System.Web.Mvc/Async/ReflectedAsyncControllerDescriptor.cs new file mode 100644 index 00000000..de60cdbf --- /dev/null +++ b/src/System.Web.Mvc/Async/ReflectedAsyncControllerDescriptor.cs @@ -0,0 +1,104 @@ +using System.Collections.Generic; + +namespace System.Web.Mvc.Async +{ + public class ReflectedAsyncControllerDescriptor : ControllerDescriptor + { + private static readonly ActionDescriptor[] _emptyCanonicalActions = new ActionDescriptor[0]; + + private readonly Type _controllerType; + private readonly AsyncActionMethodSelector _selector; + + public ReflectedAsyncControllerDescriptor(Type controllerType) + { + if (controllerType == null) + { + throw new ArgumentNullException("controllerType"); + } + + _controllerType = controllerType; + bool allowLegacyAsyncActions = AllowLegacyAsyncActions(_controllerType); + _selector = new AsyncActionMethodSelector(_controllerType, allowLegacyAsyncActions); + } + + public sealed override Type ControllerType + { + get { return _controllerType; } + } + + /// <summary> + /// Determines if we should bind "Foo" to FooAsync/FooCompleted pattern. + /// </summary> + /// <param name="controllerType"></param> + /// <returns></returns> + private static bool AllowLegacyAsyncActions(Type controllerType) + { + if (typeof(AsyncController).IsAssignableFrom(controllerType)) + { + return true; + } + if (typeof(Controller).IsAssignableFrom(controllerType)) + { + // for backwards compat. Controller now supports IAsyncController, + // but still use synchronous bindings patterns. + return false; + } + if (!typeof(IAsyncController).IsAssignableFrom(controllerType)) + { + return false; + } + return true; + } + + public override ActionDescriptor FindAction(ControllerContext controllerContext, string actionName) + { + if (controllerContext == null) + { + throw new ArgumentNullException("controllerContext"); + } + if (String.IsNullOrEmpty(actionName)) + { + throw Error.ParameterCannotBeNullOrEmpty("actionName"); + } + + ActionDescriptorCreator creator = _selector.FindAction(controllerContext, actionName); + if (creator == null) + { + return null; + } + + return creator(actionName, this); + } + + public override ActionDescriptor[] GetCanonicalActions() + { + // everything is looked up dymanically, so there are no 'canonical' actions + return _emptyCanonicalActions; + } + + public override object[] GetCustomAttributes(bool inherit) + { + return ControllerType.GetCustomAttributes(inherit); + } + + public override object[] GetCustomAttributes(Type attributeType, bool inherit) + { + return ControllerType.GetCustomAttributes(attributeType, inherit); + } + + public override IEnumerable<FilterAttribute> GetFilterAttributes(bool useCache) + { + if (useCache && GetType() == typeof(ReflectedAsyncControllerDescriptor)) + { + // Do not look at cache in types derived from this type because they might incorrectly implement GetCustomAttributes + return ReflectedAttributeCache.GetTypeFilterAttributes(ControllerType); + } + return base.GetFilterAttributes(useCache); + } + + public override bool IsDefined(Type attributeType, bool inherit) + { + return ControllerType.IsDefined(attributeType, inherit); + } + } +} diff --git a/src/System.Web.Mvc/Async/SimpleAsyncResult.cs b/src/System.Web.Mvc/Async/SimpleAsyncResult.cs new file mode 100644 index 00000000..3e4df09a --- /dev/null +++ b/src/System.Web.Mvc/Async/SimpleAsyncResult.cs @@ -0,0 +1,53 @@ +using System.Threading; + +namespace System.Web.Mvc.Async +{ + internal sealed class SimpleAsyncResult : IAsyncResult + { + private readonly object _asyncState; + private bool _completedSynchronously; + private volatile bool _isCompleted; + + public SimpleAsyncResult(object asyncState) + { + _asyncState = asyncState; + } + + public object AsyncState + { + get { return _asyncState; } + } + + // ASP.NET IAsyncResult objects should never expose a WaitHandle due to potential deadlocking + public WaitHandle AsyncWaitHandle + { + get { return null; } + } + + public bool CompletedSynchronously + { + get { return _completedSynchronously; } + } + + public bool IsCompleted + { + get { return _isCompleted; } + } + + // Proper order of execution: + // 1. Set the CompletedSynchronously property to the correct value + // 2. Set the IsCompleted flag + // 3. Execute the callback + // 4. Signal the WaitHandle (which we don't have) + public void MarkCompleted(bool completedSynchronously, AsyncCallback callback) + { + _completedSynchronously = completedSynchronously; + _isCompleted = true; + + if (callback != null) + { + callback(this); + } + } + } +} diff --git a/src/System.Web.Mvc/Async/SingleEntryGate.cs b/src/System.Web.Mvc/Async/SingleEntryGate.cs new file mode 100644 index 00000000..746593b4 --- /dev/null +++ b/src/System.Web.Mvc/Async/SingleEntryGate.cs @@ -0,0 +1,20 @@ +using System.Threading; + +namespace System.Web.Mvc.Async +{ + // used to synchronize access to a single-use consumable resource + internal sealed class SingleEntryGate + { + private const int NotEntered = 0; + private const int Entered = 1; + + private int _status; + + // returns true if this is the first call to TryEnter(), false otherwise + public bool TryEnter() + { + int oldStatus = Interlocked.Exchange(ref _status, Entered); + return (oldStatus == NotEntered); + } + } +} diff --git a/src/System.Web.Mvc/Async/SynchronizationContextUtil.cs b/src/System.Web.Mvc/Async/SynchronizationContextUtil.cs new file mode 100644 index 00000000..f64a9f6b --- /dev/null +++ b/src/System.Web.Mvc/Async/SynchronizationContextUtil.cs @@ -0,0 +1,52 @@ +using System.Diagnostics.CodeAnalysis; +using System.Threading; + +namespace System.Web.Mvc.Async +{ + internal static class SynchronizationContextUtil + { + public static SynchronizationContext GetSynchronizationContext() + { + // In a runtime environment, SynchronizationContext.Current will be set to an instance + // of AspNetSynchronizationContext. In a unit test environment, the Current property + // won't be set and we have to create one on the fly. + return SynchronizationContext.Current ?? new SynchronizationContext(); + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The exception is swallowed and immediately re-thrown")] + public static T Sync<T>(this SynchronizationContext syncContext, Func<T> func) + { + T theValue = default(T); + Exception thrownException = null; + + syncContext.Send(o => + { + try + { + theValue = func(); + } + catch (Exception ex) + { + // by default, the AspNetSynchronizationContext type will swallow thrown exceptions, + // so we need to save and propagate them + thrownException = ex; + } + }, null); + + if (thrownException != null) + { + throw Error.SynchronizationContextUtil_ExceptionThrown(thrownException); + } + return theValue; + } + + public static void Sync(this SynchronizationContext syncContext, Action action) + { + Sync<AsyncVoid>(syncContext, () => + { + action(); + return default(AsyncVoid); + }); + } + } +} diff --git a/src/System.Web.Mvc/Async/SynchronousOperationException.cs b/src/System.Web.Mvc/Async/SynchronousOperationException.cs new file mode 100644 index 00000000..b7ac88e1 --- /dev/null +++ b/src/System.Web.Mvc/Async/SynchronousOperationException.cs @@ -0,0 +1,30 @@ +using System.Runtime.Serialization; + +namespace System.Web.Mvc.Async +{ + // This exception type is thrown by the SynchronizationContextUtil helper class since the AspNetSynchronizationContext + // type swallows exceptions. The inner exception contains the data the user cares about. + + [Serializable] + public sealed class SynchronousOperationException : HttpException + { + public SynchronousOperationException() + { + } + + private SynchronousOperationException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + public SynchronousOperationException(string message) + : base(message) + { + } + + public SynchronousOperationException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/src/System.Web.Mvc/Async/TaskAsyncActionDescriptor.cs b/src/System.Web.Mvc/Async/TaskAsyncActionDescriptor.cs new file mode 100644 index 00000000..22e8e8b1 --- /dev/null +++ b/src/System.Web.Mvc/Async/TaskAsyncActionDescriptor.cs @@ -0,0 +1,264 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc.Async +{ + /// <summary> + /// When an action method returns either Task or Task{T} the TaskAsyncActionDescriptor provides information about the action. + /// </summary> + public class TaskAsyncActionDescriptor : AsyncActionDescriptor + { + /// <summary> + /// dictionary to hold methods that can read Task{T}.Result + /// </summary> + private static readonly ConcurrentDictionary<Type, Func<object, object>> _taskValueExtractors = new ConcurrentDictionary<Type, Func<object, object>>(); + private readonly string _actionName; + private readonly ControllerDescriptor _controllerDescriptor; + private readonly Lazy<string> _uniqueId; + private ParameterDescriptor[] _parametersCache; + + public TaskAsyncActionDescriptor(MethodInfo taskMethodInfo, string actionName, ControllerDescriptor controllerDescriptor) + : this(taskMethodInfo, actionName, controllerDescriptor, validateMethod: true) + { + } + + internal TaskAsyncActionDescriptor(MethodInfo taskMethodInfo, string actionName, ControllerDescriptor controllerDescriptor, bool validateMethod) + { + if (taskMethodInfo == null) + { + throw new ArgumentNullException("taskMethodInfo"); + } + if (String.IsNullOrEmpty(actionName)) + { + throw Error.ParameterCannotBeNullOrEmpty("actionName"); + } + if (controllerDescriptor == null) + { + throw new ArgumentNullException("controllerDescriptor"); + } + + if (validateMethod) + { + string taskFailedMessage = VerifyActionMethodIsCallable(taskMethodInfo); + if (taskFailedMessage != null) + { + throw new ArgumentException(taskFailedMessage, "taskMethodInfo"); + } + } + + TaskMethodInfo = taskMethodInfo; + _actionName = actionName; + _controllerDescriptor = controllerDescriptor; + _uniqueId = new Lazy<string>(CreateUniqueId); + } + + public override string ActionName + { + get { return _actionName; } + } + + public MethodInfo TaskMethodInfo { get; private set; } + + public override ControllerDescriptor ControllerDescriptor + { + get { return _controllerDescriptor; } + } + + public override string UniqueId + { + get { return _uniqueId.Value; } + } + + private string CreateUniqueId() + { + return base.UniqueId + DescriptorUtil.CreateUniqueId(TaskMethodInfo); + } + + public override IAsyncResult BeginExecute(ControllerContext controllerContext, IDictionary<string, object> parameters, AsyncCallback callback, object state) + { + if (controllerContext == null) + { + throw new ArgumentNullException("controllerContext"); + } + if (parameters == null) + { + throw new ArgumentNullException("parameters"); + } + + ParameterInfo[] parameterInfos = TaskMethodInfo.GetParameters(); + var rawParameterValues = from parameterInfo in parameterInfos + select ExtractParameterFromDictionary(parameterInfo, parameters, TaskMethodInfo); + object[] parametersArray = rawParameterValues.ToArray(); + + CancellationTokenSource tokenSource = null; + bool disposedTimer = false; + Timer taskCancelledTimer = null; + bool taskCancelledTimerRequired = false; + + int timeout = GetAsyncManager(controllerContext.Controller).Timeout; + + for (int i = 0; i < parametersArray.Length; i++) + { + if (default(CancellationToken).Equals(parametersArray[i])) + { + tokenSource = new CancellationTokenSource(); + parametersArray[i] = tokenSource.Token; + + // If there is a timeout we will create a timer to cancel the task when the + // timeout expires. + taskCancelledTimerRequired = timeout > Timeout.Infinite; + break; + } + } + + ActionMethodDispatcher dispatcher = DispatcherCache.GetDispatcher(TaskMethodInfo); + + if (taskCancelledTimerRequired) + { + taskCancelledTimer = new Timer(_ => + { + lock (tokenSource) + { + if (!disposedTimer) + { + tokenSource.Cancel(); + } + } + }, + state: null, dueTime: timeout, period: Timeout.Infinite); + } + + Task taskUser = dispatcher.Execute(controllerContext.Controller, parametersArray) as Task; + Action cleanupAtEndExecute = () => + { + // Cleanup code that's run in EndExecute, after we've waited on the task value. + + if (taskCancelledTimer != null) + { + // Timer callback may still fire after Dispose is called. + taskCancelledTimer.Dispose(); + } + + if (tokenSource != null) + { + lock (tokenSource) + { + disposedTimer = true; + tokenSource.Dispose(); + if (tokenSource.IsCancellationRequested) + { + // Give Timeout exceptions higher priority over other exceptions, mainly OperationCancelled exceptions + // that were signaled with out timeout token. + throw new TimeoutException(); + } + } + } + }; + + TaskWrapperAsyncResult result = new TaskWrapperAsyncResult(taskUser, state, cleanupAtEndExecute); + + // if user supplied a callback, invoke that when their task has finished running. + if (callback != null) + { + taskUser.Finally(() => + { + callback(result); + }); + } + + return result; + } + + public override object Execute(ControllerContext controllerContext, IDictionary<string, object> parameters) + { + string errorMessage = String.Format(CultureInfo.CurrentCulture, MvcResources.TaskAsyncActionDescriptor_CannotExecuteSynchronously, + ActionName); + + throw new InvalidOperationException(errorMessage); + } + + public override object EndExecute(IAsyncResult asyncResult) + { + TaskWrapperAsyncResult wrapperResult = (TaskWrapperAsyncResult)asyncResult; + + // Throw an exception with the correct call stack + try + { + wrapperResult.Task.ThrowIfFaulted(); + } + finally + { + if (wrapperResult.CleanupThunk != null) + { + wrapperResult.CleanupThunk(); + } + } + + // Extract the result of the task if there is a result + return _taskValueExtractors.GetOrAdd(TaskMethodInfo.ReturnType, CreateTaskValueExtractor)(wrapperResult.Task); + } + + private static Func<object, object> CreateTaskValueExtractor(Type taskType) + { + // Task<T>? + if (taskType.IsGenericType && taskType.GetGenericTypeDefinition() == typeof(Task<>)) + { + // lambda = arg => (object)(((Task<T>)arg).Result) + var arg = Expression.Parameter(typeof(object)); + var castArg = Expression.Convert(arg, taskType); + var fieldAccess = Expression.Property(castArg, "Result"); + var castResult = Expression.Convert(fieldAccess, typeof(object)); + var lambda = Expression.Lambda<Func<object, object>>(castResult, arg); + return lambda.Compile(); + } + + // Any exceptions should be thrown before getting the task value so just return null. + return theTask => + { + return null; + }; + } + + public override object[] GetCustomAttributes(bool inherit) + { + return ActionDescriptorHelper.GetCustomAttributes(TaskMethodInfo, inherit); + } + + public override object[] GetCustomAttributes(Type attributeType, bool inherit) + { + return ActionDescriptorHelper.GetCustomAttributes(TaskMethodInfo, attributeType, inherit); + } + + public override ParameterDescriptor[] GetParameters() + { + return ActionDescriptorHelper.GetParameters(this, TaskMethodInfo, ref _parametersCache); + } + + public override ICollection<ActionSelector> GetSelectors() + { + return ActionDescriptorHelper.GetSelectors(TaskMethodInfo); + } + + public override bool IsDefined(Type attributeType, bool inherit) + { + return ActionDescriptorHelper.IsDefined(TaskMethodInfo, attributeType, inherit); + } + + public override IEnumerable<FilterAttribute> GetFilterAttributes(bool useCache) + { + if (useCache && GetType() == typeof(TaskAsyncActionDescriptor)) + { + // Do not look at cache in types derived from this type because they might incorrectly implement GetCustomAttributes + return ReflectedAttributeCache.GetMethodFilterAttributes(TaskMethodInfo); + } + return base.GetFilterAttributes(useCache); + } + } +} diff --git a/src/System.Web.Mvc/Async/TaskWrapperAsyncResult.cs b/src/System.Web.Mvc/Async/TaskWrapperAsyncResult.cs new file mode 100644 index 00000000..a9d31a94 --- /dev/null +++ b/src/System.Web.Mvc/Async/TaskWrapperAsyncResult.cs @@ -0,0 +1,43 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace System.Web.Mvc.Async +{ + /// <summary> + /// Wraps a <see cref="Task"/> class, optionally overriding the State object (since the Task Asynchronous Pattern doesn't normally use it). + /// Copied from System.Web. + /// </summary> + internal sealed class TaskWrapperAsyncResult : IAsyncResult + { + internal TaskWrapperAsyncResult(Task task, object asyncState, Action cleanupThunk = null) + { + Task = task; + AsyncState = asyncState; + CleanupThunk = cleanupThunk; + } + + public object AsyncState { get; private set; } + + public WaitHandle AsyncWaitHandle + { + get { return ((IAsyncResult)Task).AsyncWaitHandle; } + } + + /// <summary> + /// Cleanup logic to run after Task is finished + /// </summary> + public Action CleanupThunk { get; private set; } + + public bool CompletedSynchronously + { + get { return ((IAsyncResult)Task).CompletedSynchronously; } + } + + public bool IsCompleted + { + get { return ((IAsyncResult)Task).IsCompleted; } + } + + internal Task Task { get; private set; } + } +} diff --git a/src/System.Web.Mvc/Async/Trigger.cs b/src/System.Web.Mvc/Async/Trigger.cs new file mode 100644 index 00000000..53efafbb --- /dev/null +++ b/src/System.Web.Mvc/Async/Trigger.cs @@ -0,0 +1,20 @@ +namespace System.Web.Mvc.Async +{ + // Provides a trigger for the TriggerListener class. + + internal sealed class Trigger + { + private readonly Action _fireAction; + + // Constructor should only be called by TriggerListener. + internal Trigger(Action fireAction) + { + _fireAction = fireAction; + } + + public void Fire() + { + _fireAction(); + } + } +} diff --git a/src/System.Web.Mvc/Async/TriggerListener.cs b/src/System.Web.Mvc/Async/TriggerListener.cs new file mode 100644 index 00000000..84b9a578 --- /dev/null +++ b/src/System.Web.Mvc/Async/TriggerListener.cs @@ -0,0 +1,65 @@ +using System.Threading; + +namespace System.Web.Mvc.Async +{ + // This class is used to wait for triggers and a continuation. When the continuation has been provded + // and all triggers have been fired, the continuation is called. Similar to WaitHandle.WaitAll(). + // New instances of this type are initially in the inactive state; activation is enabled by a call + // to Activate(). + + // This class is thread-safe. + + internal sealed class TriggerListener + { + private readonly Trigger _activateTrigger; + private readonly SingleEntryGate _continuationFiredGate = new SingleEntryGate(); + private readonly Trigger _setContinuationTrigger; + private volatile Action _continuation; + private int _outstandingTriggers; + + public TriggerListener() + { + _activateTrigger = CreateTrigger(); + _setContinuationTrigger = CreateTrigger(); + } + + public void Activate() + { + _activateTrigger.Fire(); + } + + public Trigger CreateTrigger() + { + Interlocked.Increment(ref _outstandingTriggers); + + SingleEntryGate triggerFiredGate = new SingleEntryGate(); + return new Trigger(() => + { + if (triggerFiredGate.TryEnter()) + { + HandleTriggerFired(); + } + }); + } + + private void HandleTriggerFired() + { + if (Interlocked.Decrement(ref _outstandingTriggers) == 0) + { + if (_continuationFiredGate.TryEnter()) + { + _continuation(); + } + } + } + + public void SetContinuation(Action continuation) + { + if (continuation != null) + { + _continuation = continuation; + _setContinuationTrigger.Fire(); + } + } + } +} diff --git a/src/System.Web.Mvc/AsyncController.cs b/src/System.Web.Mvc/AsyncController.cs new file mode 100644 index 00000000..91e6c165 --- /dev/null +++ b/src/System.Web.Mvc/AsyncController.cs @@ -0,0 +1,10 @@ +namespace System.Web.Mvc +{ + // Controller now supports asynchronous operations. + // This class only exists + // a) for backwards compat for callers that derive from it, + // b) ActionMethodSelector can detect it to bind to ActionAsync/ActionCompleted patterns. + public abstract class AsyncController : Controller + { + } +} diff --git a/src/System.Web.Mvc/AsyncTimeoutAttribute.cs b/src/System.Web.Mvc/AsyncTimeoutAttribute.cs new file mode 100644 index 00000000..4933269b --- /dev/null +++ b/src/System.Web.Mvc/AsyncTimeoutAttribute.cs @@ -0,0 +1,41 @@ +using System.Diagnostics.CodeAnalysis; +using System.Web.Mvc.Async; + +namespace System.Web.Mvc +{ + [SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "Unsealed so that subclassed types can set properties in the default constructor.")] + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)] + public class AsyncTimeoutAttribute : ActionFilterAttribute + { + // duration is specified in milliseconds + public AsyncTimeoutAttribute(int duration) + { + if (duration < -1) + { + throw Error.AsyncCommon_InvalidTimeout("duration"); + } + + Duration = duration; + } + + public int Duration { get; private set; } + + public override void OnActionExecuting(ActionExecutingContext filterContext) + { + if (filterContext == null) + { + throw new ArgumentNullException("filterContext"); + } + + IAsyncManagerContainer container = filterContext.Controller as IAsyncManagerContainer; + if (container == null) + { + throw Error.AsyncCommon_ControllerMustImplementIAsyncManagerContainer(filterContext.Controller.GetType()); + } + + container.AsyncManager.Timeout = Duration; + + base.OnActionExecuting(filterContext); + } + } +} diff --git a/src/System.Web.Mvc/AuthorizationContext.cs b/src/System.Web.Mvc/AuthorizationContext.cs new file mode 100644 index 00000000..a6546b5b --- /dev/null +++ b/src/System.Web.Mvc/AuthorizationContext.cs @@ -0,0 +1,34 @@ +using System.Diagnostics.CodeAnalysis; + +namespace System.Web.Mvc +{ + public class AuthorizationContext : ControllerContext + { + // parameterless constructor used for mocking + public AuthorizationContext() + { + } + + [Obsolete("The recommended alternative is the constructor AuthorizationContext(ControllerContext controllerContext, ActionDescriptor actionDescriptor).")] + public AuthorizationContext(ControllerContext controllerContext) + : base(controllerContext) + { + } + + [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "The virtual property setters are only to support mocking frameworks, in which case this constructor shouldn't be called anyway.")] + public AuthorizationContext(ControllerContext controllerContext, ActionDescriptor actionDescriptor) + : base(controllerContext) + { + if (actionDescriptor == null) + { + throw new ArgumentNullException("actionDescriptor"); + } + + ActionDescriptor = actionDescriptor; + } + + public virtual ActionDescriptor ActionDescriptor { get; set; } + + public ActionResult Result { get; set; } + } +} diff --git a/src/System.Web.Mvc/AuthorizeAttribute.cs b/src/System.Web.Mvc/AuthorizeAttribute.cs new file mode 100644 index 00000000..434b94e1 --- /dev/null +++ b/src/System.Web.Mvc/AuthorizeAttribute.cs @@ -0,0 +1,152 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Security.Principal; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + [SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "Unsealed so that subclassed types can set properties in the default constructor or override our behavior.")] + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)] + public class AuthorizeAttribute : FilterAttribute, IAuthorizationFilter + { + private readonly object _typeId = new object(); + + private string _roles; + private string[] _rolesSplit = new string[0]; + private string _users; + private string[] _usersSplit = new string[0]; + + public string Roles + { + get { return _roles ?? String.Empty; } + set + { + _roles = value; + _rolesSplit = SplitString(value); + } + } + + public override object TypeId + { + get { return _typeId; } + } + + public string Users + { + get { return _users ?? String.Empty; } + set + { + _users = value; + _usersSplit = SplitString(value); + } + } + + // This method must be thread-safe since it is called by the thread-safe OnCacheAuthorization() method. + protected virtual bool AuthorizeCore(HttpContextBase httpContext) + { + if (httpContext == null) + { + throw new ArgumentNullException("httpContext"); + } + + IPrincipal user = httpContext.User; + if (!user.Identity.IsAuthenticated) + { + return false; + } + + if (_usersSplit.Length > 0 && !_usersSplit.Contains(user.Identity.Name, StringComparer.OrdinalIgnoreCase)) + { + return false; + } + + if (_rolesSplit.Length > 0 && !_rolesSplit.Any(user.IsInRole)) + { + return false; + } + + return true; + } + + private void CacheValidateHandler(HttpContext context, object data, ref HttpValidationStatus validationStatus) + { + validationStatus = OnCacheAuthorization(new HttpContextWrapper(context)); + } + + public virtual void OnAuthorization(AuthorizationContext filterContext) + { + if (filterContext == null) + { + throw new ArgumentNullException("filterContext"); + } + + if (OutputCacheAttribute.IsChildActionCacheActive(filterContext)) + { + // If a child action cache block is active, we need to fail immediately, even if authorization + // would have succeeded. The reason is that there's no way to hook a callback to rerun + // authorization before the fragment is served from the cache, so we can't guarantee that this + // filter will be re-run on subsequent requests. + throw new InvalidOperationException(MvcResources.AuthorizeAttribute_CannotUseWithinChildActionCache); + } + + bool skipAuthorization = filterContext.ActionDescriptor.IsDefined(typeof(AllowAnonymousAttribute), inherit: true) + || filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowAnonymousAttribute), inherit: true); + + if (skipAuthorization) + { + return; + } + + if (AuthorizeCore(filterContext.HttpContext)) + { + // ** IMPORTANT ** + // Since we're performing authorization at the action level, the authorization code runs + // after the output caching module. In the worst case this could allow an authorized user + // to cause the page to be cached, then an unauthorized user would later be served the + // cached page. We work around this by telling proxies not to cache the sensitive page, + // then we hook our custom authorization code into the caching mechanism so that we have + // the final say on whether a page should be served from the cache. + + HttpCachePolicyBase cachePolicy = filterContext.HttpContext.Response.Cache; + cachePolicy.SetProxyMaxAge(new TimeSpan(0)); + cachePolicy.AddValidationCallback(CacheValidateHandler, null /* data */); + } + else + { + HandleUnauthorizedRequest(filterContext); + } + } + + protected virtual void HandleUnauthorizedRequest(AuthorizationContext filterContext) + { + // Returns HTTP 401 - see comment in HttpUnauthorizedResult.cs. + filterContext.Result = new HttpUnauthorizedResult(); + } + + // This method must be thread-safe since it is called by the caching module. + protected virtual HttpValidationStatus OnCacheAuthorization(HttpContextBase httpContext) + { + if (httpContext == null) + { + throw new ArgumentNullException("httpContext"); + } + + bool isAuthorized = AuthorizeCore(httpContext); + return (isAuthorized) ? HttpValidationStatus.Valid : HttpValidationStatus.IgnoreThisRequest; + } + + internal static string[] SplitString(string original) + { + if (String.IsNullOrEmpty(original)) + { + return new string[0]; + } + + var split = from piece in original.Split(',') + let trimmed = piece.Trim() + where !String.IsNullOrEmpty(trimmed) + select trimmed; + return split.ToArray(); + } + } +} diff --git a/src/System.Web.Mvc/BindAttribute.cs b/src/System.Web.Mvc/BindAttribute.cs new file mode 100644 index 00000000..55165e42 --- /dev/null +++ b/src/System.Web.Mvc/BindAttribute.cs @@ -0,0 +1,50 @@ +using System.Linq; + +namespace System.Web.Mvc +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] + public sealed class BindAttribute : Attribute + { + private string _exclude; + private string[] _excludeSplit = new string[0]; + private string _include; + private string[] _includeSplit = new string[0]; + + public string Exclude + { + get { return _exclude ?? String.Empty; } + set + { + _exclude = value; + _excludeSplit = AuthorizeAttribute.SplitString(value); + } + } + + public string Include + { + get { return _include ?? String.Empty; } + set + { + _include = value; + _includeSplit = AuthorizeAttribute.SplitString(value); + } + } + + public string Prefix { get; set; } + + internal static bool IsPropertyAllowed(string propertyName, string[] includeProperties, string[] excludeProperties) + { + // We allow a property to be bound if its both in the include list AND not in the exclude list. + // An empty include list implies all properties are allowed. + // An empty exclude list implies no properties are disallowed. + bool includeProperty = (includeProperties == null) || (includeProperties.Length == 0) || includeProperties.Contains(propertyName, StringComparer.OrdinalIgnoreCase); + bool excludeProperty = (excludeProperties != null) && excludeProperties.Contains(propertyName, StringComparer.OrdinalIgnoreCase); + return includeProperty && !excludeProperty; + } + + public bool IsPropertyAllowed(string propertyName) + { + return IsPropertyAllowed(propertyName, _includeSplit, _excludeSplit); + } + } +} diff --git a/src/System.Web.Mvc/BuildManagerCompiledView.cs b/src/System.Web.Mvc/BuildManagerCompiledView.cs new file mode 100644 index 00000000..a7edef36 --- /dev/null +++ b/src/System.Web.Mvc/BuildManagerCompiledView.cs @@ -0,0 +1,85 @@ +using System.Globalization; +using System.IO; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + public abstract class BuildManagerCompiledView : IView + { + internal IViewPageActivator ViewPageActivator; + private IBuildManager _buildManager; + private ControllerContext _controllerContext; + + protected BuildManagerCompiledView(ControllerContext controllerContext, string viewPath) + : this(controllerContext, viewPath, null) + { + } + + protected BuildManagerCompiledView(ControllerContext controllerContext, string viewPath, IViewPageActivator viewPageActivator) + : this(controllerContext, viewPath, viewPageActivator, null) + { + } + + internal BuildManagerCompiledView(ControllerContext controllerContext, string viewPath, IViewPageActivator viewPageActivator, IDependencyResolver dependencyResolver) + { + if (controllerContext == null) + { + throw new ArgumentNullException("controllerContext"); + } + if (String.IsNullOrEmpty(viewPath)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "viewPath"); + } + + _controllerContext = controllerContext; + + ViewPath = viewPath; + + ViewPageActivator = viewPageActivator ?? new BuildManagerViewEngine.DefaultViewPageActivator(dependencyResolver); + } + + internal IBuildManager BuildManager + { + get + { + if (_buildManager == null) + { + _buildManager = new BuildManagerWrapper(); + } + return _buildManager; + } + set { _buildManager = value; } + } + + public string ViewPath { get; protected set; } + + public void Render(ViewContext viewContext, TextWriter writer) + { + if (viewContext == null) + { + throw new ArgumentNullException("viewContext"); + } + + object instance = null; + + Type type = BuildManager.GetCompiledType(ViewPath); + if (type != null) + { + instance = ViewPageActivator.Create(_controllerContext, type); + } + + if (instance == null) + { + throw new InvalidOperationException( + String.Format( + CultureInfo.CurrentCulture, + MvcResources.CshtmlView_ViewCouldNotBeCreated, + ViewPath)); + } + + RenderView(viewContext, writer, instance); + } + + protected abstract void RenderView(ViewContext viewContext, TextWriter writer, object instance); + } +} diff --git a/src/System.Web.Mvc/BuildManagerViewEngine.cs b/src/System.Web.Mvc/BuildManagerViewEngine.cs new file mode 100644 index 00000000..cbc4efc3 --- /dev/null +++ b/src/System.Web.Mvc/BuildManagerViewEngine.cs @@ -0,0 +1,92 @@ +namespace System.Web.Mvc +{ + public abstract class BuildManagerViewEngine : VirtualPathProviderViewEngine + { + private IBuildManager _buildManager; + private IViewPageActivator _viewPageActivator; + private IResolver<IViewPageActivator> _activatorResolver; + + protected BuildManagerViewEngine() + : this(null, null, null) + { + } + + protected BuildManagerViewEngine(IViewPageActivator viewPageActivator) + : this(viewPageActivator, null, null) + { + } + + internal BuildManagerViewEngine(IViewPageActivator viewPageActivator, IResolver<IViewPageActivator> activatorResolver, IDependencyResolver dependencyResolver) + { + if (viewPageActivator != null) + { + _viewPageActivator = viewPageActivator; + } + else + { + _activatorResolver = activatorResolver ?? new SingleServiceResolver<IViewPageActivator>( + () => null, + new DefaultViewPageActivator(dependencyResolver), + "BuildManagerViewEngine constructor"); + } + } + + internal IBuildManager BuildManager + { + get + { + if (_buildManager == null) + { + _buildManager = new BuildManagerWrapper(); + } + return _buildManager; + } + set { _buildManager = value; } + } + + protected IViewPageActivator ViewPageActivator + { + get + { + if (_viewPageActivator != null) + { + return _viewPageActivator; + } + _viewPageActivator = _activatorResolver.Current; + return _viewPageActivator; + } + } + + protected override bool FileExists(ControllerContext controllerContext, string virtualPath) + { + return BuildManager.FileExists(virtualPath); + } + + internal class DefaultViewPageActivator : IViewPageActivator + { + private Func<IDependencyResolver> _resolverThunk; + + public DefaultViewPageActivator() + : this(null) + { + } + + public DefaultViewPageActivator(IDependencyResolver resolver) + { + if (resolver == null) + { + _resolverThunk = () => DependencyResolver.Current; + } + else + { + _resolverThunk = () => resolver; + } + } + + public object Create(ControllerContext controllerContext, Type type) + { + return _resolverThunk().GetService(type) ?? Activator.CreateInstance(type); + } + } + } +} diff --git a/src/System.Web.Mvc/BuildManagerWrapper.cs b/src/System.Web.Mvc/BuildManagerWrapper.cs new file mode 100644 index 00000000..13f2e94c --- /dev/null +++ b/src/System.Web.Mvc/BuildManagerWrapper.cs @@ -0,0 +1,34 @@ +using System.Collections; +using System.IO; +using System.Web.Compilation; + +namespace System.Web.Mvc +{ + internal sealed class BuildManagerWrapper : IBuildManager + { + bool IBuildManager.FileExists(string virtualPath) + { + return BuildManager.GetObjectFactory(virtualPath, false) != null; + } + + Type IBuildManager.GetCompiledType(string virtualPath) + { + return BuildManager.GetCompiledType(virtualPath); + } + + ICollection IBuildManager.GetReferencedAssemblies() + { + return BuildManager.GetReferencedAssemblies(); + } + + Stream IBuildManager.ReadCachedFile(string fileName) + { + return BuildManager.ReadCachedFile(fileName); + } + + Stream IBuildManager.CreateCachedFile(string fileName) + { + return BuildManager.CreateCachedFile(fileName); + } + } +} diff --git a/src/System.Web.Mvc/ByteArrayModelBinder.cs b/src/System.Web.Mvc/ByteArrayModelBinder.cs new file mode 100644 index 00000000..df14ddb0 --- /dev/null +++ b/src/System.Web.Mvc/ByteArrayModelBinder.cs @@ -0,0 +1,34 @@ +namespace System.Web.Mvc +{ + public class ByteArrayModelBinder : IModelBinder + { + public virtual object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) + { + if (bindingContext == null) + { + throw new ArgumentNullException("bindingContext"); + } + + ValueProviderResult valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); + + // case 1: there was no <input ... /> element containing this data + if (valueResult == null) + { + return null; + } + + string value = valueResult.AttemptedValue; + + // case 2: there was an <input ... /> element but it was left blank + if (String.IsNullOrEmpty(value)) + { + return null; + } + + // Future proofing. If the byte array is actually an instance of System.Data.Linq.Binary + // then we need to remove these quotes put in place by the ToString() method. + string realValue = value.Replace("\"", String.Empty); + return Convert.FromBase64String(realValue); + } + } +} diff --git a/src/System.Web.Mvc/CachedAssociatedMetadataProvider`1.cs b/src/System.Web.Mvc/CachedAssociatedMetadataProvider`1.cs new file mode 100644 index 00000000..b0a00cb2 --- /dev/null +++ b/src/System.Web.Mvc/CachedAssociatedMetadataProvider`1.cs @@ -0,0 +1,94 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.Caching; + +namespace System.Web.Mvc +{ + public abstract class CachedAssociatedMetadataProvider<TModelMetadata> : AssociatedMetadataProvider + where TModelMetadata : ModelMetadata + { + private static ConcurrentDictionary<Type, string> _typeIds = new ConcurrentDictionary<Type, string>(); + private string _cacheKeyPrefix; + private CacheItemPolicy _cacheItemPolicy = new CacheItemPolicy { SlidingExpiration = TimeSpan.FromMinutes(20) }; + private ObjectCache _prototypeCache; + + protected internal CacheItemPolicy CacheItemPolicy + { + get { return _cacheItemPolicy; } + set { _cacheItemPolicy = value; } + } + + protected string CacheKeyPrefix + { + get + { + if (_cacheKeyPrefix == null) + { + _cacheKeyPrefix = "MetadataPrototypes::" + GetType().GUID.ToString("B"); + } + return _cacheKeyPrefix; + } + } + + protected internal ObjectCache PrototypeCache + { + get { return _prototypeCache ?? MemoryCache.Default; } + set { _prototypeCache = value; } + } + + protected sealed override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName) + { + // If metadata is being created for a property then containerType != null && propertyName != null + // If metadata is being created for a type then containerType == null && propertyName == null, so we have to use modelType for the cache key. + Type typeForCache = containerType ?? modelType; + string cacheKey = GetCacheKey(typeForCache, propertyName); + TModelMetadata prototype = PrototypeCache.Get(cacheKey) as TModelMetadata; + if (prototype == null) + { + prototype = CreateMetadataPrototype(attributes, containerType, modelType, propertyName); + PrototypeCache.Add(cacheKey, prototype, CacheItemPolicy); + } + + return CreateMetadataFromPrototype(prototype, modelAccessor); + } + + // New override for creating the prototype metadata (without the accessor) + protected abstract TModelMetadata CreateMetadataPrototype(IEnumerable<Attribute> attributes, Type containerType, Type modelType, string propertyName); + + // New override for applying the prototype + modelAccess to yield the final metadata + protected abstract TModelMetadata CreateMetadataFromPrototype(TModelMetadata prototype, Func<object> modelAccessor); + + internal string GetCacheKey(Type type, string propertyName = null) + { + propertyName = propertyName ?? String.Empty; + return CacheKeyPrefix + GetTypeId(type) + propertyName; + } + + public sealed override ModelMetadata GetMetadataForProperty(Func<object> modelAccessor, Type containerType, string propertyName) + { + return base.GetMetadataForProperty(modelAccessor, containerType, propertyName); + } + + protected sealed override ModelMetadata GetMetadataForProperty(Func<object> modelAccessor, Type containerType, PropertyDescriptor propertyDescriptor) + { + return base.GetMetadataForProperty(modelAccessor, containerType, propertyDescriptor); + } + + public sealed override IEnumerable<ModelMetadata> GetMetadataForProperties(object container, Type containerType) + { + return base.GetMetadataForProperties(container, containerType); + } + + public sealed override ModelMetadata GetMetadataForType(Func<object> modelAccessor, Type modelType) + { + return base.GetMetadataForType(modelAccessor, modelType); + } + + private static string GetTypeId(Type type) + { + // It's fine using a random Guid since we store the mapping for types to guids. + return _typeIds.GetOrAdd(type, _ => Guid.NewGuid().ToString("B")); + } + } +} diff --git a/src/System.Web.Mvc/CachedDataAnnotationsMetadataAttributes.cs b/src/System.Web.Mvc/CachedDataAnnotationsMetadataAttributes.cs new file mode 100644 index 00000000..94a092ef --- /dev/null +++ b/src/System.Web.Mvc/CachedDataAnnotationsMetadataAttributes.cs @@ -0,0 +1,54 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Linq; + +namespace System.Web.Mvc +{ + public class CachedDataAnnotationsMetadataAttributes + { + public CachedDataAnnotationsMetadataAttributes(Attribute[] attributes) + { + DataType = attributes.OfType<DataTypeAttribute>().FirstOrDefault(); + Display = attributes.OfType<DisplayAttribute>().FirstOrDefault(); + DisplayColumn = attributes.OfType<DisplayColumnAttribute>().FirstOrDefault(); + DisplayFormat = attributes.OfType<DisplayFormatAttribute>().FirstOrDefault(); + DisplayName = attributes.OfType<DisplayNameAttribute>().FirstOrDefault(); + Editable = attributes.OfType<EditableAttribute>().FirstOrDefault(); + HiddenInput = attributes.OfType<HiddenInputAttribute>().FirstOrDefault(); + ReadOnly = attributes.OfType<ReadOnlyAttribute>().FirstOrDefault(); + Required = attributes.OfType<RequiredAttribute>().FirstOrDefault(); + ScaffoldColumn = attributes.OfType<ScaffoldColumnAttribute>().FirstOrDefault(); + + var uiHintAttributes = attributes.OfType<UIHintAttribute>(); + UIHint = uiHintAttributes.FirstOrDefault(a => String.Equals(a.PresentationLayer, "MVC", StringComparison.OrdinalIgnoreCase)) + ?? uiHintAttributes.FirstOrDefault(a => String.IsNullOrEmpty(a.PresentationLayer)); + + if (DisplayFormat == null && DataType != null) + { + DisplayFormat = DataType.DisplayFormat; + } + } + + public DataTypeAttribute DataType { get; protected set; } + + public DisplayAttribute Display { get; protected set; } + + public DisplayColumnAttribute DisplayColumn { get; protected set; } + + public DisplayFormatAttribute DisplayFormat { get; protected set; } + + public DisplayNameAttribute DisplayName { get; protected set; } + + public EditableAttribute Editable { get; protected set; } + + public HiddenInputAttribute HiddenInput { get; protected set; } + + public ReadOnlyAttribute ReadOnly { get; protected set; } + + public RequiredAttribute Required { get; protected set; } + + public ScaffoldColumnAttribute ScaffoldColumn { get; protected set; } + + public UIHintAttribute UIHint { get; protected set; } + } +} diff --git a/src/System.Web.Mvc/CachedDataAnnotationsModelMetadata.cs b/src/System.Web.Mvc/CachedDataAnnotationsModelMetadata.cs new file mode 100644 index 00000000..399b1481 --- /dev/null +++ b/src/System.Web.Mvc/CachedDataAnnotationsModelMetadata.cs @@ -0,0 +1,216 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + public class CachedDataAnnotationsModelMetadata : CachedModelMetadata<CachedDataAnnotationsMetadataAttributes> + { + public CachedDataAnnotationsModelMetadata(CachedDataAnnotationsModelMetadata prototype, Func<object> modelAccessor) + : base(prototype, modelAccessor) + { + } + + public CachedDataAnnotationsModelMetadata(CachedDataAnnotationsModelMetadataProvider provider, Type containerType, Type modelType, string propertyName, IEnumerable<Attribute> attributes) + : base(provider, containerType, modelType, propertyName, new CachedDataAnnotationsMetadataAttributes(attributes.ToArray())) + { + } + + protected override bool ComputeConvertEmptyStringToNull() + { + return PrototypeCache.DisplayFormat != null + ? PrototypeCache.DisplayFormat.ConvertEmptyStringToNull + : base.ComputeConvertEmptyStringToNull(); + } + + protected override string ComputeDataTypeName() + { + if (PrototypeCache.DataType != null) + { + return PrototypeCache.DataType.ToDataTypeName(); + } + + if (PrototypeCache.DisplayFormat != null && !PrototypeCache.DisplayFormat.HtmlEncode) + { + return DataTypeUtil.HtmlTypeName; + } + + return base.ComputeDataTypeName(); + } + + protected override string ComputeDescription() + { + return PrototypeCache.Display != null + ? PrototypeCache.Display.GetDescription() + : base.ComputeDescription(); + } + + protected override string ComputeDisplayFormatString() + { + return PrototypeCache.DisplayFormat != null + ? PrototypeCache.DisplayFormat.DataFormatString + : base.ComputeDisplayFormatString(); + } + + protected override string ComputeDisplayName() + { + string result = null; + + if (PrototypeCache.Display != null) + { + result = PrototypeCache.Display.GetName(); + } + + if (result == null && PrototypeCache.DisplayName != null) + { + result = PrototypeCache.DisplayName.DisplayName; + } + + return result ?? base.ComputeDisplayName(); + } + + protected override string ComputeEditFormatString() + { + if (PrototypeCache.DisplayFormat != null && PrototypeCache.DisplayFormat.ApplyFormatInEditMode) + { + return PrototypeCache.DisplayFormat.DataFormatString; + } + + return base.ComputeEditFormatString(); + } + + protected override bool ComputeHideSurroundingHtml() + { + return PrototypeCache.HiddenInput != null + ? !PrototypeCache.HiddenInput.DisplayValue + : base.ComputeHideSurroundingHtml(); + } + + protected override bool ComputeIsReadOnly() + { + if (PrototypeCache.Editable != null) + { + return !PrototypeCache.Editable.AllowEdit; + } + + if (PrototypeCache.ReadOnly != null) + { + return PrototypeCache.ReadOnly.IsReadOnly; + } + + return base.ComputeIsReadOnly(); + } + + protected override bool ComputeIsRequired() + { + return PrototypeCache.Required != null + ? true + : base.ComputeIsRequired(); + } + + protected override string ComputeNullDisplayText() + { + return PrototypeCache.DisplayFormat != null + ? PrototypeCache.DisplayFormat.NullDisplayText + : base.ComputeNullDisplayText(); + } + + protected override int ComputeOrder() + { + int? result = null; + + if (PrototypeCache.Display != null) + { + result = PrototypeCache.Display.GetOrder(); + } + + return result ?? base.ComputeOrder(); + } + + protected override string ComputeShortDisplayName() + { + return PrototypeCache.Display != null + ? PrototypeCache.Display.GetShortName() + : base.ComputeShortDisplayName(); + } + + protected override bool ComputeShowForDisplay() + { + return PrototypeCache.ScaffoldColumn != null + ? PrototypeCache.ScaffoldColumn.Scaffold + : base.ComputeShowForDisplay(); + } + + protected override bool ComputeShowForEdit() + { + return PrototypeCache.ScaffoldColumn != null + ? PrototypeCache.ScaffoldColumn.Scaffold + : base.ComputeShowForEdit(); + } + + protected override string ComputeSimpleDisplayText() + { + if (Model != null) + { + if (PrototypeCache.DisplayColumn != null && !String.IsNullOrEmpty(PrototypeCache.DisplayColumn.DisplayColumn)) + { + PropertyInfo displayColumnProperty = ModelType.GetProperty(PrototypeCache.DisplayColumn.DisplayColumn, BindingFlags.Public | BindingFlags.IgnoreCase | BindingFlags.Instance); + ValidateDisplayColumnAttribute(PrototypeCache.DisplayColumn, displayColumnProperty, ModelType); + + object simpleDisplayTextValue = displayColumnProperty.GetValue(Model, new object[0]); + if (simpleDisplayTextValue != null) + { + return simpleDisplayTextValue.ToString(); + } + } + } + + return base.ComputeSimpleDisplayText(); + } + + protected override string ComputeTemplateHint() + { + if (PrototypeCache.UIHint != null) + { + return PrototypeCache.UIHint.UIHint; + } + + if (PrototypeCache.HiddenInput != null) + { + return "HiddenInput"; + } + + return base.ComputeTemplateHint(); + } + + protected override string ComputeWatermark() + { + return PrototypeCache.Display != null + ? PrototypeCache.Display.GetPrompt() + : base.ComputeWatermark(); + } + + private static void ValidateDisplayColumnAttribute(DisplayColumnAttribute displayColumnAttribute, PropertyInfo displayColumnProperty, Type modelType) + { + if (displayColumnProperty == null) + { + throw new InvalidOperationException( + String.Format( + CultureInfo.CurrentCulture, + MvcResources.DataAnnotationsModelMetadataProvider_UnknownProperty, + modelType.FullName, displayColumnAttribute.DisplayColumn)); + } + if (displayColumnProperty.GetGetMethod() == null) + { + throw new InvalidOperationException( + String.Format( + CultureInfo.CurrentCulture, + MvcResources.DataAnnotationsModelMetadataProvider_UnreadableProperty, + modelType.FullName, displayColumnAttribute.DisplayColumn)); + } + } + } +} diff --git a/src/System.Web.Mvc/CachedDataAnnotationsModelMetadataProvider.cs b/src/System.Web.Mvc/CachedDataAnnotationsModelMetadataProvider.cs new file mode 100644 index 00000000..fab73542 --- /dev/null +++ b/src/System.Web.Mvc/CachedDataAnnotationsModelMetadataProvider.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace System.Web.Mvc +{ + public class CachedDataAnnotationsModelMetadataProvider : CachedAssociatedMetadataProvider<CachedDataAnnotationsModelMetadata> + { + protected override CachedDataAnnotationsModelMetadata CreateMetadataPrototype(IEnumerable<Attribute> attributes, Type containerType, Type modelType, string propertyName) + { + return new CachedDataAnnotationsModelMetadata(this, containerType, modelType, propertyName, attributes); + } + + protected override CachedDataAnnotationsModelMetadata CreateMetadataFromPrototype(CachedDataAnnotationsModelMetadata prototype, Func<object> modelAccessor) + { + return new CachedDataAnnotationsModelMetadata(prototype, modelAccessor); + } + } +} diff --git a/src/System.Web.Mvc/CachedModelMetadata`1.cs b/src/System.Web.Mvc/CachedModelMetadata`1.cs new file mode 100644 index 00000000..bcc723e5 --- /dev/null +++ b/src/System.Web.Mvc/CachedModelMetadata`1.cs @@ -0,0 +1,415 @@ +namespace System.Web.Mvc +{ + // This class assumes that model metadata is expensive to create, and allows the user to + // stash a cache object that can be copied around as a prototype to make creation and + // computation quicker. It delegates the retrieval of values to getter methods, the results + // of which are cached on a per-metadata-instance basis. + // + // This allows flexible caching strategies: either caching the source of information across + // instances or caching of the actual information itself, depending on what the developer + // decides to put into the prototype cache. + public abstract class CachedModelMetadata<TPrototypeCache> : ModelMetadata + { + private bool _convertEmptyStringToNull; + private string _dataTypeName; + private string _description; + private string _displayFormatString; + private string _displayName; + private string _editFormatString; + private bool _hideSurroundingHtml; + private bool _isReadOnly; + private bool _isRequired; + private string _nullDisplayText; + private int _order; + private string _shortDisplayName; + private bool _showForDisplay; + private bool _showForEdit; + private string _templateHint; + private string _watermark; + + private bool _convertEmptyStringToNullComputed; + private bool _dataTypeNameComputed; + private bool _descriptionComputed; + private bool _displayFormatStringComputed; + private bool _displayNameComputed; + private bool _editFormatStringComputed; + private bool _hideSurroundingHtmlComputed; + private bool _isReadOnlyComputed; + private bool _isRequiredComputed; + private bool _nullDisplayTextComputed; + private bool _orderComputed; + private bool _shortDisplayNameComputed; + private bool _showForDisplayComputed; + private bool _showForEditComputed; + private bool _templateHintComputed; + private bool _watermarkComputed; + + // Constructor for creating real instances of the metadata class based on a prototype + protected CachedModelMetadata(CachedModelMetadata<TPrototypeCache> prototype, Func<object> modelAccessor) + : base(prototype.Provider, prototype.ContainerType, modelAccessor, prototype.ModelType, prototype.PropertyName) + { + PrototypeCache = prototype.PrototypeCache; + } + + // Constructor for creating the prototype instances of the metadata class + protected CachedModelMetadata(CachedDataAnnotationsModelMetadataProvider provider, Type containerType, Type modelType, string propertyName, TPrototypeCache prototypeCache) + : base(provider, containerType, null /* modelAccessor */, modelType, propertyName) + { + PrototypeCache = prototypeCache; + } + + public sealed override bool ConvertEmptyStringToNull + { + get + { + return CacheOrCompute(ComputeConvertEmptyStringToNull, + ref _convertEmptyStringToNull, + ref _convertEmptyStringToNullComputed); + } + set + { + _convertEmptyStringToNull = value; + _convertEmptyStringToNullComputed = true; + } + } + + public sealed override string DataTypeName + { + get + { + return CacheOrCompute(ComputeDataTypeName, + ref _dataTypeName, + ref _dataTypeNameComputed); + } + set + { + _dataTypeName = value; + _dataTypeNameComputed = true; + } + } + + public sealed override string Description + { + get + { + return CacheOrCompute(ComputeDescription, + ref _description, + ref _descriptionComputed); + } + set + { + _description = value; + _descriptionComputed = true; + } + } + + public sealed override string DisplayFormatString + { + get + { + return CacheOrCompute(ComputeDisplayFormatString, + ref _displayFormatString, + ref _displayFormatStringComputed); + } + set + { + _displayFormatString = value; + _displayFormatStringComputed = true; + } + } + + public sealed override string DisplayName + { + get + { + return CacheOrCompute(ComputeDisplayName, + ref _displayName, + ref _displayNameComputed); + } + set + { + _displayName = value; + _displayNameComputed = true; + } + } + + public sealed override string EditFormatString + { + get + { + return CacheOrCompute(ComputeEditFormatString, + ref _editFormatString, + ref _editFormatStringComputed); + } + set + { + _editFormatString = value; + _editFormatStringComputed = true; + } + } + + public sealed override bool HideSurroundingHtml + { + get + { + return CacheOrCompute(ComputeHideSurroundingHtml, + ref _hideSurroundingHtml, + ref _hideSurroundingHtmlComputed); + } + set + { + _hideSurroundingHtml = value; + _hideSurroundingHtmlComputed = true; + } + } + + public sealed override bool IsReadOnly + { + get + { + return CacheOrCompute(ComputeIsReadOnly, + ref _isReadOnly, + ref _isReadOnlyComputed); + } + set + { + _isReadOnly = value; + _isReadOnlyComputed = true; + } + } + + public sealed override bool IsRequired + { + get + { + return CacheOrCompute(ComputeIsRequired, + ref _isRequired, + ref _isRequiredComputed); + } + set + { + _isRequired = value; + _isRequiredComputed = true; + } + } + + public sealed override string NullDisplayText + { + get + { + return CacheOrCompute(ComputeNullDisplayText, + ref _nullDisplayText, + ref _nullDisplayTextComputed); + } + set + { + _nullDisplayText = value; + _nullDisplayTextComputed = true; + } + } + + public sealed override int Order + { + get + { + return CacheOrCompute(ComputeOrder, + ref _order, + ref _orderComputed); + } + set + { + _order = value; + _orderComputed = true; + } + } + + protected TPrototypeCache PrototypeCache { get; set; } + + public sealed override string ShortDisplayName + { + get + { + return CacheOrCompute(ComputeShortDisplayName, + ref _shortDisplayName, + ref _shortDisplayNameComputed); + } + set + { + _shortDisplayName = value; + _shortDisplayNameComputed = true; + } + } + + public sealed override bool ShowForDisplay + { + get + { + return CacheOrCompute(ComputeShowForDisplay, + ref _showForDisplay, + ref _showForDisplayComputed); + } + set + { + _showForDisplay = value; + _showForDisplayComputed = true; + } + } + + public sealed override bool ShowForEdit + { + get + { + return CacheOrCompute(ComputeShowForEdit, + ref _showForEdit, + ref _showForEditComputed); + } + set + { + _showForEdit = value; + _showForEditComputed = true; + } + } + + public sealed override string SimpleDisplayText + { + get + { + // This is already cached in the base class with an appropriate override available + return base.SimpleDisplayText; + } + set { base.SimpleDisplayText = value; } + } + + public sealed override string TemplateHint + { + get + { + return CacheOrCompute(ComputeTemplateHint, + ref _templateHint, + ref _templateHintComputed); + } + set + { + _templateHint = value; + _templateHintComputed = true; + } + } + + public sealed override string Watermark + { + get + { + return CacheOrCompute(ComputeWatermark, + ref _watermark, + ref _watermarkComputed); + } + set + { + _watermark = value; + _watermarkComputed = true; + } + } + + private static TResult CacheOrCompute<TResult>(Func<TResult> computeThunk, ref TResult value, ref bool computed) + { + if (!computed) + { + value = computeThunk(); + computed = true; + } + + return value; + } + + protected virtual bool ComputeConvertEmptyStringToNull() + { + return base.ConvertEmptyStringToNull; + } + + protected virtual string ComputeDataTypeName() + { + return base.DataTypeName; + } + + protected virtual string ComputeDescription() + { + return base.Description; + } + + protected virtual string ComputeDisplayFormatString() + { + return base.DisplayFormatString; + } + + protected virtual string ComputeDisplayName() + { + return base.DisplayName; + } + + protected virtual string ComputeEditFormatString() + { + return base.EditFormatString; + } + + protected virtual bool ComputeHideSurroundingHtml() + { + return base.HideSurroundingHtml; + } + + protected virtual bool ComputeIsReadOnly() + { + return base.IsReadOnly; + } + + protected virtual bool ComputeIsRequired() + { + return base.IsRequired; + } + + protected virtual string ComputeNullDisplayText() + { + return base.NullDisplayText; + } + + protected virtual int ComputeOrder() + { + return base.Order; + } + + protected virtual string ComputeShortDisplayName() + { + return base.ShortDisplayName; + } + + protected virtual bool ComputeShowForDisplay() + { + return base.ShowForDisplay; + } + + protected virtual bool ComputeShowForEdit() + { + return base.ShowForEdit; + } + + protected virtual string ComputeSimpleDisplayText() + { + return base.GetSimpleDisplayText(); + } + + protected virtual string ComputeTemplateHint() + { + return base.TemplateHint; + } + + protected virtual string ComputeWatermark() + { + return base.Watermark; + } + + protected sealed override string GetSimpleDisplayText() + { + // Rename for consistency + return ComputeSimpleDisplayText(); + } + } +} diff --git a/src/System.Web.Mvc/CancellationTokenModelBinder.cs b/src/System.Web.Mvc/CancellationTokenModelBinder.cs new file mode 100644 index 00000000..24522296 --- /dev/null +++ b/src/System.Web.Mvc/CancellationTokenModelBinder.cs @@ -0,0 +1,12 @@ +using System.Threading; + +namespace System.Web.Mvc +{ + public class CancellationTokenModelBinder : IModelBinder + { + public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) + { + return default(CancellationToken); + } + } +} diff --git a/src/System.Web.Mvc/ChildActionOnlyAttribute.cs b/src/System.Web.Mvc/ChildActionOnlyAttribute.cs new file mode 100644 index 00000000..16647f4a --- /dev/null +++ b/src/System.Web.Mvc/ChildActionOnlyAttribute.cs @@ -0,0 +1,19 @@ +namespace System.Web.Mvc +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public sealed class ChildActionOnlyAttribute : FilterAttribute, IAuthorizationFilter + { + public void OnAuthorization(AuthorizationContext filterContext) + { + if (filterContext == null) + { + throw new ArgumentNullException("filterContext"); + } + + if (!filterContext.IsChildAction) + { + throw Error.ChildActionOnlyAttribute_MustBeInChildRequest(filterContext.ActionDescriptor); + } + } + } +} diff --git a/src/System.Web.Mvc/ChildActionValueProvider.cs b/src/System.Web.Mvc/ChildActionValueProvider.cs new file mode 100644 index 00000000..ec1d3528 --- /dev/null +++ b/src/System.Web.Mvc/ChildActionValueProvider.cs @@ -0,0 +1,39 @@ +using System.Globalization; + +namespace System.Web.Mvc +{ + public sealed class ChildActionValueProvider : DictionaryValueProvider<object> + { + private static string _childActionValuesKey = Guid.NewGuid().ToString(); + + public ChildActionValueProvider(ControllerContext controllerContext) + : base(controllerContext.RouteData.Values, CultureInfo.InvariantCulture) + { + } + + internal static string ChildActionValuesKey + { + get { return _childActionValuesKey; } + } + + public override ValueProviderResult GetValue(string key) + { + if (key == null) + { + throw new ArgumentNullException("key"); + } + + ValueProviderResult explicitValues = base.GetValue(ChildActionValuesKey); + if (explicitValues != null) + { + DictionaryValueProvider<object> rawExplicitValues = explicitValues.RawValue as DictionaryValueProvider<object>; + if (rawExplicitValues != null) + { + return rawExplicitValues.GetValue(key); + } + } + + return null; + } + } +} diff --git a/src/System.Web.Mvc/ChildActionValueProviderFactory.cs b/src/System.Web.Mvc/ChildActionValueProviderFactory.cs new file mode 100644 index 00000000..1231a381 --- /dev/null +++ b/src/System.Web.Mvc/ChildActionValueProviderFactory.cs @@ -0,0 +1,15 @@ +namespace System.Web.Mvc +{ + public sealed class ChildActionValueProviderFactory : ValueProviderFactory + { + public override IValueProvider GetValueProvider(ControllerContext controllerContext) + { + if (controllerContext == null) + { + throw new ArgumentNullException("controllerContext"); + } + + return new ChildActionValueProvider(controllerContext); + } + } +} diff --git a/src/System.Web.Mvc/ClientDataTypeModelValidatorProvider.cs b/src/System.Web.Mvc/ClientDataTypeModelValidatorProvider.cs new file mode 100644 index 00000000..080520b7 --- /dev/null +++ b/src/System.Web.Mvc/ClientDataTypeModelValidatorProvider.cs @@ -0,0 +1,159 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + public class ClientDataTypeModelValidatorProvider : ModelValidatorProvider + { + private static readonly HashSet<Type> _numericTypes = new HashSet<Type>(new Type[] + { + typeof(byte), typeof(sbyte), + typeof(short), typeof(ushort), + typeof(int), typeof(uint), + typeof(long), typeof(ulong), + typeof(float), typeof(double), typeof(decimal) + }); + + private static string _resourceClassKey; + + public static string ResourceClassKey + { + get { return _resourceClassKey ?? String.Empty; } + set { _resourceClassKey = value; } + } + + public override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context) + { + if (metadata == null) + { + throw new ArgumentNullException("metadata"); + } + if (context == null) + { + throw new ArgumentNullException("context"); + } + + return GetValidatorsImpl(metadata, context); + } + + private static IEnumerable<ModelValidator> GetValidatorsImpl(ModelMetadata metadata, ControllerContext context) + { + Type type = metadata.ModelType; + + if (IsDateTimeType(type)) + { + yield return new DateModelValidator(metadata, context); + } + + if (IsNumericType(type)) + { + yield return new NumericModelValidator(metadata, context); + } + } + + private static bool IsNumericType(Type type) + { + return _numericTypes.Contains(GetTypeToValidate(type)); + } + + private static bool IsDateTimeType(Type type) + { + return typeof(DateTime) == GetTypeToValidate(type); + } + + private static Type GetTypeToValidate(Type type) + { + return Nullable.GetUnderlyingType(type) ?? type; // strip off the Nullable<> + } + + // If the user specified a ResourceClassKey try to load the resource they specified. + // If the class key is invalid, an exception will be thrown. + // If the class key is valid but the resource is not found, it returns null, in which + // case it will fall back to the MVC default error message. + private static string GetUserResourceString(ControllerContext controllerContext, string resourceName) + { + string result = null; + + if (!String.IsNullOrEmpty(ResourceClassKey) && (controllerContext != null) && (controllerContext.HttpContext != null)) + { + result = controllerContext.HttpContext.GetGlobalResourceObject(ResourceClassKey, resourceName, CultureInfo.CurrentUICulture) as string; + } + + return result; + } + + private static string GetFieldMustBeNumericResource(ControllerContext controllerContext) + { + return GetUserResourceString(controllerContext, "FieldMustBeNumeric") ?? MvcResources.ClientDataTypeModelValidatorProvider_FieldMustBeNumeric; + } + + private static string GetFieldMustBeDateResource(ControllerContext controllerContext) + { + return GetUserResourceString(controllerContext, "FieldMustBeDate") ?? MvcResources.ClientDataTypeModelValidatorProvider_FieldMustBeDate; + } + + internal class ClientModelValidator : ModelValidator + { + private string _errorMessage; + private string _validationType; + + public ClientModelValidator(ModelMetadata metadata, ControllerContext controllerContext, string validationType, string errorMessage) + : base(metadata, controllerContext) + { + if (String.IsNullOrEmpty(validationType)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "validationType"); + } + + if (String.IsNullOrEmpty(errorMessage)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "errorMessage"); + } + + _validationType = validationType; + _errorMessage = errorMessage; + } + + public sealed override IEnumerable<ModelClientValidationRule> GetClientValidationRules() + { + ModelClientValidationRule rule = new ModelClientValidationRule() + { + ValidationType = _validationType, + ErrorMessage = FormatErrorMessage(Metadata.GetDisplayName()) + }; + + return new ModelClientValidationRule[] { rule }; + } + + private string FormatErrorMessage(string displayName) + { + // use CurrentCulture since this message is intended for the site visitor + return String.Format(CultureInfo.CurrentCulture, _errorMessage, displayName); + } + + public sealed override IEnumerable<ModelValidationResult> Validate(object container) + { + // this is not a server-side validator + return Enumerable.Empty<ModelValidationResult>(); + } + } + + internal sealed class DateModelValidator : ClientModelValidator + { + public DateModelValidator(ModelMetadata metadata, ControllerContext controllerContext) + : base(metadata, controllerContext, "date", GetFieldMustBeDateResource(controllerContext)) + { + } + } + + internal sealed class NumericModelValidator : ClientModelValidator + { + public NumericModelValidator(ModelMetadata metadata, ControllerContext controllerContext) + : base(metadata, controllerContext, "number", GetFieldMustBeNumericResource(controllerContext)) + { + } + } + } +} diff --git a/src/System.Web.Mvc/CompareAttribute.cs b/src/System.Web.Mvc/CompareAttribute.cs new file mode 100644 index 00000000..dc80ed05 --- /dev/null +++ b/src/System.Web.Mvc/CompareAttribute.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Reflection; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + [AttributeUsage(AttributeTargets.Property)] + [SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "This attribute is designed to be a base class for other attributes.")] + public class CompareAttribute : ValidationAttribute, IClientValidatable + { + public CompareAttribute(string otherProperty) + : base(MvcResources.CompareAttribute_MustMatch) + { + if (otherProperty == null) + { + throw new ArgumentNullException("otherProperty"); + } + OtherProperty = otherProperty; + } + + public string OtherProperty { get; private set; } + + public string OtherPropertyDisplayName { get; internal set; } + + public override string FormatErrorMessage(string name) + { + return String.Format(CultureInfo.CurrentCulture, ErrorMessageString, name, OtherPropertyDisplayName ?? OtherProperty); + } + + protected override ValidationResult IsValid(object value, ValidationContext validationContext) + { + PropertyInfo otherPropertyInfo = validationContext.ObjectType.GetProperty(OtherProperty); + if (otherPropertyInfo == null) + { + return new ValidationResult(String.Format(CultureInfo.CurrentCulture, MvcResources.CompareAttribute_UnknownProperty, OtherProperty)); + } + + object otherPropertyValue = otherPropertyInfo.GetValue(validationContext.ObjectInstance, null); + if (!Equals(value, otherPropertyValue)) + { + if (OtherPropertyDisplayName == null) + { + OtherPropertyDisplayName = ModelMetadataProviders.Current.GetMetadataForProperty(() => validationContext.ObjectInstance, validationContext.ObjectType, OtherProperty).GetDisplayName(); + } + return new ValidationResult(FormatErrorMessage(validationContext.DisplayName)); + } + return null; + } + + public static string FormatPropertyForClientValidation(string property) + { + if (property == null) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "property"); + } + return "*." + property; + } + + public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context) + { + if (metadata.ContainerType != null) + { + if (OtherPropertyDisplayName == null) + { + OtherPropertyDisplayName = ModelMetadataProviders.Current.GetMetadataForProperty(() => metadata.Model, metadata.ContainerType, OtherProperty).GetDisplayName(); + } + } + yield return new ModelClientValidationEqualToRule(FormatErrorMessage(metadata.GetDisplayName()), FormatPropertyForClientValidation(OtherProperty)); + } + } +} diff --git a/src/System.Web.Mvc/ContentResult.cs b/src/System.Web.Mvc/ContentResult.cs new file mode 100644 index 00000000..18812c36 --- /dev/null +++ b/src/System.Web.Mvc/ContentResult.cs @@ -0,0 +1,36 @@ +using System.Text; + +namespace System.Web.Mvc +{ + public class ContentResult : ActionResult + { + public string Content { get; set; } + + public Encoding ContentEncoding { get; set; } + + public string ContentType { get; set; } + + public override void ExecuteResult(ControllerContext context) + { + if (context == null) + { + throw new ArgumentNullException("context"); + } + + HttpResponseBase response = context.HttpContext.Response; + + if (!String.IsNullOrEmpty(ContentType)) + { + response.ContentType = ContentType; + } + if (ContentEncoding != null) + { + response.ContentEncoding = ContentEncoding; + } + if (Content != null) + { + response.Write(Content); + } + } + } +} diff --git a/src/System.Web.Mvc/Controller.cs b/src/System.Web.Mvc/Controller.cs new file mode 100644 index 00000000..e654c650 --- /dev/null +++ b/src/System.Web.Mvc/Controller.cs @@ -0,0 +1,920 @@ +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Security.Principal; +using System.Text; +using System.Web.Mvc.Async; +using System.Web.Mvc.Properties; +using System.Web.Profile; +using System.Web.Routing; + +namespace System.Web.Mvc +{ + [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Class complexity dictated by public surface area")] + public abstract class Controller : ControllerBase, IActionFilter, IAuthorizationFilter, IDisposable, IExceptionFilter, IResultFilter, IAsyncController, IAsyncManagerContainer + { + private static readonly object _executeTag = new object(); + private static readonly object _executeCoreTag = new object(); + + private readonly AsyncManager _asyncManager = new AsyncManager(); + private IActionInvoker _actionInvoker; + private ModelBinderDictionary _binders; + private RouteCollection _routeCollection; + private ITempDataProvider _tempDataProvider; + private ViewEngineCollection _viewEngineCollection; + + private IDependencyResolver _resolver; + + // By default, use the global resolver with caching. + // Or we can override to supply this instance with its own cache. + internal IDependencyResolver Resolver + { + get { return _resolver ?? DependencyResolver.CurrentCache; } + set { _resolver = value; } + } + + public AsyncManager AsyncManager + { + get { return _asyncManager; } + } + + /// <summary> + /// This is for backwards compat. MVC 4.0 starts allowing Controller to support asynchronous patterns. + /// This means ExecuteCore doesn't get called on derived classes. Derived classes can override this + /// flag and set to true if they still need ExecuteCore to be called. + /// </summary> + protected virtual bool DisableAsyncSupport + { + get { return false; } + } + + public IActionInvoker ActionInvoker + { + get + { + if (_actionInvoker == null) + { + _actionInvoker = CreateActionInvoker(); + } + return _actionInvoker; + } + set { _actionInvoker = value; } + } + + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "Property is settable so that the dictionary can be provided for unit testing purposes.")] + protected internal ModelBinderDictionary Binders + { + get + { + if (_binders == null) + { + _binders = ModelBinders.Binders; + } + return _binders; + } + set { _binders = value; } + } + + public HttpContextBase HttpContext + { + get { return ControllerContext == null ? null : ControllerContext.HttpContext; } + } + + public ModelStateDictionary ModelState + { + get { return ViewData.ModelState; } + } + + public ProfileBase Profile + { + get { return HttpContext == null ? null : HttpContext.Profile; } + } + + public HttpRequestBase Request + { + get { return HttpContext == null ? null : HttpContext.Request; } + } + + public HttpResponseBase Response + { + get { return HttpContext == null ? null : HttpContext.Response; } + } + + internal RouteCollection RouteCollection + { + get + { + if (_routeCollection == null) + { + _routeCollection = RouteTable.Routes; + } + return _routeCollection; + } + set { _routeCollection = value; } + } + + public RouteData RouteData + { + get { return ControllerContext == null ? null : ControllerContext.RouteData; } + } + + public HttpServerUtilityBase Server + { + get { return HttpContext == null ? null : HttpContext.Server; } + } + + public HttpSessionStateBase Session + { + get { return HttpContext == null ? null : HttpContext.Session; } + } + + public ITempDataProvider TempDataProvider + { + get + { + if (_tempDataProvider == null) + { + _tempDataProvider = CreateTempDataProvider(); + } + return _tempDataProvider; + } + set { _tempDataProvider = value; } + } + + public UrlHelper Url { get; set; } + + public IPrincipal User + { + get { return HttpContext == null ? null : HttpContext.User; } + } + + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This entire type is meant to be mutable.")] + public ViewEngineCollection ViewEngineCollection + { + get { return _viewEngineCollection ?? ViewEngines.Engines; } + set { _viewEngineCollection = value; } + } + + [SuppressMessage("Microsoft.Naming", "CA1719:ParameterNamesShouldNotMatchMemberNames", MessageId = "0#", Justification = "'Content' refers to ContentResult type; 'content' refers to ContentResult.Content property.")] + protected internal ContentResult Content(string content) + { + return Content(content, null /* contentType */); + } + + [SuppressMessage("Microsoft.Naming", "CA1719:ParameterNamesShouldNotMatchMemberNames", MessageId = "0#", Justification = "'Content' refers to ContentResult type; 'content' refers to ContentResult.Content property.")] + protected internal ContentResult Content(string content, string contentType) + { + return Content(content, contentType, null /* contentEncoding */); + } + + [SuppressMessage("Microsoft.Naming", "CA1719:ParameterNamesShouldNotMatchMemberNames", MessageId = "0#", Justification = "'Content' refers to ContentResult type; 'content' refers to ContentResult.Content property.")] + protected internal virtual ContentResult Content(string content, string contentType, Encoding contentEncoding) + { + return new ContentResult + { + Content = content, + ContentType = contentType, + ContentEncoding = contentEncoding + }; + } + + protected virtual IActionInvoker CreateActionInvoker() + { + // Controller supports asynchronous operations by default. + return Resolver.GetService<IAsyncActionInvoker>() ?? Resolver.GetService<IActionInvoker>() ?? new AsyncControllerActionInvoker(); + } + + protected virtual ITempDataProvider CreateTempDataProvider() + { + return Resolver.GetService<ITempDataProvider>() ?? new SessionStateTempDataProvider(); + } + + // The default invoker will never match methods defined on the Controller type, so + // the Dispose() method is not web-callable. However, in general, since implicitly- + // implemented interface methods are public, they are web-callable unless decorated with + // [NonAction]. + public void Dispose() + { + Dispose(true /* disposing */); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + } + + protected override void ExecuteCore() + { + // If code in this method needs to be updated, please also check the BeginExecuteCore() and + // EndExecuteCore() methods of AsyncController to see if that code also must be updated. + + PossiblyLoadTempData(); + try + { + string actionName = RouteData.GetRequiredString("action"); + if (!ActionInvoker.InvokeAction(ControllerContext, actionName)) + { + HandleUnknownAction(actionName); + } + } + finally + { + PossiblySaveTempData(); + } + } + + protected internal FileContentResult File(byte[] fileContents, string contentType) + { + return File(fileContents, contentType, null /* fileDownloadName */); + } + + protected internal virtual FileContentResult File(byte[] fileContents, string contentType, string fileDownloadName) + { + return new FileContentResult(fileContents, contentType) { FileDownloadName = fileDownloadName }; + } + + protected internal FileStreamResult File(Stream fileStream, string contentType) + { + return File(fileStream, contentType, null /* fileDownloadName */); + } + + protected internal virtual FileStreamResult File(Stream fileStream, string contentType, string fileDownloadName) + { + return new FileStreamResult(fileStream, contentType) { FileDownloadName = fileDownloadName }; + } + + protected internal FilePathResult File(string fileName, string contentType) + { + return File(fileName, contentType, null /* fileDownloadName */); + } + + protected internal virtual FilePathResult File(string fileName, string contentType, string fileDownloadName) + { + return new FilePathResult(fileName, contentType) { FileDownloadName = fileDownloadName }; + } + + protected virtual void HandleUnknownAction(string actionName) + { + throw new HttpException(404, String.Format(CultureInfo.CurrentCulture, + MvcResources.Controller_UnknownAction, actionName, GetType().FullName)); + } + + protected internal HttpNotFoundResult HttpNotFound() + { + return HttpNotFound(null); + } + + protected internal virtual HttpNotFoundResult HttpNotFound(string statusDescription) + { + return new HttpNotFoundResult(statusDescription); + } + + protected internal virtual JavaScriptResult JavaScript(string script) + { + return new JavaScriptResult { Script = script }; + } + + protected internal JsonResult Json(object data) + { + return Json(data, null /* contentType */, null /* contentEncoding */, JsonRequestBehavior.DenyGet); + } + + protected internal JsonResult Json(object data, string contentType) + { + return Json(data, contentType, null /* contentEncoding */, JsonRequestBehavior.DenyGet); + } + + protected internal virtual JsonResult Json(object data, string contentType, Encoding contentEncoding) + { + return Json(data, contentType, contentEncoding, JsonRequestBehavior.DenyGet); + } + + protected internal JsonResult Json(object data, JsonRequestBehavior behavior) + { + return Json(data, null /* contentType */, null /* contentEncoding */, behavior); + } + + protected internal JsonResult Json(object data, string contentType, JsonRequestBehavior behavior) + { + return Json(data, contentType, null /* contentEncoding */, behavior); + } + + protected internal virtual JsonResult Json(object data, string contentType, Encoding contentEncoding, JsonRequestBehavior behavior) + { + return new JsonResult + { + Data = data, + ContentType = contentType, + ContentEncoding = contentEncoding, + JsonRequestBehavior = behavior + }; + } + + protected override void Initialize(RequestContext requestContext) + { + base.Initialize(requestContext); + Url = new UrlHelper(requestContext); + } + + protected virtual void OnActionExecuting(ActionExecutingContext filterContext) + { + } + + protected virtual void OnActionExecuted(ActionExecutedContext filterContext) + { + } + + protected virtual void OnAuthorization(AuthorizationContext filterContext) + { + } + + protected virtual void OnException(ExceptionContext filterContext) + { + } + + protected virtual void OnResultExecuted(ResultExecutedContext filterContext) + { + } + + protected virtual void OnResultExecuting(ResultExecutingContext filterContext) + { + } + + protected internal PartialViewResult PartialView() + { + return PartialView(null /* viewName */, null /* model */); + } + + protected internal PartialViewResult PartialView(object model) + { + return PartialView(null /* viewName */, model); + } + + protected internal PartialViewResult PartialView(string viewName) + { + return PartialView(viewName, null /* model */); + } + + protected internal virtual PartialViewResult PartialView(string viewName, object model) + { + if (model != null) + { + ViewData.Model = model; + } + + return new PartialViewResult + { + ViewName = viewName, + ViewData = ViewData, + TempData = TempData, + ViewEngineCollection = ViewEngineCollection + }; + } + + internal void PossiblyLoadTempData() + { + if (!ControllerContext.IsChildAction) + { + TempData.Load(ControllerContext, TempDataProvider); + } + } + + internal void PossiblySaveTempData() + { + if (!ControllerContext.IsChildAction) + { + TempData.Save(ControllerContext, TempDataProvider); + } + } + + [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "0#", Justification = "Response.Redirect() takes its URI as a string parameter.")] + protected internal virtual RedirectResult Redirect(string url) + { + if (String.IsNullOrEmpty(url)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "url"); + } + + return new RedirectResult(url); + } + + [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "0#", Justification = "Response.RedirectPermanent() takes its URI as a string parameter.")] + protected internal virtual RedirectResult RedirectPermanent(string url) + { + if (String.IsNullOrEmpty(url)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "url"); + } + + return new RedirectResult(url, permanent: true); + } + + protected internal RedirectToRouteResult RedirectToAction(string actionName) + { + return RedirectToAction(actionName, (RouteValueDictionary)null); + } + + protected internal RedirectToRouteResult RedirectToAction(string actionName, object routeValues) + { + return RedirectToAction(actionName, new RouteValueDictionary(routeValues)); + } + + protected internal RedirectToRouteResult RedirectToAction(string actionName, RouteValueDictionary routeValues) + { + return RedirectToAction(actionName, null /* controllerName */, routeValues); + } + + protected internal RedirectToRouteResult RedirectToAction(string actionName, string controllerName) + { + return RedirectToAction(actionName, controllerName, (RouteValueDictionary)null); + } + + protected internal RedirectToRouteResult RedirectToAction(string actionName, string controllerName, object routeValues) + { + return RedirectToAction(actionName, controllerName, new RouteValueDictionary(routeValues)); + } + + protected internal virtual RedirectToRouteResult RedirectToAction(string actionName, string controllerName, RouteValueDictionary routeValues) + { + RouteValueDictionary mergedRouteValues; + + if (RouteData == null) + { + mergedRouteValues = RouteValuesHelpers.MergeRouteValues(actionName, controllerName, null, routeValues, includeImplicitMvcValues: true); + } + else + { + mergedRouteValues = RouteValuesHelpers.MergeRouteValues(actionName, controllerName, RouteData.Values, routeValues, includeImplicitMvcValues: true); + } + + return new RedirectToRouteResult(mergedRouteValues); + } + + protected internal RedirectToRouteResult RedirectToActionPermanent(string actionName) + { + return RedirectToActionPermanent(actionName, (RouteValueDictionary)null); + } + + protected internal RedirectToRouteResult RedirectToActionPermanent(string actionName, object routeValues) + { + return RedirectToActionPermanent(actionName, new RouteValueDictionary(routeValues)); + } + + protected internal RedirectToRouteResult RedirectToActionPermanent(string actionName, RouteValueDictionary routeValues) + { + return RedirectToActionPermanent(actionName, null /* controllerName */, routeValues); + } + + protected internal RedirectToRouteResult RedirectToActionPermanent(string actionName, string controllerName) + { + return RedirectToActionPermanent(actionName, controllerName, (RouteValueDictionary)null); + } + + protected internal RedirectToRouteResult RedirectToActionPermanent(string actionName, string controllerName, object routeValues) + { + return RedirectToActionPermanent(actionName, controllerName, new RouteValueDictionary(routeValues)); + } + + protected internal virtual RedirectToRouteResult RedirectToActionPermanent(string actionName, string controllerName, RouteValueDictionary routeValues) + { + RouteValueDictionary implicitRouteValues = (RouteData != null) ? RouteData.Values : null; + + RouteValueDictionary mergedRouteValues = + RouteValuesHelpers.MergeRouteValues(actionName, controllerName, implicitRouteValues, routeValues, includeImplicitMvcValues: true); + + return new RedirectToRouteResult(null, mergedRouteValues, permanent: true); + } + + protected internal RedirectToRouteResult RedirectToRoute(object routeValues) + { + return RedirectToRoute(new RouteValueDictionary(routeValues)); + } + + protected internal RedirectToRouteResult RedirectToRoute(RouteValueDictionary routeValues) + { + return RedirectToRoute(null /* routeName */, routeValues); + } + + protected internal RedirectToRouteResult RedirectToRoute(string routeName) + { + return RedirectToRoute(routeName, (RouteValueDictionary)null); + } + + protected internal RedirectToRouteResult RedirectToRoute(string routeName, object routeValues) + { + return RedirectToRoute(routeName, new RouteValueDictionary(routeValues)); + } + + protected internal virtual RedirectToRouteResult RedirectToRoute(string routeName, RouteValueDictionary routeValues) + { + return new RedirectToRouteResult(routeName, RouteValuesHelpers.GetRouteValues(routeValues)); + } + + protected internal RedirectToRouteResult RedirectToRoutePermanent(object routeValues) + { + return RedirectToRoutePermanent(new RouteValueDictionary(routeValues)); + } + + protected internal RedirectToRouteResult RedirectToRoutePermanent(RouteValueDictionary routeValues) + { + return RedirectToRoutePermanent(null /* routeName */, routeValues); + } + + protected internal RedirectToRouteResult RedirectToRoutePermanent(string routeName) + { + return RedirectToRoutePermanent(routeName, (RouteValueDictionary)null); + } + + protected internal RedirectToRouteResult RedirectToRoutePermanent(string routeName, object routeValues) + { + return RedirectToRoutePermanent(routeName, new RouteValueDictionary(routeValues)); + } + + protected internal virtual RedirectToRouteResult RedirectToRoutePermanent(string routeName, RouteValueDictionary routeValues) + { + return new RedirectToRouteResult(routeName, RouteValuesHelpers.GetRouteValues(routeValues), permanent: true); + } + + protected internal bool TryUpdateModel<TModel>(TModel model) where TModel : class + { + return TryUpdateModel(model, null, null, null, ValueProvider); + } + + protected internal bool TryUpdateModel<TModel>(TModel model, string prefix) where TModel : class + { + return TryUpdateModel(model, prefix, null, null, ValueProvider); + } + + protected internal bool TryUpdateModel<TModel>(TModel model, string[] includeProperties) where TModel : class + { + return TryUpdateModel(model, null, includeProperties, null, ValueProvider); + } + + protected internal bool TryUpdateModel<TModel>(TModel model, string prefix, string[] includeProperties) where TModel : class + { + return TryUpdateModel(model, prefix, includeProperties, null, ValueProvider); + } + + protected internal bool TryUpdateModel<TModel>(TModel model, string prefix, string[] includeProperties, string[] excludeProperties) where TModel : class + { + return TryUpdateModel(model, prefix, includeProperties, excludeProperties, ValueProvider); + } + + protected internal bool TryUpdateModel<TModel>(TModel model, IValueProvider valueProvider) where TModel : class + { + return TryUpdateModel(model, null, null, null, valueProvider); + } + + protected internal bool TryUpdateModel<TModel>(TModel model, string prefix, IValueProvider valueProvider) where TModel : class + { + return TryUpdateModel(model, prefix, null, null, valueProvider); + } + + protected internal bool TryUpdateModel<TModel>(TModel model, string[] includeProperties, IValueProvider valueProvider) where TModel : class + { + return TryUpdateModel(model, null, includeProperties, null, valueProvider); + } + + protected internal bool TryUpdateModel<TModel>(TModel model, string prefix, string[] includeProperties, IValueProvider valueProvider) where TModel : class + { + return TryUpdateModel(model, prefix, includeProperties, null, valueProvider); + } + + protected internal bool TryUpdateModel<TModel>(TModel model, string prefix, string[] includeProperties, string[] excludeProperties, IValueProvider valueProvider) where TModel : class + { + if (model == null) + { + throw new ArgumentNullException("model"); + } + if (valueProvider == null) + { + throw new ArgumentNullException("valueProvider"); + } + + Predicate<string> propertyFilter = propertyName => BindAttribute.IsPropertyAllowed(propertyName, includeProperties, excludeProperties); + IModelBinder binder = Binders.GetBinder(typeof(TModel)); + + ModelBindingContext bindingContext = new ModelBindingContext() + { + ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, typeof(TModel)), + ModelName = prefix, + ModelState = ModelState, + PropertyFilter = propertyFilter, + ValueProvider = valueProvider + }; + binder.BindModel(ControllerContext, bindingContext); + return ModelState.IsValid; + } + + protected internal bool TryValidateModel(object model) + { + return TryValidateModel(model, null /* prefix */); + } + + protected internal bool TryValidateModel(object model, string prefix) + { + if (model == null) + { + throw new ArgumentNullException("model"); + } + + ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType()); + + foreach (ModelValidationResult validationResult in ModelValidator.GetModelValidator(metadata, ControllerContext).Validate(null)) + { + ModelState.AddModelError(DefaultModelBinder.CreateSubPropertyName(prefix, validationResult.MemberName), validationResult.Message); + } + + return ModelState.IsValid; + } + + protected internal void UpdateModel<TModel>(TModel model) where TModel : class + { + UpdateModel(model, null, null, null, ValueProvider); + } + + protected internal void UpdateModel<TModel>(TModel model, string prefix) where TModel : class + { + UpdateModel(model, prefix, null, null, ValueProvider); + } + + protected internal void UpdateModel<TModel>(TModel model, string[] includeProperties) where TModel : class + { + UpdateModel(model, null, includeProperties, null, ValueProvider); + } + + protected internal void UpdateModel<TModel>(TModel model, string prefix, string[] includeProperties) where TModel : class + { + UpdateModel(model, prefix, includeProperties, null, ValueProvider); + } + + protected internal void UpdateModel<TModel>(TModel model, string prefix, string[] includeProperties, string[] excludeProperties) where TModel : class + { + UpdateModel(model, prefix, includeProperties, excludeProperties, ValueProvider); + } + + protected internal void UpdateModel<TModel>(TModel model, IValueProvider valueProvider) where TModel : class + { + UpdateModel(model, null, null, null, valueProvider); + } + + protected internal void UpdateModel<TModel>(TModel model, string prefix, IValueProvider valueProvider) where TModel : class + { + UpdateModel(model, prefix, null, null, valueProvider); + } + + protected internal void UpdateModel<TModel>(TModel model, string[] includeProperties, IValueProvider valueProvider) where TModel : class + { + UpdateModel(model, null, includeProperties, null, valueProvider); + } + + protected internal void UpdateModel<TModel>(TModel model, string prefix, string[] includeProperties, IValueProvider valueProvider) where TModel : class + { + UpdateModel(model, prefix, includeProperties, null, valueProvider); + } + + protected internal void UpdateModel<TModel>(TModel model, string prefix, string[] includeProperties, string[] excludeProperties, IValueProvider valueProvider) where TModel : class + { + bool success = TryUpdateModel(model, prefix, includeProperties, excludeProperties, valueProvider); + if (!success) + { + string message = String.Format(CultureInfo.CurrentCulture, MvcResources.Controller_UpdateModel_UpdateUnsuccessful, + typeof(TModel).FullName); + throw new InvalidOperationException(message); + } + } + + protected internal void ValidateModel(object model) + { + ValidateModel(model, null /* prefix */); + } + + protected internal void ValidateModel(object model, string prefix) + { + if (!TryValidateModel(model, prefix)) + { + throw new InvalidOperationException( + String.Format( + CultureInfo.CurrentCulture, + MvcResources.Controller_Validate_ValidationFailed, + model.GetType().FullName)); + } + } + + protected internal ViewResult View() + { + return View(viewName: null, masterName: null, model: null); + } + + protected internal ViewResult View(object model) + { + return View(null /* viewName */, null /* masterName */, model); + } + + protected internal ViewResult View(string viewName) + { + return View(viewName, masterName: null, model: null); + } + + protected internal ViewResult View(string viewName, string masterName) + { + return View(viewName, masterName, null /* model */); + } + + protected internal ViewResult View(string viewName, object model) + { + return View(viewName, null /* masterName */, model); + } + + protected internal virtual ViewResult View(string viewName, string masterName, object model) + { + if (model != null) + { + ViewData.Model = model; + } + + return new ViewResult + { + ViewName = viewName, + MasterName = masterName, + ViewData = ViewData, + TempData = TempData, + ViewEngineCollection = ViewEngineCollection + }; + } + + [SuppressMessage("Microsoft.Naming", "CA1719:ParameterNamesShouldNotMatchMemberNames", MessageId = "0#", Justification = "The method name 'View' is a convenient shorthand for 'CreateViewResult'.")] + protected internal ViewResult View(IView view) + { + return View(view, null /* model */); + } + + [SuppressMessage("Microsoft.Naming", "CA1719:ParameterNamesShouldNotMatchMemberNames", MessageId = "0#", Justification = "The method name 'View' is a convenient shorthand for 'CreateViewResult'.")] + protected internal virtual ViewResult View(IView view, object model) + { + if (model != null) + { + ViewData.Model = model; + } + + return new ViewResult + { + View = view, + ViewData = ViewData, + TempData = TempData + }; + } + + IAsyncResult IAsyncController.BeginExecute(RequestContext requestContext, AsyncCallback callback, object state) + { + return BeginExecute(requestContext, callback, state); + } + + void IAsyncController.EndExecute(IAsyncResult asyncResult) + { + EndExecute(asyncResult); + } + + protected virtual IAsyncResult BeginExecute(RequestContext requestContext, AsyncCallback callback, object state) + { + if (DisableAsyncSupport) + { + // For backwards compat, we can disallow async support and just chain to the sync Execute() function. + Action action = () => + { + Execute(requestContext); + }; + + return AsyncResultWrapper.BeginSynchronous(callback, state, action, _executeTag); + } + else + { + if (requestContext == null) + { + throw new ArgumentNullException("requestContext"); + } + + // Support Asynchronous behavior. + // Execute/ExecuteCore are no longer called. + + VerifyExecuteCalledOnce(); + Initialize(requestContext); + return AsyncResultWrapper.Begin(callback, state, BeginExecuteCore, EndExecuteCore, _executeTag); + } + } + + protected virtual IAsyncResult BeginExecuteCore(AsyncCallback callback, object state) + { + // If code in this method needs to be updated, please also check the ExecuteCore() method + // of Controller to see if that code also must be updated. + PossiblyLoadTempData(); + try + { + string actionName = RouteData.GetRequiredString("action"); + IActionInvoker invoker = ActionInvoker; + IAsyncActionInvoker asyncInvoker = invoker as IAsyncActionInvoker; + if (asyncInvoker != null) + { + // asynchronous invocation + BeginInvokeDelegate beginDelegate = delegate(AsyncCallback asyncCallback, object asyncState) + { + return asyncInvoker.BeginInvokeAction(ControllerContext, actionName, asyncCallback, asyncState); + }; + + EndInvokeDelegate endDelegate = delegate(IAsyncResult asyncResult) + { + if (!asyncInvoker.EndInvokeAction(asyncResult)) + { + HandleUnknownAction(actionName); + } + }; + + return AsyncResultWrapper.Begin(callback, state, beginDelegate, endDelegate, _executeCoreTag); + } + else + { + // synchronous invocation + Action action = () => + { + if (!invoker.InvokeAction(ControllerContext, actionName)) + { + HandleUnknownAction(actionName); + } + }; + return AsyncResultWrapper.BeginSynchronous(callback, state, action, _executeCoreTag); + } + } + catch + { + PossiblySaveTempData(); + throw; + } + } + + protected virtual void EndExecute(IAsyncResult asyncResult) + { + AsyncResultWrapper.End(asyncResult, _executeTag); + } + + protected virtual void EndExecuteCore(IAsyncResult asyncResult) + { + // If code in this method needs to be updated, please also check the ExecuteCore() method + // of Controller to see if that code also must be updated. + + try + { + AsyncResultWrapper.End(asyncResult, _executeCoreTag); + } + finally + { + PossiblySaveTempData(); + } + } + + #region IActionFilter Members + + void IActionFilter.OnActionExecuting(ActionExecutingContext filterContext) + { + OnActionExecuting(filterContext); + } + + void IActionFilter.OnActionExecuted(ActionExecutedContext filterContext) + { + OnActionExecuted(filterContext); + } + + #endregion + + #region IAuthorizationFilter Members + + void IAuthorizationFilter.OnAuthorization(AuthorizationContext filterContext) + { + OnAuthorization(filterContext); + } + + #endregion + + #region IExceptionFilter Members + + void IExceptionFilter.OnException(ExceptionContext filterContext) + { + OnException(filterContext); + } + + #endregion + + #region IResultFilter Members + + void IResultFilter.OnResultExecuting(ResultExecutingContext filterContext) + { + OnResultExecuting(filterContext); + } + + void IResultFilter.OnResultExecuted(ResultExecutedContext filterContext) + { + OnResultExecuted(filterContext); + } + + #endregion + } +} diff --git a/src/System.Web.Mvc/ControllerActionInvoker.cs b/src/System.Web.Mvc/ControllerActionInvoker.cs new file mode 100644 index 00000000..64310b72 --- /dev/null +++ b/src/System.Web.Mvc/ControllerActionInvoker.cs @@ -0,0 +1,372 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Web.Mvc.Properties; +using Microsoft.Web.Infrastructure.DynamicValidationHelper; + +namespace System.Web.Mvc +{ + public class ControllerActionInvoker : IActionInvoker + { + private static readonly ControllerDescriptorCache _staticDescriptorCache = new ControllerDescriptorCache(); + + private ModelBinderDictionary _binders; + private Func<ControllerContext, ActionDescriptor, IEnumerable<Filter>> _getFiltersThunk = FilterProviders.Providers.GetFilters; + private ControllerDescriptorCache _instanceDescriptorCache; + + public ControllerActionInvoker() + { + } + + internal ControllerActionInvoker(params object[] filters) + : this() + { + if (filters != null) + { + _getFiltersThunk = (cc, ad) => filters.Select(f => new Filter(f, FilterScope.Action, null)); + } + } + + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "Property is settable so that the dictionary can be provided for unit testing purposes.")] + protected internal ModelBinderDictionary Binders + { + get + { + if (_binders == null) + { + _binders = ModelBinders.Binders; + } + return _binders; + } + set { _binders = value; } + } + + internal ControllerDescriptorCache DescriptorCache + { + get + { + if (_instanceDescriptorCache == null) + { + _instanceDescriptorCache = _staticDescriptorCache; + } + return _instanceDescriptorCache; + } + set { _instanceDescriptorCache = value; } + } + + protected virtual ActionResult CreateActionResult(ControllerContext controllerContext, ActionDescriptor actionDescriptor, object actionReturnValue) + { + if (actionReturnValue == null) + { + return new EmptyResult(); + } + + ActionResult actionResult = (actionReturnValue as ActionResult) ?? + new ContentResult { Content = Convert.ToString(actionReturnValue, CultureInfo.InvariantCulture) }; + return actionResult; + } + + protected virtual ControllerDescriptor GetControllerDescriptor(ControllerContext controllerContext) + { + Type controllerType = controllerContext.Controller.GetType(); + ControllerDescriptor controllerDescriptor = DescriptorCache.GetDescriptor(controllerType, () => new ReflectedControllerDescriptor(controllerType)); + return controllerDescriptor; + } + + protected virtual ActionDescriptor FindAction(ControllerContext controllerContext, ControllerDescriptor controllerDescriptor, string actionName) + { + ActionDescriptor actionDescriptor = controllerDescriptor.FindAction(controllerContext, actionName); + return actionDescriptor; + } + + protected virtual FilterInfo GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor) + { + return new FilterInfo(_getFiltersThunk(controllerContext, actionDescriptor)); + } + + private IModelBinder GetModelBinder(ParameterDescriptor parameterDescriptor) + { + // look on the parameter itself, then look in the global table + return parameterDescriptor.BindingInfo.Binder ?? Binders.GetBinder(parameterDescriptor.ParameterType); + } + + protected virtual object GetParameterValue(ControllerContext controllerContext, ParameterDescriptor parameterDescriptor) + { + // collect all of the necessary binding properties + Type parameterType = parameterDescriptor.ParameterType; + IModelBinder binder = GetModelBinder(parameterDescriptor); + IValueProvider valueProvider = controllerContext.Controller.ValueProvider; + string parameterName = parameterDescriptor.BindingInfo.Prefix ?? parameterDescriptor.ParameterName; + Predicate<string> propertyFilter = GetPropertyFilter(parameterDescriptor); + + // finally, call into the binder + ModelBindingContext bindingContext = new ModelBindingContext() + { + FallbackToEmptyPrefix = (parameterDescriptor.BindingInfo.Prefix == null), // only fall back if prefix not specified + ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, parameterType), + ModelName = parameterName, + ModelState = controllerContext.Controller.ViewData.ModelState, + PropertyFilter = propertyFilter, + ValueProvider = valueProvider + }; + + object result = binder.BindModel(controllerContext, bindingContext); + return result ?? parameterDescriptor.DefaultValue; + } + + protected virtual IDictionary<string, object> GetParameterValues(ControllerContext controllerContext, ActionDescriptor actionDescriptor) + { + Dictionary<string, object> parametersDict = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); + ParameterDescriptor[] parameterDescriptors = actionDescriptor.GetParameters(); + + foreach (ParameterDescriptor parameterDescriptor in parameterDescriptors) + { + parametersDict[parameterDescriptor.ParameterName] = GetParameterValue(controllerContext, parameterDescriptor); + } + return parametersDict; + } + + private static Predicate<string> GetPropertyFilter(ParameterDescriptor parameterDescriptor) + { + ParameterBindingInfo bindingInfo = parameterDescriptor.BindingInfo; + return propertyName => BindAttribute.IsPropertyAllowed(propertyName, bindingInfo.Include.ToArray(), bindingInfo.Exclude.ToArray()); + } + + public virtual bool InvokeAction(ControllerContext controllerContext, string actionName) + { + if (controllerContext == null) + { + throw new ArgumentNullException("controllerContext"); + } + if (String.IsNullOrEmpty(actionName)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "actionName"); + } + + ControllerDescriptor controllerDescriptor = GetControllerDescriptor(controllerContext); + ActionDescriptor actionDescriptor = FindAction(controllerContext, controllerDescriptor, actionName); + if (actionDescriptor != null) + { + FilterInfo filterInfo = GetFilters(controllerContext, actionDescriptor); + + try + { + AuthorizationContext authContext = InvokeAuthorizationFilters(controllerContext, filterInfo.AuthorizationFilters, actionDescriptor); + if (authContext.Result != null) + { + // the auth filter signaled that we should let it short-circuit the request + InvokeActionResult(controllerContext, authContext.Result); + } + else + { + if (controllerContext.Controller.ValidateRequest) + { + ValidateRequest(controllerContext); + } + + IDictionary<string, object> parameters = GetParameterValues(controllerContext, actionDescriptor); + ActionExecutedContext postActionContext = InvokeActionMethodWithFilters(controllerContext, filterInfo.ActionFilters, actionDescriptor, parameters); + InvokeActionResultWithFilters(controllerContext, filterInfo.ResultFilters, postActionContext.Result); + } + } + catch (ThreadAbortException) + { + // This type of exception occurs as a result of Response.Redirect(), but we special-case so that + // the filters don't see this as an error. + throw; + } + catch (Exception ex) + { + // something blew up, so execute the exception filters + ExceptionContext exceptionContext = InvokeExceptionFilters(controllerContext, filterInfo.ExceptionFilters, ex); + if (!exceptionContext.ExceptionHandled) + { + throw; + } + InvokeActionResult(controllerContext, exceptionContext.Result); + } + + return true; + } + + // notify controller that no method matched + return false; + } + + protected virtual ActionResult InvokeActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary<string, object> parameters) + { + object returnValue = actionDescriptor.Execute(controllerContext, parameters); + ActionResult result = CreateActionResult(controllerContext, actionDescriptor, returnValue); + return result; + } + + internal static ActionExecutedContext InvokeActionMethodFilter(IActionFilter filter, ActionExecutingContext preContext, Func<ActionExecutedContext> continuation) + { + filter.OnActionExecuting(preContext); + if (preContext.Result != null) + { + return new ActionExecutedContext(preContext, preContext.ActionDescriptor, true /* canceled */, null /* exception */) + { + Result = preContext.Result + }; + } + + bool wasError = false; + ActionExecutedContext postContext = null; + try + { + postContext = continuation(); + } + catch (ThreadAbortException) + { + // This type of exception occurs as a result of Response.Redirect(), but we special-case so that + // the filters don't see this as an error. + postContext = new ActionExecutedContext(preContext, preContext.ActionDescriptor, false /* canceled */, null /* exception */); + filter.OnActionExecuted(postContext); + throw; + } + catch (Exception ex) + { + wasError = true; + postContext = new ActionExecutedContext(preContext, preContext.ActionDescriptor, false /* canceled */, ex); + filter.OnActionExecuted(postContext); + if (!postContext.ExceptionHandled) + { + throw; + } + } + if (!wasError) + { + filter.OnActionExecuted(postContext); + } + return postContext; + } + + protected virtual ActionExecutedContext InvokeActionMethodWithFilters(ControllerContext controllerContext, IList<IActionFilter> filters, ActionDescriptor actionDescriptor, IDictionary<string, object> parameters) + { + ActionExecutingContext preContext = new ActionExecutingContext(controllerContext, actionDescriptor, parameters); + Func<ActionExecutedContext> continuation = () => + new ActionExecutedContext(controllerContext, actionDescriptor, false /* canceled */, null /* exception */) + { + Result = InvokeActionMethod(controllerContext, actionDescriptor, parameters) + }; + + // need to reverse the filter list because the continuations are built up backward + Func<ActionExecutedContext> thunk = filters.Reverse().Aggregate(continuation, + (next, filter) => () => InvokeActionMethodFilter(filter, preContext, next)); + return thunk(); + } + + protected virtual void InvokeActionResult(ControllerContext controllerContext, ActionResult actionResult) + { + actionResult.ExecuteResult(controllerContext); + } + + internal static ResultExecutedContext InvokeActionResultFilter(IResultFilter filter, ResultExecutingContext preContext, Func<ResultExecutedContext> continuation) + { + filter.OnResultExecuting(preContext); + if (preContext.Cancel) + { + return new ResultExecutedContext(preContext, preContext.Result, true /* canceled */, null /* exception */); + } + + bool wasError = false; + ResultExecutedContext postContext = null; + try + { + postContext = continuation(); + } + catch (ThreadAbortException) + { + // This type of exception occurs as a result of Response.Redirect(), but we special-case so that + // the filters don't see this as an error. + postContext = new ResultExecutedContext(preContext, preContext.Result, false /* canceled */, null /* exception */); + filter.OnResultExecuted(postContext); + throw; + } + catch (Exception ex) + { + wasError = true; + postContext = new ResultExecutedContext(preContext, preContext.Result, false /* canceled */, ex); + filter.OnResultExecuted(postContext); + if (!postContext.ExceptionHandled) + { + throw; + } + } + if (!wasError) + { + filter.OnResultExecuted(postContext); + } + return postContext; + } + + protected virtual ResultExecutedContext InvokeActionResultWithFilters(ControllerContext controllerContext, IList<IResultFilter> filters, ActionResult actionResult) + { + ResultExecutingContext preContext = new ResultExecutingContext(controllerContext, actionResult); + Func<ResultExecutedContext> continuation = delegate + { + InvokeActionResult(controllerContext, actionResult); + return new ResultExecutedContext(controllerContext, actionResult, false /* canceled */, null /* exception */); + }; + + // need to reverse the filter list because the continuations are built up backward + Func<ResultExecutedContext> thunk = filters.Reverse().Aggregate(continuation, + (next, filter) => () => InvokeActionResultFilter(filter, preContext, next)); + return thunk(); + } + + protected virtual AuthorizationContext InvokeAuthorizationFilters(ControllerContext controllerContext, IList<IAuthorizationFilter> filters, ActionDescriptor actionDescriptor) + { + AuthorizationContext context = new AuthorizationContext(controllerContext, actionDescriptor); + foreach (IAuthorizationFilter filter in filters) + { + filter.OnAuthorization(context); + // short-circuit evaluation + if (context.Result != null) + { + break; + } + } + + return context; + } + + protected virtual ExceptionContext InvokeExceptionFilters(ControllerContext controllerContext, IList<IExceptionFilter> filters, Exception exception) + { + ExceptionContext context = new ExceptionContext(controllerContext, exception); + foreach (IExceptionFilter filter in filters.Reverse()) + { + filter.OnException(context); + } + + return context; + } + + internal static void ValidateRequest(ControllerContext controllerContext) + { + if (controllerContext.IsChildAction) + { + return; + } + + // DevDiv 214040: Enable Request Validation by default for all controller requests + // + // Earlier versions of this method dereferenced Request.RawUrl to force validation of + // that field. This was necessary for Routing before ASP.NET v4, which read the incoming + // path from RawUrl. Request validation has been moved earlier in the pipeline by default and + // routing no longer consumes this property, so we don't have to either. + + // Tolerate null HttpContext for testing + HttpContext currentContext = HttpContext.Current; + if (currentContext != null) + { + ValidationUtility.EnableDynamicValidation(currentContext); + } + + controllerContext.HttpContext.Request.ValidateInput(); + } + } +} diff --git a/src/System.Web.Mvc/ControllerBase.cs b/src/System.Web.Mvc/ControllerBase.cs new file mode 100644 index 00000000..f58696c6 --- /dev/null +++ b/src/System.Web.Mvc/ControllerBase.cs @@ -0,0 +1,130 @@ +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Web.Mvc.Async; +using System.Web.Mvc.Properties; +using System.Web.Routing; +using System.Web.WebPages.Scope; + +namespace System.Web.Mvc +{ + public abstract class ControllerBase : IController + { + private readonly SingleEntryGate _executeWasCalledGate = new SingleEntryGate(); + + private DynamicViewDataDictionary _dynamicViewDataDictionary; + private TempDataDictionary _tempDataDictionary; + private bool _validateRequest = true; + private IValueProvider _valueProvider; + private ViewDataDictionary _viewDataDictionary; + + public ControllerContext ControllerContext { get; set; } + + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This property is settable so that unit tests can provide mock implementations.")] + public TempDataDictionary TempData + { + get + { + if (ControllerContext != null && ControllerContext.IsChildAction) + { + return ControllerContext.ParentActionViewContext.TempData; + } + if (_tempDataDictionary == null) + { + _tempDataDictionary = new TempDataDictionary(); + } + return _tempDataDictionary; + } + set { _tempDataDictionary = value; } + } + + public bool ValidateRequest + { + get { return _validateRequest; } + set { _validateRequest = value; } + } + + public IValueProvider ValueProvider + { + get + { + if (_valueProvider == null) + { + _valueProvider = ValueProviderFactories.Factories.GetValueProvider(ControllerContext); + } + return _valueProvider; + } + set { _valueProvider = value; } + } + + public dynamic ViewBag + { + get + { + if (_dynamicViewDataDictionary == null) + { + _dynamicViewDataDictionary = new DynamicViewDataDictionary(() => ViewData); + } + return _dynamicViewDataDictionary; + } + } + + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This property is settable so that unit tests can provide mock implementations.")] + public ViewDataDictionary ViewData + { + get + { + if (_viewDataDictionary == null) + { + _viewDataDictionary = new ViewDataDictionary(); + } + return _viewDataDictionary; + } + set { _viewDataDictionary = value; } + } + + protected virtual void Execute(RequestContext requestContext) + { + if (requestContext == null) + { + throw new ArgumentNullException("requestContext"); + } + if (requestContext.HttpContext == null) + { + throw new ArgumentException(MvcResources.ControllerBase_CannotExecuteWithNullHttpContext, "requestContext"); + } + + VerifyExecuteCalledOnce(); + Initialize(requestContext); + + using (ScopeStorage.CreateTransientScope()) + { + ExecuteCore(); + } + } + + protected abstract void ExecuteCore(); + + protected virtual void Initialize(RequestContext requestContext) + { + ControllerContext = new ControllerContext(requestContext, this); + } + + internal void VerifyExecuteCalledOnce() + { + if (!_executeWasCalledGate.TryEnter()) + { + string message = String.Format(CultureInfo.CurrentCulture, MvcResources.ControllerBase_CannotHandleMultipleRequests, GetType()); + throw new InvalidOperationException(message); + } + } + + #region IController Members + + void IController.Execute(RequestContext requestContext) + { + Execute(requestContext); + } + + #endregion + } +} diff --git a/src/System.Web.Mvc/ControllerBuilder.cs b/src/System.Web.Mvc/ControllerBuilder.cs new file mode 100644 index 00000000..004f5361 --- /dev/null +++ b/src/System.Web.Mvc/ControllerBuilder.cs @@ -0,0 +1,88 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + public class ControllerBuilder + { + private static ControllerBuilder _instance = new ControllerBuilder(); + private Func<IControllerFactory> _factoryThunk = () => null; + private HashSet<string> _namespaces = new HashSet<string>(StringComparer.OrdinalIgnoreCase); + private IResolver<IControllerFactory> _serviceResolver; + + public ControllerBuilder() + : this(null) + { + } + + internal ControllerBuilder(IResolver<IControllerFactory> serviceResolver) + { + _serviceResolver = serviceResolver ?? new SingleServiceResolver<IControllerFactory>( + () => _factoryThunk(), + new DefaultControllerFactory { ControllerBuilder = this }, + "ControllerBuilder.GetControllerFactory"); + } + + public static ControllerBuilder Current + { + get { return _instance; } + } + + public HashSet<string> DefaultNamespaces + { + get { return _namespaces; } + } + + [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Calling method multiple times might return different objects.")] + public IControllerFactory GetControllerFactory() + { + return _serviceResolver.Current; + } + + public void SetControllerFactory(IControllerFactory controllerFactory) + { + if (controllerFactory == null) + { + throw new ArgumentNullException("controllerFactory"); + } + + _factoryThunk = () => controllerFactory; + } + + public void SetControllerFactory(Type controllerFactoryType) + { + if (controllerFactoryType == null) + { + throw new ArgumentNullException("controllerFactoryType"); + } + if (!typeof(IControllerFactory).IsAssignableFrom(controllerFactoryType)) + { + throw new ArgumentException( + String.Format( + CultureInfo.CurrentCulture, + MvcResources.ControllerBuilder_MissingIControllerFactory, + controllerFactoryType), + "controllerFactoryType"); + } + + _factoryThunk = delegate + { + try + { + return (IControllerFactory)Activator.CreateInstance(controllerFactoryType); + } + catch (Exception ex) + { + throw new InvalidOperationException( + String.Format( + CultureInfo.CurrentCulture, + MvcResources.ControllerBuilder_ErrorCreatingControllerFactory, + controllerFactoryType), + ex); + } + }; + } + } +} diff --git a/src/System.Web.Mvc/ControllerContext.cs b/src/System.Web.Mvc/ControllerContext.cs new file mode 100644 index 00000000..54bfcd46 --- /dev/null +++ b/src/System.Web.Mvc/ControllerContext.cs @@ -0,0 +1,133 @@ +using System.Diagnostics.CodeAnalysis; +using System.Web.Routing; +using System.Web.WebPages; + +namespace System.Web.Mvc +{ + // Though many of the properties on ControllerContext and its subclassed types are virtual, there are still sealed + // properties (like ControllerContext.RequestContext, ActionExecutingContext.Result, etc.). If these properties + // were virtual, a mocking framework might override them with incorrect behavior (property getters would return + // null, property setters would be no-ops). By sealing these properties, we are forcing them to have the default + // "get or store a value" semantics that they were intended to have. + + public class ControllerContext + { + internal const string ParentActionViewContextToken = "ParentActionViewContext"; + private HttpContextBase _httpContext; + private RequestContext _requestContext; + private RouteData _routeData; + + // parameterless constructor used for mocking + public ControllerContext() + { + } + + // copy constructor - allows for subclassed types to take an existing ControllerContext as a parameter + // and we'll automatically set the appropriate properties + [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "The virtual property setters are only to support mocking frameworks, in which case this constructor shouldn't be called anyway.")] + protected ControllerContext(ControllerContext controllerContext) + { + if (controllerContext == null) + { + throw new ArgumentNullException("controllerContext"); + } + + Controller = controllerContext.Controller; + RequestContext = controllerContext.RequestContext; + } + + public ControllerContext(HttpContextBase httpContext, RouteData routeData, ControllerBase controller) + : this(new RequestContext(httpContext, routeData), controller) + { + } + + [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "The virtual property setters are only to support mocking frameworks, in which case this constructor shouldn't be called anyway.")] + public ControllerContext(RequestContext requestContext, ControllerBase controller) + { + if (requestContext == null) + { + throw new ArgumentNullException("requestContext"); + } + if (controller == null) + { + throw new ArgumentNullException("controller"); + } + + RequestContext = requestContext; + Controller = controller; + } + + public virtual ControllerBase Controller { get; set; } + + public IDisplayMode DisplayMode + { + get { return DisplayModeProvider.GetDisplayMode(HttpContext); } + set { DisplayModeProvider.SetDisplayMode(HttpContext, value); } + } + + public virtual HttpContextBase HttpContext + { + get + { + if (_httpContext == null) + { + _httpContext = (_requestContext != null) ? _requestContext.HttpContext : new EmptyHttpContext(); + } + return _httpContext; + } + set { _httpContext = value; } + } + + public virtual bool IsChildAction + { + get + { + RouteData routeData = RouteData; + if (routeData == null) + { + return false; + } + return routeData.DataTokens.ContainsKey(ParentActionViewContextToken); + } + } + + public ViewContext ParentActionViewContext + { + get { return RouteData.DataTokens[ParentActionViewContextToken] as ViewContext; } + } + + public RequestContext RequestContext + { + get + { + if (_requestContext == null) + { + // still need explicit calls to constructors since the property getters are virtual and might return null + HttpContextBase httpContext = HttpContext ?? new EmptyHttpContext(); + RouteData routeData = RouteData ?? new RouteData(); + + _requestContext = new RequestContext(httpContext, routeData); + } + return _requestContext; + } + set { _requestContext = value; } + } + + public virtual RouteData RouteData + { + get + { + if (_routeData == null) + { + _routeData = (_requestContext != null) ? _requestContext.RouteData : new RouteData(); + } + return _routeData; + } + set { _routeData = value; } + } + + private sealed class EmptyHttpContext : HttpContextBase + { + } + } +} diff --git a/src/System.Web.Mvc/ControllerDescriptor.cs b/src/System.Web.Mvc/ControllerDescriptor.cs new file mode 100644 index 00000000..45c0af9b --- /dev/null +++ b/src/System.Web.Mvc/ControllerDescriptor.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; + +namespace System.Web.Mvc +{ + public abstract class ControllerDescriptor : ICustomAttributeProvider, IUniquelyIdentifiable + { + private readonly Lazy<string> _uniqueId; + + protected ControllerDescriptor() + { + _uniqueId = new Lazy<string>(CreateUniqueId); + } + + public virtual string ControllerName + { + get + { + string typeName = ControllerType.Name; + if (typeName.EndsWith("Controller", StringComparison.OrdinalIgnoreCase)) + { + return typeName.Substring(0, typeName.Length - "Controller".Length); + } + + return typeName; + } + } + + public abstract Type ControllerType { get; } + + [SuppressMessage("Microsoft.Security", "CA2119:SealMethodsThatSatisfyPrivateInterfaces", Justification = "This is overridden elsewhere in System.Web.Mvc")] + public virtual string UniqueId + { + get { return _uniqueId.Value; } + } + + private string CreateUniqueId() + { + return DescriptorUtil.CreateUniqueId(GetType(), ControllerName, ControllerType); + } + + public abstract ActionDescriptor FindAction(ControllerContext controllerContext, string actionName); + + public abstract ActionDescriptor[] GetCanonicalActions(); + + public virtual object[] GetCustomAttributes(bool inherit) + { + return GetCustomAttributes(typeof(object), inherit); + } + + public virtual object[] GetCustomAttributes(Type attributeType, bool inherit) + { + if (attributeType == null) + { + throw new ArgumentNullException("attributeType"); + } + + return (object[])Array.CreateInstance(attributeType, 0); + } + + public virtual IEnumerable<FilterAttribute> GetFilterAttributes(bool useCache) + { + return GetCustomAttributes(typeof(FilterAttribute), inherit: true).Cast<FilterAttribute>(); + } + + public virtual bool IsDefined(Type attributeType, bool inherit) + { + if (attributeType == null) + { + throw new ArgumentNullException("attributeType"); + } + + return false; + } + } +} diff --git a/src/System.Web.Mvc/ControllerDescriptorCache.cs b/src/System.Web.Mvc/ControllerDescriptorCache.cs new file mode 100644 index 00000000..538e03a0 --- /dev/null +++ b/src/System.Web.Mvc/ControllerDescriptorCache.cs @@ -0,0 +1,14 @@ +namespace System.Web.Mvc +{ + internal sealed class ControllerDescriptorCache : ReaderWriterCache<Type, ControllerDescriptor> + { + public ControllerDescriptorCache() + { + } + + public ControllerDescriptor GetDescriptor(Type controllerType, Func<ControllerDescriptor> creator) + { + return FetchOrCreateItem(controllerType, creator); + } + } +} diff --git a/src/System.Web.Mvc/ControllerInstanceFilterProvider.cs b/src/System.Web.Mvc/ControllerInstanceFilterProvider.cs new file mode 100644 index 00000000..89ee3350 --- /dev/null +++ b/src/System.Web.Mvc/ControllerInstanceFilterProvider.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace System.Web.Mvc +{ + public class ControllerInstanceFilterProvider : IFilterProvider + { + public IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor) + { + if (controllerContext.Controller != null) + { + // Use FilterScope.First and Order of Int32.MinValue to ensure controller instance methods always run first + yield return new Filter(controllerContext.Controller, FilterScope.First, Int32.MinValue); + } + } + } +} diff --git a/src/System.Web.Mvc/ControllerTypeCache.cs b/src/System.Web.Mvc/ControllerTypeCache.cs new file mode 100644 index 00000000..7b560f4a --- /dev/null +++ b/src/System.Web.Mvc/ControllerTypeCache.cs @@ -0,0 +1,138 @@ +using System.Collections.Generic; +using System.Linq; + +namespace System.Web.Mvc +{ + internal sealed class ControllerTypeCache + { + private const string TypeCacheName = "MVC-ControllerTypeCache.xml"; + + private Dictionary<string, ILookup<string, Type>> _cache; + private object _lockObj = new object(); + + internal int Count + { + get + { + int count = 0; + foreach (var lookup in _cache.Values) + { + foreach (var grouping in lookup) + { + count += grouping.Count(); + } + } + return count; + } + } + + public void EnsureInitialized(IBuildManager buildManager) + { + if (_cache == null) + { + lock (_lockObj) + { + if (_cache == null) + { + List<Type> controllerTypes = TypeCacheUtil.GetFilteredTypesFromAssemblies(TypeCacheName, IsControllerType, buildManager); + var groupedByName = controllerTypes.GroupBy( + t => t.Name.Substring(0, t.Name.Length - "Controller".Length), + StringComparer.OrdinalIgnoreCase); + _cache = groupedByName.ToDictionary( + g => g.Key, + g => g.ToLookup(t => t.Namespace ?? String.Empty, StringComparer.OrdinalIgnoreCase), + StringComparer.OrdinalIgnoreCase); + } + } + } + } + + public ICollection<Type> GetControllerTypes(string controllerName, HashSet<string> namespaces) + { + HashSet<Type> matchingTypes = new HashSet<Type>(); + + ILookup<string, Type> namespaceLookup; + if (_cache.TryGetValue(controllerName, out namespaceLookup)) + { + // this friendly name was located in the cache, now cycle through namespaces + if (namespaces != null) + { + foreach (string requestedNamespace in namespaces) + { + foreach (var targetNamespaceGrouping in namespaceLookup) + { + if (IsNamespaceMatch(requestedNamespace, targetNamespaceGrouping.Key)) + { + matchingTypes.UnionWith(targetNamespaceGrouping); + } + } + } + } + else + { + // if the namespaces parameter is null, search *every* namespace + foreach (var namespaceGroup in namespaceLookup) + { + matchingTypes.UnionWith(namespaceGroup); + } + } + } + + return matchingTypes; + } + + internal static bool IsControllerType(Type t) + { + return + t != null && + t.IsPublic && + t.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) && + !t.IsAbstract && + typeof(IController).IsAssignableFrom(t); + } + + internal static bool IsNamespaceMatch(string requestedNamespace, string targetNamespace) + { + // degenerate cases + if (requestedNamespace == null) + { + return false; + } + else if (requestedNamespace.Length == 0) + { + return true; + } + + if (!requestedNamespace.EndsWith(".*", StringComparison.OrdinalIgnoreCase)) + { + // looking for exact namespace match + return String.Equals(requestedNamespace, targetNamespace, StringComparison.OrdinalIgnoreCase); + } + else + { + // looking for exact or sub-namespace match + requestedNamespace = requestedNamespace.Substring(0, requestedNamespace.Length - ".*".Length); + if (!targetNamespace.StartsWith(requestedNamespace, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (requestedNamespace.Length == targetNamespace.Length) + { + // exact match + return true; + } + else if (targetNamespace[requestedNamespace.Length] == '.') + { + // good prefix match, e.g. requestedNamespace = "Foo.Bar" and targetNamespace = "Foo.Bar.Baz" + return true; + } + else + { + // bad prefix match, e.g. requestedNamespace = "Foo.Bar" and targetNamespace = "Foo.Bar2" + return false; + } + } + } + } +} diff --git a/src/System.Web.Mvc/CustomModelBinderAttribute.cs b/src/System.Web.Mvc/CustomModelBinderAttribute.cs new file mode 100644 index 00000000..c2e676f9 --- /dev/null +++ b/src/System.Web.Mvc/CustomModelBinderAttribute.cs @@ -0,0 +1,13 @@ +using System.Diagnostics.CodeAnalysis; + +namespace System.Web.Mvc +{ + [AttributeUsage(ValidTargets, AllowMultiple = false, Inherited = false)] + public abstract class CustomModelBinderAttribute : Attribute + { + internal const AttributeTargets ValidTargets = AttributeTargets.Class | AttributeTargets.Enum | AttributeTargets.Interface | AttributeTargets.Parameter | AttributeTargets.Struct; + + [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This method can potentially perform a non-trivial amount of work.")] + public abstract IModelBinder GetBinder(); + } +} diff --git a/src/System.Web.Mvc/DataAnnotationsModelMetadata.cs b/src/System.Web.Mvc/DataAnnotationsModelMetadata.cs new file mode 100644 index 00000000..9b8156e3 --- /dev/null +++ b/src/System.Web.Mvc/DataAnnotationsModelMetadata.cs @@ -0,0 +1,60 @@ +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using System.Reflection; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + public class DataAnnotationsModelMetadata : ModelMetadata + { + private DisplayColumnAttribute _displayColumnAttribute; + + public DataAnnotationsModelMetadata(DataAnnotationsModelMetadataProvider provider, Type containerType, + Func<object> modelAccessor, Type modelType, string propertyName, + DisplayColumnAttribute displayColumnAttribute) + : base(provider, containerType, modelAccessor, modelType, propertyName) + { + _displayColumnAttribute = displayColumnAttribute; + } + + protected override string GetSimpleDisplayText() + { + if (Model != null) + { + if (_displayColumnAttribute != null && !String.IsNullOrEmpty(_displayColumnAttribute.DisplayColumn)) + { + PropertyInfo displayColumnProperty = ModelType.GetProperty(_displayColumnAttribute.DisplayColumn, BindingFlags.Public | BindingFlags.IgnoreCase | BindingFlags.Instance); + ValidateDisplayColumnAttribute(_displayColumnAttribute, displayColumnProperty, ModelType); + + object simpleDisplayTextValue = displayColumnProperty.GetValue(Model, new object[0]); + if (simpleDisplayTextValue != null) + { + return simpleDisplayTextValue.ToString(); + } + } + } + + return base.GetSimpleDisplayText(); + } + + private static void ValidateDisplayColumnAttribute(DisplayColumnAttribute displayColumnAttribute, PropertyInfo displayColumnProperty, Type modelType) + { + if (displayColumnProperty == null) + { + throw new InvalidOperationException( + String.Format( + CultureInfo.CurrentCulture, + MvcResources.DataAnnotationsModelMetadataProvider_UnknownProperty, + modelType.FullName, displayColumnAttribute.DisplayColumn)); + } + if (displayColumnProperty.GetGetMethod() == null) + { + throw new InvalidOperationException( + String.Format( + CultureInfo.CurrentCulture, + MvcResources.DataAnnotationsModelMetadataProvider_UnreadableProperty, + modelType.FullName, displayColumnAttribute.DisplayColumn)); + } + } + } +} diff --git a/src/System.Web.Mvc/DataAnnotationsModelMetadataProvider.cs b/src/System.Web.Mvc/DataAnnotationsModelMetadataProvider.cs new file mode 100644 index 00000000..7ffef9c7 --- /dev/null +++ b/src/System.Web.Mvc/DataAnnotationsModelMetadataProvider.cs @@ -0,0 +1,115 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Linq; + +namespace System.Web.Mvc +{ + public class DataAnnotationsModelMetadataProvider : AssociatedMetadataProvider + { + protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName) + { + List<Attribute> attributeList = new List<Attribute>(attributes); + DisplayColumnAttribute displayColumnAttribute = attributeList.OfType<DisplayColumnAttribute>().FirstOrDefault(); + DataAnnotationsModelMetadata result = new DataAnnotationsModelMetadata(this, containerType, modelAccessor, modelType, propertyName, displayColumnAttribute); + + // Do [HiddenInput] before [UIHint], so you can override the template hint + HiddenInputAttribute hiddenInputAttribute = attributeList.OfType<HiddenInputAttribute>().FirstOrDefault(); + if (hiddenInputAttribute != null) + { + result.TemplateHint = "HiddenInput"; + result.HideSurroundingHtml = !hiddenInputAttribute.DisplayValue; + } + + // We prefer [UIHint("...", PresentationLayer = "MVC")] but will fall back to [UIHint("...")] + IEnumerable<UIHintAttribute> uiHintAttributes = attributeList.OfType<UIHintAttribute>(); + UIHintAttribute uiHintAttribute = uiHintAttributes.FirstOrDefault(a => String.Equals(a.PresentationLayer, "MVC", StringComparison.OrdinalIgnoreCase)) + ?? uiHintAttributes.FirstOrDefault(a => String.IsNullOrEmpty(a.PresentationLayer)); + if (uiHintAttribute != null) + { + result.TemplateHint = uiHintAttribute.UIHint; + } + + DataTypeAttribute dataTypeAttribute = attributeList.OfType<DataTypeAttribute>().FirstOrDefault(); + if (dataTypeAttribute != null) + { + result.DataTypeName = dataTypeAttribute.ToDataTypeName(); + } + + EditableAttribute editable = attributes.OfType<EditableAttribute>().FirstOrDefault(); + if (editable != null) + { + result.IsReadOnly = !editable.AllowEdit; + } + else + { + ReadOnlyAttribute readOnlyAttribute = attributeList.OfType<ReadOnlyAttribute>().FirstOrDefault(); + if (readOnlyAttribute != null) + { + result.IsReadOnly = readOnlyAttribute.IsReadOnly; + } + } + + DisplayFormatAttribute displayFormatAttribute = attributeList.OfType<DisplayFormatAttribute>().FirstOrDefault(); + if (displayFormatAttribute == null && dataTypeAttribute != null) + { + displayFormatAttribute = dataTypeAttribute.DisplayFormat; + } + if (displayFormatAttribute != null) + { + result.NullDisplayText = displayFormatAttribute.NullDisplayText; + result.DisplayFormatString = displayFormatAttribute.DataFormatString; + result.ConvertEmptyStringToNull = displayFormatAttribute.ConvertEmptyStringToNull; + + if (displayFormatAttribute.ApplyFormatInEditMode) + { + result.EditFormatString = displayFormatAttribute.DataFormatString; + } + + if (!displayFormatAttribute.HtmlEncode && String.IsNullOrWhiteSpace(result.DataTypeName)) + { + result.DataTypeName = DataTypeUtil.HtmlTypeName; + } + } + + ScaffoldColumnAttribute scaffoldColumnAttribute = attributeList.OfType<ScaffoldColumnAttribute>().FirstOrDefault(); + if (scaffoldColumnAttribute != null) + { + result.ShowForDisplay = result.ShowForEdit = scaffoldColumnAttribute.Scaffold; + } + + DisplayAttribute display = attributes.OfType<DisplayAttribute>().FirstOrDefault(); + string name = null; + if (display != null) + { + result.Description = display.GetDescription(); + result.ShortDisplayName = display.GetShortName(); + result.Watermark = display.GetPrompt(); + result.Order = display.GetOrder() ?? ModelMetadata.DefaultOrder; + + name = display.GetName(); + } + + if (name != null) + { + result.DisplayName = name; + } + else + { + DisplayNameAttribute displayNameAttribute = attributeList.OfType<DisplayNameAttribute>().FirstOrDefault(); + if (displayNameAttribute != null) + { + result.DisplayName = displayNameAttribute.DisplayName; + } + } + + RequiredAttribute requiredAttribute = attributeList.OfType<RequiredAttribute>().FirstOrDefault(); + if (requiredAttribute != null) + { + result.IsRequired = true; + } + + return result; + } + } +} diff --git a/src/System.Web.Mvc/DataAnnotationsModelValidator.cs b/src/System.Web.Mvc/DataAnnotationsModelValidator.cs new file mode 100644 index 00000000..1bb8952b --- /dev/null +++ b/src/System.Web.Mvc/DataAnnotationsModelValidator.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; + +namespace System.Web.Mvc +{ + public class DataAnnotationsModelValidator : ModelValidator + { + public DataAnnotationsModelValidator(ModelMetadata metadata, ControllerContext context, ValidationAttribute attribute) + : base(metadata, context) + { + if (attribute == null) + { + throw new ArgumentNullException("attribute"); + } + + Attribute = attribute; + } + + protected internal ValidationAttribute Attribute { get; private set; } + + protected internal string ErrorMessage + { + get { return Attribute.FormatErrorMessage(Metadata.GetDisplayName()); } + } + + public override bool IsRequired + { + get { return Attribute is RequiredAttribute; } + } + + internal static ModelValidator Create(ModelMetadata metadata, ControllerContext context, ValidationAttribute attribute) + { + return new DataAnnotationsModelValidator(metadata, context, attribute); + } + + public override IEnumerable<ModelClientValidationRule> GetClientValidationRules() + { + IEnumerable<ModelClientValidationRule> results = base.GetClientValidationRules(); + + IClientValidatable clientValidatable = Attribute as IClientValidatable; + if (clientValidatable != null) + { + results = results.Concat(clientValidatable.GetClientValidationRules(Metadata, ControllerContext)); + } + + return results; + } + + public override IEnumerable<ModelValidationResult> Validate(object container) + { + // Per the WCF RIA Services team, instance can never be null (if you have + // no parent, you pass yourself for the "instance" parameter). + ValidationContext context = new ValidationContext(container ?? Metadata.Model, null, null); + context.DisplayName = Metadata.GetDisplayName(); + + ValidationResult result = Attribute.GetValidationResult(Metadata.Model, context); + if (result != ValidationResult.Success) + { + yield return new ModelValidationResult + { + Message = result.ErrorMessage + }; + } + } + } +} diff --git a/src/System.Web.Mvc/DataAnnotationsModelValidatorProvider.cs b/src/System.Web.Mvc/DataAnnotationsModelValidatorProvider.cs new file mode 100644 index 00000000..26d80c96 --- /dev/null +++ b/src/System.Web.Mvc/DataAnnotationsModelValidatorProvider.cs @@ -0,0 +1,375 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + // A factory for validators based on ValidationAttribute + public delegate ModelValidator DataAnnotationsModelValidationFactory(ModelMetadata metadata, ControllerContext context, ValidationAttribute attribute); + + // A factory for validators based on IValidatableObject + public delegate ModelValidator DataAnnotationsValidatableObjectAdapterFactory(ModelMetadata metadata, ControllerContext context); + + /// <summary> + /// An implementation of <see cref="ModelValidatorProvider"/> which providers validators + /// for attributes which derive from <see cref="ValidationAttribute"/>. It also provides + /// a validator for types which implement <see cref="IValidatableObject"/>. To support + /// client side validation, you can either register adapters through the static methods + /// on this class, or by having your validation attributes implement + /// <see cref="IClientValidatable"/>. The logic to support IClientValidatable + /// is implemented in <see cref="DataAnnotationsModelValidator"/>. + /// </summary> + public class DataAnnotationsModelValidatorProvider : AssociatedValidatorProvider + { + private static bool _addImplicitRequiredAttributeForValueTypes = true; + private static ReaderWriterLockSlim _adaptersLock = new ReaderWriterLockSlim(); + + // Factories for validation attributes + + internal static DataAnnotationsModelValidationFactory DefaultAttributeFactory = + (metadata, context, attribute) => new DataAnnotationsModelValidator(metadata, context, attribute); + + internal static Dictionary<Type, DataAnnotationsModelValidationFactory> AttributeFactories = new Dictionary<Type, DataAnnotationsModelValidationFactory>() + { + { + typeof(RangeAttribute), + (metadata, context, attribute) => new RangeAttributeAdapter(metadata, context, (RangeAttribute)attribute) + }, + { + typeof(RegularExpressionAttribute), + (metadata, context, attribute) => new RegularExpressionAttributeAdapter(metadata, context, (RegularExpressionAttribute)attribute) + }, + { + typeof(RequiredAttribute), + (metadata, context, attribute) => new RequiredAttributeAdapter(metadata, context, (RequiredAttribute)attribute) + }, + { + typeof(StringLengthAttribute), + (metadata, context, attribute) => new StringLengthAttributeAdapter(metadata, context, (StringLengthAttribute)attribute) + }, + }; + + // Factories for IValidatableObject models + + internal static DataAnnotationsValidatableObjectAdapterFactory DefaultValidatableFactory = + (metadata, context) => new ValidatableObjectAdapter(metadata, context); + + internal static Dictionary<Type, DataAnnotationsValidatableObjectAdapterFactory> ValidatableFactories = new Dictionary<Type, DataAnnotationsValidatableObjectAdapterFactory>(); + + public static bool AddImplicitRequiredAttributeForValueTypes + { + get { return _addImplicitRequiredAttributeForValueTypes; } + set { _addImplicitRequiredAttributeForValueTypes = value; } + } + + protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes) + { + _adaptersLock.EnterReadLock(); + + try + { + List<ModelValidator> results = new List<ModelValidator>(); + + // Add an implied [Required] attribute for any non-nullable value type, + // unless they've configured us not to do that. + if (AddImplicitRequiredAttributeForValueTypes && + metadata.IsRequired && + !attributes.Any(a => a is RequiredAttribute)) + { + attributes = attributes.Concat(new[] { new RequiredAttribute() }); + } + + // Produce a validator for each validation attribute we find + foreach (ValidationAttribute attribute in attributes.OfType<ValidationAttribute>()) + { + DataAnnotationsModelValidationFactory factory; + if (!AttributeFactories.TryGetValue(attribute.GetType(), out factory)) + { + factory = DefaultAttributeFactory; + } + results.Add(factory(metadata, context, attribute)); + } + + // Produce a validator if the type supports IValidatableObject + if (typeof(IValidatableObject).IsAssignableFrom(metadata.ModelType)) + { + DataAnnotationsValidatableObjectAdapterFactory factory; + if (!ValidatableFactories.TryGetValue(metadata.ModelType, out factory)) + { + factory = DefaultValidatableFactory; + } + results.Add(factory(metadata, context)); + } + + return results; + } + finally + { + _adaptersLock.ExitReadLock(); + } + } + + #region Validation attribute adapter registration + + public static void RegisterAdapter(Type attributeType, Type adapterType) + { + ValidateAttributeType(attributeType); + ValidateAttributeAdapterType(adapterType); + ConstructorInfo constructor = GetAttributeAdapterConstructor(attributeType, adapterType); + + _adaptersLock.EnterWriteLock(); + + try + { + AttributeFactories[attributeType] = (metadata, context, attribute) => (ModelValidator)constructor.Invoke(new object[] { metadata, context, attribute }); + } + finally + { + _adaptersLock.ExitWriteLock(); + } + } + + public static void RegisterAdapterFactory(Type attributeType, DataAnnotationsModelValidationFactory factory) + { + ValidateAttributeType(attributeType); + ValidateAttributeFactory(factory); + + _adaptersLock.EnterWriteLock(); + + try + { + AttributeFactories[attributeType] = factory; + } + finally + { + _adaptersLock.ExitWriteLock(); + } + } + + public static void RegisterDefaultAdapter(Type adapterType) + { + ValidateAttributeAdapterType(adapterType); + ConstructorInfo constructor = GetAttributeAdapterConstructor(typeof(ValidationAttribute), adapterType); + + DefaultAttributeFactory = (metadata, context, attribute) => (ModelValidator)constructor.Invoke(new object[] { metadata, context, attribute }); + } + + public static void RegisterDefaultAdapterFactory(DataAnnotationsModelValidationFactory factory) + { + ValidateAttributeFactory(factory); + + DefaultAttributeFactory = factory; + } + + // Helpers + + private static ConstructorInfo GetAttributeAdapterConstructor(Type attributeType, Type adapterType) + { + ConstructorInfo constructor = adapterType.GetConstructor(new[] { typeof(ModelMetadata), typeof(ControllerContext), attributeType }); + if (constructor == null) + { + throw new ArgumentException( + String.Format( + CultureInfo.CurrentCulture, + MvcResources.DataAnnotationsModelValidatorProvider_ConstructorRequirements, + adapterType.FullName, + typeof(ModelMetadata).FullName, + typeof(ControllerContext).FullName, + attributeType.FullName), + "adapterType"); + } + + return constructor; + } + + private static void ValidateAttributeAdapterType(Type adapterType) + { + if (adapterType == null) + { + throw new ArgumentNullException("adapterType"); + } + if (!typeof(ModelValidator).IsAssignableFrom(adapterType)) + { + throw new ArgumentException( + String.Format( + CultureInfo.CurrentCulture, + MvcResources.Common_TypeMustDriveFromType, + adapterType.FullName, + typeof(ModelValidator).FullName), + "adapterType"); + } + } + + private static void ValidateAttributeType(Type attributeType) + { + if (attributeType == null) + { + throw new ArgumentNullException("attributeType"); + } + if (!typeof(ValidationAttribute).IsAssignableFrom(attributeType)) + { + throw new ArgumentException( + String.Format( + CultureInfo.CurrentCulture, + MvcResources.Common_TypeMustDriveFromType, + attributeType.FullName, + typeof(ValidationAttribute).FullName), + "attributeType"); + } + } + + private static void ValidateAttributeFactory(DataAnnotationsModelValidationFactory factory) + { + if (factory == null) + { + throw new ArgumentNullException("factory"); + } + } + + #endregion + + #region IValidatableObject adapter registration + + /// <summary> + /// Registers an adapter type for the given <paramref name="modelType"/>, which must + /// implement <see cref="IValidatableObject"/>. The adapter type must derive from + /// <see cref="ModelValidator"/> and it must contain a public constructor + /// which takes two parameters of types <see cref="ModelMetadata"/> and + /// <see cref="ControllerContext"/>. + /// </summary> + public static void RegisterValidatableObjectAdapter(Type modelType, Type adapterType) + { + ValidateValidatableModelType(modelType); + ValidateValidatableAdapterType(adapterType); + ConstructorInfo constructor = GetValidatableAdapterConstructor(adapterType); + + _adaptersLock.EnterWriteLock(); + + try + { + ValidatableFactories[modelType] = (metadata, context) => (ModelValidator)constructor.Invoke(new object[] { metadata, context }); + } + finally + { + _adaptersLock.ExitWriteLock(); + } + } + + /// <summary> + /// Registers an adapter factory for the given <paramref name="modelType"/>, which must + /// implement <see cref="IValidatableObject"/>. + /// </summary> + public static void RegisterValidatableObjectAdapterFactory(Type modelType, DataAnnotationsValidatableObjectAdapterFactory factory) + { + ValidateValidatableModelType(modelType); + ValidateValidatableFactory(factory); + + _adaptersLock.EnterWriteLock(); + + try + { + ValidatableFactories[modelType] = factory; + } + finally + { + _adaptersLock.ExitWriteLock(); + } + } + + /// <summary> + /// Registers the default adapter type for objects which implement + /// <see cref="IValidatableObject"/>. The adapter type must derive from + /// <see cref="ModelValidator"/> and it must contain a public constructor + /// which takes two parameters of types <see cref="ModelMetadata"/> and + /// <see cref="ControllerContext"/>. + /// </summary> + public static void RegisterDefaultValidatableObjectAdapter(Type adapterType) + { + ValidateValidatableAdapterType(adapterType); + ConstructorInfo constructor = GetValidatableAdapterConstructor(adapterType); + + DefaultValidatableFactory = (metadata, context) => (ModelValidator)constructor.Invoke(new object[] { metadata, context }); + } + + /// <summary> + /// Registers the default adapter factory for objects which implement + /// <see cref="IValidatableObject"/>. + /// </summary> + public static void RegisterDefaultValidatableObjectAdapterFactory(DataAnnotationsValidatableObjectAdapterFactory factory) + { + ValidateValidatableFactory(factory); + + DefaultValidatableFactory = factory; + } + + // Helpers + + private static ConstructorInfo GetValidatableAdapterConstructor(Type adapterType) + { + ConstructorInfo constructor = adapterType.GetConstructor(new[] { typeof(ModelMetadata), typeof(ControllerContext) }); + if (constructor == null) + { + throw new ArgumentException( + String.Format( + CultureInfo.CurrentCulture, + MvcResources.DataAnnotationsModelValidatorProvider_ValidatableConstructorRequirements, + adapterType.FullName, + typeof(ModelMetadata).FullName, + typeof(ControllerContext).FullName), + "adapterType"); + } + + return constructor; + } + + private static void ValidateValidatableAdapterType(Type adapterType) + { + if (adapterType == null) + { + throw new ArgumentNullException("adapterType"); + } + if (!typeof(ModelValidator).IsAssignableFrom(adapterType)) + { + throw new ArgumentException( + String.Format( + CultureInfo.CurrentCulture, + MvcResources.Common_TypeMustDriveFromType, + adapterType.FullName, + typeof(ModelValidator).FullName), + "adapterType"); + } + } + + private static void ValidateValidatableModelType(Type modelType) + { + if (modelType == null) + { + throw new ArgumentNullException("modelType"); + } + if (!typeof(IValidatableObject).IsAssignableFrom(modelType)) + { + throw new ArgumentException( + String.Format( + CultureInfo.CurrentCulture, + MvcResources.Common_TypeMustDriveFromType, + modelType.FullName, + typeof(IValidatableObject).FullName), + "modelType"); + } + } + + private static void ValidateValidatableFactory(DataAnnotationsValidatableObjectAdapterFactory factory) + { + if (factory == null) + { + throw new ArgumentNullException("factory"); + } + } + + #endregion + } +} diff --git a/src/System.Web.Mvc/DataAnnotationsModelValidator`1.cs b/src/System.Web.Mvc/DataAnnotationsModelValidator`1.cs new file mode 100644 index 00000000..19a55969 --- /dev/null +++ b/src/System.Web.Mvc/DataAnnotationsModelValidator`1.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace System.Web.Mvc +{ + public class DataAnnotationsModelValidator<TAttribute> : DataAnnotationsModelValidator + where TAttribute : ValidationAttribute + { + public DataAnnotationsModelValidator(ModelMetadata metadata, ControllerContext context, TAttribute attribute) + : base(metadata, context, attribute) + { + } + + protected new TAttribute Attribute + { + get { return (TAttribute)base.Attribute; } + } + } +} diff --git a/src/System.Web.Mvc/DataErrorInfoModelValidatorProvider.cs b/src/System.Web.Mvc/DataErrorInfoModelValidatorProvider.cs new file mode 100644 index 00000000..cdfc72dc --- /dev/null +++ b/src/System.Web.Mvc/DataErrorInfoModelValidatorProvider.cs @@ -0,0 +1,95 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; + +namespace System.Web.Mvc +{ + public class DataErrorInfoModelValidatorProvider : ModelValidatorProvider + { + public override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context) + { + if (metadata == null) + { + throw new ArgumentNullException("metadata"); + } + if (context == null) + { + throw new ArgumentNullException("context"); + } + + return GetValidatorsImpl(metadata, context); + } + + private static IEnumerable<ModelValidator> GetValidatorsImpl(ModelMetadata metadata, ControllerContext context) + { + // If the metadata describes a model that implements IDataErrorInfo, we should call its + // Error property at the appropriate time. + if (TypeImplementsIDataErrorInfo(metadata.ModelType)) + { + yield return new DataErrorInfoClassModelValidator(metadata, context); + } + + // If the metadata describes a property of a container that implements IDataErrorInfo, + // we should call its Item indexer at the appropriate time. + if (TypeImplementsIDataErrorInfo(metadata.ContainerType)) + { + yield return new DataErrorInfoPropertyModelValidator(metadata, context); + } + } + + private static bool TypeImplementsIDataErrorInfo(Type type) + { + return typeof(IDataErrorInfo).IsAssignableFrom(type); + } + + internal sealed class DataErrorInfoClassModelValidator : ModelValidator + { + public DataErrorInfoClassModelValidator(ModelMetadata metadata, ControllerContext controllerContext) + : base(metadata, controllerContext) + { + } + + public override IEnumerable<ModelValidationResult> Validate(object container) + { + IDataErrorInfo castModel = Metadata.Model as IDataErrorInfo; + if (castModel != null) + { + string errorMessage = castModel.Error; + if (!String.IsNullOrEmpty(errorMessage)) + { + return new ModelValidationResult[] + { + new ModelValidationResult() { Message = errorMessage } + }; + } + } + return Enumerable.Empty<ModelValidationResult>(); + } + } + + internal sealed class DataErrorInfoPropertyModelValidator : ModelValidator + { + public DataErrorInfoPropertyModelValidator(ModelMetadata metadata, ControllerContext controllerContext) + : base(metadata, controllerContext) + { + } + + public override IEnumerable<ModelValidationResult> Validate(object container) + { + IDataErrorInfo castContainer = container as IDataErrorInfo; + if (castContainer != null && !String.Equals(Metadata.PropertyName, "error", StringComparison.OrdinalIgnoreCase)) + { + string errorMessage = castContainer[Metadata.PropertyName]; + if (!String.IsNullOrEmpty(errorMessage)) + { + return new ModelValidationResult[] + { + new ModelValidationResult() { Message = errorMessage } + }; + } + } + return Enumerable.Empty<ModelValidationResult>(); + } + } + } +} diff --git a/src/System.Web.Mvc/DataTypeUtil.cs b/src/System.Web.Mvc/DataTypeUtil.cs new file mode 100644 index 00000000..79eb9ab6 --- /dev/null +++ b/src/System.Web.Mvc/DataTypeUtil.cs @@ -0,0 +1,109 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace System.Web.Mvc +{ + internal static class DataTypeUtil + { + internal static readonly string CurrencyTypeName = DataType.Currency.ToString(); + internal static readonly string DateTypeName = DataType.Date.ToString(); + internal static readonly string DateTimeTypeName = DataType.DateTime.ToString(); + internal static readonly string DurationTypeName = DataType.Duration.ToString(); + internal static readonly string EmailAddressTypeName = DataType.EmailAddress.ToString(); + internal static readonly string HtmlTypeName = DataType.Html.ToString(); + internal static readonly string ImageUrlTypeName = DataType.ImageUrl.ToString(); + internal static readonly string MultiLineTextTypeName = DataType.MultilineText.ToString(); + internal static readonly string PasswordTypeName = DataType.Password.ToString(); + internal static readonly string PhoneNumberTypeName = DataType.PhoneNumber.ToString(); + internal static readonly string TextTypeName = DataType.Text.ToString(); + internal static readonly string TimeTypeName = DataType.Time.ToString(); + internal static readonly string UrlTypeName = DataType.Url.ToString(); + + private static readonly Lazy<Dictionary<object, string>> _dataTypeToName = new Lazy<Dictionary<object, string>>(CreateDataTypeToName, isThreadSafe: true); + + // This is a faster version of GetDataTypeName(). It internally calls ToString() on the enum + // value, which can be quite slow because of value verification. + internal static string ToDataTypeName(this DataTypeAttribute attribute, Func<DataTypeAttribute, Boolean> isDataType = null) + { + if (isDataType == null) + { + isDataType = t => t.GetType().Equals(typeof(DataTypeAttribute)); + } + + // GetDataTypeName is virtual, so this is only safe if they haven't derived from DataTypeAttribute. + // However, if they derive from DataTypeAttribute, they can help their own perf by overriding GetDataTypeName + // and returning an appropriate string without invoking the ToString() on the enum. + if (isDataType(attribute)) + { + // Statically known dataTypes are handled separately for performance + string name = KnownDataTypeToString(attribute.DataType); + if (name == null) + { + // Unknown types fallback to a dictionary lookup. + // 4.0 will not enter this code for statically known data types. + // 4.5 will enter this code for the new data types added to 4.5. + _dataTypeToName.Value.TryGetValue(attribute.DataType, out name); + } + + if (name != null) + { + return name; + } + } + + return attribute.GetDataTypeName(); + } + + private static string KnownDataTypeToString(DataType dataType) + { + switch (dataType) + { + case DataType.Currency: + return CurrencyTypeName; + case DataType.Date: + return DateTypeName; + case DataType.DateTime: + return DateTimeTypeName; + case DataType.Duration: + return DurationTypeName; + case DataType.EmailAddress: + return EmailAddressTypeName; + case DataType.Html: + return HtmlTypeName; + case DataType.ImageUrl: + return ImageUrlTypeName; + case DataType.MultilineText: + return MultiLineTextTypeName; + case DataType.Password: + return PasswordTypeName; + case DataType.PhoneNumber: + return PhoneNumberTypeName; + case DataType.Text: + return TextTypeName; + case DataType.Time: + return TimeTypeName; + case DataType.Url: + return UrlTypeName; + } + + return null; + } + + private static Dictionary<object, string> CreateDataTypeToName() + { + Dictionary<object, string> dataTypeToName = new Dictionary<object, string>(); + foreach (DataType dataTypeValue in Enum.GetValues(typeof(DataType))) + { + // Don't add to the dictionary any of the statically known types. + // This is a workingset size optimization. + if (dataTypeValue != DataType.Custom && KnownDataTypeToString(dataTypeValue) == null) + { + string name = Enum.GetName(typeof(DataType), dataTypeValue); + dataTypeToName[dataTypeValue] = name; + } + } + + return dataTypeToName; + } + } +} diff --git a/src/System.Web.Mvc/DefaultControllerFactory.cs b/src/System.Web.Mvc/DefaultControllerFactory.cs new file mode 100644 index 00000000..8f67ca52 --- /dev/null +++ b/src/System.Web.Mvc/DefaultControllerFactory.cs @@ -0,0 +1,294 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Web.Mvc.Properties; +using System.Web.Routing; +using System.Web.SessionState; + +namespace System.Web.Mvc +{ + public class DefaultControllerFactory : IControllerFactory + { + private static readonly ConcurrentDictionary<Type, SessionStateBehavior> _sessionStateCache = new ConcurrentDictionary<Type, SessionStateBehavior>(); + private static ControllerTypeCache _staticControllerTypeCache = new ControllerTypeCache(); + private IBuildManager _buildManager; + private IResolver<IControllerActivator> _activatorResolver; + private IControllerActivator _controllerActivator; + private ControllerBuilder _controllerBuilder; + private ControllerTypeCache _instanceControllerTypeCache; + + public DefaultControllerFactory() + : this(null, null, null) + { + } + + public DefaultControllerFactory(IControllerActivator controllerActivator) + : this(controllerActivator, null, null) + { + } + + internal DefaultControllerFactory(IControllerActivator controllerActivator, IResolver<IControllerActivator> activatorResolver, IDependencyResolver dependencyResolver) + { + if (controllerActivator != null) + { + _controllerActivator = controllerActivator; + } + else + { + _activatorResolver = activatorResolver ?? new SingleServiceResolver<IControllerActivator>( + () => null, + new DefaultControllerActivator(dependencyResolver), + "DefaultControllerFactory constructor"); + } + } + + private IControllerActivator ControllerActivator + { + get + { + if (_controllerActivator != null) + { + return _controllerActivator; + } + _controllerActivator = _activatorResolver.Current; + return _controllerActivator; + } + } + + internal IBuildManager BuildManager + { + get + { + if (_buildManager == null) + { + _buildManager = new BuildManagerWrapper(); + } + return _buildManager; + } + set { _buildManager = value; } + } + + internal ControllerBuilder ControllerBuilder + { + get { return _controllerBuilder ?? ControllerBuilder.Current; } + set { _controllerBuilder = value; } + } + + internal ControllerTypeCache ControllerTypeCache + { + get { return _instanceControllerTypeCache ?? _staticControllerTypeCache; } + set { _instanceControllerTypeCache = value; } + } + + internal static InvalidOperationException CreateAmbiguousControllerException(RouteBase route, string controllerName, ICollection<Type> matchingTypes) + { + // we need to generate an exception containing all the controller types + StringBuilder typeList = new StringBuilder(); + foreach (Type matchedType in matchingTypes) + { + typeList.AppendLine(); + typeList.Append(matchedType.FullName); + } + + string errorText; + Route castRoute = route as Route; + if (castRoute != null) + { + errorText = String.Format(CultureInfo.CurrentCulture, MvcResources.DefaultControllerFactory_ControllerNameAmbiguous_WithRouteUrl, + controllerName, castRoute.Url, typeList); + } + else + { + errorText = String.Format(CultureInfo.CurrentCulture, MvcResources.DefaultControllerFactory_ControllerNameAmbiguous_WithoutRouteUrl, + controllerName, typeList); + } + + return new InvalidOperationException(errorText); + } + + public virtual IController CreateController(RequestContext requestContext, string controllerName) + { + if (requestContext == null) + { + throw new ArgumentNullException("requestContext"); + } + if (String.IsNullOrEmpty(controllerName)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "controllerName"); + } + Type controllerType = GetControllerType(requestContext, controllerName); + IController controller = GetControllerInstance(requestContext, controllerType); + return controller; + } + + protected internal virtual IController GetControllerInstance(RequestContext requestContext, Type controllerType) + { + if (controllerType == null) + { + throw new HttpException(404, + String.Format( + CultureInfo.CurrentCulture, + MvcResources.DefaultControllerFactory_NoControllerFound, + requestContext.HttpContext.Request.Path)); + } + if (!typeof(IController).IsAssignableFrom(controllerType)) + { + throw new ArgumentException( + String.Format( + CultureInfo.CurrentCulture, + MvcResources.DefaultControllerFactory_TypeDoesNotSubclassControllerBase, + controllerType), + "controllerType"); + } + return ControllerActivator.Create(requestContext, controllerType); + } + + protected internal virtual SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, Type controllerType) + { + if (controllerType == null) + { + return SessionStateBehavior.Default; + } + + return _sessionStateCache.GetOrAdd( + controllerType, + type => + { + var attr = type.GetCustomAttributes(typeof(SessionStateAttribute), inherit: true) + .OfType<SessionStateAttribute>() + .FirstOrDefault(); + + return (attr != null) ? attr.Behavior : SessionStateBehavior.Default; + }); + } + + protected internal virtual Type GetControllerType(RequestContext requestContext, string controllerName) + { + if (String.IsNullOrEmpty(controllerName)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "controllerName"); + } + + // first search in the current route's namespace collection + object routeNamespacesObj; + Type match; + if (requestContext != null && requestContext.RouteData.DataTokens.TryGetValue("Namespaces", out routeNamespacesObj)) + { + IEnumerable<string> routeNamespaces = routeNamespacesObj as IEnumerable<string>; + if (routeNamespaces != null && routeNamespaces.Any()) + { + HashSet<string> namespaceHash = new HashSet<string>(routeNamespaces, StringComparer.OrdinalIgnoreCase); + match = GetControllerTypeWithinNamespaces(requestContext.RouteData.Route, controllerName, namespaceHash); + + // the UseNamespaceFallback key might not exist, in which case its value is implicitly "true" + if (match != null || false.Equals(requestContext.RouteData.DataTokens["UseNamespaceFallback"])) + { + // got a match or the route requested we stop looking + return match; + } + } + } + + // then search in the application's default namespace collection + if (ControllerBuilder.DefaultNamespaces.Count > 0) + { + HashSet<string> namespaceDefaults = new HashSet<string>(ControllerBuilder.DefaultNamespaces, StringComparer.OrdinalIgnoreCase); + match = GetControllerTypeWithinNamespaces(requestContext.RouteData.Route, controllerName, namespaceDefaults); + if (match != null) + { + return match; + } + } + + // if all else fails, search every namespace + return GetControllerTypeWithinNamespaces(requestContext.RouteData.Route, controllerName, null /* namespaces */); + } + + private Type GetControllerTypeWithinNamespaces(RouteBase route, string controllerName, HashSet<string> namespaces) + { + // Once the master list of controllers has been created we can quickly index into it + ControllerTypeCache.EnsureInitialized(BuildManager); + + ICollection<Type> matchingTypes = ControllerTypeCache.GetControllerTypes(controllerName, namespaces); + switch (matchingTypes.Count) + { + case 0: + // no matching types + return null; + + case 1: + // single matching type + return matchingTypes.First(); + + default: + // multiple matching types + throw CreateAmbiguousControllerException(route, controllerName, matchingTypes); + } + } + + public virtual void ReleaseController(IController controller) + { + IDisposable disposable = controller as IDisposable; + if (disposable != null) + { + disposable.Dispose(); + } + } + + SessionStateBehavior IControllerFactory.GetControllerSessionBehavior(RequestContext requestContext, string controllerName) + { + if (requestContext == null) + { + throw new ArgumentNullException("requestContext"); + } + if (String.IsNullOrEmpty(controllerName)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "controllerName"); + } + + Type controllerType = GetControllerType(requestContext, controllerName); + return GetControllerSessionBehavior(requestContext, controllerType); + } + + private class DefaultControllerActivator : IControllerActivator + { + private Func<IDependencyResolver> _resolverThunk; + + public DefaultControllerActivator() + : this(null) + { + } + + public DefaultControllerActivator(IDependencyResolver resolver) + { + if (resolver == null) + { + _resolverThunk = () => DependencyResolver.Current; + } + else + { + _resolverThunk = () => resolver; + } + } + + public IController Create(RequestContext requestContext, Type controllerType) + { + try + { + return (IController)(_resolverThunk().GetService(controllerType) ?? Activator.CreateInstance(controllerType)); + } + catch (Exception ex) + { + throw new InvalidOperationException( + String.Format( + CultureInfo.CurrentCulture, + MvcResources.DefaultControllerFactory_ErrorCreatingController, + controllerType), + ex); + } + } + } + } +} diff --git a/src/System.Web.Mvc/DefaultModelBinder.cs b/src/System.Web.Mvc/DefaultModelBinder.cs new file mode 100644 index 00000000..6a0377e9 --- /dev/null +++ b/src/System.Web.Mvc/DefaultModelBinder.cs @@ -0,0 +1,840 @@ +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + public class DefaultModelBinder : IModelBinder + { + private static string _resourceClassKey; + private ModelBinderDictionary _binders; + + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "Property is settable so that the dictionary can be provided for unit testing purposes.")] + protected internal ModelBinderDictionary Binders + { + get + { + if (_binders == null) + { + _binders = ModelBinders.Binders; + } + return _binders; + } + set { _binders = value; } + } + + public static string ResourceClassKey + { + get { return _resourceClassKey ?? String.Empty; } + set { _resourceClassKey = value; } + } + + private static void AddValueRequiredMessageToModelState(ControllerContext controllerContext, ModelStateDictionary modelState, string modelStateKey, Type elementType, object value) + { + if (value == null && !TypeHelpers.TypeAllowsNullValue(elementType) && modelState.IsValidField(modelStateKey)) + { + modelState.AddModelError(modelStateKey, GetValueRequiredResource(controllerContext)); + } + } + + internal void BindComplexElementalModel(ControllerContext controllerContext, ModelBindingContext bindingContext, object model) + { + // need to replace the property filter + model object and create an inner binding context + ModelBindingContext newBindingContext = CreateComplexElementalModelBindingContext(controllerContext, bindingContext, model); + + // validation + if (OnModelUpdating(controllerContext, newBindingContext)) + { + BindProperties(controllerContext, newBindingContext); + OnModelUpdated(controllerContext, newBindingContext); + } + } + + internal object BindComplexModel(ControllerContext controllerContext, ModelBindingContext bindingContext) + { + object model = bindingContext.Model; + Type modelType = bindingContext.ModelType; + + // if we're being asked to create an array, create a list instead, then coerce to an array after the list is created + if (model == null && modelType.IsArray) + { + Type elementType = modelType.GetElementType(); + Type listType = typeof(List<>).MakeGenericType(elementType); + object collection = CreateModel(controllerContext, bindingContext, listType); + + ModelBindingContext arrayBindingContext = new ModelBindingContext() + { + ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => collection, listType), + ModelName = bindingContext.ModelName, + ModelState = bindingContext.ModelState, + PropertyFilter = bindingContext.PropertyFilter, + ValueProvider = bindingContext.ValueProvider + }; + IList list = (IList)UpdateCollection(controllerContext, arrayBindingContext, elementType); + + if (list == null) + { + return null; + } + + Array array = Array.CreateInstance(elementType, list.Count); + list.CopyTo(array, 0); + return array; + } + + if (model == null) + { + model = CreateModel(controllerContext, bindingContext, modelType); + } + + // special-case IDictionary<,> and ICollection<> + Type dictionaryType = TypeHelpers.ExtractGenericInterface(modelType, typeof(IDictionary<,>)); + if (dictionaryType != null) + { + Type[] genericArguments = dictionaryType.GetGenericArguments(); + Type keyType = genericArguments[0]; + Type valueType = genericArguments[1]; + + ModelBindingContext dictionaryBindingContext = new ModelBindingContext() + { + ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, modelType), + ModelName = bindingContext.ModelName, + ModelState = bindingContext.ModelState, + PropertyFilter = bindingContext.PropertyFilter, + ValueProvider = bindingContext.ValueProvider + }; + object dictionary = UpdateDictionary(controllerContext, dictionaryBindingContext, keyType, valueType); + return dictionary; + } + + Type enumerableType = TypeHelpers.ExtractGenericInterface(modelType, typeof(IEnumerable<>)); + if (enumerableType != null) + { + Type elementType = enumerableType.GetGenericArguments()[0]; + + Type collectionType = typeof(ICollection<>).MakeGenericType(elementType); + if (collectionType.IsInstanceOfType(model)) + { + ModelBindingContext collectionBindingContext = new ModelBindingContext() + { + ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, modelType), + ModelName = bindingContext.ModelName, + ModelState = bindingContext.ModelState, + PropertyFilter = bindingContext.PropertyFilter, + ValueProvider = bindingContext.ValueProvider + }; + object collection = UpdateCollection(controllerContext, collectionBindingContext, elementType); + return collection; + } + } + + // otherwise, just update the properties on the complex type + BindComplexElementalModel(controllerContext, bindingContext, model); + return model; + } + + public virtual object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) + { + if (bindingContext == null) + { + throw new ArgumentNullException("bindingContext"); + } + + bool performedFallback = false; + + if (!String.IsNullOrEmpty(bindingContext.ModelName) && !bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName)) + { + // We couldn't find any entry that began with the prefix. If this is the top-level element, fall back + // to the empty prefix. + if (bindingContext.FallbackToEmptyPrefix) + { + bindingContext = new ModelBindingContext() + { + ModelMetadata = bindingContext.ModelMetadata, + ModelState = bindingContext.ModelState, + PropertyFilter = bindingContext.PropertyFilter, + ValueProvider = bindingContext.ValueProvider + }; + performedFallback = true; + } + else + { + return null; + } + } + + // Simple model = int, string, etc.; determined by calling TypeConverter.CanConvertFrom(typeof(string)) + // or by seeing if a value in the request exactly matches the name of the model we're binding. + // Complex type = everything else. + if (!performedFallback) + { + bool performRequestValidation = ShouldPerformRequestValidation(controllerContext, bindingContext); + ValueProviderResult valueProviderResult = bindingContext.UnvalidatedValueProvider.GetValue(bindingContext.ModelName, skipValidation: !performRequestValidation); + if (valueProviderResult != null) + { + return BindSimpleModel(controllerContext, bindingContext, valueProviderResult); + } + } + if (!bindingContext.ModelMetadata.IsComplexType) + { + return null; + } + + return BindComplexModel(controllerContext, bindingContext); + } + + private void BindProperties(ControllerContext controllerContext, ModelBindingContext bindingContext) + { + IEnumerable<PropertyDescriptor> properties = GetFilteredModelProperties(controllerContext, bindingContext); + foreach (PropertyDescriptor property in properties) + { + BindProperty(controllerContext, bindingContext, property); + } + } + + protected virtual void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor) + { + // need to skip properties that aren't part of the request, else we might hit a StackOverflowException + string fullPropertyKey = CreateSubPropertyName(bindingContext.ModelName, propertyDescriptor.Name); + if (!bindingContext.ValueProvider.ContainsPrefix(fullPropertyKey)) + { + return; + } + + // call into the property's model binder + IModelBinder propertyBinder = Binders.GetBinder(propertyDescriptor.PropertyType); + object originalPropertyValue = propertyDescriptor.GetValue(bindingContext.Model); + ModelMetadata propertyMetadata = bindingContext.PropertyMetadata[propertyDescriptor.Name]; + propertyMetadata.Model = originalPropertyValue; + ModelBindingContext innerBindingContext = new ModelBindingContext() + { + ModelMetadata = propertyMetadata, + ModelName = fullPropertyKey, + ModelState = bindingContext.ModelState, + ValueProvider = bindingContext.ValueProvider + }; + object newPropertyValue = GetPropertyValue(controllerContext, innerBindingContext, propertyDescriptor, propertyBinder); + propertyMetadata.Model = newPropertyValue; + + // validation + ModelState modelState = bindingContext.ModelState[fullPropertyKey]; + if (modelState == null || modelState.Errors.Count == 0) + { + if (OnPropertyValidating(controllerContext, bindingContext, propertyDescriptor, newPropertyValue)) + { + SetProperty(controllerContext, bindingContext, propertyDescriptor, newPropertyValue); + OnPropertyValidated(controllerContext, bindingContext, propertyDescriptor, newPropertyValue); + } + } + else + { + SetProperty(controllerContext, bindingContext, propertyDescriptor, newPropertyValue); + + // Convert FormatExceptions (type conversion failures) into InvalidValue messages + foreach (ModelError error in modelState.Errors.Where(err => String.IsNullOrEmpty(err.ErrorMessage) && err.Exception != null).ToList()) + { + for (Exception exception = error.Exception; exception != null; exception = exception.InnerException) + { + if (exception is FormatException) + { + string displayName = propertyMetadata.GetDisplayName(); + string errorMessageTemplate = GetValueInvalidResource(controllerContext); + string errorMessage = String.Format(CultureInfo.CurrentCulture, errorMessageTemplate, modelState.Value.AttemptedValue, displayName); + modelState.Errors.Remove(error); + modelState.Errors.Add(errorMessage); + break; + } + } + } + } + } + + internal object BindSimpleModel(ControllerContext controllerContext, ModelBindingContext bindingContext, ValueProviderResult valueProviderResult) + { + bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult); + + // if the value provider returns an instance of the requested data type, we can just short-circuit + // the evaluation and return that instance + if (bindingContext.ModelType.IsInstanceOfType(valueProviderResult.RawValue)) + { + return valueProviderResult.RawValue; + } + + // since a string is an IEnumerable<char>, we want it to skip the two checks immediately following + if (bindingContext.ModelType != typeof(string)) + { + // conversion results in 3 cases, as below + if (bindingContext.ModelType.IsArray) + { + // case 1: user asked for an array + // ValueProviderResult.ConvertTo() understands array types, so pass in the array type directly + object modelArray = ConvertProviderResult(bindingContext.ModelState, bindingContext.ModelName, valueProviderResult, bindingContext.ModelType); + return modelArray; + } + + Type enumerableType = TypeHelpers.ExtractGenericInterface(bindingContext.ModelType, typeof(IEnumerable<>)); + if (enumerableType != null) + { + // case 2: user asked for a collection rather than an array + // need to call ConvertTo() on the array type, then copy the array to the collection + object modelCollection = CreateModel(controllerContext, bindingContext, bindingContext.ModelType); + Type elementType = enumerableType.GetGenericArguments()[0]; + Type arrayType = elementType.MakeArrayType(); + object modelArray = ConvertProviderResult(bindingContext.ModelState, bindingContext.ModelName, valueProviderResult, arrayType); + + Type collectionType = typeof(ICollection<>).MakeGenericType(elementType); + if (collectionType.IsInstanceOfType(modelCollection)) + { + CollectionHelpers.ReplaceCollection(elementType, modelCollection, modelArray); + } + return modelCollection; + } + } + + // case 3: user asked for an individual element + object model = ConvertProviderResult(bindingContext.ModelState, bindingContext.ModelName, valueProviderResult, bindingContext.ModelType); + return model; + } + + private static bool CanUpdateReadonlyTypedReference(Type type) + { + // value types aren't strictly immutable, but because they have copy-by-value semantics + // we can't update a value type that is marked readonly + if (type.IsValueType) + { + return false; + } + + // arrays are mutable, but because we can't change their length we shouldn't try + // to update an array that is referenced readonly + if (type.IsArray) + { + return false; + } + + // special-case known common immutable types + if (type == typeof(string)) + { + return false; + } + + return true; + } + + [SuppressMessage("Microsoft.Globalization", "CA1304:SpecifyCultureInfo", MessageId = "System.Web.Mvc.ValueProviderResult.ConvertTo(System.Type)", Justification = "The target object should make the correct culture determination, not this method.")] + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We're recording this exception so that we can act on it later.")] + private static object ConvertProviderResult(ModelStateDictionary modelState, string modelStateKey, ValueProviderResult valueProviderResult, Type destinationType) + { + try + { + object convertedValue = valueProviderResult.ConvertTo(destinationType); + return convertedValue; + } + catch (Exception ex) + { + modelState.AddModelError(modelStateKey, ex); + return null; + } + } + + internal ModelBindingContext CreateComplexElementalModelBindingContext(ControllerContext controllerContext, ModelBindingContext bindingContext, object model) + { + BindAttribute bindAttr = (BindAttribute)GetTypeDescriptor(controllerContext, bindingContext).GetAttributes()[typeof(BindAttribute)]; + Predicate<string> newPropertyFilter = (bindAttr != null) + ? propertyName => bindAttr.IsPropertyAllowed(propertyName) && bindingContext.PropertyFilter(propertyName) + : bindingContext.PropertyFilter; + + ModelBindingContext newBindingContext = new ModelBindingContext() + { + ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, bindingContext.ModelType), + ModelName = bindingContext.ModelName, + ModelState = bindingContext.ModelState, + PropertyFilter = newPropertyFilter, + ValueProvider = bindingContext.ValueProvider + }; + + return newBindingContext; + } + + protected virtual object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) + { + Type typeToCreate = modelType; + + // we can understand some collection interfaces, e.g. IList<>, IDictionary<,> + if (modelType.IsGenericType) + { + Type genericTypeDefinition = modelType.GetGenericTypeDefinition(); + if (genericTypeDefinition == typeof(IDictionary<,>)) + { + typeToCreate = typeof(Dictionary<,>).MakeGenericType(modelType.GetGenericArguments()); + } + else if (genericTypeDefinition == typeof(IEnumerable<>) || genericTypeDefinition == typeof(ICollection<>) || genericTypeDefinition == typeof(IList<>)) + { + typeToCreate = typeof(List<>).MakeGenericType(modelType.GetGenericArguments()); + } + } + + // fallback to the type's default constructor + return Activator.CreateInstance(typeToCreate); + } + + protected static string CreateSubIndexName(string prefix, int index) + { + return String.Format(CultureInfo.InvariantCulture, "{0}[{1}]", prefix, index); + } + + protected static string CreateSubIndexName(string prefix, string index) + { + return String.Format(CultureInfo.InvariantCulture, "{0}[{1}]", prefix, index); + } + + protected internal static string CreateSubPropertyName(string prefix, string propertyName) + { + if (String.IsNullOrEmpty(prefix)) + { + return propertyName; + } + else if (String.IsNullOrEmpty(propertyName)) + { + return prefix; + } + else + { + return prefix + "." + propertyName; + } + } + + protected IEnumerable<PropertyDescriptor> GetFilteredModelProperties(ControllerContext controllerContext, ModelBindingContext bindingContext) + { + PropertyDescriptorCollection properties = GetModelProperties(controllerContext, bindingContext); + Predicate<string> propertyFilter = bindingContext.PropertyFilter; + + return from PropertyDescriptor property in properties + where ShouldUpdateProperty(property, propertyFilter) + select property; + } + + [SuppressMessage("Microsoft.Globalization", "CA1304:SpecifyCultureInfo", MessageId = "System.Web.Mvc.ValueProviderResult.ConvertTo(System.Type)", Justification = "ValueProviderResult already handles culture conversion appropriately.")] + private static void GetIndexes(ModelBindingContext bindingContext, out bool stopOnIndexNotFound, out IEnumerable<string> indexes) + { + string indexKey = CreateSubPropertyName(bindingContext.ModelName, "index"); + ValueProviderResult valueProviderResult = bindingContext.ValueProvider.GetValue(indexKey); + + if (valueProviderResult != null) + { + string[] indexesArray = valueProviderResult.ConvertTo(typeof(string[])) as string[]; + if (indexesArray != null) + { + stopOnIndexNotFound = false; + indexes = indexesArray; + return; + } + } + + // just use a simple zero-based system + stopOnIndexNotFound = true; + indexes = GetZeroBasedIndexes(); + } + + protected virtual PropertyDescriptorCollection GetModelProperties(ControllerContext controllerContext, ModelBindingContext bindingContext) + { + return GetTypeDescriptor(controllerContext, bindingContext).GetProperties(); + } + + protected virtual object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder) + { + object value = propertyBinder.BindModel(controllerContext, bindingContext); + + if (bindingContext.ModelMetadata.ConvertEmptyStringToNull && Equals(value, String.Empty)) + { + return null; + } + + return value; + } + + protected virtual ICustomTypeDescriptor GetTypeDescriptor(ControllerContext controllerContext, ModelBindingContext bindingContext) + { + return TypeDescriptorHelper.Get(bindingContext.ModelType); + } + + // If the user specified a ResourceClassKey try to load the resource they specified. + // If the class key is invalid, an exception will be thrown. + // If the class key is valid but the resource is not found, it returns null, in which + // case it will fall back to the MVC default error message. + private static string GetUserResourceString(ControllerContext controllerContext, string resourceName) + { + string result = null; + + if (!String.IsNullOrEmpty(ResourceClassKey) && (controllerContext != null) && (controllerContext.HttpContext != null)) + { + result = controllerContext.HttpContext.GetGlobalResourceObject(ResourceClassKey, resourceName, CultureInfo.CurrentUICulture) as string; + } + + return result; + } + + private static string GetValueInvalidResource(ControllerContext controllerContext) + { + return GetUserResourceString(controllerContext, "PropertyValueInvalid") ?? MvcResources.DefaultModelBinder_ValueInvalid; + } + + private static string GetValueRequiredResource(ControllerContext controllerContext) + { + return GetUserResourceString(controllerContext, "PropertyValueRequired") ?? MvcResources.DefaultModelBinder_ValueRequired; + } + + private static IEnumerable<string> GetZeroBasedIndexes() + { + int i = 0; + while (true) + { + yield return i.ToString(CultureInfo.InvariantCulture); + i++; + } + } + + protected static bool IsModelValid(ModelBindingContext bindingContext) + { + if (bindingContext == null) + { + throw new ArgumentNullException("bindingContext"); + } + if (String.IsNullOrEmpty(bindingContext.ModelName)) + { + return bindingContext.ModelState.IsValid; + } + return bindingContext.ModelState.IsValidField(bindingContext.ModelName); + } + + protected virtual void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext) + { + Dictionary<string, bool> startedValid = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase); + + foreach (ModelValidationResult validationResult in ModelValidator.GetModelValidator(bindingContext.ModelMetadata, controllerContext).Validate(null)) + { + string subPropertyName = CreateSubPropertyName(bindingContext.ModelName, validationResult.MemberName); + + if (!startedValid.ContainsKey(subPropertyName)) + { + startedValid[subPropertyName] = bindingContext.ModelState.IsValidField(subPropertyName); + } + + if (startedValid[subPropertyName]) + { + bindingContext.ModelState.AddModelError(subPropertyName, validationResult.Message); + } + } + } + + protected virtual bool OnModelUpdating(ControllerContext controllerContext, ModelBindingContext bindingContext) + { + // default implementation does nothing + return true; + } + + protected virtual void OnPropertyValidated(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value) + { + // default implementation does nothing + } + + protected virtual bool OnPropertyValidating(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value) + { + // default implementation does nothing + return true; + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We're recording this exception so that we can act on it later.")] + protected virtual void SetProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value) + { + ModelMetadata propertyMetadata = bindingContext.PropertyMetadata[propertyDescriptor.Name]; + propertyMetadata.Model = value; + string modelStateKey = CreateSubPropertyName(bindingContext.ModelName, propertyMetadata.PropertyName); + + // If the value is null, and the validation system can find a Required validator for + // us, we'd prefer to run it before we attempt to set the value; otherwise, property + // setters which throw on null (f.e., Entity Framework properties which are backed by + // non-nullable strings in the DB) will get their error message in ahead of us. + // + // We are effectively using the special validator -- Required -- as a helper to the + // binding system, which is why this code is here instead of in the Validating/Validated + // methods, which are really the old-school validation hooks. + if (value == null && bindingContext.ModelState.IsValidField(modelStateKey)) + { + ModelValidator requiredValidator = ModelValidatorProviders.Providers.GetValidators(propertyMetadata, controllerContext).Where(v => v.IsRequired).FirstOrDefault(); + if (requiredValidator != null) + { + foreach (ModelValidationResult validationResult in requiredValidator.Validate(bindingContext.Model)) + { + bindingContext.ModelState.AddModelError(modelStateKey, validationResult.Message); + } + } + } + + bool isNullValueOnNonNullableType = + value == null && + !TypeHelpers.TypeAllowsNullValue(propertyDescriptor.PropertyType); + + // Try to set a value into the property unless we know it will fail (read-only + // properties and null values with non-nullable types) + if (!propertyDescriptor.IsReadOnly && !isNullValueOnNonNullableType) + { + try + { + propertyDescriptor.SetValue(bindingContext.Model, value); + } + catch (Exception ex) + { + // Only add if we're not already invalid + if (bindingContext.ModelState.IsValidField(modelStateKey)) + { + bindingContext.ModelState.AddModelError(modelStateKey, ex); + } + } + } + + // Last chance for an error on null values with non-nullable types, we'll use + // the default "A value is required." message. + if (isNullValueOnNonNullableType && bindingContext.ModelState.IsValidField(modelStateKey)) + { + bindingContext.ModelState.AddModelError(modelStateKey, GetValueRequiredResource(controllerContext)); + } + } + + private static bool ShouldPerformRequestValidation(ControllerContext controllerContext, ModelBindingContext bindingContext) + { + if (controllerContext == null || controllerContext.Controller == null || bindingContext == null || bindingContext.ModelMetadata == null) + { + // To make unit testing easier, if the caller hasn't specified enough contextual information we just default + // to always pulling the data from a collection that goes through request validation. + return true; + } + + // We should perform request validation only if both the controller and the model ask for it. This is the + // default behavior for both. If either the controller (via [ValidateInput(false)]) or the model (via [AllowHtml]) + // opts out, we don't validate. + return (controllerContext.Controller.ValidateRequest && bindingContext.ModelMetadata.RequestValidationEnabled); + } + + private static bool ShouldUpdateProperty(PropertyDescriptor property, Predicate<string> propertyFilter) + { + if (property.IsReadOnly && !CanUpdateReadonlyTypedReference(property.PropertyType)) + { + return false; + } + + // if this property is rejected by the filter, move on + if (!propertyFilter(property.Name)) + { + return false; + } + + // otherwise, allow + return true; + } + + internal object UpdateCollection(ControllerContext controllerContext, ModelBindingContext bindingContext, Type elementType) + { + bool stopOnIndexNotFound; + IEnumerable<string> indexes; + GetIndexes(bindingContext, out stopOnIndexNotFound, out indexes); + IModelBinder elementBinder = Binders.GetBinder(elementType); + + // build up a list of items from the request + List<object> modelList = new List<object>(); + foreach (string currentIndex in indexes) + { + string subIndexKey = CreateSubIndexName(bindingContext.ModelName, currentIndex); + if (!bindingContext.ValueProvider.ContainsPrefix(subIndexKey)) + { + if (stopOnIndexNotFound) + { + // we ran out of elements to pull + break; + } + else + { + continue; + } + } + + ModelBindingContext innerContext = new ModelBindingContext() + { + ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, elementType), + ModelName = subIndexKey, + ModelState = bindingContext.ModelState, + PropertyFilter = bindingContext.PropertyFilter, + ValueProvider = bindingContext.ValueProvider + }; + object thisElement = elementBinder.BindModel(controllerContext, innerContext); + + // we need to merge model errors up + AddValueRequiredMessageToModelState(controllerContext, bindingContext.ModelState, subIndexKey, elementType, thisElement); + modelList.Add(thisElement); + } + + // if there weren't any elements at all in the request, just return + if (modelList.Count == 0) + { + return null; + } + + // replace the original collection + object collection = bindingContext.Model; + CollectionHelpers.ReplaceCollection(elementType, collection, modelList); + return collection; + } + + internal object UpdateDictionary(ControllerContext controllerContext, ModelBindingContext bindingContext, Type keyType, Type valueType) + { + bool stopOnIndexNotFound; + IEnumerable<string> indexes; + GetIndexes(bindingContext, out stopOnIndexNotFound, out indexes); + + IModelBinder keyBinder = Binders.GetBinder(keyType); + IModelBinder valueBinder = Binders.GetBinder(valueType); + + // build up a list of items from the request + List<KeyValuePair<object, object>> modelList = new List<KeyValuePair<object, object>>(); + foreach (string currentIndex in indexes) + { + string subIndexKey = CreateSubIndexName(bindingContext.ModelName, currentIndex); + string keyFieldKey = CreateSubPropertyName(subIndexKey, "key"); + string valueFieldKey = CreateSubPropertyName(subIndexKey, "value"); + + if (!(bindingContext.ValueProvider.ContainsPrefix(keyFieldKey) && bindingContext.ValueProvider.ContainsPrefix(valueFieldKey))) + { + if (stopOnIndexNotFound) + { + // we ran out of elements to pull + break; + } + else + { + continue; + } + } + + // bind the key + ModelBindingContext keyBindingContext = new ModelBindingContext() + { + ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, keyType), + ModelName = keyFieldKey, + ModelState = bindingContext.ModelState, + ValueProvider = bindingContext.ValueProvider + }; + object thisKey = keyBinder.BindModel(controllerContext, keyBindingContext); + + // we need to merge model errors up + AddValueRequiredMessageToModelState(controllerContext, bindingContext.ModelState, keyFieldKey, keyType, thisKey); + if (!keyType.IsInstanceOfType(thisKey)) + { + // we can't add an invalid key, so just move on + continue; + } + + // bind the value + modelList.Add(CreateEntryForModel(controllerContext, bindingContext, valueType, valueBinder, valueFieldKey, thisKey)); + } + + // Let's try another method + if (modelList.Count == 0) + { + IEnumerableValueProvider enumerableValueProvider = bindingContext.ValueProvider as IEnumerableValueProvider; + if (enumerableValueProvider != null) + { + IDictionary<string, string> keys = enumerableValueProvider.GetKeysFromPrefix(bindingContext.ModelName); + foreach (var thisKey in keys) + { + modelList.Add(CreateEntryForModel(controllerContext, bindingContext, valueType, valueBinder, thisKey.Value, thisKey.Key)); + } + } + } + + // if there weren't any elements at all in the request, just return + if (modelList.Count == 0) + { + return null; + } + + // replace the original collection + object dictionary = bindingContext.Model; + CollectionHelpers.ReplaceDictionary(keyType, valueType, dictionary, modelList); + return dictionary; + } + + private static KeyValuePair<object, object> CreateEntryForModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type valueType, IModelBinder valueBinder, string modelName, object modelKey) + { + ModelBindingContext valueBindingContext = new ModelBindingContext() + { + ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, valueType), + ModelName = modelName, + ModelState = bindingContext.ModelState, + PropertyFilter = bindingContext.PropertyFilter, + ValueProvider = bindingContext.ValueProvider + }; + object thisValue = valueBinder.BindModel(controllerContext, valueBindingContext); + AddValueRequiredMessageToModelState(controllerContext, bindingContext.ModelState, modelName, valueType, thisValue); + return new KeyValuePair<object, object>(modelKey, thisValue); + } + + // This helper type is used because we're working with strongly-typed collections, but we don't know the Ts + // ahead of time. By using the generic methods below, we can consolidate the collection-specific code in a + // single helper type rather than having reflection-based calls spread throughout the DefaultModelBinder type. + // There is a single point of entry to each of the methods below, so they're fairly simple to maintain. + + private static class CollectionHelpers + { + private static readonly MethodInfo _replaceCollectionMethod = typeof(CollectionHelpers).GetMethod("ReplaceCollectionImpl", BindingFlags.Static | BindingFlags.NonPublic); + private static readonly MethodInfo _replaceDictionaryMethod = typeof(CollectionHelpers).GetMethod("ReplaceDictionaryImpl", BindingFlags.Static | BindingFlags.NonPublic); + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public static void ReplaceCollection(Type collectionType, object collection, object newContents) + { + MethodInfo targetMethod = _replaceCollectionMethod.MakeGenericMethod(collectionType); + targetMethod.Invoke(null, new object[] { collection, newContents }); + } + + private static void ReplaceCollectionImpl<T>(ICollection<T> collection, IEnumerable newContents) + { + collection.Clear(); + if (newContents != null) + { + foreach (object item in newContents) + { + // if the item was not a T, some conversion failed. the error message will be propagated, + // but in the meanwhile we need to make a placeholder element in the array. + T castItem = (item is T) ? (T)item : default(T); + collection.Add(castItem); + } + } + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public static void ReplaceDictionary(Type keyType, Type valueType, object dictionary, object newContents) + { + MethodInfo targetMethod = _replaceDictionaryMethod.MakeGenericMethod(keyType, valueType); + targetMethod.Invoke(null, new object[] { dictionary, newContents }); + } + + private static void ReplaceDictionaryImpl<TKey, TValue>(IDictionary<TKey, TValue> dictionary, IEnumerable<KeyValuePair<object, object>> newContents) + { + dictionary.Clear(); + foreach (KeyValuePair<object, object> item in newContents) + { + // if the item was not a T, some conversion failed. the error message will be propagated, + // but in the meanwhile we need to make a placeholder element in the dictionary. + TKey castKey = (TKey)item.Key; // this cast shouldn't fail + TValue castValue = (item.Value is TValue) ? (TValue)item.Value : default(TValue); + dictionary[castKey] = castValue; + } + } + } + } +} diff --git a/src/System.Web.Mvc/DefaultViewLocationCache.cs b/src/System.Web.Mvc/DefaultViewLocationCache.cs new file mode 100644 index 00000000..3e6090f6 --- /dev/null +++ b/src/System.Web.Mvc/DefaultViewLocationCache.cs @@ -0,0 +1,52 @@ +using System.Diagnostics.CodeAnalysis; +using System.Web.Caching; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + public class DefaultViewLocationCache : IViewLocationCache + { + private static readonly TimeSpan _defaultTimeSpan = new TimeSpan(0, 15, 0); + + [SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification = "The reference type is immutable. ")] + public static readonly IViewLocationCache Null = new NullViewLocationCache(); + + public DefaultViewLocationCache() + : this(_defaultTimeSpan) + { + } + + public DefaultViewLocationCache(TimeSpan timeSpan) + { + if (timeSpan.Ticks < 0) + { + throw new InvalidOperationException(MvcResources.DefaultViewLocationCache_NegativeTimeSpan); + } + TimeSpan = timeSpan; + } + + public TimeSpan TimeSpan { get; private set; } + + #region IViewLocationCache Members + + public string GetViewLocation(HttpContextBase httpContext, string key) + { + if (httpContext == null) + { + throw new ArgumentNullException("httpContext"); + } + return (string)httpContext.Cache[key]; + } + + public void InsertViewLocation(HttpContextBase httpContext, string key, string virtualPath) + { + if (httpContext == null) + { + throw new ArgumentNullException("httpContext"); + } + httpContext.Cache.Insert(key, virtualPath, null /* dependencies */, Cache.NoAbsoluteExpiration, TimeSpan); + } + + #endregion + } +} diff --git a/src/System.Web.Mvc/DependencyResolver.cs b/src/System.Web.Mvc/DependencyResolver.cs new file mode 100644 index 00000000..6fdb0807 --- /dev/null +++ b/src/System.Web.Mvc/DependencyResolver.cs @@ -0,0 +1,210 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + public class DependencyResolver + { + private static DependencyResolver _instance = new DependencyResolver(); + + private IDependencyResolver _current; + + /// <summary> + /// Cache should always be a new CacheDependencyResolver(_current). + /// </summary> + private CacheDependencyResolver _currentCache; + + public DependencyResolver() + { + InnerSetResolver(new DefaultDependencyResolver()); + } + + public static IDependencyResolver Current + { + get { return _instance.InnerCurrent; } + } + + internal static IDependencyResolver CurrentCache + { + get { return _instance.InnerCurrentCache; } + } + + public IDependencyResolver InnerCurrent + { + get { return _current; } + } + + /// <summary> + /// Provides caching over results returned by Current. + /// </summary> + internal IDependencyResolver InnerCurrentCache + { + get { return _currentCache; } + } + + public static void SetResolver(IDependencyResolver resolver) + { + _instance.InnerSetResolver(resolver); + } + + public static void SetResolver(object commonServiceLocator) + { + _instance.InnerSetResolver(commonServiceLocator); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types.")] + public static void SetResolver(Func<Type, object> getService, Func<Type, IEnumerable<object>> getServices) + { + _instance.InnerSetResolver(getService, getServices); + } + + public void InnerSetResolver(IDependencyResolver resolver) + { + if (resolver == null) + { + throw new ArgumentNullException("resolver"); + } + + _current = resolver; + _currentCache = new CacheDependencyResolver(_current); + } + + public void InnerSetResolver(object commonServiceLocator) + { + if (commonServiceLocator == null) + { + throw new ArgumentNullException("commonServiceLocator"); + } + + Type locatorType = commonServiceLocator.GetType(); + MethodInfo getInstance = locatorType.GetMethod("GetInstance", new[] { typeof(Type) }); + MethodInfo getInstances = locatorType.GetMethod("GetAllInstances", new[] { typeof(Type) }); + + if (getInstance == null || + getInstance.ReturnType != typeof(object) || + getInstances == null || + getInstances.ReturnType != typeof(IEnumerable<object>)) + { + throw new ArgumentException( + String.Format( + CultureInfo.CurrentCulture, + MvcResources.DependencyResolver_DoesNotImplementICommonServiceLocator, + locatorType.FullName), + "commonServiceLocator"); + } + + var getService = (Func<Type, object>)Delegate.CreateDelegate(typeof(Func<Type, object>), commonServiceLocator, getInstance); + var getServices = (Func<Type, IEnumerable<object>>)Delegate.CreateDelegate(typeof(Func<Type, IEnumerable<object>>), commonServiceLocator, getInstances); + + InnerSetResolver(new DelegateBasedDependencyResolver(getService, getServices)); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types.")] + public void InnerSetResolver(Func<Type, object> getService, Func<Type, IEnumerable<object>> getServices) + { + if (getService == null) + { + throw new ArgumentNullException("getService"); + } + if (getServices == null) + { + throw new ArgumentNullException("getServices"); + } + + InnerSetResolver(new DelegateBasedDependencyResolver(getService, getServices)); + } + + /// <summary> + /// Wraps an IDependencyResolver and ensures single instance per-type. + /// </summary> + /// <remarks> + /// Note it's possible for multiple threads to race and call the _resolver service multiple times. + /// We'll pick one winner and ignore the others and still guarantee a unique instance. + /// </remarks> + private sealed class CacheDependencyResolver : IDependencyResolver + { + private readonly ConcurrentDictionary<Type, object> _cache = new ConcurrentDictionary<Type, object>(); + private readonly ConcurrentDictionary<Type, IEnumerable<object>> _cacheMultiple = new ConcurrentDictionary<Type, IEnumerable<object>>(); + + private readonly IDependencyResolver _resolver; + + public CacheDependencyResolver(IDependencyResolver resolver) + { + _resolver = resolver; + } + + public object GetService(Type serviceType) + { + return _cache.GetOrAdd(serviceType, _resolver.GetService); + } + + public IEnumerable<object> GetServices(Type serviceType) + { + return _cacheMultiple.GetOrAdd(serviceType, _resolver.GetServices); + } + } + + private class DefaultDependencyResolver : IDependencyResolver + { + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "This method might throw exceptions whose type we cannot strongly link against; namely, ActivationException from common service locator")] + public object GetService(Type serviceType) + { + // Since attempting to create an instance of an interface or an abstract type results in an exception, immediately return null + // to improve performance and the debugging experience with first-chance exceptions enabled. + if (serviceType.IsInterface || serviceType.IsAbstract) + { + return null; + } + + try + { + return Activator.CreateInstance(serviceType); + } + catch + { + return null; + } + } + + public IEnumerable<object> GetServices(Type serviceType) + { + return Enumerable.Empty<object>(); + } + } + + private class DelegateBasedDependencyResolver : IDependencyResolver + { + private Func<Type, object> _getService; + private Func<Type, IEnumerable<object>> _getServices; + + public DelegateBasedDependencyResolver(Func<Type, object> getService, Func<Type, IEnumerable<object>> getServices) + { + _getService = getService; + _getServices = getServices; + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "This method might throw exceptions whose type we cannot strongly link against; namely, ActivationException from common service locator")] + public object GetService(Type type) + { + try + { + return _getService.Invoke(type); + } + catch + { + return null; + } + } + + public IEnumerable<object> GetServices(Type type) + { + return _getServices(type); + } + } + } +} diff --git a/src/System.Web.Mvc/DependencyResolverExtensions.cs b/src/System.Web.Mvc/DependencyResolverExtensions.cs new file mode 100644 index 00000000..db9c5129 --- /dev/null +++ b/src/System.Web.Mvc/DependencyResolverExtensions.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Linq; + +namespace System.Web.Mvc +{ + public static class DependencyResolverExtensions + { + public static TService GetService<TService>(this IDependencyResolver resolver) + { + return (TService)resolver.GetService(typeof(TService)); + } + + public static IEnumerable<TService> GetServices<TService>(this IDependencyResolver resolver) + { + return resolver.GetServices(typeof(TService)).Cast<TService>(); + } + } +} diff --git a/src/System.Web.Mvc/DescriptorUtil.cs b/src/System.Web.Mvc/DescriptorUtil.cs new file mode 100644 index 00000000..8e7905fb --- /dev/null +++ b/src/System.Web.Mvc/DescriptorUtil.cs @@ -0,0 +1,86 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; +using System.Text; +using System.Threading; + +namespace System.Web.Mvc +{ + internal static class DescriptorUtil + { + private static void AppendPartToUniqueIdBuilder(StringBuilder builder, object part) + { + if (part == null) + { + builder.Append("[-1]"); + } + else + { + string partString = Convert.ToString(part, CultureInfo.InvariantCulture); + builder.AppendFormat("[{0}]{1}", partString.Length, partString); + } + } + + public static string CreateUniqueId(params object[] parts) + { + return CreateUniqueId((IEnumerable<object>)parts); + } + + public static string CreateUniqueId(IEnumerable<object> parts) + { + // returns a unique string made up of the pieces passed in + StringBuilder builder = new StringBuilder(); + foreach (object part in parts) + { + // We can special-case certain part types + + MemberInfo memberInfo = part as MemberInfo; + if (memberInfo != null) + { + AppendPartToUniqueIdBuilder(builder, memberInfo.Module.ModuleVersionId); + AppendPartToUniqueIdBuilder(builder, memberInfo.MetadataToken); + continue; + } + + IUniquelyIdentifiable uniquelyIdentifiable = part as IUniquelyIdentifiable; + if (uniquelyIdentifiable != null) + { + AppendPartToUniqueIdBuilder(builder, uniquelyIdentifiable.UniqueId); + continue; + } + + AppendPartToUniqueIdBuilder(builder, part); + } + + return builder.ToString(); + } + + public static TDescriptor[] LazilyFetchOrCreateDescriptors<TReflection, TDescriptor>(ref TDescriptor[] cacheLocation, Func<TReflection[]> initializer, Func<TReflection, TDescriptor> converter) + { + // did we already calculate this once? + TDescriptor[] existingCache = Interlocked.CompareExchange(ref cacheLocation, null, null); + if (existingCache != null) + { + return existingCache; + } + + // Note: since this code operates on arrays it is more efficient to call simple array operations + // instead of LINQ-y extension methods such as Select and Where. DO NOT attempt to simplify this + // without testing the performance impact. + TReflection[] memberInfos = initializer(); + List<TDescriptor> descriptorsList = new List<TDescriptor>(memberInfos.Length); + for (int i = 0; i < memberInfos.Length; i++) + { + TDescriptor descriptor = converter(memberInfos[i]); + if (descriptor != null) + { + descriptorsList.Add(descriptor); + } + } + TDescriptor[] descriptors = descriptorsList.ToArray(); + + TDescriptor[] updatedCache = Interlocked.CompareExchange(ref cacheLocation, descriptors, null); + return updatedCache ?? descriptors; + } + } +} diff --git a/src/System.Web.Mvc/DictionaryHelpers.cs b/src/System.Web.Mvc/DictionaryHelpers.cs new file mode 100644 index 00000000..7f917971 --- /dev/null +++ b/src/System.Web.Mvc/DictionaryHelpers.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Linq; + +namespace System.Web.Mvc +{ + internal static class DictionaryHelpers + { + public static IEnumerable<KeyValuePair<string, TValue>> FindKeysWithPrefix<TValue>(IDictionary<string, TValue> dictionary, string prefix) + { + TValue exactMatchValue; + if (dictionary.TryGetValue(prefix, out exactMatchValue)) + { + yield return new KeyValuePair<string, TValue>(prefix, exactMatchValue); + } + + foreach (var entry in dictionary) + { + string key = entry.Key; + + if (key.Length <= prefix.Length) + { + continue; + } + + if (!key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + char charAfterPrefix = key[prefix.Length]; + switch (charAfterPrefix) + { + case '[': + case '.': + yield return entry; + break; + } + } + } + + public static bool DoesAnyKeyHavePrefix<TValue>(IDictionary<string, TValue> dictionary, string prefix) + { + return FindKeysWithPrefix(dictionary, prefix).Any(); + } + + public static TValue GetOrDefault<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key, TValue @default) + { + TValue value; + if (dict.TryGetValue(key, out value)) + { + return value; + } + return @default; + } + } +} diff --git a/src/System.Web.Mvc/DictionaryValueProvider`1.cs b/src/System.Web.Mvc/DictionaryValueProvider`1.cs new file mode 100644 index 00000000..d1abfa94 --- /dev/null +++ b/src/System.Web.Mvc/DictionaryValueProvider`1.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using System.Globalization; + +namespace System.Web.Mvc +{ + public class DictionaryValueProvider<TValue> : IValueProvider, IEnumerableValueProvider + { + private readonly HashSet<string> _prefixes = new HashSet<string>(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary<string, ValueProviderResult> _values = new Dictionary<string, ValueProviderResult>(StringComparer.OrdinalIgnoreCase); + + public DictionaryValueProvider(IDictionary<string, TValue> dictionary, CultureInfo culture) + { + if (dictionary == null) + { + throw new ArgumentNullException("dictionary"); + } + + AddValues(dictionary, culture); + } + + private void AddValues(IDictionary<string, TValue> dictionary, CultureInfo culture) + { + if (dictionary.Count > 0) + { + _prefixes.Add(String.Empty); + } + + foreach (var entry in dictionary) + { + _prefixes.UnionWith(ValueProviderUtil.GetPrefixes(entry.Key)); + + object rawValue = entry.Value; + string attemptedValue = Convert.ToString(rawValue, culture); + _values[entry.Key] = new ValueProviderResult(rawValue, attemptedValue, culture); + } + } + + public virtual bool ContainsPrefix(string prefix) + { + if (prefix == null) + { + throw new ArgumentNullException("prefix"); + } + + return _prefixes.Contains(prefix); + } + + public virtual ValueProviderResult GetValue(string key) + { + if (key == null) + { + throw new ArgumentNullException("key"); + } + + ValueProviderResult valueProviderResult; + _values.TryGetValue(key, out valueProviderResult); + return valueProviderResult; + } + + public virtual IDictionary<string, string> GetKeysFromPrefix(string prefix) + { + return ValueProviderUtil.GetKeysFromPrefix(_prefixes, prefix); + } + } +} diff --git a/src/System.Web.Mvc/DynamicViewDataDictionary.cs b/src/System.Web.Mvc/DynamicViewDataDictionary.cs new file mode 100644 index 00000000..b7b36b75 --- /dev/null +++ b/src/System.Web.Mvc/DynamicViewDataDictionary.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Dynamic; + +namespace System.Web.Mvc +{ + internal sealed class DynamicViewDataDictionary : DynamicObject + { + private readonly Func<ViewDataDictionary> _viewDataThunk; + + public DynamicViewDataDictionary(Func<ViewDataDictionary> viewDataThunk) + { + _viewDataThunk = viewDataThunk; + } + + private ViewDataDictionary ViewData + { + get + { + ViewDataDictionary viewData = _viewDataThunk(); + Debug.Assert(viewData != null); + return viewData; + } + } + + // Implementing this function improves the debugging experience as it provides the debugger with the list of all + // the properties currently defined on the object + public override IEnumerable<string> GetDynamicMemberNames() + { + return ViewData.Keys; + } + + public override bool TryGetMember(GetMemberBinder binder, out object result) + { + result = ViewData[binder.Name]; + // since ViewDataDictionary always returns a result even if the key does not exist, always return true + return true; + } + + public override bool TrySetMember(SetMemberBinder binder, object value) + { + ViewData[binder.Name] = value; + // you can always set a key in the dictionary so return true + return true; + } + } +} diff --git a/src/System.Web.Mvc/EmptyModelMetadataProvider.cs b/src/System.Web.Mvc/EmptyModelMetadataProvider.cs new file mode 100644 index 00000000..000f334c --- /dev/null +++ b/src/System.Web.Mvc/EmptyModelMetadataProvider.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace System.Web.Mvc +{ + public class EmptyModelMetadataProvider : AssociatedMetadataProvider + { + protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName) + { + return new ModelMetadata(this, containerType, modelAccessor, modelType, propertyName); + } + } +} diff --git a/src/System.Web.Mvc/EmptyModelValidatorProvider.cs b/src/System.Web.Mvc/EmptyModelValidatorProvider.cs new file mode 100644 index 00000000..0bd617c4 --- /dev/null +++ b/src/System.Web.Mvc/EmptyModelValidatorProvider.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Linq; + +namespace System.Web.Mvc +{ + public class EmptyModelValidatorProvider : ModelValidatorProvider + { + public override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context) + { + return Enumerable.Empty<ModelValidator>(); + } + } +} diff --git a/src/System.Web.Mvc/EmptyResult.cs b/src/System.Web.Mvc/EmptyResult.cs new file mode 100644 index 00000000..f67ab681 --- /dev/null +++ b/src/System.Web.Mvc/EmptyResult.cs @@ -0,0 +1,17 @@ +namespace System.Web.Mvc +{ + // represents a result that doesn't do anything, like a controller action returning null + public class EmptyResult : ActionResult + { + private static readonly EmptyResult _singleton = new EmptyResult(); + + internal static EmptyResult Instance + { + get { return _singleton; } + } + + public override void ExecuteResult(ControllerContext context) + { + } + } +} diff --git a/src/System.Web.Mvc/Error.cs b/src/System.Web.Mvc/Error.cs new file mode 100644 index 00000000..173de8ca --- /dev/null +++ b/src/System.Web.Mvc/Error.cs @@ -0,0 +1,76 @@ +using System.Globalization; +using System.Web.Mvc.Async; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + internal static class Error + { + public static InvalidOperationException AsyncActionMethodSelector_CouldNotFindMethod(string methodName, Type controllerType) + { + string message = String.Format(CultureInfo.CurrentCulture, MvcResources.AsyncActionMethodSelector_CouldNotFindMethod, + methodName, controllerType); + return new InvalidOperationException(message); + } + + public static InvalidOperationException AsyncCommon_AsyncResultAlreadyConsumed() + { + return new InvalidOperationException(MvcResources.AsyncCommon_AsyncResultAlreadyConsumed); + } + + public static InvalidOperationException AsyncCommon_ControllerMustImplementIAsyncManagerContainer(Type actualControllerType) + { + string message = String.Format(CultureInfo.CurrentCulture, MvcResources.AsyncCommon_ControllerMustImplementIAsyncManagerContainer, + actualControllerType); + return new InvalidOperationException(message); + } + + public static ArgumentException AsyncCommon_InvalidAsyncResult(string parameterName) + { + return new ArgumentException(MvcResources.AsyncCommon_InvalidAsyncResult, parameterName); + } + + public static ArgumentOutOfRangeException AsyncCommon_InvalidTimeout(string parameterName) + { + return new ArgumentOutOfRangeException(parameterName, MvcResources.AsyncCommon_InvalidTimeout); + } + + public static InvalidOperationException ChildActionOnlyAttribute_MustBeInChildRequest(ActionDescriptor actionDescriptor) + { + string message = String.Format(CultureInfo.CurrentCulture, MvcResources.ChildActionOnlyAttribute_MustBeInChildRequest, + actionDescriptor.ActionName); + return new InvalidOperationException(message); + } + + public static ArgumentException ParameterCannotBeNullOrEmpty(string parameterName) + { + return new ArgumentException(MvcResources.Common_NullOrEmpty, parameterName); + } + + public static InvalidOperationException PropertyCannotBeNullOrEmpty(string propertyName) + { + string message = String.Format(CultureInfo.CurrentCulture, MvcResources.Common_PropertyCannotBeNullOrEmpty, + propertyName); + return new InvalidOperationException(message); + } + + public static SynchronousOperationException SynchronizationContextUtil_ExceptionThrown(Exception innerException) + { + return new SynchronousOperationException(MvcResources.SynchronizationContextUtil_ExceptionThrown, innerException); + } + + public static InvalidOperationException ViewDataDictionary_WrongTModelType(Type valueType, Type modelType) + { + string message = String.Format(CultureInfo.CurrentCulture, MvcResources.ViewDataDictionary_WrongTModelType, + valueType, modelType); + return new InvalidOperationException(message); + } + + public static InvalidOperationException ViewDataDictionary_ModelCannotBeNull(Type modelType) + { + string message = String.Format(CultureInfo.CurrentCulture, MvcResources.ViewDataDictionary_ModelCannotBeNull, + modelType); + return new InvalidOperationException(message); + } + } +} diff --git a/src/System.Web.Mvc/ExceptionContext.cs b/src/System.Web.Mvc/ExceptionContext.cs new file mode 100644 index 00000000..29cbf6df --- /dev/null +++ b/src/System.Web.Mvc/ExceptionContext.cs @@ -0,0 +1,36 @@ +using System.Diagnostics.CodeAnalysis; + +namespace System.Web.Mvc +{ + public class ExceptionContext : ControllerContext + { + private ActionResult _result; + + // parameterless constructor used for mocking + public ExceptionContext() + { + } + + [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "The virtual property setters are only to support mocking frameworks, in which case this constructor shouldn't be called anyway.")] + public ExceptionContext(ControllerContext controllerContext, Exception exception) + : base(controllerContext) + { + if (exception == null) + { + throw new ArgumentNullException("exception"); + } + + Exception = exception; + } + + public virtual Exception Exception { get; set; } + + public bool ExceptionHandled { get; set; } + + public ActionResult Result + { + get { return _result ?? EmptyResult.Instance; } + set { _result = value; } + } + } +} diff --git a/src/System.Web.Mvc/ExpressionHelper.cs b/src/System.Web.Mvc/ExpressionHelper.cs new file mode 100644 index 00000000..69432305 --- /dev/null +++ b/src/System.Web.Mvc/ExpressionHelper.cs @@ -0,0 +1,131 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Web.Mvc.ExpressionUtil; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + public static class ExpressionHelper + { + public static string GetExpressionText(string expression) + { + return + String.Equals(expression, "model", StringComparison.OrdinalIgnoreCase) + ? String.Empty // If it's exactly "model", then give them an empty string, to replicate the lambda behavior + : expression; + } + + public static string GetExpressionText(LambdaExpression expression) + { + // Split apart the expression string for property/field accessors to create its name + Stack<string> nameParts = new Stack<string>(); + Expression part = expression.Body; + + while (part != null) + { + if (part.NodeType == ExpressionType.Call) + { + MethodCallExpression methodExpression = (MethodCallExpression)part; + + if (!IsSingleArgumentIndexer(methodExpression)) + { + break; + } + + nameParts.Push( + GetIndexerInvocation( + methodExpression.Arguments.Single(), + expression.Parameters.ToArray())); + + part = methodExpression.Object; + } + else if (part.NodeType == ExpressionType.ArrayIndex) + { + BinaryExpression binaryExpression = (BinaryExpression)part; + + nameParts.Push( + GetIndexerInvocation( + binaryExpression.Right, + expression.Parameters.ToArray())); + + part = binaryExpression.Left; + } + else if (part.NodeType == ExpressionType.MemberAccess) + { + MemberExpression memberExpressionPart = (MemberExpression)part; + nameParts.Push("." + memberExpressionPart.Member.Name); + part = memberExpressionPart.Expression; + } + else if (part.NodeType == ExpressionType.Parameter) + { + // Dev10 Bug #907611 + // When the expression is parameter based (m => m.Something...), we'll push an empty + // string onto the stack and stop evaluating. The extra empty string makes sure that + // we don't accidentally cut off too much of m => m.Model. + nameParts.Push(String.Empty); + part = null; + } + else + { + break; + } + } + + // If it starts with "model", then strip that away + if (nameParts.Count > 0 && String.Equals(nameParts.Peek(), ".model", StringComparison.OrdinalIgnoreCase)) + { + nameParts.Pop(); + } + + if (nameParts.Count > 0) + { + return nameParts.Aggregate((left, right) => left + right).TrimStart('.'); + } + + return String.Empty; + } + + private static string GetIndexerInvocation(Expression expression, ParameterExpression[] parameters) + { + Expression converted = Expression.Convert(expression, typeof(object)); + ParameterExpression fakeParameter = Expression.Parameter(typeof(object), null); + Expression<Func<object, object>> lambda = Expression.Lambda<Func<object, object>>(converted, fakeParameter); + Func<object, object> func; + + try + { + func = CachedExpressionCompiler.Process(lambda); + } + catch (InvalidOperationException ex) + { + throw new InvalidOperationException( + String.Format( + CultureInfo.CurrentCulture, + MvcResources.ExpressionHelper_InvalidIndexerExpression, + expression, + parameters[0].Name), + ex); + } + + return "[" + Convert.ToString(func(null), CultureInfo.InvariantCulture) + "]"; + } + + internal static bool IsSingleArgumentIndexer(Expression expression) + { + MethodCallExpression methodExpression = expression as MethodCallExpression; + if (methodExpression == null || methodExpression.Arguments.Count != 1) + { + return false; + } + + return methodExpression.Method + .DeclaringType + .GetDefaultMembers() + .OfType<PropertyInfo>() + .Any(p => p.GetGetMethod() == methodExpression.Method); + } + } +} diff --git a/src/System.Web.Mvc/ExpressionUtil/BinaryExpressionFingerprint.cs b/src/System.Web.Mvc/ExpressionUtil/BinaryExpressionFingerprint.cs new file mode 100644 index 00000000..e1c50e74 --- /dev/null +++ b/src/System.Web.Mvc/ExpressionUtil/BinaryExpressionFingerprint.cs @@ -0,0 +1,41 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; +using System.Reflection; + +#pragma warning disable 659 // overrides AddToHashCodeCombiner instead + +namespace System.Web.Mvc.ExpressionUtil +{ + // BinaryExpression fingerprint class + // Useful for things like array[index] + + [SuppressMessage("Microsoft.Usage", "CA2218:OverrideGetHashCodeOnOverridingEquals", Justification = "Overrides AddToHashCodeCombiner() instead.")] + internal sealed class BinaryExpressionFingerprint : ExpressionFingerprint + { + public BinaryExpressionFingerprint(ExpressionType nodeType, Type type, MethodInfo method) + : base(nodeType, type) + { + // Other properties on BinaryExpression (like IsLifted / IsLiftedToNull) are simply derived + // from Type and NodeType, so they're not necessary for inclusion in the fingerprint. + + Method = method; + } + + // http://msdn.microsoft.com/en-us/library/system.linq.expressions.binaryexpression.method.aspx + public MethodInfo Method { get; private set; } + + public override bool Equals(object obj) + { + BinaryExpressionFingerprint other = obj as BinaryExpressionFingerprint; + return (other != null) + && Equals(this.Method, other.Method) + && this.Equals(other); + } + + internal override void AddToHashCodeCombiner(HashCodeCombiner combiner) + { + combiner.AddObject(Method); + base.AddToHashCodeCombiner(combiner); + } + } +} diff --git a/src/System.Web.Mvc/ExpressionUtil/CachedExpressionCompiler.cs b/src/System.Web.Mvc/ExpressionUtil/CachedExpressionCompiler.cs new file mode 100644 index 00000000..9d6d3fba --- /dev/null +++ b/src/System.Web.Mvc/ExpressionUtil/CachedExpressionCompiler.cs @@ -0,0 +1,142 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; + +namespace System.Web.Mvc.ExpressionUtil +{ + internal static class CachedExpressionCompiler + { + // This is the entry point to the cached expression compilation system. The system + // will try to turn the expression into an actual delegate as quickly as possible, + // relying on cache lookups and other techniques to save time if appropriate. + // If the provided expression is particularly obscure and the system doesn't know + // how to handle it, we'll just compile the expression as normal. + public static Func<TModel, TValue> Process<TModel, TValue>(Expression<Func<TModel, TValue>> lambdaExpression) + { + return Compiler<TModel, TValue>.Compile(lambdaExpression); + } + + private static class Compiler<TIn, TOut> + { + private static Func<TIn, TOut> _identityFunc; + + private static readonly ConcurrentDictionary<MemberInfo, Func<TIn, TOut>> _simpleMemberAccessDict = + new ConcurrentDictionary<MemberInfo, Func<TIn, TOut>>(); + + private static readonly ConcurrentDictionary<MemberInfo, Func<object, TOut>> _constMemberAccessDict = + new ConcurrentDictionary<MemberInfo, Func<object, TOut>>(); + + private static readonly ConcurrentDictionary<ExpressionFingerprintChain, Hoisted<TIn, TOut>> _fingerprintedCache = + new ConcurrentDictionary<ExpressionFingerprintChain, Hoisted<TIn, TOut>>(); + + public static Func<TIn, TOut> Compile(Expression<Func<TIn, TOut>> expr) + { + return CompileFromIdentityFunc(expr) + ?? CompileFromConstLookup(expr) + ?? CompileFromMemberAccess(expr) + ?? CompileFromFingerprint(expr) + ?? CompileSlow(expr); + } + + private static Func<TIn, TOut> CompileFromConstLookup(Expression<Func<TIn, TOut>> expr) + { + ConstantExpression constExpr = expr.Body as ConstantExpression; + if (constExpr != null) + { + // model => {const} + + TOut constantValue = (TOut)constExpr.Value; + return _ => constantValue; + } + + return null; + } + + private static Func<TIn, TOut> CompileFromIdentityFunc(Expression<Func<TIn, TOut>> expr) + { + if (expr.Body == expr.Parameters[0]) + { + // model => model + + // don't need to lock, as all identity funcs are identical + if (_identityFunc == null) + { + _identityFunc = expr.Compile(); + } + + return _identityFunc; + } + + return null; + } + + private static Func<TIn, TOut> CompileFromFingerprint(Expression<Func<TIn, TOut>> expr) + { + List<object> capturedConstants; + ExpressionFingerprintChain fingerprint = FingerprintingExpressionVisitor.GetFingerprintChain(expr, out capturedConstants); + + if (fingerprint != null) + { + var del = _fingerprintedCache.GetOrAdd(fingerprint, _ => + { + // Fingerprinting succeeded, but there was a cache miss. Rewrite the expression + // and add the rewritten expression to the cache. + + var hoistedExpr = HoistingExpressionVisitor<TIn, TOut>.Hoist(expr); + return hoistedExpr.Compile(); + }); + return model => del(model, capturedConstants); + } + + // couldn't be fingerprinted + return null; + } + + private static Func<TIn, TOut> CompileFromMemberAccess(Expression<Func<TIn, TOut>> expr) + { + // Performance tests show that on the x64 platform, special-casing static member and + // captured local variable accesses is faster than letting the fingerprinting system + // handle them. On the x86 platform, the fingerprinting system is faster, but only + // by around one microsecond, so it's not worth it to complicate the logic here with + // an architecture check. + + MemberExpression memberExpr = expr.Body as MemberExpression; + if (memberExpr != null) + { + if (memberExpr.Expression == expr.Parameters[0] || memberExpr.Expression == null) + { + // model => model.Member or model => StaticMember + return _simpleMemberAccessDict.GetOrAdd(memberExpr.Member, _ => expr.Compile()); + } + + ConstantExpression constExpr = memberExpr.Expression as ConstantExpression; + if (constExpr != null) + { + // model => {const}.Member (captured local variable) + var del = _constMemberAccessDict.GetOrAdd(memberExpr.Member, _ => + { + // rewrite as capturedLocal => ((TDeclaringType)capturedLocal).Member + var constParamExpr = Expression.Parameter(typeof(object), "capturedLocal"); + var constCastExpr = Expression.Convert(constParamExpr, memberExpr.Member.DeclaringType); + var newMemberAccessExpr = memberExpr.Update(constCastExpr); + var newLambdaExpr = Expression.Lambda<Func<object, TOut>>(newMemberAccessExpr, constParamExpr); + return newLambdaExpr.Compile(); + }); + + object capturedLocal = constExpr.Value; + return _ => del(capturedLocal); + } + } + + return null; + } + + private static Func<TIn, TOut> CompileSlow(Expression<Func<TIn, TOut>> expr) + { + // fallback compilation system - just compile the expression directly + return expr.Compile(); + } + } + } +} diff --git a/src/System.Web.Mvc/ExpressionUtil/ConditionalExpressionFingerprint.cs b/src/System.Web.Mvc/ExpressionUtil/ConditionalExpressionFingerprint.cs new file mode 100644 index 00000000..ceb7d327 --- /dev/null +++ b/src/System.Web.Mvc/ExpressionUtil/ConditionalExpressionFingerprint.cs @@ -0,0 +1,28 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; + +#pragma warning disable 659 // overrides AddToHashCodeCombiner instead + +namespace System.Web.Mvc.ExpressionUtil +{ + // ConditionalExpression fingerprint class + // Expression of form (test) ? ifTrue : ifFalse + + [SuppressMessage("Microsoft.Usage", "CA2218:OverrideGetHashCodeOnOverridingEquals", Justification = "Overrides AddToHashCodeCombiner() instead.")] + internal sealed class ConditionalExpressionFingerprint : ExpressionFingerprint + { + public ConditionalExpressionFingerprint(ExpressionType nodeType, Type type) + : base(nodeType, type) + { + // There are no properties on ConditionalExpression that are worth including in + // the fingerprint. + } + + public override bool Equals(object obj) + { + ConditionalExpressionFingerprint other = obj as ConditionalExpressionFingerprint; + return (other != null) + && this.Equals(other); + } + } +} diff --git a/src/System.Web.Mvc/ExpressionUtil/ConstantExpressionFingerprint.cs b/src/System.Web.Mvc/ExpressionUtil/ConstantExpressionFingerprint.cs new file mode 100644 index 00000000..484e7976 --- /dev/null +++ b/src/System.Web.Mvc/ExpressionUtil/ConstantExpressionFingerprint.cs @@ -0,0 +1,32 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; + +#pragma warning disable 659 // overrides AddToHashCodeCombiner instead + +namespace System.Web.Mvc.ExpressionUtil +{ + // ConstantExpression fingerprint class + // + // A ConstantExpression might represent a captured local variable, so we can't compile + // the value directly into the cached function. Instead, a placeholder is generated + // and the value is hoisted into a local variables array. This placeholder can then + // be compiled and cached, and the array lookup happens at runtime. + + [SuppressMessage("Microsoft.Usage", "CA2218:OverrideGetHashCodeOnOverridingEquals", Justification = "Overrides AddToHashCodeCombiner() instead.")] + internal sealed class ConstantExpressionFingerprint : ExpressionFingerprint + { + public ConstantExpressionFingerprint(ExpressionType nodeType, Type type) + : base(nodeType, type) + { + // There are no properties on ConstantExpression that are worth including in + // the fingerprint. + } + + public override bool Equals(object obj) + { + ConstantExpressionFingerprint other = obj as ConstantExpressionFingerprint; + return (other != null) + && this.Equals(other); + } + } +} diff --git a/src/System.Web.Mvc/ExpressionUtil/DefaultExpressionFingerprint.cs b/src/System.Web.Mvc/ExpressionUtil/DefaultExpressionFingerprint.cs new file mode 100644 index 00000000..e8503774 --- /dev/null +++ b/src/System.Web.Mvc/ExpressionUtil/DefaultExpressionFingerprint.cs @@ -0,0 +1,28 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; + +#pragma warning disable 659 // overrides AddToHashCodeCombiner instead + +namespace System.Web.Mvc.ExpressionUtil +{ + // DefaultExpression fingerprint class + // Expression of form default(T) + + [SuppressMessage("Microsoft.Usage", "CA2218:OverrideGetHashCodeOnOverridingEquals", Justification = "Overrides AddToHashCodeCombiner() instead.")] + internal sealed class DefaultExpressionFingerprint : ExpressionFingerprint + { + public DefaultExpressionFingerprint(ExpressionType nodeType, Type type) + : base(nodeType, type) + { + // There are no properties on DefaultExpression that are worth including in + // the fingerprint. + } + + public override bool Equals(object obj) + { + DefaultExpressionFingerprint other = obj as DefaultExpressionFingerprint; + return (other != null) + && this.Equals(other); + } + } +} diff --git a/src/System.Web.Mvc/ExpressionUtil/ExpressionFingerprint.cs b/src/System.Web.Mvc/ExpressionUtil/ExpressionFingerprint.cs new file mode 100644 index 00000000..1d9cf5e2 --- /dev/null +++ b/src/System.Web.Mvc/ExpressionUtil/ExpressionFingerprint.cs @@ -0,0 +1,47 @@ +using System.Linq.Expressions; + +namespace System.Web.Mvc.ExpressionUtil +{ + // Serves as the base class for all expression fingerprints. Provides a default implementation + // of GetHashCode(). + + internal abstract class ExpressionFingerprint + { + protected ExpressionFingerprint(ExpressionType nodeType, Type type) + { + NodeType = nodeType; + Type = type; + } + + // the type of expression node, e.g. OP_ADD, MEMBER_ACCESS, etc. + public ExpressionType NodeType { get; private set; } + + // the CLR type resulting from this expression, e.g. int, string, etc. + public Type Type { get; private set; } + + internal virtual void AddToHashCodeCombiner(HashCodeCombiner combiner) + { + combiner.AddInt32((int)NodeType); + combiner.AddObject(Type); + } + + protected bool Equals(ExpressionFingerprint other) + { + return (other != null) + && (this.NodeType == other.NodeType) + && Equals(this.Type, other.Type); + } + + public override bool Equals(object obj) + { + return Equals(obj as ExpressionFingerprint); + } + + public override int GetHashCode() + { + HashCodeCombiner combiner = new HashCodeCombiner(); + AddToHashCodeCombiner(combiner); + return combiner.CombinedHash; + } + } +} diff --git a/src/System.Web.Mvc/ExpressionUtil/ExpressionFingerprintChain.cs b/src/System.Web.Mvc/ExpressionUtil/ExpressionFingerprintChain.cs new file mode 100644 index 00000000..3399dcee --- /dev/null +++ b/src/System.Web.Mvc/ExpressionUtil/ExpressionFingerprintChain.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; + +namespace System.Web.Mvc.ExpressionUtil +{ + // Expression fingerprint chain class + // Contains information used for generalizing, comparing, and recreating Expression instances + // + // Since Expression objects are immutable and are recreated for every invocation of an expression + // helper method, they can't be compared directly. Fingerprinting Expression objects allows + // information about them to be abstracted away, and the fingerprints can be directly compared. + // Consider the process of fingerprinting that all values (parameters, constants, etc.) are hoisted + // and replaced with dummies. What remains can be decomposed into a sequence of operations on specific + // types and specific inputs. + // + // Some sample fingerprints chains: + // + // 2 + 4 -> OP_ADD, CONST:int, NULL, CONST:int + // 2 + 8 -> OP_ADD, CONST:int, NULL, CONST:int + // 2.0 + 4.0 -> OP_ADD, CONST:double, NULL, CONST:double + // + // 2 + 4 and 2 + 8 have the same fingerprint, but 2.0 + 4.0 has a different fingerprint since its + // underlying types differ. Note that this looks a bit like prefix notation and is a side effect + // of how the ExpressionVisitor class recurses into expressions. (Occasionally there will be a NULL + // in the fingerprint chain, which depending on context can denote a static member, a null Conversion + // in a BinaryExpression, and so forth.) + // + // "Hello " + "world" -> OP_ADD, CONST:string, NULL, CONST:string + // "Hello " + {model} -> OP_ADD, CONST:string, NULL, PARAM_0:string + // + // These string concatenations have different fingerprints since the inputs are provided differently: + // one is a constant, the other is a parameter. + // + // ({model} ?? "sample").Length -> MEMBER_ACCESS(String.Length), OP_COALESCE, PARAM_0:string, NULL, CONST:string + // ({model} ?? "other sample").Length -> MEMBER_ACCESS(String.Length), OP_COALESCE, PARAM_0:string, NULL, CONST:string + // + // These expressions have the same fingerprint since all constants of the same underlying type are + // treated equally. + // + // It's also important that the fingerprints don't reference the actual Expression objects that were + // used to generate them, as the fingerprints will be cached, and caching a fingerprint that references + // an Expression will root the Expression (and any objects it references). + + internal sealed class ExpressionFingerprintChain : IEquatable<ExpressionFingerprintChain> + { + public readonly List<ExpressionFingerprint> Elements = new List<ExpressionFingerprint>(); + + public bool Equals(ExpressionFingerprintChain other) + { + // Two chains are considered equal if two elements appearing in the same index in + // each chain are equal (value equality, not referential equality). + + if (other == null) + { + return false; + } + + if (this.Elements.Count != other.Elements.Count) + { + return false; + } + + for (int i = 0; i < this.Elements.Count; i++) + { + if (!Equals(this.Elements[i], other.Elements[i])) + { + return false; + } + } + + return true; + } + + public override bool Equals(object obj) + { + return Equals(obj as ExpressionFingerprintChain); + } + + public override int GetHashCode() + { + HashCodeCombiner combiner = new HashCodeCombiner(); + Elements.ForEach(combiner.AddFingerprint); + return combiner.CombinedHash; + } + } +} diff --git a/src/System.Web.Mvc/ExpressionUtil/FingerprintingExpressionVisitor.cs b/src/System.Web.Mvc/ExpressionUtil/FingerprintingExpressionVisitor.cs new file mode 100644 index 00000000..ceaa6987 --- /dev/null +++ b/src/System.Web.Mvc/ExpressionUtil/FingerprintingExpressionVisitor.cs @@ -0,0 +1,296 @@ +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace System.Web.Mvc.ExpressionUtil +{ + // This is a visitor which produces a fingerprint of an expression. It doesn't + // rewrite the expression in a form which can be compiled and cached. + + internal sealed class FingerprintingExpressionVisitor : ExpressionVisitor + { + private readonly List<object> _seenConstants = new List<object>(); + private readonly List<ParameterExpression> _seenParameters = new List<ParameterExpression>(); + private readonly ExpressionFingerprintChain _currentChain = new ExpressionFingerprintChain(); + private bool _gaveUp; + + private FingerprintingExpressionVisitor() + { + } + + private T GiveUp<T>(T node) + { + // We don't understand this node, so just quit. + + _gaveUp = true; + return node; + } + + // Returns the fingerprint chain + captured constants list for this expression, or null + // if the expression couldn't be fingerprinted. + public static ExpressionFingerprintChain GetFingerprintChain(Expression expr, out List<object> capturedConstants) + { + FingerprintingExpressionVisitor visitor = new FingerprintingExpressionVisitor(); + visitor.Visit(expr); + + if (visitor._gaveUp) + { + capturedConstants = null; + return null; + } + else + { + capturedConstants = visitor._seenConstants; + return visitor._currentChain; + } + } + + public override Expression Visit(Expression node) + { + if (node == null) + { + _currentChain.Elements.Add(null); + return null; + } + else + { + return base.Visit(node); + } + } + + protected override Expression VisitBinary(BinaryExpression node) + { + if (_gaveUp) + { + return node; + } + _currentChain.Elements.Add(new BinaryExpressionFingerprint(node.NodeType, node.Type, node.Method)); + return base.VisitBinary(node); + } + + protected override Expression VisitBlock(BlockExpression node) + { + return GiveUp(node); + } + + protected override CatchBlock VisitCatchBlock(CatchBlock node) + { + return GiveUp(node); + } + + protected override Expression VisitConditional(ConditionalExpression node) + { + if (_gaveUp) + { + return node; + } + _currentChain.Elements.Add(new ConditionalExpressionFingerprint(node.NodeType, node.Type)); + return base.VisitConditional(node); + } + + protected override Expression VisitConstant(ConstantExpression node) + { + if (_gaveUp) + { + return node; + } + + _seenConstants.Add(node.Value); + _currentChain.Elements.Add(new ConstantExpressionFingerprint(node.NodeType, node.Type)); + return base.VisitConstant(node); + } + + protected override Expression VisitDebugInfo(DebugInfoExpression node) + { + return GiveUp(node); + } + + protected override Expression VisitDefault(DefaultExpression node) + { + if (_gaveUp) + { + return node; + } + _currentChain.Elements.Add(new DefaultExpressionFingerprint(node.NodeType, node.Type)); + return base.VisitDefault(node); + } + + protected override Expression VisitDynamic(DynamicExpression node) + { + return GiveUp(node); + } + + protected override ElementInit VisitElementInit(ElementInit node) + { + return GiveUp(node); + } + + protected override Expression VisitExtension(Expression node) + { + return GiveUp(node); + } + + protected override Expression VisitGoto(GotoExpression node) + { + return GiveUp(node); + } + + protected override Expression VisitIndex(IndexExpression node) + { + if (_gaveUp) + { + return node; + } + _currentChain.Elements.Add(new IndexExpressionFingerprint(node.NodeType, node.Type, node.Indexer)); + return base.VisitIndex(node); + } + + protected override Expression VisitInvocation(InvocationExpression node) + { + return GiveUp(node); + } + + protected override Expression VisitLabel(LabelExpression node) + { + return GiveUp(node); + } + + protected override LabelTarget VisitLabelTarget(LabelTarget node) + { + return GiveUp(node); + } + + protected override Expression VisitLambda<T>(Expression<T> node) + { + if (_gaveUp) + { + return node; + } + _currentChain.Elements.Add(new LambdaExpressionFingerprint(node.NodeType, node.Type)); + return base.VisitLambda<T>(node); + } + + protected override Expression VisitListInit(ListInitExpression node) + { + return GiveUp(node); + } + + protected override Expression VisitLoop(LoopExpression node) + { + return GiveUp(node); + } + + protected override Expression VisitMember(MemberExpression node) + { + if (_gaveUp) + { + return node; + } + _currentChain.Elements.Add(new MemberExpressionFingerprint(node.NodeType, node.Type, node.Member)); + return base.VisitMember(node); + } + + protected override MemberAssignment VisitMemberAssignment(MemberAssignment node) + { + return GiveUp(node); + } + + protected override MemberBinding VisitMemberBinding(MemberBinding node) + { + return GiveUp(node); + } + + protected override Expression VisitMemberInit(MemberInitExpression node) + { + return GiveUp(node); + } + + protected override MemberListBinding VisitMemberListBinding(MemberListBinding node) + { + return GiveUp(node); + } + + protected override MemberMemberBinding VisitMemberMemberBinding(MemberMemberBinding node) + { + return GiveUp(node); + } + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (_gaveUp) + { + return node; + } + _currentChain.Elements.Add(new MethodCallExpressionFingerprint(node.NodeType, node.Type, node.Method)); + return base.VisitMethodCall(node); + } + + protected override Expression VisitNew(NewExpression node) + { + return GiveUp(node); + } + + protected override Expression VisitNewArray(NewArrayExpression node) + { + return GiveUp(node); + } + + protected override Expression VisitParameter(ParameterExpression node) + { + if (_gaveUp) + { + return node; + } + + int parameterIndex = _seenParameters.IndexOf(node); + if (parameterIndex < 0) + { + // first time seeing this parameter + parameterIndex = _seenParameters.Count; + _seenParameters.Add(node); + } + + _currentChain.Elements.Add(new ParameterExpressionFingerprint(node.NodeType, node.Type, parameterIndex)); + return base.VisitParameter(node); + } + + protected override Expression VisitRuntimeVariables(RuntimeVariablesExpression node) + { + return GiveUp(node); + } + + protected override Expression VisitSwitch(SwitchExpression node) + { + return GiveUp(node); + } + + protected override SwitchCase VisitSwitchCase(SwitchCase node) + { + return GiveUp(node); + } + + protected override Expression VisitTry(TryExpression node) + { + return GiveUp(node); + } + + protected override Expression VisitTypeBinary(TypeBinaryExpression node) + { + if (_gaveUp) + { + return node; + } + _currentChain.Elements.Add(new TypeBinaryExpressionFingerprint(node.NodeType, node.Type, node.TypeOperand)); + return base.VisitTypeBinary(node); + } + + protected override Expression VisitUnary(UnaryExpression node) + { + if (_gaveUp) + { + return node; + } + _currentChain.Elements.Add(new UnaryExpressionFingerprint(node.NodeType, node.Type, node.Method)); + return base.VisitUnary(node); + } + } +} diff --git a/src/System.Web.Mvc/ExpressionUtil/HashCodeCombiner.cs b/src/System.Web.Mvc/ExpressionUtil/HashCodeCombiner.cs new file mode 100644 index 00000000..37349bfc --- /dev/null +++ b/src/System.Web.Mvc/ExpressionUtil/HashCodeCombiner.cs @@ -0,0 +1,56 @@ +using System.Collections; + +namespace System.Web.Mvc.ExpressionUtil +{ + // based on System.Web.Util.HashCodeCombiner + internal class HashCodeCombiner + { + private long _combinedHash64 = 0x1505L; + + public int CombinedHash + { + get { return _combinedHash64.GetHashCode(); } + } + + public void AddFingerprint(ExpressionFingerprint fingerprint) + { + if (fingerprint != null) + { + fingerprint.AddToHashCodeCombiner(this); + } + else + { + AddInt32(0); + } + } + + public void AddEnumerable(IEnumerable e) + { + if (e == null) + { + AddInt32(0); + } + else + { + int count = 0; + foreach (object o in e) + { + AddObject(o); + count++; + } + AddInt32(count); + } + } + + public void AddInt32(int i) + { + _combinedHash64 = ((_combinedHash64 << 5) + _combinedHash64) ^ i; + } + + public void AddObject(object o) + { + int hashCode = (o != null) ? o.GetHashCode() : 0; + AddInt32(hashCode); + } + } +} diff --git a/src/System.Web.Mvc/ExpressionUtil/Hoisted`2.cs b/src/System.Web.Mvc/ExpressionUtil/Hoisted`2.cs new file mode 100644 index 00000000..1785e627 --- /dev/null +++ b/src/System.Web.Mvc/ExpressionUtil/Hoisted`2.cs @@ -0,0 +1,6 @@ +using System.Collections.Generic; + +namespace System.Web.Mvc.ExpressionUtil +{ + internal delegate TValue Hoisted<TModel, TValue>(TModel model, List<object> capturedConstants); +} diff --git a/src/System.Web.Mvc/ExpressionUtil/HoistingExpressionVisitor.cs b/src/System.Web.Mvc/ExpressionUtil/HoistingExpressionVisitor.cs new file mode 100644 index 00000000..40772cbc --- /dev/null +++ b/src/System.Web.Mvc/ExpressionUtil/HoistingExpressionVisitor.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace System.Web.Mvc.ExpressionUtil +{ + // This is a visitor which rewrites constant expressions as parameter lookups. It's meant + // to produce an expression which can be cached safely. + + internal sealed class HoistingExpressionVisitor<TIn, TOut> : ExpressionVisitor + { + private static readonly ParameterExpression _hoistedConstantsParamExpr = Expression.Parameter(typeof(List<object>), "hoistedConstants"); + private int _numConstantsProcessed; + + // factory will create instance + private HoistingExpressionVisitor() + { + } + + public static Expression<Hoisted<TIn, TOut>> Hoist(Expression<Func<TIn, TOut>> expr) + { + // rewrite Expression<Func<TIn, TOut>> as Expression<Hoisted<TIn, TOut>> + + var visitor = new HoistingExpressionVisitor<TIn, TOut>(); + var rewrittenBodyExpr = visitor.Visit(expr.Body); + var rewrittenLambdaExpr = Expression.Lambda<Hoisted<TIn, TOut>>(rewrittenBodyExpr, expr.Parameters[0], _hoistedConstantsParamExpr); + return rewrittenLambdaExpr; + } + + protected override Expression VisitConstant(ConstantExpression node) + { + // rewrite the constant expression as (TConst)hoistedConstants[i]; + return Expression.Convert(Expression.Property(_hoistedConstantsParamExpr, "Item", Expression.Constant(_numConstantsProcessed++)), node.Type); + } + } +} diff --git a/src/System.Web.Mvc/ExpressionUtil/IndexExpressionFingerprint.cs b/src/System.Web.Mvc/ExpressionUtil/IndexExpressionFingerprint.cs new file mode 100644 index 00000000..038e2f53 --- /dev/null +++ b/src/System.Web.Mvc/ExpressionUtil/IndexExpressionFingerprint.cs @@ -0,0 +1,41 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; +using System.Reflection; + +#pragma warning disable 659 // overrides AddToHashCodeCombiner instead + +namespace System.Web.Mvc.ExpressionUtil +{ + // IndexExpression fingerprint class + // Represents certain forms of array access or indexer property access + + [SuppressMessage("Microsoft.Usage", "CA2218:OverrideGetHashCodeOnOverridingEquals", Justification = "Overrides AddToHashCodeCombiner() instead.")] + internal sealed class IndexExpressionFingerprint : ExpressionFingerprint + { + public IndexExpressionFingerprint(ExpressionType nodeType, Type type, PropertyInfo indexer) + : base(nodeType, type) + { + // Other properties on IndexExpression (like the argument count) are simply derived + // from Type and Indexer, so they're not necessary for inclusion in the fingerprint. + + Indexer = indexer; + } + + // http://msdn.microsoft.com/en-us/library/system.linq.expressions.indexexpression.indexer.aspx + public PropertyInfo Indexer { get; private set; } + + public override bool Equals(object obj) + { + IndexExpressionFingerprint other = obj as IndexExpressionFingerprint; + return (other != null) + && Equals(this.Indexer, other.Indexer) + && this.Equals(other); + } + + internal override void AddToHashCodeCombiner(HashCodeCombiner combiner) + { + combiner.AddObject(Indexer); + base.AddToHashCodeCombiner(combiner); + } + } +} diff --git a/src/System.Web.Mvc/ExpressionUtil/LambdaExpressionFingerprint.cs b/src/System.Web.Mvc/ExpressionUtil/LambdaExpressionFingerprint.cs new file mode 100644 index 00000000..1bcb58bf --- /dev/null +++ b/src/System.Web.Mvc/ExpressionUtil/LambdaExpressionFingerprint.cs @@ -0,0 +1,28 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; + +#pragma warning disable 659 // overrides AddToHashCodeCombiner instead + +namespace System.Web.Mvc.ExpressionUtil +{ + // LambdaExpression fingerprint class + // Represents a lambda expression (root element in Expression<T>) + + [SuppressMessage("Microsoft.Usage", "CA2218:OverrideGetHashCodeOnOverridingEquals", Justification = "Overrides AddToHashCodeCombiner() instead.")] + internal sealed class LambdaExpressionFingerprint : ExpressionFingerprint + { + public LambdaExpressionFingerprint(ExpressionType nodeType, Type type) + : base(nodeType, type) + { + // There are no properties on LambdaExpression that are worth including in + // the fingerprint. + } + + public override bool Equals(object obj) + { + LambdaExpressionFingerprint other = obj as LambdaExpressionFingerprint; + return (other != null) + && this.Equals(other); + } + } +} diff --git a/src/System.Web.Mvc/ExpressionUtil/MemberExpressionFingerprint.cs b/src/System.Web.Mvc/ExpressionUtil/MemberExpressionFingerprint.cs new file mode 100644 index 00000000..309dedca --- /dev/null +++ b/src/System.Web.Mvc/ExpressionUtil/MemberExpressionFingerprint.cs @@ -0,0 +1,38 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; +using System.Reflection; + +#pragma warning disable 659 // overrides AddToHashCodeCombiner instead + +namespace System.Web.Mvc.ExpressionUtil +{ + // MemberExpression fingerprint class + // Expression of form xxx.FieldOrProperty + + [SuppressMessage("Microsoft.Usage", "CA2218:OverrideGetHashCodeOnOverridingEquals", Justification = "Overrides AddToHashCodeCombiner() instead.")] + internal sealed class MemberExpressionFingerprint : ExpressionFingerprint + { + public MemberExpressionFingerprint(ExpressionType nodeType, Type type, MemberInfo member) + : base(nodeType, type) + { + Member = member; + } + + // http://msdn.microsoft.com/en-us/library/system.linq.expressions.memberexpression.member.aspx + public MemberInfo Member { get; private set; } + + public override bool Equals(object obj) + { + MemberExpressionFingerprint other = obj as MemberExpressionFingerprint; + return (other != null) + && Equals(this.Member, other.Member) + && this.Equals(other); + } + + internal override void AddToHashCodeCombiner(HashCodeCombiner combiner) + { + combiner.AddObject(Member); + base.AddToHashCodeCombiner(combiner); + } + } +} diff --git a/src/System.Web.Mvc/ExpressionUtil/MethodCallExpressionFingerprint.cs b/src/System.Web.Mvc/ExpressionUtil/MethodCallExpressionFingerprint.cs new file mode 100644 index 00000000..082541a8 --- /dev/null +++ b/src/System.Web.Mvc/ExpressionUtil/MethodCallExpressionFingerprint.cs @@ -0,0 +1,41 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; +using System.Reflection; + +#pragma warning disable 659 // overrides AddToHashCodeCombiner instead + +namespace System.Web.Mvc.ExpressionUtil +{ + // MethodCallExpression fingerprint class + // Expression of form xxx.Foo(...), xxx[...] (get_Item()), etc. + + [SuppressMessage("Microsoft.Usage", "CA2218:OverrideGetHashCodeOnOverridingEquals", Justification = "Overrides AddToHashCodeCombiner() instead.")] + internal sealed class MethodCallExpressionFingerprint : ExpressionFingerprint + { + public MethodCallExpressionFingerprint(ExpressionType nodeType, Type type, MethodInfo method) + : base(nodeType, type) + { + // Other properties on MethodCallExpression (like the argument count) are simply derived + // from Type and Indexer, so they're not necessary for inclusion in the fingerprint. + + Method = method; + } + + // http://msdn.microsoft.com/en-us/library/system.linq.expressions.methodcallexpression.method.aspx + public MethodInfo Method { get; private set; } + + public override bool Equals(object obj) + { + MethodCallExpressionFingerprint other = obj as MethodCallExpressionFingerprint; + return (other != null) + && Equals(this.Method, other.Method) + && this.Equals(other); + } + + internal override void AddToHashCodeCombiner(HashCodeCombiner combiner) + { + combiner.AddObject(Method); + base.AddToHashCodeCombiner(combiner); + } + } +} diff --git a/src/System.Web.Mvc/ExpressionUtil/ParameterExpressionFingerprint.cs b/src/System.Web.Mvc/ExpressionUtil/ParameterExpressionFingerprint.cs new file mode 100644 index 00000000..5799d064 --- /dev/null +++ b/src/System.Web.Mvc/ExpressionUtil/ParameterExpressionFingerprint.cs @@ -0,0 +1,37 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; + +#pragma warning disable 659 // overrides AddToHashCodeCombiner instead + +namespace System.Web.Mvc.ExpressionUtil +{ + // ParameterExpression fingerprint class + // Can represent the model parameter or an inner parameter in an open lambda expression + + [SuppressMessage("Microsoft.Usage", "CA2218:OverrideGetHashCodeOnOverridingEquals", Justification = "Overrides AddToHashCodeCombiner() instead.")] + internal sealed class ParameterExpressionFingerprint : ExpressionFingerprint + { + public ParameterExpressionFingerprint(ExpressionType nodeType, Type type, int parameterIndex) + : base(nodeType, type) + { + ParameterIndex = parameterIndex; + } + + // Parameter position within the overall expression, used to maintain alpha equivalence. + public int ParameterIndex { get; private set; } + + public override bool Equals(object obj) + { + ParameterExpressionFingerprint other = obj as ParameterExpressionFingerprint; + return (other != null) + && (this.ParameterIndex == other.ParameterIndex) + && this.Equals(other); + } + + internal override void AddToHashCodeCombiner(HashCodeCombiner combiner) + { + combiner.AddInt32(ParameterIndex); + base.AddToHashCodeCombiner(combiner); + } + } +} diff --git a/src/System.Web.Mvc/ExpressionUtil/TypeBinaryExpressionFingerprint.cs b/src/System.Web.Mvc/ExpressionUtil/TypeBinaryExpressionFingerprint.cs new file mode 100644 index 00000000..6e60efd4 --- /dev/null +++ b/src/System.Web.Mvc/ExpressionUtil/TypeBinaryExpressionFingerprint.cs @@ -0,0 +1,37 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; + +#pragma warning disable 659 // overrides AddToHashCodeCombiner instead + +namespace System.Web.Mvc.ExpressionUtil +{ + // TypeBinary fingerprint class + // Expression of form "obj is T" + + [SuppressMessage("Microsoft.Usage", "CA2218:OverrideGetHashCodeOnOverridingEquals", Justification = "Overrides AddToHashCodeCombiner() instead.")] + internal sealed class TypeBinaryExpressionFingerprint : ExpressionFingerprint + { + public TypeBinaryExpressionFingerprint(ExpressionType nodeType, Type type, Type typeOperand) + : base(nodeType, type) + { + TypeOperand = typeOperand; + } + + // http://msdn.microsoft.com/en-us/library/system.linq.expressions.typebinaryexpression.typeoperand.aspx + public Type TypeOperand { get; private set; } + + public override bool Equals(object obj) + { + TypeBinaryExpressionFingerprint other = obj as TypeBinaryExpressionFingerprint; + return (other != null) + && Equals(this.TypeOperand, other.TypeOperand) + && this.Equals(other); + } + + internal override void AddToHashCodeCombiner(HashCodeCombiner combiner) + { + combiner.AddObject(TypeOperand); + base.AddToHashCodeCombiner(combiner); + } + } +} diff --git a/src/System.Web.Mvc/ExpressionUtil/UnaryExpressionFingerprint.cs b/src/System.Web.Mvc/ExpressionUtil/UnaryExpressionFingerprint.cs new file mode 100644 index 00000000..2df90a8a --- /dev/null +++ b/src/System.Web.Mvc/ExpressionUtil/UnaryExpressionFingerprint.cs @@ -0,0 +1,41 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; +using System.Reflection; + +#pragma warning disable 659 // overrides AddToHashCodeCombiner instead + +namespace System.Web.Mvc.ExpressionUtil +{ + // UnaryExpression fingerprint class + // The most common appearance of a UnaryExpression is a cast or other conversion operator + + [SuppressMessage("Microsoft.Usage", "CA2218:OverrideGetHashCodeOnOverridingEquals", Justification = "Overrides AddToHashCodeCombiner() instead.")] + internal sealed class UnaryExpressionFingerprint : ExpressionFingerprint + { + public UnaryExpressionFingerprint(ExpressionType nodeType, Type type, MethodInfo method) + : base(nodeType, type) + { + // Other properties on UnaryExpression (like IsLifted / IsLiftedToNull) are simply derived + // from Type and NodeType, so they're not necessary for inclusion in the fingerprint. + + Method = method; + } + + // http://msdn.microsoft.com/en-us/library/system.linq.expressions.unaryexpression.method.aspx + public MethodInfo Method { get; private set; } + + public override bool Equals(object obj) + { + UnaryExpressionFingerprint other = obj as UnaryExpressionFingerprint; + return (other != null) + && Equals(this.Method, other.Method) + && this.Equals(other); + } + + internal override void AddToHashCodeCombiner(HashCodeCombiner combiner) + { + combiner.AddObject(Method); + base.AddToHashCodeCombiner(combiner); + } + } +} diff --git a/src/System.Web.Mvc/FieldValidationMetadata.cs b/src/System.Web.Mvc/FieldValidationMetadata.cs new file mode 100644 index 00000000..e025e6ac --- /dev/null +++ b/src/System.Web.Mvc/FieldValidationMetadata.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace System.Web.Mvc +{ + public class FieldValidationMetadata + { + private readonly Collection<ModelClientValidationRule> _validationRules = new Collection<ModelClientValidationRule>(); + private string _fieldName; + + public string FieldName + { + get { return _fieldName ?? String.Empty; } + set { _fieldName = value; } + } + + public bool ReplaceValidationMessageContents { get; set; } + + public string ValidationMessageId { get; set; } + + public ICollection<ModelClientValidationRule> ValidationRules + { + get { return _validationRules; } + } + } +} diff --git a/src/System.Web.Mvc/FileContentResult.cs b/src/System.Web.Mvc/FileContentResult.cs new file mode 100644 index 00000000..ab28d0ba --- /dev/null +++ b/src/System.Web.Mvc/FileContentResult.cs @@ -0,0 +1,26 @@ +using System.Diagnostics.CodeAnalysis; + +namespace System.Web.Mvc +{ + public class FileContentResult : FileResult + { + public FileContentResult(byte[] fileContents, string contentType) + : base(contentType) + { + if (fileContents == null) + { + throw new ArgumentNullException("fileContents"); + } + + FileContents = fileContents; + } + + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "There's no reason to tamper-proof this array since it's supplied to the type's constructor.")] + public byte[] FileContents { get; private set; } + + protected override void WriteFile(HttpResponseBase response) + { + response.OutputStream.Write(FileContents, 0, FileContents.Length); + } + } +} diff --git a/src/System.Web.Mvc/FilePathResult.cs b/src/System.Web.Mvc/FilePathResult.cs new file mode 100644 index 00000000..fd2507a6 --- /dev/null +++ b/src/System.Web.Mvc/FilePathResult.cs @@ -0,0 +1,25 @@ +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + public class FilePathResult : FileResult + { + public FilePathResult(string fileName, string contentType) + : base(contentType) + { + if (String.IsNullOrEmpty(fileName)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "fileName"); + } + + FileName = fileName; + } + + public string FileName { get; private set; } + + protected override void WriteFile(HttpResponseBase response) + { + response.TransmitFile(FileName); + } + } +} diff --git a/src/System.Web.Mvc/FileResult.cs b/src/System.Web.Mvc/FileResult.cs new file mode 100644 index 00000000..dcaab762 --- /dev/null +++ b/src/System.Web.Mvc/FileResult.cs @@ -0,0 +1,143 @@ +using System.Net.Mime; +using System.Text; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + public abstract class FileResult : ActionResult + { + private string _fileDownloadName; + + protected FileResult(string contentType) + { + if (String.IsNullOrEmpty(contentType)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "contentType"); + } + + ContentType = contentType; + } + + public string ContentType { get; private set; } + + public string FileDownloadName + { + get { return _fileDownloadName ?? String.Empty; } + set { _fileDownloadName = value; } + } + + public override void ExecuteResult(ControllerContext context) + { + if (context == null) + { + throw new ArgumentNullException("context"); + } + + HttpResponseBase response = context.HttpContext.Response; + response.ContentType = ContentType; + + if (!String.IsNullOrEmpty(FileDownloadName)) + { + // From RFC 2183, Sec. 2.3: + // The sender may want to suggest a filename to be used if the entity is + // detached and stored in a separate file. If the receiving MUA writes + // the entity to a file, the suggested filename should be used as a + // basis for the actual filename, where possible. + string headerValue = ContentDispositionUtil.GetHeaderValue(FileDownloadName); + context.HttpContext.Response.AddHeader("Content-Disposition", headerValue); + } + + WriteFile(response); + } + + protected abstract void WriteFile(HttpResponseBase response); + + private static class ContentDispositionUtil + { + private const string HexDigits = "0123456789ABCDEF"; + + private static void AddByteToStringBuilder(byte b, StringBuilder builder) + { + builder.Append('%'); + + int i = b; + AddHexDigitToStringBuilder(i >> 4, builder); + AddHexDigitToStringBuilder(i % 16, builder); + } + + private static void AddHexDigitToStringBuilder(int digit, StringBuilder builder) + { + builder.Append(HexDigits[digit]); + } + + private static string CreateRfc2231HeaderValue(string filename) + { + StringBuilder builder = new StringBuilder("attachment; filename*=UTF-8''"); + + byte[] filenameBytes = Encoding.UTF8.GetBytes(filename); + foreach (byte b in filenameBytes) + { + if (IsByteValidHeaderValueCharacter(b)) + { + builder.Append((char)b); + } + else + { + AddByteToStringBuilder(b, builder); + } + } + + return builder.ToString(); + } + + public static string GetHeaderValue(string fileName) + { + try + { + // first, try using the .NET built-in generator + ContentDisposition disposition = new ContentDisposition() { FileName = fileName }; + return disposition.ToString(); + } + catch (FormatException) + { + // otherwise, fall back to RFC 2231 extensions generator + return CreateRfc2231HeaderValue(fileName); + } + } + + // Application of RFC 2231 Encoding to Hypertext Transfer Protocol (HTTP) Header Fields, sec. 3.2 + // http://greenbytes.de/tech/webdav/draft-reschke-rfc2231-in-http-latest.html + private static bool IsByteValidHeaderValueCharacter(byte b) + { + if ((byte)'0' <= b && b <= (byte)'9') + { + return true; // is digit + } + if ((byte)'a' <= b && b <= (byte)'z') + { + return true; // lowercase letter + } + if ((byte)'A' <= b && b <= (byte)'Z') + { + return true; // uppercase letter + } + + switch (b) + { + case (byte)'-': + case (byte)'.': + case (byte)'_': + case (byte)'~': + case (byte)':': + case (byte)'!': + case (byte)'$': + case (byte)'&': + case (byte)'+': + return true; + } + + return false; + } + } + } +} diff --git a/src/System.Web.Mvc/FileStreamResult.cs b/src/System.Web.Mvc/FileStreamResult.cs new file mode 100644 index 00000000..2fc5a132 --- /dev/null +++ b/src/System.Web.Mvc/FileStreamResult.cs @@ -0,0 +1,45 @@ +using System.IO; + +namespace System.Web.Mvc +{ + public class FileStreamResult : FileResult + { + // default buffer size as defined in BufferedStream type + private const int BufferSize = 0x1000; + + public FileStreamResult(Stream fileStream, string contentType) + : base(contentType) + { + if (fileStream == null) + { + throw new ArgumentNullException("fileStream"); + } + + FileStream = fileStream; + } + + public Stream FileStream { get; private set; } + + protected override void WriteFile(HttpResponseBase response) + { + // grab chunks of data and write to the output stream + Stream outputStream = response.OutputStream; + using (FileStream) + { + byte[] buffer = new byte[BufferSize]; + + while (true) + { + int bytesRead = FileStream.Read(buffer, 0, BufferSize); + if (bytesRead == 0) + { + // no more data + break; + } + + outputStream.Write(buffer, 0, bytesRead); + } + } + } + } +} diff --git a/src/System.Web.Mvc/Filter.cs b/src/System.Web.Mvc/Filter.cs new file mode 100644 index 00000000..e027dbef --- /dev/null +++ b/src/System.Web.Mvc/Filter.cs @@ -0,0 +1,34 @@ +namespace System.Web.Mvc +{ + public class Filter + { + public const int DefaultOrder = -1; + + public Filter(object instance, FilterScope scope, int? order) + { + if (instance == null) + { + throw new ArgumentNullException("instance"); + } + + if (order == null) + { + IMvcFilter mvcFilter = instance as IMvcFilter; + if (mvcFilter != null) + { + order = mvcFilter.Order; + } + } + + Instance = instance; + Order = order ?? DefaultOrder; + Scope = scope; + } + + public object Instance { get; protected set; } + + public int Order { get; protected set; } + + public FilterScope Scope { get; protected set; } + } +} diff --git a/src/System.Web.Mvc/FilterAttribute.cs b/src/System.Web.Mvc/FilterAttribute.cs new file mode 100644 index 00000000..88fcdaf4 --- /dev/null +++ b/src/System.Web.Mvc/FilterAttribute.cs @@ -0,0 +1,41 @@ +using System.Collections.Concurrent; +using System.Linq; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)] + public abstract class FilterAttribute : Attribute, IMvcFilter + { + private static readonly ConcurrentDictionary<Type, bool> _multiuseAttributeCache = new ConcurrentDictionary<Type, bool>(); + private int _order = Filter.DefaultOrder; + + public bool AllowMultiple + { + get { return AllowsMultiple(GetType()); } + } + + public int Order + { + get { return _order; } + set + { + if (value < Filter.DefaultOrder) + { + throw new ArgumentOutOfRangeException("value", MvcResources.FilterAttribute_OrderOutOfRange); + } + _order = value; + } + } + + private static bool AllowsMultiple(Type attributeType) + { + return _multiuseAttributeCache.GetOrAdd( + attributeType, + type => type.GetCustomAttributes(typeof(AttributeUsageAttribute), true) + .Cast<AttributeUsageAttribute>() + .First() + .AllowMultiple); + } + } +} diff --git a/src/System.Web.Mvc/FilterAttributeFilterProvider.cs b/src/System.Web.Mvc/FilterAttributeFilterProvider.cs new file mode 100644 index 00000000..74022795 --- /dev/null +++ b/src/System.Web.Mvc/FilterAttributeFilterProvider.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using System.Linq; + +namespace System.Web.Mvc +{ + public class FilterAttributeFilterProvider : IFilterProvider + { + private readonly bool _cacheAttributeInstances; + + public FilterAttributeFilterProvider() + : this(true) + { + } + + public FilterAttributeFilterProvider(bool cacheAttributeInstances) + { + _cacheAttributeInstances = cacheAttributeInstances; + } + + protected virtual IEnumerable<FilterAttribute> GetActionAttributes(ControllerContext controllerContext, ActionDescriptor actionDescriptor) + { + return actionDescriptor.GetFilterAttributes(_cacheAttributeInstances); + } + + protected virtual IEnumerable<FilterAttribute> GetControllerAttributes(ControllerContext controllerContext, ActionDescriptor actionDescriptor) + { + return actionDescriptor.ControllerDescriptor.GetFilterAttributes(_cacheAttributeInstances); + } + + public virtual IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor) + { + ControllerBase controller = controllerContext.Controller; + if (controller == null) + { + return Enumerable.Empty<Filter>(); + } + + var typeFilters = GetControllerAttributes(controllerContext, actionDescriptor) + .Select(attr => new Filter(attr, FilterScope.Controller, null)); + var methodFilters = GetActionAttributes(controllerContext, actionDescriptor) + .Select(attr => new Filter(attr, FilterScope.Action, null)); + + return typeFilters.Concat(methodFilters).ToList(); + } + } +} diff --git a/src/System.Web.Mvc/FilterInfo.cs b/src/System.Web.Mvc/FilterInfo.cs new file mode 100644 index 00000000..1d14ce79 --- /dev/null +++ b/src/System.Web.Mvc/FilterInfo.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.Linq; + +namespace System.Web.Mvc +{ + public class FilterInfo + { + private List<IActionFilter> _actionFilters = new List<IActionFilter>(); + private List<IAuthorizationFilter> _authorizationFilters = new List<IAuthorizationFilter>(); + private List<IExceptionFilter> _exceptionFilters = new List<IExceptionFilter>(); + private List<IResultFilter> _resultFilters = new List<IResultFilter>(); + + public FilterInfo() + { + } + + public FilterInfo(IEnumerable<Filter> filters) + { + // evaluate the 'filters' enumerable only once since the operation can be quite expensive + var filterInstances = filters.Select(f => f.Instance).ToList(); + + _actionFilters.AddRange(filterInstances.OfType<IActionFilter>()); + _authorizationFilters.AddRange(filterInstances.OfType<IAuthorizationFilter>()); + _exceptionFilters.AddRange(filterInstances.OfType<IExceptionFilter>()); + _resultFilters.AddRange(filterInstances.OfType<IResultFilter>()); + } + + public IList<IActionFilter> ActionFilters + { + get { return _actionFilters; } + } + + public IList<IAuthorizationFilter> AuthorizationFilters + { + get { return _authorizationFilters; } + } + + public IList<IExceptionFilter> ExceptionFilters + { + get { return _exceptionFilters; } + } + + public IList<IResultFilter> ResultFilters + { + get { return _resultFilters; } + } + } +} diff --git a/src/System.Web.Mvc/FilterProviderCollection.cs b/src/System.Web.Mvc/FilterProviderCollection.cs new file mode 100644 index 00000000..854733d7 --- /dev/null +++ b/src/System.Web.Mvc/FilterProviderCollection.cs @@ -0,0 +1,125 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +namespace System.Web.Mvc +{ + public class FilterProviderCollection : Collection<IFilterProvider> + { + private static FilterComparer _filterComparer = new FilterComparer(); + private IResolver<IEnumerable<IFilterProvider>> _serviceResolver; + + public FilterProviderCollection() + { + _serviceResolver = new MultiServiceResolver<IFilterProvider>(() => Items); + } + + public FilterProviderCollection(IList<IFilterProvider> providers) + : base(providers) + { + _serviceResolver = new MultiServiceResolver<IFilterProvider>(() => Items); + } + + internal FilterProviderCollection(IResolver<IEnumerable<IFilterProvider>> serviceResolver, params IFilterProvider[] providers) + : base(providers) + { + _serviceResolver = serviceResolver ?? new MultiServiceResolver<IFilterProvider>(() => Items); + } + + private IEnumerable<IFilterProvider> CombinedItems + { + get { return _serviceResolver.Current; } + } + + private static bool AllowMultiple(object filterInstance) + { + IMvcFilter mvcFilter = filterInstance as IMvcFilter; + if (mvcFilter == null) + { + return true; + } + + return mvcFilter.AllowMultiple; + } + + public IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor) + { + if (controllerContext == null) + { + throw new ArgumentNullException("controllerContext"); + } + if (actionDescriptor == null) + { + throw new ArgumentNullException("actionDescriptor"); + } + + IEnumerable<Filter> combinedFilters = + CombinedItems.SelectMany(fp => fp.GetFilters(controllerContext, actionDescriptor)) + .OrderBy(filter => filter, _filterComparer); + + // Remove duplicates from the back forward + return RemoveDuplicates(combinedFilters.Reverse()).Reverse(); + } + + private IEnumerable<Filter> RemoveDuplicates(IEnumerable<Filter> filters) + { + HashSet<Type> visitedTypes = new HashSet<Type>(); + + foreach (Filter filter in filters) + { + object filterInstance = filter.Instance; + Type filterInstanceType = filterInstance.GetType(); + + if (!visitedTypes.Contains(filterInstanceType) || AllowMultiple(filterInstance)) + { + yield return filter; + visitedTypes.Add(filterInstanceType); + } + } + } + + private class FilterComparer : IComparer<Filter> + { + public int Compare(Filter x, Filter y) + { + // Nulls always have to be less than non-nulls + if (x == null && y == null) + { + return 0; + } + if (x == null) + { + return -1; + } + if (y == null) + { + return 1; + } + + // Sort first by order... + + if (x.Order < y.Order) + { + return -1; + } + if (x.Order > y.Order) + { + return 1; + } + + // ...then by scope + + if (x.Scope < y.Scope) + { + return -1; + } + if (x.Scope > y.Scope) + { + return 1; + } + + return 0; + } + } + } +} diff --git a/src/System.Web.Mvc/FilterProviders.cs b/src/System.Web.Mvc/FilterProviders.cs new file mode 100644 index 00000000..7ff3db9a --- /dev/null +++ b/src/System.Web.Mvc/FilterProviders.cs @@ -0,0 +1,15 @@ +namespace System.Web.Mvc +{ + public static class FilterProviders + { + static FilterProviders() + { + Providers = new FilterProviderCollection(); + Providers.Add(GlobalFilters.Filters); + Providers.Add(new FilterAttributeFilterProvider()); + Providers.Add(new ControllerInstanceFilterProvider()); + } + + public static FilterProviderCollection Providers { get; private set; } + } +} diff --git a/src/System.Web.Mvc/FilterScope.cs b/src/System.Web.Mvc/FilterScope.cs new file mode 100644 index 00000000..ca315e00 --- /dev/null +++ b/src/System.Web.Mvc/FilterScope.cs @@ -0,0 +1,11 @@ +namespace System.Web.Mvc +{ + public enum FilterScope + { + First = 0, + Global = 10, + Controller = 20, + Action = 30, + Last = 100, + } +} diff --git a/src/System.Web.Mvc/FormCollection.cs b/src/System.Web.Mvc/FormCollection.cs new file mode 100644 index 00000000..4adc8b42 --- /dev/null +++ b/src/System.Web.Mvc/FormCollection.cs @@ -0,0 +1,96 @@ +using System.Collections.Specialized; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Web.Helpers; + +namespace System.Web.Mvc +{ + [SuppressMessage("Microsoft.Usage", "CA2237:MarkISerializableTypesWithSerializable", Justification = "It is not anticipated that users will need to serialize this type.")] + [SuppressMessage("Microsoft.Design", "CA1035:ICollectionImplementationsHaveStronglyTypedMembers", Justification = "It is not anticipated that users will call FormCollection.CopyTo().")] + [FormCollectionBinder] + public sealed class FormCollection : NameValueCollection, IValueProvider + { + public FormCollection() + { + } + + public FormCollection(NameValueCollection collection) + { + if (collection == null) + { + throw new ArgumentNullException("collection"); + } + + Add(collection); + } + + internal FormCollection(ControllerBase controller, Func<NameValueCollection> validatedValuesThunk, Func<NameValueCollection> unvalidatedValuesThunk) + { + Add(controller == null || controller.ValidateRequest ? validatedValuesThunk() : unvalidatedValuesThunk()); + } + + public ValueProviderResult GetValue(string name) + { + if (name == null) + { + throw new ArgumentNullException("name"); + } + + string[] rawValue = GetValues(name); + if (rawValue == null) + { + return null; + } + + string attemptedValue = this[name]; + return new ValueProviderResult(rawValue, attemptedValue, CultureInfo.CurrentCulture); + } + + public IValueProvider ToValueProvider() + { + return this; + } + + #region IValueProvider Members + + bool IValueProvider.ContainsPrefix(string prefix) + { + return ValueProviderUtil.CollectionContainsPrefix(AllKeys, prefix); + } + + ValueProviderResult IValueProvider.GetValue(string key) + { + return GetValue(key); + } + + #endregion + + private sealed class FormCollectionBinderAttribute : CustomModelBinderAttribute + { + // since the FormCollectionModelBinder.BindModel() method is thread-safe, we only need to keep + // a single instance of the binder around + private static readonly FormCollectionModelBinder _binder = new FormCollectionModelBinder(); + + public override IModelBinder GetBinder() + { + return _binder; + } + + // this class is used for generating a FormCollection object + private sealed class FormCollectionModelBinder : IModelBinder + { + public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) + { + if (controllerContext == null) + { + throw new ArgumentNullException("controllerContext"); + } + + return new FormCollection(controllerContext.Controller, + () => controllerContext.HttpContext.Request.Form, + () => controllerContext.HttpContext.Request.Unvalidated().Form); + } + } + } + } +} diff --git a/src/System.Web.Mvc/FormContext.cs b/src/System.Web.Mvc/FormContext.cs new file mode 100644 index 00000000..b117c2b2 --- /dev/null +++ b/src/System.Web.Mvc/FormContext.cs @@ -0,0 +1,81 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Web.Script.Serialization; + +namespace System.Web.Mvc +{ + public class FormContext + { + private readonly Dictionary<string, FieldValidationMetadata> _fieldValidators = new Dictionary<string, FieldValidationMetadata>(); + private readonly Dictionary<string, bool> _renderedFields = new Dictionary<string, bool>(); + + public IDictionary<string, FieldValidationMetadata> FieldValidators + { + get { return _fieldValidators; } + } + + public string FormId { get; set; } + + public bool ReplaceValidationSummary { get; set; } + + public string ValidationSummaryId { get; set; } + + [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Performs a potentially time-consuming conversion.")] + public string GetJsonValidationMetadata() + { + JavaScriptSerializer serializer = new JavaScriptSerializer(); + + SortedDictionary<string, object> dict = new SortedDictionary<string, object>() + { + { "Fields", FieldValidators.Values }, + { "FormId", FormId } + }; + if (!String.IsNullOrEmpty(ValidationSummaryId)) + { + dict["ValidationSummaryId"] = ValidationSummaryId; + } + dict["ReplaceValidationSummary"] = ReplaceValidationSummary; + + return serializer.Serialize(dict); + } + + public FieldValidationMetadata GetValidationMetadataForField(string fieldName) + { + return GetValidationMetadataForField(fieldName, false /* createIfNotFound */); + } + + public FieldValidationMetadata GetValidationMetadataForField(string fieldName, bool createIfNotFound) + { + if (String.IsNullOrEmpty(fieldName)) + { + throw Error.ParameterCannotBeNullOrEmpty("fieldName"); + } + + FieldValidationMetadata metadata; + if (!FieldValidators.TryGetValue(fieldName, out metadata)) + { + if (createIfNotFound) + { + metadata = new FieldValidationMetadata() + { + FieldName = fieldName + }; + FieldValidators[fieldName] = metadata; + } + } + return metadata; + } + + public bool RenderedField(string fieldName) + { + bool result; + _renderedFields.TryGetValue(fieldName, out result); + return result; + } + + public void RenderedField(string fieldName, bool value) + { + _renderedFields[fieldName] = value; + } + } +} diff --git a/src/System.Web.Mvc/FormMethod.cs b/src/System.Web.Mvc/FormMethod.cs new file mode 100644 index 00000000..5e49bb3b --- /dev/null +++ b/src/System.Web.Mvc/FormMethod.cs @@ -0,0 +1,8 @@ +namespace System.Web.Mvc +{ + public enum FormMethod + { + Get, + Post + } +} diff --git a/src/System.Web.Mvc/FormValueProvider.cs b/src/System.Web.Mvc/FormValueProvider.cs new file mode 100644 index 00000000..5b9bf1e3 --- /dev/null +++ b/src/System.Web.Mvc/FormValueProvider.cs @@ -0,0 +1,19 @@ +using System.Globalization; +using System.Web.Helpers; + +namespace System.Web.Mvc +{ + public sealed class FormValueProvider : NameValueCollectionValueProvider + { + public FormValueProvider(ControllerContext controllerContext) + : this(controllerContext, new UnvalidatedRequestValuesWrapper(controllerContext.HttpContext.Request.Unvalidated())) + { + } + + // For unit testing + internal FormValueProvider(ControllerContext controllerContext, IUnvalidatedRequestValues unvalidatedValues) + : base(controllerContext.HttpContext.Request.Form, unvalidatedValues.Form, CultureInfo.CurrentCulture) + { + } + } +} diff --git a/src/System.Web.Mvc/FormValueProviderFactory.cs b/src/System.Web.Mvc/FormValueProviderFactory.cs new file mode 100644 index 00000000..d6b28452 --- /dev/null +++ b/src/System.Web.Mvc/FormValueProviderFactory.cs @@ -0,0 +1,30 @@ +using System.Web.Helpers; + +namespace System.Web.Mvc +{ + public sealed class FormValueProviderFactory : ValueProviderFactory + { + private readonly UnvalidatedRequestValuesAccessor _unvalidatedValuesAccessor; + + public FormValueProviderFactory() + : this(null) + { + } + + // For unit testing + internal FormValueProviderFactory(UnvalidatedRequestValuesAccessor unvalidatedValuesAccessor) + { + _unvalidatedValuesAccessor = unvalidatedValuesAccessor ?? (cc => new UnvalidatedRequestValuesWrapper(cc.HttpContext.Request.Unvalidated())); + } + + public override IValueProvider GetValueProvider(ControllerContext controllerContext) + { + if (controllerContext == null) + { + throw new ArgumentNullException("controllerContext"); + } + + return new FormValueProvider(controllerContext, _unvalidatedValuesAccessor(controllerContext)); + } + } +} diff --git a/src/System.Web.Mvc/GlobalFilterCollection.cs b/src/System.Web.Mvc/GlobalFilterCollection.cs new file mode 100644 index 00000000..8dfee88f --- /dev/null +++ b/src/System.Web.Mvc/GlobalFilterCollection.cs @@ -0,0 +1,61 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace System.Web.Mvc +{ + public sealed class GlobalFilterCollection : IEnumerable<Filter>, IFilterProvider + { + private List<Filter> _filters = new List<Filter>(); + + public int Count + { + get { return _filters.Count; } + } + + public void Add(object filter) + { + AddInternal(filter, order: null); + } + + public void Add(object filter, int order) + { + AddInternal(filter, order); + } + + private void AddInternal(object filter, int? order) + { + _filters.Add(new Filter(filter, FilterScope.Global, order)); + } + + public void Clear() + { + _filters.Clear(); + } + + public bool Contains(object filter) + { + return _filters.Any(f => f.Instance == filter); + } + + public IEnumerator<Filter> GetEnumerator() + { + return _filters.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _filters.GetEnumerator(); + } + + IEnumerable<Filter> IFilterProvider.GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor) + { + return this; + } + + public void Remove(object filter) + { + _filters.RemoveAll(f => f.Instance == filter); + } + } +} diff --git a/src/System.Web.Mvc/GlobalFilters.cs b/src/System.Web.Mvc/GlobalFilters.cs new file mode 100644 index 00000000..926e4fcd --- /dev/null +++ b/src/System.Web.Mvc/GlobalFilters.cs @@ -0,0 +1,12 @@ +namespace System.Web.Mvc +{ + public static class GlobalFilters + { + static GlobalFilters() + { + Filters = new GlobalFilterCollection(); + } + + public static GlobalFilterCollection Filters { get; private set; } + } +} diff --git a/src/System.Web.Mvc/GlobalSuppressions.cs b/src/System.Web.Mvc/GlobalSuppressions.cs new file mode 100644 index 00000000..f7a121ee --- /dev/null +++ b/src/System.Web.Mvc/GlobalSuppressions.cs @@ -0,0 +1,19 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. +// +// To add a suppression to this file, right-click the message in the +// Error List, point to "Suppress Message(s)", and click +// "In Project Suppression File". +// You do not need to add suppressions to this file manually. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Microsoft.Design", "CA2210:AssembliesShouldHaveValidStrongNames", Justification = "Assembly is delay-signed.")] +[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.Web.Mvc.Ajax", Justification = "Helpers reside within a separate namespace to support alternate helper classes.")] +[assembly: SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Scope = "member", Target = "System.Web.Mvc.TempDataDictionary.#System.Collections.Generic.ICollection`1<System.Collections.Generic.KeyValuePair`2<System.String,System.Object>>.Contains(System.Collections.Generic.KeyValuePair`2<System.String,System.Object>)", Justification = "There are no defined scenarios for wanting to derive from this class, but we don't want to prevent it either.")] +[assembly: SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Scope = "member", Target = "System.Web.Mvc.TempDataDictionary.#System.Collections.Generic.ICollection`1<System.Collections.Generic.KeyValuePair`2<System.String,System.Object>>.CopyTo(System.Collections.Generic.KeyValuePair`2<System.String,System.Object>[],System.Int32)", Justification = "There are no defined scenarios for wanting to derive from this class, but we don't want to prevent it either.")] +[assembly: SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Scope = "member", Target = "System.Web.Mvc.TempDataDictionary.#System.Collections.Generic.ICollection`1<System.Collections.Generic.KeyValuePair`2<System.String,System.Object>>.IsReadOnly", Justification = "There are no defined scenarios for wanting to derive from this class, but we don't want to prevent it either.")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId = "Param", Scope = "resource", Target = "System.Web.Mvc.Properties.MvcResources.resources", Justification = "This is the name that matches ASP.NET")] +[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.Web.Mvc.Razor", Justification = "This is a grouping of functionally similar components, thus a namespace is a valid way to group them.")] diff --git a/src/System.Web.Mvc/HandleErrorAttribute.cs b/src/System.Web.Mvc/HandleErrorAttribute.cs new file mode 100644 index 00000000..c4b61341 --- /dev/null +++ b/src/System.Web.Mvc/HandleErrorAttribute.cs @@ -0,0 +1,107 @@ +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + [SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "This attribute is AllowMultiple = true and users might want to override behavior.")] + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)] + public class HandleErrorAttribute : FilterAttribute, IExceptionFilter + { + private const string DefaultView = "Error"; + + private readonly object _typeId = new object(); + + private Type _exceptionType = typeof(Exception); + private string _master; + private string _view; + + public Type ExceptionType + { + get { return _exceptionType; } + set + { + if (value == null) + { + throw new ArgumentNullException("value"); + } + if (!typeof(Exception).IsAssignableFrom(value)) + { + throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, + MvcResources.ExceptionViewAttribute_NonExceptionType, value.FullName)); + } + + _exceptionType = value; + } + } + + public string Master + { + get { return _master ?? String.Empty; } + set { _master = value; } + } + + public override object TypeId + { + get { return _typeId; } + } + + public string View + { + get { return (!String.IsNullOrEmpty(_view)) ? _view : DefaultView; } + set { _view = value; } + } + + public virtual void OnException(ExceptionContext filterContext) + { + if (filterContext == null) + { + throw new ArgumentNullException("filterContext"); + } + if (filterContext.IsChildAction) + { + return; + } + + // If custom errors are disabled, we need to let the normal ASP.NET exception handler + // execute so that the user can see useful debugging information. + if (filterContext.ExceptionHandled || !filterContext.HttpContext.IsCustomErrorEnabled) + { + return; + } + + Exception exception = filterContext.Exception; + + // If this is not an HTTP 500 (for example, if somebody throws an HTTP 404 from an action method), + // ignore it. + if (new HttpException(null, exception).GetHttpCode() != 500) + { + return; + } + + if (!ExceptionType.IsInstanceOfType(exception)) + { + return; + } + + string controllerName = (string)filterContext.RouteData.Values["controller"]; + string actionName = (string)filterContext.RouteData.Values["action"]; + HandleErrorInfo model = new HandleErrorInfo(filterContext.Exception, controllerName, actionName); + filterContext.Result = new ViewResult + { + ViewName = View, + MasterName = Master, + ViewData = new ViewDataDictionary<HandleErrorInfo>(model), + TempData = filterContext.Controller.TempData + }; + filterContext.ExceptionHandled = true; + filterContext.HttpContext.Response.Clear(); + filterContext.HttpContext.Response.StatusCode = 500; + + // Certain versions of IIS will sometimes use their own error page when + // they detect a server error. Setting this property indicates that we + // want it to try to render ASP.NET MVC's error page instead. + filterContext.HttpContext.Response.TrySkipIisCustomErrors = true; + } + } +} diff --git a/src/System.Web.Mvc/HandleErrorInfo.cs b/src/System.Web.Mvc/HandleErrorInfo.cs new file mode 100644 index 00000000..7b708c60 --- /dev/null +++ b/src/System.Web.Mvc/HandleErrorInfo.cs @@ -0,0 +1,33 @@ +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + public class HandleErrorInfo + { + public HandleErrorInfo(Exception exception, string controllerName, string actionName) + { + if (exception == null) + { + throw new ArgumentNullException("exception"); + } + if (String.IsNullOrEmpty(controllerName)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "controllerName"); + } + if (String.IsNullOrEmpty(actionName)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "actionName"); + } + + Exception = exception; + ControllerName = controllerName; + ActionName = actionName; + } + + public string ActionName { get; private set; } + + public string ControllerName { get; private set; } + + public Exception Exception { get; private set; } + } +} diff --git a/src/System.Web.Mvc/HiddenInputAttribute.cs b/src/System.Web.Mvc/HiddenInputAttribute.cs new file mode 100644 index 00000000..1692a6b0 --- /dev/null +++ b/src/System.Web.Mvc/HiddenInputAttribute.cs @@ -0,0 +1,13 @@ +namespace System.Web.Mvc +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public sealed class HiddenInputAttribute : Attribute + { + public HiddenInputAttribute() + { + DisplayValue = true; + } + + public bool DisplayValue { get; set; } + } +} diff --git a/src/System.Web.Mvc/Html/ChildActionExtensions.cs b/src/System.Web.Mvc/Html/ChildActionExtensions.cs new file mode 100644 index 00000000..4c74a763 --- /dev/null +++ b/src/System.Web.Mvc/Html/ChildActionExtensions.cs @@ -0,0 +1,181 @@ +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Web.Mvc.Properties; +using System.Web.Routing; + +namespace System.Web.Mvc.Html +{ + public static class ChildActionExtensions + { + // Action + + public static MvcHtmlString Action(this HtmlHelper htmlHelper, string actionName) + { + return Action(htmlHelper, actionName, null /* controllerName */, null /* routeValues */); + } + + public static MvcHtmlString Action(this HtmlHelper htmlHelper, string actionName, object routeValues) + { + return Action(htmlHelper, actionName, null /* controllerName */, new RouteValueDictionary(routeValues)); + } + + public static MvcHtmlString Action(this HtmlHelper htmlHelper, string actionName, RouteValueDictionary routeValues) + { + return Action(htmlHelper, actionName, null /* controllerName */, routeValues); + } + + public static MvcHtmlString Action(this HtmlHelper htmlHelper, string actionName, string controllerName) + { + return Action(htmlHelper, actionName, controllerName, null /* routeValues */); + } + + public static MvcHtmlString Action(this HtmlHelper htmlHelper, string actionName, string controllerName, object routeValues) + { + return Action(htmlHelper, actionName, controllerName, new RouteValueDictionary(routeValues)); + } + + public static MvcHtmlString Action(this HtmlHelper htmlHelper, string actionName, string controllerName, RouteValueDictionary routeValues) + { + using (StringWriter writer = new StringWriter(CultureInfo.CurrentCulture)) + { + ActionHelper(htmlHelper, actionName, controllerName, routeValues, writer); + return MvcHtmlString.Create(writer.ToString()); + } + } + + // RenderAction + + public static void RenderAction(this HtmlHelper htmlHelper, string actionName) + { + RenderAction(htmlHelper, actionName, null /* controllerName */, null /* routeValues */); + } + + public static void RenderAction(this HtmlHelper htmlHelper, string actionName, object routeValues) + { + RenderAction(htmlHelper, actionName, null /* controllerName */, new RouteValueDictionary(routeValues)); + } + + public static void RenderAction(this HtmlHelper htmlHelper, string actionName, RouteValueDictionary routeValues) + { + RenderAction(htmlHelper, actionName, null /* controllerName */, routeValues); + } + + public static void RenderAction(this HtmlHelper htmlHelper, string actionName, string controllerName) + { + RenderAction(htmlHelper, actionName, controllerName, null /* routeValues */); + } + + public static void RenderAction(this HtmlHelper htmlHelper, string actionName, string controllerName, object routeValues) + { + RenderAction(htmlHelper, actionName, controllerName, new RouteValueDictionary(routeValues)); + } + + public static void RenderAction(this HtmlHelper htmlHelper, string actionName, string controllerName, RouteValueDictionary routeValues) + { + ActionHelper(htmlHelper, actionName, controllerName, routeValues, htmlHelper.ViewContext.Writer); + } + + // Helpers + + internal static void ActionHelper(HtmlHelper htmlHelper, string actionName, string controllerName, RouteValueDictionary routeValues, TextWriter textWriter) + { + if (htmlHelper == null) + { + throw new ArgumentNullException("htmlHelper"); + } + if (String.IsNullOrEmpty(actionName)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "actionName"); + } + + RouteValueDictionary additionalRouteValues = routeValues; + routeValues = MergeDictionaries(routeValues, htmlHelper.ViewContext.RouteData.Values); + + routeValues["action"] = actionName; + if (!String.IsNullOrEmpty(controllerName)) + { + routeValues["controller"] = controllerName; + } + + bool usingAreas; + VirtualPathData vpd = htmlHelper.RouteCollection.GetVirtualPathForArea(htmlHelper.ViewContext.RequestContext, null /* name */, routeValues, out usingAreas); + if (vpd == null) + { + throw new InvalidOperationException(MvcResources.Common_NoRouteMatched); + } + + if (usingAreas) + { + routeValues.Remove("area"); + if (additionalRouteValues != null) + { + additionalRouteValues.Remove("area"); + } + } + + if (additionalRouteValues != null) + { + routeValues[ChildActionValueProvider.ChildActionValuesKey] = new DictionaryValueProvider<object>(additionalRouteValues, CultureInfo.InvariantCulture); + } + + RouteData routeData = CreateRouteData(vpd.Route, routeValues, vpd.DataTokens, htmlHelper.ViewContext); + HttpContextBase httpContext = htmlHelper.ViewContext.HttpContext; + RequestContext requestContext = new RequestContext(httpContext, routeData); + ChildActionMvcHandler handler = new ChildActionMvcHandler(requestContext); + httpContext.Server.Execute(HttpHandlerUtil.WrapForServerExecute(handler), textWriter, true /* preserveForm */); + } + + private static RouteData CreateRouteData(RouteBase route, RouteValueDictionary routeValues, RouteValueDictionary dataTokens, ViewContext parentViewContext) + { + RouteData routeData = new RouteData(); + + foreach (KeyValuePair<string, object> kvp in routeValues) + { + routeData.Values.Add(kvp.Key, kvp.Value); + } + + foreach (KeyValuePair<string, object> kvp in dataTokens) + { + routeData.DataTokens.Add(kvp.Key, kvp.Value); + } + + routeData.Route = route; + routeData.DataTokens[ControllerContext.ParentActionViewContextToken] = parentViewContext; + return routeData; + } + + private static RouteValueDictionary MergeDictionaries(params RouteValueDictionary[] dictionaries) + { + // Merge existing route values with the user provided values + var result = new RouteValueDictionary(); + + foreach (RouteValueDictionary dictionary in dictionaries.Where(d => d != null)) + { + foreach (KeyValuePair<string, object> kvp in dictionary) + { + if (!result.ContainsKey(kvp.Key)) + { + result.Add(kvp.Key, kvp.Value); + } + } + } + + return result; + } + + internal class ChildActionMvcHandler : MvcHandler + { + public ChildActionMvcHandler(RequestContext context) + : base(context) + { + } + + protected internal override void AddVersionHeader(HttpContextBase httpContext) + { + // No version header for child actions + } + } + } +} diff --git a/src/System.Web.Mvc/Html/DefaultDisplayTemplates.cs b/src/System.Web.Mvc/Html/DefaultDisplayTemplates.cs new file mode 100644 index 00000000..cd329237 --- /dev/null +++ b/src/System.Web.Mvc/Html/DefaultDisplayTemplates.cs @@ -0,0 +1,225 @@ +using System.Collections; +using System.Collections.Generic; +using System.Data; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Web.Mvc.Properties; +using System.Web.UI.WebControls; + +namespace System.Web.Mvc.Html +{ + internal static class DefaultDisplayTemplates + { + internal static string BooleanTemplate(HtmlHelper html) + { + bool? value = null; + if (html.ViewContext.ViewData.Model != null) + { + value = Convert.ToBoolean(html.ViewContext.ViewData.Model, CultureInfo.InvariantCulture); + } + + return html.ViewContext.ViewData.ModelMetadata.IsNullableValueType + ? BooleanTemplateDropDownList(value) + : BooleanTemplateCheckbox(value ?? false); + } + + private static string BooleanTemplateCheckbox(bool value) + { + TagBuilder inputTag = new TagBuilder("input"); + inputTag.AddCssClass("check-box"); + inputTag.Attributes["disabled"] = "disabled"; + inputTag.Attributes["type"] = "checkbox"; + if (value) + { + inputTag.Attributes["checked"] = "checked"; + } + + return inputTag.ToString(TagRenderMode.SelfClosing); + } + + private static string BooleanTemplateDropDownList(bool? value) + { + StringBuilder builder = new StringBuilder(); + + TagBuilder selectTag = new TagBuilder("select"); + selectTag.AddCssClass("list-box"); + selectTag.AddCssClass("tri-state"); + selectTag.Attributes["disabled"] = "disabled"; + builder.Append(selectTag.ToString(TagRenderMode.StartTag)); + + foreach (SelectListItem item in DefaultEditorTemplates.TriStateValues(value)) + { + builder.Append(SelectExtensions.ListItemToOption(item)); + } + + builder.Append(selectTag.ToString(TagRenderMode.EndTag)); + return builder.ToString(); + } + + internal static string CollectionTemplate(HtmlHelper html) + { + return CollectionTemplate(html, TemplateHelpers.TemplateHelper); + } + + internal static string CollectionTemplate(HtmlHelper html, TemplateHelpers.TemplateHelperDelegate templateHelper) + { + object model = html.ViewContext.ViewData.ModelMetadata.Model; + if (model == null) + { + return String.Empty; + } + + IEnumerable collection = model as IEnumerable; + if (collection == null) + { + throw new InvalidOperationException( + String.Format( + CultureInfo.CurrentCulture, + MvcResources.Templates_TypeMustImplementIEnumerable, + model.GetType().FullName)); + } + + Type typeInCollection = typeof(string); + Type genericEnumerableType = TypeHelpers.ExtractGenericInterface(collection.GetType(), typeof(IEnumerable<>)); + if (genericEnumerableType != null) + { + typeInCollection = genericEnumerableType.GetGenericArguments()[0]; + } + bool typeInCollectionIsNullableValueType = TypeHelpers.IsNullableValueType(typeInCollection); + + string oldPrefix = html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix; + + try + { + html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = String.Empty; + + string fieldNameBase = oldPrefix; + StringBuilder result = new StringBuilder(); + int index = 0; + + foreach (object item in collection) + { + Type itemType = typeInCollection; + if (item != null && !typeInCollectionIsNullableValueType) + { + itemType = item.GetType(); + } + ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForType(() => item, itemType); + string fieldName = String.Format(CultureInfo.InvariantCulture, "{0}[{1}]", fieldNameBase, index++); + string output = templateHelper(html, metadata, fieldName, null /* templateName */, DataBoundControlMode.ReadOnly, null /* additionalViewData */); + result.Append(output); + } + + return result.ToString(); + } + finally + { + html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = oldPrefix; + } + } + + internal static string DecimalTemplate(HtmlHelper html) + { + if (html.ViewContext.ViewData.TemplateInfo.FormattedModelValue == html.ViewContext.ViewData.ModelMetadata.Model) + { + html.ViewContext.ViewData.TemplateInfo.FormattedModelValue = String.Format(CultureInfo.CurrentCulture, "{0:0.00}", html.ViewContext.ViewData.ModelMetadata.Model); + } + + return StringTemplate(html); + } + + internal static string EmailAddressTemplate(HtmlHelper html) + { + return String.Format(CultureInfo.InvariantCulture, + "<a href=\"mailto:{0}\">{1}</a>", + html.AttributeEncode(html.ViewContext.ViewData.Model), + html.Encode(html.ViewContext.ViewData.TemplateInfo.FormattedModelValue)); + } + + internal static string HiddenInputTemplate(HtmlHelper html) + { + if (html.ViewContext.ViewData.ModelMetadata.HideSurroundingHtml) + { + return String.Empty; + } + return StringTemplate(html); + } + + internal static string HtmlTemplate(HtmlHelper html) + { + return html.ViewContext.ViewData.TemplateInfo.FormattedModelValue.ToString(); + } + + internal static string ObjectTemplate(HtmlHelper html) + { + return ObjectTemplate(html, TemplateHelpers.TemplateHelper); + } + + internal static string ObjectTemplate(HtmlHelper html, TemplateHelpers.TemplateHelperDelegate templateHelper) + { + ViewDataDictionary viewData = html.ViewContext.ViewData; + TemplateInfo templateInfo = viewData.TemplateInfo; + ModelMetadata modelMetadata = viewData.ModelMetadata; + StringBuilder builder = new StringBuilder(); + + if (modelMetadata.Model == null) + { + // DDB #225237 + return modelMetadata.NullDisplayText; + } + + if (templateInfo.TemplateDepth > 1) + { + // DDB #224751 + return modelMetadata.SimpleDisplayText; + } + + foreach (ModelMetadata propertyMetadata in modelMetadata.Properties.Where(pm => ShouldShow(pm, templateInfo))) + { + if (!propertyMetadata.HideSurroundingHtml) + { + string label = propertyMetadata.GetDisplayName(); + if (!String.IsNullOrEmpty(label)) + { + builder.AppendFormat(CultureInfo.InvariantCulture, "<div class=\"display-label\">{0}</div>", label); + builder.AppendLine(); + } + + builder.Append("<div class=\"display-field\">"); + } + + builder.Append(templateHelper(html, propertyMetadata, propertyMetadata.PropertyName, null /* templateName */, DataBoundControlMode.ReadOnly, null /* additionalViewData */)); + + if (!propertyMetadata.HideSurroundingHtml) + { + builder.AppendLine("</div>"); + } + } + + return builder.ToString(); + } + + private static bool ShouldShow(ModelMetadata metadata, TemplateInfo templateInfo) + { + return + metadata.ShowForDisplay + && metadata.ModelType != typeof(EntityState) + && !metadata.IsComplexType + && !templateInfo.Visited(metadata); + } + + internal static string StringTemplate(HtmlHelper html) + { + return html.Encode(html.ViewContext.ViewData.TemplateInfo.FormattedModelValue); + } + + internal static string UrlTemplate(HtmlHelper html) + { + return String.Format(CultureInfo.InvariantCulture, + "<a href=\"{0}\">{1}</a>", + html.AttributeEncode(html.ViewContext.ViewData.Model), + html.Encode(html.ViewContext.ViewData.TemplateInfo.FormattedModelValue)); + } + } +} diff --git a/src/System.Web.Mvc/Html/DefaultEditorTemplates.cs b/src/System.Web.Mvc/Html/DefaultEditorTemplates.cs new file mode 100644 index 00000000..c19b9986 --- /dev/null +++ b/src/System.Web.Mvc/Html/DefaultEditorTemplates.cs @@ -0,0 +1,236 @@ +using System.Collections; +using System.Collections.Generic; +using System.Data; +using System.Data.Linq; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Web.Mvc.Properties; +using System.Web.UI.WebControls; + +namespace System.Web.Mvc.Html +{ + internal static class DefaultEditorTemplates + { + internal static string BooleanTemplate(HtmlHelper html) + { + bool? value = null; + if (html.ViewContext.ViewData.Model != null) + { + value = Convert.ToBoolean(html.ViewContext.ViewData.Model, CultureInfo.InvariantCulture); + } + + return html.ViewContext.ViewData.ModelMetadata.IsNullableValueType + ? BooleanTemplateDropDownList(html, value) + : BooleanTemplateCheckbox(html, value ?? false); + } + + private static string BooleanTemplateCheckbox(HtmlHelper html, bool value) + { + return html.CheckBox(String.Empty, value, CreateHtmlAttributes("check-box")).ToHtmlString(); + } + + private static string BooleanTemplateDropDownList(HtmlHelper html, bool? value) + { + return html.DropDownList(String.Empty, TriStateValues(value), CreateHtmlAttributes("list-box tri-state")).ToHtmlString(); + } + + internal static string CollectionTemplate(HtmlHelper html) + { + return CollectionTemplate(html, TemplateHelpers.TemplateHelper); + } + + internal static string CollectionTemplate(HtmlHelper html, TemplateHelpers.TemplateHelperDelegate templateHelper) + { + object model = html.ViewContext.ViewData.ModelMetadata.Model; + if (model == null) + { + return String.Empty; + } + + IEnumerable collection = model as IEnumerable; + if (collection == null) + { + throw new InvalidOperationException( + String.Format( + CultureInfo.CurrentCulture, + MvcResources.Templates_TypeMustImplementIEnumerable, + model.GetType().FullName)); + } + + Type typeInCollection = typeof(string); + Type genericEnumerableType = TypeHelpers.ExtractGenericInterface(collection.GetType(), typeof(IEnumerable<>)); + if (genericEnumerableType != null) + { + typeInCollection = genericEnumerableType.GetGenericArguments()[0]; + } + bool typeInCollectionIsNullableValueType = TypeHelpers.IsNullableValueType(typeInCollection); + + string oldPrefix = html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix; + + try + { + html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = String.Empty; + + string fieldNameBase = oldPrefix; + StringBuilder result = new StringBuilder(); + int index = 0; + + foreach (object item in collection) + { + Type itemType = typeInCollection; + if (item != null && !typeInCollectionIsNullableValueType) + { + itemType = item.GetType(); + } + ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForType(() => item, itemType); + string fieldName = String.Format(CultureInfo.InvariantCulture, "{0}[{1}]", fieldNameBase, index++); + string output = templateHelper(html, metadata, fieldName, null /* templateName */, DataBoundControlMode.Edit, null /* additionalViewData */); + result.Append(output); + } + + return result.ToString(); + } + finally + { + html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix = oldPrefix; + } + } + + internal static string DecimalTemplate(HtmlHelper html) + { + if (html.ViewContext.ViewData.TemplateInfo.FormattedModelValue == html.ViewContext.ViewData.ModelMetadata.Model) + { + html.ViewContext.ViewData.TemplateInfo.FormattedModelValue = String.Format(CultureInfo.CurrentCulture, "{0:0.00}", html.ViewContext.ViewData.ModelMetadata.Model); + } + + return StringTemplate(html); + } + + internal static string HiddenInputTemplate(HtmlHelper html) + { + string result; + + if (html.ViewContext.ViewData.ModelMetadata.HideSurroundingHtml) + { + result = String.Empty; + } + else + { + result = DefaultDisplayTemplates.StringTemplate(html); + } + + object model = html.ViewContext.ViewData.Model; + + Binary modelAsBinary = model as Binary; + if (modelAsBinary != null) + { + model = Convert.ToBase64String(modelAsBinary.ToArray()); + } + else + { + byte[] modelAsByteArray = model as byte[]; + if (modelAsByteArray != null) + { + model = Convert.ToBase64String(modelAsByteArray); + } + } + + result += html.Hidden(String.Empty, model).ToHtmlString(); + return result; + } + + internal static string MultilineTextTemplate(HtmlHelper html) + { + return html.TextArea(String.Empty, + html.ViewContext.ViewData.TemplateInfo.FormattedModelValue.ToString(), + 0 /* rows */, 0 /* columns */, + CreateHtmlAttributes("text-box multi-line")).ToHtmlString(); + } + + private static IDictionary<string, object> CreateHtmlAttributes(string className) + { + return new Dictionary<string, object>() + { + { "class", className } + }; + } + + internal static string ObjectTemplate(HtmlHelper html) + { + return ObjectTemplate(html, TemplateHelpers.TemplateHelper); + } + + internal static string ObjectTemplate(HtmlHelper html, TemplateHelpers.TemplateHelperDelegate templateHelper) + { + ViewDataDictionary viewData = html.ViewContext.ViewData; + TemplateInfo templateInfo = viewData.TemplateInfo; + ModelMetadata modelMetadata = viewData.ModelMetadata; + StringBuilder builder = new StringBuilder(); + + if (templateInfo.TemplateDepth > 1) + { + // DDB #224751 + return modelMetadata.Model == null ? modelMetadata.NullDisplayText : modelMetadata.SimpleDisplayText; + } + + foreach (ModelMetadata propertyMetadata in modelMetadata.Properties.Where(pm => ShouldShow(pm, templateInfo))) + { + if (!propertyMetadata.HideSurroundingHtml) + { + string label = LabelExtensions.LabelHelper(html, propertyMetadata, propertyMetadata.PropertyName).ToHtmlString(); + if (!String.IsNullOrEmpty(label)) + { + builder.AppendFormat(CultureInfo.InvariantCulture, "<div class=\"editor-label\">{0}</div>\r\n", label); + } + + builder.Append("<div class=\"editor-field\">"); + } + + builder.Append(templateHelper(html, propertyMetadata, propertyMetadata.PropertyName, null /* templateName */, DataBoundControlMode.Edit, null /* additionalViewData */)); + + if (!propertyMetadata.HideSurroundingHtml) + { + builder.Append(" "); + builder.Append(html.ValidationMessage(propertyMetadata.PropertyName)); + builder.Append("</div>\r\n"); + } + } + + return builder.ToString(); + } + + internal static string PasswordTemplate(HtmlHelper html) + { + return html.Password(String.Empty, + html.ViewContext.ViewData.TemplateInfo.FormattedModelValue, + CreateHtmlAttributes("text-box single-line password")).ToHtmlString(); + } + + private static bool ShouldShow(ModelMetadata metadata, TemplateInfo templateInfo) + { + return + metadata.ShowForEdit + && metadata.ModelType != typeof(EntityState) + && !metadata.IsComplexType + && !templateInfo.Visited(metadata); + } + + internal static string StringTemplate(HtmlHelper html) + { + return html.TextBox(String.Empty, + html.ViewContext.ViewData.TemplateInfo.FormattedModelValue, + CreateHtmlAttributes("text-box single-line")).ToHtmlString(); + } + + internal static List<SelectListItem> TriStateValues(bool? value) + { + return new List<SelectListItem> + { + new SelectListItem { Text = MvcResources.Common_TriState_NotSet, Value = String.Empty, Selected = !value.HasValue }, + new SelectListItem { Text = MvcResources.Common_TriState_True, Value = "true", Selected = value.HasValue && value.Value }, + new SelectListItem { Text = MvcResources.Common_TriState_False, Value = "false", Selected = value.HasValue && !value.Value }, + }; + } + } +} diff --git a/src/System.Web.Mvc/Html/DisplayExtensions.cs b/src/System.Web.Mvc/Html/DisplayExtensions.cs new file mode 100644 index 00000000..4b5eaa0a --- /dev/null +++ b/src/System.Web.Mvc/Html/DisplayExtensions.cs @@ -0,0 +1,105 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; +using System.Web.UI.WebControls; + +namespace System.Web.Mvc.Html +{ + public static class DisplayExtensions + { + public static MvcHtmlString Display(this HtmlHelper html, string expression) + { + return TemplateHelpers.Template(html, expression, null /* templateName */, null /* htmlFieldName */, DataBoundControlMode.ReadOnly, null /* additionalViewData */); + } + + public static MvcHtmlString Display(this HtmlHelper html, string expression, object additionalViewData) + { + return TemplateHelpers.Template(html, expression, null /* templateName */, null /* htmlFieldName */, DataBoundControlMode.ReadOnly, additionalViewData); + } + + public static MvcHtmlString Display(this HtmlHelper html, string expression, string templateName) + { + return TemplateHelpers.Template(html, expression, templateName, null /* htmlFieldName */, DataBoundControlMode.ReadOnly, null /* additionalViewData */); + } + + public static MvcHtmlString Display(this HtmlHelper html, string expression, string templateName, object additionalViewData) + { + return TemplateHelpers.Template(html, expression, templateName, null /* htmlFieldName */, DataBoundControlMode.ReadOnly, additionalViewData); + } + + public static MvcHtmlString Display(this HtmlHelper html, string expression, string templateName, string htmlFieldName) + { + return TemplateHelpers.Template(html, expression, templateName, htmlFieldName, DataBoundControlMode.ReadOnly, null /* additionalViewData */); + } + + public static MvcHtmlString Display(this HtmlHelper html, string expression, string templateName, string htmlFieldName, object additionalViewData) + { + return TemplateHelpers.Template(html, expression, templateName, htmlFieldName, DataBoundControlMode.ReadOnly, additionalViewData); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString DisplayFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression) + { + return TemplateHelpers.TemplateFor(html, expression, null /* templateName */, null /* htmlFieldName */, DataBoundControlMode.ReadOnly, null /* additionalViewData */); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString DisplayFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, object additionalViewData) + { + return TemplateHelpers.TemplateFor(html, expression, null /* templateName */, null /* htmlFieldName */, DataBoundControlMode.ReadOnly, additionalViewData); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString DisplayFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string templateName) + { + return TemplateHelpers.TemplateFor(html, expression, templateName, null /* htmlFieldName */, DataBoundControlMode.ReadOnly, null /* additionalViewData */); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString DisplayFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string templateName, object additionalViewData) + { + return TemplateHelpers.TemplateFor(html, expression, templateName, null /* htmlFieldName */, DataBoundControlMode.ReadOnly, additionalViewData); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString DisplayFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string templateName, string htmlFieldName) + { + return TemplateHelpers.TemplateFor(html, expression, templateName, htmlFieldName, DataBoundControlMode.ReadOnly, null /* additionalViewData */); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString DisplayFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string templateName, string htmlFieldName, object additionalViewData) + { + return TemplateHelpers.TemplateFor(html, expression, templateName, htmlFieldName, DataBoundControlMode.ReadOnly, additionalViewData); + } + + public static MvcHtmlString DisplayForModel(this HtmlHelper html) + { + return MvcHtmlString.Create(TemplateHelpers.TemplateHelper(html, html.ViewData.ModelMetadata, String.Empty, null /* templateName */, DataBoundControlMode.ReadOnly, null /* additionalViewData */)); + } + + public static MvcHtmlString DisplayForModel(this HtmlHelper html, object additionalViewData) + { + return MvcHtmlString.Create(TemplateHelpers.TemplateHelper(html, html.ViewData.ModelMetadata, String.Empty, null /* templateName */, DataBoundControlMode.ReadOnly, additionalViewData)); + } + + public static MvcHtmlString DisplayForModel(this HtmlHelper html, string templateName) + { + return MvcHtmlString.Create(TemplateHelpers.TemplateHelper(html, html.ViewData.ModelMetadata, String.Empty, templateName, DataBoundControlMode.ReadOnly, null /* additionalViewData */)); + } + + public static MvcHtmlString DisplayForModel(this HtmlHelper html, string templateName, object additionalViewData) + { + return MvcHtmlString.Create(TemplateHelpers.TemplateHelper(html, html.ViewData.ModelMetadata, String.Empty, templateName, DataBoundControlMode.ReadOnly, additionalViewData)); + } + + public static MvcHtmlString DisplayForModel(this HtmlHelper html, string templateName, string htmlFieldName) + { + return MvcHtmlString.Create(TemplateHelpers.TemplateHelper(html, html.ViewData.ModelMetadata, htmlFieldName, templateName, DataBoundControlMode.ReadOnly, null /* additionalViewData */)); + } + + public static MvcHtmlString DisplayForModel(this HtmlHelper html, string templateName, string htmlFieldName, object additionalViewData) + { + return MvcHtmlString.Create(TemplateHelpers.TemplateHelper(html, html.ViewData.ModelMetadata, htmlFieldName, templateName, DataBoundControlMode.ReadOnly, additionalViewData)); + } + } +} diff --git a/src/System.Web.Mvc/Html/DisplayNameExtensions.cs b/src/System.Web.Mvc/Html/DisplayNameExtensions.cs new file mode 100644 index 00000000..b683aef2 --- /dev/null +++ b/src/System.Web.Mvc/Html/DisplayNameExtensions.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Linq.Expressions; + +namespace System.Web.Mvc.Html +{ + public static class DisplayNameExtensions + { + public static MvcHtmlString DisplayName(this HtmlHelper html, string expression) + { + return DisplayNameInternal(html, expression, metadataProvider: null); + } + + internal static MvcHtmlString DisplayNameInternal(this HtmlHelper html, string expression, ModelMetadataProvider metadataProvider) + { + return DisplayNameHelper(ModelMetadata.FromStringExpression(expression, html.ViewData, metadataProvider), + expression); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString DisplayNameFor<TModel, TValue>(this HtmlHelper<IEnumerable<TModel>> html, Expression<Func<TModel, TValue>> expression) + { + return DisplayNameForInternal(html, expression, metadataProvider: null); + } + + [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", Justification = "This is an extension method")] + internal static MvcHtmlString DisplayNameForInternal<TModel, TValue>(this HtmlHelper<IEnumerable<TModel>> html, Expression<Func<TModel, TValue>> expression, ModelMetadataProvider metadataProvider) + { + return DisplayNameHelper(ModelMetadata.FromLambdaExpression(expression, new ViewDataDictionary<TModel>(), metadataProvider), + ExpressionHelper.GetExpressionText(expression)); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString DisplayNameFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression) + { + return DisplayNameForInternal(html, expression, metadataProvider: null); + } + + internal static MvcHtmlString DisplayNameForInternal<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, ModelMetadataProvider metadataProvider) + { + return DisplayNameHelper(ModelMetadata.FromLambdaExpression(expression, html.ViewData, metadataProvider), + ExpressionHelper.GetExpressionText(expression)); + } + + public static MvcHtmlString DisplayNameForModel(this HtmlHelper html) + { + return DisplayNameHelper(html.ViewData.ModelMetadata, String.Empty); + } + + internal static MvcHtmlString DisplayNameHelper(ModelMetadata metadata, string htmlFieldName) + { + // We don't call ModelMetadata.GetDisplayName here because we want to fall back to the field name rather than the ModelType. + // This is similar to how the LabelHelpers get the text of a label. + string resolvedDisplayName = metadata.DisplayName ?? metadata.PropertyName ?? htmlFieldName.Split('.').Last(); + + return new MvcHtmlString(HttpUtility.HtmlEncode(resolvedDisplayName)); + } + } +} diff --git a/src/System.Web.Mvc/Html/DisplayTextExtensions.cs b/src/System.Web.Mvc/Html/DisplayTextExtensions.cs new file mode 100644 index 00000000..92142125 --- /dev/null +++ b/src/System.Web.Mvc/Html/DisplayTextExtensions.cs @@ -0,0 +1,24 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; + +namespace System.Web.Mvc.Html +{ + public static class DisplayTextExtensions + { + public static MvcHtmlString DisplayText(this HtmlHelper html, string name) + { + return DisplayTextHelper(ModelMetadata.FromStringExpression(name, html.ViewContext.ViewData)); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString DisplayTextFor<TModel, TResult>(this HtmlHelper<TModel> html, Expression<Func<TModel, TResult>> expression) + { + return DisplayTextHelper(ModelMetadata.FromLambdaExpression(expression, html.ViewData)); + } + + private static MvcHtmlString DisplayTextHelper(ModelMetadata metadata) + { + return MvcHtmlString.Create(metadata.SimpleDisplayText); + } + } +} diff --git a/src/System.Web.Mvc/Html/EditorExtensions.cs b/src/System.Web.Mvc/Html/EditorExtensions.cs new file mode 100644 index 00000000..7caa4887 --- /dev/null +++ b/src/System.Web.Mvc/Html/EditorExtensions.cs @@ -0,0 +1,105 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; +using System.Web.UI.WebControls; + +namespace System.Web.Mvc.Html +{ + public static class EditorExtensions + { + public static MvcHtmlString Editor(this HtmlHelper html, string expression) + { + return TemplateHelpers.Template(html, expression, null /* templateName */, null /* htmlFieldName */, DataBoundControlMode.Edit, null /* additionalViewData */); + } + + public static MvcHtmlString Editor(this HtmlHelper html, string expression, object additionalViewData) + { + return TemplateHelpers.Template(html, expression, null /* templateName */, null /* htmlFieldName */, DataBoundControlMode.Edit, additionalViewData); + } + + public static MvcHtmlString Editor(this HtmlHelper html, string expression, string templateName) + { + return TemplateHelpers.Template(html, expression, templateName, null /* htmlFieldName */, DataBoundControlMode.Edit, null /* additionalViewData */); + } + + public static MvcHtmlString Editor(this HtmlHelper html, string expression, string templateName, object additionalViewData) + { + return TemplateHelpers.Template(html, expression, templateName, null /* htmlFieldName */, DataBoundControlMode.Edit, additionalViewData); + } + + public static MvcHtmlString Editor(this HtmlHelper html, string expression, string templateName, string htmlFieldName) + { + return TemplateHelpers.Template(html, expression, templateName, htmlFieldName, DataBoundControlMode.Edit, null /* additionalViewData */); + } + + public static MvcHtmlString Editor(this HtmlHelper html, string expression, string templateName, string htmlFieldName, object additionalViewData) + { + return TemplateHelpers.Template(html, expression, templateName, htmlFieldName, DataBoundControlMode.Edit, additionalViewData); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString EditorFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression) + { + return TemplateHelpers.TemplateFor(html, expression, null /* templateName */, null /* htmlFieldName */, DataBoundControlMode.Edit, null /* additionalViewData */); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString EditorFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, object additionalViewData) + { + return TemplateHelpers.TemplateFor(html, expression, null /* templateName */, null /* htmlFieldName */, DataBoundControlMode.Edit, additionalViewData); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString EditorFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string templateName) + { + return TemplateHelpers.TemplateFor(html, expression, templateName, null /* htmlFieldName */, DataBoundControlMode.Edit, null /* additionalViewData */); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString EditorFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string templateName, object additionalViewData) + { + return TemplateHelpers.TemplateFor(html, expression, templateName, null /* htmlFieldName */, DataBoundControlMode.Edit, additionalViewData); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString EditorFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string templateName, string htmlFieldName) + { + return TemplateHelpers.TemplateFor(html, expression, templateName, htmlFieldName, DataBoundControlMode.Edit, null /* additionalViewData */); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString EditorFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string templateName, string htmlFieldName, object additionalViewData) + { + return TemplateHelpers.TemplateFor(html, expression, templateName, htmlFieldName, DataBoundControlMode.Edit, additionalViewData); + } + + public static MvcHtmlString EditorForModel(this HtmlHelper html) + { + return MvcHtmlString.Create(TemplateHelpers.TemplateHelper(html, html.ViewData.ModelMetadata, String.Empty, null /* templateName */, DataBoundControlMode.Edit, null /* additionalViewData */)); + } + + public static MvcHtmlString EditorForModel(this HtmlHelper html, object additionalViewData) + { + return MvcHtmlString.Create(TemplateHelpers.TemplateHelper(html, html.ViewData.ModelMetadata, String.Empty, null /* templateName */, DataBoundControlMode.Edit, additionalViewData)); + } + + public static MvcHtmlString EditorForModel(this HtmlHelper html, string templateName) + { + return MvcHtmlString.Create(TemplateHelpers.TemplateHelper(html, html.ViewData.ModelMetadata, String.Empty, templateName, DataBoundControlMode.Edit, null /* additionalViewData */)); + } + + public static MvcHtmlString EditorForModel(this HtmlHelper html, string templateName, object additionalViewData) + { + return MvcHtmlString.Create(TemplateHelpers.TemplateHelper(html, html.ViewData.ModelMetadata, String.Empty, templateName, DataBoundControlMode.Edit, additionalViewData)); + } + + public static MvcHtmlString EditorForModel(this HtmlHelper html, string templateName, string htmlFieldName) + { + return MvcHtmlString.Create(TemplateHelpers.TemplateHelper(html, html.ViewData.ModelMetadata, htmlFieldName, templateName, DataBoundControlMode.Edit, null /* additionalViewData */)); + } + + public static MvcHtmlString EditorForModel(this HtmlHelper html, string templateName, string htmlFieldName, object additionalViewData) + { + return MvcHtmlString.Create(TemplateHelpers.TemplateHelper(html, html.ViewData.ModelMetadata, htmlFieldName, templateName, DataBoundControlMode.Edit, additionalViewData)); + } + } +} diff --git a/src/System.Web.Mvc/Html/FormExtensions.cs b/src/System.Web.Mvc/Html/FormExtensions.cs new file mode 100644 index 00000000..4a6780cb --- /dev/null +++ b/src/System.Web.Mvc/Html/FormExtensions.cs @@ -0,0 +1,180 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Web.Routing; + +namespace System.Web.Mvc.Html +{ + public static class FormExtensions + { + public static MvcForm BeginForm(this HtmlHelper htmlHelper) + { + // generates <form action="{current url}" method="post">...</form> + string formAction = htmlHelper.ViewContext.HttpContext.Request.RawUrl; + return FormHelper(htmlHelper, formAction, FormMethod.Post, new RouteValueDictionary()); + } + + public static MvcForm BeginForm(this HtmlHelper htmlHelper, object routeValues) + { + return BeginForm(htmlHelper, null, null, new RouteValueDictionary(routeValues), FormMethod.Post, new RouteValueDictionary()); + } + + public static MvcForm BeginForm(this HtmlHelper htmlHelper, RouteValueDictionary routeValues) + { + return BeginForm(htmlHelper, null, null, routeValues, FormMethod.Post, new RouteValueDictionary()); + } + + public static MvcForm BeginForm(this HtmlHelper htmlHelper, string actionName, string controllerName) + { + return BeginForm(htmlHelper, actionName, controllerName, new RouteValueDictionary(), FormMethod.Post, new RouteValueDictionary()); + } + + public static MvcForm BeginForm(this HtmlHelper htmlHelper, string actionName, string controllerName, object routeValues) + { + return BeginForm(htmlHelper, actionName, controllerName, new RouteValueDictionary(routeValues), FormMethod.Post, new RouteValueDictionary()); + } + + public static MvcForm BeginForm(this HtmlHelper htmlHelper, string actionName, string controllerName, RouteValueDictionary routeValues) + { + return BeginForm(htmlHelper, actionName, controllerName, routeValues, FormMethod.Post, new RouteValueDictionary()); + } + + public static MvcForm BeginForm(this HtmlHelper htmlHelper, string actionName, string controllerName, FormMethod method) + { + return BeginForm(htmlHelper, actionName, controllerName, new RouteValueDictionary(), method, new RouteValueDictionary()); + } + + public static MvcForm BeginForm(this HtmlHelper htmlHelper, string actionName, string controllerName, object routeValues, FormMethod method) + { + return BeginForm(htmlHelper, actionName, controllerName, new RouteValueDictionary(routeValues), method, new RouteValueDictionary()); + } + + public static MvcForm BeginForm(this HtmlHelper htmlHelper, string actionName, string controllerName, RouteValueDictionary routeValues, FormMethod method) + { + return BeginForm(htmlHelper, actionName, controllerName, routeValues, method, new RouteValueDictionary()); + } + + public static MvcForm BeginForm(this HtmlHelper htmlHelper, string actionName, string controllerName, FormMethod method, object htmlAttributes) + { + return BeginForm(htmlHelper, actionName, controllerName, new RouteValueDictionary(), method, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static MvcForm BeginForm(this HtmlHelper htmlHelper, string actionName, string controllerName, FormMethod method, IDictionary<string, object> htmlAttributes) + { + return BeginForm(htmlHelper, actionName, controllerName, new RouteValueDictionary(), method, htmlAttributes); + } + + public static MvcForm BeginForm(this HtmlHelper htmlHelper, string actionName, string controllerName, object routeValues, FormMethod method, object htmlAttributes) + { + return BeginForm(htmlHelper, actionName, controllerName, new RouteValueDictionary(routeValues), method, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static MvcForm BeginForm(this HtmlHelper htmlHelper, string actionName, string controllerName, RouteValueDictionary routeValues, FormMethod method, IDictionary<string, object> htmlAttributes) + { + string formAction = UrlHelper.GenerateUrl(null /* routeName */, actionName, controllerName, routeValues, htmlHelper.RouteCollection, htmlHelper.ViewContext.RequestContext, true /* includeImplicitMvcValues */); + return FormHelper(htmlHelper, formAction, method, htmlAttributes); + } + + public static MvcForm BeginRouteForm(this HtmlHelper htmlHelper, object routeValues) + { + return BeginRouteForm(htmlHelper, null /* routeName */, new RouteValueDictionary(routeValues), FormMethod.Post, new RouteValueDictionary()); + } + + public static MvcForm BeginRouteForm(this HtmlHelper htmlHelper, RouteValueDictionary routeValues) + { + return BeginRouteForm(htmlHelper, null /* routeName */, routeValues, FormMethod.Post, new RouteValueDictionary()); + } + + public static MvcForm BeginRouteForm(this HtmlHelper htmlHelper, string routeName) + { + return BeginRouteForm(htmlHelper, routeName, new RouteValueDictionary(), FormMethod.Post, new RouteValueDictionary()); + } + + public static MvcForm BeginRouteForm(this HtmlHelper htmlHelper, string routeName, object routeValues) + { + return BeginRouteForm(htmlHelper, routeName, new RouteValueDictionary(routeValues), FormMethod.Post, new RouteValueDictionary()); + } + + public static MvcForm BeginRouteForm(this HtmlHelper htmlHelper, string routeName, RouteValueDictionary routeValues) + { + return BeginRouteForm(htmlHelper, routeName, routeValues, FormMethod.Post, new RouteValueDictionary()); + } + + public static MvcForm BeginRouteForm(this HtmlHelper htmlHelper, string routeName, FormMethod method) + { + return BeginRouteForm(htmlHelper, routeName, new RouteValueDictionary(), method, new RouteValueDictionary()); + } + + public static MvcForm BeginRouteForm(this HtmlHelper htmlHelper, string routeName, object routeValues, FormMethod method) + { + return BeginRouteForm(htmlHelper, routeName, new RouteValueDictionary(routeValues), method, new RouteValueDictionary()); + } + + public static MvcForm BeginRouteForm(this HtmlHelper htmlHelper, string routeName, RouteValueDictionary routeValues, FormMethod method) + { + return BeginRouteForm(htmlHelper, routeName, routeValues, method, new RouteValueDictionary()); + } + + public static MvcForm BeginRouteForm(this HtmlHelper htmlHelper, string routeName, FormMethod method, object htmlAttributes) + { + return BeginRouteForm(htmlHelper, routeName, new RouteValueDictionary(), method, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static MvcForm BeginRouteForm(this HtmlHelper htmlHelper, string routeName, FormMethod method, IDictionary<string, object> htmlAttributes) + { + return BeginRouteForm(htmlHelper, routeName, new RouteValueDictionary(), method, htmlAttributes); + } + + public static MvcForm BeginRouteForm(this HtmlHelper htmlHelper, string routeName, object routeValues, FormMethod method, object htmlAttributes) + { + return BeginRouteForm(htmlHelper, routeName, new RouteValueDictionary(routeValues), method, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static MvcForm BeginRouteForm(this HtmlHelper htmlHelper, string routeName, RouteValueDictionary routeValues, FormMethod method, IDictionary<string, object> htmlAttributes) + { + string formAction = UrlHelper.GenerateUrl(routeName, null, null, routeValues, htmlHelper.RouteCollection, htmlHelper.ViewContext.RequestContext, false /* includeImplicitMvcValues */); + return FormHelper(htmlHelper, formAction, method, htmlAttributes); + } + + public static void EndForm(this HtmlHelper htmlHelper) + { + EndForm(htmlHelper.ViewContext); + } + + internal static void EndForm(ViewContext viewContext) + { + viewContext.Writer.Write("</form>"); + viewContext.OutputClientValidation(); + viewContext.FormContext = null; + } + + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Because disposing the object would write to the response stream, you don't want to prematurely dispose of this object.")] + private static MvcForm FormHelper(this HtmlHelper htmlHelper, string formAction, FormMethod method, IDictionary<string, object> htmlAttributes) + { + TagBuilder tagBuilder = new TagBuilder("form"); + tagBuilder.MergeAttributes(htmlAttributes); + // action is implicitly generated, so htmlAttributes take precedence. + tagBuilder.MergeAttribute("action", formAction); + // method is an explicit parameter, so it takes precedence over the htmlAttributes. + tagBuilder.MergeAttribute("method", HtmlHelper.GetFormMethodString(method), true); + + bool traditionalJavascriptEnabled = htmlHelper.ViewContext.ClientValidationEnabled + && !htmlHelper.ViewContext.UnobtrusiveJavaScriptEnabled; + + if (traditionalJavascriptEnabled) + { + // forms must have an ID for client validation + tagBuilder.GenerateId(htmlHelper.ViewContext.FormIdGenerator()); + } + + htmlHelper.ViewContext.Writer.Write(tagBuilder.ToString(TagRenderMode.StartTag)); + MvcForm theForm = new MvcForm(htmlHelper.ViewContext); + + if (traditionalJavascriptEnabled) + { + htmlHelper.ViewContext.FormContext.FormId = tagBuilder.Attributes["id"]; + } + + return theForm; + } + } +} diff --git a/src/System.Web.Mvc/Html/InputExtensions.cs b/src/System.Web.Mvc/Html/InputExtensions.cs new file mode 100644 index 00000000..fa3d7e9c --- /dev/null +++ b/src/System.Web.Mvc/Html/InputExtensions.cs @@ -0,0 +1,581 @@ +using System.Collections.Generic; +using System.Data.Linq; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq.Expressions; +using System.Text; +using System.Web.Mvc.Properties; +using System.Web.Routing; + +namespace System.Web.Mvc.Html +{ + public static class InputExtensions + { + // CheckBox + + public static MvcHtmlString CheckBox(this HtmlHelper htmlHelper, string name) + { + return CheckBox(htmlHelper, name, htmlAttributes: (object)null); + } + + public static MvcHtmlString CheckBox(this HtmlHelper htmlHelper, string name, bool isChecked) + { + return CheckBox(htmlHelper, name, isChecked, htmlAttributes: (object)null); + } + + public static MvcHtmlString CheckBox(this HtmlHelper htmlHelper, string name, bool isChecked, object htmlAttributes) + { + return CheckBox(htmlHelper, name, isChecked, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static MvcHtmlString CheckBox(this HtmlHelper htmlHelper, string name, object htmlAttributes) + { + return CheckBox(htmlHelper, name, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static MvcHtmlString CheckBox(this HtmlHelper htmlHelper, string name, IDictionary<string, object> htmlAttributes) + { + return CheckBoxHelper(htmlHelper, metadata: null, name: name, isChecked: null, htmlAttributes: htmlAttributes); + } + + public static MvcHtmlString CheckBox(this HtmlHelper htmlHelper, string name, bool isChecked, IDictionary<string, object> htmlAttributes) + { + return CheckBoxHelper(htmlHelper, metadata: null, name: name, isChecked: isChecked, htmlAttributes: htmlAttributes); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString CheckBoxFor<TModel>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, bool>> expression) + { + return CheckBoxFor(htmlHelper, expression, htmlAttributes: null); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString CheckBoxFor<TModel>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, bool>> expression, object htmlAttributes) + { + return CheckBoxFor(htmlHelper, expression, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString CheckBoxFor<TModel>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, bool>> expression, IDictionary<string, object> htmlAttributes) + { + if (expression == null) + { + throw new ArgumentNullException("expression"); + } + + ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData); + bool? isChecked = null; + if (metadata.Model != null) + { + bool modelChecked; + if (Boolean.TryParse(metadata.Model.ToString(), out modelChecked)) + { + isChecked = modelChecked; + } + } + + return CheckBoxHelper(htmlHelper, metadata, ExpressionHelper.GetExpressionText(expression), isChecked, htmlAttributes); + } + + private static MvcHtmlString CheckBoxHelper(HtmlHelper htmlHelper, ModelMetadata metadata, string name, bool? isChecked, IDictionary<string, object> htmlAttributes) + { + RouteValueDictionary attributes = ToRouteValueDictionary(htmlAttributes); + + bool explicitValue = isChecked.HasValue; + if (explicitValue) + { + attributes.Remove("checked"); // Explicit value must override dictionary + } + + return InputHelper(htmlHelper, + InputType.CheckBox, + metadata, + name, + value: "true", + useViewData: !explicitValue, + isChecked: isChecked ?? false, + setId: true, + isExplicitValue: false, + format: null, + htmlAttributes: attributes); + } + + // Hidden + + public static MvcHtmlString Hidden(this HtmlHelper htmlHelper, string name) + { + return Hidden(htmlHelper, name, value: null, htmlAttributes: null); + } + + public static MvcHtmlString Hidden(this HtmlHelper htmlHelper, string name, object value) + { + return Hidden(htmlHelper, name, value, htmlAttributes: null); + } + + public static MvcHtmlString Hidden(this HtmlHelper htmlHelper, string name, object value, object htmlAttributes) + { + return Hidden(htmlHelper, name, value, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static MvcHtmlString Hidden(this HtmlHelper htmlHelper, string name, object value, IDictionary<string, object> htmlAttributes) + { + return HiddenHelper(htmlHelper, + metadata: null, + value: value, + useViewData: value == null, + expression: name, + htmlAttributes: htmlAttributes); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString HiddenFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression) + { + return HiddenFor(htmlHelper, expression, (IDictionary<string, object>)null); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString HiddenFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object htmlAttributes) + { + return HiddenFor(htmlHelper, expression, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString HiddenFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IDictionary<string, object> htmlAttributes) + { + ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData); + return HiddenHelper(htmlHelper, + metadata, + metadata.Model, + false, + ExpressionHelper.GetExpressionText(expression), + htmlAttributes); + } + + private static MvcHtmlString HiddenHelper(HtmlHelper htmlHelper, ModelMetadata metadata, object value, bool useViewData, string expression, IDictionary<string, object> htmlAttributes) + { + Binary binaryValue = value as Binary; + if (binaryValue != null) + { + value = binaryValue.ToArray(); + } + + byte[] byteArrayValue = value as byte[]; + if (byteArrayValue != null) + { + value = Convert.ToBase64String(byteArrayValue); + } + + return InputHelper(htmlHelper, + InputType.Hidden, + metadata, + expression, + value, + useViewData, + isChecked: false, + setId: true, + isExplicitValue: true, + format: null, + htmlAttributes: htmlAttributes); + } + + // Password + + public static MvcHtmlString Password(this HtmlHelper htmlHelper, string name) + { + return Password(htmlHelper, name, value: null); + } + + public static MvcHtmlString Password(this HtmlHelper htmlHelper, string name, object value) + { + return Password(htmlHelper, name, value, htmlAttributes: null); + } + + public static MvcHtmlString Password(this HtmlHelper htmlHelper, string name, object value, object htmlAttributes) + { + return Password(htmlHelper, name, value, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static MvcHtmlString Password(this HtmlHelper htmlHelper, string name, object value, IDictionary<string, object> htmlAttributes) + { + return PasswordHelper(htmlHelper, metadata: null, name: name, value: value, htmlAttributes: htmlAttributes); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString PasswordFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression) + { + return PasswordFor(htmlHelper, expression, htmlAttributes: null); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString PasswordFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object htmlAttributes) + { + return PasswordFor(htmlHelper, expression, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Users cannot use anonymous methods with the LambdaExpression type")] + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString PasswordFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IDictionary<string, object> htmlAttributes) + { + if (expression == null) + { + throw new ArgumentNullException("expression"); + } + + return PasswordHelper(htmlHelper, + ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData), + ExpressionHelper.GetExpressionText(expression), + value: null, + htmlAttributes: htmlAttributes); + } + + private static MvcHtmlString PasswordHelper(HtmlHelper htmlHelper, ModelMetadata metadata, string name, object value, IDictionary<string, object> htmlAttributes) + { + return InputHelper(htmlHelper, + InputType.Password, + metadata, + name, + value, + useViewData: false, + isChecked: false, + setId: true, + isExplicitValue: true, + format: null, + htmlAttributes: htmlAttributes); + } + + // RadioButton + + public static MvcHtmlString RadioButton(this HtmlHelper htmlHelper, string name, object value) + { + return RadioButton(htmlHelper, name, value, htmlAttributes: (object)null); + } + + public static MvcHtmlString RadioButton(this HtmlHelper htmlHelper, string name, object value, object htmlAttributes) + { + return RadioButton(htmlHelper, name, value, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static MvcHtmlString RadioButton(this HtmlHelper htmlHelper, string name, object value, IDictionary<string, object> htmlAttributes) + { + // Determine whether or not to render the checked attribute based on the contents of ViewData. + string valueString = Convert.ToString(value, CultureInfo.CurrentCulture); + bool isChecked = (!String.IsNullOrEmpty(name)) && (String.Equals(htmlHelper.EvalString(name), valueString, StringComparison.OrdinalIgnoreCase)); + // checked attributes is implicit, so we need to ensure that the dictionary takes precedence. + RouteValueDictionary attributes = ToRouteValueDictionary(htmlAttributes); + if (attributes.ContainsKey("checked")) + { + return InputHelper(htmlHelper, + InputType.Radio, + metadata: null, + name: name, + value: value, + useViewData: false, + isChecked: false, + setId: true, + isExplicitValue: true, + format: null, + htmlAttributes: attributes); + } + + return RadioButton(htmlHelper, name, value, isChecked, htmlAttributes); + } + + public static MvcHtmlString RadioButton(this HtmlHelper htmlHelper, string name, object value, bool isChecked) + { + return RadioButton(htmlHelper, name, value, isChecked, htmlAttributes: (object)null); + } + + public static MvcHtmlString RadioButton(this HtmlHelper htmlHelper, string name, object value, bool isChecked, object htmlAttributes) + { + return RadioButton(htmlHelper, name, value, isChecked, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static MvcHtmlString RadioButton(this HtmlHelper htmlHelper, string name, object value, bool isChecked, IDictionary<string, object> htmlAttributes) + { + if (value == null) + { + throw new ArgumentNullException("value"); + } + // checked attribute is an explicit parameter so it takes precedence. + RouteValueDictionary attributes = ToRouteValueDictionary(htmlAttributes); + attributes.Remove("checked"); + return InputHelper(htmlHelper, + InputType.Radio, + metadata: null, + name: name, + value: value, + useViewData: false, + isChecked: isChecked, + setId: true, + isExplicitValue: true, + format: null, + htmlAttributes: attributes); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString RadioButtonFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object value) + { + return RadioButtonFor(htmlHelper, expression, value, htmlAttributes: null); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString RadioButtonFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object value, object htmlAttributes) + { + return RadioButtonFor(htmlHelper, expression, value, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString RadioButtonFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object value, IDictionary<string, object> htmlAttributes) + { + ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData); + return RadioButtonHelper(htmlHelper, + metadata, + metadata.Model, + ExpressionHelper.GetExpressionText(expression), + value, + null /* isChecked */, + htmlAttributes); + } + + private static MvcHtmlString RadioButtonHelper(HtmlHelper htmlHelper, ModelMetadata metadata, object model, string name, object value, bool? isChecked, IDictionary<string, object> htmlAttributes) + { + if (value == null) + { + throw new ArgumentNullException("value"); + } + + RouteValueDictionary attributes = ToRouteValueDictionary(htmlAttributes); + + bool explicitValue = isChecked.HasValue; + if (explicitValue) + { + attributes.Remove("checked"); // Explicit value must override dictionary + } + else + { + string valueString = Convert.ToString(value, CultureInfo.CurrentCulture); + isChecked = model != null && + !String.IsNullOrEmpty(name) && + String.Equals(model.ToString(), valueString, StringComparison.OrdinalIgnoreCase); + } + + return InputHelper(htmlHelper, + InputType.Radio, + metadata, + name, + value, + useViewData: false, + isChecked: isChecked ?? false, + setId: true, + isExplicitValue: true, + format: null, + htmlAttributes: attributes); + } + + // TextBox + + public static MvcHtmlString TextBox(this HtmlHelper htmlHelper, string name) + { + return TextBox(htmlHelper, name, value: null); + } + + public static MvcHtmlString TextBox(this HtmlHelper htmlHelper, string name, object value) + { + return TextBox(htmlHelper, name, value, format: null); + } + + public static MvcHtmlString TextBox(this HtmlHelper htmlHelper, string name, object value, string format) + { + return TextBox(htmlHelper, name, value, format, htmlAttributes: (object)null); + } + + public static MvcHtmlString TextBox(this HtmlHelper htmlHelper, string name, object value, object htmlAttributes) + { + return TextBox(htmlHelper, name, value, format: null, htmlAttributes: htmlAttributes); + } + + public static MvcHtmlString TextBox(this HtmlHelper htmlHelper, string name, object value, string format, object htmlAttributes) + { + return TextBox(htmlHelper, name, value, format, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static MvcHtmlString TextBox(this HtmlHelper htmlHelper, string name, object value, IDictionary<string, object> htmlAttributes) + { + return TextBox(htmlHelper, name, value, format: null, htmlAttributes: htmlAttributes); + } + + public static MvcHtmlString TextBox(this HtmlHelper htmlHelper, string name, object value, string format, IDictionary<string, object> htmlAttributes) + { + return InputHelper(htmlHelper, + InputType.Text, + metadata: null, + name: name, + value: value, + useViewData: (value == null), + isChecked: false, + setId: true, + isExplicitValue: true, + format: format, + htmlAttributes: htmlAttributes); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString TextBoxFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression) + { + return htmlHelper.TextBoxFor(expression, format: null); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString TextBoxFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string format) + { + return htmlHelper.TextBoxFor(expression, format, (IDictionary<string, object>)null); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString TextBoxFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object htmlAttributes) + { + return htmlHelper.TextBoxFor(expression, format: null, htmlAttributes: htmlAttributes); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString TextBoxFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string format, object htmlAttributes) + { + return htmlHelper.TextBoxFor(expression, format: format, htmlAttributes: HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString TextBoxFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IDictionary<string, object> htmlAttributes) + { + return htmlHelper.TextBoxFor(expression, format: null, htmlAttributes: htmlAttributes); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString TextBoxFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string format, IDictionary<string, object> htmlAttributes) + { + ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData); + return TextBoxHelper(htmlHelper, + metadata, + metadata.Model, + ExpressionHelper.GetExpressionText(expression), + format, + htmlAttributes); + } + + private static MvcHtmlString TextBoxHelper(this HtmlHelper htmlHelper, ModelMetadata metadata, object model, string expression, string format, IDictionary<string, object> htmlAttributes) + { + return InputHelper(htmlHelper, + InputType.Text, + metadata, + expression, + model, + useViewData: false, + isChecked: false, + setId: true, + isExplicitValue: true, + format: format, + htmlAttributes: htmlAttributes); + } + + // Helper methods + + private static MvcHtmlString InputHelper(HtmlHelper htmlHelper, InputType inputType, ModelMetadata metadata, string name, object value, bool useViewData, bool isChecked, bool setId, bool isExplicitValue, string format, IDictionary<string, object> htmlAttributes) + { + string fullName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name); + if (String.IsNullOrEmpty(fullName)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "name"); + } + + TagBuilder tagBuilder = new TagBuilder("input"); + tagBuilder.MergeAttributes(htmlAttributes); + tagBuilder.MergeAttribute("type", HtmlHelper.GetInputTypeString(inputType)); + tagBuilder.MergeAttribute("name", fullName, true); + + string valueParameter = htmlHelper.FormatValue(value, format); + bool usedModelState = false; + + switch (inputType) + { + case InputType.CheckBox: + bool? modelStateWasChecked = htmlHelper.GetModelStateValue(fullName, typeof(bool)) as bool?; + if (modelStateWasChecked.HasValue) + { + isChecked = modelStateWasChecked.Value; + usedModelState = true; + } + goto case InputType.Radio; + case InputType.Radio: + if (!usedModelState) + { + string modelStateValue = htmlHelper.GetModelStateValue(fullName, typeof(string)) as string; + if (modelStateValue != null) + { + isChecked = String.Equals(modelStateValue, valueParameter, StringComparison.Ordinal); + usedModelState = true; + } + } + if (!usedModelState && useViewData) + { + isChecked = htmlHelper.EvalBoolean(fullName); + } + if (isChecked) + { + tagBuilder.MergeAttribute("checked", "checked"); + } + tagBuilder.MergeAttribute("value", valueParameter, isExplicitValue); + break; + case InputType.Password: + if (value != null) + { + tagBuilder.MergeAttribute("value", valueParameter, isExplicitValue); + } + break; + default: + string attemptedValue = (string)htmlHelper.GetModelStateValue(fullName, typeof(string)); + tagBuilder.MergeAttribute("value", attemptedValue ?? ((useViewData) ? htmlHelper.EvalString(fullName, format) : valueParameter), isExplicitValue); + break; + } + + if (setId) + { + tagBuilder.GenerateId(fullName); + } + + // If there are any errors for a named field, we add the css attribute. + ModelState modelState; + if (htmlHelper.ViewData.ModelState.TryGetValue(fullName, out modelState)) + { + if (modelState.Errors.Count > 0) + { + tagBuilder.AddCssClass(HtmlHelper.ValidationInputCssClassName); + } + } + + tagBuilder.MergeAttributes(htmlHelper.GetUnobtrusiveValidationAttributes(name, metadata)); + + if (inputType == InputType.CheckBox) + { + // Render an additional <input type="hidden".../> for checkboxes. This + // addresses scenarios where unchecked checkboxes are not sent in the request. + // Sending a hidden input makes it possible to know that the checkbox was present + // on the page when the request was submitted. + StringBuilder inputItemBuilder = new StringBuilder(); + inputItemBuilder.Append(tagBuilder.ToString(TagRenderMode.SelfClosing)); + + TagBuilder hiddenInput = new TagBuilder("input"); + hiddenInput.MergeAttribute("type", HtmlHelper.GetInputTypeString(InputType.Hidden)); + hiddenInput.MergeAttribute("name", fullName); + hiddenInput.MergeAttribute("value", "false"); + inputItemBuilder.Append(hiddenInput.ToString(TagRenderMode.SelfClosing)); + return MvcHtmlString.Create(inputItemBuilder.ToString()); + } + + return tagBuilder.ToMvcHtmlString(TagRenderMode.SelfClosing); + } + + private static RouteValueDictionary ToRouteValueDictionary(IDictionary<string, object> dictionary) + { + return dictionary == null ? new RouteValueDictionary() : new RouteValueDictionary(dictionary); + } + } +} diff --git a/src/System.Web.Mvc/Html/LabelExtensions.cs b/src/System.Web.Mvc/Html/LabelExtensions.cs new file mode 100644 index 00000000..96c06b2e --- /dev/null +++ b/src/System.Web.Mvc/Html/LabelExtensions.cs @@ -0,0 +1,159 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Linq.Expressions; + +namespace System.Web.Mvc.Html +{ + public static class LabelExtensions + { + public static MvcHtmlString Label(this HtmlHelper html, string expression) + { + return Label(html, + expression, + labelText: null); + } + + public static MvcHtmlString Label(this HtmlHelper html, string expression, string labelText) + { + return Label(html, expression, labelText, htmlAttributes: null, metadataProvider: null); + } + + public static MvcHtmlString Label(this HtmlHelper html, string expression, object htmlAttributes) + { + return Label(html, expression, labelText: null, htmlAttributes: htmlAttributes, metadataProvider: null); + } + + public static MvcHtmlString Label(this HtmlHelper html, string expression, IDictionary<string, object> htmlAttributes) + { + return Label(html, expression, labelText: null, htmlAttributes: htmlAttributes, metadataProvider: null); + } + + public static MvcHtmlString Label(this HtmlHelper html, string expression, string labelText, object htmlAttributes) + { + return Label(html, expression, labelText, htmlAttributes, metadataProvider: null); + } + + public static MvcHtmlString Label(this HtmlHelper html, string expression, string labelText, IDictionary<string, object> htmlAttributes) + { + return Label(html, expression, labelText, htmlAttributes, metadataProvider: null); + } + + internal static MvcHtmlString Label(this HtmlHelper html, string expression, string labelText, object htmlAttributes, ModelMetadataProvider metadataProvider) + { + return Label(html, + expression, + labelText, + HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes), + metadataProvider); + } + + internal static MvcHtmlString Label(this HtmlHelper html, string expression, string labelText, IDictionary<string, object> htmlAttributes, ModelMetadataProvider metadataProvider) + { + return LabelHelper(html, + ModelMetadata.FromStringExpression(expression, html.ViewData, metadataProvider), + expression, + labelText, + htmlAttributes); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString LabelFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression) + { + return LabelFor<TModel, TValue>(html, expression, labelText: null); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString LabelFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string labelText) + { + return LabelFor(html, expression, labelText, htmlAttributes: null, metadataProvider: null); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString LabelFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, object htmlAttributes) + { + return LabelFor(html, expression, labelText: null, htmlAttributes: htmlAttributes, metadataProvider: null); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString LabelFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, IDictionary<string, object> htmlAttributes) + { + return LabelFor(html, expression, labelText: null, htmlAttributes: htmlAttributes, metadataProvider: null); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString LabelFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string labelText, object htmlAttributes) + { + return LabelFor(html, expression, labelText, htmlAttributes, metadataProvider: null); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString LabelFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string labelText, IDictionary<string, object> htmlAttributes) + { + return LabelFor(html, expression, labelText, htmlAttributes, metadataProvider: null); + } + + internal static MvcHtmlString LabelFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string labelText, object htmlAttributes, ModelMetadataProvider metadataProvider) + { + return LabelFor(html, + expression, + labelText, + HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes), + metadataProvider); + } + + internal static MvcHtmlString LabelFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string labelText, IDictionary<string, object> htmlAttributes, ModelMetadataProvider metadataProvider) + { + return LabelHelper(html, + ModelMetadata.FromLambdaExpression(expression, html.ViewData, metadataProvider), + ExpressionHelper.GetExpressionText(expression), + labelText, + htmlAttributes); + } + + public static MvcHtmlString LabelForModel(this HtmlHelper html) + { + return LabelForModel(html, labelText: null); + } + + public static MvcHtmlString LabelForModel(this HtmlHelper html, string labelText) + { + return LabelHelper(html, html.ViewData.ModelMetadata, String.Empty, labelText); + } + + public static MvcHtmlString LabelForModel(this HtmlHelper html, object htmlAttributes) + { + return LabelHelper(html, html.ViewData.ModelMetadata, String.Empty, labelText: null, htmlAttributes: HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static MvcHtmlString LabelForModel(this HtmlHelper html, IDictionary<string, object> htmlAttributes) + { + return LabelHelper(html, html.ViewData.ModelMetadata, String.Empty, labelText: null, htmlAttributes: htmlAttributes); + } + + public static MvcHtmlString LabelForModel(this HtmlHelper html, string labelText, object htmlAttributes) + { + return LabelHelper(html, html.ViewData.ModelMetadata, String.Empty, labelText, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static MvcHtmlString LabelForModel(this HtmlHelper html, string labelText, IDictionary<string, object> htmlAttributes) + { + return LabelHelper(html, html.ViewData.ModelMetadata, String.Empty, labelText, htmlAttributes); + } + + internal static MvcHtmlString LabelHelper(HtmlHelper html, ModelMetadata metadata, string htmlFieldName, string labelText = null, IDictionary<string, object> htmlAttributes = null) + { + string resolvedLabelText = labelText ?? metadata.DisplayName ?? metadata.PropertyName ?? htmlFieldName.Split('.').Last(); + if (String.IsNullOrEmpty(resolvedLabelText)) + { + return MvcHtmlString.Empty; + } + + TagBuilder tag = new TagBuilder("label"); + tag.Attributes.Add("for", TagBuilder.CreateSanitizedId(html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(htmlFieldName))); + tag.SetInnerText(resolvedLabelText); + tag.MergeAttributes(htmlAttributes, replaceExisting: true); + return tag.ToMvcHtmlString(TagRenderMode.Normal); + } + } +} diff --git a/src/System.Web.Mvc/Html/LinkExtensions.cs b/src/System.Web.Mvc/Html/LinkExtensions.cs new file mode 100644 index 00000000..cde4318c --- /dev/null +++ b/src/System.Web.Mvc/Html/LinkExtensions.cs @@ -0,0 +1,130 @@ +using System.Collections.Generic; +using System.Web.Mvc.Properties; +using System.Web.Routing; + +namespace System.Web.Mvc.Html +{ + public static class LinkExtensions + { + public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName) + { + return ActionLink(htmlHelper, linkText, actionName, null /* controllerName */, new RouteValueDictionary(), new RouteValueDictionary()); + } + + public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, object routeValues) + { + return ActionLink(htmlHelper, linkText, actionName, null /* controllerName */, new RouteValueDictionary(routeValues), new RouteValueDictionary()); + } + + public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, object routeValues, object htmlAttributes) + { + return ActionLink(htmlHelper, linkText, actionName, null /* controllerName */, new RouteValueDictionary(routeValues), HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, RouteValueDictionary routeValues) + { + return ActionLink(htmlHelper, linkText, actionName, null /* controllerName */, routeValues, new RouteValueDictionary()); + } + + public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes) + { + return ActionLink(htmlHelper, linkText, actionName, null /* controllerName */, routeValues, htmlAttributes); + } + + public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName) + { + return ActionLink(htmlHelper, linkText, actionName, controllerName, new RouteValueDictionary(), new RouteValueDictionary()); + } + + public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, object routeValues, object htmlAttributes) + { + return ActionLink(htmlHelper, linkText, actionName, controllerName, new RouteValueDictionary(routeValues), HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes) + { + if (String.IsNullOrEmpty(linkText)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "linkText"); + } + return MvcHtmlString.Create(HtmlHelper.GenerateLink(htmlHelper.ViewContext.RequestContext, htmlHelper.RouteCollection, linkText, null /* routeName */, actionName, controllerName, routeValues, htmlAttributes)); + } + + public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, string protocol, string hostName, string fragment, object routeValues, object htmlAttributes) + { + return ActionLink(htmlHelper, linkText, actionName, controllerName, protocol, hostName, fragment, new RouteValueDictionary(routeValues), HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, string protocol, string hostName, string fragment, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes) + { + if (String.IsNullOrEmpty(linkText)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "linkText"); + } + return MvcHtmlString.Create(HtmlHelper.GenerateLink(htmlHelper.ViewContext.RequestContext, htmlHelper.RouteCollection, linkText, null /* routeName */, actionName, controllerName, protocol, hostName, fragment, routeValues, htmlAttributes)); + } + + public static MvcHtmlString RouteLink(this HtmlHelper htmlHelper, string linkText, object routeValues) + { + return RouteLink(htmlHelper, linkText, new RouteValueDictionary(routeValues)); + } + + public static MvcHtmlString RouteLink(this HtmlHelper htmlHelper, string linkText, RouteValueDictionary routeValues) + { + return RouteLink(htmlHelper, linkText, routeValues, new RouteValueDictionary()); + } + + public static MvcHtmlString RouteLink(this HtmlHelper htmlHelper, string linkText, string routeName) + { + return RouteLink(htmlHelper, linkText, routeName, (object)null /* routeValues */); + } + + public static MvcHtmlString RouteLink(this HtmlHelper htmlHelper, string linkText, string routeName, object routeValues) + { + return RouteLink(htmlHelper, linkText, routeName, new RouteValueDictionary(routeValues)); + } + + public static MvcHtmlString RouteLink(this HtmlHelper htmlHelper, string linkText, string routeName, RouteValueDictionary routeValues) + { + return RouteLink(htmlHelper, linkText, routeName, routeValues, new RouteValueDictionary()); + } + + public static MvcHtmlString RouteLink(this HtmlHelper htmlHelper, string linkText, object routeValues, object htmlAttributes) + { + return RouteLink(htmlHelper, linkText, new RouteValueDictionary(routeValues), HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static MvcHtmlString RouteLink(this HtmlHelper htmlHelper, string linkText, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes) + { + return RouteLink(htmlHelper, linkText, null /* routeName */, routeValues, htmlAttributes); + } + + public static MvcHtmlString RouteLink(this HtmlHelper htmlHelper, string linkText, string routeName, object routeValues, object htmlAttributes) + { + return RouteLink(htmlHelper, linkText, routeName, new RouteValueDictionary(routeValues), HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static MvcHtmlString RouteLink(this HtmlHelper htmlHelper, string linkText, string routeName, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes) + { + if (String.IsNullOrEmpty(linkText)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "linkText"); + } + return MvcHtmlString.Create(HtmlHelper.GenerateRouteLink(htmlHelper.ViewContext.RequestContext, htmlHelper.RouteCollection, linkText, routeName, routeValues, htmlAttributes)); + } + + public static MvcHtmlString RouteLink(this HtmlHelper htmlHelper, string linkText, string routeName, string protocol, string hostName, string fragment, object routeValues, object htmlAttributes) + { + return RouteLink(htmlHelper, linkText, routeName, protocol, hostName, fragment, new RouteValueDictionary(routeValues), HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static MvcHtmlString RouteLink(this HtmlHelper htmlHelper, string linkText, string routeName, string protocol, string hostName, string fragment, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes) + { + if (String.IsNullOrEmpty(linkText)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "linkText"); + } + return MvcHtmlString.Create(HtmlHelper.GenerateRouteLink(htmlHelper.ViewContext.RequestContext, htmlHelper.RouteCollection, linkText, routeName, protocol, hostName, fragment, routeValues, htmlAttributes)); + } + } +} diff --git a/src/System.Web.Mvc/Html/MvcForm.cs b/src/System.Web.Mvc/Html/MvcForm.cs new file mode 100644 index 00000000..1d700468 --- /dev/null +++ b/src/System.Web.Mvc/Html/MvcForm.cs @@ -0,0 +1,51 @@ +using System.Diagnostics.CodeAnalysis; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc.Html +{ + public class MvcForm : IDisposable + { + private readonly ViewContext _viewContext; + private bool _disposed; + + [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "httpResponse", Justification = "This method existed in MVC 1.0 and has been deprecated.")] + [Obsolete("This constructor is obsolete, because its functionality has been moved to MvcForm(ViewContext) now.", true /* error */)] + public MvcForm(HttpResponseBase httpResponse) + { + throw new InvalidOperationException(MvcResources.MvcForm_ConstructorObsolete); + } + + public MvcForm(ViewContext viewContext) + { + if (viewContext == null) + { + throw new ArgumentNullException("viewContext"); + } + + _viewContext = viewContext; + + // push the new FormContext + _viewContext.FormContext = new FormContext(); + } + + public void Dispose() + { + Dispose(true /* disposing */); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + _disposed = true; + FormExtensions.EndForm(_viewContext); + } + } + + public void EndForm() + { + Dispose(true); + } + } +} diff --git a/src/System.Web.Mvc/Html/NameExtensions.cs b/src/System.Web.Mvc/Html/NameExtensions.cs new file mode 100644 index 00000000..9d5f9b16 --- /dev/null +++ b/src/System.Web.Mvc/Html/NameExtensions.cs @@ -0,0 +1,43 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; + +namespace System.Web.Mvc.Html +{ + public static class NameExtensions + { + public static MvcHtmlString Id(this HtmlHelper html, string name) + { + return MvcHtmlString.Create(html.AttributeEncode(html.ViewData.TemplateInfo.GetFullHtmlFieldId(name))); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Users cannot use anonymous methods with the LambdaExpression type")] + public static MvcHtmlString IdFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression) + { + return Id(html, ExpressionHelper.GetExpressionText(expression)); + } + + public static MvcHtmlString IdForModel(this HtmlHelper html) + { + return Id(html, String.Empty); + } + + [SuppressMessage("Microsoft.Naming", "CA1719:ParameterNamesShouldNotMatchMemberNames", MessageId = "1#", Justification = "This is a shipped API.")] + public static MvcHtmlString Name(this HtmlHelper html, string name) + { + return MvcHtmlString.Create(html.AttributeEncode(html.ViewData.TemplateInfo.GetFullHtmlFieldName(name))); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Users cannot use anonymous methods with the LambdaExpression type")] + public static MvcHtmlString NameFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression) + { + return Name(html, ExpressionHelper.GetExpressionText(expression)); + } + + public static MvcHtmlString NameForModel(this HtmlHelper html) + { + return Name(html, String.Empty); + } + } +} diff --git a/src/System.Web.Mvc/Html/PartialExtensions.cs b/src/System.Web.Mvc/Html/PartialExtensions.cs new file mode 100644 index 00000000..ee3dcc7d --- /dev/null +++ b/src/System.Web.Mvc/Html/PartialExtensions.cs @@ -0,0 +1,32 @@ +using System.Globalization; +using System.IO; + +namespace System.Web.Mvc.Html +{ + public static class PartialExtensions + { + public static MvcHtmlString Partial(this HtmlHelper htmlHelper, string partialViewName) + { + return Partial(htmlHelper, partialViewName, null /* model */, htmlHelper.ViewData); + } + + public static MvcHtmlString Partial(this HtmlHelper htmlHelper, string partialViewName, ViewDataDictionary viewData) + { + return Partial(htmlHelper, partialViewName, null /* model */, viewData); + } + + public static MvcHtmlString Partial(this HtmlHelper htmlHelper, string partialViewName, object model) + { + return Partial(htmlHelper, partialViewName, model, htmlHelper.ViewData); + } + + public static MvcHtmlString Partial(this HtmlHelper htmlHelper, string partialViewName, object model, ViewDataDictionary viewData) + { + using (StringWriter writer = new StringWriter(CultureInfo.CurrentCulture)) + { + htmlHelper.RenderPartialInternal(partialViewName, viewData, model, writer, ViewEngines.Engines); + return MvcHtmlString.Create(writer.ToString()); + } + } + } +} diff --git a/src/System.Web.Mvc/Html/RenderPartialExtensions.cs b/src/System.Web.Mvc/Html/RenderPartialExtensions.cs new file mode 100644 index 00000000..751c5dd9 --- /dev/null +++ b/src/System.Web.Mvc/Html/RenderPartialExtensions.cs @@ -0,0 +1,29 @@ +namespace System.Web.Mvc.Html +{ + public static class RenderPartialExtensions + { + // Renders the partial view with the parent's view data and model + public static void RenderPartial(this HtmlHelper htmlHelper, string partialViewName) + { + htmlHelper.RenderPartialInternal(partialViewName, htmlHelper.ViewData, null /* model */, htmlHelper.ViewContext.Writer, ViewEngines.Engines); + } + + // Renders the partial view with the given view data and, implicitly, the given view data's model + public static void RenderPartial(this HtmlHelper htmlHelper, string partialViewName, ViewDataDictionary viewData) + { + htmlHelper.RenderPartialInternal(partialViewName, viewData, null /* model */, htmlHelper.ViewContext.Writer, ViewEngines.Engines); + } + + // Renders the partial view with an empty view data and the given model + public static void RenderPartial(this HtmlHelper htmlHelper, string partialViewName, object model) + { + htmlHelper.RenderPartialInternal(partialViewName, htmlHelper.ViewData, model, htmlHelper.ViewContext.Writer, ViewEngines.Engines); + } + + // Renders the partial view with a copy of the given view data plus the given model + public static void RenderPartial(this HtmlHelper htmlHelper, string partialViewName, object model, ViewDataDictionary viewData) + { + htmlHelper.RenderPartialInternal(partialViewName, viewData, model, htmlHelper.ViewContext.Writer, ViewEngines.Engines); + } + } +} diff --git a/src/System.Web.Mvc/Html/SelectExtensions.cs b/src/System.Web.Mvc/Html/SelectExtensions.cs new file mode 100644 index 00000000..98a93f47 --- /dev/null +++ b/src/System.Web.Mvc/Html/SelectExtensions.cs @@ -0,0 +1,317 @@ +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc.Html +{ + public static class SelectExtensions + { + // DropDownList + + public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper, string name) + { + return DropDownList(htmlHelper, name, null /* selectList */, null /* optionLabel */, null /* htmlAttributes */); + } + + public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper, string name, string optionLabel) + { + return DropDownList(htmlHelper, name, null /* selectList */, optionLabel, null /* htmlAttributes */); + } + + public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList) + { + return DropDownList(htmlHelper, name, selectList, null /* optionLabel */, null /* htmlAttributes */); + } + + public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, object htmlAttributes) + { + return DropDownList(htmlHelper, name, selectList, null /* optionLabel */, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, IDictionary<string, object> htmlAttributes) + { + return DropDownList(htmlHelper, name, selectList, null /* optionLabel */, htmlAttributes); + } + + public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, string optionLabel) + { + return DropDownList(htmlHelper, name, selectList, optionLabel, null /* htmlAttributes */); + } + + public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, string optionLabel, object htmlAttributes) + { + return DropDownList(htmlHelper, name, selectList, optionLabel, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, string optionLabel, IDictionary<string, object> htmlAttributes) + { + return DropDownListHelper(htmlHelper, metadata: null, expression: name, selectList: selectList, optionLabel: optionLabel, htmlAttributes: htmlAttributes); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString DropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList) + { + return DropDownListFor(htmlHelper, expression, selectList, null /* optionLabel */, null /* htmlAttributes */); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString DropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, object htmlAttributes) + { + return DropDownListFor(htmlHelper, expression, selectList, null /* optionLabel */, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString DropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, IDictionary<string, object> htmlAttributes) + { + return DropDownListFor(htmlHelper, expression, selectList, null /* optionLabel */, htmlAttributes); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString DropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, string optionLabel) + { + return DropDownListFor(htmlHelper, expression, selectList, optionLabel, null /* htmlAttributes */); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString DropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, string optionLabel, object htmlAttributes) + { + return DropDownListFor(htmlHelper, expression, selectList, optionLabel, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Users cannot use anonymous methods with the LambdaExpression type")] + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString DropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, string optionLabel, IDictionary<string, object> htmlAttributes) + { + if (expression == null) + { + throw new ArgumentNullException("expression"); + } + + ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData); + + return DropDownListHelper(htmlHelper, metadata, ExpressionHelper.GetExpressionText(expression), selectList, optionLabel, htmlAttributes); + } + + private static MvcHtmlString DropDownListHelper(HtmlHelper htmlHelper, ModelMetadata metadata, string expression, IEnumerable<SelectListItem> selectList, string optionLabel, IDictionary<string, object> htmlAttributes) + { + return SelectInternal(htmlHelper, metadata, optionLabel, expression, selectList, allowMultiple: false, htmlAttributes: htmlAttributes); + } + + // ListBox + + public static MvcHtmlString ListBox(this HtmlHelper htmlHelper, string name) + { + return ListBox(htmlHelper, name, null /* selectList */, null /* htmlAttributes */); + } + + public static MvcHtmlString ListBox(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList) + { + return ListBox(htmlHelper, name, selectList, (IDictionary<string, object>)null); + } + + public static MvcHtmlString ListBox(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, object htmlAttributes) + { + return ListBox(htmlHelper, name, selectList, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static MvcHtmlString ListBox(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, IDictionary<string, object> htmlAttributes) + { + return ListBoxHelper(htmlHelper, metadata: null, name: name, selectList: selectList, htmlAttributes: htmlAttributes); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString ListBoxFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList) + { + return ListBoxFor(htmlHelper, expression, selectList, null /* htmlAttributes */); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString ListBoxFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, object htmlAttributes) + { + return ListBoxFor(htmlHelper, expression, selectList, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Users cannot use anonymous methods with the LambdaExpression type")] + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString ListBoxFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, IDictionary<string, object> htmlAttributes) + { + if (expression == null) + { + throw new ArgumentNullException("expression"); + } + + ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData); + + return ListBoxHelper(htmlHelper, + metadata, + ExpressionHelper.GetExpressionText(expression), + selectList, + htmlAttributes); + } + + private static MvcHtmlString ListBoxHelper(HtmlHelper htmlHelper, ModelMetadata metadata, string name, IEnumerable<SelectListItem> selectList, IDictionary<string, object> htmlAttributes) + { + return SelectInternal(htmlHelper, metadata, optionLabel: null, name: name, selectList: selectList, allowMultiple: true, htmlAttributes: htmlAttributes); + } + + // Helper methods + + private static IEnumerable<SelectListItem> GetSelectData(this HtmlHelper htmlHelper, string name) + { + object o = null; + if (htmlHelper.ViewData != null) + { + o = htmlHelper.ViewData.Eval(name); + } + if (o == null) + { + throw new InvalidOperationException( + String.Format( + CultureInfo.CurrentCulture, + MvcResources.HtmlHelper_MissingSelectData, + name, + "IEnumerable<SelectListItem>")); + } + IEnumerable<SelectListItem> selectList = o as IEnumerable<SelectListItem>; + if (selectList == null) + { + throw new InvalidOperationException( + String.Format( + CultureInfo.CurrentCulture, + MvcResources.HtmlHelper_WrongSelectDataType, + name, + o.GetType().FullName, + "IEnumerable<SelectListItem>")); + } + return selectList; + } + + internal static string ListItemToOption(SelectListItem item) + { + TagBuilder builder = new TagBuilder("option") + { + InnerHtml = HttpUtility.HtmlEncode(item.Text) + }; + if (item.Value != null) + { + builder.Attributes["value"] = item.Value; + } + if (item.Selected) + { + builder.Attributes["selected"] = "selected"; + } + return builder.ToString(TagRenderMode.Normal); + } + + private static IEnumerable<SelectListItem> GetSelectListWithDefaultValue(IEnumerable<SelectListItem> selectList, object defaultValue, bool allowMultiple) + { + IEnumerable defaultValues; + + if (allowMultiple) + { + defaultValues = defaultValue as IEnumerable; + if (defaultValues == null || defaultValues is string) + { + throw new InvalidOperationException( + String.Format( + CultureInfo.CurrentCulture, + MvcResources.HtmlHelper_SelectExpressionNotEnumerable, + "expression")); + } + } + else + { + defaultValues = new[] { defaultValue }; + } + + IEnumerable<string> values = from object value in defaultValues + select Convert.ToString(value, CultureInfo.CurrentCulture); + HashSet<string> selectedValues = new HashSet<string>(values, StringComparer.OrdinalIgnoreCase); + List<SelectListItem> newSelectList = new List<SelectListItem>(); + + foreach (SelectListItem item in selectList) + { + item.Selected = (item.Value != null) ? selectedValues.Contains(item.Value) : selectedValues.Contains(item.Text); + newSelectList.Add(item); + } + return newSelectList; + } + + private static MvcHtmlString SelectInternal(this HtmlHelper htmlHelper, ModelMetadata metadata, string optionLabel, string name, IEnumerable<SelectListItem> selectList, bool allowMultiple, IDictionary<string, object> htmlAttributes) + { + string fullName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name); + if (String.IsNullOrEmpty(fullName)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "name"); + } + + bool usedViewData = false; + + // If we got a null selectList, try to use ViewData to get the list of items. + if (selectList == null) + { + selectList = htmlHelper.GetSelectData(name); + usedViewData = true; + } + + object defaultValue = (allowMultiple) ? htmlHelper.GetModelStateValue(fullName, typeof(string[])) : htmlHelper.GetModelStateValue(fullName, typeof(string)); + + // If we haven't already used ViewData to get the entire list of items then we need to + // use the ViewData-supplied value before using the parameter-supplied value. + if (!usedViewData && defaultValue == null && !String.IsNullOrEmpty(name)) + { + defaultValue = htmlHelper.ViewData.Eval(name); + } + + if (defaultValue != null) + { + selectList = GetSelectListWithDefaultValue(selectList, defaultValue, allowMultiple); + } + + // Convert each ListItem to an <option> tag + StringBuilder listItemBuilder = new StringBuilder(); + + // Make optionLabel the first item that gets rendered. + if (optionLabel != null) + { + listItemBuilder.AppendLine(ListItemToOption(new SelectListItem() { Text = optionLabel, Value = String.Empty, Selected = false })); + } + + foreach (SelectListItem item in selectList) + { + listItemBuilder.AppendLine(ListItemToOption(item)); + } + + TagBuilder tagBuilder = new TagBuilder("select") + { + InnerHtml = listItemBuilder.ToString() + }; + tagBuilder.MergeAttributes(htmlAttributes); + tagBuilder.MergeAttribute("name", fullName, true /* replaceExisting */); + tagBuilder.GenerateId(fullName); + if (allowMultiple) + { + tagBuilder.MergeAttribute("multiple", "multiple"); + } + + // If there are any errors for a named field, we add the css attribute. + ModelState modelState; + if (htmlHelper.ViewData.ModelState.TryGetValue(fullName, out modelState)) + { + if (modelState.Errors.Count > 0) + { + tagBuilder.AddCssClass(HtmlHelper.ValidationInputCssClassName); + } + } + + tagBuilder.MergeAttributes(htmlHelper.GetUnobtrusiveValidationAttributes(name, metadata)); + + return tagBuilder.ToMvcHtmlString(TagRenderMode.Normal); + } + } +} diff --git a/src/System.Web.Mvc/Html/TemplateHelpers.cs b/src/System.Web.Mvc/Html/TemplateHelpers.cs new file mode 100644 index 00000000..26290b8c --- /dev/null +++ b/src/System.Web.Mvc/Html/TemplateHelpers.cs @@ -0,0 +1,333 @@ +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Linq.Expressions; +using System.Web.Mvc.Properties; +using System.Web.Routing; +using System.Web.UI.WebControls; + +namespace System.Web.Mvc.Html +{ + internal static class TemplateHelpers + { + private static readonly Dictionary<DataBoundControlMode, string> _modeViewPaths = + new Dictionary<DataBoundControlMode, string> + { + { DataBoundControlMode.ReadOnly, "DisplayTemplates" }, + { DataBoundControlMode.Edit, "EditorTemplates" } + }; + + private static readonly Dictionary<string, Func<HtmlHelper, string>> _defaultDisplayActions = + new Dictionary<string, Func<HtmlHelper, string>>(StringComparer.OrdinalIgnoreCase) + { + { "EmailAddress", DefaultDisplayTemplates.EmailAddressTemplate }, + { "HiddenInput", DefaultDisplayTemplates.HiddenInputTemplate }, + { "Html", DefaultDisplayTemplates.HtmlTemplate }, + { "Text", DefaultDisplayTemplates.StringTemplate }, + { "Url", DefaultDisplayTemplates.UrlTemplate }, + { "Collection", DefaultDisplayTemplates.CollectionTemplate }, + { typeof(bool).Name, DefaultDisplayTemplates.BooleanTemplate }, + { typeof(decimal).Name, DefaultDisplayTemplates.DecimalTemplate }, + { typeof(string).Name, DefaultDisplayTemplates.StringTemplate }, + { typeof(object).Name, DefaultDisplayTemplates.ObjectTemplate }, + }; + + private static readonly Dictionary<string, Func<HtmlHelper, string>> _defaultEditorActions = + new Dictionary<string, Func<HtmlHelper, string>>(StringComparer.OrdinalIgnoreCase) + { + { "HiddenInput", DefaultEditorTemplates.HiddenInputTemplate }, + { "MultilineText", DefaultEditorTemplates.MultilineTextTemplate }, + { "Password", DefaultEditorTemplates.PasswordTemplate }, + { "Text", DefaultEditorTemplates.StringTemplate }, + { "Collection", DefaultEditorTemplates.CollectionTemplate }, + { typeof(bool).Name, DefaultEditorTemplates.BooleanTemplate }, + { typeof(decimal).Name, DefaultEditorTemplates.DecimalTemplate }, + { typeof(string).Name, DefaultEditorTemplates.StringTemplate }, + { typeof(object).Name, DefaultEditorTemplates.ObjectTemplate }, + }; + + internal static string CacheItemId = Guid.NewGuid().ToString(); + + internal delegate string ExecuteTemplateDelegate(HtmlHelper html, ViewDataDictionary viewData, string templateName, + DataBoundControlMode mode, GetViewNamesDelegate getViewNames, + GetDefaultActionsDelegate getDefaultActions); + + internal delegate Dictionary<string, Func<HtmlHelper, string>> GetDefaultActionsDelegate(DataBoundControlMode mode); + + internal delegate IEnumerable<string> GetViewNamesDelegate(ModelMetadata metadata, params string[] templateHints); + + internal delegate string TemplateHelperDelegate(HtmlHelper html, ModelMetadata metadata, string htmlFieldName, + string templateName, DataBoundControlMode mode, object additionalViewData); + + internal static string ExecuteTemplate(HtmlHelper html, ViewDataDictionary viewData, string templateName, DataBoundControlMode mode, GetViewNamesDelegate getViewNames, GetDefaultActionsDelegate getDefaultActions) + { + Dictionary<string, ActionCacheItem> actionCache = GetActionCache(html); + Dictionary<string, Func<HtmlHelper, string>> defaultActions = getDefaultActions(mode); + string modeViewPath = _modeViewPaths[mode]; + + foreach (string viewName in getViewNames(viewData.ModelMetadata, templateName, viewData.ModelMetadata.TemplateHint, viewData.ModelMetadata.DataTypeName)) + { + string fullViewName = modeViewPath + "/" + viewName; + ActionCacheItem cacheItem; + + if (actionCache.TryGetValue(fullViewName, out cacheItem)) + { + if (cacheItem != null) + { + return cacheItem.Execute(html, viewData); + } + } + else + { + ViewEngineResult viewEngineResult = ViewEngines.Engines.FindPartialView(html.ViewContext, fullViewName); + if (viewEngineResult.View != null) + { + actionCache[fullViewName] = new ActionCacheViewItem { ViewName = fullViewName }; + + using (StringWriter writer = new StringWriter(CultureInfo.InvariantCulture)) + { + viewEngineResult.View.Render(new ViewContext(html.ViewContext, viewEngineResult.View, viewData, html.ViewContext.TempData, writer), writer); + return writer.ToString(); + } + } + + Func<HtmlHelper, string> defaultAction; + if (defaultActions.TryGetValue(viewName, out defaultAction)) + { + actionCache[fullViewName] = new ActionCacheCodeItem { Action = defaultAction }; + return defaultAction(MakeHtmlHelper(html, viewData)); + } + + actionCache[fullViewName] = null; + } + } + + throw new InvalidOperationException( + String.Format( + CultureInfo.CurrentCulture, + MvcResources.TemplateHelpers_NoTemplate, + viewData.ModelMetadata.RealModelType.FullName)); + } + + internal static Dictionary<string, ActionCacheItem> GetActionCache(HtmlHelper html) + { + HttpContextBase context = html.ViewContext.HttpContext; + Dictionary<string, ActionCacheItem> result; + + if (!context.Items.Contains(CacheItemId)) + { + result = new Dictionary<string, ActionCacheItem>(); + context.Items[CacheItemId] = result; + } + else + { + result = (Dictionary<string, ActionCacheItem>)context.Items[CacheItemId]; + } + + return result; + } + + internal static Dictionary<string, Func<HtmlHelper, string>> GetDefaultActions(DataBoundControlMode mode) + { + return mode == DataBoundControlMode.ReadOnly ? _defaultDisplayActions : _defaultEditorActions; + } + + internal static IEnumerable<string> GetViewNames(ModelMetadata metadata, params string[] templateHints) + { + foreach (string templateHint in templateHints.Where(s => !String.IsNullOrEmpty(s))) + { + yield return templateHint; + } + + // We don't want to search for Nullable<T>, we want to search for T (which should handle both T and Nullable<T>) + Type fieldType = Nullable.GetUnderlyingType(metadata.RealModelType) ?? metadata.RealModelType; + + // TODO: Make better string names for generic types + yield return fieldType.Name; + + if (!metadata.IsComplexType) + { + yield return "String"; + } + else if (fieldType.IsInterface) + { + if (typeof(IEnumerable).IsAssignableFrom(fieldType)) + { + yield return "Collection"; + } + + yield return "Object"; + } + else + { + bool isEnumerable = typeof(IEnumerable).IsAssignableFrom(fieldType); + + while (true) + { + fieldType = fieldType.BaseType; + if (fieldType == null) + { + break; + } + + if (isEnumerable && fieldType == typeof(Object)) + { + yield return "Collection"; + } + + yield return fieldType.Name; + } + } + } + + internal static MvcHtmlString Template(HtmlHelper html, string expression, string templateName, string htmlFieldName, DataBoundControlMode mode, object additionalViewData) + { + return MvcHtmlString.Create(Template(html, expression, templateName, htmlFieldName, mode, additionalViewData, TemplateHelper)); + } + + // Unit testing version + internal static string Template(HtmlHelper html, string expression, string templateName, string htmlFieldName, + DataBoundControlMode mode, object additionalViewData, TemplateHelperDelegate templateHelper) + { + return templateHelper(html, + ModelMetadata.FromStringExpression(expression, html.ViewData), + htmlFieldName ?? ExpressionHelper.GetExpressionText(expression), + templateName, + mode, + additionalViewData); + } + + internal static MvcHtmlString TemplateFor<TContainer, TValue>(this HtmlHelper<TContainer> html, Expression<Func<TContainer, TValue>> expression, + string templateName, string htmlFieldName, DataBoundControlMode mode, + object additionalViewData) + { + return MvcHtmlString.Create(TemplateFor(html, expression, templateName, htmlFieldName, mode, additionalViewData, TemplateHelper)); + } + + // Unit testing version + internal static string TemplateFor<TContainer, TValue>(this HtmlHelper<TContainer> html, Expression<Func<TContainer, TValue>> expression, + string templateName, string htmlFieldName, DataBoundControlMode mode, + object additionalViewData, TemplateHelperDelegate templateHelper) + { + return templateHelper(html, + ModelMetadata.FromLambdaExpression(expression, html.ViewData), + htmlFieldName ?? ExpressionHelper.GetExpressionText(expression), + templateName, + mode, + additionalViewData); + } + + internal static string TemplateHelper(HtmlHelper html, ModelMetadata metadata, string htmlFieldName, string templateName, DataBoundControlMode mode, object additionalViewData) + { + return TemplateHelper(html, metadata, htmlFieldName, templateName, mode, additionalViewData, ExecuteTemplate); + } + + internal static string TemplateHelper(HtmlHelper html, ModelMetadata metadata, string htmlFieldName, string templateName, DataBoundControlMode mode, object additionalViewData, ExecuteTemplateDelegate executeTemplate) + { + // TODO: Convert Editor into Display if model.IsReadOnly is true? Need to be careful about this because + // the Model property on the ViewPage/ViewUserControl is get-only, so the type descriptor automatically + // decorates it with a [ReadOnly] attribute... + + if (metadata.ConvertEmptyStringToNull && String.Empty.Equals(metadata.Model)) + { + metadata.Model = null; + } + + object formattedModelValue = metadata.Model; + if (metadata.Model == null && mode == DataBoundControlMode.ReadOnly) + { + formattedModelValue = metadata.NullDisplayText; + } + + string formatString = mode == DataBoundControlMode.ReadOnly ? metadata.DisplayFormatString : metadata.EditFormatString; + if (metadata.Model != null && !String.IsNullOrEmpty(formatString)) + { + formattedModelValue = String.Format(CultureInfo.CurrentCulture, formatString, metadata.Model); + } + + // Normally this shouldn't happen, unless someone writes their own custom Object templates which + // don't check to make sure that the object hasn't already been displayed + object visitedObjectsKey = metadata.Model ?? metadata.RealModelType; + if (html.ViewDataContainer.ViewData.TemplateInfo.VisitedObjects.Contains(visitedObjectsKey)) + { + // DDB #224750 + return String.Empty; + } + + ViewDataDictionary viewData = new ViewDataDictionary(html.ViewDataContainer.ViewData) + { + Model = metadata.Model, + ModelMetadata = metadata, + TemplateInfo = new TemplateInfo + { + FormattedModelValue = formattedModelValue, + HtmlFieldPrefix = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(htmlFieldName), + VisitedObjects = new HashSet<object>(html.ViewContext.ViewData.TemplateInfo.VisitedObjects), // DDB #224750 + } + }; + + if (additionalViewData != null) + { + foreach (KeyValuePair<string, object> kvp in new RouteValueDictionary(additionalViewData)) + { + viewData[kvp.Key] = kvp.Value; + } + } + + viewData.TemplateInfo.VisitedObjects.Add(visitedObjectsKey); // DDB #224750 + + return executeTemplate(html, viewData, templateName, mode, GetViewNames, GetDefaultActions); + } + + // Helpers + + private static HtmlHelper MakeHtmlHelper(HtmlHelper html, ViewDataDictionary viewData) + { + return new HtmlHelper( + new ViewContext(html.ViewContext, html.ViewContext.View, viewData, html.ViewContext.TempData, html.ViewContext.Writer), + new ViewDataContainer(viewData)); + } + + internal class ActionCacheCodeItem : ActionCacheItem + { + public Func<HtmlHelper, string> Action { get; set; } + + public override string Execute(HtmlHelper html, ViewDataDictionary viewData) + { + return Action(MakeHtmlHelper(html, viewData)); + } + } + + internal abstract class ActionCacheItem + { + public abstract string Execute(HtmlHelper html, ViewDataDictionary viewData); + } + + internal class ActionCacheViewItem : ActionCacheItem + { + public string ViewName { get; set; } + + public override string Execute(HtmlHelper html, ViewDataDictionary viewData) + { + ViewEngineResult viewEngineResult = ViewEngines.Engines.FindPartialView(html.ViewContext, ViewName); + using (StringWriter writer = new StringWriter(CultureInfo.InvariantCulture)) + { + viewEngineResult.View.Render(new ViewContext(html.ViewContext, viewEngineResult.View, viewData, html.ViewContext.TempData, writer), writer); + return writer.ToString(); + } + } + } + + private class ViewDataContainer : IViewDataContainer + { + public ViewDataContainer(ViewDataDictionary viewData) + { + ViewData = viewData; + } + + public ViewDataDictionary ViewData { get; set; } + } + } +} diff --git a/src/System.Web.Mvc/Html/TextAreaExtensions.cs b/src/System.Web.Mvc/Html/TextAreaExtensions.cs new file mode 100644 index 00000000..490a76e7 --- /dev/null +++ b/src/System.Web.Mvc/Html/TextAreaExtensions.cs @@ -0,0 +1,192 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq.Expressions; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc.Html +{ + public static class TextAreaExtensions + { + // These values are similar to the defaults used by WebForms + // when using <asp:TextBox TextMode="MultiLine"> without specifying + // the Rows and Columns attributes. + private const int TextAreaRows = 2; + private const int TextAreaColumns = 20; + + private static Dictionary<string, object> implicitRowsAndColumns = new Dictionary<string, object> + { + { "rows", TextAreaRows.ToString(CultureInfo.InvariantCulture) }, + { "cols", TextAreaColumns.ToString(CultureInfo.InvariantCulture) }, + }; + + private static Dictionary<string, object> GetRowsAndColumnsDictionary(int rows, int columns) + { + if (rows < 0) + { + throw new ArgumentOutOfRangeException("rows", MvcResources.HtmlHelper_TextAreaParameterOutOfRange); + } + if (columns < 0) + { + throw new ArgumentOutOfRangeException("columns", MvcResources.HtmlHelper_TextAreaParameterOutOfRange); + } + + Dictionary<string, object> result = new Dictionary<string, object>(); + if (rows > 0) + { + result.Add("rows", rows.ToString(CultureInfo.InvariantCulture)); + } + if (columns > 0) + { + result.Add("cols", columns.ToString(CultureInfo.InvariantCulture)); + } + + return result; + } + + public static MvcHtmlString TextArea(this HtmlHelper htmlHelper, string name) + { + return TextArea(htmlHelper, name, null /* value */, null /* htmlAttributes */); + } + + public static MvcHtmlString TextArea(this HtmlHelper htmlHelper, string name, object htmlAttributes) + { + return TextArea(htmlHelper, name, null /* value */, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static MvcHtmlString TextArea(this HtmlHelper htmlHelper, string name, IDictionary<string, object> htmlAttributes) + { + return TextArea(htmlHelper, name, null /* value */, htmlAttributes); + } + + public static MvcHtmlString TextArea(this HtmlHelper htmlHelper, string name, string value) + { + return TextArea(htmlHelper, name, value, null /* htmlAttributes */); + } + + public static MvcHtmlString TextArea(this HtmlHelper htmlHelper, string name, string value, object htmlAttributes) + { + return TextArea(htmlHelper, name, value, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static MvcHtmlString TextArea(this HtmlHelper htmlHelper, string name, string value, IDictionary<string, object> htmlAttributes) + { + ModelMetadata metadata = ModelMetadata.FromStringExpression(name, htmlHelper.ViewContext.ViewData); + if (value != null) + { + metadata.Model = value; + } + + return TextAreaHelper(htmlHelper, metadata, name, implicitRowsAndColumns, htmlAttributes); + } + + public static MvcHtmlString TextArea(this HtmlHelper htmlHelper, string name, string value, int rows, int columns, object htmlAttributes) + { + return TextArea(htmlHelper, name, value, rows, columns, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static MvcHtmlString TextArea(this HtmlHelper htmlHelper, string name, string value, int rows, int columns, IDictionary<string, object> htmlAttributes) + { + ModelMetadata metadata = ModelMetadata.FromStringExpression(name, htmlHelper.ViewContext.ViewData); + if (value != null) + { + metadata.Model = value; + } + + return TextAreaHelper(htmlHelper, metadata, name, GetRowsAndColumnsDictionary(rows, columns), htmlAttributes); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString TextAreaFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression) + { + return TextAreaFor(htmlHelper, expression, (IDictionary<string, object>)null); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString TextAreaFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object htmlAttributes) + { + return TextAreaFor(htmlHelper, expression, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString TextAreaFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IDictionary<string, object> htmlAttributes) + { + if (expression == null) + { + throw new ArgumentNullException("expression"); + } + + return TextAreaHelper(htmlHelper, + ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData), + ExpressionHelper.GetExpressionText(expression), + implicitRowsAndColumns, + htmlAttributes); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString TextAreaFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, int rows, int columns, object htmlAttributes) + { + return TextAreaFor(htmlHelper, expression, rows, columns, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString TextAreaFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, int rows, int columns, IDictionary<string, object> htmlAttributes) + { + if (expression == null) + { + throw new ArgumentNullException("expression"); + } + + return TextAreaHelper(htmlHelper, + ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData), + ExpressionHelper.GetExpressionText(expression), + GetRowsAndColumnsDictionary(rows, columns), + htmlAttributes); + } + + [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Justification = "If this fails, it is because the string-based version had an empty 'name' parameter")] + internal static MvcHtmlString TextAreaHelper(HtmlHelper htmlHelper, ModelMetadata modelMetadata, string name, IDictionary<string, object> rowsAndColumns, IDictionary<string, object> htmlAttributes, string innerHtmlPrefix = null) + { + string fullName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name); + if (String.IsNullOrEmpty(fullName)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "name"); + } + + TagBuilder tagBuilder = new TagBuilder("textarea"); + tagBuilder.GenerateId(fullName); + tagBuilder.MergeAttributes(htmlAttributes, true); + tagBuilder.MergeAttributes(rowsAndColumns, rowsAndColumns != implicitRowsAndColumns); // Only force explicit rows/cols + tagBuilder.MergeAttribute("name", fullName, true); + + // If there are any errors for a named field, we add the CSS attribute. + ModelState modelState; + if (htmlHelper.ViewData.ModelState.TryGetValue(fullName, out modelState) && modelState.Errors.Count > 0) + { + tagBuilder.AddCssClass(HtmlHelper.ValidationInputCssClassName); + } + + tagBuilder.MergeAttributes(htmlHelper.GetUnobtrusiveValidationAttributes(name)); + + string value; + if (modelState != null && modelState.Value != null) + { + value = modelState.Value.AttemptedValue; + } + else if (modelMetadata.Model != null) + { + value = modelMetadata.Model.ToString(); + } + else + { + value = String.Empty; + } + + // The first newline is always trimmed when a TextArea is rendered, so we add an extra one + // in case the value being rendered is something like "\r\nHello". + tagBuilder.InnerHtml = (innerHtmlPrefix ?? Environment.NewLine) + HttpUtility.HtmlEncode(value); + + return tagBuilder.ToMvcHtmlString(TagRenderMode.Normal); + } + } +} diff --git a/src/System.Web.Mvc/Html/ValidationExtensions.cs b/src/System.Web.Mvc/Html/ValidationExtensions.cs new file mode 100644 index 00000000..b225110e --- /dev/null +++ b/src/System.Web.Mvc/Html/ValidationExtensions.cs @@ -0,0 +1,390 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Web.Mvc.Properties; +using System.Web.Routing; + +namespace System.Web.Mvc.Html +{ + public static class ValidationExtensions + { + private const string HiddenListItem = @"<li style=""display:none""></li>"; + private static string _resourceClassKey; + + public static string ResourceClassKey + { + get { return _resourceClassKey ?? String.Empty; } + set { _resourceClassKey = value; } + } + + private static FieldValidationMetadata ApplyFieldValidationMetadata(HtmlHelper htmlHelper, ModelMetadata modelMetadata, string modelName) + { + FormContext formContext = htmlHelper.ViewContext.FormContext; + FieldValidationMetadata fieldMetadata = formContext.GetValidationMetadataForField(modelName, true /* createIfNotFound */); + + // write rules to context object + IEnumerable<ModelValidator> validators = ModelValidatorProviders.Providers.GetValidators(modelMetadata, htmlHelper.ViewContext); + foreach (ModelClientValidationRule rule in validators.SelectMany(v => v.GetClientValidationRules())) + { + fieldMetadata.ValidationRules.Add(rule); + } + + return fieldMetadata; + } + + private static string GetInvalidPropertyValueResource(HttpContextBase httpContext) + { + string resourceValue = null; + if (!String.IsNullOrEmpty(ResourceClassKey) && (httpContext != null)) + { + // If the user specified a ResourceClassKey try to load the resource they specified. + // If the class key is invalid, an exception will be thrown. + // If the class key is valid but the resource is not found, it returns null, in which + // case it will fall back to the MVC default error message. + resourceValue = httpContext.GetGlobalResourceObject(ResourceClassKey, "InvalidPropertyValue", CultureInfo.CurrentUICulture) as string; + } + return resourceValue ?? MvcResources.Common_ValueNotValidForProperty; + } + + private static string GetUserErrorMessageOrDefault(HttpContextBase httpContext, ModelError error, ModelState modelState) + { + if (!String.IsNullOrEmpty(error.ErrorMessage)) + { + return error.ErrorMessage; + } + if (modelState == null) + { + return null; + } + + string attemptedValue = (modelState.Value != null) ? modelState.Value.AttemptedValue : null; + return String.Format(CultureInfo.CurrentCulture, GetInvalidPropertyValueResource(httpContext), attemptedValue); + } + + // Validate + + public static void Validate(this HtmlHelper htmlHelper, string modelName) + { + if (modelName == null) + { + throw new ArgumentNullException("modelName"); + } + + ValidateHelper(htmlHelper, + ModelMetadata.FromStringExpression(modelName, htmlHelper.ViewContext.ViewData), + modelName); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static void ValidateFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression) + { + ValidateHelper(htmlHelper, + ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData), + ExpressionHelper.GetExpressionText(expression)); + } + + private static void ValidateHelper(HtmlHelper htmlHelper, ModelMetadata modelMetadata, string expression) + { + FormContext formContext = htmlHelper.ViewContext.GetFormContextForClientValidation(); + if (formContext == null || htmlHelper.ViewContext.UnobtrusiveJavaScriptEnabled) + { + return; // nothing to do + } + + string modelName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(expression); + ApplyFieldValidationMetadata(htmlHelper, modelMetadata, modelName); + } + + // ValidationMessage + + public static MvcHtmlString ValidationMessage(this HtmlHelper htmlHelper, string modelName) + { + return ValidationMessage(htmlHelper, modelName, null /* validationMessage */, new RouteValueDictionary()); + } + + public static MvcHtmlString ValidationMessage(this HtmlHelper htmlHelper, string modelName, object htmlAttributes) + { + return ValidationMessage(htmlHelper, modelName, null /* validationMessage */, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + [SuppressMessage("Microsoft.Naming", "CA1719:ParameterNamesShouldNotMatchMemberNames", Justification = "'validationMessage' refers to the message that will be rendered by the ValidationMessage helper.")] + public static MvcHtmlString ValidationMessage(this HtmlHelper htmlHelper, string modelName, string validationMessage) + { + return ValidationMessage(htmlHelper, modelName, validationMessage, new RouteValueDictionary()); + } + + [SuppressMessage("Microsoft.Naming", "CA1719:ParameterNamesShouldNotMatchMemberNames", Justification = "'validationMessage' refers to the message that will be rendered by the ValidationMessage helper.")] + public static MvcHtmlString ValidationMessage(this HtmlHelper htmlHelper, string modelName, string validationMessage, object htmlAttributes) + { + return ValidationMessage(htmlHelper, modelName, validationMessage, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static MvcHtmlString ValidationMessage(this HtmlHelper htmlHelper, string modelName, IDictionary<string, object> htmlAttributes) + { + return ValidationMessage(htmlHelper, modelName, null /* validationMessage */, htmlAttributes); + } + + [SuppressMessage("Microsoft.Naming", "CA1719:ParameterNamesShouldNotMatchMemberNames", Justification = "'validationMessage' refers to the message that will be rendered by the ValidationMessage helper.")] + public static MvcHtmlString ValidationMessage(this HtmlHelper htmlHelper, string modelName, string validationMessage, IDictionary<string, object> htmlAttributes) + { + if (modelName == null) + { + throw new ArgumentNullException("modelName"); + } + + return ValidationMessageHelper(htmlHelper, + ModelMetadata.FromStringExpression(modelName, htmlHelper.ViewContext.ViewData), + modelName, + validationMessage, + htmlAttributes); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString ValidationMessageFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression) + { + return ValidationMessageFor(htmlHelper, expression, null /* validationMessage */, new RouteValueDictionary()); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString ValidationMessageFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string validationMessage) + { + return ValidationMessageFor(htmlHelper, expression, validationMessage, new RouteValueDictionary()); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString ValidationMessageFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string validationMessage, object htmlAttributes) + { + return ValidationMessageFor(htmlHelper, expression, validationMessage, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString ValidationMessageFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string validationMessage, IDictionary<string, object> htmlAttributes) + { + return ValidationMessageHelper(htmlHelper, + ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData), + ExpressionHelper.GetExpressionText(expression), + validationMessage, + htmlAttributes); + } + + [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "Normalization to lowercase is a common requirement for JavaScript and HTML values")] + private static MvcHtmlString ValidationMessageHelper(this HtmlHelper htmlHelper, ModelMetadata modelMetadata, string expression, string validationMessage, IDictionary<string, object> htmlAttributes) + { + string modelName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(expression); + FormContext formContext = htmlHelper.ViewContext.GetFormContextForClientValidation(); + + if (!htmlHelper.ViewData.ModelState.ContainsKey(modelName) && formContext == null) + { + return null; + } + + ModelState modelState = htmlHelper.ViewData.ModelState[modelName]; + ModelErrorCollection modelErrors = (modelState == null) ? null : modelState.Errors; + ModelError modelError = (((modelErrors == null) || (modelErrors.Count == 0)) ? null : modelErrors.FirstOrDefault(m => !String.IsNullOrEmpty(m.ErrorMessage)) ?? modelErrors[0]); + + if (modelError == null && formContext == null) + { + return null; + } + + TagBuilder builder = new TagBuilder("span"); + builder.MergeAttributes(htmlAttributes); + builder.AddCssClass((modelError != null) ? HtmlHelper.ValidationMessageCssClassName : HtmlHelper.ValidationMessageValidCssClassName); + + if (!String.IsNullOrEmpty(validationMessage)) + { + builder.SetInnerText(validationMessage); + } + else if (modelError != null) + { + builder.SetInnerText(GetUserErrorMessageOrDefault(htmlHelper.ViewContext.HttpContext, modelError, modelState)); + } + + if (formContext != null) + { + bool replaceValidationMessageContents = String.IsNullOrEmpty(validationMessage); + + if (htmlHelper.ViewContext.UnobtrusiveJavaScriptEnabled) + { + builder.MergeAttribute("data-valmsg-for", modelName); + builder.MergeAttribute("data-valmsg-replace", replaceValidationMessageContents.ToString().ToLowerInvariant()); + } + else + { + FieldValidationMetadata fieldMetadata = ApplyFieldValidationMetadata(htmlHelper, modelMetadata, modelName); + // rules will already have been written to the metadata object + fieldMetadata.ReplaceValidationMessageContents = replaceValidationMessageContents; // only replace contents if no explicit message was specified + + // client validation always requires an ID + builder.GenerateId(modelName + "_validationMessage"); + fieldMetadata.ValidationMessageId = builder.Attributes["id"]; + } + } + + return builder.ToMvcHtmlString(TagRenderMode.Normal); + } + + // ValidationSummary + + public static MvcHtmlString ValidationSummary(this HtmlHelper htmlHelper) + { + return ValidationSummary(htmlHelper, false /* excludePropertyErrors */); + } + + public static MvcHtmlString ValidationSummary(this HtmlHelper htmlHelper, bool excludePropertyErrors) + { + return ValidationSummary(htmlHelper, excludePropertyErrors, null /* message */); + } + + public static MvcHtmlString ValidationSummary(this HtmlHelper htmlHelper, string message) + { + return ValidationSummary(htmlHelper, false /* excludePropertyErrors */, message, (object)null /* htmlAttributes */); + } + + public static MvcHtmlString ValidationSummary(this HtmlHelper htmlHelper, bool excludePropertyErrors, string message) + { + return ValidationSummary(htmlHelper, excludePropertyErrors, message, (object)null /* htmlAttributes */); + } + + public static MvcHtmlString ValidationSummary(this HtmlHelper htmlHelper, string message, object htmlAttributes) + { + return ValidationSummary(htmlHelper, false /* excludePropertyErrors */, message, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static MvcHtmlString ValidationSummary(this HtmlHelper htmlHelper, bool excludePropertyErrors, string message, object htmlAttributes) + { + return ValidationSummary(htmlHelper, excludePropertyErrors, message, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static MvcHtmlString ValidationSummary(this HtmlHelper htmlHelper, string message, IDictionary<string, object> htmlAttributes) + { + return ValidationSummary(htmlHelper, false /* excludePropertyErrors */, message, htmlAttributes); + } + + public static MvcHtmlString ValidationSummary(this HtmlHelper htmlHelper, bool excludePropertyErrors, string message, IDictionary<string, object> htmlAttributes) + { + if (htmlHelper == null) + { + throw new ArgumentNullException("htmlHelper"); + } + + FormContext formContext = htmlHelper.ViewContext.GetFormContextForClientValidation(); + if (htmlHelper.ViewData.ModelState.IsValid) + { + if (formContext == null) + { + // No client side validation + return null; + } + // TODO: This isn't really about unobtrusive; can we fix up non-unobtrusive to get rid of this, too? + if (htmlHelper.ViewContext.UnobtrusiveJavaScriptEnabled && excludePropertyErrors) + { + // No client-side updates + return null; + } + } + + string messageSpan; + if (!String.IsNullOrEmpty(message)) + { + TagBuilder spanTag = new TagBuilder("span"); + spanTag.SetInnerText(message); + messageSpan = spanTag.ToString(TagRenderMode.Normal) + Environment.NewLine; + } + else + { + messageSpan = null; + } + + StringBuilder htmlSummary = new StringBuilder(); + TagBuilder unorderedList = new TagBuilder("ul"); + + IEnumerable<ModelState> modelStates = GetModelStateList(htmlHelper, excludePropertyErrors); + + foreach (ModelState modelState in modelStates) + { + foreach (ModelError modelError in modelState.Errors) + { + string errorText = GetUserErrorMessageOrDefault(htmlHelper.ViewContext.HttpContext, modelError, null /* modelState */); + if (!String.IsNullOrEmpty(errorText)) + { + TagBuilder listItem = new TagBuilder("li"); + listItem.SetInnerText(errorText); + htmlSummary.AppendLine(listItem.ToString(TagRenderMode.Normal)); + } + } + } + + if (htmlSummary.Length == 0) + { + htmlSummary.AppendLine(HiddenListItem); + } + + unorderedList.InnerHtml = htmlSummary.ToString(); + + TagBuilder divBuilder = new TagBuilder("div"); + divBuilder.MergeAttributes(htmlAttributes); + divBuilder.AddCssClass((htmlHelper.ViewData.ModelState.IsValid) ? HtmlHelper.ValidationSummaryValidCssClassName : HtmlHelper.ValidationSummaryCssClassName); + divBuilder.InnerHtml = messageSpan + unorderedList.ToString(TagRenderMode.Normal); + + if (formContext != null) + { + if (htmlHelper.ViewContext.UnobtrusiveJavaScriptEnabled) + { + if (!excludePropertyErrors) + { + // Only put errors in the validation summary if they're supposed to be included there + divBuilder.MergeAttribute("data-valmsg-summary", "true"); + } + } + else + { + // client val summaries need an ID + divBuilder.GenerateId("validationSummary"); + formContext.ValidationSummaryId = divBuilder.Attributes["id"]; + formContext.ReplaceValidationSummary = !excludePropertyErrors; + } + } + return divBuilder.ToMvcHtmlString(TagRenderMode.Normal); + } + + // Returns non-null list of model states, which caller will render in order provided. + private static IEnumerable<ModelState> GetModelStateList(HtmlHelper htmlHelper, bool excludePropertyErrors) + { + if (excludePropertyErrors) + { + ModelState ms; + htmlHelper.ViewData.ModelState.TryGetValue(htmlHelper.ViewData.TemplateInfo.HtmlFieldPrefix, out ms); + if (ms != null) + { + return new ModelState[] { ms }; + } + + return new ModelState[0]; + } + else + { + // Sort modelStates to respect the ordering in the metadata. + // ModelState doesn't refer to ModelMetadata, but we can correlate via the property name. + Dictionary<string, int> ordering = new Dictionary<string, int>(); + + var metadata = htmlHelper.ViewData.ModelMetadata; + if (metadata != null) + { + foreach (ModelMetadata m in metadata.Properties) + { + ordering[m.PropertyName] = m.Order; + } + } + + return from kv in htmlHelper.ViewData.ModelState + let name = kv.Key + orderby ordering.GetOrDefault(name, ModelMetadata.DefaultOrder) + select kv.Value; + } + } + } +} diff --git a/src/System.Web.Mvc/Html/ValueExtensions.cs b/src/System.Web.Mvc/Html/ValueExtensions.cs new file mode 100644 index 00000000..53e297c7 --- /dev/null +++ b/src/System.Web.Mvc/Html/ValueExtensions.cs @@ -0,0 +1,80 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; + +namespace System.Web.Mvc.Html +{ + public static class ValueExtensions + { + public static MvcHtmlString Value(this HtmlHelper html, string name) + { + return Value(html, name, format: null); + } + + public static MvcHtmlString Value(this HtmlHelper html, string name, string format) + { + if (name == null) + { + throw new ArgumentNullException("name"); + } + + return ValueForHelper(html, name, value: null, format: format, useViewData: true); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString ValueFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression) + { + return ValueFor(html, expression, format: null); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static MvcHtmlString ValueFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression, string format) + { + ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData); + return ValueForHelper(html, ExpressionHelper.GetExpressionText(expression), metadata.Model, format, useViewData: false); + } + + public static MvcHtmlString ValueForModel(this HtmlHelper html) + { + return ValueForModel(html, format: null); + } + + public static MvcHtmlString ValueForModel(this HtmlHelper html, string format) + { + return Value(html, String.Empty, format); + } + + internal static MvcHtmlString ValueForHelper(HtmlHelper html, string name, object value, string format, bool useViewData) + { + string fullName = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name); + string attemptedValue = (string)html.GetModelStateValue(fullName, typeof(string)); + string resolvedValue; + + if (attemptedValue != null) + { + // case 1: if ModelState has a value then it's already formatted so ignore format string + resolvedValue = attemptedValue; + } + else if (useViewData) + { + if (name.Length == 0) + { + // case 2(a): format the value from ModelMetadata for the current model + ModelMetadata metadata = ModelMetadata.FromStringExpression(String.Empty, html.ViewContext.ViewData); + resolvedValue = html.FormatValue(metadata.Model, format); + } + else + { + // case 2(b): format the value from ViewData + resolvedValue = html.EvalString(name, format); + } + } + else + { + // case 3: format the explicit value from ModelMetadata + resolvedValue = html.FormatValue(value, format); + } + + return MvcHtmlString.Create(html.AttributeEncode(resolvedValue)); + } + } +} diff --git a/src/System.Web.Mvc/HtmlHelper.cs b/src/System.Web.Mvc/HtmlHelper.cs new file mode 100644 index 00000000..c6ab7021 --- /dev/null +++ b/src/System.Web.Mvc/HtmlHelper.cs @@ -0,0 +1,451 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Web.Helpers; +using System.Web.Mvc.Properties; +using System.Web.Routing; + +namespace System.Web.Mvc +{ + public class HtmlHelper + { + public static readonly string ValidationInputCssClassName = "input-validation-error"; + public static readonly string ValidationInputValidCssClassName = "input-validation-valid"; + public static readonly string ValidationMessageCssClassName = "field-validation-error"; + public static readonly string ValidationMessageValidCssClassName = "field-validation-valid"; + public static readonly string ValidationSummaryCssClassName = "validation-summary-errors"; + public static readonly string ValidationSummaryValidCssClassName = "validation-summary-valid"; + + private DynamicViewDataDictionary _dynamicViewDataDictionary; + + public HtmlHelper(ViewContext viewContext, IViewDataContainer viewDataContainer) + : this(viewContext, viewDataContainer, RouteTable.Routes) + { + } + + public HtmlHelper(ViewContext viewContext, IViewDataContainer viewDataContainer, RouteCollection routeCollection) + { + if (viewContext == null) + { + throw new ArgumentNullException("viewContext"); + } + if (viewDataContainer == null) + { + throw new ArgumentNullException("viewDataContainer"); + } + if (routeCollection == null) + { + throw new ArgumentNullException("routeCollection"); + } + + ViewContext = viewContext; + ViewDataContainer = viewDataContainer; + RouteCollection = routeCollection; + ClientValidationRuleFactory = (name, metadata) => ModelValidatorProviders.Providers.GetValidators(metadata ?? ModelMetadata.FromStringExpression(name, ViewData), ViewContext).SelectMany(v => v.GetClientValidationRules()); + } + + public static bool ClientValidationEnabled + { + get { return ViewContext.GetClientValidationEnabled(); } + set { ViewContext.SetClientValidationEnabled(value); } + } + + public static string IdAttributeDotReplacement + { + get { return WebPages.Html.HtmlHelper.IdAttributeDotReplacement; } + set { WebPages.Html.HtmlHelper.IdAttributeDotReplacement = value; } + } + + internal Func<string, ModelMetadata, IEnumerable<ModelClientValidationRule>> ClientValidationRuleFactory { get; set; } + + public RouteCollection RouteCollection { get; private set; } + + public static bool UnobtrusiveJavaScriptEnabled + { + get { return ViewContext.GetUnobtrusiveJavaScriptEnabled(); } + set { ViewContext.SetUnobtrusiveJavaScriptEnabled(value); } + } + + public dynamic ViewBag + { + get + { + if (_dynamicViewDataDictionary == null) + { + _dynamicViewDataDictionary = new DynamicViewDataDictionary(() => ViewData); + } + return _dynamicViewDataDictionary; + } + } + + public ViewContext ViewContext { get; private set; } + + public ViewDataDictionary ViewData + { + get { return ViewDataContainer.ViewData; } + } + + public IViewDataContainer ViewDataContainer { get; internal set; } + + public static RouteValueDictionary AnonymousObjectToHtmlAttributes(object htmlAttributes) + { + RouteValueDictionary result = new RouteValueDictionary(); + + if (htmlAttributes != null) + { + foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(htmlAttributes)) + { + result.Add(property.Name.Replace('_', '-'), property.GetValue(htmlAttributes)); + } + } + + return result; + } + + public MvcHtmlString AntiForgeryToken() + { + return AntiForgeryToken(salt: null); + } + + public MvcHtmlString AntiForgeryToken(string salt) + { + return AntiForgeryToken(salt, domain: null, path: null); + } + + public MvcHtmlString AntiForgeryToken(string salt, string domain, string path) + { + return new MvcHtmlString(AntiForgery.GetHtml(ViewContext.HttpContext, salt, domain, path).ToString()); + } + + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "For consistency, all helpers are instance methods.")] + public string AttributeEncode(string value) + { + return (!String.IsNullOrEmpty(value)) ? HttpUtility.HtmlAttributeEncode(value) : String.Empty; + } + + public string AttributeEncode(object value) + { + return AttributeEncode(Convert.ToString(value, CultureInfo.InvariantCulture)); + } + + public void EnableClientValidation() + { + EnableClientValidation(enabled: true); + } + + public void EnableClientValidation(bool enabled) + { + ViewContext.ClientValidationEnabled = enabled; + } + + public void EnableUnobtrusiveJavaScript() + { + EnableUnobtrusiveJavaScript(enabled: true); + } + + public void EnableUnobtrusiveJavaScript(bool enabled) + { + ViewContext.UnobtrusiveJavaScriptEnabled = enabled; + } + + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "For consistency, all helpers are instance methods.")] + public string Encode(string value) + { + return (!String.IsNullOrEmpty(value)) ? HttpUtility.HtmlEncode(value) : String.Empty; + } + + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "For consistency, all helpers are instance methods.")] + public string Encode(object value) + { + return value != null ? HttpUtility.HtmlEncode(value) : String.Empty; + } + + internal string EvalString(string key) + { + return Convert.ToString(ViewData.Eval(key), CultureInfo.CurrentCulture); + } + + internal string EvalString(string key, string format) + { + return Convert.ToString(ViewData.Eval(key, format), CultureInfo.CurrentCulture); + } + + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "For consistency, all helpers are instance methods.")] + public string FormatValue(object value, string format) + { + return ViewDataDictionary.FormatValueInternal(value, format); + } + + internal bool EvalBoolean(string key) + { + return Convert.ToBoolean(ViewData.Eval(key), CultureInfo.InvariantCulture); + } + + internal static IView FindPartialView(ViewContext viewContext, string partialViewName, ViewEngineCollection viewEngineCollection) + { + ViewEngineResult result = viewEngineCollection.FindPartialView(viewContext, partialViewName); + if (result.View != null) + { + return result.View; + } + + StringBuilder locationsText = new StringBuilder(); + foreach (string location in result.SearchedLocations) + { + locationsText.AppendLine(); + locationsText.Append(location); + } + + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, + MvcResources.Common_PartialViewNotFound, partialViewName, locationsText)); + } + + public static string GenerateIdFromName(string name) + { + return GenerateIdFromName(name, IdAttributeDotReplacement); + } + + public static string GenerateIdFromName(string name, string idAttributeDotReplacement) + { + if (name == null) + { + throw new ArgumentNullException("name"); + } + + if (idAttributeDotReplacement == null) + { + throw new ArgumentNullException("idAttributeDotReplacement"); + } + + // TagBuilder.CreateSanitizedId returns null for empty strings, return String.Empty instead to avoid breaking change + if (name.Length == 0) + { + return String.Empty; + } + + return TagBuilder.CreateSanitizedId(name, idAttributeDotReplacement); + } + + public static string GenerateLink(RequestContext requestContext, RouteCollection routeCollection, string linkText, string routeName, string actionName, string controllerName, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes) + { + return GenerateLink(requestContext, routeCollection, linkText, routeName, actionName, controllerName, null /* protocol */, null /* hostName */, null /* fragment */, routeValues, htmlAttributes); + } + + public static string GenerateLink(RequestContext requestContext, RouteCollection routeCollection, string linkText, string routeName, string actionName, string controllerName, string protocol, string hostName, string fragment, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes) + { + return GenerateLinkInternal(requestContext, routeCollection, linkText, routeName, actionName, controllerName, protocol, hostName, fragment, routeValues, htmlAttributes, true /* includeImplicitMvcValues */); + } + + private static string GenerateLinkInternal(RequestContext requestContext, RouteCollection routeCollection, string linkText, string routeName, string actionName, string controllerName, string protocol, string hostName, string fragment, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes, bool includeImplicitMvcValues) + { + string url = UrlHelper.GenerateUrl(routeName, actionName, controllerName, protocol, hostName, fragment, routeValues, routeCollection, requestContext, includeImplicitMvcValues); + TagBuilder tagBuilder = new TagBuilder("a") + { + InnerHtml = (!String.IsNullOrEmpty(linkText)) ? HttpUtility.HtmlEncode(linkText) : String.Empty + }; + tagBuilder.MergeAttributes(htmlAttributes); + tagBuilder.MergeAttribute("href", url); + return tagBuilder.ToString(TagRenderMode.Normal); + } + + public static string GenerateRouteLink(RequestContext requestContext, RouteCollection routeCollection, string linkText, string routeName, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes) + { + return GenerateRouteLink(requestContext, routeCollection, linkText, routeName, null /* protocol */, null /* hostName */, null /* fragment */, routeValues, htmlAttributes); + } + + public static string GenerateRouteLink(RequestContext requestContext, RouteCollection routeCollection, string linkText, string routeName, string protocol, string hostName, string fragment, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes) + { + return GenerateLinkInternal(requestContext, routeCollection, linkText, routeName, null /* actionName */, null /* controllerName */, protocol, hostName, fragment, routeValues, htmlAttributes, false /* includeImplicitMvcValues */); + } + + public static string GetFormMethodString(FormMethod method) + { + switch (method) + { + case FormMethod.Get: + return "get"; + case FormMethod.Post: + return "post"; + default: + return "post"; + } + } + + public static string GetInputTypeString(InputType inputType) + { + switch (inputType) + { + case InputType.CheckBox: + return "checkbox"; + case InputType.Hidden: + return "hidden"; + case InputType.Password: + return "password"; + case InputType.Radio: + return "radio"; + case InputType.Text: + return "text"; + default: + return "text"; + } + } + + internal object GetModelStateValue(string key, Type destinationType) + { + ModelState modelState; + if (ViewData.ModelState.TryGetValue(key, out modelState)) + { + if (modelState.Value != null) + { + return modelState.Value.ConvertTo(destinationType, null /* culture */); + } + } + return null; + } + + public IDictionary<string, object> GetUnobtrusiveValidationAttributes(string name) + { + return GetUnobtrusiveValidationAttributes(name, metadata: null); + } + + // Only render attributes if unobtrusive client-side validation is enabled, and then only if we've + // never rendered validation for a field with this name in this form. Also, if there's no form context, + // then we can't render the attributes (we'd have no <form> to attach them to). + public IDictionary<string, object> GetUnobtrusiveValidationAttributes(string name, ModelMetadata metadata) + { + Dictionary<string, object> results = new Dictionary<string, object>(); + + // The ordering of these 3 checks (and the early exits) is for performance reasons. + if (!ViewContext.UnobtrusiveJavaScriptEnabled) + { + return results; + } + + FormContext formContext = ViewContext.GetFormContextForClientValidation(); + if (formContext == null) + { + return results; + } + + string fullName = ViewData.TemplateInfo.GetFullHtmlFieldName(name); + if (formContext.RenderedField(fullName)) + { + return results; + } + + formContext.RenderedField(fullName, true); + + IEnumerable<ModelClientValidationRule> clientRules = ClientValidationRuleFactory(name, metadata); + UnobtrusiveValidationAttributesGenerator.GetValidationAttributes(clientRules, results); + + return results; + } + + public MvcHtmlString HttpMethodOverride(HttpVerbs httpVerb) + { + string httpMethod; + switch (httpVerb) + { + case HttpVerbs.Delete: + httpMethod = "DELETE"; + break; + case HttpVerbs.Head: + httpMethod = "HEAD"; + break; + case HttpVerbs.Put: + httpMethod = "PUT"; + break; + default: + throw new ArgumentException(MvcResources.HtmlHelper_InvalidHttpVerb, "httpVerb"); + } + + return HttpMethodOverride(httpMethod); + } + + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "For consistency, all helpers are instance methods.")] + public MvcHtmlString HttpMethodOverride(string httpMethod) + { + if (String.IsNullOrEmpty(httpMethod)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "httpMethod"); + } + if (String.Equals(httpMethod, "GET", StringComparison.OrdinalIgnoreCase) || + String.Equals(httpMethod, "POST", StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException(MvcResources.HtmlHelper_InvalidHttpMethod, "httpMethod"); + } + + TagBuilder tagBuilder = new TagBuilder("input"); + tagBuilder.Attributes["type"] = "hidden"; + tagBuilder.Attributes["name"] = HttpRequestExtensions.XHttpMethodOverrideKey; + tagBuilder.Attributes["value"] = httpMethod; + + return tagBuilder.ToMvcHtmlString(TagRenderMode.SelfClosing); + } + + /// <summary> + /// Wraps HTML markup in an IHtmlString, which will enable HTML markup to be + /// rendered to the output without getting HTML encoded. + /// </summary> + /// <param name="value">HTML markup string.</param> + /// <returns>An IHtmlString that represents HTML markup.</returns> + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "For consistency, all helpers are instance methods.")] + public IHtmlString Raw(string value) + { + return new HtmlString(value); + } + + /// <summary> + /// Wraps HTML markup from the string representation of an object in an IHtmlString, + /// which will enable HTML markup to be rendered to the output without getting HTML encoded. + /// </summary> + /// <param name="value">object with string representation as HTML markup</param> + /// <returns>An IHtmlString that represents HTML markup.</returns> + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "For consistency, all helpers are instance methods.")] + public IHtmlString Raw(object value) + { + return new HtmlString(value == null ? null : value.ToString()); + } + + internal virtual void RenderPartialInternal(string partialViewName, ViewDataDictionary viewData, object model, TextWriter writer, ViewEngineCollection viewEngineCollection) + { + if (String.IsNullOrEmpty(partialViewName)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "partialViewName"); + } + + ViewDataDictionary newViewData = null; + + if (model == null) + { + if (viewData == null) + { + newViewData = new ViewDataDictionary(ViewData); + } + else + { + newViewData = new ViewDataDictionary(viewData); + } + } + else + { + if (viewData == null) + { + newViewData = new ViewDataDictionary(model); + } + else + { + newViewData = new ViewDataDictionary(viewData) { Model = model }; + } + } + + ViewContext newViewContext = new ViewContext(ViewContext, ViewContext.View, newViewData, ViewContext.TempData, writer); + IView view = FindPartialView(newViewContext, partialViewName, viewEngineCollection); + view.Render(newViewContext, writer); + } + } +} diff --git a/src/System.Web.Mvc/HtmlHelper`1.cs b/src/System.Web.Mvc/HtmlHelper`1.cs new file mode 100644 index 00000000..77eb44a7 --- /dev/null +++ b/src/System.Web.Mvc/HtmlHelper`1.cs @@ -0,0 +1,39 @@ +using System.Web.Routing; + +namespace System.Web.Mvc +{ + public class HtmlHelper<TModel> : HtmlHelper + { + private DynamicViewDataDictionary _dynamicViewDataDictionary; + private ViewDataDictionary<TModel> _viewData; + + public HtmlHelper(ViewContext viewContext, IViewDataContainer viewDataContainer) + : this(viewContext, viewDataContainer, RouteTable.Routes) + { + } + + public HtmlHelper(ViewContext viewContext, IViewDataContainer viewDataContainer, RouteCollection routeCollection) + : base(viewContext, viewDataContainer, routeCollection) + { + _viewData = new ViewDataDictionary<TModel>(viewDataContainer.ViewData); + } + + public new dynamic ViewBag + { + get + { + if (_dynamicViewDataDictionary == null) + { + _dynamicViewDataDictionary = new DynamicViewDataDictionary(() => ViewData); + } + + return _dynamicViewDataDictionary; + } + } + + public new ViewDataDictionary<TModel> ViewData + { + get { return _viewData; } + } + } +} diff --git a/src/System.Web.Mvc/HttpDeleteAttribute.cs b/src/System.Web.Mvc/HttpDeleteAttribute.cs new file mode 100644 index 00000000..5e661dfa --- /dev/null +++ b/src/System.Web.Mvc/HttpDeleteAttribute.cs @@ -0,0 +1,15 @@ +using System.Reflection; + +namespace System.Web.Mvc +{ + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public sealed class HttpDeleteAttribute : ActionMethodSelectorAttribute + { + private static readonly AcceptVerbsAttribute _innerAttribute = new AcceptVerbsAttribute(HttpVerbs.Delete); + + public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo) + { + return _innerAttribute.IsValidForRequest(controllerContext, methodInfo); + } + } +} diff --git a/src/System.Web.Mvc/HttpFileCollectionValueProvider.cs b/src/System.Web.Mvc/HttpFileCollectionValueProvider.cs new file mode 100644 index 00000000..3c3760eb --- /dev/null +++ b/src/System.Web.Mvc/HttpFileCollectionValueProvider.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace System.Web.Mvc +{ + public sealed class HttpFileCollectionValueProvider : DictionaryValueProvider<HttpPostedFileBase[]> + { + private static readonly Dictionary<string, HttpPostedFileBase[]> _emptyDictionary = new Dictionary<string, HttpPostedFileBase[]>(); + + public HttpFileCollectionValueProvider(ControllerContext controllerContext) + : base(GetHttpPostedFileDictionary(controllerContext), CultureInfo.InvariantCulture) + { + } + + private static Dictionary<string, HttpPostedFileBase[]> GetHttpPostedFileDictionary(ControllerContext controllerContext) + { + HttpFileCollectionBase files = controllerContext.HttpContext.Request.Files; + + // fast-track common case of no files + if (files.Count == 0) + { + return _emptyDictionary; + } + + // build up the 1:many file mapping + List<KeyValuePair<string, HttpPostedFileBase>> mapping = new List<KeyValuePair<string, HttpPostedFileBase>>(); + string[] allKeys = files.AllKeys; + for (int i = 0; i < files.Count; i++) + { + string key = allKeys[i]; + if (key != null) + { + HttpPostedFileBase file = HttpPostedFileBaseModelBinder.ChooseFileOrNull(files[i]); + mapping.Add(new KeyValuePair<string, HttpPostedFileBase>(key, file)); + } + } + + // turn the mapping into a 1:many dictionary + var grouped = mapping.GroupBy(el => el.Key, el => el.Value, StringComparer.OrdinalIgnoreCase); + return grouped.ToDictionary(g => g.Key, g => g.ToArray(), StringComparer.OrdinalIgnoreCase); + } + } +} diff --git a/src/System.Web.Mvc/HttpFileCollectionValueProviderFactory.cs b/src/System.Web.Mvc/HttpFileCollectionValueProviderFactory.cs new file mode 100644 index 00000000..0c93d4dd --- /dev/null +++ b/src/System.Web.Mvc/HttpFileCollectionValueProviderFactory.cs @@ -0,0 +1,15 @@ +namespace System.Web.Mvc +{ + public sealed class HttpFileCollectionValueProviderFactory : ValueProviderFactory + { + public override IValueProvider GetValueProvider(ControllerContext controllerContext) + { + if (controllerContext == null) + { + throw new ArgumentNullException("controllerContext"); + } + + return new HttpFileCollectionValueProvider(controllerContext); + } + } +} diff --git a/src/System.Web.Mvc/HttpGetAttribute.cs b/src/System.Web.Mvc/HttpGetAttribute.cs new file mode 100644 index 00000000..41029ceb --- /dev/null +++ b/src/System.Web.Mvc/HttpGetAttribute.cs @@ -0,0 +1,15 @@ +using System.Reflection; + +namespace System.Web.Mvc +{ + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public sealed class HttpGetAttribute : ActionMethodSelectorAttribute + { + private static readonly AcceptVerbsAttribute _innerAttribute = new AcceptVerbsAttribute(HttpVerbs.Get); + + public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo) + { + return _innerAttribute.IsValidForRequest(controllerContext, methodInfo); + } + } +} diff --git a/src/System.Web.Mvc/HttpHandlerUtil.cs b/src/System.Web.Mvc/HttpHandlerUtil.cs new file mode 100644 index 00000000..89d1981b --- /dev/null +++ b/src/System.Web.Mvc/HttpHandlerUtil.cs @@ -0,0 +1,91 @@ +using System.Diagnostics.CodeAnalysis; +using System.Web.Mvc.Properties; +using System.Web.UI; + +namespace System.Web.Mvc +{ + internal static class HttpHandlerUtil + { + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "The Dispose on Page doesn't do anything by default, and we control both of these internal types.")] + public static IHttpHandler WrapForServerExecute(IHttpHandler httpHandler) + { + // Since Server.Execute() doesn't propagate HttpExceptions where the status code is + // anything other than 500, we need to wrap these exceptions ourselves. + IHttpAsyncHandler asyncHandler = httpHandler as IHttpAsyncHandler; + return (asyncHandler != null) ? new ServerExecuteHttpHandlerAsyncWrapper(asyncHandler) : new ServerExecuteHttpHandlerWrapper(httpHandler); + } + + private sealed class ServerExecuteHttpHandlerAsyncWrapper : ServerExecuteHttpHandlerWrapper, IHttpAsyncHandler + { + private readonly IHttpAsyncHandler _httpHandler; + + public ServerExecuteHttpHandlerAsyncWrapper(IHttpAsyncHandler httpHandler) + : base(httpHandler) + { + _httpHandler = httpHandler; + } + + public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData) + { + return Wrap(() => _httpHandler.BeginProcessRequest(context, cb, extraData)); + } + + public void EndProcessRequest(IAsyncResult result) + { + Wrap(() => _httpHandler.EndProcessRequest(result)); + } + } + + /// <remarks> + /// Server.Execute() requires that the provided IHttpHandler subclass Page. + /// </remarks> + internal class ServerExecuteHttpHandlerWrapper : Page + { + private readonly IHttpHandler _httpHandler; + + public ServerExecuteHttpHandlerWrapper(IHttpHandler httpHandler) + { + _httpHandler = httpHandler; + } + + internal IHttpHandler InnerHandler + { + get { return _httpHandler; } + } + + public override void ProcessRequest(HttpContext context) + { + Wrap(() => _httpHandler.ProcessRequest(context)); + } + + protected static void Wrap(Action action) + { + Wrap(delegate + { + action(); + return (object)null; + }); + } + + protected static TResult Wrap<TResult>(Func<TResult> func) + { + try + { + return func(); + } + catch (HttpException he) + { + if (he.GetHttpCode() == 500) + { + throw; // doesn't need to be wrapped + } + else + { + HttpException newHe = new HttpException(500, MvcResources.ViewPageHttpHandlerWrapper_ExceptionOccurred, he); + throw newHe; + } + } + } + } + } +} diff --git a/src/System.Web.Mvc/HttpNotFoundResult.cs b/src/System.Web.Mvc/HttpNotFoundResult.cs new file mode 100644 index 00000000..9b749ace --- /dev/null +++ b/src/System.Web.Mvc/HttpNotFoundResult.cs @@ -0,0 +1,18 @@ +using System.Net; + +namespace System.Web.Mvc +{ + public class HttpNotFoundResult : HttpStatusCodeResult + { + public HttpNotFoundResult() + : this(null) + { + } + + // NotFound is equivalent to HTTP status 404. + public HttpNotFoundResult(string statusDescription) + : base(HttpStatusCode.NotFound, statusDescription) + { + } + } +} diff --git a/src/System.Web.Mvc/HttpPostAttribute.cs b/src/System.Web.Mvc/HttpPostAttribute.cs new file mode 100644 index 00000000..da4e10cc --- /dev/null +++ b/src/System.Web.Mvc/HttpPostAttribute.cs @@ -0,0 +1,15 @@ +using System.Reflection; + +namespace System.Web.Mvc +{ + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public sealed class HttpPostAttribute : ActionMethodSelectorAttribute + { + private static readonly AcceptVerbsAttribute _innerAttribute = new AcceptVerbsAttribute(HttpVerbs.Post); + + public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo) + { + return _innerAttribute.IsValidForRequest(controllerContext, methodInfo); + } + } +} diff --git a/src/System.Web.Mvc/HttpPostedFileBaseModelBinder.cs b/src/System.Web.Mvc/HttpPostedFileBaseModelBinder.cs new file mode 100644 index 00000000..43545acc --- /dev/null +++ b/src/System.Web.Mvc/HttpPostedFileBaseModelBinder.cs @@ -0,0 +1,39 @@ +namespace System.Web.Mvc +{ + public class HttpPostedFileBaseModelBinder : IModelBinder + { + public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) + { + if (controllerContext == null) + { + throw new ArgumentNullException("controllerContext"); + } + if (bindingContext == null) + { + throw new ArgumentNullException("bindingContext"); + } + + HttpPostedFileBase theFile = controllerContext.HttpContext.Request.Files[bindingContext.ModelName]; + return ChooseFileOrNull(theFile); + } + + // helper that returns the original file if there was content uploaded, null if empty + internal static HttpPostedFileBase ChooseFileOrNull(HttpPostedFileBase rawFile) + { + // case 1: there was no <input type="file" ... /> element in the post + if (rawFile == null) + { + return null; + } + + // case 2: there was an <input type="file" ... /> element in the post, but it was left blank + if (rawFile.ContentLength == 0 && String.IsNullOrEmpty(rawFile.FileName)) + { + return null; + } + + // case 3: the file was posted + return rawFile; + } + } +} diff --git a/src/System.Web.Mvc/HttpPutAttribute.cs b/src/System.Web.Mvc/HttpPutAttribute.cs new file mode 100644 index 00000000..7f0fe8e3 --- /dev/null +++ b/src/System.Web.Mvc/HttpPutAttribute.cs @@ -0,0 +1,15 @@ +using System.Reflection; + +namespace System.Web.Mvc +{ + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public sealed class HttpPutAttribute : ActionMethodSelectorAttribute + { + private static readonly AcceptVerbsAttribute _innerAttribute = new AcceptVerbsAttribute(HttpVerbs.Put); + + public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo) + { + return _innerAttribute.IsValidForRequest(controllerContext, methodInfo); + } + } +} diff --git a/src/System.Web.Mvc/HttpRequestExtensions.cs b/src/System.Web.Mvc/HttpRequestExtensions.cs new file mode 100644 index 00000000..d1719da6 --- /dev/null +++ b/src/System.Web.Mvc/HttpRequestExtensions.cs @@ -0,0 +1,54 @@ +namespace System.Web.Mvc +{ + public static class HttpRequestExtensions + { + internal const string XHttpMethodOverrideKey = "X-HTTP-Method-Override"; + + public static string GetHttpMethodOverride(this HttpRequestBase request) + { + if (request == null) + { + throw new ArgumentNullException("request"); + } + + string incomingVerb = request.HttpMethod; + + if (!String.Equals(incomingVerb, "POST", StringComparison.OrdinalIgnoreCase)) + { + return incomingVerb; + } + + string verbOverride = null; + string headerOverrideValue = request.Headers[XHttpMethodOverrideKey]; + if (!String.IsNullOrEmpty(headerOverrideValue)) + { + verbOverride = headerOverrideValue; + } + else + { + string formOverrideValue = request.Form[XHttpMethodOverrideKey]; + if (!String.IsNullOrEmpty(formOverrideValue)) + { + verbOverride = formOverrideValue; + } + else + { + string queryStringOverrideValue = request.QueryString[XHttpMethodOverrideKey]; + if (!String.IsNullOrEmpty(queryStringOverrideValue)) + { + verbOverride = queryStringOverrideValue; + } + } + } + if (verbOverride != null) + { + if (!String.Equals(verbOverride, "GET", StringComparison.OrdinalIgnoreCase) && + !String.Equals(verbOverride, "POST", StringComparison.OrdinalIgnoreCase)) + { + incomingVerb = verbOverride; + } + } + return incomingVerb; + } + } +} diff --git a/src/System.Web.Mvc/HttpStatusCodeResult.cs b/src/System.Web.Mvc/HttpStatusCodeResult.cs new file mode 100644 index 00000000..785e1eae --- /dev/null +++ b/src/System.Web.Mvc/HttpStatusCodeResult.cs @@ -0,0 +1,46 @@ +using System.Net; + +namespace System.Web.Mvc +{ + public class HttpStatusCodeResult : ActionResult + { + public HttpStatusCodeResult(int statusCode) + : this(statusCode, null) + { + } + + public HttpStatusCodeResult(HttpStatusCode statusCode) + : this(statusCode, null) + { + } + + public HttpStatusCodeResult(HttpStatusCode statusCode, string statusDescription) + : this((int)statusCode, statusDescription) + { + } + + public HttpStatusCodeResult(int statusCode, string statusDescription) + { + StatusCode = statusCode; + StatusDescription = statusDescription; + } + + public int StatusCode { get; private set; } + + public string StatusDescription { get; private set; } + + public override void ExecuteResult(ControllerContext context) + { + if (context == null) + { + throw new ArgumentNullException("context"); + } + + context.HttpContext.Response.StatusCode = StatusCode; + if (StatusDescription != null) + { + context.HttpContext.Response.StatusDescription = StatusDescription; + } + } + } +} diff --git a/src/System.Web.Mvc/HttpUnauthorizedResult.cs b/src/System.Web.Mvc/HttpUnauthorizedResult.cs new file mode 100644 index 00000000..fe672422 --- /dev/null +++ b/src/System.Web.Mvc/HttpUnauthorizedResult.cs @@ -0,0 +1,21 @@ +using System.Net; + +namespace System.Web.Mvc +{ + public class HttpUnauthorizedResult : HttpStatusCodeResult + { + public HttpUnauthorizedResult() + : this(null) + { + } + + // Unauthorized is equivalent to HTTP status 401, the status code for unauthorized + // access. Other code might intercept this and perform some special logic. For + // example, the FormsAuthenticationModule looks for 401 responses and instead + // redirects the user to the login page. + public HttpUnauthorizedResult(string statusDescription) + : base(HttpStatusCode.Unauthorized, statusDescription) + { + } + } +} diff --git a/src/System.Web.Mvc/HttpVerbs.cs b/src/System.Web.Mvc/HttpVerbs.cs new file mode 100644 index 00000000..22049977 --- /dev/null +++ b/src/System.Web.Mvc/HttpVerbs.cs @@ -0,0 +1,12 @@ +namespace System.Web.Mvc +{ + [Flags] + public enum HttpVerbs + { + Get = 1 << 0, + Post = 1 << 1, + Put = 1 << 2, + Delete = 1 << 3, + Head = 1 << 4 + } +} diff --git a/src/System.Web.Mvc/IActionFilter.cs b/src/System.Web.Mvc/IActionFilter.cs new file mode 100644 index 00000000..8ebbbca5 --- /dev/null +++ b/src/System.Web.Mvc/IActionFilter.cs @@ -0,0 +1,8 @@ +namespace System.Web.Mvc +{ + public interface IActionFilter + { + void OnActionExecuting(ActionExecutingContext filterContext); + void OnActionExecuted(ActionExecutedContext filterContext); + } +} diff --git a/src/System.Web.Mvc/IActionInvoker.cs b/src/System.Web.Mvc/IActionInvoker.cs new file mode 100644 index 00000000..0cec2baf --- /dev/null +++ b/src/System.Web.Mvc/IActionInvoker.cs @@ -0,0 +1,7 @@ +namespace System.Web.Mvc +{ + public interface IActionInvoker + { + bool InvokeAction(ControllerContext controllerContext, string actionName); + } +} diff --git a/src/System.Web.Mvc/IAuthorizationFilter.cs b/src/System.Web.Mvc/IAuthorizationFilter.cs new file mode 100644 index 00000000..e0f01b26 --- /dev/null +++ b/src/System.Web.Mvc/IAuthorizationFilter.cs @@ -0,0 +1,7 @@ +namespace System.Web.Mvc +{ + public interface IAuthorizationFilter + { + void OnAuthorization(AuthorizationContext filterContext); + } +} diff --git a/src/System.Web.Mvc/IBuildManager.cs b/src/System.Web.Mvc/IBuildManager.cs new file mode 100644 index 00000000..5782fbd6 --- /dev/null +++ b/src/System.Web.Mvc/IBuildManager.cs @@ -0,0 +1,14 @@ +using System.Collections; +using System.IO; + +namespace System.Web.Mvc +{ + internal interface IBuildManager + { + bool FileExists(string virtualPath); + Type GetCompiledType(string virtualPath); + ICollection GetReferencedAssemblies(); + Stream ReadCachedFile(string fileName); + Stream CreateCachedFile(string fileName); + } +} diff --git a/src/System.Web.Mvc/IClientValidatable.cs b/src/System.Web.Mvc/IClientValidatable.cs new file mode 100644 index 00000000..1eeee7c4 --- /dev/null +++ b/src/System.Web.Mvc/IClientValidatable.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace System.Web.Mvc +{ + // The purpose of this interface is to make something as supporting client-side + // validation, which could be discovered at runtime by whatever validation + // framework you're using. Because this interface is designed to be independent + // of underlying implementation details, where you apply this interface will + // depend on your specific validation framework. + // + // For DataAnnotations, you'll apply this interface to your validation attribute + // (the class which derives from ValidationAttribute). When you've implemented + // this interface, it will alleviate the need of writing a validator and registering + // it with the DataAnnotationsModelValidatorProvider. + public interface IClientValidatable + { + IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context); + } +} diff --git a/src/System.Web.Mvc/IController.cs b/src/System.Web.Mvc/IController.cs new file mode 100644 index 00000000..751f8d29 --- /dev/null +++ b/src/System.Web.Mvc/IController.cs @@ -0,0 +1,9 @@ +using System.Web.Routing; + +namespace System.Web.Mvc +{ + public interface IController + { + void Execute(RequestContext requestContext); + } +} diff --git a/src/System.Web.Mvc/IControllerActivator.cs b/src/System.Web.Mvc/IControllerActivator.cs new file mode 100644 index 00000000..38bece0e --- /dev/null +++ b/src/System.Web.Mvc/IControllerActivator.cs @@ -0,0 +1,9 @@ +using System.Web.Routing; + +namespace System.Web.Mvc +{ + public interface IControllerActivator + { + IController Create(RequestContext requestContext, Type controllerType); + } +} diff --git a/src/System.Web.Mvc/IControllerFactory.cs b/src/System.Web.Mvc/IControllerFactory.cs new file mode 100644 index 00000000..55166ba7 --- /dev/null +++ b/src/System.Web.Mvc/IControllerFactory.cs @@ -0,0 +1,12 @@ +using System.Web.Routing; +using System.Web.SessionState; + +namespace System.Web.Mvc +{ + public interface IControllerFactory + { + IController CreateController(RequestContext requestContext, string controllerName); + SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName); + void ReleaseController(IController controller); + } +} diff --git a/src/System.Web.Mvc/IDependencyResolver.cs b/src/System.Web.Mvc/IDependencyResolver.cs new file mode 100644 index 00000000..fe5a6cbd --- /dev/null +++ b/src/System.Web.Mvc/IDependencyResolver.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace System.Web.Mvc +{ + public interface IDependencyResolver + { + object GetService(Type serviceType); + IEnumerable<object> GetServices(Type serviceType); + } +} diff --git a/src/System.Web.Mvc/IEnumerableValueProvider.cs b/src/System.Web.Mvc/IEnumerableValueProvider.cs new file mode 100644 index 00000000..e4fe1bde --- /dev/null +++ b/src/System.Web.Mvc/IEnumerableValueProvider.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace System.Web.Mvc +{ + // Represents a special IValueProvider that has the ability to be enumerable. + public interface IEnumerableValueProvider : IValueProvider + { + IDictionary<string, string> GetKeysFromPrefix(string prefix); + } +} diff --git a/src/System.Web.Mvc/IExceptionFilter.cs b/src/System.Web.Mvc/IExceptionFilter.cs new file mode 100644 index 00000000..545187e6 --- /dev/null +++ b/src/System.Web.Mvc/IExceptionFilter.cs @@ -0,0 +1,7 @@ +namespace System.Web.Mvc +{ + public interface IExceptionFilter + { + void OnException(ExceptionContext filterContext); + } +} diff --git a/src/System.Web.Mvc/IFilterProvider.cs b/src/System.Web.Mvc/IFilterProvider.cs new file mode 100644 index 00000000..edae298e --- /dev/null +++ b/src/System.Web.Mvc/IFilterProvider.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace System.Web.Mvc +{ + public interface IFilterProvider + { + IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor); + } +} diff --git a/src/System.Web.Mvc/IMetadataAware.cs b/src/System.Web.Mvc/IMetadataAware.cs new file mode 100644 index 00000000..c1e9df24 --- /dev/null +++ b/src/System.Web.Mvc/IMetadataAware.cs @@ -0,0 +1,12 @@ +namespace System.Web.Mvc +{ + // This interface is implemented by attributes which wish to contribute to the + // ModelMetadata creation process without needing to write a custom metadata + // provider. It is consumed by AssociatedMetadataProvider, so this behavior is + // automatically inherited by all classes which derive from it (notably, the + // DataAnnotationsModelMetadataProvider). + public interface IMetadataAware + { + void OnMetadataCreated(ModelMetadata metadata); + } +} diff --git a/src/System.Web.Mvc/IModelBinder.cs b/src/System.Web.Mvc/IModelBinder.cs new file mode 100644 index 00000000..41a488d2 --- /dev/null +++ b/src/System.Web.Mvc/IModelBinder.cs @@ -0,0 +1,7 @@ +namespace System.Web.Mvc +{ + public interface IModelBinder + { + object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext); + } +} diff --git a/src/System.Web.Mvc/IModelBinderProvider.cs b/src/System.Web.Mvc/IModelBinderProvider.cs new file mode 100644 index 00000000..eac700a1 --- /dev/null +++ b/src/System.Web.Mvc/IModelBinderProvider.cs @@ -0,0 +1,7 @@ +namespace System.Web.Mvc +{ + public interface IModelBinderProvider + { + IModelBinder GetBinder(Type modelType); + } +} diff --git a/src/System.Web.Mvc/IMvcControlBuilder.cs b/src/System.Web.Mvc/IMvcControlBuilder.cs new file mode 100644 index 00000000..2f7e2064 --- /dev/null +++ b/src/System.Web.Mvc/IMvcControlBuilder.cs @@ -0,0 +1,7 @@ +namespace System.Web.Mvc +{ + internal interface IMvcControlBuilder + { + string Inherits { set; } + } +} diff --git a/src/System.Web.Mvc/IMvcFilter.cs b/src/System.Web.Mvc/IMvcFilter.cs new file mode 100644 index 00000000..05aa035f --- /dev/null +++ b/src/System.Web.Mvc/IMvcFilter.cs @@ -0,0 +1,8 @@ +namespace System.Web.Mvc +{ + public interface IMvcFilter + { + bool AllowMultiple { get; } + int Order { get; } + } +} diff --git a/src/System.Web.Mvc/IResolver.cs b/src/System.Web.Mvc/IResolver.cs new file mode 100644 index 00000000..0d0a01d6 --- /dev/null +++ b/src/System.Web.Mvc/IResolver.cs @@ -0,0 +1,7 @@ +namespace System.Web.Mvc +{ + internal interface IResolver<T> + { + T Current { get; } + } +} diff --git a/src/System.Web.Mvc/IResultFilter.cs b/src/System.Web.Mvc/IResultFilter.cs new file mode 100644 index 00000000..892489cb --- /dev/null +++ b/src/System.Web.Mvc/IResultFilter.cs @@ -0,0 +1,8 @@ +namespace System.Web.Mvc +{ + public interface IResultFilter + { + void OnResultExecuting(ResultExecutingContext filterContext); + void OnResultExecuted(ResultExecutedContext filterContext); + } +} diff --git a/src/System.Web.Mvc/IRouteWithArea.cs b/src/System.Web.Mvc/IRouteWithArea.cs new file mode 100644 index 00000000..83fb00fc --- /dev/null +++ b/src/System.Web.Mvc/IRouteWithArea.cs @@ -0,0 +1,7 @@ +namespace System.Web.Mvc +{ + public interface IRouteWithArea + { + string Area { get; } + } +} diff --git a/src/System.Web.Mvc/ITempDataProvider.cs b/src/System.Web.Mvc/ITempDataProvider.cs new file mode 100644 index 00000000..7f40896c --- /dev/null +++ b/src/System.Web.Mvc/ITempDataProvider.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace System.Web.Mvc +{ + public interface ITempDataProvider + { + IDictionary<string, object> LoadTempData(ControllerContext controllerContext); + void SaveTempData(ControllerContext controllerContext, IDictionary<string, object> values); + } +} diff --git a/src/System.Web.Mvc/IUniquelyIdentifiable.cs b/src/System.Web.Mvc/IUniquelyIdentifiable.cs new file mode 100644 index 00000000..a310c93a --- /dev/null +++ b/src/System.Web.Mvc/IUniquelyIdentifiable.cs @@ -0,0 +1,7 @@ +namespace System.Web.Mvc +{ + internal interface IUniquelyIdentifiable + { + string UniqueId { get; } + } +} diff --git a/src/System.Web.Mvc/IUnvalidatedRequestValues.cs b/src/System.Web.Mvc/IUnvalidatedRequestValues.cs new file mode 100644 index 00000000..1f5c67bc --- /dev/null +++ b/src/System.Web.Mvc/IUnvalidatedRequestValues.cs @@ -0,0 +1,13 @@ +using System.Collections.Specialized; + +namespace System.Web.Mvc +{ + // Used for mocking the UnvalidatedRequestValues type in System.Web.WebPages + + internal interface IUnvalidatedRequestValues + { + NameValueCollection Form { get; } + NameValueCollection QueryString { get; } + string this[string key] { get; } + } +} diff --git a/src/System.Web.Mvc/IUnvalidatedValueProvider.cs b/src/System.Web.Mvc/IUnvalidatedValueProvider.cs new file mode 100644 index 00000000..91882c3a --- /dev/null +++ b/src/System.Web.Mvc/IUnvalidatedValueProvider.cs @@ -0,0 +1,8 @@ +namespace System.Web.Mvc +{ + // Represents a special IValueProvider that has the ability to skip request validation. + public interface IUnvalidatedValueProvider : IValueProvider + { + ValueProviderResult GetValue(string key, bool skipValidation); + } +} diff --git a/src/System.Web.Mvc/IValueProvider.cs b/src/System.Web.Mvc/IValueProvider.cs new file mode 100644 index 00000000..37b83a14 --- /dev/null +++ b/src/System.Web.Mvc/IValueProvider.cs @@ -0,0 +1,8 @@ +namespace System.Web.Mvc +{ + public interface IValueProvider + { + bool ContainsPrefix(string prefix); + ValueProviderResult GetValue(string key); + } +} diff --git a/src/System.Web.Mvc/IView.cs b/src/System.Web.Mvc/IView.cs new file mode 100644 index 00000000..2d48cf70 --- /dev/null +++ b/src/System.Web.Mvc/IView.cs @@ -0,0 +1,9 @@ +using System.IO; + +namespace System.Web.Mvc +{ + public interface IView + { + void Render(ViewContext viewContext, TextWriter writer); + } +} diff --git a/src/System.Web.Mvc/IViewDataContainer.cs b/src/System.Web.Mvc/IViewDataContainer.cs new file mode 100644 index 00000000..34063148 --- /dev/null +++ b/src/System.Web.Mvc/IViewDataContainer.cs @@ -0,0 +1,10 @@ +using System.Diagnostics.CodeAnalysis; + +namespace System.Web.Mvc +{ + public interface IViewDataContainer + { + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This is the mechanism by which the ViewPage / ViewUserControl get their ViewDataDictionary objects.")] + ViewDataDictionary ViewData { get; set; } + } +} diff --git a/src/System.Web.Mvc/IViewEngine.cs b/src/System.Web.Mvc/IViewEngine.cs new file mode 100644 index 00000000..cda8075f --- /dev/null +++ b/src/System.Web.Mvc/IViewEngine.cs @@ -0,0 +1,9 @@ +namespace System.Web.Mvc +{ + public interface IViewEngine + { + ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache); + ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache); + void ReleaseView(ControllerContext controllerContext, IView view); + } +} diff --git a/src/System.Web.Mvc/IViewLocationCache.cs b/src/System.Web.Mvc/IViewLocationCache.cs new file mode 100644 index 00000000..a8661349 --- /dev/null +++ b/src/System.Web.Mvc/IViewLocationCache.cs @@ -0,0 +1,8 @@ +namespace System.Web.Mvc +{ + public interface IViewLocationCache + { + string GetViewLocation(HttpContextBase httpContext, string key); + void InsertViewLocation(HttpContextBase httpContext, string key, string virtualPath); + } +} diff --git a/src/System.Web.Mvc/IViewPageActivator.cs b/src/System.Web.Mvc/IViewPageActivator.cs new file mode 100644 index 00000000..40420ff8 --- /dev/null +++ b/src/System.Web.Mvc/IViewPageActivator.cs @@ -0,0 +1,7 @@ +namespace System.Web.Mvc +{ + public interface IViewPageActivator + { + object Create(ControllerContext controllerContext, Type type); + } +} diff --git a/src/System.Web.Mvc/IViewStartPageChild.cs b/src/System.Web.Mvc/IViewStartPageChild.cs new file mode 100644 index 00000000..b97d02ed --- /dev/null +++ b/src/System.Web.Mvc/IViewStartPageChild.cs @@ -0,0 +1,9 @@ +namespace System.Web.Mvc +{ + internal interface IViewStartPageChild + { + HtmlHelper<object> Html { get; } + UrlHelper Url { get; } + ViewContext ViewContext { get; } + } +} diff --git a/src/System.Web.Mvc/InputType.cs b/src/System.Web.Mvc/InputType.cs new file mode 100644 index 00000000..d3c3b204 --- /dev/null +++ b/src/System.Web.Mvc/InputType.cs @@ -0,0 +1,11 @@ +namespace System.Web.Mvc +{ + public enum InputType + { + CheckBox, + Hidden, + Password, + Radio, + Text + } +} diff --git a/src/System.Web.Mvc/JavaScriptResult.cs b/src/System.Web.Mvc/JavaScriptResult.cs new file mode 100644 index 00000000..e692648d --- /dev/null +++ b/src/System.Web.Mvc/JavaScriptResult.cs @@ -0,0 +1,23 @@ +namespace System.Web.Mvc +{ + public class JavaScriptResult : ActionResult + { + public string Script { get; set; } + + public override void ExecuteResult(ControllerContext context) + { + if (context == null) + { + throw new ArgumentNullException("context"); + } + + HttpResponseBase response = context.HttpContext.Response; + response.ContentType = "application/x-javascript"; + + if (Script != null) + { + response.Write(Script); + } + } + } +} diff --git a/src/System.Web.Mvc/JsonRequestBehavior.cs b/src/System.Web.Mvc/JsonRequestBehavior.cs new file mode 100644 index 00000000..438d3cb3 --- /dev/null +++ b/src/System.Web.Mvc/JsonRequestBehavior.cs @@ -0,0 +1,8 @@ +namespace System.Web.Mvc +{ + public enum JsonRequestBehavior + { + AllowGet, + DenyGet, + } +} diff --git a/src/System.Web.Mvc/JsonResult.cs b/src/System.Web.Mvc/JsonResult.cs new file mode 100644 index 00000000..762a787c --- /dev/null +++ b/src/System.Web.Mvc/JsonResult.cs @@ -0,0 +1,73 @@ +using System.Text; +using System.Web.Mvc.Properties; +using System.Web.Script.Serialization; + +namespace System.Web.Mvc +{ + public class JsonResult : ActionResult + { + public JsonResult() + { + JsonRequestBehavior = JsonRequestBehavior.DenyGet; + } + + public Encoding ContentEncoding { get; set; } + + public string ContentType { get; set; } + + public object Data { get; set; } + + public JsonRequestBehavior JsonRequestBehavior { get; set; } + + /// <summary> + /// When set MaxJsonLength passed to the JavaScriptSerializer. + /// </summary> + public int? MaxJsonLength { get; set; } + + /// <summary> + /// When set RecursionLimit passed to the JavaScriptSerializer. + /// </summary> + public int? RecursionLimit { get; set; } + + public override void ExecuteResult(ControllerContext context) + { + if (context == null) + { + throw new ArgumentNullException("context"); + } + if (JsonRequestBehavior == JsonRequestBehavior.DenyGet && + String.Equals(context.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException(MvcResources.JsonRequest_GetNotAllowed); + } + + HttpResponseBase response = context.HttpContext.Response; + + if (!String.IsNullOrEmpty(ContentType)) + { + response.ContentType = ContentType; + } + else + { + response.ContentType = "application/json"; + } + if (ContentEncoding != null) + { + response.ContentEncoding = ContentEncoding; + } + if (Data != null) + { + JavaScriptSerializer serializer = new JavaScriptSerializer(); + if (MaxJsonLength.HasValue) + { + serializer.MaxJsonLength = MaxJsonLength.Value; + } + if (RecursionLimit.HasValue) + { + serializer.RecursionLimit = RecursionLimit.Value; + } + response.Write(serializer.Serialize(Data)); + } + } + } +} diff --git a/src/System.Web.Mvc/JsonValueProviderFactory.cs b/src/System.Web.Mvc/JsonValueProviderFactory.cs new file mode 100644 index 00000000..31be9845 --- /dev/null +++ b/src/System.Web.Mvc/JsonValueProviderFactory.cs @@ -0,0 +1,131 @@ +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Configuration; +using System.Globalization; +using System.IO; +using System.Web.Mvc.Properties; +using System.Web.Script.Serialization; + +namespace System.Web.Mvc +{ + public sealed class JsonValueProviderFactory : ValueProviderFactory + { + private static void AddToBackingStore(EntryLimitedDictionary backingStore, string prefix, object value) + { + IDictionary<string, object> d = value as IDictionary<string, object>; + if (d != null) + { + foreach (KeyValuePair<string, object> entry in d) + { + AddToBackingStore(backingStore, MakePropertyKey(prefix, entry.Key), entry.Value); + } + return; + } + + IList l = value as IList; + if (l != null) + { + for (int i = 0; i < l.Count; i++) + { + AddToBackingStore(backingStore, MakeArrayKey(prefix, i), l[i]); + } + return; + } + + // primitive + backingStore.Add(prefix, value); + } + + private static object GetDeserializedObject(ControllerContext controllerContext) + { + if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase)) + { + // not JSON request + return null; + } + + StreamReader reader = new StreamReader(controllerContext.HttpContext.Request.InputStream); + string bodyText = reader.ReadToEnd(); + if (String.IsNullOrEmpty(bodyText)) + { + // no JSON data + return null; + } + + JavaScriptSerializer serializer = new JavaScriptSerializer(); + object jsonData = serializer.DeserializeObject(bodyText); + return jsonData; + } + + public override IValueProvider GetValueProvider(ControllerContext controllerContext) + { + if (controllerContext == null) + { + throw new ArgumentNullException("controllerContext"); + } + + object jsonData = GetDeserializedObject(controllerContext); + if (jsonData == null) + { + return null; + } + + Dictionary<string, object> backingStore = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); + EntryLimitedDictionary backingStoreWrapper = new EntryLimitedDictionary(backingStore); + AddToBackingStore(backingStoreWrapper, String.Empty, jsonData); + return new DictionaryValueProvider<object>(backingStore, CultureInfo.CurrentCulture); + } + + private static string MakeArrayKey(string prefix, int index) + { + return prefix + "[" + index.ToString(CultureInfo.InvariantCulture) + "]"; + } + + private static string MakePropertyKey(string prefix, string propertyName) + { + return (String.IsNullOrEmpty(prefix)) ? propertyName : prefix + "." + propertyName; + } + + private class EntryLimitedDictionary + { + private static int _maximumDepth = GetMaximumDepth(); + private readonly IDictionary<string, object> _innerDictionary; + private int _itemCount = 0; + + public EntryLimitedDictionary(IDictionary<string, object> innerDictionary) + { + _innerDictionary = innerDictionary; + } + + public void Add(string key, object value) + { + if (++_itemCount > _maximumDepth) + { + throw new InvalidOperationException(MvcResources.JsonValueProviderFactory_RequestTooLarge); + } + + _innerDictionary.Add(key, value); + } + + private static int GetMaximumDepth() + { + NameValueCollection appSettings = ConfigurationManager.AppSettings; + if (appSettings != null) + { + string[] valueArray = appSettings.GetValues("aspnet:MaxJsonDeserializerMembers"); + if (valueArray != null && valueArray.Length > 0) + { + int result; + if (Int32.TryParse(valueArray[0], out result)) + { + return result; + } + } + } + + return 1000; // Fallback default + } + } + } +} diff --git a/src/System.Web.Mvc/LinqBinaryModelBinder.cs b/src/System.Web.Mvc/LinqBinaryModelBinder.cs new file mode 100644 index 00000000..005a36df --- /dev/null +++ b/src/System.Web.Mvc/LinqBinaryModelBinder.cs @@ -0,0 +1,18 @@ +using System.Data.Linq; + +namespace System.Web.Mvc +{ + public class LinqBinaryModelBinder : ByteArrayModelBinder + { + public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) + { + byte[] byteValue = (byte[])base.BindModel(controllerContext, bindingContext); + if (byteValue == null) + { + return null; + } + + return new Binary(byteValue); + } + } +} diff --git a/src/System.Web.Mvc/ModelBinderAttribute.cs b/src/System.Web.Mvc/ModelBinderAttribute.cs new file mode 100644 index 00000000..4e517d3c --- /dev/null +++ b/src/System.Web.Mvc/ModelBinderAttribute.cs @@ -0,0 +1,44 @@ +using System.Globalization; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + [AttributeUsage(ValidTargets, AllowMultiple = false, Inherited = false)] + public sealed class ModelBinderAttribute : CustomModelBinderAttribute + { + public ModelBinderAttribute(Type binderType) + { + if (binderType == null) + { + throw new ArgumentNullException("binderType"); + } + if (!typeof(IModelBinder).IsAssignableFrom(binderType)) + { + string message = String.Format(CultureInfo.CurrentCulture, + MvcResources.ModelBinderAttribute_TypeNotIModelBinder, binderType.FullName); + throw new ArgumentException(message, "binderType"); + } + + BinderType = binderType; + } + + public Type BinderType { get; private set; } + + public override IModelBinder GetBinder() + { + try + { + return (IModelBinder)Activator.CreateInstance(BinderType); + } + catch (Exception ex) + { + throw new InvalidOperationException( + String.Format( + CultureInfo.CurrentCulture, + MvcResources.ModelBinderAttribute_ErrorCreatingModelBinder, + BinderType.FullName), + ex); + } + } + } +} diff --git a/src/System.Web.Mvc/ModelBinderDictionary.cs b/src/System.Web.Mvc/ModelBinderDictionary.cs new file mode 100644 index 00000000..273787e5 --- /dev/null +++ b/src/System.Web.Mvc/ModelBinderDictionary.cs @@ -0,0 +1,167 @@ +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + public class ModelBinderDictionary : IDictionary<Type, IModelBinder> + { + private readonly Dictionary<Type, IModelBinder> _innerDictionary = new Dictionary<Type, IModelBinder>(); + private IModelBinder _defaultBinder; + private ModelBinderProviderCollection _modelBinderProviders; + + public ModelBinderDictionary() + : this(ModelBinderProviders.BinderProviders) + { + } + + internal ModelBinderDictionary(ModelBinderProviderCollection modelBinderProviders) + { + _modelBinderProviders = modelBinderProviders; + } + + public int Count + { + get { return _innerDictionary.Count; } + } + + public IModelBinder DefaultBinder + { + get + { + if (_defaultBinder == null) + { + _defaultBinder = new DefaultModelBinder(); + } + return _defaultBinder; + } + set { _defaultBinder = value; } + } + + public bool IsReadOnly + { + get { return ((IDictionary<Type, IModelBinder>)_innerDictionary).IsReadOnly; } + } + + public ICollection<Type> Keys + { + get { return _innerDictionary.Keys; } + } + + public ICollection<IModelBinder> Values + { + get { return _innerDictionary.Values; } + } + + public IModelBinder this[Type key] + { + get + { + IModelBinder binder; + _innerDictionary.TryGetValue(key, out binder); + return binder; + } + set { _innerDictionary[key] = value; } + } + + public void Add(KeyValuePair<Type, IModelBinder> item) + { + ((IDictionary<Type, IModelBinder>)_innerDictionary).Add(item); + } + + public void Add(Type key, IModelBinder value) + { + _innerDictionary.Add(key, value); + } + + public void Clear() + { + _innerDictionary.Clear(); + } + + public bool Contains(KeyValuePair<Type, IModelBinder> item) + { + return ((IDictionary<Type, IModelBinder>)_innerDictionary).Contains(item); + } + + public bool ContainsKey(Type key) + { + return _innerDictionary.ContainsKey(key); + } + + public void CopyTo(KeyValuePair<Type, IModelBinder>[] array, int arrayIndex) + { + ((IDictionary<Type, IModelBinder>)_innerDictionary).CopyTo(array, arrayIndex); + } + + public IModelBinder GetBinder(Type modelType) + { + return GetBinder(modelType, true /* fallbackToDefault */); + } + + public virtual IModelBinder GetBinder(Type modelType, bool fallbackToDefault) + { + if (modelType == null) + { + throw new ArgumentNullException("modelType"); + } + + return GetBinder(modelType, (fallbackToDefault) ? DefaultBinder : null); + } + + private IModelBinder GetBinder(Type modelType, IModelBinder fallbackBinder) + { + // Try to look up a binder for this type. We use this order of precedence: + // 1. Binder returned from provider + // 2. Binder registered in the global table + // 3. Binder attribute defined on the type + // 4. Supplied fallback binder + + IModelBinder binder = _modelBinderProviders.GetBinder(modelType); + if (binder != null) + { + return binder; + } + + if (_innerDictionary.TryGetValue(modelType, out binder)) + { + return binder; + } + + binder = ModelBinders.GetBinderFromAttributes(modelType, + () => String.Format(CultureInfo.CurrentCulture, MvcResources.ModelBinderDictionary_MultipleAttributes, modelType.FullName)); + + return binder ?? fallbackBinder; + } + + public IEnumerator<KeyValuePair<Type, IModelBinder>> GetEnumerator() + { + return _innerDictionary.GetEnumerator(); + } + + public bool Remove(KeyValuePair<Type, IModelBinder> item) + { + return ((IDictionary<Type, IModelBinder>)_innerDictionary).Remove(item); + } + + public bool Remove(Type key) + { + return _innerDictionary.Remove(key); + } + + public bool TryGetValue(Type key, out IModelBinder value) + { + return _innerDictionary.TryGetValue(key, out value); + } + + #region IEnumerable Members + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_innerDictionary).GetEnumerator(); + } + + #endregion + } +} diff --git a/src/System.Web.Mvc/ModelBinderProviderCollection.cs b/src/System.Web.Mvc/ModelBinderProviderCollection.cs new file mode 100644 index 00000000..eb3117a3 --- /dev/null +++ b/src/System.Web.Mvc/ModelBinderProviderCollection.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +namespace System.Web.Mvc +{ + public class ModelBinderProviderCollection : Collection<IModelBinderProvider> + { + private IResolver<IEnumerable<IModelBinderProvider>> _serviceResolver; + + public ModelBinderProviderCollection() + { + _serviceResolver = new MultiServiceResolver<IModelBinderProvider>(() => Items); + } + + public ModelBinderProviderCollection(IList<IModelBinderProvider> list) + : base(list) + { + _serviceResolver = new MultiServiceResolver<IModelBinderProvider>(() => Items); + } + + internal ModelBinderProviderCollection(IResolver<IEnumerable<IModelBinderProvider>> resolver, params IModelBinderProvider[] providers) + : base(providers) + { + _serviceResolver = resolver ?? new MultiServiceResolver<IModelBinderProvider>(() => Items); + } + + private IEnumerable<IModelBinderProvider> CombinedItems + { + get { return _serviceResolver.Current; } + } + + protected override void InsertItem(int index, IModelBinderProvider item) + { + if (item == null) + { + throw new ArgumentNullException("item"); + } + base.InsertItem(index, item); + } + + protected override void SetItem(int index, IModelBinderProvider item) + { + if (item == null) + { + throw new ArgumentNullException("item"); + } + base.SetItem(index, item); + } + + public IModelBinder GetBinder(Type modelType) + { + if (modelType == null) + { + throw new ArgumentNullException("modelType"); + } + + var modelBinders = from providers in CombinedItems + let modelBinder = providers.GetBinder(modelType) + where modelBinder != null + select modelBinder; + + return modelBinders.FirstOrDefault(); + } + } +} diff --git a/src/System.Web.Mvc/ModelBinderProviders.cs b/src/System.Web.Mvc/ModelBinderProviders.cs new file mode 100644 index 00000000..dcbed64e --- /dev/null +++ b/src/System.Web.Mvc/ModelBinderProviders.cs @@ -0,0 +1,14 @@ +namespace System.Web.Mvc +{ + public static class ModelBinderProviders + { + private static readonly ModelBinderProviderCollection _binderProviders = new ModelBinderProviderCollection + { + }; + + public static ModelBinderProviderCollection BinderProviders + { + get { return _binderProviders; } + } + } +} diff --git a/src/System.Web.Mvc/ModelBinders.cs b/src/System.Web.Mvc/ModelBinders.cs new file mode 100644 index 00000000..da986a42 --- /dev/null +++ b/src/System.Web.Mvc/ModelBinders.cs @@ -0,0 +1,71 @@ +using System.ComponentModel; +using System.Data.Linq; +using System.Linq; +using System.Reflection; +using System.Threading; + +namespace System.Web.Mvc +{ + public static class ModelBinders + { + private static readonly ModelBinderDictionary _binders = CreateDefaultBinderDictionary(); + + public static ModelBinderDictionary Binders + { + get { return _binders; } + } + + internal static IModelBinder GetBinderFromAttributes(Type type, Func<string> errorMessageAccessor) + { + AttributeCollection allAttrs = TypeDescriptorHelper.Get(type).GetAttributes(); + CustomModelBinderAttribute[] filteredAttrs = allAttrs.OfType<CustomModelBinderAttribute>().ToArray(); + return GetBinderFromAttributesImpl(filteredAttrs, errorMessageAccessor); + } + + internal static IModelBinder GetBinderFromAttributes(ICustomAttributeProvider element, Func<string> errorMessageAccessor) + { + CustomModelBinderAttribute[] attrs = (CustomModelBinderAttribute[])element.GetCustomAttributes(typeof(CustomModelBinderAttribute), true /* inherit */); + return GetBinderFromAttributesImpl(attrs, errorMessageAccessor); + } + + private static IModelBinder GetBinderFromAttributesImpl(CustomModelBinderAttribute[] attrs, Func<string> errorMessageAccessor) + { + // this method is used to get a custom binder based on the attributes of the element passed to it. + // it will return null if a binder cannot be detected based on the attributes alone. + + if (attrs == null) + { + return null; + } + + switch (attrs.Length) + { + case 0: + return null; + + case 1: + IModelBinder binder = attrs[0].GetBinder(); + return binder; + + default: + string errorMessage = errorMessageAccessor(); + throw new InvalidOperationException(errorMessage); + } + } + + private static ModelBinderDictionary CreateDefaultBinderDictionary() + { + // We can't add a binder to the HttpPostedFileBase type as an attribute, so we'll just + // prepopulate the dictionary as a convenience to users. + + ModelBinderDictionary binders = new ModelBinderDictionary() + { + { typeof(HttpPostedFileBase), new HttpPostedFileBaseModelBinder() }, + { typeof(byte[]), new ByteArrayModelBinder() }, + { typeof(Binary), new LinqBinaryModelBinder() }, + { typeof(CancellationToken), new CancellationTokenModelBinder() } + }; + return binders; + } + } +} diff --git a/src/System.Web.Mvc/ModelBindingContext.cs b/src/System.Web.Mvc/ModelBindingContext.cs new file mode 100644 index 00000000..e57be059 --- /dev/null +++ b/src/System.Web.Mvc/ModelBindingContext.cs @@ -0,0 +1,138 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + public class ModelBindingContext + { + private static readonly Predicate<string> _defaultPropertyFilter = _ => true; + + private string _modelName; + private ModelStateDictionary _modelState; + private Predicate<string> _propertyFilter; + private Dictionary<string, ModelMetadata> _propertyMetadata; + + public ModelBindingContext() + : this(null) + { + } + + // copies certain values that won't change between parent and child objects, + // e.g. ValueProvider, ModelState + public ModelBindingContext(ModelBindingContext bindingContext) + { + if (bindingContext != null) + { + ModelState = bindingContext.ModelState; + ValueProvider = bindingContext.ValueProvider; + } + } + + public bool FallbackToEmptyPrefix { get; set; } + + [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "value", Justification = "Cannot remove setter as that's a breaking change")] + public object Model + { + get { return ModelMetadata.Model; } + set { throw new InvalidOperationException(MvcResources.ModelMetadata_PropertyNotSettable); } + } + + public ModelMetadata ModelMetadata { get; set; } + + public string ModelName + { + get + { + if (_modelName == null) + { + _modelName = String.Empty; + } + return _modelName; + } + set { _modelName = value; } + } + + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "The containing type is mutable.")] + public ModelStateDictionary ModelState + { + get + { + if (_modelState == null) + { + _modelState = new ModelStateDictionary(); + } + return _modelState; + } + set { _modelState = value; } + } + + [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "value", Justification = "Cannot remove setter as that's a breaking change")] + public Type ModelType + { + get { return ModelMetadata.ModelType; } + set { throw new InvalidOperationException(MvcResources.ModelMetadata_PropertyNotSettable); } + } + + public Predicate<string> PropertyFilter + { + get + { + if (_propertyFilter == null) + { + _propertyFilter = _defaultPropertyFilter; + } + return _propertyFilter; + } + set { _propertyFilter = value; } + } + + public IDictionary<string, ModelMetadata> PropertyMetadata + { + get + { + if (_propertyMetadata == null) + { + _propertyMetadata = ModelMetadata.Properties.ToDictionary(m => m.PropertyName, StringComparer.OrdinalIgnoreCase); + } + + return _propertyMetadata; + } + } + + public IValueProvider ValueProvider { get; set; } + + internal IUnvalidatedValueProvider UnvalidatedValueProvider + { + get { return (ValueProvider as IUnvalidatedValueProvider) ?? new UnvalidatedValueProviderWrapper(ValueProvider); } + } + + // Used to wrap an IValueProvider in an IUnvalidatedValueProvider + private sealed class UnvalidatedValueProviderWrapper : IValueProvider, IUnvalidatedValueProvider + { + private readonly IValueProvider _backingProvider; + + public UnvalidatedValueProviderWrapper(IValueProvider backingProvider) + { + _backingProvider = backingProvider; + } + + public ValueProviderResult GetValue(string key, bool skipValidation) + { + // 'skipValidation' isn't understood by the backing provider and can be ignored + return GetValue(key); + } + + public bool ContainsPrefix(string prefix) + { + return _backingProvider.ContainsPrefix(prefix); + } + + public ValueProviderResult GetValue(string key) + { + return _backingProvider.GetValue(key); + } + } + } +} diff --git a/src/System.Web.Mvc/ModelError.cs b/src/System.Web.Mvc/ModelError.cs new file mode 100644 index 00000000..d05f2509 --- /dev/null +++ b/src/System.Web.Mvc/ModelError.cs @@ -0,0 +1,31 @@ +namespace System.Web.Mvc +{ + [Serializable] + public class ModelError + { + public ModelError(Exception exception) + : this(exception, null /* errorMessage */) + { + } + + public ModelError(Exception exception, string errorMessage) + : this(errorMessage) + { + if (exception == null) + { + throw new ArgumentNullException("exception"); + } + + Exception = exception; + } + + public ModelError(string errorMessage) + { + ErrorMessage = errorMessage ?? String.Empty; + } + + public Exception Exception { get; private set; } + + public string ErrorMessage { get; private set; } + } +} diff --git a/src/System.Web.Mvc/ModelErrorCollection.cs b/src/System.Web.Mvc/ModelErrorCollection.cs new file mode 100644 index 00000000..e6a7867d --- /dev/null +++ b/src/System.Web.Mvc/ModelErrorCollection.cs @@ -0,0 +1,18 @@ +using System.Collections.ObjectModel; + +namespace System.Web.Mvc +{ + [Serializable] + public class ModelErrorCollection : Collection<ModelError> + { + public void Add(Exception exception) + { + Add(new ModelError(exception)); + } + + public void Add(string errorMessage) + { + Add(new ModelError(errorMessage)); + } + } +} diff --git a/src/System.Web.Mvc/ModelMetadata.cs b/src/System.Web.Mvc/ModelMetadata.cs new file mode 100644 index 00000000..4a2ad50c --- /dev/null +++ b/src/System.Web.Mvc/ModelMetadata.cs @@ -0,0 +1,407 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Web.Mvc.ExpressionUtil; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + public class ModelMetadata + { + public const int DefaultOrder = 10000; + + private readonly Type _containerType; + private readonly Type _modelType; + private readonly string _propertyName; + + /// <summary> + /// Explicit backing store for the things we want initialized by default, so don't have to call + /// the protected virtual setters of an auto-generated property + /// </summary> + private Dictionary<string, object> _additionalValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); + private bool _convertEmptyStringToNull = true; + private bool _isRequired; + private object _model; + private Func<object> _modelAccessor; + private int _order = DefaultOrder; + private IEnumerable<ModelMetadata> _properties; + private Type _realModelType; + private bool _requestValidationEnabled = true; + private bool _showForDisplay = true; + private bool _showForEdit = true; + private string _simpleDisplayText; + + public ModelMetadata(ModelMetadataProvider provider, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName) + { + if (provider == null) + { + throw new ArgumentNullException("provider"); + } + if (modelType == null) + { + throw new ArgumentNullException("modelType"); + } + + Provider = provider; + + _containerType = containerType; + _isRequired = !TypeHelpers.TypeAllowsNullValue(modelType); + _modelAccessor = modelAccessor; + _modelType = modelType; + _propertyName = propertyName; + } + + public virtual Dictionary<string, object> AdditionalValues + { + get { return _additionalValues; } + } + + public Type ContainerType + { + get { return _containerType; } + } + + public virtual bool ConvertEmptyStringToNull + { + get { return _convertEmptyStringToNull; } + set { _convertEmptyStringToNull = value; } + } + + public virtual string DataTypeName { get; set; } + + public virtual string Description { get; set; } + + public virtual string DisplayFormatString { get; set; } + + [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "The method is a delegating helper to choose among multiple property values")] + public virtual string DisplayName { get; set; } + + public virtual string EditFormatString { get; set; } + + public virtual bool HideSurroundingHtml { get; set; } + + public virtual bool IsComplexType + { + get { return !(TypeDescriptor.GetConverter(ModelType).CanConvertFrom(typeof(string))); } + } + + public bool IsNullableValueType + { + get { return TypeHelpers.IsNullableValueType(ModelType); } + } + + public virtual bool IsReadOnly { get; set; } + + public virtual bool IsRequired + { + get { return _isRequired; } + set { _isRequired = value; } + } + + public object Model + { + get + { + if (_modelAccessor != null) + { + _model = _modelAccessor(); + _modelAccessor = null; + } + return _model; + } + set + { + _model = value; + _modelAccessor = null; + _properties = null; + _realModelType = null; + } + } + + public Type ModelType + { + get { return _modelType; } + } + + public virtual string NullDisplayText { get; set; } + + public virtual int Order + { + get { return _order; } + set { _order = value; } + } + + public virtual IEnumerable<ModelMetadata> Properties + { + get + { + if (_properties == null) + { + _properties = Provider.GetMetadataForProperties(Model, RealModelType).OrderBy(m => m.Order); + } + return _properties; + } + } + + public string PropertyName + { + get { return _propertyName; } + } + + protected ModelMetadataProvider Provider { get; set; } + + internal Type RealModelType + { + get + { + if (_realModelType == null) + { + _realModelType = ModelType; + + // Don't call GetType() if the model is Nullable<T>, because it will + // turn Nullable<T> into T for non-null values + if (Model != null && !TypeHelpers.IsNullableValueType(ModelType)) + { + _realModelType = Model.GetType(); + } + } + + return _realModelType; + } + } + + public virtual bool RequestValidationEnabled + { + get { return _requestValidationEnabled; } + set { _requestValidationEnabled = value; } + } + + public virtual string ShortDisplayName { get; set; } + + public virtual bool ShowForDisplay + { + get { return _showForDisplay; } + set { _showForDisplay = value; } + } + + public virtual bool ShowForEdit + { + get { return _showForEdit; } + set { _showForEdit = value; } + } + + [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "This property delegates to the method when the user has not yet set a simple display text value.")] + public virtual string SimpleDisplayText + { + get + { + if (_simpleDisplayText == null) + { + _simpleDisplayText = GetSimpleDisplayText(); + } + return _simpleDisplayText; + } + set { _simpleDisplayText = value; } + } + + public virtual string TemplateHint { get; set; } + + public virtual string Watermark { get; set; } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")] + public static ModelMetadata FromLambdaExpression<TParameter, TValue>(Expression<Func<TParameter, TValue>> expression, + ViewDataDictionary<TParameter> viewData) + { + return FromLambdaExpression(expression, viewData, metadataProvider: null); + } + + internal static ModelMetadata FromLambdaExpression<TParameter, TValue>(Expression<Func<TParameter, TValue>> expression, + ViewDataDictionary<TParameter> viewData, + ModelMetadataProvider metadataProvider) + { + if (expression == null) + { + throw new ArgumentNullException("expression"); + } + if (viewData == null) + { + throw new ArgumentNullException("viewData"); + } + + string propertyName = null; + Type containerType = null; + bool legalExpression = false; + + // Need to verify the expression is valid; it needs to at least end in something + // that we can convert to a meaningful string for model binding purposes + + switch (expression.Body.NodeType) + { + case ExpressionType.ArrayIndex: + // ArrayIndex always means a single-dimensional indexer; multi-dimensional indexer is a method call to Get() + legalExpression = true; + break; + + case ExpressionType.Call: + // Only legal method call is a single argument indexer/DefaultMember call + legalExpression = ExpressionHelper.IsSingleArgumentIndexer(expression.Body); + break; + + case ExpressionType.MemberAccess: + // Property/field access is always legal + MemberExpression memberExpression = (MemberExpression)expression.Body; + propertyName = memberExpression.Member is PropertyInfo ? memberExpression.Member.Name : null; + containerType = memberExpression.Expression.Type; + legalExpression = true; + break; + + case ExpressionType.Parameter: + // Parameter expression means "model => model", so we delegate to FromModel + return FromModel(viewData, metadataProvider); + } + + if (!legalExpression) + { + throw new InvalidOperationException(MvcResources.TemplateHelpers_TemplateLimitations); + } + + TParameter container = viewData.Model; + Func<object> modelAccessor = () => + { + try + { + return CachedExpressionCompiler.Process(expression)(container); + } + catch (NullReferenceException) + { + return null; + } + }; + + return GetMetadataFromProvider(modelAccessor, typeof(TValue), propertyName, containerType, metadataProvider); + } + + private static ModelMetadata FromModel(ViewDataDictionary viewData, ModelMetadataProvider metadataProvider) + { + return viewData.ModelMetadata ?? GetMetadataFromProvider(null, typeof(string), null, null, metadataProvider); + } + + public static ModelMetadata FromStringExpression(string expression, ViewDataDictionary viewData) + { + return FromStringExpression(expression, viewData, metadataProvider: null); + } + + internal static ModelMetadata FromStringExpression(string expression, ViewDataDictionary viewData, ModelMetadataProvider metadataProvider) + { + if (expression == null) + { + throw new ArgumentNullException("expression"); + } + if (viewData == null) + { + throw new ArgumentNullException("viewData"); + } + if (expression.Length == 0) + { + // Empty string really means "model metadata for the current model" + return FromModel(viewData, metadataProvider); + } + + ViewDataInfo vdi = viewData.GetViewDataInfo(expression); + Type containerType = null; + Type modelType = null; + Func<object> modelAccessor = null; + string propertyName = null; + + if (vdi != null) + { + if (vdi.Container != null) + { + containerType = vdi.Container.GetType(); + } + + modelAccessor = () => vdi.Value; + + if (vdi.PropertyDescriptor != null) + { + propertyName = vdi.PropertyDescriptor.Name; + modelType = vdi.PropertyDescriptor.PropertyType; + } + else if (vdi.Value != null) + { + // We only need to delay accessing properties (for LINQ to SQL) + modelType = vdi.Value.GetType(); + } + } + else if (viewData.ModelMetadata != null) + { + // Try getting a property from ModelMetadata if we couldn't find an answer in ViewData + ModelMetadata propertyMetadata = viewData.ModelMetadata.Properties.Where(p => p.PropertyName == expression).FirstOrDefault(); + if (propertyMetadata != null) + { + return propertyMetadata; + } + } + + return GetMetadataFromProvider(modelAccessor, modelType ?? typeof(string), propertyName, containerType, metadataProvider); + } + + [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "The method is a delegating helper to choose among multiple property values")] + public string GetDisplayName() + { + return DisplayName ?? PropertyName ?? ModelType.Name; + } + + private static ModelMetadata GetMetadataFromProvider(Func<object> modelAccessor, Type modelType, string propertyName, Type containerType, ModelMetadataProvider metadataProvider) + { + metadataProvider = metadataProvider ?? ModelMetadataProviders.Current; + if (containerType != null && !String.IsNullOrEmpty(propertyName)) + { + return metadataProvider.GetMetadataForProperty(modelAccessor, containerType, propertyName); + } + return metadataProvider.GetMetadataForType(modelAccessor, modelType); + } + + [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This method is used to resolve the simple display text when it was not explicitly set through other means.")] + protected virtual string GetSimpleDisplayText() + { + if (Model == null) + { + return NullDisplayText; + } + + string toStringResult = Convert.ToString(Model, CultureInfo.CurrentCulture); + if (toStringResult == null) + { + return String.Empty; + } + + if (!toStringResult.Equals(Model.GetType().FullName, StringComparison.Ordinal)) + { + return toStringResult; + } + + ModelMetadata firstProperty = Properties.FirstOrDefault(); + if (firstProperty == null) + { + return String.Empty; + } + + if (firstProperty.Model == null) + { + return firstProperty.NullDisplayText; + } + + return Convert.ToString(firstProperty.Model, CultureInfo.CurrentCulture); + } + + public virtual IEnumerable<ModelValidator> GetValidators(ControllerContext context) + { + return ModelValidatorProviders.Providers.GetValidators(this, context); + } + } +} diff --git a/src/System.Web.Mvc/ModelMetadataProvider.cs b/src/System.Web.Mvc/ModelMetadataProvider.cs new file mode 100644 index 00000000..0db73fba --- /dev/null +++ b/src/System.Web.Mvc/ModelMetadataProvider.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace System.Web.Mvc +{ + public abstract class ModelMetadataProvider + { + public abstract IEnumerable<ModelMetadata> GetMetadataForProperties(object container, Type containerType); + + public abstract ModelMetadata GetMetadataForProperty(Func<object> modelAccessor, Type containerType, string propertyName); + + public abstract ModelMetadata GetMetadataForType(Func<object> modelAccessor, Type modelType); + } +} diff --git a/src/System.Web.Mvc/ModelMetadataProviders.cs b/src/System.Web.Mvc/ModelMetadataProviders.cs new file mode 100644 index 00000000..d9cd8b92 --- /dev/null +++ b/src/System.Web.Mvc/ModelMetadataProviders.cs @@ -0,0 +1,29 @@ +namespace System.Web.Mvc +{ + public class ModelMetadataProviders + { + private static ModelMetadataProviders _instance = new ModelMetadataProviders(); + private ModelMetadataProvider _currentProvider; + private IResolver<ModelMetadataProvider> _resolver; + + internal ModelMetadataProviders(IResolver<ModelMetadataProvider> resolver = null) + { + _resolver = resolver ?? new SingleServiceResolver<ModelMetadataProvider>( + () => _currentProvider, + new CachedDataAnnotationsModelMetadataProvider(), + "ModelMetadataProviders.Current"); + } + + public static ModelMetadataProvider Current + { + get { return _instance.CurrentInternal; } + set { _instance.CurrentInternal = value; } + } + + internal ModelMetadataProvider CurrentInternal + { + get { return _resolver.Current; } + set { _currentProvider = value ?? new EmptyModelMetadataProvider(); } + } + } +} diff --git a/src/System.Web.Mvc/ModelState.cs b/src/System.Web.Mvc/ModelState.cs new file mode 100644 index 00000000..ae1d28d4 --- /dev/null +++ b/src/System.Web.Mvc/ModelState.cs @@ -0,0 +1,15 @@ +namespace System.Web.Mvc +{ + [Serializable] + public class ModelState + { + private ModelErrorCollection _errors = new ModelErrorCollection(); + + public ValueProviderResult Value { get; set; } + + public ModelErrorCollection Errors + { + get { return _errors; } + } + } +} diff --git a/src/System.Web.Mvc/ModelStateDictionary.cs b/src/System.Web.Mvc/ModelStateDictionary.cs new file mode 100644 index 00000000..f1b149f2 --- /dev/null +++ b/src/System.Web.Mvc/ModelStateDictionary.cs @@ -0,0 +1,180 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace System.Web.Mvc +{ + [Serializable] + public class ModelStateDictionary : IDictionary<string, ModelState> + { + private readonly Dictionary<string, ModelState> _innerDictionary = new Dictionary<string, ModelState>(StringComparer.OrdinalIgnoreCase); + + public ModelStateDictionary() + { + } + + public ModelStateDictionary(ModelStateDictionary dictionary) + { + if (dictionary == null) + { + throw new ArgumentNullException("dictionary"); + } + + foreach (var entry in dictionary) + { + _innerDictionary.Add(entry.Key, entry.Value); + } + } + + public int Count + { + get { return _innerDictionary.Count; } + } + + public bool IsReadOnly + { + get { return ((IDictionary<string, ModelState>)_innerDictionary).IsReadOnly; } + } + + public bool IsValid + { + get { return Values.All(modelState => modelState.Errors.Count == 0); } + } + + public ICollection<string> Keys + { + get { return _innerDictionary.Keys; } + } + + public ICollection<ModelState> Values + { + get { return _innerDictionary.Values; } + } + + public ModelState this[string key] + { + get + { + ModelState value; + _innerDictionary.TryGetValue(key, out value); + return value; + } + set { _innerDictionary[key] = value; } + } + + public void Add(KeyValuePair<string, ModelState> item) + { + ((IDictionary<string, ModelState>)_innerDictionary).Add(item); + } + + public void Add(string key, ModelState value) + { + _innerDictionary.Add(key, value); + } + + public void AddModelError(string key, Exception exception) + { + GetModelStateForKey(key).Errors.Add(exception); + } + + public void AddModelError(string key, string errorMessage) + { + GetModelStateForKey(key).Errors.Add(errorMessage); + } + + public void Clear() + { + _innerDictionary.Clear(); + } + + public bool Contains(KeyValuePair<string, ModelState> item) + { + return ((IDictionary<string, ModelState>)_innerDictionary).Contains(item); + } + + public bool ContainsKey(string key) + { + return _innerDictionary.ContainsKey(key); + } + + public void CopyTo(KeyValuePair<string, ModelState>[] array, int arrayIndex) + { + ((IDictionary<string, ModelState>)_innerDictionary).CopyTo(array, arrayIndex); + } + + public IEnumerator<KeyValuePair<string, ModelState>> GetEnumerator() + { + return _innerDictionary.GetEnumerator(); + } + + private ModelState GetModelStateForKey(string key) + { + if (key == null) + { + throw new ArgumentNullException("key"); + } + + ModelState modelState; + if (!TryGetValue(key, out modelState)) + { + modelState = new ModelState(); + this[key] = modelState; + } + + return modelState; + } + + public bool IsValidField(string key) + { + if (key == null) + { + throw new ArgumentNullException("key"); + } + + // if the key is not found in the dictionary, we just say that it's valid (since there are no errors) + return DictionaryHelpers.FindKeysWithPrefix(this, key).All(entry => entry.Value.Errors.Count == 0); + } + + public void Merge(ModelStateDictionary dictionary) + { + if (dictionary == null) + { + return; + } + + foreach (var entry in dictionary) + { + this[entry.Key] = entry.Value; + } + } + + public bool Remove(KeyValuePair<string, ModelState> item) + { + return ((IDictionary<string, ModelState>)_innerDictionary).Remove(item); + } + + public bool Remove(string key) + { + return _innerDictionary.Remove(key); + } + + public void SetModelValue(string key, ValueProviderResult value) + { + GetModelStateForKey(key).Value = value; + } + + public bool TryGetValue(string key, out ModelState value) + { + return _innerDictionary.TryGetValue(key, out value); + } + + #region IEnumerable Members + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_innerDictionary).GetEnumerator(); + } + + #endregion + } +} diff --git a/src/System.Web.Mvc/ModelValidationResult.cs b/src/System.Web.Mvc/ModelValidationResult.cs new file mode 100644 index 00000000..f7815371 --- /dev/null +++ b/src/System.Web.Mvc/ModelValidationResult.cs @@ -0,0 +1,20 @@ +namespace System.Web.Mvc +{ + public class ModelValidationResult + { + private string _memberName; + private string _message; + + public string MemberName + { + get { return _memberName ?? String.Empty; } + set { _memberName = value; } + } + + public string Message + { + get { return _message ?? String.Empty; } + set { _message = value; } + } + } +} diff --git a/src/System.Web.Mvc/ModelValidator.cs b/src/System.Web.Mvc/ModelValidator.cs new file mode 100644 index 00000000..e5c9774c --- /dev/null +++ b/src/System.Web.Mvc/ModelValidator.cs @@ -0,0 +1,86 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace System.Web.Mvc +{ + public abstract class ModelValidator + { + protected ModelValidator(ModelMetadata metadata, ControllerContext controllerContext) + { + if (metadata == null) + { + throw new ArgumentNullException("metadata"); + } + if (controllerContext == null) + { + throw new ArgumentNullException("controllerContext"); + } + + Metadata = metadata; + ControllerContext = controllerContext; + } + + protected internal ControllerContext ControllerContext { get; private set; } + + public virtual bool IsRequired + { + get { return false; } + } + + protected internal ModelMetadata Metadata { get; private set; } + + [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This method may perform non-trivial work.")] + public virtual IEnumerable<ModelClientValidationRule> GetClientValidationRules() + { + return Enumerable.Empty<ModelClientValidationRule>(); + } + + public static ModelValidator GetModelValidator(ModelMetadata metadata, ControllerContext context) + { + return new CompositeModelValidator(metadata, context); + } + + public abstract IEnumerable<ModelValidationResult> Validate(object container); + + private class CompositeModelValidator : ModelValidator + { + public CompositeModelValidator(ModelMetadata metadata, ControllerContext controllerContext) + : base(metadata, controllerContext) + { + } + + public override IEnumerable<ModelValidationResult> Validate(object container) + { + bool propertiesValid = true; + + foreach (ModelMetadata propertyMetadata in Metadata.Properties) + { + foreach (ModelValidator propertyValidator in propertyMetadata.GetValidators(ControllerContext)) + { + foreach (ModelValidationResult propertyResult in propertyValidator.Validate(Metadata.Model)) + { + propertiesValid = false; + yield return new ModelValidationResult + { + MemberName = DefaultModelBinder.CreateSubPropertyName(propertyMetadata.PropertyName, propertyResult.MemberName), + Message = propertyResult.Message + }; + } + } + } + + if (propertiesValid) + { + foreach (ModelValidator typeValidator in Metadata.GetValidators(ControllerContext)) + { + foreach (ModelValidationResult typeResult in typeValidator.Validate(container)) + { + yield return typeResult; + } + } + } + } + } + } +} diff --git a/src/System.Web.Mvc/ModelValidatorProvider.cs b/src/System.Web.Mvc/ModelValidatorProvider.cs new file mode 100644 index 00000000..932a091f --- /dev/null +++ b/src/System.Web.Mvc/ModelValidatorProvider.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace System.Web.Mvc +{ + public abstract class ModelValidatorProvider + { + public abstract IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context); + } +} diff --git a/src/System.Web.Mvc/ModelValidatorProviderCollection.cs b/src/System.Web.Mvc/ModelValidatorProviderCollection.cs new file mode 100644 index 00000000..666fdbf0 --- /dev/null +++ b/src/System.Web.Mvc/ModelValidatorProviderCollection.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +namespace System.Web.Mvc +{ + public class ModelValidatorProviderCollection : Collection<ModelValidatorProvider> + { + private IResolver<IEnumerable<ModelValidatorProvider>> _serviceResolver; + + public ModelValidatorProviderCollection() + { + _serviceResolver = new MultiServiceResolver<ModelValidatorProvider>(() => Items); + } + + public ModelValidatorProviderCollection(IList<ModelValidatorProvider> list) + : base(list) + { + _serviceResolver = new MultiServiceResolver<ModelValidatorProvider>(() => Items); + } + + internal ModelValidatorProviderCollection(IResolver<IEnumerable<ModelValidatorProvider>> serviceResolver, params ModelValidatorProvider[] validatorProvidors) + : base(validatorProvidors) + { + _serviceResolver = serviceResolver ?? new MultiServiceResolver<ModelValidatorProvider>(() => Items); + } + + private IEnumerable<ModelValidatorProvider> CombinedItems + { + get { return _serviceResolver.Current; } + } + + protected override void InsertItem(int index, ModelValidatorProvider item) + { + if (item == null) + { + throw new ArgumentNullException("item"); + } + base.InsertItem(index, item); + } + + protected override void SetItem(int index, ModelValidatorProvider item) + { + if (item == null) + { + throw new ArgumentNullException("item"); + } + base.SetItem(index, item); + } + + public IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context) + { + return CombinedItems.SelectMany(provider => provider.GetValidators(metadata, context)); + } + } +} diff --git a/src/System.Web.Mvc/ModelValidatorProviders.cs b/src/System.Web.Mvc/ModelValidatorProviders.cs new file mode 100644 index 00000000..66852578 --- /dev/null +++ b/src/System.Web.Mvc/ModelValidatorProviders.cs @@ -0,0 +1,17 @@ +namespace System.Web.Mvc +{ + public static class ModelValidatorProviders + { + private static readonly ModelValidatorProviderCollection _providers = new ModelValidatorProviderCollection() + { + new DataAnnotationsModelValidatorProvider(), + new DataErrorInfoModelValidatorProvider(), + new ClientDataTypeModelValidatorProvider() + }; + + public static ModelValidatorProviderCollection Providers + { + get { return _providers; } + } + } +} diff --git a/src/System.Web.Mvc/MultiSelectList.cs b/src/System.Web.Mvc/MultiSelectList.cs new file mode 100644 index 00000000..eedb0100 --- /dev/null +++ b/src/System.Web.Mvc/MultiSelectList.cs @@ -0,0 +1,118 @@ +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Web.UI; + +namespace System.Web.Mvc +{ + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Multi", Justification = "FxCop won't accept this in the custom dictionary, so we're suppressing it in source")] + [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", Justification = "This is a shipped API")] + public class MultiSelectList : IEnumerable<SelectListItem> + { + public MultiSelectList(IEnumerable items) + : this(items, null /* selectedValues */) + { + } + + public MultiSelectList(IEnumerable items, IEnumerable selectedValues) + : this(items, null /* dataValuefield */, null /* dataTextField */, selectedValues) + { + } + + public MultiSelectList(IEnumerable items, string dataValueField, string dataTextField) + : this(items, dataValueField, dataTextField, null /* selectedValues */) + { + } + + public MultiSelectList(IEnumerable items, string dataValueField, string dataTextField, IEnumerable selectedValues) + { + if (items == null) + { + throw new ArgumentNullException("items"); + } + + Items = items; + DataValueField = dataValueField; + DataTextField = dataTextField; + SelectedValues = selectedValues; + } + + public string DataTextField { get; private set; } + + public string DataValueField { get; private set; } + + public IEnumerable Items { get; private set; } + + public IEnumerable SelectedValues { get; private set; } + + public virtual IEnumerator<SelectListItem> GetEnumerator() + { + return GetListItems().GetEnumerator(); + } + + internal IList<SelectListItem> GetListItems() + { + return (!String.IsNullOrEmpty(DataValueField)) + ? GetListItemsWithValueField() + : GetListItemsWithoutValueField(); + } + + private IList<SelectListItem> GetListItemsWithValueField() + { + HashSet<string> selectedValues = new HashSet<string>(StringComparer.OrdinalIgnoreCase); + if (SelectedValues != null) + { + selectedValues.UnionWith(from object value in SelectedValues + select Convert.ToString(value, CultureInfo.CurrentCulture)); + } + + var listItems = from object item in Items + let value = Eval(item, DataValueField) + select new SelectListItem + { + Value = value, + Text = Eval(item, DataTextField), + Selected = selectedValues.Contains(value) + }; + return listItems.ToList(); + } + + private IList<SelectListItem> GetListItemsWithoutValueField() + { + HashSet<object> selectedValues = new HashSet<object>(); + if (SelectedValues != null) + { + selectedValues.UnionWith(SelectedValues.Cast<object>()); + } + + var listItems = from object item in Items + select new SelectListItem + { + Text = Eval(item, DataTextField), + Selected = selectedValues.Contains(item) + }; + return listItems.ToList(); + } + + private static string Eval(object container, string expression) + { + object value = container; + if (!String.IsNullOrEmpty(expression)) + { + value = DataBinder.Eval(container, expression); + } + return Convert.ToString(value, CultureInfo.CurrentCulture); + } + + #region IEnumerable Members + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion + } +} diff --git a/src/System.Web.Mvc/MultiServiceResolver.cs b/src/System.Web.Mvc/MultiServiceResolver.cs new file mode 100644 index 00000000..d3506fa2 --- /dev/null +++ b/src/System.Web.Mvc/MultiServiceResolver.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Linq; + +namespace System.Web.Mvc +{ + internal class MultiServiceResolver<TService> : IResolver<IEnumerable<TService>> + where TService : class + { + private IEnumerable<TService> _itemsFromService; + private Func<IEnumerable<TService>> _itemsThunk; + private Func<IDependencyResolver> _resolverThunk; + + public MultiServiceResolver(Func<IEnumerable<TService>> itemsThunk) + { + if (itemsThunk == null) + { + throw new ArgumentNullException("itemsThunk"); + } + + _itemsThunk = itemsThunk; + _resolverThunk = () => DependencyResolver.Current; + } + + internal MultiServiceResolver(Func<IEnumerable<TService>> itemsThunk, IDependencyResolver resolver) + : this(itemsThunk) + { + if (resolver != null) + { + _resolverThunk = () => resolver; + } + } + + public IEnumerable<TService> Current + { + get + { + if (_itemsFromService == null) + { + lock (_itemsThunk) + { + if (_itemsFromService == null) + { + _itemsFromService = _resolverThunk().GetServices<TService>(); + } + } + } + return _itemsFromService.Concat(_itemsThunk()); + } + } + } +} diff --git a/src/System.Web.Mvc/MvcFilter.cs b/src/System.Web.Mvc/MvcFilter.cs new file mode 100644 index 00000000..480b66ac --- /dev/null +++ b/src/System.Web.Mvc/MvcFilter.cs @@ -0,0 +1,19 @@ +namespace System.Web.Mvc +{ + public abstract class MvcFilter : IMvcFilter + { + protected MvcFilter() + { + } + + protected MvcFilter(bool allowMultiple, int order) + { + AllowMultiple = allowMultiple; + Order = order; + } + + public bool AllowMultiple { get; private set; } + + public int Order { get; private set; } + } +} diff --git a/src/System.Web.Mvc/MvcHandler.cs b/src/System.Web.Mvc/MvcHandler.cs new file mode 100644 index 00000000..de4a6d8b --- /dev/null +++ b/src/System.Web.Mvc/MvcHandler.cs @@ -0,0 +1,248 @@ +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Web.Mvc.Async; +using System.Web.Mvc.Properties; +using System.Web.Routing; +using System.Web.SessionState; +using Microsoft.Web.Infrastructure.DynamicValidationHelper; + +namespace System.Web.Mvc +{ + public class MvcHandler : IHttpAsyncHandler, IHttpHandler, IRequiresSessionState + { + private static readonly object _processRequestTag = new object(); + + internal static readonly string MvcVersion = GetMvcVersionString(); + public static readonly string MvcVersionHeaderName = "X-AspNetMvc-Version"; + private ControllerBuilder _controllerBuilder; + + public MvcHandler(RequestContext requestContext) + { + if (requestContext == null) + { + throw new ArgumentNullException("requestContext"); + } + + RequestContext = requestContext; + } + + internal ControllerBuilder ControllerBuilder + { + get + { + if (_controllerBuilder == null) + { + _controllerBuilder = ControllerBuilder.Current; + } + return _controllerBuilder; + } + set { _controllerBuilder = value; } + } + + public static bool DisableMvcResponseHeader { get; set; } + + protected virtual bool IsReusable + { + get { return false; } + } + + public RequestContext RequestContext { get; private set; } + + protected internal virtual void AddVersionHeader(HttpContextBase httpContext) + { + if (!DisableMvcResponseHeader) + { + httpContext.Response.AppendHeader(MvcVersionHeaderName, MvcVersion); + } + } + + protected virtual IAsyncResult BeginProcessRequest(HttpContext httpContext, AsyncCallback callback, object state) + { + HttpContextBase httpContextBase = new HttpContextWrapper(httpContext); + return BeginProcessRequest(httpContextBase, callback, state); + } + + protected internal virtual IAsyncResult BeginProcessRequest(HttpContextBase httpContext, AsyncCallback callback, object state) + { + return SecurityUtil.ProcessInApplicationTrust(() => + { + IController controller; + IControllerFactory factory; + ProcessRequestInit(httpContext, out controller, out factory); + + IAsyncController asyncController = controller as IAsyncController; + if (asyncController != null) + { + // asynchronous controller + BeginInvokeDelegate beginDelegate = delegate(AsyncCallback asyncCallback, object asyncState) + { + try + { + return asyncController.BeginExecute(RequestContext, asyncCallback, asyncState); + } + catch + { + factory.ReleaseController(asyncController); + throw; + } + }; + + EndInvokeDelegate endDelegate = delegate(IAsyncResult asyncResult) + { + try + { + asyncController.EndExecute(asyncResult); + } + finally + { + factory.ReleaseController(asyncController); + } + }; + + SynchronizationContext syncContext = SynchronizationContextUtil.GetSynchronizationContext(); + AsyncCallback newCallback = AsyncUtil.WrapCallbackForSynchronizedExecution(callback, syncContext); + return AsyncResultWrapper.Begin(newCallback, state, beginDelegate, endDelegate, _processRequestTag); + } + else + { + // synchronous controller + Action action = delegate + { + try + { + controller.Execute(RequestContext); + } + finally + { + factory.ReleaseController(controller); + } + }; + + return AsyncResultWrapper.BeginSynchronous(callback, state, action, _processRequestTag); + } + }); + } + + protected internal virtual void EndProcessRequest(IAsyncResult asyncResult) + { + SecurityUtil.ProcessInApplicationTrust(() => + { + AsyncResultWrapper.End(asyncResult, _processRequestTag); + }); + } + + private static string GetMvcVersionString() + { + // DevDiv 216459: + // This code originally used Assembly.GetName(), but that requires FileIOPermission, which isn't granted in + // medium trust. However, Assembly.FullName *is* accessible in medium trust. + return new AssemblyName(typeof(MvcHandler).Assembly.FullName).Version.ToString(2); + } + + protected virtual void ProcessRequest(HttpContext httpContext) + { + HttpContextBase httpContextBase = new HttpContextWrapper(httpContext); + ProcessRequest(httpContextBase); + } + + protected internal virtual void ProcessRequest(HttpContextBase httpContext) + { + SecurityUtil.ProcessInApplicationTrust(() => + { + IController controller; + IControllerFactory factory; + ProcessRequestInit(httpContext, out controller, out factory); + + try + { + controller.Execute(RequestContext); + } + finally + { + factory.ReleaseController(controller); + } + }); + } + + private void ProcessRequestInit(HttpContextBase httpContext, out IController controller, out IControllerFactory factory) + { + // If request validation has already been enabled, make it lazy. This allows attributes like [HttpPost] (which looks + // at Request.Form) to work correctly without triggering full validation. + // Tolerate null HttpContext for testing. + HttpContext currentContext = HttpContext.Current; + if (currentContext != null) + { + bool? isRequestValidationEnabled = ValidationUtility.IsValidationEnabled(currentContext); + if (isRequestValidationEnabled == true) + { + ValidationUtility.EnableDynamicValidation(currentContext); + } + } + + AddVersionHeader(httpContext); + RemoveOptionalRoutingParameters(); + + // Get the controller type + string controllerName = RequestContext.RouteData.GetRequiredString("controller"); + + // Instantiate the controller and call Execute + factory = ControllerBuilder.GetControllerFactory(); + controller = factory.CreateController(RequestContext, controllerName); + if (controller == null) + { + throw new InvalidOperationException( + String.Format( + CultureInfo.CurrentCulture, + MvcResources.ControllerBuilder_FactoryReturnedNull, + factory.GetType(), + controllerName)); + } + } + + private void RemoveOptionalRoutingParameters() + { + RouteValueDictionary rvd = RequestContext.RouteData.Values; + + // Get all keys for which the corresponding value is 'Optional'. + // ToArray() necessary so that we don't manipulate the dictionary while enumerating. + string[] matchingKeys = (from entry in rvd + where entry.Value == UrlParameter.Optional + select entry.Key).ToArray(); + + foreach (string key in matchingKeys) + { + rvd.Remove(key); + } + } + + #region IHttpHandler Members + + bool IHttpHandler.IsReusable + { + get { return IsReusable; } + } + + void IHttpHandler.ProcessRequest(HttpContext httpContext) + { + ProcessRequest(httpContext); + } + + #endregion + + #region IHttpAsyncHandler Members + + IAsyncResult IHttpAsyncHandler.BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData) + { + return BeginProcessRequest(context, cb, extraData); + } + + void IHttpAsyncHandler.EndProcessRequest(IAsyncResult result) + { + EndProcessRequest(result); + } + + #endregion + } +} diff --git a/src/System.Web.Mvc/MvcHtmlString.cs b/src/System.Web.Mvc/MvcHtmlString.cs new file mode 100644 index 00000000..8905cd04 --- /dev/null +++ b/src/System.Web.Mvc/MvcHtmlString.cs @@ -0,0 +1,28 @@ +using System.Diagnostics.CodeAnalysis; + +namespace System.Web.Mvc +{ + public sealed class MvcHtmlString : HtmlString + { + [SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification = "MvcHtmlString is immutable")] + public static readonly MvcHtmlString Empty = Create(String.Empty); + + private readonly string _value; + + public MvcHtmlString(string value) + : base(value ?? String.Empty) + { + _value = value ?? String.Empty; + } + + public static MvcHtmlString Create(string value) + { + return new MvcHtmlString(value); + } + + public static bool IsNullOrEmpty(MvcHtmlString value) + { + return (value == null || value._value.Length == 0); + } + } +} diff --git a/src/System.Web.Mvc/MvcHttpHandler.cs b/src/System.Web.Mvc/MvcHttpHandler.cs new file mode 100644 index 00000000..a64fb272 --- /dev/null +++ b/src/System.Web.Mvc/MvcHttpHandler.cs @@ -0,0 +1,105 @@ +using System.Web.Mvc.Async; +using System.Web.Routing; +using System.Web.SessionState; + +namespace System.Web.Mvc +{ + public class MvcHttpHandler : UrlRoutingHandler, IHttpAsyncHandler, IRequiresSessionState + { + private static readonly object _processRequestTag = new object(); + + protected virtual IAsyncResult BeginProcessRequest(HttpContext httpContext, AsyncCallback callback, object state) + { + HttpContextBase httpContextBase = new HttpContextWrapper(httpContext); + return BeginProcessRequest(httpContextBase, callback, state); + } + + protected internal virtual IAsyncResult BeginProcessRequest(HttpContextBase httpContext, AsyncCallback callback, object state) + { + IHttpHandler httpHandler = GetHttpHandler(httpContext); + IHttpAsyncHandler httpAsyncHandler = httpHandler as IHttpAsyncHandler; + + if (httpAsyncHandler != null) + { + // asynchronous handler + BeginInvokeDelegate beginDelegate = delegate(AsyncCallback asyncCallback, object asyncState) + { + return httpAsyncHandler.BeginProcessRequest(HttpContext.Current, asyncCallback, asyncState); + }; + EndInvokeDelegate endDelegate = delegate(IAsyncResult asyncResult) + { + httpAsyncHandler.EndProcessRequest(asyncResult); + }; + return AsyncResultWrapper.Begin(callback, state, beginDelegate, endDelegate, _processRequestTag); + } + else + { + // synchronous handler + Action action = delegate + { + httpHandler.ProcessRequest(HttpContext.Current); + }; + return AsyncResultWrapper.BeginSynchronous(callback, state, action, _processRequestTag); + } + } + + protected internal virtual void EndProcessRequest(IAsyncResult asyncResult) + { + AsyncResultWrapper.End(asyncResult, _processRequestTag); + } + + private static IHttpHandler GetHttpHandler(HttpContextBase httpContext) + { + DummyHttpHandler dummyHandler = new DummyHttpHandler(); + dummyHandler.PublicProcessRequest(httpContext); + return dummyHandler.HttpHandler; + } + + // synchronous code + protected override void VerifyAndProcessRequest(IHttpHandler httpHandler, HttpContextBase httpContext) + { + if (httpHandler == null) + { + throw new ArgumentNullException("httpHandler"); + } + + httpHandler.ProcessRequest(HttpContext.Current); + } + + #region IHttpAsyncHandler Members + + IAsyncResult IHttpAsyncHandler.BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData) + { + return BeginProcessRequest(context, cb, extraData); + } + + void IHttpAsyncHandler.EndProcessRequest(IAsyncResult result) + { + EndProcessRequest(result); + } + + #endregion + + // Since UrlRoutingHandler.ProcessRequest() does the heavy lifting of looking at the RouteCollection for + // a matching route, we need to call into it. However, that method is also responsible for kicking off + // the synchronous request, and we can't allow it to do that. The purpose of this dummy class is to run + // only the lookup portion of UrlRoutingHandler.ProcessRequest(), then intercept the handler it returns + // and execute it asynchronously. + + private sealed class DummyHttpHandler : UrlRoutingHandler + { + public IHttpHandler HttpHandler; + + public void PublicProcessRequest(HttpContextBase httpContext) + { + ProcessRequest(httpContext); + } + + protected override void VerifyAndProcessRequest(IHttpHandler httpHandler, HttpContextBase httpContext) + { + // don't process the request, just store a reference to it + HttpHandler = httpHandler; + } + } + } +} diff --git a/src/System.Web.Mvc/MvcRouteHandler.cs b/src/System.Web.Mvc/MvcRouteHandler.cs new file mode 100644 index 00000000..4b898b2c --- /dev/null +++ b/src/System.Web.Mvc/MvcRouteHandler.cs @@ -0,0 +1,47 @@ +using System.Web.Mvc.Properties; +using System.Web.Routing; +using System.Web.SessionState; + +namespace System.Web.Mvc +{ + public class MvcRouteHandler : IRouteHandler + { + private IControllerFactory _controllerFactory; + + public MvcRouteHandler() + { + } + + public MvcRouteHandler(IControllerFactory controllerFactory) + { + _controllerFactory = controllerFactory; + } + + protected virtual IHttpHandler GetHttpHandler(RequestContext requestContext) + { + requestContext.HttpContext.SetSessionStateBehavior(GetSessionStateBehavior(requestContext)); + return new MvcHandler(requestContext); + } + + protected virtual SessionStateBehavior GetSessionStateBehavior(RequestContext requestContext) + { + string controllerName = (string)requestContext.RouteData.Values["controller"]; + if (String.IsNullOrWhiteSpace(controllerName)) + { + throw new InvalidOperationException(MvcResources.MvcRouteHandler_RouteValuesHasNoController); + } + + IControllerFactory controllerFactory = _controllerFactory ?? ControllerBuilder.Current.GetControllerFactory(); + return controllerFactory.GetControllerSessionBehavior(requestContext, controllerName); + } + + #region IRouteHandler Members + + IHttpHandler IRouteHandler.GetHttpHandler(RequestContext requestContext) + { + return GetHttpHandler(requestContext); + } + + #endregion + } +} diff --git a/src/System.Web.Mvc/MvcWebRazorHostFactory.cs b/src/System.Web.Mvc/MvcWebRazorHostFactory.cs new file mode 100644 index 00000000..f26f01ac --- /dev/null +++ b/src/System.Web.Mvc/MvcWebRazorHostFactory.cs @@ -0,0 +1,20 @@ +using System.Web.Mvc.Razor; +using System.Web.WebPages.Razor; + +namespace System.Web.Mvc +{ + public class MvcWebRazorHostFactory : WebRazorHostFactory + { + public override WebPageRazorHost CreateHost(string virtualPath, string physicalPath) + { + WebPageRazorHost host = base.CreateHost(virtualPath, physicalPath); + + if (!host.IsSpecialPage) + { + return new MvcWebPageRazorHost(virtualPath, physicalPath); + } + + return host; + } + } +} diff --git a/src/System.Web.Mvc/NameValueCollectionExtensions.cs b/src/System.Web.Mvc/NameValueCollectionExtensions.cs new file mode 100644 index 00000000..31bdd4f3 --- /dev/null +++ b/src/System.Web.Mvc/NameValueCollectionExtensions.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Collections.Specialized; + +namespace System.Web.Mvc +{ + public static class NameValueCollectionExtensions + { + public static void CopyTo(this NameValueCollection collection, IDictionary<string, object> destination) + { + CopyTo(collection, destination, false /* replaceEntries */); + } + + public static void CopyTo(this NameValueCollection collection, IDictionary<string, object> destination, bool replaceEntries) + { + if (collection == null) + { + throw new ArgumentNullException("collection"); + } + if (destination == null) + { + throw new ArgumentNullException("destination"); + } + + foreach (string key in collection.Keys) + { + if (replaceEntries || !destination.ContainsKey(key)) + { + destination[key] = collection[key]; + } + } + } + } +} diff --git a/src/System.Web.Mvc/NameValueCollectionValueProvider.cs b/src/System.Web.Mvc/NameValueCollectionValueProvider.cs new file mode 100644 index 00000000..0db898e2 --- /dev/null +++ b/src/System.Web.Mvc/NameValueCollectionValueProvider.cs @@ -0,0 +1,121 @@ +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Globalization; +using System.Threading; + +namespace System.Web.Mvc +{ + public class NameValueCollectionValueProvider : IValueProvider, IUnvalidatedValueProvider, IEnumerableValueProvider + { + private readonly HashSet<string> _prefixes = new HashSet<string>(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary<string, ValueProviderResultPlaceholder> _values = new Dictionary<string, ValueProviderResultPlaceholder>(StringComparer.OrdinalIgnoreCase); + + public NameValueCollectionValueProvider(NameValueCollection collection, CultureInfo culture) + : this(collection, null /* unvalidatedCollection */, culture) + { + } + + public NameValueCollectionValueProvider(NameValueCollection collection, NameValueCollection unvalidatedCollection, CultureInfo culture) + { + if (collection == null) + { + throw new ArgumentNullException("collection"); + } + + AddValues(collection, unvalidatedCollection ?? collection, culture); + } + + private void AddValues(NameValueCollection validatedCollection, NameValueCollection unvalidatedCollection, CultureInfo culture) + { + // Need to read keys from the unvalidated collection, as M.W.I's granular request validation is a bit touchy + // and validated entries at the time the key or value is looked at. For example, GetKey() will throw if the + // value fails request validation, even though the value's not being looked at (M.W.I can't tell the difference). + + if (unvalidatedCollection.Count > 0) + { + _prefixes.Add(String.Empty); + } + + foreach (string key in unvalidatedCollection) + { + if (key != null) + { + _prefixes.UnionWith(ValueProviderUtil.GetPrefixes(key)); + + // need to look up values lazily, as eagerly looking at the collection might trigger validation + _values[key] = new ValueProviderResultPlaceholder(key, validatedCollection, unvalidatedCollection, culture); + } + } + } + + public virtual bool ContainsPrefix(string prefix) + { + if (prefix == null) + { + throw new ArgumentNullException("prefix"); + } + + return _prefixes.Contains(prefix); + } + + public virtual ValueProviderResult GetValue(string key) + { + return GetValue(key, skipValidation: false); + } + + public virtual ValueProviderResult GetValue(string key, bool skipValidation) + { + if (key == null) + { + throw new ArgumentNullException("key"); + } + + ValueProviderResultPlaceholder placeholder; + _values.TryGetValue(key, out placeholder); + if (placeholder == null) + { + return null; + } + else + { + return (skipValidation) ? placeholder.UnvalidatedResult : placeholder.ValidatedResult; + } + } + + public virtual IDictionary<string, string> GetKeysFromPrefix(string prefix) + { + return ValueProviderUtil.GetKeysFromPrefix(_prefixes, prefix); + } + + // Placeholder that can store a validated (in relation to request validation) or unvalidated + // ValueProviderResult for a given key. + private sealed class ValueProviderResultPlaceholder + { + private readonly Lazy<ValueProviderResult> _validatedResultPlaceholder; + private readonly Lazy<ValueProviderResult> _unvalidatedResultPlaceholder; + + public ValueProviderResultPlaceholder(string key, NameValueCollection validatedCollection, NameValueCollection unvalidatedCollection, CultureInfo culture) + { + _validatedResultPlaceholder = new Lazy<ValueProviderResult>(() => GetResultFromCollection(key, validatedCollection, culture), LazyThreadSafetyMode.None); + _unvalidatedResultPlaceholder = new Lazy<ValueProviderResult>(() => GetResultFromCollection(key, unvalidatedCollection, culture), LazyThreadSafetyMode.None); + } + + public ValueProviderResult ValidatedResult + { + get { return _validatedResultPlaceholder.Value; } + } + + public ValueProviderResult UnvalidatedResult + { + get { return _unvalidatedResultPlaceholder.Value; } + } + + private static ValueProviderResult GetResultFromCollection(string key, NameValueCollection collection, CultureInfo culture) + { + string[] rawValue = collection.GetValues(key); + string attemptedValue = collection[key]; + return new ValueProviderResult(rawValue, attemptedValue, culture); + } + } + } +} diff --git a/src/System.Web.Mvc/NoAsyncTimeoutAttribute.cs b/src/System.Web.Mvc/NoAsyncTimeoutAttribute.cs new file mode 100644 index 00000000..05da36c4 --- /dev/null +++ b/src/System.Web.Mvc/NoAsyncTimeoutAttribute.cs @@ -0,0 +1,13 @@ +using System.Threading; + +namespace System.Web.Mvc +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)] + public sealed class NoAsyncTimeoutAttribute : AsyncTimeoutAttribute + { + public NoAsyncTimeoutAttribute() + : base(Timeout.Infinite) + { + } + } +} diff --git a/src/System.Web.Mvc/NonActionAttribute.cs b/src/System.Web.Mvc/NonActionAttribute.cs new file mode 100644 index 00000000..be4e1d47 --- /dev/null +++ b/src/System.Web.Mvc/NonActionAttribute.cs @@ -0,0 +1,13 @@ +using System.Reflection; + +namespace System.Web.Mvc +{ + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public sealed class NonActionAttribute : ActionMethodSelectorAttribute + { + public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo) + { + return false; + } + } +} diff --git a/src/System.Web.Mvc/NullViewLocationCache.cs b/src/System.Web.Mvc/NullViewLocationCache.cs new file mode 100644 index 00000000..681e7e1e --- /dev/null +++ b/src/System.Web.Mvc/NullViewLocationCache.cs @@ -0,0 +1,18 @@ +namespace System.Web.Mvc +{ + internal sealed class NullViewLocationCache : IViewLocationCache + { + #region IViewLocationCache Members + + public string GetViewLocation(HttpContextBase httpContext, string key) + { + return null; + } + + public void InsertViewLocation(HttpContextBase httpContext, string key, string virtualPath) + { + } + + #endregion + } +} diff --git a/src/System.Web.Mvc/OutputCacheAttribute.cs b/src/System.Web.Mvc/OutputCacheAttribute.cs new file mode 100644 index 00000000..f205f49a --- /dev/null +++ b/src/System.Web.Mvc/OutputCacheAttribute.cs @@ -0,0 +1,355 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Runtime.Caching; +using System.Security.Cryptography; +using System.Text; +using System.Web.Mvc.Properties; +using System.Web.UI; + +namespace System.Web.Mvc +{ + [SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "Unsealed so that subclassed types can set properties in the default constructor.")] + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)] + public class OutputCacheAttribute : ActionFilterAttribute, IExceptionFilter + { + private const string CacheKeyPrefix = "_MvcChildActionCache_"; + private static ObjectCache _childActionCache; + private static object _childActionFilterFinishCallbackKey = new object(); + private OutputCacheParameters _cacheSettings = new OutputCacheParameters { VaryByParam = "*" }; + private Func<ObjectCache> _childActionCacheThunk = () => ChildActionCache; + private bool _locationWasSet; + private bool _noStoreWasSet; + + public OutputCacheAttribute() + { + } + + internal OutputCacheAttribute(ObjectCache childActionCache) + { + _childActionCacheThunk = () => childActionCache; + } + + public string CacheProfile + { + get { return _cacheSettings.CacheProfile ?? String.Empty; } + set { _cacheSettings.CacheProfile = value; } + } + + internal OutputCacheParameters CacheSettings + { + get { return _cacheSettings; } + } + + public static ObjectCache ChildActionCache + { + get { return _childActionCache ?? MemoryCache.Default; } + set { _childActionCache = value; } + } + + private ObjectCache ChildActionCacheInternal + { + get { return _childActionCacheThunk(); } + } + + public int Duration + { + get { return _cacheSettings.Duration; } + set { _cacheSettings.Duration = value; } + } + + public OutputCacheLocation Location + { + get { return _cacheSettings.Location; } + set + { + _cacheSettings.Location = value; + _locationWasSet = true; + } + } + + public bool NoStore + { + get { return _cacheSettings.NoStore; } + set + { + _cacheSettings.NoStore = value; + _noStoreWasSet = true; + } + } + + public string SqlDependency + { + get { return _cacheSettings.SqlDependency ?? String.Empty; } + set { _cacheSettings.SqlDependency = value; } + } + + public string VaryByContentEncoding + { + get { return _cacheSettings.VaryByContentEncoding ?? String.Empty; } + set { _cacheSettings.VaryByContentEncoding = value; } + } + + public string VaryByCustom + { + get { return _cacheSettings.VaryByCustom ?? String.Empty; } + set { _cacheSettings.VaryByCustom = value; } + } + + public string VaryByHeader + { + get { return _cacheSettings.VaryByHeader ?? String.Empty; } + set { _cacheSettings.VaryByHeader = value; } + } + + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Param", Justification = "Matches the @ OutputCache page directive. Suppressed in source because this is a special case suppression.")] + public string VaryByParam + { + get { return _cacheSettings.VaryByParam ?? String.Empty; } + set { _cacheSettings.VaryByParam = value; } + } + + private static void ClearChildActionFilterFinishCallback(ControllerContext controllerContext) + { + controllerContext.HttpContext.Items.Remove(_childActionFilterFinishCallbackKey); + } + + private static void CompleteChildAction(ControllerContext filterContext, bool wasException) + { + Action<bool> callback = GetChildActionFilterFinishCallback(filterContext); + + if (callback != null) + { + ClearChildActionFilterFinishCallback(filterContext); + callback(wasException); + } + } + + private static Action<bool> GetChildActionFilterFinishCallback(ControllerContext controllerContext) + { + return controllerContext.HttpContext.Items[_childActionFilterFinishCallbackKey] as Action<bool>; + } + + internal string GetChildActionUniqueId(ActionExecutingContext filterContext) + { + StringBuilder uniqueIdBuilder = new StringBuilder(); + + // Start with a prefix, presuming that we share the cache with other users + uniqueIdBuilder.Append(CacheKeyPrefix); + + // Unique ID of the action description + uniqueIdBuilder.Append(filterContext.ActionDescriptor.UniqueId); + + // Unique ID from the VaryByCustom settings, if any + uniqueIdBuilder.Append(DescriptorUtil.CreateUniqueId(VaryByCustom)); + if (!String.IsNullOrEmpty(VaryByCustom)) + { + string varyByCustomResult = filterContext.HttpContext.ApplicationInstance.GetVaryByCustomString(HttpContext.Current, VaryByCustom); + uniqueIdBuilder.Append(varyByCustomResult); + } + + // Unique ID from the VaryByParam settings, if any + uniqueIdBuilder.Append(GetUniqueIdFromActionParameters(filterContext, SplitVaryByParam(VaryByParam))); + + // The key is typically too long to be useful, so we use a cryptographic hash + // as the actual key (better randomization and key distribution, so small vary + // values will generate dramtically different keys). + using (SHA256 sha = SHA256.Create()) + { + return Convert.ToBase64String(sha.ComputeHash(Encoding.UTF8.GetBytes(uniqueIdBuilder.ToString()))); + } + } + + private static string GetUniqueIdFromActionParameters(ActionExecutingContext filterContext, IEnumerable<string> keys) + { + // Generate a unique ID of normalized key names + key values + var keyValues = new Dictionary<string, object>(filterContext.ActionParameters, StringComparer.OrdinalIgnoreCase); + keys = (keys ?? keyValues.Keys).Select(key => key.ToUpperInvariant()) + .OrderBy(key => key, StringComparer.Ordinal); + + return DescriptorUtil.CreateUniqueId(keys.Concat(keys.Select(key => keyValues.ContainsKey(key) ? keyValues[key] : null))); + } + + public static bool IsChildActionCacheActive(ControllerContext controllerContext) + { + return GetChildActionFilterFinishCallback(controllerContext) != null; + } + + public override void OnActionExecuted(ActionExecutedContext filterContext) + { + if (filterContext == null) + { + throw new ArgumentNullException("filterContext"); + } + + // Complete the request if the child action threw an exception + if (filterContext.IsChildAction && filterContext.Exception != null) + { + CompleteChildAction(filterContext, wasException: true); + } + } + + public override void OnActionExecuting(ActionExecutingContext filterContext) + { + if (filterContext == null) + { + throw new ArgumentNullException("filterContext"); + } + + if (filterContext.IsChildAction) + { + ValidateChildActionConfiguration(); + + // Already actively being captured? (i.e., cached child action inside of cached child action) + // Realistically, this needs write substitution to do properly (including things like authentication) + if (GetChildActionFilterFinishCallback(filterContext) != null) + { + throw new InvalidOperationException(MvcResources.OutputCacheAttribute_CannotNestChildCache); + } + + // Already cached? + string uniqueId = GetChildActionUniqueId(filterContext); + string cachedValue = ChildActionCacheInternal.Get(uniqueId) as string; + if (cachedValue != null) + { + filterContext.Result = new ContentResult() { Content = cachedValue }; + return; + } + + // Swap in a new TextWriter so we can capture the output + StringWriter cachingWriter = new StringWriter(CultureInfo.InvariantCulture); + TextWriter originalWriter = filterContext.HttpContext.Response.Output; + filterContext.HttpContext.Response.Output = cachingWriter; + + // Set a finish callback to clean up + SetChildActionFilterFinishCallback(filterContext, wasException => + { + // Restore original writer + filterContext.HttpContext.Response.Output = originalWriter; + + // Grab output and write it + string capturedText = cachingWriter.ToString(); + filterContext.HttpContext.Response.Write(capturedText); + + // Only cache output if this wasn't an error + if (!wasException) + { + ChildActionCacheInternal.Add(uniqueId, capturedText, DateTimeOffset.UtcNow.AddSeconds(Duration)); + } + }); + } + } + + public void OnException(ExceptionContext filterContext) + { + if (filterContext == null) + { + throw new ArgumentNullException("filterContext"); + } + + if (filterContext.IsChildAction) + { + CompleteChildAction(filterContext, wasException: true); + } + } + + public override void OnResultExecuting(ResultExecutingContext filterContext) + { + if (filterContext == null) + { + throw new ArgumentNullException("filterContext"); + } + + if (!filterContext.IsChildAction) + { + // we need to call ProcessRequest() since there's no other way to set the Page.Response intrinsic + using (OutputCachedPage page = new OutputCachedPage(_cacheSettings)) + { + page.ProcessRequest(HttpContext.Current); + } + } + } + + public override void OnResultExecuted(ResultExecutedContext filterContext) + { + if (filterContext == null) + { + throw new ArgumentNullException("filterContext"); + } + + if (filterContext.IsChildAction) + { + CompleteChildAction(filterContext, wasException: filterContext.Exception != null); + } + } + + private static void SetChildActionFilterFinishCallback(ControllerContext controllerContext, Action<bool> callback) + { + controllerContext.HttpContext.Items[_childActionFilterFinishCallbackKey] = callback; + } + + private static IEnumerable<string> SplitVaryByParam(string varyByParam) + { + if (String.Equals(varyByParam, "none", StringComparison.OrdinalIgnoreCase)) + { + // Vary by nothing + return Enumerable.Empty<string>(); + } + + if (String.Equals(varyByParam, "*", StringComparison.OrdinalIgnoreCase)) + { + // Vary by everything + return null; + } + + return from part in varyByParam.Split(';') // Vary by specific parameters + let trimmed = part.Trim() + where !String.IsNullOrEmpty(trimmed) + select trimmed; + } + + private void ValidateChildActionConfiguration() + { + if (Duration <= 0) + { + throw new InvalidOperationException(MvcResources.OutputCacheAttribute_InvalidDuration); + } + + if (String.IsNullOrWhiteSpace(VaryByParam)) + { + throw new InvalidOperationException(MvcResources.OutputCacheAttribute_InvalidVaryByParam); + } + + if (!String.IsNullOrWhiteSpace(CacheProfile) || + !String.IsNullOrWhiteSpace(SqlDependency) || + !String.IsNullOrWhiteSpace(VaryByContentEncoding) || + !String.IsNullOrWhiteSpace(VaryByHeader) || + _locationWasSet || _noStoreWasSet) + { + throw new InvalidOperationException(MvcResources.OutputCacheAttribute_ChildAction_UnsupportedSetting); + } + } + + [SuppressMessage("ASP.NET.Security", "CA5328:ValidateRequestShouldBeEnabled", Justification = "Instances of this type are not created in response to direct user input.")] + private sealed class OutputCachedPage : Page + { + private OutputCacheParameters _cacheSettings; + + public OutputCachedPage(OutputCacheParameters cacheSettings) + { + // Tracing requires Page IDs to be unique. + ID = Guid.NewGuid().ToString(); + _cacheSettings = cacheSettings; + } + + protected override void FrameworkInitialize() + { + // when you put the <%@ OutputCache %> directive on a page, the generated code calls InitOutputCache() from here + base.FrameworkInitialize(); + InitOutputCache(_cacheSettings); + } + } + } +} diff --git a/src/System.Web.Mvc/ParameterBindingInfo.cs b/src/System.Web.Mvc/ParameterBindingInfo.cs new file mode 100644 index 00000000..a005900c --- /dev/null +++ b/src/System.Web.Mvc/ParameterBindingInfo.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +namespace System.Web.Mvc +{ + public abstract class ParameterBindingInfo + { + public virtual IModelBinder Binder + { + get { return null; } + } + + public virtual ICollection<string> Exclude + { + get { return new string[0]; } + } + + public virtual ICollection<string> Include + { + get { return new string[0]; } + } + + public virtual string Prefix + { + get { return null; } + } + } +} diff --git a/src/System.Web.Mvc/ParameterDescriptor.cs b/src/System.Web.Mvc/ParameterDescriptor.cs new file mode 100644 index 00000000..fbed437f --- /dev/null +++ b/src/System.Web.Mvc/ParameterDescriptor.cs @@ -0,0 +1,54 @@ +using System.Reflection; + +namespace System.Web.Mvc +{ + public abstract class ParameterDescriptor : ICustomAttributeProvider + { + private static readonly EmptyParameterBindingInfo _emptyBindingInfo = new EmptyParameterBindingInfo(); + + public abstract ActionDescriptor ActionDescriptor { get; } + + public virtual ParameterBindingInfo BindingInfo + { + get { return _emptyBindingInfo; } + } + + public virtual object DefaultValue + { + get { return null; } + } + + public abstract string ParameterName { get; } + + public abstract Type ParameterType { get; } + + public virtual object[] GetCustomAttributes(bool inherit) + { + return GetCustomAttributes(typeof(object), inherit); + } + + public virtual object[] GetCustomAttributes(Type attributeType, bool inherit) + { + if (attributeType == null) + { + throw new ArgumentNullException("attributeType"); + } + + return (object[])Array.CreateInstance(attributeType, 0); + } + + public virtual bool IsDefined(Type attributeType, bool inherit) + { + if (attributeType == null) + { + throw new ArgumentNullException("attributeType"); + } + + return false; + } + + private sealed class EmptyParameterBindingInfo : ParameterBindingInfo + { + } + } +} diff --git a/src/System.Web.Mvc/ParameterInfoUtil.cs b/src/System.Web.Mvc/ParameterInfoUtil.cs new file mode 100644 index 00000000..e7343cdf --- /dev/null +++ b/src/System.Web.Mvc/ParameterInfoUtil.cs @@ -0,0 +1,33 @@ +using System.ComponentModel; +using System.Reflection; + +namespace System.Web.Mvc +{ + internal static class ParameterInfoUtil + { + public static bool TryGetDefaultValue(ParameterInfo parameterInfo, out object value) + { + // this will get the default value as seen by the VB / C# compilers + // if no value was baked in, RawDefaultValue returns DBNull.Value + object defaultValue = parameterInfo.DefaultValue; + if (defaultValue != DBNull.Value) + { + value = defaultValue; + return true; + } + + // if the compiler did not bake in a default value, check the [DefaultValue] attribute + DefaultValueAttribute[] attrs = (DefaultValueAttribute[])parameterInfo.GetCustomAttributes(typeof(DefaultValueAttribute), false); + if (attrs == null || attrs.Length == 0) + { + value = default(object); + return false; + } + else + { + value = attrs[0].Value; + return true; + } + } + } +} diff --git a/src/System.Web.Mvc/PartialViewResult.cs b/src/System.Web.Mvc/PartialViewResult.cs new file mode 100644 index 00000000..19e08795 --- /dev/null +++ b/src/System.Web.Mvc/PartialViewResult.cs @@ -0,0 +1,28 @@ +using System.Globalization; +using System.Text; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + public class PartialViewResult : ViewResultBase + { + protected override ViewEngineResult FindView(ControllerContext context) + { + ViewEngineResult result = ViewEngineCollection.FindPartialView(context, ViewName); + if (result.View != null) + { + return result; + } + + // we need to generate an exception containing all the locations we searched + StringBuilder locationsText = new StringBuilder(); + foreach (string location in result.SearchedLocations) + { + locationsText.AppendLine(); + locationsText.Append(location); + } + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, + MvcResources.Common_PartialViewNotFound, ViewName, locationsText)); + } + } +} diff --git a/src/System.Web.Mvc/PathHelpers.cs b/src/System.Web.Mvc/PathHelpers.cs new file mode 100644 index 00000000..b6d25b09 --- /dev/null +++ b/src/System.Web.Mvc/PathHelpers.cs @@ -0,0 +1,97 @@ +namespace System.Web.Mvc +{ + internal static class PathHelpers + { + private static UrlRewriterHelper _urlRewriterHelper = new UrlRewriterHelper(); + + // this method can accept an app-relative path or an absolute path for contentPath + public static string GenerateClientUrl(HttpContextBase httpContext, string contentPath) + { + if (String.IsNullOrEmpty(contentPath)) + { + return contentPath; + } + + // many of the methods we call internally can't handle query strings properly, so just strip it out for + // the time being + string query; + contentPath = StripQuery(contentPath, out query); + + return GenerateClientUrlInternal(httpContext, contentPath) + query; + } + + private static string GenerateClientUrlInternal(HttpContextBase httpContext, string contentPath) + { + if (String.IsNullOrEmpty(contentPath)) + { + return contentPath; + } + + // can't call VirtualPathUtility.IsAppRelative since it throws on some inputs + bool isAppRelative = contentPath[0] == '~'; + if (isAppRelative) + { + string absoluteContentPath = VirtualPathUtility.ToAbsolute(contentPath, httpContext.Request.ApplicationPath); + string modifiedAbsoluteContentPath = httpContext.Response.ApplyAppPathModifier(absoluteContentPath); + return GenerateClientUrlInternal(httpContext, modifiedAbsoluteContentPath); + } + + // we only want to manipulate the path if URL rewriting is active for this request, else we risk breaking the generated URL + bool wasRequestRewritten = _urlRewriterHelper.WasRequestRewritten(httpContext); + if (!wasRequestRewritten) + { + return contentPath; + } + + // Since the rawUrl represents what the user sees in his browser, it is what we want to use as the base + // of our absolute paths. For example, consider mysite.example.com/foo, which is internally + // rewritten to content.example.com/mysite/foo. When we want to generate a link to ~/bar, we want to + // base it from / instead of /foo, otherwise the user ends up seeing mysite.example.com/foo/bar, + // which is incorrect. + string relativeUrlToDestination = MakeRelative(httpContext.Request.Path, contentPath); + string absoluteUrlToDestination = MakeAbsolute(httpContext.Request.RawUrl, relativeUrlToDestination); + return absoluteUrlToDestination; + } + + public static string MakeAbsolute(string basePath, string relativePath) + { + // The Combine() method can't handle query strings on the base path, so we trim it off. + string query; + basePath = StripQuery(basePath, out query); + return VirtualPathUtility.Combine(basePath, relativePath); + } + + public static string MakeRelative(string fromPath, string toPath) + { + string relativeUrl = VirtualPathUtility.MakeRelative(fromPath, toPath); + if (String.IsNullOrEmpty(relativeUrl) || relativeUrl[0] == '?') + { + // Sometimes VirtualPathUtility.MakeRelative() will return an empty string when it meant to return '.', + // but links to {empty string} are browser dependent. We replace it with an explicit path to force + // consistency across browsers. + relativeUrl = "./" + relativeUrl; + } + return relativeUrl; + } + + private static string StripQuery(string path, out string query) + { + int queryIndex = path.IndexOf('?'); + if (queryIndex >= 0) + { + query = path.Substring(queryIndex); + return path.Substring(0, queryIndex); + } + else + { + query = null; + return path; + } + } + + internal static void ResetUrlRewriterHelper() + { + _urlRewriterHelper = new UrlRewriterHelper(); + } + } +} diff --git a/src/System.Web.Mvc/PreApplicationStartCode.cs b/src/System.Web.Mvc/PreApplicationStartCode.cs new file mode 100644 index 00000000..05da00ac --- /dev/null +++ b/src/System.Web.Mvc/PreApplicationStartCode.cs @@ -0,0 +1,26 @@ +using System.ComponentModel; +using System.Web.WebPages.Scope; + +namespace System.Web.Mvc +{ + [EditorBrowsable(EditorBrowsableState.Never)] + public static class PreApplicationStartCode + { + private static bool _startWasCalled; + + public static void Start() + { + // Guard against multiple calls. All Start calls are made on same thread, so no lock needed here + if (_startWasCalled) + { + return; + } + _startWasCalled = true; + + WebPages.Razor.PreApplicationStartCode.Start(); + WebPages.PreApplicationStartCode.Start(); + + ViewContext.GlobalScopeThunk = () => ScopeStorage.CurrentScope; + } + } +} diff --git a/src/System.Web.Mvc/Properties/AssemblyInfo.cs b/src/System.Web.Mvc/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..939b03c0 --- /dev/null +++ b/src/System.Web.Mvc/Properties/AssemblyInfo.cs @@ -0,0 +1,27 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Security; +using System.Web; +using System.Web.Mvc; + +[assembly: AssemblyTitle("System.Web.Mvc.dll")] +[assembly: AssemblyDescription("System.Web.Mvc.dll")] +[assembly: Guid("4b5f4208-c6b0-4c37-9a41-63325ffa52ad")] + +#if !CODE_COVERAGE +[assembly: AllowPartiallyTrustedCallers] +#endif + +[assembly: InternalsVisibleTo("System.Web.Mvc.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] +[assembly: PreApplicationStartMethod(typeof(PreApplicationStartCode), "Start")] +[assembly: TypeForwardedTo(typeof(TagBuilder))] +[assembly: TypeForwardedTo(typeof(TagRenderMode))] +[assembly: TypeForwardedTo(typeof(HttpAntiForgeryException))] +[assembly: TypeForwardedTo(typeof(ModelClientValidationEqualToRule))] +[assembly: TypeForwardedTo(typeof(ModelClientValidationRangeRule))] +[assembly: TypeForwardedTo(typeof(ModelClientValidationRegexRule))] +[assembly: TypeForwardedTo(typeof(ModelClientValidationRemoteRule))] +[assembly: TypeForwardedTo(typeof(ModelClientValidationRequiredRule))] +[assembly: TypeForwardedTo(typeof(ModelClientValidationRule))] +[assembly: TypeForwardedTo(typeof(ModelClientValidationStringLengthRule))] diff --git a/src/System.Web.Mvc/Properties/MvcResources.Designer.cs b/src/System.Web.Mvc/Properties/MvcResources.Designer.cs new file mode 100644 index 00000000..0618068e --- /dev/null +++ b/src/System.Web.Mvc/Properties/MvcResources.Designer.cs @@ -0,0 +1,1012 @@ +//------------------------------------------------------------------------------ +// <auto-generated> +// This code was generated by a tool. +// Runtime Version:4.0.30319.17369 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// </auto-generated> +//------------------------------------------------------------------------------ + +namespace System.Web.Mvc.Properties { + using System; + + + /// <summary> + /// A strongly-typed resource class, for looking up localized strings, etc. + /// </summary> + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class MvcResources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal MvcResources() { + } + + /// <summary> + /// Returns the cached ResourceManager instance used by this class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("System.Web.Mvc.Properties.MvcResources", typeof(MvcResources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// <summary> + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// <summary> + /// Looks up a localized string similar to The current request for action '{0}' on controller type '{1}' is ambiguous between the following action methods:{2}. + /// </summary> + internal static string ActionMethodSelector_AmbiguousMatch { + get { + return ResourceManager.GetString("ActionMethodSelector_AmbiguousMatch", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to {0} on type {1}. + /// </summary> + internal static string ActionMethodSelector_AmbiguousMatchType { + get { + return ResourceManager.GetString("ActionMethodSelector_AmbiguousMatchType", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The asynchronous action method '{0}' cannot be executed synchronously.. + /// </summary> + internal static string AsyncActionDescriptor_CannotExecuteSynchronously { + get { + return ResourceManager.GetString("AsyncActionDescriptor_CannotExecuteSynchronously", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Lookup for method '{0}' on controller type '{1}' failed because of an ambiguity between the following methods:{2}. + /// </summary> + internal static string AsyncActionMethodSelector_AmbiguousMethodMatch { + get { + return ResourceManager.GetString("AsyncActionMethodSelector_AmbiguousMethodMatch", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Could not locate a method named '{0}' on controller type {1}.. + /// </summary> + internal static string AsyncActionMethodSelector_CouldNotFindMethod { + get { + return ResourceManager.GetString("AsyncActionMethodSelector_CouldNotFindMethod", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The provided IAsyncResult has already been consumed.. + /// </summary> + internal static string AsyncCommon_AsyncResultAlreadyConsumed { + get { + return ResourceManager.GetString("AsyncCommon_AsyncResultAlreadyConsumed", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The controller of type '{0}' must subclass AsyncController or implement the IAsyncManagerContainer interface.. + /// </summary> + internal static string AsyncCommon_ControllerMustImplementIAsyncManagerContainer { + get { + return ResourceManager.GetString("AsyncCommon_ControllerMustImplementIAsyncManagerContainer", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The provided IAsyncResult is not valid for this method.. + /// </summary> + internal static string AsyncCommon_InvalidAsyncResult { + get { + return ResourceManager.GetString("AsyncCommon_InvalidAsyncResult", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The timeout value must be non-negative or Timeout.Infinite.. + /// </summary> + internal static string AsyncCommon_InvalidTimeout { + get { + return ResourceManager.GetString("AsyncCommon_InvalidTimeout", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to AuthorizeAttribute cannot be used within a child action caching block.. + /// </summary> + internal static string AuthorizeAttribute_CannotUseWithinChildActionCache { + get { + return ResourceManager.GetString("AuthorizeAttribute_CannotUseWithinChildActionCache", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The action '{0}' is accessible only by a child request.. + /// </summary> + internal static string ChildActionOnlyAttribute_MustBeInChildRequest { + get { + return ResourceManager.GetString("ChildActionOnlyAttribute_MustBeInChildRequest", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The field {0} must be a date.. + /// </summary> + internal static string ClientDataTypeModelValidatorProvider_FieldMustBeDate { + get { + return ResourceManager.GetString("ClientDataTypeModelValidatorProvider_FieldMustBeDate", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The field {0} must be a number.. + /// </summary> + internal static string ClientDataTypeModelValidatorProvider_FieldMustBeNumeric { + get { + return ResourceManager.GetString("ClientDataTypeModelValidatorProvider_FieldMustBeNumeric", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to No route in the route table matches the supplied values.. + /// </summary> + internal static string Common_NoRouteMatched { + get { + return ResourceManager.GetString("Common_NoRouteMatched", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Value cannot be null or empty.. + /// </summary> + internal static string Common_NullOrEmpty { + get { + return ResourceManager.GetString("Common_NullOrEmpty", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The partial view '{0}' was not found or no view engine supports the searched locations. The following locations were searched:{1}. + /// </summary> + internal static string Common_PartialViewNotFound { + get { + return ResourceManager.GetString("Common_PartialViewNotFound", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The property '{0}' cannot be null or empty.. + /// </summary> + internal static string Common_PropertyCannotBeNullOrEmpty { + get { + return ResourceManager.GetString("Common_PropertyCannotBeNullOrEmpty", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The property {0}.{1} could not be found.. + /// </summary> + internal static string Common_PropertyNotFound { + get { + return ResourceManager.GetString("Common_PropertyNotFound", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to False. + /// </summary> + internal static string Common_TriState_False { + get { + return ResourceManager.GetString("Common_TriState_False", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Not Set. + /// </summary> + internal static string Common_TriState_NotSet { + get { + return ResourceManager.GetString("Common_TriState_NotSet", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to True. + /// </summary> + internal static string Common_TriState_True { + get { + return ResourceManager.GetString("Common_TriState_True", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The type {0} must derive from {1}. + /// </summary> + internal static string Common_TypeMustDriveFromType { + get { + return ResourceManager.GetString("Common_TypeMustDriveFromType", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The value '{0}' is invalid.. + /// </summary> + internal static string Common_ValueNotValidForProperty { + get { + return ResourceManager.GetString("Common_ValueNotValidForProperty", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The view '{0}' or its master was not found or no view engine supports the searched locations. The following locations were searched:{1}. + /// </summary> + internal static string Common_ViewNotFound { + get { + return ResourceManager.GetString("Common_ViewNotFound", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to '{0}' and '{1}' do not match.. + /// </summary> + internal static string CompareAttribute_MustMatch { + get { + return ResourceManager.GetString("CompareAttribute_MustMatch", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Could not find a property named {0}.. + /// </summary> + internal static string CompareAttribute_UnknownProperty { + get { + return ResourceManager.GetString("CompareAttribute_UnknownProperty", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to A public action method '{0}' was not found on controller '{1}'.. + /// </summary> + internal static string Controller_UnknownAction { + get { + return ResourceManager.GetString("Controller_UnknownAction", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The model of type '{0}' could not be updated.. + /// </summary> + internal static string Controller_UpdateModel_UpdateUnsuccessful { + get { + return ResourceManager.GetString("Controller_UpdateModel_UpdateUnsuccessful", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The model of type '{0}' is not valid.. + /// </summary> + internal static string Controller_Validate_ValidationFailed { + get { + return ResourceManager.GetString("Controller_Validate_ValidationFailed", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Cannot execute Controller with a null HttpContext.. + /// </summary> + internal static string ControllerBase_CannotExecuteWithNullHttpContext { + get { + return ResourceManager.GetString("ControllerBase_CannotExecuteWithNullHttpContext", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to A single instance of controller '{0}' cannot be used to handle multiple requests. If a custom controller factory is in use, make sure that it creates a new instance of the controller for each request.. + /// </summary> + internal static string ControllerBase_CannotHandleMultipleRequests { + get { + return ResourceManager.GetString("ControllerBase_CannotHandleMultipleRequests", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to An error occurred when trying to create the IControllerFactory '{0}'. Make sure that the controller factory has a public parameterless constructor.. + /// </summary> + internal static string ControllerBuilder_ErrorCreatingControllerFactory { + get { + return ResourceManager.GetString("ControllerBuilder_ErrorCreatingControllerFactory", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The IControllerFactory '{0}' did not return a controller for the name '{1}'.. + /// </summary> + internal static string ControllerBuilder_FactoryReturnedNull { + get { + return ResourceManager.GetString("ControllerBuilder_FactoryReturnedNull", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The controller factory type '{0}' must implement the IControllerFactory interface.. + /// </summary> + internal static string ControllerBuilder_MissingIControllerFactory { + get { + return ResourceManager.GetString("ControllerBuilder_MissingIControllerFactory", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The view found at '{0}' was not created.. + /// </summary> + internal static string CshtmlView_ViewCouldNotBeCreated { + get { + return ResourceManager.GetString("CshtmlView_ViewCouldNotBeCreated", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The view at '{0}' must derive from WebViewPage, or WebViewPage<TModel>.. + /// </summary> + internal static string CshtmlView_WrongViewBase { + get { + return ResourceManager.GetString("CshtmlView_WrongViewBase", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to {0} has a DisplayColumn attribute for {1}, but property {1} does not exist.. + /// </summary> + internal static string DataAnnotationsModelMetadataProvider_UnknownProperty { + get { + return ResourceManager.GetString("DataAnnotationsModelMetadataProvider_UnknownProperty", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to {0} has a DisplayColumn attribute for {1}, but property {1} does not have a public getter.. + /// </summary> + internal static string DataAnnotationsModelMetadataProvider_UnreadableProperty { + get { + return ResourceManager.GetString("DataAnnotationsModelMetadataProvider_UnreadableProperty", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The type {0} must have a public constructor which accepts three parameters of types {1}, {2}, and {3}. + /// </summary> + internal static string DataAnnotationsModelValidatorProvider_ConstructorRequirements { + get { + return ResourceManager.GetString("DataAnnotationsModelValidatorProvider_ConstructorRequirements", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The type {0} must have a public constructor which accepts two parameters of types {1} and {2}.. + /// </summary> + internal static string DataAnnotationsModelValidatorProvider_ValidatableConstructorRequirements { + get { + return ResourceManager.GetString("DataAnnotationsModelValidatorProvider_ValidatableConstructorRequirements", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Multiple types were found that match the controller named '{0}'. This can happen if the route that services this request does not specify namespaces to search for a controller that matches the request. If this is the case, register this route by calling an overload of the 'MapRoute' method that takes a 'namespaces' parameter. + /// + ///The request for '{0}' has found the following matching controllers:{1}. + /// </summary> + internal static string DefaultControllerFactory_ControllerNameAmbiguous_WithoutRouteUrl { + get { + return ResourceManager.GetString("DefaultControllerFactory_ControllerNameAmbiguous_WithoutRouteUrl", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Multiple types were found that match the controller named '{0}'. This can happen if the route that services this request ('{1}') does not specify namespaces to search for a controller that matches the request. If this is the case, register this route by calling an overload of the 'MapRoute' method that takes a 'namespaces' parameter. + /// + ///The request for '{0}' has found the following matching controllers:{2}. + /// </summary> + internal static string DefaultControllerFactory_ControllerNameAmbiguous_WithRouteUrl { + get { + return ResourceManager.GetString("DefaultControllerFactory_ControllerNameAmbiguous_WithRouteUrl", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to An error occurred when trying to create a controller of type '{0}'. Make sure that the controller has a parameterless public constructor.. + /// </summary> + internal static string DefaultControllerFactory_ErrorCreatingController { + get { + return ResourceManager.GetString("DefaultControllerFactory_ErrorCreatingController", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The controller for path '{0}' was not found or does not implement IController.. + /// </summary> + internal static string DefaultControllerFactory_NoControllerFound { + get { + return ResourceManager.GetString("DefaultControllerFactory_NoControllerFound", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The controller type '{0}' must implement IController.. + /// </summary> + internal static string DefaultControllerFactory_TypeDoesNotSubclassControllerBase { + get { + return ResourceManager.GetString("DefaultControllerFactory_TypeDoesNotSubclassControllerBase", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The value '{0}' is not valid for {1}.. + /// </summary> + internal static string DefaultModelBinder_ValueInvalid { + get { + return ResourceManager.GetString("DefaultModelBinder_ValueInvalid", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to A value is required.. + /// </summary> + internal static string DefaultModelBinder_ValueRequired { + get { + return ResourceManager.GetString("DefaultModelBinder_ValueRequired", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The number of ticks for the TimeSpan value must be greater than or equal to 0.. + /// </summary> + internal static string DefaultViewLocationCache_NegativeTimeSpan { + get { + return ResourceManager.GetString("DefaultViewLocationCache_NegativeTimeSpan", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The type {0} does not appear to implement Microsoft.Practices.ServiceLocation.IServiceLocator.. + /// </summary> + internal static string DependencyResolver_DoesNotImplementICommonServiceLocator { + get { + return ResourceManager.GetString("DependencyResolver_DoesNotImplementICommonServiceLocator", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The type '{0}' does not inherit from Exception.. + /// </summary> + internal static string ExceptionViewAttribute_NonExceptionType { + get { + return ResourceManager.GetString("ExceptionViewAttribute_NonExceptionType", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The expression compiler was unable to evaluate the indexer expression '{0}' because it references the model parameter '{1}' which is unavailable.. + /// </summary> + internal static string ExpressionHelper_InvalidIndexerExpression { + get { + return ResourceManager.GetString("ExpressionHelper_InvalidIndexerExpression", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Order must be greater than or equal to -1.. + /// </summary> + internal static string FilterAttribute_OrderOutOfRange { + get { + return ResourceManager.GetString("FilterAttribute_OrderOutOfRange", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The GET and POST HTTP methods are not supported.. + /// </summary> + internal static string HtmlHelper_InvalidHttpMethod { + get { + return ResourceManager.GetString("HtmlHelper_InvalidHttpMethod", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The specified HttpVerbs value is not supported. The supported values are Delete, Head, and Put.. + /// </summary> + internal static string HtmlHelper_InvalidHttpVerb { + get { + return ResourceManager.GetString("HtmlHelper_InvalidHttpVerb", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to There is no ViewData item of type '{1}' that has the key '{0}'.. + /// </summary> + internal static string HtmlHelper_MissingSelectData { + get { + return ResourceManager.GetString("HtmlHelper_MissingSelectData", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The parameter '{0}' must evaluate to an IEnumerable when multiple selection is allowed.. + /// </summary> + internal static string HtmlHelper_SelectExpressionNotEnumerable { + get { + return ResourceManager.GetString("HtmlHelper_SelectExpressionNotEnumerable", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The value must be greater than or equal to zero.. + /// </summary> + internal static string HtmlHelper_TextAreaParameterOutOfRange { + get { + return ResourceManager.GetString("HtmlHelper_TextAreaParameterOutOfRange", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The ViewData item that has the key '{0}' is of type '{1}' but must be of type '{2}'.. + /// </summary> + internal static string HtmlHelper_WrongSelectDataType { + get { + return ResourceManager.GetString("HtmlHelper_WrongSelectDataType", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to This request has been blocked because sensitive information could be disclosed to third party web sites when this is used in a GET request. To allow GET requests, set JsonRequestBehavior to AllowGet.. + /// </summary> + internal static string JsonRequest_GetNotAllowed { + get { + return ResourceManager.GetString("JsonRequest_GetNotAllowed", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The JSON request was too large to be deserialized.. + /// </summary> + internal static string JsonValueProviderFactory_RequestTooLarge { + get { + return ResourceManager.GetString("JsonValueProviderFactory_RequestTooLarge", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to An error occurred when trying to create the IModelBinder '{0}'. Make sure that the binder has a public parameterless constructor.. + /// </summary> + internal static string ModelBinderAttribute_ErrorCreatingModelBinder { + get { + return ResourceManager.GetString("ModelBinderAttribute_ErrorCreatingModelBinder", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The type '{0}' does not implement the IModelBinder interface.. + /// </summary> + internal static string ModelBinderAttribute_TypeNotIModelBinder { + get { + return ResourceManager.GetString("ModelBinderAttribute_TypeNotIModelBinder", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The type '{0}' contains multiple attributes that inherit from CustomModelBinderAttribute.. + /// </summary> + internal static string ModelBinderDictionary_MultipleAttributes { + get { + return ResourceManager.GetString("ModelBinderDictionary_MultipleAttributes", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to This property setter is obsolete, because its value is derived from ModelMetadata.Model now.. + /// </summary> + internal static string ModelMetadata_PropertyNotSettable { + get { + return ResourceManager.GetString("ModelMetadata_PropertyNotSettable", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to This constructor is obsolete, because its functionality has been moved to MvcForm(ViewContext) now.. + /// </summary> + internal static string MvcForm_ConstructorObsolete { + get { + return ResourceManager.GetString("MvcForm_ConstructorObsolete", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The 'inherits' keyword is not allowed when a '{0}' keyword is used.. + /// </summary> + internal static string MvcRazorCodeParser_CannotHaveModelAndInheritsKeyword { + get { + return ResourceManager.GetString("MvcRazorCodeParser_CannotHaveModelAndInheritsKeyword", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The '{0}' keyword must be followed by a type name on the same line.. + /// </summary> + internal static string MvcRazorCodeParser_ModelKeywordMustBeFollowedByTypeName { + get { + return ResourceManager.GetString("MvcRazorCodeParser_ModelKeywordMustBeFollowedByTypeName", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Only one '{0}' statement is allowed in a file.. + /// </summary> + internal static string MvcRazorCodeParser_OnlyOneModelStatementIsAllowed { + get { + return ResourceManager.GetString("MvcRazorCodeParser_OnlyOneModelStatementIsAllowed", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The matched route does not include a 'controller' route value, which is required.. + /// </summary> + internal static string MvcRouteHandler_RouteValuesHasNoController { + get { + return ResourceManager.GetString("MvcRouteHandler_RouteValuesHasNoController", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to OutputCacheAttribute is not allowed on child actions which are children of an already cached child action.. + /// </summary> + internal static string OutputCacheAttribute_CannotNestChildCache { + get { + return ResourceManager.GetString("OutputCacheAttribute_CannotNestChildCache", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to OutputCacheAttribute for child actions only supports Duration, VaryByCustom, and VaryByParam values. Please do not set CacheProfile, Location, NoStore, SqlDependency, VaryByContentEncoding, or VaryByHeader values for child actions.. + /// </summary> + internal static string OutputCacheAttribute_ChildAction_UnsupportedSetting { + get { + return ResourceManager.GetString("OutputCacheAttribute_ChildAction_UnsupportedSetting", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Duration must be a positive number.. + /// </summary> + internal static string OutputCacheAttribute_InvalidDuration { + get { + return ResourceManager.GetString("OutputCacheAttribute_InvalidDuration", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to VaryByParam must be '*', 'none', or a semicolon-delimited list of keys.. + /// </summary> + internal static string OutputCacheAttribute_InvalidVaryByParam { + get { + return ResourceManager.GetString("OutputCacheAttribute_InvalidVaryByParam", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Child actions are not allowed to perform redirect actions.. + /// </summary> + internal static string RedirectAction_CannotRedirectInChildAction { + get { + return ResourceManager.GetString("RedirectAction_CannotRedirectInChildAction", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Cannot create a descriptor for instance method '{0}' on type '{1}' because the type does not derive from ControllerBase.. + /// </summary> + internal static string ReflectedActionDescriptor_CannotCallInstanceMethodOnNonControllerType { + get { + return ResourceManager.GetString("ReflectedActionDescriptor_CannotCallInstanceMethodOnNonControllerType", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Cannot call action method '{0}' on controller '{1}' because the parameter '{2}' is passed by reference.. + /// </summary> + internal static string ReflectedActionDescriptor_CannotCallMethodsWithOutOrRefParameters { + get { + return ResourceManager.GetString("ReflectedActionDescriptor_CannotCallMethodsWithOutOrRefParameters", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Cannot call action method '{0}' on controller '{1}' because the action method is a generic method.. + /// </summary> + internal static string ReflectedActionDescriptor_CannotCallOpenGenericMethods { + get { + return ResourceManager.GetString("ReflectedActionDescriptor_CannotCallOpenGenericMethods", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Cannot call action method '{0}' on controller '{1}' because the action method is a static method.. + /// </summary> + internal static string ReflectedActionDescriptor_CannotCallStaticMethod { + get { + return ResourceManager.GetString("ReflectedActionDescriptor_CannotCallStaticMethod", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The parameters dictionary contains a null entry for parameter '{0}' of non-nullable type '{1}' for method '{2}' in '{3}'. An optional parameter must be a reference type, a nullable type, or be declared as an optional parameter.. + /// </summary> + internal static string ReflectedActionDescriptor_ParameterCannotBeNull { + get { + return ResourceManager.GetString("ReflectedActionDescriptor_ParameterCannotBeNull", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The parameters dictionary does not contain an entry for parameter '{0}' of type '{1}' for method '{2}' in '{3}'. The dictionary must contain an entry for each parameter, including parameters that have null values.. + /// </summary> + internal static string ReflectedActionDescriptor_ParameterNotInDictionary { + get { + return ResourceManager.GetString("ReflectedActionDescriptor_ParameterNotInDictionary", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The parameters dictionary contains an invalid entry for parameter '{0}' for method '{1}' in '{2}'. The dictionary contains a value of type '{3}', but the parameter requires a value of type '{4}'.. + /// </summary> + internal static string ReflectedActionDescriptor_ParameterValueHasWrongType { + get { + return ResourceManager.GetString("ReflectedActionDescriptor_ParameterValueHasWrongType", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The parameter '{0}' on method '{1}' contains multiple attributes that inherit from CustomModelBinderAttribute.. + /// </summary> + internal static string ReflectedParameterBindingInfo_MultipleConverterAttributes { + get { + return ResourceManager.GetString("ReflectedParameterBindingInfo_MultipleConverterAttributes", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to No url for remote validation could be found.. + /// </summary> + internal static string RemoteAttribute_NoUrlFound { + get { + return ResourceManager.GetString("RemoteAttribute_NoUrlFound", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to '{0}' is invalid.. + /// </summary> + internal static string RemoteAttribute_RemoteValidationFailed { + get { + return ResourceManager.GetString("RemoteAttribute_RemoteValidationFailed", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The requested resource can only be accessed via SSL.. + /// </summary> + internal static string RequireHttpsAttribute_MustUseSsl { + get { + return ResourceManager.GetString("RequireHttpsAttribute_MustUseSsl", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The SessionStateTempDataProvider class requires session state to be enabled.. + /// </summary> + internal static string SessionStateTempDataProvider_SessionStateDisabled { + get { + return ResourceManager.GetString("SessionStateTempDataProvider_SessionStateDisabled", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to An instance of {0} was found in the resolver as well as a custom registered provider in {1}. Please set only one or the other.. + /// </summary> + internal static string SingleServiceResolver_CannotRegisterTwoInstances { + get { + return ResourceManager.GetString("SingleServiceResolver_CannotRegisterTwoInstances", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to An operation that crossed a synchronization context failed. See the inner exception for more information.. + /// </summary> + internal static string SynchronizationContextUtil_ExceptionThrown { + get { + return ResourceManager.GetString("SynchronizationContextUtil_ExceptionThrown", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The asynchronous action method '{0}' returns a Task, which cannot be executed synchronously.. + /// </summary> + internal static string TaskAsyncActionDescriptor_CannotExecuteSynchronously { + get { + return ResourceManager.GetString("TaskAsyncActionDescriptor_CannotExecuteSynchronously", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Unable to locate an appropriate template for type {0}.. + /// </summary> + internal static string TemplateHelpers_NoTemplate { + get { + return ResourceManager.GetString("TemplateHelpers_NoTemplate", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Templates can be used only with field access, property access, single-dimension array index, or single-parameter custom indexer expressions.. + /// </summary> + internal static string TemplateHelpers_TemplateLimitations { + get { + return ResourceManager.GetString("TemplateHelpers_TemplateLimitations", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The Collection template was used with an object of type '{0}', which does not implement System.IEnumerable.. + /// </summary> + internal static string Templates_TypeMustImplementIEnumerable { + get { + return ResourceManager.GetString("Templates_TypeMustImplementIEnumerable", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to This file is automatically generated. Please do not modify the contents of this file.. + /// </summary> + internal static string TypeCache_DoNotModify { + get { + return ResourceManager.GetString("TypeCache_DoNotModify", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The model object inside the metadata claimed to be compatible with {0}, but was actually {1}.. + /// </summary> + internal static string ValidatableObjectAdapter_IncompatibleType { + get { + return ResourceManager.GetString("ValidatableObjectAdapter_IncompatibleType", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The parameter conversion from type '{0}' to type '{1}' failed. See the inner exception for more information.. + /// </summary> + internal static string ValueProviderResult_ConversionThrew { + get { + return ResourceManager.GetString("ValueProviderResult_ConversionThrew", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The parameter conversion from type '{0}' to type '{1}' failed because no type converter can convert between these types.. + /// </summary> + internal static string ValueProviderResult_NoConverterExists { + get { + return ResourceManager.GetString("ValueProviderResult_NoConverterExists", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The model item passed into the dictionary is null, but this dictionary requires a non-null model item of type '{0}'.. + /// </summary> + internal static string ViewDataDictionary_ModelCannotBeNull { + get { + return ResourceManager.GetString("ViewDataDictionary_ModelCannotBeNull", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The model item passed into the dictionary is of type '{0}', but this dictionary requires a model item of type '{1}'.. + /// </summary> + internal static string ViewDataDictionary_WrongTModelType { + get { + return ResourceManager.GetString("ViewDataDictionary_WrongTModelType", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to A ViewMasterPage can be used only with content pages that derive from ViewPage or ViewPage<TModel>.. + /// </summary> + internal static string ViewMasterPage_RequiresViewPage { + get { + return ResourceManager.GetString("ViewMasterPage_RequiresViewPage", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Execution of the child request failed. Please examine the InnerException for more information.. + /// </summary> + internal static string ViewPageHttpHandlerWrapper_ExceptionOccurred { + get { + return ResourceManager.GetString("ViewPageHttpHandlerWrapper_ExceptionOccurred", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to A ViewStartPage can be used only with with a page that derives from WebViewPage or another ViewStartPage.. + /// </summary> + internal static string ViewStartPage_RequiresMvcRazorView { + get { + return ResourceManager.GetString("ViewStartPage_RequiresMvcRazorView", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The ViewUserControl '{0}' cannot find an IViewDataContainer object. The ViewUserControl must be inside a ViewPage, a ViewMasterPage, or another ViewUserControl.. + /// </summary> + internal static string ViewUserControl_RequiresViewDataProvider { + get { + return ResourceManager.GetString("ViewUserControl_RequiresViewDataProvider", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to A ViewUserControl can be used only in pages that derive from ViewPage or ViewPage<TModel>.. + /// </summary> + internal static string ViewUserControl_RequiresViewPage { + get { + return ResourceManager.GetString("ViewUserControl_RequiresViewPage", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to A master name cannot be specified when the view is a ViewUserControl.. + /// </summary> + internal static string WebFormViewEngine_UserControlCannotHaveMaster { + get { + return ResourceManager.GetString("WebFormViewEngine_UserControlCannotHaveMaster", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The view at '{0}' must derive from ViewPage, ViewPage<TModel>, ViewUserControl, or ViewUserControl<TModel>.. + /// </summary> + internal static string WebFormViewEngine_WrongViewBase { + get { + return ResourceManager.GetString("WebFormViewEngine_WrongViewBase", resourceCulture); + } + } + } +} diff --git a/src/System.Web.Mvc/Properties/MvcResources.resx b/src/System.Web.Mvc/Properties/MvcResources.resx new file mode 100644 index 00000000..5bcc68f5 --- /dev/null +++ b/src/System.Web.Mvc/Properties/MvcResources.resx @@ -0,0 +1,439 @@ +<?xml version="1.0" encoding="utf-8"?> +<root> + <!-- + Microsoft ResX Schema + + Version 2.0 + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes + associated with the data types. + + Example: + + ... ado.net/XML headers & schema ... + <resheader name="resmimetype">text/microsoft-resx</resheader> + <resheader name="version">2.0</resheader> + <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> + <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> + <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> + <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> + <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> + <value>[base64 mime encoded serialized .NET Framework object]</value> + </data> + <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> + <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> + <comment>This is a comment</comment> + </data> + + There are any number of "resheader" rows that contain simple + name/value pairs. + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the + mimetype set. + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not + extensible. For a given mimetype the value must be set accordingly: + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can + read any of the formats listed below. + + mimetype: application/x-microsoft.net.object.binary.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.soap.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Soap.SoapFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> + <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> + <xsd:element name="root" msdata:IsDataSet="true"> + <xsd:complexType> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="metadata"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" /> + </xsd:sequence> + <xsd:attribute name="name" use="required" type="xsd:string" /> + <xsd:attribute name="type" type="xsd:string" /> + <xsd:attribute name="mimetype" type="xsd:string" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="assembly"> + <xsd:complexType> + <xsd:attribute name="alias" type="xsd:string" /> + <xsd:attribute name="name" type="xsd:string" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="data"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> + <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> + <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="resheader"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" /> + </xsd:complexType> + </xsd:element> + </xsd:choice> + </xsd:complexType> + </xsd:element> + </xsd:schema> + <resheader name="resmimetype"> + <value>text/microsoft-resx</value> + </resheader> + <resheader name="version"> + <value>2.0</value> + </resheader> + <resheader name="reader"> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <resheader name="writer"> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <data name="ActionMethodSelector_AmbiguousMatch" xml:space="preserve"> + <value>The current request for action '{0}' on controller type '{1}' is ambiguous between the following action methods:{2}</value> + </data> + <data name="Common_NoRouteMatched" xml:space="preserve"> + <value>No route in the route table matches the supplied values.</value> + </data> + <data name="Common_NullOrEmpty" xml:space="preserve"> + <value>Value cannot be null or empty.</value> + </data> + <data name="Common_PartialViewNotFound" xml:space="preserve"> + <value>The partial view '{0}' was not found or no view engine supports the searched locations. The following locations were searched:{1}</value> + </data> + <data name="Common_PropertyCannotBeNullOrEmpty" xml:space="preserve"> + <value>The property '{0}' cannot be null or empty.</value> + </data> + <data name="Common_ViewNotFound" xml:space="preserve"> + <value>The view '{0}' or its master was not found or no view engine supports the searched locations. The following locations were searched:{1}</value> + </data> + <data name="ControllerBuilder_ErrorCreatingControllerFactory" xml:space="preserve"> + <value>An error occurred when trying to create the IControllerFactory '{0}'. Make sure that the controller factory has a public parameterless constructor.</value> + </data> + <data name="ControllerBuilder_FactoryReturnedNull" xml:space="preserve"> + <value>The IControllerFactory '{0}' did not return a controller for the name '{1}'.</value> + </data> + <data name="ControllerBuilder_MissingIControllerFactory" xml:space="preserve"> + <value>The controller factory type '{0}' must implement the IControllerFactory interface.</value> + </data> + <data name="Controller_UnknownAction" xml:space="preserve"> + <value>A public action method '{0}' was not found on controller '{1}'.</value> + </data> + <data name="DefaultControllerFactory_ErrorCreatingController" xml:space="preserve"> + <value>An error occurred when trying to create a controller of type '{0}'. Make sure that the controller has a parameterless public constructor.</value> + </data> + <data name="DefaultControllerFactory_NoControllerFound" xml:space="preserve"> + <value>The controller for path '{0}' was not found or does not implement IController.</value> + </data> + <data name="DefaultControllerFactory_TypeDoesNotSubclassControllerBase" xml:space="preserve"> + <value>The controller type '{0}' must implement IController.</value> + </data> + <data name="ValueProviderResult_ConversionThrew" xml:space="preserve"> + <value>The parameter conversion from type '{0}' to type '{1}' failed. See the inner exception for more information.</value> + </data> + <data name="ValueProviderResult_NoConverterExists" xml:space="preserve"> + <value>The parameter conversion from type '{0}' to type '{1}' failed because no type converter can convert between these types.</value> + </data> + <data name="ExceptionViewAttribute_NonExceptionType" xml:space="preserve"> + <value>The type '{0}' does not inherit from Exception.</value> + </data> + <data name="FilterAttribute_OrderOutOfRange" xml:space="preserve"> + <value>Order must be greater than or equal to -1.</value> + </data> + <data name="HtmlHelper_MissingSelectData" xml:space="preserve"> + <value>There is no ViewData item of type '{1}' that has the key '{0}'.</value> + </data> + <data name="HtmlHelper_TextAreaParameterOutOfRange" xml:space="preserve"> + <value>The value must be greater than or equal to zero.</value> + </data> + <data name="HtmlHelper_WrongSelectDataType" xml:space="preserve"> + <value>The ViewData item that has the key '{0}' is of type '{1}' but must be of type '{2}'.</value> + </data> + <data name="ModelBinderAttribute_ErrorCreatingModelBinder" xml:space="preserve"> + <value>An error occurred when trying to create the IModelBinder '{0}'. Make sure that the binder has a public parameterless constructor.</value> + </data> + <data name="ModelBinderAttribute_TypeNotIModelBinder" xml:space="preserve"> + <value>The type '{0}' does not implement the IModelBinder interface.</value> + </data> + <data name="ModelBinderDictionary_MultipleAttributes" xml:space="preserve"> + <value>The type '{0}' contains multiple attributes that inherit from CustomModelBinderAttribute.</value> + </data> + <data name="SessionStateTempDataProvider_SessionStateDisabled" xml:space="preserve"> + <value>The SessionStateTempDataProvider class requires session state to be enabled.</value> + </data> + <data name="ViewDataDictionary_WrongTModelType" xml:space="preserve"> + <value>The model item passed into the dictionary is of type '{0}', but this dictionary requires a model item of type '{1}'.</value> + </data> + <data name="ViewMasterPage_RequiresViewPage" xml:space="preserve"> + <value>A ViewMasterPage can be used only with content pages that derive from ViewPage or ViewPage<TModel>.</value> + </data> + <data name="ViewUserControl_RequiresViewDataProvider" xml:space="preserve"> + <value>The ViewUserControl '{0}' cannot find an IViewDataContainer object. The ViewUserControl must be inside a ViewPage, a ViewMasterPage, or another ViewUserControl.</value> + </data> + <data name="ViewUserControl_RequiresViewPage" xml:space="preserve"> + <value>A ViewUserControl can be used only in pages that derive from ViewPage or ViewPage<TModel>.</value> + </data> + <data name="WebFormViewEngine_UserControlCannotHaveMaster" xml:space="preserve"> + <value>A master name cannot be specified when the view is a ViewUserControl.</value> + </data> + <data name="WebFormViewEngine_WrongViewBase" xml:space="preserve"> + <value>The view at '{0}' must derive from ViewPage, ViewPage<TModel>, ViewUserControl, or ViewUserControl<TModel>.</value> + </data> + <data name="Common_ValueNotValidForProperty" xml:space="preserve"> + <value>The value '{0}' is invalid.</value> + </data> + <data name="ActionMethodSelector_AmbiguousMatchType" xml:space="preserve"> + <value>{0} on type {1}</value> + </data> + <data name="Controller_UpdateModel_UpdateUnsuccessful" xml:space="preserve"> + <value>The model of type '{0}' could not be updated.</value> + </data> + <data name="DefaultModelBinder_ValueRequired" xml:space="preserve"> + <value>A value is required.</value> + </data> + <data name="ReflectedActionDescriptor_ParameterCannotBeNull" xml:space="preserve"> + <value>The parameters dictionary contains a null entry for parameter '{0}' of non-nullable type '{1}' for method '{2}' in '{3}'. An optional parameter must be a reference type, a nullable type, or be declared as an optional parameter.</value> + </data> + <data name="ReflectedActionDescriptor_ParameterNotInDictionary" xml:space="preserve"> + <value>The parameters dictionary does not contain an entry for parameter '{0}' of type '{1}' for method '{2}' in '{3}'. The dictionary must contain an entry for each parameter, including parameters that have null values.</value> + </data> + <data name="ReflectedActionDescriptor_ParameterValueHasWrongType" xml:space="preserve"> + <value>The parameters dictionary contains an invalid entry for parameter '{0}' for method '{1}' in '{2}'. The dictionary contains a value of type '{3}', but the parameter requires a value of type '{4}'.</value> + </data> + <data name="ReflectedParameterBindingInfo_MultipleConverterAttributes" xml:space="preserve"> + <value>The parameter '{0}' on method '{1}' contains multiple attributes that inherit from CustomModelBinderAttribute.</value> + </data> + <data name="ReflectedActionDescriptor_CannotCallInstanceMethodOnNonControllerType" xml:space="preserve"> + <value>Cannot create a descriptor for instance method '{0}' on type '{1}' because the type does not derive from ControllerBase.</value> + </data> + <data name="ReflectedActionDescriptor_CannotCallMethodsWithOutOrRefParameters" xml:space="preserve"> + <value>Cannot call action method '{0}' on controller '{1}' because the parameter '{2}' is passed by reference.</value> + </data> + <data name="ReflectedActionDescriptor_CannotCallOpenGenericMethods" xml:space="preserve"> + <value>Cannot call action method '{0}' on controller '{1}' because the action method is a generic method.</value> + </data> + <data name="DefaultViewLocationCache_NegativeTimeSpan" xml:space="preserve"> + <value>The number of ticks for the TimeSpan value must be greater than or equal to 0.</value> + </data> + <data name="DefaultModelBinder_ValueInvalid" xml:space="preserve"> + <value>The value '{0}' is not valid for {1}.</value> + </data> + <data name="TemplateHelpers_TemplateLimitations" xml:space="preserve"> + <value>Templates can be used only with field access, property access, single-dimension array index, or single-parameter custom indexer expressions.</value> + </data> + <data name="Common_TriState_False" xml:space="preserve"> + <value>False</value> + </data> + <data name="Common_TriState_NotSet" xml:space="preserve"> + <value>Not Set</value> + </data> + <data name="Common_TriState_True" xml:space="preserve"> + <value>True</value> + </data> + <data name="ControllerBase_CannotHandleMultipleRequests" xml:space="preserve"> + <value>A single instance of controller '{0}' cannot be used to handle multiple requests. If a custom controller factory is in use, make sure that it creates a new instance of the controller for each request.</value> + </data> + <data name="Common_PropertyNotFound" xml:space="preserve"> + <value>The property {0}.{1} could not be found.</value> + </data> + <data name="DataAnnotationsModelMetadataProvider_UnknownProperty" xml:space="preserve"> + <value>{0} has a DisplayColumn attribute for {1}, but property {1} does not exist.</value> + </data> + <data name="DataAnnotationsModelMetadataProvider_UnreadableProperty" xml:space="preserve"> + <value>{0} has a DisplayColumn attribute for {1}, but property {1} does not have a public getter.</value> + </data> + <data name="TemplateHelpers_NoTemplate" xml:space="preserve"> + <value>Unable to locate an appropriate template for type {0}.</value> + </data> + <data name="RequireHttpsAttribute_MustUseSsl" xml:space="preserve"> + <value>The requested resource can only be accessed via SSL.</value> + </data> + <data name="HtmlHelper_InvalidHttpVerb" xml:space="preserve"> + <value>The specified HttpVerbs value is not supported. The supported values are Delete, Head, and Put.</value> + </data> + <data name="HtmlHelper_InvalidHttpMethod" xml:space="preserve"> + <value>The GET and POST HTTP methods are not supported.</value> + </data> + <data name="JsonRequest_GetNotAllowed" xml:space="preserve"> + <value>This request has been blocked because sensitive information could be disclosed to third party web sites when this is used in a GET request. To allow GET requests, set JsonRequestBehavior to AllowGet.</value> + </data> + <data name="ModelMetadata_PropertyNotSettable" xml:space="preserve"> + <value>This property setter is obsolete, because its value is derived from ModelMetadata.Model now.</value> + </data> + <data name="ViewDataDictionary_ModelCannotBeNull" xml:space="preserve"> + <value>The model item passed into the dictionary is null, but this dictionary requires a non-null model item of type '{0}'.</value> + </data> + <data name="Common_TypeMustDriveFromType" xml:space="preserve"> + <value>The type {0} must derive from {1}</value> + </data> + <data name="DataAnnotationsModelValidatorProvider_ConstructorRequirements" xml:space="preserve"> + <value>The type {0} must have a public constructor which accepts three parameters of types {1}, {2}, and {3}</value> + </data> + <data name="ViewPageHttpHandlerWrapper_ExceptionOccurred" xml:space="preserve"> + <value>Execution of the child request failed. Please examine the InnerException for more information.</value> + </data> + <data name="RedirectAction_CannotRedirectInChildAction" xml:space="preserve"> + <value>Child actions are not allowed to perform redirect actions.</value> + </data> + <data name="AsyncCommon_AsyncResultAlreadyConsumed" xml:space="preserve"> + <value>The provided IAsyncResult has already been consumed.</value> + </data> + <data name="AsyncCommon_InvalidAsyncResult" xml:space="preserve"> + <value>The provided IAsyncResult is not valid for this method.</value> + </data> + <data name="SynchronizationContextUtil_ExceptionThrown" xml:space="preserve"> + <value>An operation that crossed a synchronization context failed. See the inner exception for more information.</value> + </data> + <data name="AsyncCommon_ControllerMustImplementIAsyncManagerContainer" xml:space="preserve"> + <value>The controller of type '{0}' must subclass AsyncController or implement the IAsyncManagerContainer interface.</value> + </data> + <data name="AsyncCommon_InvalidTimeout" xml:space="preserve"> + <value>The timeout value must be non-negative or Timeout.Infinite.</value> + </data> + <data name="AsyncActionMethodSelector_AmbiguousMethodMatch" xml:space="preserve"> + <value>Lookup for method '{0}' on controller type '{1}' failed because of an ambiguity between the following methods:{2}</value> + </data> + <data name="AsyncActionMethodSelector_CouldNotFindMethod" xml:space="preserve"> + <value>Could not locate a method named '{0}' on controller type {1}.</value> + </data> + <data name="ChildActionOnlyAttribute_MustBeInChildRequest" xml:space="preserve"> + <value>The action '{0}' is accessible only by a child request.</value> + </data> + <data name="Templates_TypeMustImplementIEnumerable" xml:space="preserve"> + <value>The Collection template was used with an object of type '{0}', which does not implement System.IEnumerable.</value> + </data> + <data name="TypeCache_DoNotModify" xml:space="preserve"> + <value>This file is automatically generated. Please do not modify the contents of this file.</value> + </data> + <data name="ClientDataTypeModelValidatorProvider_FieldMustBeNumeric" xml:space="preserve"> + <value>The field {0} must be a number.</value> + </data> + <data name="ExpressionHelper_InvalidIndexerExpression" xml:space="preserve"> + <value>The expression compiler was unable to evaluate the indexer expression '{0}' because it references the model parameter '{1}' which is unavailable.</value> + </data> + <data name="Controller_Validate_ValidationFailed" xml:space="preserve"> + <value>The model of type '{0}' is not valid.</value> + </data> + <data name="DefaultControllerFactory_ControllerNameAmbiguous_WithoutRouteUrl" xml:space="preserve"> + <value>Multiple types were found that match the controller named '{0}'. This can happen if the route that services this request does not specify namespaces to search for a controller that matches the request. If this is the case, register this route by calling an overload of the 'MapRoute' method that takes a 'namespaces' parameter. + +The request for '{0}' has found the following matching controllers:{1}</value> + </data> + <data name="DefaultControllerFactory_ControllerNameAmbiguous_WithRouteUrl" xml:space="preserve"> + <value>Multiple types were found that match the controller named '{0}'. This can happen if the route that services this request ('{1}') does not specify namespaces to search for a controller that matches the request. If this is the case, register this route by calling an overload of the 'MapRoute' method that takes a 'namespaces' parameter. + +The request for '{0}' has found the following matching controllers:{2}</value> + </data> + <data name="DataAnnotationsModelValidatorProvider_ValidatableConstructorRequirements" xml:space="preserve"> + <value>The type {0} must have a public constructor which accepts two parameters of types {1} and {2}.</value> + </data> + <data name="ValidatableObjectAdapter_IncompatibleType" xml:space="preserve"> + <value>The model object inside the metadata claimed to be compatible with {0}, but was actually {1}.</value> + </data> + <data name="CshtmlView_ViewCouldNotBeCreated" xml:space="preserve"> + <value>The view found at '{0}' was not created.</value> + </data> + <data name="CshtmlView_WrongViewBase" xml:space="preserve"> + <value>The view at '{0}' must derive from WebViewPage, or WebViewPage<TModel>.</value> + </data> + <data name="ReflectedActionDescriptor_CannotCallStaticMethod" xml:space="preserve"> + <value>Cannot call action method '{0}' on controller '{1}' because the action method is a static method.</value> + </data> + <data name="MvcRazorCodeParser_ModelKeywordMustBeFollowedByTypeName" xml:space="preserve"> + <value>The '{0}' keyword must be followed by a type name on the same line.</value> + </data> + <data name="MvcRazorCodeParser_CannotHaveModelAndInheritsKeyword" xml:space="preserve"> + <value>The 'inherits' keyword is not allowed when a '{0}' keyword is used.</value> + </data> + <data name="MvcRazorCodeParser_OnlyOneModelStatementIsAllowed" xml:space="preserve"> + <value>Only one '{0}' statement is allowed in a file.</value> + </data> + <data name="SingleServiceResolver_CannotRegisterTwoInstances" xml:space="preserve"> + <value>An instance of {0} was found in the resolver as well as a custom registered provider in {1}. Please set only one or the other.</value> + </data> + <data name="DependencyResolver_DoesNotImplementICommonServiceLocator" xml:space="preserve"> + <value>The type {0} does not appear to implement Microsoft.Practices.ServiceLocation.IServiceLocator.</value> + </data> + <data name="ViewStartPage_RequiresMvcRazorView" xml:space="preserve"> + <value>A ViewStartPage can be used only with with a page that derives from WebViewPage or another ViewStartPage.</value> + </data> + <data name="ControllerBase_CannotExecuteWithNullHttpContext" xml:space="preserve"> + <value>Cannot execute Controller with a null HttpContext.</value> + </data> + <data name="CompareAttribute_MustMatch" xml:space="preserve"> + <value>'{0}' and '{1}' do not match.</value> + </data> + <data name="RemoteAttribute_RemoteValidationFailed" xml:space="preserve"> + <value>'{0}' is invalid.</value> + </data> + <data name="RemoteAttribute_NoUrlFound" xml:space="preserve"> + <value>No url for remote validation could be found.</value> + </data> + <data name="AuthorizeAttribute_CannotUseWithinChildActionCache" xml:space="preserve"> + <value>AuthorizeAttribute cannot be used within a child action caching block.</value> + </data> + <data name="OutputCacheAttribute_InvalidDuration" xml:space="preserve"> + <value>Duration must be a positive number.</value> + </data> + <data name="OutputCacheAttribute_InvalidVaryByParam" xml:space="preserve"> + <value>VaryByParam must be '*', 'none', or a semicolon-delimited list of keys.</value> + </data> + <data name="OutputCacheAttribute_ChildAction_UnsupportedSetting" xml:space="preserve"> + <value>OutputCacheAttribute for child actions only supports Duration, VaryByCustom, and VaryByParam values. Please do not set CacheProfile, Location, NoStore, SqlDependency, VaryByContentEncoding, or VaryByHeader values for child actions.</value> + </data> + <data name="OutputCacheAttribute_CannotNestChildCache" xml:space="preserve"> + <value>OutputCacheAttribute is not allowed on child actions which are children of an already cached child action.</value> + </data> + <data name="CompareAttribute_UnknownProperty" xml:space="preserve"> + <value>Could not find a property named {0}.</value> + </data> + <data name="HtmlHelper_SelectExpressionNotEnumerable" xml:space="preserve"> + <value>The parameter '{0}' must evaluate to an IEnumerable when multiple selection is allowed.</value> + </data> + <data name="MvcRouteHandler_RouteValuesHasNoController" xml:space="preserve"> + <value>The matched route does not include a 'controller' route value, which is required.</value> + </data> + <data name="ClientDataTypeModelValidatorProvider_FieldMustBeDate" xml:space="preserve"> + <value>The field {0} must be a date.</value> + </data> + <data name="TaskAsyncActionDescriptor_CannotExecuteSynchronously" xml:space="preserve"> + <value>The asynchronous action method '{0}' returns a Task, which cannot be executed synchronously.</value> + </data> + <data name="AsyncActionDescriptor_CannotExecuteSynchronously" xml:space="preserve"> + <value>The asynchronous action method '{0}' cannot be executed synchronously.</value> + </data> + <data name="MvcForm_ConstructorObsolete" xml:space="preserve"> + <value>This constructor is obsolete, because its functionality has been moved to MvcForm(ViewContext) now.</value> + </data> + <data name="JsonValueProviderFactory_RequestTooLarge" xml:space="preserve"> + <value>The JSON request was too large to be deserialized.</value> + </data> +</root>
\ No newline at end of file diff --git a/src/System.Web.Mvc/QueryStringValueProvider.cs b/src/System.Web.Mvc/QueryStringValueProvider.cs new file mode 100644 index 00000000..3a2d34ba --- /dev/null +++ b/src/System.Web.Mvc/QueryStringValueProvider.cs @@ -0,0 +1,21 @@ +using System.Globalization; +using System.Web.Helpers; + +namespace System.Web.Mvc +{ + public sealed class QueryStringValueProvider : NameValueCollectionValueProvider + { + // QueryString should use the invariant culture since it's part of the URL, and the URL should be + // interpreted in a uniform fashion regardless of the origin of a particular request. + public QueryStringValueProvider(ControllerContext controllerContext) + : this(controllerContext, new UnvalidatedRequestValuesWrapper(controllerContext.HttpContext.Request.Unvalidated())) + { + } + + // For unit testing + internal QueryStringValueProvider(ControllerContext controllerContext, IUnvalidatedRequestValues unvalidatedValues) + : base(controllerContext.HttpContext.Request.QueryString, unvalidatedValues.QueryString, CultureInfo.InvariantCulture) + { + } + } +} diff --git a/src/System.Web.Mvc/QueryStringValueProviderFactory.cs b/src/System.Web.Mvc/QueryStringValueProviderFactory.cs new file mode 100644 index 00000000..eaa0f696 --- /dev/null +++ b/src/System.Web.Mvc/QueryStringValueProviderFactory.cs @@ -0,0 +1,30 @@ +using System.Web.Helpers; + +namespace System.Web.Mvc +{ + public sealed class QueryStringValueProviderFactory : ValueProviderFactory + { + private readonly UnvalidatedRequestValuesAccessor _unvalidatedValuesAccessor; + + public QueryStringValueProviderFactory() + : this(null) + { + } + + // For unit testing + internal QueryStringValueProviderFactory(UnvalidatedRequestValuesAccessor unvalidatedValuesAccessor) + { + _unvalidatedValuesAccessor = unvalidatedValuesAccessor ?? (cc => new UnvalidatedRequestValuesWrapper(cc.HttpContext.Request.Unvalidated())); + } + + public override IValueProvider GetValueProvider(ControllerContext controllerContext) + { + if (controllerContext == null) + { + throw new ArgumentNullException("controllerContext"); + } + + return new QueryStringValueProvider(controllerContext, _unvalidatedValuesAccessor(controllerContext)); + } + } +} diff --git a/src/System.Web.Mvc/RangeAttributeAdapter.cs b/src/System.Web.Mvc/RangeAttributeAdapter.cs new file mode 100644 index 00000000..d0b41660 --- /dev/null +++ b/src/System.Web.Mvc/RangeAttributeAdapter.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace System.Web.Mvc +{ + public class RangeAttributeAdapter : DataAnnotationsModelValidator<RangeAttribute> + { + public RangeAttributeAdapter(ModelMetadata metadata, ControllerContext context, RangeAttribute attribute) + : base(metadata, context, attribute) + { + } + + public override IEnumerable<ModelClientValidationRule> GetClientValidationRules() + { + string errorMessage = ErrorMessage; // Per Dev10 Bug #923283, need to make sure ErrorMessage is called before Minimum/Maximum + return new[] { new ModelClientValidationRangeRule(errorMessage, Attribute.Minimum, Attribute.Maximum) }; + } + } +} diff --git a/src/System.Web.Mvc/Razor/MvcCSharpRazorCodeGenerator.cs b/src/System.Web.Mvc/Razor/MvcCSharpRazorCodeGenerator.cs new file mode 100644 index 00000000..b09a95a9 --- /dev/null +++ b/src/System.Web.Mvc/Razor/MvcCSharpRazorCodeGenerator.cs @@ -0,0 +1,30 @@ +using System.CodeDom; +using System.Web.Razor; +using System.Web.Razor.Generator; + +namespace System.Web.Mvc.Razor +{ + internal class MvcCSharpRazorCodeGenerator : CSharpRazorCodeGenerator + { + private const string DefaultModelTypeName = "dynamic"; + + public MvcCSharpRazorCodeGenerator(string className, string rootNamespaceName, string sourceFileName, RazorEngineHost host) + : base(className, rootNamespaceName, sourceFileName, host) + { + var mvcHost = host as MvcWebPageRazorHost; + if (mvcHost != null && !mvcHost.IsSpecialPage) + { + // set the default model type to "dynamic" (Dev10 bug 935656) + // don't set it for "special" pages (such as "_viewStart.cshtml") + SetBaseType(DefaultModelTypeName); + } + } + + private void SetBaseType(string modelTypeName) + { + var baseType = new CodeTypeReference(Context.Host.DefaultBaseClass + "<" + modelTypeName + ">"); + Context.GeneratedClass.BaseTypes.Clear(); + Context.GeneratedClass.BaseTypes.Add(baseType); + } + } +} diff --git a/src/System.Web.Mvc/Razor/MvcCSharpRazorCodeParser.cs b/src/System.Web.Mvc/Razor/MvcCSharpRazorCodeParser.cs new file mode 100644 index 00000000..e2d30351 --- /dev/null +++ b/src/System.Web.Mvc/Razor/MvcCSharpRazorCodeParser.cs @@ -0,0 +1,68 @@ +using System.Globalization; +using System.Web.Mvc.Properties; +using System.Web.Razor.Generator; +using System.Web.Razor.Parser; +using System.Web.Razor.Text; + +namespace System.Web.Mvc.Razor +{ + public class MvcCSharpRazorCodeParser : CSharpCodeParser + { + private const string ModelKeyword = "model"; + private const string GenericTypeFormatString = "{0}<{1}>"; + private SourceLocation? _endInheritsLocation; + private bool _modelStatementFound; + + public MvcCSharpRazorCodeParser() + { + MapDirectives(ModelDirective, ModelKeyword); + } + + protected override void InheritsDirective() + { + // Verify we're on the right keyword and accept + AssertDirective(SyntaxConstants.CSharp.InheritsKeyword); + AcceptAndMoveNext(); + _endInheritsLocation = CurrentLocation; + + InheritsDirectiveCore(); + CheckForInheritsAndModelStatements(); + } + + private void CheckForInheritsAndModelStatements() + { + if (_modelStatementFound && _endInheritsLocation.HasValue) + { + Context.OnError(_endInheritsLocation.Value, String.Format(CultureInfo.CurrentCulture, MvcResources.MvcRazorCodeParser_CannotHaveModelAndInheritsKeyword, ModelKeyword)); + } + } + + protected virtual void ModelDirective() + { + // Verify we're on the right keyword and accept + AssertDirective(ModelKeyword); + AcceptAndMoveNext(); + + SourceLocation endModelLocation = CurrentLocation; + + BaseTypeDirective( + String.Format(CultureInfo.CurrentCulture, + MvcResources.MvcRazorCodeParser_ModelKeywordMustBeFollowedByTypeName, ModelKeyword), + CreateModelCodeGenerator); + + if (_modelStatementFound) + { + Context.OnError(endModelLocation, String.Format(CultureInfo.CurrentCulture, MvcResources.MvcRazorCodeParser_OnlyOneModelStatementIsAllowed, ModelKeyword)); + } + + _modelStatementFound = true; + + CheckForInheritsAndModelStatements(); + } + + private SpanCodeGenerator CreateModelCodeGenerator(string model) + { + return new SetModelTypeCodeGenerator(model, GenericTypeFormatString); + } + } +} diff --git a/src/System.Web.Mvc/Razor/MvcVBRazorCodeParser.cs b/src/System.Web.Mvc/Razor/MvcVBRazorCodeParser.cs new file mode 100644 index 00000000..d2e33479 --- /dev/null +++ b/src/System.Web.Mvc/Razor/MvcVBRazorCodeParser.cs @@ -0,0 +1,93 @@ +using System.Globalization; +using System.Linq; +using System.Web.Mvc.Properties; +using System.Web.Razor.Generator; +using System.Web.Razor.Parser; +using System.Web.Razor.Parser.SyntaxTree; +using System.Web.Razor.Text; +using System.Web.Razor.Tokenizer.Symbols; + +namespace System.Web.Mvc.Razor +{ + public class MvcVBRazorCodeParser : VBCodeParser + { + internal const string ModelTypeKeyword = "ModelType"; + private const string GenericTypeFormatString = "{0}(Of {1})"; + private SourceLocation? _endInheritsLocation; + private bool _modelStatementFound; + + public MvcVBRazorCodeParser() + { + MapDirective(ModelTypeKeyword, ModelTypeDirective); + } + + protected override bool InheritsStatement() + { + // Verify we're on the right keyword and accept + Assert(VBKeyword.Inherits); + VBSymbol inherits = CurrentSymbol; + NextToken(); + _endInheritsLocation = CurrentLocation; + PutCurrentBack(); + PutBack(inherits); + EnsureCurrent(); + + bool result = base.InheritsStatement(); + CheckForInheritsAndModelStatements(); + return result; + } + + private void CheckForInheritsAndModelStatements() + { + if (_modelStatementFound && _endInheritsLocation.HasValue) + { + Context.OnError(_endInheritsLocation.Value, String.Format(CultureInfo.CurrentCulture, MvcResources.MvcRazorCodeParser_CannotHaveModelAndInheritsKeyword, ModelTypeKeyword)); + } + } + + protected virtual bool ModelTypeDirective() + { + AssertDirective(ModelTypeKeyword); + + Span.CodeGenerator = SpanCodeGenerator.Null; + Context.CurrentBlock.Type = BlockType.Directive; + + AcceptAndMoveNext(); + SourceLocation endModelLocation = CurrentLocation; + + if (At(VBSymbolType.WhiteSpace)) + { + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + } + + AcceptWhile(VBSymbolType.WhiteSpace); + Output(SpanKind.MetaCode); + + if (_modelStatementFound) + { + Context.OnError(endModelLocation, String.Format(CultureInfo.CurrentCulture, MvcResources.MvcRazorCodeParser_OnlyOneModelStatementIsAllowed, ModelTypeKeyword)); + } + _modelStatementFound = true; + + if (EndOfFile || At(VBSymbolType.WhiteSpace) || At(VBSymbolType.NewLine)) + { + Context.OnError(endModelLocation, MvcResources.MvcRazorCodeParser_ModelKeywordMustBeFollowedByTypeName, ModelTypeKeyword); + } + + // Just accept to a newline + AcceptUntil(VBSymbolType.NewLine); + if (!Context.DesignTimeMode) + { + // We want the newline to be treated as code, but it causes issues at design-time. + Optional(VBSymbolType.NewLine); + } + + string baseType = String.Concat(Span.Symbols.Select(s => s.Content)).Trim(); + Span.CodeGenerator = new SetModelTypeCodeGenerator(baseType, GenericTypeFormatString); + + CheckForInheritsAndModelStatements(); + Output(SpanKind.Code); + return false; + } + } +} diff --git a/src/System.Web.Mvc/Razor/MvcWebPageRazorHost.cs b/src/System.Web.Mvc/Razor/MvcWebPageRazorHost.cs new file mode 100644 index 00000000..69f058f7 --- /dev/null +++ b/src/System.Web.Mvc/Razor/MvcWebPageRazorHost.cs @@ -0,0 +1,64 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Web.Razor.Generator; +using System.Web.Razor.Parser; +using System.Web.WebPages.Razor; + +namespace System.Web.Mvc.Razor +{ + [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "WebPage", Justification = "The class name is derived from the name of the base type")] + public class MvcWebPageRazorHost : WebPageRazorHost + { + [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "The NamespaceImports property should not be virtual. This is a temporary fix.")] + public MvcWebPageRazorHost(string virtualPath, string physicalPath) + : base(virtualPath, physicalPath) + { + RegisterSpecialFile(RazorViewEngine.ViewStartFileName, typeof(ViewStartPage)); + + DefaultPageBaseClass = typeof(WebViewPage).FullName; + + // REVIEW get rid of the namespace import to not force additional references in default MVC projects + GetRidOfNamespace("System.Web.WebPages.Html"); + } + + public override RazorCodeGenerator DecorateCodeGenerator(RazorCodeGenerator incomingCodeGenerator) + { + if (incomingCodeGenerator is CSharpRazorCodeGenerator) + { + return new MvcCSharpRazorCodeGenerator(incomingCodeGenerator.ClassName, + incomingCodeGenerator.RootNamespaceName, + incomingCodeGenerator.SourceFileName, + incomingCodeGenerator.Host); + } + else + { + return base.DecorateCodeGenerator(incomingCodeGenerator); + } + } + + public override ParserBase DecorateCodeParser(ParserBase incomingCodeParser) + { + if (incomingCodeParser is CSharpCodeParser) + { + return new MvcCSharpRazorCodeParser(); + } + else if (incomingCodeParser is VBCodeParser) + { + return new MvcVBRazorCodeParser(); + } + else + { + return base.DecorateCodeParser(incomingCodeParser); + } + } + + private void GetRidOfNamespace(string ns) + { + Debug.Assert(NamespaceImports.Contains(ns), ns + " is not a default namespace anymore"); + if (NamespaceImports.Contains(ns)) + { + NamespaceImports.Remove(ns); + } + } + } +} diff --git a/src/System.Web.Mvc/Razor/SetModelTypeCodeGenerator.cs b/src/System.Web.Mvc/Razor/SetModelTypeCodeGenerator.cs new file mode 100644 index 00000000..e8640b20 --- /dev/null +++ b/src/System.Web.Mvc/Razor/SetModelTypeCodeGenerator.cs @@ -0,0 +1,47 @@ +using System.Globalization; +using System.Web.Mvc.ExpressionUtil; +using System.Web.Razor.Generator; + +namespace System.Web.Mvc.Razor +{ + internal class SetModelTypeCodeGenerator : SetBaseTypeCodeGenerator + { + private string _genericTypeFormat; + + public SetModelTypeCodeGenerator(string modelType, string genericTypeFormat) + : base(modelType) + { + _genericTypeFormat = genericTypeFormat; + } + + protected override string ResolveType(CodeGeneratorContext context, string baseType) + { + return String.Format( + CultureInfo.InvariantCulture, + _genericTypeFormat, + context.Host.DefaultBaseClass, + baseType); + } + + public override bool Equals(object obj) + { + SetModelTypeCodeGenerator other = obj as SetModelTypeCodeGenerator; + return other != null && + base.Equals(obj) && + String.Equals(_genericTypeFormat, other._genericTypeFormat, StringComparison.Ordinal); + } + + public override int GetHashCode() + { + var combiner = new HashCodeCombiner(); + combiner.AddInt32(base.GetHashCode()); + combiner.AddObject(_genericTypeFormat); + return combiner.CombinedHash; + } + + public override string ToString() + { + return "Model:" + BaseType; + } + } +} diff --git a/src/System.Web.Mvc/Razor/StartPageLookupDelegate.cs b/src/System.Web.Mvc/Razor/StartPageLookupDelegate.cs new file mode 100644 index 00000000..07197610 --- /dev/null +++ b/src/System.Web.Mvc/Razor/StartPageLookupDelegate.cs @@ -0,0 +1,7 @@ +using System.Collections.Generic; +using System.Web.WebPages; + +namespace System.Web.Mvc.Razor +{ + internal delegate WebPageRenderingBase StartPageLookupDelegate(WebPageRenderingBase page, string fileName, IEnumerable<string> supportedExtensions); +} diff --git a/src/System.Web.Mvc/RazorView.cs b/src/System.Web.Mvc/RazorView.cs new file mode 100644 index 00000000..70b96cb3 --- /dev/null +++ b/src/System.Web.Mvc/RazorView.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Web.Mvc.Properties; +using System.Web.Mvc.Razor; +using System.Web.WebPages; + +namespace System.Web.Mvc +{ + public class RazorView : BuildManagerCompiledView + { + public RazorView(ControllerContext controllerContext, string viewPath, string layoutPath, bool runViewStartPages, IEnumerable<string> viewStartFileExtensions) + : this(controllerContext, viewPath, layoutPath, runViewStartPages, viewStartFileExtensions, null) + { + } + + public RazorView(ControllerContext controllerContext, string viewPath, string layoutPath, bool runViewStartPages, IEnumerable<string> viewStartFileExtensions, IViewPageActivator viewPageActivator) + : base(controllerContext, viewPath, viewPageActivator) + { + LayoutPath = layoutPath ?? String.Empty; + RunViewStartPages = runViewStartPages; + StartPageLookup = StartPage.GetStartPage; + ViewStartFileExtensions = viewStartFileExtensions ?? Enumerable.Empty<string>(); + } + + public string LayoutPath { get; private set; } + + public bool RunViewStartPages { get; private set; } + + internal StartPageLookupDelegate StartPageLookup { get; set; } + + internal IVirtualPathFactory VirtualPathFactory { get; set; } + + internal DisplayModeProvider DisplayModeProvider { get; set; } + + public IEnumerable<string> ViewStartFileExtensions { get; private set; } + + protected override void RenderView(ViewContext viewContext, TextWriter writer, object instance) + { + if (writer == null) + { + throw new ArgumentNullException("writer"); + } + + WebViewPage webViewPage = instance as WebViewPage; + if (webViewPage == null) + { + throw new InvalidOperationException( + String.Format( + CultureInfo.CurrentCulture, + MvcResources.CshtmlView_WrongViewBase, + ViewPath)); + } + + // An overriden master layout might have been specified when the ViewActionResult got returned. + // We need to hold on to it so that we can set it on the inner page once it has executed. + webViewPage.OverridenLayoutPath = LayoutPath; + webViewPage.VirtualPath = ViewPath; + webViewPage.ViewContext = viewContext; + webViewPage.ViewData = viewContext.ViewData; + + webViewPage.InitHelpers(); + + if (VirtualPathFactory != null) + { + webViewPage.VirtualPathFactory = VirtualPathFactory; + } + if (DisplayModeProvider != null) + { + webViewPage.DisplayModeProvider = DisplayModeProvider; + } + + WebPageRenderingBase startPage = null; + if (RunViewStartPages) + { + startPage = StartPageLookup(webViewPage, RazorViewEngine.ViewStartFileName, ViewStartFileExtensions); + } + webViewPage.ExecutePageHierarchy(new WebPageContext(context: viewContext.HttpContext, page: null, model: null), writer, startPage); + } + } +} diff --git a/src/System.Web.Mvc/RazorViewEngine.cs b/src/System.Web.Mvc/RazorViewEngine.cs new file mode 100644 index 00000000..9e903be6 --- /dev/null +++ b/src/System.Web.Mvc/RazorViewEngine.cs @@ -0,0 +1,85 @@ +namespace System.Web.Mvc +{ + public class RazorViewEngine : BuildManagerViewEngine + { + internal static readonly string ViewStartFileName = "_ViewStart"; + + public RazorViewEngine() + : this(null) + { + } + + public RazorViewEngine(IViewPageActivator viewPageActivator) + : base(viewPageActivator) + { + AreaViewLocationFormats = new[] + { + "~/Areas/{2}/Views/{1}/{0}.cshtml", + "~/Areas/{2}/Views/{1}/{0}.vbhtml", + "~/Areas/{2}/Views/Shared/{0}.cshtml", + "~/Areas/{2}/Views/Shared/{0}.vbhtml" + }; + AreaMasterLocationFormats = new[] + { + "~/Areas/{2}/Views/{1}/{0}.cshtml", + "~/Areas/{2}/Views/{1}/{0}.vbhtml", + "~/Areas/{2}/Views/Shared/{0}.cshtml", + "~/Areas/{2}/Views/Shared/{0}.vbhtml" + }; + AreaPartialViewLocationFormats = new[] + { + "~/Areas/{2}/Views/{1}/{0}.cshtml", + "~/Areas/{2}/Views/{1}/{0}.vbhtml", + "~/Areas/{2}/Views/Shared/{0}.cshtml", + "~/Areas/{2}/Views/Shared/{0}.vbhtml" + }; + + ViewLocationFormats = new[] + { + "~/Views/{1}/{0}.cshtml", + "~/Views/{1}/{0}.vbhtml", + "~/Views/Shared/{0}.cshtml", + "~/Views/Shared/{0}.vbhtml" + }; + MasterLocationFormats = new[] + { + "~/Views/{1}/{0}.cshtml", + "~/Views/{1}/{0}.vbhtml", + "~/Views/Shared/{0}.cshtml", + "~/Views/Shared/{0}.vbhtml" + }; + PartialViewLocationFormats = new[] + { + "~/Views/{1}/{0}.cshtml", + "~/Views/{1}/{0}.vbhtml", + "~/Views/Shared/{0}.cshtml", + "~/Views/Shared/{0}.vbhtml" + }; + + FileExtensions = new[] + { + "cshtml", + "vbhtml", + }; + } + + protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath) + { + return new RazorView(controllerContext, partialPath, + layoutPath: null, runViewStartPages: false, viewStartFileExtensions: FileExtensions, viewPageActivator: ViewPageActivator) + { + DisplayModeProvider = DisplayModeProvider + }; + } + + protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath) + { + var view = new RazorView(controllerContext, viewPath, + layoutPath: masterPath, runViewStartPages: true, viewStartFileExtensions: FileExtensions, viewPageActivator: ViewPageActivator) + { + DisplayModeProvider = DisplayModeProvider + }; + return view; + } + } +} diff --git a/src/System.Web.Mvc/ReaderWriterCache`2.cs b/src/System.Web.Mvc/ReaderWriterCache`2.cs new file mode 100644 index 00000000..a02b4740 --- /dev/null +++ b/src/System.Web.Mvc/ReaderWriterCache`2.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading; + +namespace System.Web.Mvc +{ + [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "Instances of this type are meant to be singletons.")] + internal abstract class ReaderWriterCache<TKey, TValue> + { + private readonly Dictionary<TKey, TValue> _cache; + private readonly ReaderWriterLockSlim _readerWriterLock = new ReaderWriterLockSlim(); + + protected ReaderWriterCache() + : this(null) + { + } + + protected ReaderWriterCache(IEqualityComparer<TKey> comparer) + { + _cache = new Dictionary<TKey, TValue>(comparer); + } + + protected Dictionary<TKey, TValue> Cache + { + get { return _cache; } + } + + protected TValue FetchOrCreateItem(TKey key, Func<TValue> creator) + { + // first, see if the item already exists in the cache + _readerWriterLock.EnterReadLock(); + try + { + TValue existingEntry; + if (_cache.TryGetValue(key, out existingEntry)) + { + return existingEntry; + } + } + finally + { + _readerWriterLock.ExitReadLock(); + } + + // insert the new item into the cache + TValue newEntry = creator(); + _readerWriterLock.EnterWriteLock(); + try + { + TValue existingEntry; + if (_cache.TryGetValue(key, out existingEntry)) + { + // another thread already inserted an item, so use that one + return existingEntry; + } + + _cache[key] = newEntry; + return newEntry; + } + finally + { + _readerWriterLock.ExitWriteLock(); + } + } + } +} diff --git a/src/System.Web.Mvc/RedirectResult.cs b/src/System.Web.Mvc/RedirectResult.cs new file mode 100644 index 00000000..0dfad042 --- /dev/null +++ b/src/System.Web.Mvc/RedirectResult.cs @@ -0,0 +1,56 @@ +using System.Diagnostics.CodeAnalysis; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + // represents a result that performs a redirection given some URI + public class RedirectResult : ActionResult + { + [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "0#", Justification = "Response.Redirect() takes its URI as a string parameter.")] + public RedirectResult(string url) + : this(url, permanent: false) + { + } + + [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "0#", Justification = "Response.Redirect() takes its URI as a string parameter.")] + public RedirectResult(string url, bool permanent) + { + if (String.IsNullOrEmpty(url)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "url"); + } + + Permanent = permanent; + Url = url; + } + + public bool Permanent { get; private set; } + + [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Response.Redirect() takes its URI as a string parameter.")] + public string Url { get; private set; } + + public override void ExecuteResult(ControllerContext context) + { + if (context == null) + { + throw new ArgumentNullException("context"); + } + if (context.IsChildAction) + { + throw new InvalidOperationException(MvcResources.RedirectAction_CannotRedirectInChildAction); + } + + string destinationUrl = UrlHelper.GenerateContentUrl(Url, context.HttpContext); + context.Controller.TempData.Keep(); + + if (Permanent) + { + context.HttpContext.Response.RedirectPermanent(destinationUrl, endResponse: false); + } + else + { + context.HttpContext.Response.Redirect(destinationUrl, endResponse: false); + } + } + } +} diff --git a/src/System.Web.Mvc/RedirectToRouteResult.cs b/src/System.Web.Mvc/RedirectToRouteResult.cs new file mode 100644 index 00000000..c35846d0 --- /dev/null +++ b/src/System.Web.Mvc/RedirectToRouteResult.cs @@ -0,0 +1,77 @@ +using System.Web.Mvc.Properties; +using System.Web.Routing; + +namespace System.Web.Mvc +{ + // represents a result that performs a redirection given some values dictionary + public class RedirectToRouteResult : ActionResult + { + private RouteCollection _routes; + + public RedirectToRouteResult(RouteValueDictionary routeValues) + : + this(null, routeValues) + { + } + + public RedirectToRouteResult(string routeName, RouteValueDictionary routeValues) + : this(routeName, routeValues, permanent: false) + { + } + + public RedirectToRouteResult(string routeName, RouteValueDictionary routeValues, bool permanent) + { + Permanent = permanent; + RouteName = routeName ?? String.Empty; + RouteValues = routeValues ?? new RouteValueDictionary(); + } + + public bool Permanent { get; private set; } + + public string RouteName { get; private set; } + + public RouteValueDictionary RouteValues { get; private set; } + + internal RouteCollection Routes + { + get + { + if (_routes == null) + { + _routes = RouteTable.Routes; + } + return _routes; + } + set { _routes = value; } + } + + public override void ExecuteResult(ControllerContext context) + { + if (context == null) + { + throw new ArgumentNullException("context"); + } + if (context.IsChildAction) + { + throw new InvalidOperationException(MvcResources.RedirectAction_CannotRedirectInChildAction); + } + + string destinationUrl = UrlHelper.GenerateUrl(RouteName, null /* actionName */, null /* controllerName */, RouteValues, Routes, context.RequestContext, false /* includeImplicitMvcValues */); + if (String.IsNullOrEmpty(destinationUrl)) + { + throw new InvalidOperationException(MvcResources.Common_NoRouteMatched); + } + + context.Controller.TempData.Keep(); + + if (Permanent) + { + context.HttpContext.Response.RedirectPermanent(destinationUrl, endResponse: false); + } + else + { + context.HttpContext.Response.Redirect(destinationUrl, endResponse: false); + } + } + } +} diff --git a/src/System.Web.Mvc/ReflectedActionDescriptor.cs b/src/System.Web.Mvc/ReflectedActionDescriptor.cs new file mode 100644 index 00000000..707521ec --- /dev/null +++ b/src/System.Web.Mvc/ReflectedActionDescriptor.cs @@ -0,0 +1,135 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + public class ReflectedActionDescriptor : ActionDescriptor + { + private readonly string _actionName; + private readonly ControllerDescriptor _controllerDescriptor; + private readonly Lazy<string> _uniqueId; + private ParameterDescriptor[] _parametersCache; + + public ReflectedActionDescriptor(MethodInfo methodInfo, string actionName, ControllerDescriptor controllerDescriptor) + : this(methodInfo, actionName, controllerDescriptor, true /* validateMethod */) + { + } + + internal ReflectedActionDescriptor(MethodInfo methodInfo, string actionName, ControllerDescriptor controllerDescriptor, bool validateMethod) + { + if (methodInfo == null) + { + throw new ArgumentNullException("methodInfo"); + } + if (String.IsNullOrEmpty(actionName)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "actionName"); + } + if (controllerDescriptor == null) + { + throw new ArgumentNullException("controllerDescriptor"); + } + + if (validateMethod) + { + string failedMessage = VerifyActionMethodIsCallable(methodInfo); + if (failedMessage != null) + { + throw new ArgumentException(failedMessage, "methodInfo"); + } + } + + MethodInfo = methodInfo; + _actionName = actionName; + _controllerDescriptor = controllerDescriptor; + _uniqueId = new Lazy<string>(CreateUniqueId); + } + + public override string ActionName + { + get { return _actionName; } + } + + public override ControllerDescriptor ControllerDescriptor + { + get { return _controllerDescriptor; } + } + + public MethodInfo MethodInfo { get; private set; } + + public override string UniqueId + { + get { return _uniqueId.Value; } + } + + private string CreateUniqueId() + { + return base.UniqueId + DescriptorUtil.CreateUniqueId(MethodInfo); + } + + public override object Execute(ControllerContext controllerContext, IDictionary<string, object> parameters) + { + if (controllerContext == null) + { + throw new ArgumentNullException("controllerContext"); + } + if (parameters == null) + { + throw new ArgumentNullException("parameters"); + } + + ParameterInfo[] parameterInfos = MethodInfo.GetParameters(); + var rawParameterValues = from parameterInfo in parameterInfos + select ExtractParameterFromDictionary(parameterInfo, parameters, MethodInfo); + object[] parametersArray = rawParameterValues.ToArray(); + + ActionMethodDispatcher dispatcher = DispatcherCache.GetDispatcher(MethodInfo); + object actionReturnValue = dispatcher.Execute(controllerContext.Controller, parametersArray); + return actionReturnValue; + } + + public override object[] GetCustomAttributes(bool inherit) + { + return ActionDescriptorHelper.GetCustomAttributes(MethodInfo, inherit); + } + + public override object[] GetCustomAttributes(Type attributeType, bool inherit) + { + return ActionDescriptorHelper.GetCustomAttributes(MethodInfo, attributeType, inherit); + } + + public override IEnumerable<FilterAttribute> GetFilterAttributes(bool useCache) + { + if (useCache && GetType() == typeof(ReflectedActionDescriptor)) + { + // Do not look at cache in types derived from this type because they might incorrectly implement GetCustomAttributes + return ReflectedAttributeCache.GetMethodFilterAttributes(MethodInfo); + } + return base.GetFilterAttributes(useCache); + } + + public override ParameterDescriptor[] GetParameters() + { + return ActionDescriptorHelper.GetParameters(this, MethodInfo, ref _parametersCache); + } + + public override ICollection<ActionSelector> GetSelectors() + { + return ActionDescriptorHelper.GetSelectors(MethodInfo); + } + + public override bool IsDefined(Type attributeType, bool inherit) + { + return ActionDescriptorHelper.IsDefined(MethodInfo, attributeType, inherit); + } + + internal static ReflectedActionDescriptor TryCreateDescriptor(MethodInfo methodInfo, string name, ControllerDescriptor controllerDescriptor) + { + ReflectedActionDescriptor descriptor = new ReflectedActionDescriptor(methodInfo, name, controllerDescriptor, false /* validateMethod */); + string failedMessage = VerifyActionMethodIsCallable(methodInfo); + return (failedMessage == null) ? descriptor : null; + } + } +} diff --git a/src/System.Web.Mvc/ReflectedAttributeCache.cs b/src/System.Web.Mvc/ReflectedAttributeCache.cs new file mode 100644 index 00000000..98b3b159 --- /dev/null +++ b/src/System.Web.Mvc/ReflectedAttributeCache.cs @@ -0,0 +1,46 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Reflection; + +namespace System.Web.Mvc +{ + internal static class ReflectedAttributeCache + { + private static readonly ConcurrentDictionary<MethodInfo, ReadOnlyCollection<ActionMethodSelectorAttribute>> _actionMethodSelectorAttributeCache = new ConcurrentDictionary<MethodInfo, ReadOnlyCollection<ActionMethodSelectorAttribute>>(); + private static readonly ConcurrentDictionary<MethodInfo, ReadOnlyCollection<ActionNameSelectorAttribute>> _actionNameSelectorAttributeCache = new ConcurrentDictionary<MethodInfo, ReadOnlyCollection<ActionNameSelectorAttribute>>(); + private static readonly ConcurrentDictionary<MethodInfo, ReadOnlyCollection<FilterAttribute>> _methodFilterAttributeCache = new ConcurrentDictionary<MethodInfo, ReadOnlyCollection<FilterAttribute>>(); + + private static readonly ConcurrentDictionary<Type, ReadOnlyCollection<FilterAttribute>> _typeFilterAttributeCache = new ConcurrentDictionary<Type, ReadOnlyCollection<FilterAttribute>>(); + + public static ICollection<FilterAttribute> GetTypeFilterAttributes(Type type) + { + return GetAttributes(_typeFilterAttributeCache, type); + } + + public static ICollection<FilterAttribute> GetMethodFilterAttributes(MethodInfo methodInfo) + { + return GetAttributes(_methodFilterAttributeCache, methodInfo); + } + + public static ICollection<ActionMethodSelectorAttribute> GetActionMethodSelectorAttributes(MethodInfo methodInfo) + { + return GetAttributes(_actionMethodSelectorAttributeCache, methodInfo); + } + + public static ICollection<ActionNameSelectorAttribute> GetActionNameSelectorAttributes(MethodInfo methodInfo) + { + return GetAttributes(_actionNameSelectorAttributeCache, methodInfo); + } + + private static ReadOnlyCollection<TAttribute> GetAttributes<TMemberInfo, TAttribute>(ConcurrentDictionary<TMemberInfo, ReadOnlyCollection<TAttribute>> lookup, TMemberInfo memberInfo) + where TAttribute : Attribute + where TMemberInfo : MemberInfo + { + Debug.Assert(memberInfo != null); + Debug.Assert(lookup != null); + return lookup.GetOrAdd(memberInfo, mi => new ReadOnlyCollection<TAttribute>((TAttribute[])memberInfo.GetCustomAttributes(typeof(TAttribute), inherit: true))); + } + } +} diff --git a/src/System.Web.Mvc/ReflectedControllerDescriptor.cs b/src/System.Web.Mvc/ReflectedControllerDescriptor.cs new file mode 100644 index 00000000..bdda9050 --- /dev/null +++ b/src/System.Web.Mvc/ReflectedControllerDescriptor.cs @@ -0,0 +1,99 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + public class ReflectedControllerDescriptor : ControllerDescriptor + { + private readonly Type _controllerType; + private readonly ActionMethodSelector _selector; + private ActionDescriptor[] _canonicalActionsCache; + + public ReflectedControllerDescriptor(Type controllerType) + { + if (controllerType == null) + { + throw new ArgumentNullException("controllerType"); + } + + _controllerType = controllerType; + _selector = new ActionMethodSelector(_controllerType); + } + + public sealed override Type ControllerType + { + get { return _controllerType; } + } + + public override ActionDescriptor FindAction(ControllerContext controllerContext, string actionName) + { + if (controllerContext == null) + { + throw new ArgumentNullException("controllerContext"); + } + if (String.IsNullOrEmpty(actionName)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "actionName"); + } + + MethodInfo matched = _selector.FindActionMethod(controllerContext, actionName); + if (matched == null) + { + return null; + } + + return new ReflectedActionDescriptor(matched, actionName, this); + } + + private MethodInfo[] GetAllActionMethodsFromSelector() + { + List<MethodInfo> allValidMethods = new List<MethodInfo>(); + allValidMethods.AddRange(_selector.AliasedMethods); + allValidMethods.AddRange(_selector.NonAliasedMethods.SelectMany(g => g)); + return allValidMethods.ToArray(); + } + + public override ActionDescriptor[] GetCanonicalActions() + { + ActionDescriptor[] actions = LazilyFetchCanonicalActionsCollection(); + + // need to clone array so that user modifications aren't accidentally stored + return (ActionDescriptor[])actions.Clone(); + } + + public override object[] GetCustomAttributes(bool inherit) + { + return ControllerType.GetCustomAttributes(inherit); + } + + public override object[] GetCustomAttributes(Type attributeType, bool inherit) + { + return ControllerType.GetCustomAttributes(attributeType, inherit); + } + + public override IEnumerable<FilterAttribute> GetFilterAttributes(bool useCache) + { + if (useCache && GetType() == typeof(ReflectedControllerDescriptor)) + { + // Do not look at cache in types derived from this type because they might incorrectly implement GetCustomAttributes + return ReflectedAttributeCache.GetTypeFilterAttributes(ControllerType); + } + return base.GetFilterAttributes(useCache); + } + + public override bool IsDefined(Type attributeType, bool inherit) + { + return ControllerType.IsDefined(attributeType, inherit); + } + + private ActionDescriptor[] LazilyFetchCanonicalActionsCollection() + { + return DescriptorUtil.LazilyFetchOrCreateDescriptors<MethodInfo, ActionDescriptor>( + ref _canonicalActionsCache /* cacheLocation */, + GetAllActionMethodsFromSelector /* initializer */, + methodInfo => ReflectedActionDescriptor.TryCreateDescriptor(methodInfo, methodInfo.Name, this) /* converter */); + } + } +} diff --git a/src/System.Web.Mvc/ReflectedParameterBindingInfo.cs b/src/System.Web.Mvc/ReflectedParameterBindingInfo.cs new file mode 100644 index 00000000..27254f2d --- /dev/null +++ b/src/System.Web.Mvc/ReflectedParameterBindingInfo.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Globalization; +using System.Reflection; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + internal class ReflectedParameterBindingInfo : ParameterBindingInfo + { + private readonly ParameterInfo _parameterInfo; + private ICollection<string> _exclude = new string[0]; + private ICollection<string> _include = new string[0]; + private string _prefix; + + public ReflectedParameterBindingInfo(ParameterInfo parameterInfo) + { + _parameterInfo = parameterInfo; + ReadSettingsFromBindAttribute(); + } + + public override IModelBinder Binder + { + get + { + IModelBinder binder = ModelBinders.GetBinderFromAttributes(_parameterInfo, + () => String.Format(CultureInfo.CurrentCulture, MvcResources.ReflectedParameterBindingInfo_MultipleConverterAttributes, + _parameterInfo.Name, _parameterInfo.Member)); + + return binder; + } + } + + public override ICollection<string> Exclude + { + get { return _exclude; } + } + + public override ICollection<string> Include + { + get { return _include; } + } + + public override string Prefix + { + get { return _prefix; } + } + + private void ReadSettingsFromBindAttribute() + { + BindAttribute attr = (BindAttribute)Attribute.GetCustomAttribute(_parameterInfo, typeof(BindAttribute)); + if (attr == null) + { + return; + } + + _exclude = new ReadOnlyCollection<string>(AuthorizeAttribute.SplitString(attr.Exclude)); + _include = new ReadOnlyCollection<string>(AuthorizeAttribute.SplitString(attr.Include)); + _prefix = attr.Prefix; + } + } +} diff --git a/src/System.Web.Mvc/ReflectedParameterDescriptor.cs b/src/System.Web.Mvc/ReflectedParameterDescriptor.cs new file mode 100644 index 00000000..28f653df --- /dev/null +++ b/src/System.Web.Mvc/ReflectedParameterDescriptor.cs @@ -0,0 +1,79 @@ +using System.Reflection; + +namespace System.Web.Mvc +{ + public class ReflectedParameterDescriptor : ParameterDescriptor + { + private readonly ActionDescriptor _actionDescriptor; + private readonly ReflectedParameterBindingInfo _bindingInfo; + + public ReflectedParameterDescriptor(ParameterInfo parameterInfo, ActionDescriptor actionDescriptor) + { + if (parameterInfo == null) + { + throw new ArgumentNullException("parameterInfo"); + } + if (actionDescriptor == null) + { + throw new ArgumentNullException("actionDescriptor"); + } + + ParameterInfo = parameterInfo; + _actionDescriptor = actionDescriptor; + _bindingInfo = new ReflectedParameterBindingInfo(parameterInfo); + } + + public override ActionDescriptor ActionDescriptor + { + get { return _actionDescriptor; } + } + + public override ParameterBindingInfo BindingInfo + { + get { return _bindingInfo; } + } + + public override object DefaultValue + { + get + { + object value; + if (ParameterInfoUtil.TryGetDefaultValue(ParameterInfo, out value)) + { + return value; + } + else + { + return base.DefaultValue; + } + } + } + + public ParameterInfo ParameterInfo { get; private set; } + + public override string ParameterName + { + get { return ParameterInfo.Name; } + } + + public override Type ParameterType + { + get { return ParameterInfo.ParameterType; } + } + + public override object[] GetCustomAttributes(bool inherit) + { + return ParameterInfo.GetCustomAttributes(inherit); + } + + public override object[] GetCustomAttributes(Type attributeType, bool inherit) + { + return ParameterInfo.GetCustomAttributes(attributeType, inherit); + } + + public override bool IsDefined(Type attributeType, bool inherit) + { + return ParameterInfo.IsDefined(attributeType, inherit); + } + } +} diff --git a/src/System.Web.Mvc/RegularExpressionAttributeAdapter.cs b/src/System.Web.Mvc/RegularExpressionAttributeAdapter.cs new file mode 100644 index 00000000..18617497 --- /dev/null +++ b/src/System.Web.Mvc/RegularExpressionAttributeAdapter.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace System.Web.Mvc +{ + public class RegularExpressionAttributeAdapter : DataAnnotationsModelValidator<RegularExpressionAttribute> + { + public RegularExpressionAttributeAdapter(ModelMetadata metadata, ControllerContext context, RegularExpressionAttribute attribute) + : base(metadata, context, attribute) + { + } + + public override IEnumerable<ModelClientValidationRule> GetClientValidationRules() + { + return new[] { new ModelClientValidationRegexRule(ErrorMessage, Attribute.Pattern) }; + } + } +} diff --git a/src/System.Web.Mvc/RemoteAttribute.cs b/src/System.Web.Mvc/RemoteAttribute.cs new file mode 100644 index 00000000..68230a46 --- /dev/null +++ b/src/System.Web.Mvc/RemoteAttribute.cs @@ -0,0 +1,139 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Web.Mvc.Properties; +using System.Web.Routing; + +namespace System.Web.Mvc +{ + [AttributeUsage(AttributeTargets.Property)] + [SuppressMessage("Microsoft.Design", "CA1019:DefineAccessorsForAttributeArguments", Justification = "The constructor parameters are used to feed RouteData, which is public.")] + [SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "This attribute is designed to be a base class for other attributes.")] + public class RemoteAttribute : ValidationAttribute, IClientValidatable + { + private string _additionalFields; + private string[] _additonalFieldsSplit = new string[0]; + + protected RemoteAttribute() + : base(MvcResources.RemoteAttribute_RemoteValidationFailed) + { + RouteData = new RouteValueDictionary(); + } + + public RemoteAttribute(string routeName) + : this() + { + if (String.IsNullOrWhiteSpace(routeName)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "routeName"); + } + + RouteName = routeName; + } + + public RemoteAttribute(string action, string controller) + : + this(action, controller, null /* areaName */) + { + } + + public RemoteAttribute(string action, string controller, string areaName) + : this() + { + if (String.IsNullOrWhiteSpace(action)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "action"); + } + if (String.IsNullOrWhiteSpace(controller)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "controller"); + } + + RouteData["controller"] = controller; + RouteData["action"] = action; + + if (!String.IsNullOrWhiteSpace(areaName)) + { + RouteData["area"] = areaName; + } + } + + public string HttpMethod { get; set; } + + public string AdditionalFields + { + get { return _additionalFields ?? String.Empty; } + set + { + _additionalFields = value; + _additonalFieldsSplit = AuthorizeAttribute.SplitString(value); + } + } + + protected RouteValueDictionary RouteData { get; private set; } + + protected string RouteName { get; set; } + + protected virtual RouteCollection Routes + { + get { return RouteTable.Routes; } + } + + public string FormatAdditionalFieldsForClientValidation(string property) + { + if (String.IsNullOrEmpty(property)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "property"); + } + + string delimitedAdditionalFields = FormatPropertyForClientValidation(property); + + foreach (string field in _additonalFieldsSplit) + { + delimitedAdditionalFields += "," + FormatPropertyForClientValidation(field); + } + + return delimitedAdditionalFields; + } + + public static string FormatPropertyForClientValidation(string property) + { + if (String.IsNullOrEmpty(property)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "property"); + } + return "*." + property; + } + + [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings", Justification = "The value is a not a regular URL since it may contain ~/ ASP.NET-specific characters")] + protected virtual string GetUrl(ControllerContext controllerContext) + { + var pathData = Routes.GetVirtualPathForArea(controllerContext.RequestContext, + RouteName, + RouteData); + + if (pathData == null) + { + throw new InvalidOperationException(MvcResources.RemoteAttribute_NoUrlFound); + } + + return pathData.VirtualPath; + } + + public override string FormatErrorMessage(string name) + { + return String.Format(CultureInfo.CurrentCulture, ErrorMessageString, name); + } + + public override bool IsValid(object value) + { + return true; + } + + public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context) + { + yield return new ModelClientValidationRemoteRule(FormatErrorMessage(metadata.GetDisplayName()), GetUrl(context), HttpMethod, FormatAdditionalFieldsForClientValidation(metadata.PropertyName)); + } + } +} diff --git a/src/System.Web.Mvc/RequireHttpsAttribute.cs b/src/System.Web.Mvc/RequireHttpsAttribute.cs new file mode 100644 index 00000000..f39b7865 --- /dev/null +++ b/src/System.Web.Mvc/RequireHttpsAttribute.cs @@ -0,0 +1,38 @@ +using System.Diagnostics.CodeAnalysis; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + [SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "Unsealed because type contains virtual extensibility points.")] + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)] + public class RequireHttpsAttribute : FilterAttribute, IAuthorizationFilter + { + public virtual void OnAuthorization(AuthorizationContext filterContext) + { + if (filterContext == null) + { + throw new ArgumentNullException("filterContext"); + } + + if (!filterContext.HttpContext.Request.IsSecureConnection) + { + HandleNonHttpsRequest(filterContext); + } + } + + protected virtual void HandleNonHttpsRequest(AuthorizationContext filterContext) + { + // only redirect for GET requests, otherwise the browser might not propagate the verb and request + // body correctly. + + if (!String.Equals(filterContext.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException(MvcResources.RequireHttpsAttribute_MustUseSsl); + } + + // redirect to HTTPS version of page + string url = "https://" + filterContext.HttpContext.Request.Url.Host + filterContext.HttpContext.Request.RawUrl; + filterContext.Result = new RedirectResult(url); + } + } +} diff --git a/src/System.Web.Mvc/RequiredAttributeAdapter.cs b/src/System.Web.Mvc/RequiredAttributeAdapter.cs new file mode 100644 index 00000000..bfdce5fc --- /dev/null +++ b/src/System.Web.Mvc/RequiredAttributeAdapter.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace System.Web.Mvc +{ + public class RequiredAttributeAdapter : DataAnnotationsModelValidator<RequiredAttribute> + { + public RequiredAttributeAdapter(ModelMetadata metadata, ControllerContext context, RequiredAttribute attribute) + : base(metadata, context, attribute) + { + } + + public override IEnumerable<ModelClientValidationRule> GetClientValidationRules() + { + return new[] { new ModelClientValidationRequiredRule(ErrorMessage) }; + } + } +} diff --git a/src/System.Web.Mvc/ResultExecutedContext.cs b/src/System.Web.Mvc/ResultExecutedContext.cs new file mode 100644 index 00000000..c5e70127 --- /dev/null +++ b/src/System.Web.Mvc/ResultExecutedContext.cs @@ -0,0 +1,34 @@ +using System.Diagnostics.CodeAnalysis; + +namespace System.Web.Mvc +{ + public class ResultExecutedContext : ControllerContext + { + // parameterless constructor used for mocking + public ResultExecutedContext() + { + } + + [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "The virtual property setters are only to support mocking frameworks, in which case this constructor shouldn't be called anyway.")] + public ResultExecutedContext(ControllerContext controllerContext, ActionResult result, bool canceled, Exception exception) + : base(controllerContext) + { + if (result == null) + { + throw new ArgumentNullException("result"); + } + + Result = result; + Canceled = canceled; + Exception = exception; + } + + public virtual bool Canceled { get; set; } + + public virtual Exception Exception { get; set; } + + public bool ExceptionHandled { get; set; } + + public virtual ActionResult Result { get; set; } + } +} diff --git a/src/System.Web.Mvc/ResultExecutingContext.cs b/src/System.Web.Mvc/ResultExecutingContext.cs new file mode 100644 index 00000000..8d1910db --- /dev/null +++ b/src/System.Web.Mvc/ResultExecutingContext.cs @@ -0,0 +1,28 @@ +using System.Diagnostics.CodeAnalysis; + +namespace System.Web.Mvc +{ + public class ResultExecutingContext : ControllerContext + { + // parameterless constructor used for mocking + public ResultExecutingContext() + { + } + + [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "The virtual property setters are only to support mocking frameworks, in which case this constructor shouldn't be called anyway.")] + public ResultExecutingContext(ControllerContext controllerContext, ActionResult result) + : base(controllerContext) + { + if (result == null) + { + throw new ArgumentNullException("result"); + } + + Result = result; + } + + public bool Cancel { get; set; } + + public virtual ActionResult Result { get; set; } + } +} diff --git a/src/System.Web.Mvc/RouteCollectionExtensions.cs b/src/System.Web.Mvc/RouteCollectionExtensions.cs new file mode 100644 index 00000000..95626bbc --- /dev/null +++ b/src/System.Web.Mvc/RouteCollectionExtensions.cs @@ -0,0 +1,193 @@ +using System.Diagnostics.CodeAnalysis; +using System.Web.Routing; + +namespace System.Web.Mvc +{ + public static class RouteCollectionExtensions + { + // This method returns a new RouteCollection containing only routes that matched a particular area. + // The Boolean out parameter is just a flag specifying whether any registered routes were area-aware. + private static RouteCollection FilterRouteCollectionByArea(RouteCollection routes, string areaName, out bool usingAreas) + { + if (areaName == null) + { + areaName = String.Empty; + } + + usingAreas = false; + RouteCollection filteredRoutes = new RouteCollection(); + + using (routes.GetReadLock()) + { + foreach (RouteBase route in routes) + { + string thisAreaName = AreaHelpers.GetAreaName(route) ?? String.Empty; + usingAreas |= (thisAreaName.Length > 0); + if (String.Equals(thisAreaName, areaName, StringComparison.OrdinalIgnoreCase)) + { + filteredRoutes.Add(route); + } + } + } + + // if areas are not in use, the filtered route collection might be incorrect + return (usingAreas) ? filteredRoutes : routes; + } + + public static VirtualPathData GetVirtualPathForArea(this RouteCollection routes, RequestContext requestContext, RouteValueDictionary values) + { + return GetVirtualPathForArea(routes, requestContext, null /* name */, values); + } + + public static VirtualPathData GetVirtualPathForArea(this RouteCollection routes, RequestContext requestContext, string name, RouteValueDictionary values) + { + bool usingAreas; // don't care about this value + return GetVirtualPathForArea(routes, requestContext, name, values, out usingAreas); + } + + internal static VirtualPathData GetVirtualPathForArea(this RouteCollection routes, RequestContext requestContext, string name, RouteValueDictionary values, out bool usingAreas) + { + if (routes == null) + { + throw new ArgumentNullException("routes"); + } + + if (!String.IsNullOrEmpty(name)) + { + // the route name is a stronger qualifier than the area name, so just pipe it through + usingAreas = false; + return routes.GetVirtualPath(requestContext, name, values); + } + + string targetArea = null; + if (values != null) + { + object targetAreaRawValue; + if (values.TryGetValue("area", out targetAreaRawValue)) + { + targetArea = targetAreaRawValue as string; + } + else + { + // set target area to current area + if (requestContext != null) + { + targetArea = AreaHelpers.GetAreaName(requestContext.RouteData); + } + } + } + + // need to apply a correction to the RVD if areas are in use + RouteValueDictionary correctedValues = values; + RouteCollection filteredRoutes = FilterRouteCollectionByArea(routes, targetArea, out usingAreas); + if (usingAreas) + { + correctedValues = new RouteValueDictionary(values); + correctedValues.Remove("area"); + } + + VirtualPathData vpd = filteredRoutes.GetVirtualPath(requestContext, correctedValues); + return vpd; + } + + [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "This is not a regular URL as it may contain special routing characters.")] + public static void IgnoreRoute(this RouteCollection routes, string url) + { + IgnoreRoute(routes, url, null /* constraints */); + } + + [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "This is not a regular URL as it may contain special routing characters.")] + public static void IgnoreRoute(this RouteCollection routes, string url, object constraints) + { + if (routes == null) + { + throw new ArgumentNullException("routes"); + } + if (url == null) + { + throw new ArgumentNullException("url"); + } + + IgnoreRouteInternal route = new IgnoreRouteInternal(url) + { + Constraints = new RouteValueDictionary(constraints) + }; + + routes.Add(route); + } + + [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "2#", Justification = "This is not a regular URL as it may contain special routing characters.")] + public static Route MapRoute(this RouteCollection routes, string name, string url) + { + return MapRoute(routes, name, url, null /* defaults */, (object)null /* constraints */); + } + + [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "2#", Justification = "This is not a regular URL as it may contain special routing characters.")] + public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults) + { + return MapRoute(routes, name, url, defaults, (object)null /* constraints */); + } + + [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "2#", Justification = "This is not a regular URL as it may contain special routing characters.")] + public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints) + { + return MapRoute(routes, name, url, defaults, constraints, null /* namespaces */); + } + + [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "2#", Justification = "This is not a regular URL as it may contain special routing characters.")] + public static Route MapRoute(this RouteCollection routes, string name, string url, string[] namespaces) + { + return MapRoute(routes, name, url, null /* defaults */, null /* constraints */, namespaces); + } + + [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "2#", Justification = "This is not a regular URL as it may contain special routing characters.")] + public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, string[] namespaces) + { + return MapRoute(routes, name, url, defaults, null /* constraints */, namespaces); + } + + [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "2#", Justification = "This is not a regular URL as it may contain special routing characters.")] + public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces) + { + if (routes == null) + { + throw new ArgumentNullException("routes"); + } + if (url == null) + { + throw new ArgumentNullException("url"); + } + + Route route = new Route(url, new MvcRouteHandler()) + { + Defaults = new RouteValueDictionary(defaults), + Constraints = new RouteValueDictionary(constraints), + DataTokens = new RouteValueDictionary() + }; + + if ((namespaces != null) && (namespaces.Length > 0)) + { + route.DataTokens["Namespaces"] = namespaces; + } + + routes.Add(name, route); + + return route; + } + + private sealed class IgnoreRouteInternal : Route + { + public IgnoreRouteInternal(string url) + : base(url, new StopRoutingHandler()) + { + } + + public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary routeValues) + { + // Never match during route generation. This avoids the scenario where an IgnoreRoute with + // fairly relaxed constraints ends up eagerly matching all generated URLs. + return null; + } + } + } +} diff --git a/src/System.Web.Mvc/RouteDataValueProvider.cs b/src/System.Web.Mvc/RouteDataValueProvider.cs new file mode 100644 index 00000000..f31ec75a --- /dev/null +++ b/src/System.Web.Mvc/RouteDataValueProvider.cs @@ -0,0 +1,14 @@ +using System.Globalization; + +namespace System.Web.Mvc +{ + public sealed class RouteDataValueProvider : DictionaryValueProvider<object> + { + // RouteData should use the invariant culture since it's part of the URL, and the URL should be + // interpreted in a uniform fashion regardless of the origin of a particular request. + public RouteDataValueProvider(ControllerContext controllerContext) + : base(controllerContext.RouteData.Values, CultureInfo.InvariantCulture) + { + } + } +} diff --git a/src/System.Web.Mvc/RouteDataValueProviderFactory.cs b/src/System.Web.Mvc/RouteDataValueProviderFactory.cs new file mode 100644 index 00000000..b2054158 --- /dev/null +++ b/src/System.Web.Mvc/RouteDataValueProviderFactory.cs @@ -0,0 +1,15 @@ +namespace System.Web.Mvc +{ + public sealed class RouteDataValueProviderFactory : ValueProviderFactory + { + public override IValueProvider GetValueProvider(ControllerContext controllerContext) + { + if (controllerContext == null) + { + throw new ArgumentNullException("controllerContext"); + } + + return new RouteDataValueProvider(controllerContext); + } + } +} diff --git a/src/System.Web.Mvc/RouteValuesHelpers.cs b/src/System.Web.Mvc/RouteValuesHelpers.cs new file mode 100644 index 00000000..4e4b6904 --- /dev/null +++ b/src/System.Web.Mvc/RouteValuesHelpers.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Web.Routing; + +namespace System.Web.Mvc +{ + internal static class RouteValuesHelpers + { + public static RouteValueDictionary GetRouteValues(RouteValueDictionary routeValues) + { + return (routeValues != null) ? new RouteValueDictionary(routeValues) : new RouteValueDictionary(); + } + + public static RouteValueDictionary MergeRouteValues(string actionName, string controllerName, RouteValueDictionary implicitRouteValues, RouteValueDictionary routeValues, bool includeImplicitMvcValues) + { + // Create a new dictionary containing implicit and auto-generated values + RouteValueDictionary mergedRouteValues = new RouteValueDictionary(); + + if (includeImplicitMvcValues) + { + // We only include MVC-specific values like 'controller' and 'action' if we are generating an action link. + // If we are generating a route link [as to MapRoute("Foo", "any/url", new { controller = ... })], including + // the current controller name will cause the route match to fail if the current controller is not the same + // as the destination controller. + + object implicitValue; + if (implicitRouteValues != null && implicitRouteValues.TryGetValue("action", out implicitValue)) + { + mergedRouteValues["action"] = implicitValue; + } + + if (implicitRouteValues != null && implicitRouteValues.TryGetValue("controller", out implicitValue)) + { + mergedRouteValues["controller"] = implicitValue; + } + } + + // Merge values from the user's dictionary/object + if (routeValues != null) + { + foreach (KeyValuePair<string, object> routeElement in GetRouteValues(routeValues)) + { + mergedRouteValues[routeElement.Key] = routeElement.Value; + } + } + + // Merge explicit parameters when not null + if (actionName != null) + { + mergedRouteValues["action"] = actionName; + } + + if (controllerName != null) + { + mergedRouteValues["controller"] = controllerName; + } + + return mergedRouteValues; + } + } +} diff --git a/src/System.Web.Mvc/SecurityUtil.cs b/src/System.Web.Mvc/SecurityUtil.cs new file mode 100644 index 00000000..becded41 --- /dev/null +++ b/src/System.Web.Mvc/SecurityUtil.cs @@ -0,0 +1,79 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Security; + +namespace System.Web.Mvc +{ + internal static class SecurityUtil + { + private static Action<Action> _callInAppTrustThunk; + + // !! IMPORTANT !! + // Do not try to optimize this method or perform any extra caching; doing so could lead to MVC not operating + // correctly until the AppDomain is restarted. + [SuppressMessage("Microsoft.Security", "CA2107:ReviewDenyAndPermitOnlyUsage", + Justification = "This is essentially the same logic as Page.ProcessRequest.")] + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", + Justification = "If an exception is thrown, assume we're running in same trust level as the application itself, so we don't need to do anything special.")] + private static Action<Action> GetCallInAppTrustThunk() + { + // do we need to create the thunk? + if (_callInAppTrustThunk == null) + { + try + { + if (!typeof(SecurityUtil).Assembly.IsFullyTrusted /* bin-deployed */ + || AppDomain.CurrentDomain.IsHomogenous /* .NET 4 CAS model */) + { + // we're already running in the application's trust level, so nothing to do + _callInAppTrustThunk = f => f(); + } + else + { + // legacy CAS model - need to lower own permission level to be compatible with legacy systems + // This is essentially the same logic as Page.ProcessRequest(HttpContext) + NamedPermissionSet namedPermissionSet = (NamedPermissionSet)typeof(HttpRuntime).GetProperty("NamedPermissionSet", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static).GetValue(null, null); + bool disableProcessRequestInApplicationTrust = (bool)typeof(HttpRuntime).GetProperty("DisableProcessRequestInApplicationTrust", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static).GetValue(null, null); + if (namedPermissionSet != null && !disableProcessRequestInApplicationTrust) + { + _callInAppTrustThunk = f => + { + // lower permissions + namedPermissionSet.PermitOnly(); + f(); + }; + } + else + { + // application's trust level is FullTrust, so nothing to do + _callInAppTrustThunk = f => f(); + } + } + } + catch + { + // MVC assembly is already running in application trust, so swallow exceptions + } + } + + // if there was an error, just process transparently + return _callInAppTrustThunk ?? (Action<Action>)(f => f()); + } + + public static TResult ProcessInApplicationTrust<TResult>(Func<TResult> func) + { + TResult result = default(TResult); + ProcessInApplicationTrust(delegate + { + result = func(); + }); + return result; + } + + public static void ProcessInApplicationTrust(Action action) + { + Action<Action> executor = GetCallInAppTrustThunk(); + executor(action); + } + } +} diff --git a/src/System.Web.Mvc/SelectList.cs b/src/System.Web.Mvc/SelectList.cs new file mode 100644 index 00000000..2c605386 --- /dev/null +++ b/src/System.Web.Mvc/SelectList.cs @@ -0,0 +1,37 @@ +using System.Collections; +using System.Diagnostics.CodeAnalysis; + +namespace System.Web.Mvc +{ + [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", Justification = "This is a shipped API")] + public class SelectList : MultiSelectList + { + public SelectList(IEnumerable items) + : this(items, null /* selectedValue */) + { + } + + public SelectList(IEnumerable items, object selectedValue) + : this(items, null /* dataValuefield */, null /* dataTextField */, selectedValue) + { + } + + public SelectList(IEnumerable items, string dataValueField, string dataTextField) + : this(items, dataValueField, dataTextField, null /* selectedValue */) + { + } + + public SelectList(IEnumerable items, string dataValueField, string dataTextField, object selectedValue) + : base(items, dataValueField, dataTextField, ToEnumerable(selectedValue)) + { + SelectedValue = selectedValue; + } + + public object SelectedValue { get; private set; } + + private static IEnumerable ToEnumerable(object selectedValue) + { + return (selectedValue != null) ? new object[] { selectedValue } : null; + } + } +} diff --git a/src/System.Web.Mvc/SelectListItem.cs b/src/System.Web.Mvc/SelectListItem.cs new file mode 100644 index 00000000..650540d9 --- /dev/null +++ b/src/System.Web.Mvc/SelectListItem.cs @@ -0,0 +1,11 @@ +namespace System.Web.Mvc +{ + public class SelectListItem + { + public bool Selected { get; set; } + + public string Text { get; set; } + + public string Value { get; set; } + } +} diff --git a/src/System.Web.Mvc/SessionStateAttribute.cs b/src/System.Web.Mvc/SessionStateAttribute.cs new file mode 100644 index 00000000..c675bf60 --- /dev/null +++ b/src/System.Web.Mvc/SessionStateAttribute.cs @@ -0,0 +1,15 @@ +using System.Web.SessionState; + +namespace System.Web.Mvc +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + public sealed class SessionStateAttribute : Attribute + { + public SessionStateAttribute(SessionStateBehavior behavior) + { + Behavior = behavior; + } + + public SessionStateBehavior Behavior { get; private set; } + } +} diff --git a/src/System.Web.Mvc/SessionStateTempDataProvider.cs b/src/System.Web.Mvc/SessionStateTempDataProvider.cs new file mode 100644 index 00000000..01f83f08 --- /dev/null +++ b/src/System.Web.Mvc/SessionStateTempDataProvider.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + public class SessionStateTempDataProvider : ITempDataProvider + { + internal const string TempDataSessionStateKey = "__ControllerTempData"; + + public virtual IDictionary<string, object> LoadTempData(ControllerContext controllerContext) + { + HttpSessionStateBase session = controllerContext.HttpContext.Session; + + if (session != null) + { + Dictionary<string, object> tempDataDictionary = session[TempDataSessionStateKey] as Dictionary<string, object>; + + if (tempDataDictionary != null) + { + // If we got it from Session, remove it so that no other request gets it + session.Remove(TempDataSessionStateKey); + return tempDataDictionary; + } + } + + return new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); + } + + public virtual void SaveTempData(ControllerContext controllerContext, IDictionary<string, object> values) + { + if (controllerContext == null) + { + throw new ArgumentNullException("controllerContext"); + } + + HttpSessionStateBase session = controllerContext.HttpContext.Session; + bool isDirty = (values != null && values.Count > 0); + + if (session == null) + { + if (isDirty) + { + throw new InvalidOperationException(MvcResources.SessionStateTempDataProvider_SessionStateDisabled); + } + } + else + { + if (isDirty) + { + session[TempDataSessionStateKey] = values; + } + else + { + // Since the default implementation of Remove() (from SessionStateItemCollection) dirties the + // collection, we shouldn't call it unless we really do need to remove the existing key. + if (session[TempDataSessionStateKey] != null) + { + session.Remove(TempDataSessionStateKey); + } + } + } + } + } +} diff --git a/src/System.Web.Mvc/SingleServiceResolver.cs b/src/System.Web.Mvc/SingleServiceResolver.cs new file mode 100644 index 00000000..b1f5c344 --- /dev/null +++ b/src/System.Web.Mvc/SingleServiceResolver.cs @@ -0,0 +1,65 @@ +using System.Globalization; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + internal class SingleServiceResolver<TService> : IResolver<TService> + where TService : class + { + private TService _currentValueFromResolver; + private Func<TService> _currentValueThunk; + private TService _defaultValue; + private Func<IDependencyResolver> _resolverThunk; + private string _callerMethodName; + + public SingleServiceResolver(Func<TService> currentValueThunk, TService defaultValue, string callerMethodName) + { + if (currentValueThunk == null) + { + throw new ArgumentNullException("currentValueThunk"); + } + if (defaultValue == null) + { + throw new ArgumentNullException("defaultValue"); + } + + _resolverThunk = () => DependencyResolver.Current; + _currentValueThunk = currentValueThunk; + _defaultValue = defaultValue; + _callerMethodName = callerMethodName; + } + + internal SingleServiceResolver(Func<TService> staticAccessor, TService defaultValue, IDependencyResolver resolver, string callerMethodName) + : this(staticAccessor, defaultValue, callerMethodName) + { + if (resolver != null) + { + _resolverThunk = () => resolver; + } + } + + public TService Current + { + get + { + if (_resolverThunk != null) + { + lock (_currentValueThunk) + { + if (_resolverThunk != null) + { + _currentValueFromResolver = _resolverThunk().GetService<TService>(); + _resolverThunk = null; + + if (_currentValueFromResolver != null && _currentValueThunk() != null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, MvcResources.SingleServiceResolver_CannotRegisterTwoInstances, typeof(TService).Name.ToString(), _callerMethodName)); + } + } + } + } + return _currentValueFromResolver ?? _currentValueThunk() ?? _defaultValue; + } + } + } +} diff --git a/src/System.Web.Mvc/StringLengthAttributeAdapter.cs b/src/System.Web.Mvc/StringLengthAttributeAdapter.cs new file mode 100644 index 00000000..ad301038 --- /dev/null +++ b/src/System.Web.Mvc/StringLengthAttributeAdapter.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace System.Web.Mvc +{ + public class StringLengthAttributeAdapter : DataAnnotationsModelValidator<StringLengthAttribute> + { + public StringLengthAttributeAdapter(ModelMetadata metadata, ControllerContext context, StringLengthAttribute attribute) + : base(metadata, context, attribute) + { + } + + public override IEnumerable<ModelClientValidationRule> GetClientValidationRules() + { + return new[] { new ModelClientValidationStringLengthRule(ErrorMessage, Attribute.MinimumLength, Attribute.MaximumLength) }; + } + } +} diff --git a/src/System.Web.Mvc/System.Web.Mvc.csproj b/src/System.Web.Mvc/System.Web.Mvc.csproj new file mode 100644 index 00000000..646cce61 --- /dev/null +++ b/src/System.Web.Mvc/System.Web.Mvc.csproj @@ -0,0 +1,475 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" /> + <PropertyGroup> + <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> + <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> + <CodeAnalysis Condition=" '$(CodeAnalysis)' == '' ">false</CodeAnalysis> + <ProductVersion>9.0.30729</ProductVersion> + <SchemaVersion>2.0</SchemaVersion> + <ProjectGuid>{3D3FFD8A-624D-4E9B-954B-E1C105507975}</ProjectGuid> + <OutputType>Library</OutputType> + <AppDesignerFolder>Properties</AppDesignerFolder> + <RootNamespace>System.Web.Mvc</RootNamespace> + <AssemblyName>System.Web.Mvc</AssemblyName> + <FileAlignment>512</FileAlignment> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + <BaseAddress>1609891840</BaseAddress> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> + <DebugSymbols>true</DebugSymbols> + <DebugType>full</DebugType> + <Optimize>false</Optimize> + <OutputPath>..\..\bin\Debug\</OutputPath> + <DefineConstants>TRACE;DEBUG;ASPNETMVC</DefineConstants> + <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet> + <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile> + <NoWarn>1591</NoWarn> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> + <DebugType>pdbonly</DebugType> + <Optimize>true</Optimize> + <OutputPath>..\..\bin\Release\</OutputPath> + <DefineConstants>TRACE;ASPNETMVC</DefineConstants> + <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet> + <RunCodeAnalysis>$(CodeAnalysis)</RunCodeAnalysis> + <DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile> + <NoWarn>1591</NoWarn> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'CodeCoverage|AnyCPU'"> + <DebugSymbols>true</DebugSymbols> + <OutputPath>..\..\bin\CodeCoverage\</OutputPath> + <DefineConstants>TRACE;DEBUG;CODE_COVERAGE;ASPNETMVC</DefineConstants> + <DebugType>full</DebugType> + <CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet> + </PropertyGroup> + <ItemGroup> + <Reference Include="Microsoft.Web.Infrastructure, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <HintPath>..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll</HintPath> + </Reference> + <Reference Include="System" /> + <Reference Include="System.ComponentModel.DataAnnotations" /> + <Reference Include="System.configuration" /> + <Reference Include="System.Core" /> + <Reference Include="System.Data.Entity" /> + <Reference Include="System.Data.Linq" /> + <Reference Include="System.Runtime.Caching" /> + <Reference Include="System.Web" /> + <Reference Include="System.Data" /> + <Reference Include="System.Web.Abstractions" /> + <Reference Include="System.Web.Extensions" /> + <Reference Include="System.Web.Routing" /> + <Reference Include="System.Xml" /> + </ItemGroup> + <ItemGroup> + <Compile Include="..\CommonAssemblyInfo.cs"> + <Link>Properties\CommonAssemblyInfo.cs</Link> + </Compile> + <Compile Include="..\System.Web.Http.Common\TaskHelpers.cs"> + <Link>Async\TaskHelpers.cs</Link> + </Compile> + <Compile Include="..\System.Web.Http.Common\TaskHelpersExtensions.cs"> + <Link>Async\TaskHelpersExtensions.cs</Link> + </Compile> + <Compile Include="..\TransparentCommonAssemblyInfo.cs"> + <Link>Properties\TransparentCommonAssemblyInfo.cs</Link> + </Compile> + <Compile Include="AdditionalMetaDataAttribute.cs" /> + <Compile Include="AllowAnonymousAttribute.cs" /> + <Compile Include="ActionDescriptorHelper.cs" /> + <Compile Include="Async\TaskAsyncActionDescriptor.cs" /> + <Compile Include="Async\TaskWrapperAsyncResult.cs" /> + <Compile Include="BuildManagerCompiledView.cs" /> + <Compile Include="BuildManagerViewEngine.cs" /> + <Compile Include="CachedAssociatedMetadataProvider`1.cs" /> + <Compile Include="CachedDataAnnotationsMetadataAttributes.cs" /> + <Compile Include="CachedDataAnnotationsModelMetadata.cs" /> + <Compile Include="CachedDataAnnotationsModelMetadataProvider.cs" /> + <Compile Include="CachedModelMetadata`1.cs" /> + <Compile Include="CancellationTokenModelBinder.cs" /> + <Compile Include="CompareAttribute.cs" /> + <Compile Include="ChildActionValueProvider.cs" /> + <Compile Include="ChildActionValueProviderFactory.cs" /> + <Compile Include="IEnumerableValueProvider.cs" /> + <Compile Include="DataTypeUtil.cs" /> + <Compile Include="Html\DisplayNameExtensions.cs" /> + <Compile Include="Html\NameExtensions.cs" /> + <Compile Include="Html\ValueExtensions.cs" /> + <Compile Include="Razor\MvcCSharpRazorCodeGenerator.cs" /> + <Compile Include="Razor\SetModelTypeCodeGenerator.cs" /> + <Compile Include="ReflectedAttributeCache.cs" /> + <Compile Include="SessionStateAttribute.cs" /> + <Compile Include="AllowHtmlAttribute.cs" /> + <Compile Include="UnvalidatedRequestValuesAccessor.cs" /> + <Compile Include="UnvalidatedRequestValuesWrapper.cs" /> + <Compile Include="IUnvalidatedRequestValues.cs" /> + <Compile Include="IUnvalidatedValueProvider.cs" /> + <Compile Include="DependencyResolverExtensions.cs" /> + <Compile Include="ExpressionUtil\BinaryExpressionFingerprint.cs" /> + <Compile Include="ExpressionUtil\CachedExpressionCompiler.cs" /> + <Compile Include="ExpressionUtil\ConditionalExpressionFingerprint.cs" /> + <Compile Include="ExpressionUtil\ConstantExpressionFingerprint.cs" /> + <Compile Include="ExpressionUtil\DefaultExpressionFingerprint.cs" /> + <Compile Include="ExpressionUtil\ExpressionFingerprint.cs" /> + <Compile Include="ExpressionUtil\ExpressionFingerprintChain.cs" /> + <Compile Include="ExpressionUtil\FingerprintingExpressionVisitor.cs" /> + <Compile Include="ExpressionUtil\HashCodeCombiner.cs" /> + <Compile Include="ExpressionUtil\Hoisted`2.cs" /> + <Compile Include="ExpressionUtil\HoistingExpressionVisitor.cs" /> + <Compile Include="ExpressionUtil\IndexExpressionFingerprint.cs" /> + <Compile Include="ExpressionUtil\LambdaExpressionFingerprint.cs" /> + <Compile Include="ExpressionUtil\MemberExpressionFingerprint.cs" /> + <Compile Include="ExpressionUtil\MethodCallExpressionFingerprint.cs" /> + <Compile Include="ExpressionUtil\ParameterExpressionFingerprint.cs" /> + <Compile Include="ExpressionUtil\TypeBinaryExpressionFingerprint.cs" /> + <Compile Include="ExpressionUtil\UnaryExpressionFingerprint.cs" /> + <Compile Include="IControllerActivator.cs" /> + <Compile Include="IModelBinderProvider.cs" /> + <Compile Include="IUniquelyIdentifiable.cs" /> + <Compile Include="IViewStartPageChild.cs" /> + <Compile Include="IResolver.cs" /> + <Compile Include="ControllerInstanceFilterProvider.cs" /> + <Compile Include="RazorView.cs" /> + <Compile Include="RazorViewEngine.cs" /> + <Compile Include="DynamicViewDataDictionary.cs" /> + <Compile Include="Filter.cs" /> + <Compile Include="FilterAttributeFilterProvider.cs" /> + <Compile Include="FilterProviderCollection.cs" /> + <Compile Include="FilterProviders.cs" /> + <Compile Include="FilterScope.cs" /> + <Compile Include="GlobalFilterCollection.cs" /> + <Compile Include="GlobalFilters.cs" /> + <Compile Include="IFilterProvider.cs" /> + <Compile Include="IMvcFilter.cs" /> + <Compile Include="IViewPageActivator.cs" /> + <Compile Include="ModelBinderProviderCollection.cs" /> + <Compile Include="ModelBinderProviders.cs" /> + <Compile Include="MultiServiceResolver.cs" /> + <Compile Include="Razor\MvcCSharpRazorCodeParser.cs" /> + <Compile Include="MvcFilter.cs" /> + <Compile Include="Razor\MvcVBRazorCodeParser.cs" /> + <Compile Include="Razor\MvcWebPageRazorHost.cs" /> + <Compile Include="MvcWebRazorHostFactory.cs" /> + <Compile Include="PreApplicationStartCode.cs" /> + <Compile Include="RemoteAttribute.cs" /> + <Compile Include="SecurityUtil.cs" /> + <Compile Include="SingleServiceResolver.cs" /> + <Compile Include="Razor\StartPageLookupDelegate.cs" /> + <Compile Include="TagBuilderExtensions.cs" /> + <Compile Include="UrlRewriterHelper.cs" /> + <Compile Include="ViewStartPage.cs" /> + <Compile Include="WebViewPage.cs" /> + <Compile Include="WebViewPage`1.cs" /> + <Compile Include="HttpNotFoundResult.cs" /> + <Compile Include="HttpStatusCodeResult.cs" /> + <Compile Include="IMvcControlBuilder.cs" /> + <Compile Include="AssociatedMetadataProvider.cs" /> + <Compile Include="ActionExecutedContext.cs" /> + <Compile Include="ActionExecutingContext.cs" /> + <Compile Include="ClientDataTypeModelValidatorProvider.cs" /> + <Compile Include="AssociatedValidatorProvider.cs" /> + <Compile Include="Async\ActionDescriptorCreator.cs" /> + <Compile Include="Async\AsyncActionDescriptor.cs" /> + <Compile Include="Async\AsyncActionMethodSelector.cs" /> + <Compile Include="Async\AsyncControllerActionInvoker.cs" /> + <Compile Include="Async\SynchronousOperationException.cs" /> + <Compile Include="Async\AsyncManager.cs" /> + <Compile Include="AsyncTimeoutAttribute.cs" /> + <Compile Include="Async\BeginInvokeDelegate.cs" /> + <Compile Include="Async\AsyncResultWrapper.cs" /> + <Compile Include="Async\AsyncVoid.cs" /> + <Compile Include="AsyncController.cs" /> + <Compile Include="Async\AsyncUtil.cs" /> + <Compile Include="Async\IAsyncController.cs" /> + <Compile Include="Async\IAsyncActionInvoker.cs" /> + <Compile Include="Async\IAsyncManagerContainer.cs" /> + <Compile Include="IClientValidatable.cs" /> + <Compile Include="IMetadataAware.cs" /> + <Compile Include="IDependencyResolver.cs" /> + <Compile Include="JsonValueProviderFactory.cs" /> + <Compile Include="DependencyResolver.cs" /> + <Compile Include="UrlParameter.cs" /> + <Compile Include="FormValueProvider.cs" /> + <Compile Include="FormValueProviderFactory.cs" /> + <Compile Include="HttpFileCollectionValueProvider.cs" /> + <Compile Include="HttpFileCollectionValueProviderFactory.cs" /> + <Compile Include="QueryStringValueProvider.cs" /> + <Compile Include="QueryStringValueProviderFactory.cs" /> + <Compile Include="RangeAttributeAdapter.cs" /> + <Compile Include="RegularExpressionAttributeAdapter.cs" /> + <Compile Include="RequiredAttributeAdapter.cs" /> + <Compile Include="RouteDataValueProvider.cs" /> + <Compile Include="RouteDataValueProviderFactory.cs" /> + <Compile Include="StringLengthAttributeAdapter.cs" /> + <Compile Include="TypeCacheUtil.cs" /> + <Compile Include="TypeCacheSerializer.cs" /> + <Compile Include="Html\DisplayTextExtensions.cs" /> + <Compile Include="NoAsyncTimeoutAttribute.cs" /> + <Compile Include="Async\OperationCounter.cs" /> + <Compile Include="Async\ReflectedAsyncActionDescriptor.cs" /> + <Compile Include="Async\ReflectedAsyncControllerDescriptor.cs" /> + <Compile Include="Async\Trigger.cs" /> + <Compile Include="Async\TriggerListener.cs" /> + <Compile Include="Async\SimpleAsyncResult.cs" /> + <Compile Include="Async\EndInvokeDelegate.cs" /> + <Compile Include="Async\EndInvokeDelegate`1.cs" /> + <Compile Include="Async\SynchronizationContextUtil.cs" /> + <Compile Include="AuthorizationContext.cs" /> + <Compile Include="ByteArrayModelBinder.cs" /> + <Compile Include="ControllerContext.cs" /> + <Compile Include="Html\ChildActionExtensions.cs" /> + <Compile Include="ParameterInfoUtil.cs" /> + <Compile Include="HttpHandlerUtil.cs" /> + <Compile Include="ChildActionOnlyAttribute.cs" /> + <Compile Include="TypeDescriptorHelper.cs" /> + <Compile Include="ValidatableObjectAdapter.cs" /> + <Compile Include="ValueProviderFactories.cs" /> + <Compile Include="ValueProviderFactory.cs" /> + <Compile Include="ValueProviderFactoryCollection.cs" /> + <Compile Include="ValueProviderCollection.cs" /> + <Compile Include="DictionaryValueProvider`1.cs" /> + <Compile Include="NameValueCollectionValueProvider.cs" /> + <Compile Include="ValueProviderUtil.cs" /> + <Compile Include="IValueProvider.cs" /> + <Compile Include="DataErrorInfoModelValidatorProvider.cs" /> + <Compile Include="ModelValidatorProviderCollection.cs" /> + <Compile Include="DataAnnotationsModelMetadata.cs" /> + <Compile Include="HiddenInputAttribute.cs" /> + <Compile Include="HttpGetAttribute.cs" /> + <Compile Include="HttpPutAttribute.cs" /> + <Compile Include="HttpDeleteAttribute.cs" /> + <Compile Include="MvcHtmlString.cs" /> + <Compile Include="DataAnnotationsModelValidator.cs" /> + <Compile Include="DataAnnotationsModelValidatorProvider.cs" /> + <Compile Include="DataAnnotationsModelValidator`1.cs" /> + <Compile Include="EmptyModelValidatorProvider.cs" /> + <Compile Include="ExpressionHelper.cs" /> + <Compile Include="FieldValidationMetadata.cs" /> + <Compile Include="FormContext.cs" /> + <Compile Include="JsonRequestBehavior.cs" /> + <Compile Include="ModelValidationResult.cs" /> + <Compile Include="ModelValidator.cs" /> + <Compile Include="ModelValidatorProvider.cs" /> + <Compile Include="ModelValidatorProviders.cs" /> + <Compile Include="RequireHttpsAttribute.cs" /> + <Compile Include="HttpRequestExtensions.cs" /> + <Compile Include="DataAnnotationsModelMetadataProvider.cs" /> + <Compile Include="EmptyModelMetadataProvider.cs" /> + <Compile Include="ModelMetadata.cs" /> + <Compile Include="ModelMetadataProvider.cs" /> + <Compile Include="ModelMetadataProviders.cs" /> + <Compile Include="AreaHelpers.cs" /> + <Compile Include="AreaRegistration.cs" /> + <Compile Include="AreaRegistrationContext.cs" /> + <Compile Include="Error.cs" /> + <Compile Include="IRouteWithArea.cs" /> + <Compile Include="Async\SingleEntryGate.cs" /> + <Compile Include="Html\PartialExtensions.cs" /> + <Compile Include="LinqBinaryModelBinder.cs" /> + <Compile Include="TryGetValueDelegate.cs" /> + <Compile Include="ViewDataInfo.cs" /> + <Compile Include="Html\DefaultDisplayTemplates.cs" /> + <Compile Include="Html\DefaultEditorTemplates.cs" /> + <Compile Include="Html\DisplayExtensions.cs" /> + <Compile Include="Html\EditorExtensions.cs" /> + <Compile Include="Html\LabelExtensions.cs" /> + <Compile Include="Html\TemplateHelpers.cs" /> + <Compile Include="HttpPostAttribute.cs" /> + <Compile Include="PathHelpers.cs" /> + <Compile Include="ExceptionContext.cs" /> + <Compile Include="ResultExecutedContext.cs" /> + <Compile Include="ResultExecutingContext.cs" /> + <Compile Include="TemplateInfo.cs" /> + <Compile Include="ValidateAntiForgeryTokenAttribute.cs" /> + <Compile Include="JavaScriptResult.cs" /> + <Compile Include="ActionDescriptor.cs" /> + <Compile Include="ActionMethodDispatcher.cs" /> + <Compile Include="ActionMethodSelector.cs" /> + <Compile Include="ActionMethodSelectorAttribute.cs" /> + <Compile Include="ActionNameSelectorAttribute.cs" /> + <Compile Include="AuthorizeAttribute.cs" /> + <Compile Include="Ajax\AjaxOptions.cs" /> + <Compile Include="Ajax\AjaxExtensions.cs" /> + <Compile Include="ActionMethodDispatcherCache.cs" /> + <Compile Include="BindAttribute.cs" /> + <Compile Include="ControllerBase.cs" /> + <Compile Include="ActionNameAttribute.cs" /> + <Compile Include="AcceptVerbsAttribute.cs" /> + <Compile Include="AjaxHelper`1.cs" /> + <Compile Include="HtmlHelper`1.cs" /> + <Compile Include="DictionaryHelpers.cs" /> + <Compile Include="AjaxRequestExtensions.cs" /> + <Compile Include="ModelBinderDictionary.cs" /> + <Compile Include="ValueProviderDictionary.cs" /> + <Compile Include="ViewContext.cs" /> + <Compile Include="ViewMasterPageControlBuilder.cs" /> + <Compile Include="ViewTemplateUserControl.cs"> + <SubType>ASPXCodeBehind</SubType> + </Compile> + <Compile Include="ViewTemplateUserControl`1.cs"> + <SubType>ASPXCodeBehind</SubType> + </Compile> + <Compile Include="ViewType.cs" /> + <Compile Include="ViewTypeControlBuilder.cs" /> + <Compile Include="ViewUserControlControlBuilder.cs" /> + <Compile Include="ViewPageControlBuilder.cs" /> + <Compile Include="ViewTypeParserFilter.cs" /> + <Compile Include="DefaultViewLocationCache.cs" /> + <Compile Include="FormCollection.cs" /> + <Compile Include="HttpPostedFileBaseModelBinder.cs" /> + <Compile Include="NullViewLocationCache.cs" /> + <Compile Include="ValidateInputAttribute.cs" /> + <Compile Include="FileContentResult.cs" /> + <Compile Include="FilePathResult.cs" /> + <Compile Include="FileResult.cs" /> + <Compile Include="FileStreamResult.cs" /> + <Compile Include="InputType.cs" /> + <Compile Include="ControllerDescriptorCache.cs" /> + <Compile Include="ReflectedParameterBindingInfo.cs" /> + <Compile Include="ParameterBindingInfo.cs" /> + <Compile Include="ReaderWriterCache`2.cs" /> + <Compile Include="DescriptorUtil.cs" /> + <Compile Include="ReflectedControllerDescriptor.cs" /> + <Compile Include="ControllerDescriptor.cs" /> + <Compile Include="ActionSelector.cs" /> + <Compile Include="ReflectedActionDescriptor.cs" /> + <Compile Include="Html\MvcForm.cs" /> + <Compile Include="HttpVerbs.cs" /> + <Compile Include="DefaultModelBinder.cs" /> + <Compile Include="ModelBindingContext.cs" /> + <Compile Include="ParameterDescriptor.cs" /> + <Compile Include="RouteValuesHelpers.cs" /> + <Compile Include="SelectListItem.cs" /> + <Compile Include="ReflectedParameterDescriptor.cs" /> + <Compile Include="ValueProviderResult.cs" /> + <Compile Include="CustomModelBinderAttribute.cs" /> + <Compile Include="FormMethod.cs" /> + <Compile Include="Html\FormExtensions.cs" /> + <Compile Include="Html\InputExtensions.cs" /> + <Compile Include="Html\RenderPartialExtensions.cs" /> + <Compile Include="Html\SelectExtensions.cs" /> + <Compile Include="Html\TextAreaExtensions.cs" /> + <Compile Include="Html\ValidationExtensions.cs" /> + <Compile Include="IModelBinder.cs" /> + <Compile Include="Html\LinkExtensions.cs" /> + <Compile Include="ModelBinderAttribute.cs" /> + <Compile Include="ModelBinders.cs" /> + <Compile Include="ModelStateDictionary.cs" /> + <Compile Include="ModelState.cs" /> + <Compile Include="ModelErrorCollection.cs" /> + <Compile Include="ModelError.cs" /> + <Compile Include="Ajax\InsertionMode.cs" /> + <Compile Include="HandleErrorAttribute.cs" /> + <Compile Include="HandleErrorInfo.cs" /> + <Compile Include="HttpUnauthorizedResult.cs" /> + <Compile Include="IActionInvoker.cs" /> + <Compile Include="IView.cs" /> + <Compile Include="IViewLocationCache.cs" /> + <Compile Include="MvcHttpHandler.cs" /> + <Compile Include="PartialViewResult.cs" /> + <Compile Include="SessionStateTempDataProvider.cs" /> + <Compile Include="ITempDataProvider.cs" /> + <Compile Include="OutputCacheAttribute.cs" /> + <Compile Include="FilterInfo.cs" /> + <Compile Include="GlobalSuppressions.cs" /> + <Compile Include="ActionFilterAttribute.cs" /> + <Compile Include="ActionResult.cs" /> + <Compile Include="AjaxHelper.cs" /> + <Compile Include="BuildManagerWrapper.cs" /> + <Compile Include="Controller.cs" /> + <Compile Include="ControllerActionInvoker.cs" /> + <Compile Include="ControllerBuilder.cs" /> + <Compile Include="ControllerTypeCache.cs" /> + <Compile Include="ContentResult.cs" /> + <Compile Include="FilterAttribute.cs" /> + <Compile Include="IResultFilter.cs" /> + <Compile Include="IExceptionFilter.cs" /> + <Compile Include="IAuthorizationFilter.cs" /> + <Compile Include="JsonResult.cs" /> + <Compile Include="NameValueCollectionExtensions.cs" /> + <Compile Include="ViewDataDictionary`1.cs" /> + <Compile Include="EmptyResult.cs" /> + <Compile Include="MultiSelectList.cs" /> + <Compile Include="RedirectResult.cs" /> + <Compile Include="RedirectToRouteResult.cs" /> + <Compile Include="DefaultControllerFactory.cs" /> + <Compile Include="HtmlHelper.cs" /> + <Compile Include="IActionFilter.cs" /> + <Compile Include="IBuildManager.cs" /> + <Compile Include="IController.cs" /> + <Compile Include="IControllerFactory.cs" /> + <Compile Include="IViewDataContainer.cs" /> + <Compile Include="IViewEngine.cs" /> + <Compile Include="MvcHandler.cs" /> + <Compile Include="MvcRouteHandler.cs" /> + <Compile Include="NonActionAttribute.cs" /> + <Compile Include="RouteCollectionExtensions.cs" /> + <Compile Include="SelectList.cs" /> + <Compile Include="TempDataDictionary.cs" /> + <Compile Include="TypeHelpers.cs" /> + <Compile Include="UrlHelper.cs" /> + <Compile Include="ViewDataDictionary.cs" /> + <Compile Include="ViewEngineCollection.cs" /> + <Compile Include="ViewEngineResult.cs" /> + <Compile Include="ViewEngines.cs" /> + <Compile Include="ViewMasterPage.cs"> + <SubType>ASPXCodeBehind</SubType> + </Compile> + <Compile Include="ViewMasterPage`1.cs"> + <SubType>ASPXCodeBehind</SubType> + </Compile> + <Compile Include="ViewPage.cs"> + <SubType>ASPXCodeBehind</SubType> + </Compile> + <Compile Include="ViewPage`1.cs"> + <SubType>ASPXCodeBehind</SubType> + </Compile> + <Compile Include="ViewResult.cs" /> + <Compile Include="ViewResultBase.cs" /> + <Compile Include="ViewUserControl.cs"> + <SubType>ASPXCodeBehind</SubType> + </Compile> + <Compile Include="ViewUserControl`1.cs"> + <SubType>ASPXCodeBehind</SubType> + </Compile> + <Compile Include="VirtualPathProviderViewEngine.cs" /> + <Compile Include="WebFormView.cs" /> + <Compile Include="WebFormViewEngine.cs" /> + <Compile Include="Properties\AssemblyInfo.cs" /> + <Compile Include="Properties\MvcResources.Designer.cs"> + <AutoGen>True</AutoGen> + <DesignTime>True</DesignTime> + <DependentUpon>MvcResources.resx</DependentUpon> + </Compile> + </ItemGroup> + <ItemGroup> + <EmbeddedResource Include="Properties\MvcResources.resx"> + <Generator>ResXFileCodeGenerator</Generator> + <LastGenOutput>MvcResources.Designer.cs</LastGenOutput> + <SubType>Designer</SubType> + </EmbeddedResource> + </ItemGroup> + <ItemGroup> + <CodeAnalysisDictionary Include="..\CodeAnalysisDictionary.xml" /> + </ItemGroup> + <ItemGroup> + <ProjectReference Include="..\System.Web.Razor\System.Web.Razor.csproj"> + <Project>{8F18041B-9410-4C36-A9C5-067813DF5F31}</Project> + <Name>System.Web.Razor</Name> + </ProjectReference> + <ProjectReference Include="..\System.Web.WebPages.Razor\System.Web.WebPages.Razor.csproj"> + <Project>{0939B11A-FE4E-4BA1-8AD6-D97741EE314F}</Project> + <Name>System.Web.WebPages.Razor</Name> + </ProjectReference> + <ProjectReference Include="..\System.Web.WebPages\System.Web.WebPages.csproj"> + <Project>{76EFA9C5-8D7E-4FDF-B710-E20F8B6B00D2}</Project> + <Name>System.Web.WebPages</Name> + </ProjectReference> + </ItemGroup> + <ItemGroup> + <None Include="packages.config" /> + </ItemGroup> + <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> +</Project>
\ No newline at end of file diff --git a/src/System.Web.Mvc/TagBuilderExtensions.cs b/src/System.Web.Mvc/TagBuilderExtensions.cs new file mode 100644 index 00000000..cd458f59 --- /dev/null +++ b/src/System.Web.Mvc/TagBuilderExtensions.cs @@ -0,0 +1,13 @@ +using System.Diagnostics; + +namespace System.Web.Mvc +{ + internal static class TagBuilderExtensions + { + internal static MvcHtmlString ToMvcHtmlString(this TagBuilder tagBuilder, TagRenderMode renderMode) + { + Debug.Assert(tagBuilder != null); + return new MvcHtmlString(tagBuilder.ToString(renderMode)); + } + } +} diff --git a/src/System.Web.Mvc/TempDataDictionary.cs b/src/System.Web.Mvc/TempDataDictionary.cs new file mode 100644 index 00000000..76bb4b2c --- /dev/null +++ b/src/System.Web.Mvc/TempDataDictionary.cs @@ -0,0 +1,208 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace System.Web.Mvc +{ + public class TempDataDictionary : IDictionary<string, object> + { + internal const string TempDataSerializationKey = "__tempData"; + + private Dictionary<string, object> _data; + private HashSet<string> _initialKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase); + private HashSet<string> _retainedKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase); + + public TempDataDictionary() + { + _data = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); + } + + public int Count + { + get { return _data.Count; } + } + + public ICollection<string> Keys + { + get { return _data.Keys; } + } + + public ICollection<object> Values + { + get { return _data.Values; } + } + + bool ICollection<KeyValuePair<string, object>>.IsReadOnly + { + get { return ((ICollection<KeyValuePair<string, object>>)_data).IsReadOnly; } + } + + public object this[string key] + { + get + { + object value; + if (TryGetValue(key, out value)) + { + _initialKeys.Remove(key); + return value; + } + return null; + } + set + { + _data[key] = value; + _initialKeys.Add(key); + } + } + + public void Keep() + { + _retainedKeys.Clear(); + _retainedKeys.UnionWith(_data.Keys); + } + + public void Keep(string key) + { + _retainedKeys.Add(key); + } + + public void Load(ControllerContext controllerContext, ITempDataProvider tempDataProvider) + { + IDictionary<string, object> providerDictionary = tempDataProvider.LoadTempData(controllerContext); + _data = (providerDictionary != null) + ? new Dictionary<string, object>(providerDictionary, StringComparer.OrdinalIgnoreCase) + : new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); + _initialKeys = new HashSet<string>(_data.Keys, StringComparer.OrdinalIgnoreCase); + _retainedKeys.Clear(); + } + + public object Peek(string key) + { + object value; + _data.TryGetValue(key, out value); + return value; + } + + public void Save(ControllerContext controllerContext, ITempDataProvider tempDataProvider) + { + string[] keysToKeep = _initialKeys.Union(_retainedKeys, StringComparer.OrdinalIgnoreCase).ToArray(); + string[] keysToRemove = _data.Keys.Except(keysToKeep, StringComparer.OrdinalIgnoreCase).ToArray(); + foreach (string key in keysToRemove) + { + _data.Remove(key); + } + tempDataProvider.SaveTempData(controllerContext, _data); + } + + public void Add(string key, object value) + { + _data.Add(key, value); + _initialKeys.Add(key); + } + + public void Clear() + { + _data.Clear(); + _retainedKeys.Clear(); + _initialKeys.Clear(); + } + + public bool ContainsKey(string key) + { + return _data.ContainsKey(key); + } + + public bool ContainsValue(object value) + { + return _data.ContainsValue(value); + } + + public IEnumerator<KeyValuePair<string, object>> GetEnumerator() + { + return new TempDataDictionaryEnumerator(this); + } + + public bool Remove(string key) + { + _retainedKeys.Remove(key); + _initialKeys.Remove(key); + return _data.Remove(key); + } + + public bool TryGetValue(string key, out object value) + { + _initialKeys.Remove(key); + return _data.TryGetValue(key, out value); + } + + void ICollection<KeyValuePair<string, object>>.CopyTo(KeyValuePair<string, object>[] array, int index) + { + ((ICollection<KeyValuePair<string, object>>)_data).CopyTo(array, index); + } + + void ICollection<KeyValuePair<string, object>>.Add(KeyValuePair<string, object> keyValuePair) + { + _initialKeys.Add(keyValuePair.Key); + ((ICollection<KeyValuePair<string, object>>)_data).Add(keyValuePair); + } + + bool ICollection<KeyValuePair<string, object>>.Contains(KeyValuePair<string, object> keyValuePair) + { + return ((ICollection<KeyValuePair<string, object>>)_data).Contains(keyValuePair); + } + + bool ICollection<KeyValuePair<string, object>>.Remove(KeyValuePair<string, object> keyValuePair) + { + _initialKeys.Remove(keyValuePair.Key); + return ((ICollection<KeyValuePair<string, object>>)_data).Remove(keyValuePair); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return new TempDataDictionaryEnumerator(this); + } + + private sealed class TempDataDictionaryEnumerator : IEnumerator<KeyValuePair<string, object>> + { + private IEnumerator<KeyValuePair<string, object>> _enumerator; + private TempDataDictionary _tempData; + + public TempDataDictionaryEnumerator(TempDataDictionary tempData) + { + _tempData = tempData; + _enumerator = _tempData._data.GetEnumerator(); + } + + public KeyValuePair<string, object> Current + { + get + { + KeyValuePair<string, object> kvp = _enumerator.Current; + _tempData._initialKeys.Remove(kvp.Key); + return kvp; + } + } + + object IEnumerator.Current + { + get { return Current; } + } + + public bool MoveNext() + { + return _enumerator.MoveNext(); + } + + public void Reset() + { + _enumerator.Reset(); + } + + void IDisposable.Dispose() + { + _enumerator.Dispose(); + } + } + } +} diff --git a/src/System.Web.Mvc/TemplateInfo.cs b/src/System.Web.Mvc/TemplateInfo.cs new file mode 100644 index 00000000..0f05f1ca --- /dev/null +++ b/src/System.Web.Mvc/TemplateInfo.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; + +namespace System.Web.Mvc +{ + public class TemplateInfo + { + private string _htmlFieldPrefix; + private object _formattedModelValue; + private HashSet<object> _visitedObjects; + + public object FormattedModelValue + { + get { return _formattedModelValue ?? String.Empty; } + set { _formattedModelValue = value; } + } + + public string HtmlFieldPrefix + { + get { return _htmlFieldPrefix ?? String.Empty; } + set { _htmlFieldPrefix = value; } + } + + public int TemplateDepth + { + get { return VisitedObjects.Count; } + } + + // DDB #224750 - Keep a collection of visited objects to prevent infinite recursion + internal HashSet<object> VisitedObjects + { + get + { + if (_visitedObjects == null) + { + _visitedObjects = new HashSet<object>(); + } + return _visitedObjects; + } + set { _visitedObjects = value; } + } + + public string GetFullHtmlFieldId(string partialFieldName) + { + return HtmlHelper.GenerateIdFromName(GetFullHtmlFieldName(partialFieldName)); + } + + public string GetFullHtmlFieldName(string partialFieldName) + { + // This uses "combine and trim" because either or both of these values might be empty + return (HtmlFieldPrefix + "." + (partialFieldName ?? String.Empty)).Trim('.'); + } + + public bool Visited(ModelMetadata metadata) + { + return VisitedObjects.Contains(metadata.Model ?? metadata.ModelType); + } + } +} diff --git a/src/System.Web.Mvc/TryGetValueDelegate.cs b/src/System.Web.Mvc/TryGetValueDelegate.cs new file mode 100644 index 00000000..2092b9e0 --- /dev/null +++ b/src/System.Web.Mvc/TryGetValueDelegate.cs @@ -0,0 +1,4 @@ +namespace System.Web.Mvc +{ + internal delegate bool TryGetValueDelegate(object dictionary, string key, out object value); +} diff --git a/src/System.Web.Mvc/TypeCacheSerializer.cs b/src/System.Web.Mvc/TypeCacheSerializer.cs new file mode 100644 index 00000000..9067f0d8 --- /dev/null +++ b/src/System.Web.Mvc/TypeCacheSerializer.cs @@ -0,0 +1,122 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Web.Mvc.Properties; +using System.Xml; + +namespace System.Web.Mvc +{ + // Processes files with this format: + // + // <typeCache lastModified=... mvcVersionId=...> + // <assembly name=...> + // <module versionId=...> + // <type>...</type> + // </module> + // </assembly> + // </typeCache> + // + // This is used to store caches of files between AppDomain resets, leading to improved cold boot time + // and more efficient use of memory. + + internal sealed class TypeCacheSerializer + { + private static readonly Guid _mvcVersionId = typeof(TypeCacheSerializer).Module.ModuleVersionId; + + // used for unit testing + + private DateTime CurrentDate + { + get { return CurrentDateOverride ?? DateTime.Now; } + } + + internal DateTime? CurrentDateOverride { get; set; } + + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This is an instance method for consistency with the SerializeTypes() method.")] + public List<Type> DeserializeTypes(TextReader input) + { + XmlDocument doc = new XmlDocument(); + doc.Load(input); + XmlElement rootElement = doc.DocumentElement; + + Guid readMvcVersionId = new Guid(rootElement.Attributes["mvcVersionId"].Value); + if (readMvcVersionId != _mvcVersionId) + { + // The cache is outdated because the cache file was produced by a different version + // of MVC. + return null; + } + + List<Type> deserializedTypes = new List<Type>(); + foreach (XmlNode assemblyNode in rootElement.ChildNodes) + { + string assemblyName = assemblyNode.Attributes["name"].Value; + Assembly assembly = Assembly.Load(assemblyName); + + foreach (XmlNode moduleNode in assemblyNode.ChildNodes) + { + Guid moduleVersionId = new Guid(moduleNode.Attributes["versionId"].Value); + + foreach (XmlNode typeNode in moduleNode.ChildNodes) + { + string typeName = typeNode.InnerText; + Type type = assembly.GetType(typeName); + if (type == null || type.Module.ModuleVersionId != moduleVersionId) + { + // The cache is outdated because we couldn't find a previously recorded + // type or the type's containing module was modified. + return null; + } + else + { + deserializedTypes.Add(type); + } + } + } + } + + return deserializedTypes; + } + + public void SerializeTypes(IEnumerable<Type> types, TextWriter output) + { + var groupedByAssembly = from type in types + group type by type.Module + into groupedByModule + group groupedByModule by groupedByModule.Key.Assembly; + + XmlDocument doc = new XmlDocument(); + doc.AppendChild(doc.CreateComment(MvcResources.TypeCache_DoNotModify)); + + XmlElement typeCacheElement = doc.CreateElement("typeCache"); + doc.AppendChild(typeCacheElement); + typeCacheElement.SetAttribute("lastModified", CurrentDate.ToString()); + typeCacheElement.SetAttribute("mvcVersionId", _mvcVersionId.ToString()); + + foreach (var assemblyGroup in groupedByAssembly) + { + XmlElement assemblyElement = doc.CreateElement("assembly"); + typeCacheElement.AppendChild(assemblyElement); + assemblyElement.SetAttribute("name", assemblyGroup.Key.FullName); + + foreach (var moduleGroup in assemblyGroup) + { + XmlElement moduleElement = doc.CreateElement("module"); + assemblyElement.AppendChild(moduleElement); + moduleElement.SetAttribute("versionId", moduleGroup.Key.ModuleVersionId.ToString()); + + foreach (Type type in moduleGroup) + { + XmlElement typeElement = doc.CreateElement("type"); + moduleElement.AppendChild(typeElement); + typeElement.AppendChild(doc.CreateTextNode(type.FullName)); + } + } + } + + doc.Save(output); + } + } +} diff --git a/src/System.Web.Mvc/TypeCacheUtil.cs b/src/System.Web.Mvc/TypeCacheUtil.cs new file mode 100644 index 00000000..2f9fed55 --- /dev/null +++ b/src/System.Web.Mvc/TypeCacheUtil.cs @@ -0,0 +1,104 @@ +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace System.Web.Mvc +{ + internal static class TypeCacheUtil + { + private static IEnumerable<Type> FilterTypesInAssemblies(IBuildManager buildManager, Predicate<Type> predicate) + { + // Go through all assemblies referenced by the application and search for types matching a predicate + IEnumerable<Type> typesSoFar = Type.EmptyTypes; + + ICollection assemblies = buildManager.GetReferencedAssemblies(); + foreach (Assembly assembly in assemblies) + { + Type[] typesInAsm; + try + { + typesInAsm = assembly.GetTypes(); + } + catch (ReflectionTypeLoadException ex) + { + typesInAsm = ex.Types; + } + typesSoFar = typesSoFar.Concat(typesInAsm); + } + return typesSoFar.Where(type => TypeIsPublicClass(type) && predicate(type)); + } + + public static List<Type> GetFilteredTypesFromAssemblies(string cacheName, Predicate<Type> predicate, IBuildManager buildManager) + { + TypeCacheSerializer serializer = new TypeCacheSerializer(); + + // first, try reading from the cache on disk + List<Type> matchingTypes = ReadTypesFromCache(cacheName, predicate, buildManager, serializer); + if (matchingTypes != null) + { + return matchingTypes; + } + + // if reading from the cache failed, enumerate over every assembly looking for a matching type + matchingTypes = FilterTypesInAssemblies(buildManager, predicate).ToList(); + + // finally, save the cache back to disk + SaveTypesToCache(cacheName, matchingTypes, buildManager, serializer); + + return matchingTypes; + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Cache failures are not fatal, and the code should continue executing normally.")] + internal static List<Type> ReadTypesFromCache(string cacheName, Predicate<Type> predicate, IBuildManager buildManager, TypeCacheSerializer serializer) + { + try + { + Stream stream = buildManager.ReadCachedFile(cacheName); + if (stream != null) + { + using (StreamReader reader = new StreamReader(stream)) + { + List<Type> deserializedTypes = serializer.DeserializeTypes(reader); + if (deserializedTypes != null && deserializedTypes.All(type => TypeIsPublicClass(type) && predicate(type))) + { + // If all read types still match the predicate, success! + return deserializedTypes; + } + } + } + } + catch + { + } + + return null; + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Cache failures are not fatal, and the code should continue executing normally.")] + internal static void SaveTypesToCache(string cacheName, IList<Type> matchingTypes, IBuildManager buildManager, TypeCacheSerializer serializer) + { + try + { + Stream stream = buildManager.CreateCachedFile(cacheName); + if (stream != null) + { + using (StreamWriter writer = new StreamWriter(stream)) + { + serializer.SerializeTypes(matchingTypes, writer); + } + } + } + catch + { + } + } + + private static bool TypeIsPublicClass(Type type) + { + return (type != null && type.IsPublic && type.IsClass && !type.IsAbstract); + } + } +} diff --git a/src/System.Web.Mvc/TypeDescriptorHelper.cs b/src/System.Web.Mvc/TypeDescriptorHelper.cs new file mode 100644 index 00000000..48d9bcfb --- /dev/null +++ b/src/System.Web.Mvc/TypeDescriptorHelper.cs @@ -0,0 +1,13 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace System.Web.Mvc +{ + internal static class TypeDescriptorHelper + { + public static ICustomTypeDescriptor Get(Type type) + { + return new AssociatedMetadataTypeTypeDescriptionProvider(type).GetTypeDescriptor(type); + } + } +} diff --git a/src/System.Web.Mvc/TypeHelpers.cs b/src/System.Web.Mvc/TypeHelpers.cs new file mode 100644 index 00000000..5309b4df --- /dev/null +++ b/src/System.Web.Mvc/TypeHelpers.cs @@ -0,0 +1,146 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading; + +namespace System.Web.Mvc +{ + internal static class TypeHelpers + { + private static readonly Dictionary<Type, TryGetValueDelegate> _tryGetValueDelegateCache = new Dictionary<Type, TryGetValueDelegate>(); + private static readonly ReaderWriterLockSlim _tryGetValueDelegateCacheLock = new ReaderWriterLockSlim(); + + private static readonly MethodInfo _strongTryGetValueImplInfo = typeof(TypeHelpers).GetMethod("StrongTryGetValueImpl", BindingFlags.NonPublic | BindingFlags.Static); + + public static readonly Assembly MsCorLibAssembly = typeof(string).Assembly; + public static readonly Assembly MvcAssembly = typeof(Controller).Assembly; + public static readonly Assembly SystemWebAssembly = typeof(HttpContext).Assembly; + + // method is used primarily for lighting up new .NET Framework features even if MVC targets the previous version + // thisParameter is the 'this' parameter if target method is instance method, should be null for static method + public static TDelegate CreateDelegate<TDelegate>(Assembly assembly, string typeName, string methodName, object thisParameter) where TDelegate : class + { + // ensure target type exists + Type targetType = assembly.GetType(typeName, false /* throwOnError */); + if (targetType == null) + { + return null; + } + + return CreateDelegate<TDelegate>(targetType, methodName, thisParameter); + } + + public static TDelegate CreateDelegate<TDelegate>(Type targetType, string methodName, object thisParameter) where TDelegate : class + { + // ensure target method exists + ParameterInfo[] delegateParameters = typeof(TDelegate).GetMethod("Invoke").GetParameters(); + Type[] argumentTypes = Array.ConvertAll(delegateParameters, pInfo => pInfo.ParameterType); + MethodInfo targetMethod = targetType.GetMethod(methodName, argumentTypes); + if (targetMethod == null) + { + return null; + } + + TDelegate d = Delegate.CreateDelegate(typeof(TDelegate), thisParameter, targetMethod, false /* throwOnBindFailure */) as TDelegate; + return d; + } + + public static TryGetValueDelegate CreateTryGetValueDelegate(Type targetType) + { + TryGetValueDelegate result; + + _tryGetValueDelegateCacheLock.EnterReadLock(); + try + { + if (_tryGetValueDelegateCache.TryGetValue(targetType, out result)) + { + return result; + } + } + finally + { + _tryGetValueDelegateCacheLock.ExitReadLock(); + } + + Type dictionaryType = ExtractGenericInterface(targetType, typeof(IDictionary<,>)); + + // just wrap a call to the underlying IDictionary<TKey, TValue>.TryGetValue() where string can be cast to TKey + if (dictionaryType != null) + { + Type[] typeArguments = dictionaryType.GetGenericArguments(); + Type keyType = typeArguments[0]; + Type returnType = typeArguments[1]; + + if (keyType.IsAssignableFrom(typeof(string))) + { + MethodInfo strongImplInfo = _strongTryGetValueImplInfo.MakeGenericMethod(keyType, returnType); + result = (TryGetValueDelegate)Delegate.CreateDelegate(typeof(TryGetValueDelegate), strongImplInfo); + } + } + + // wrap a call to the underlying IDictionary.Item() + if (result == null && typeof(IDictionary).IsAssignableFrom(targetType)) + { + result = TryGetValueFromNonGenericDictionary; + } + + _tryGetValueDelegateCacheLock.EnterWriteLock(); + try + { + _tryGetValueDelegateCache[targetType] = result; + } + finally + { + _tryGetValueDelegateCacheLock.ExitWriteLock(); + } + + return result; + } + + public static Type ExtractGenericInterface(Type queryType, Type interfaceType) + { + Func<Type, bool> matchesInterface = t => t.IsGenericType && t.GetGenericTypeDefinition() == interfaceType; + return (matchesInterface(queryType)) ? queryType : queryType.GetInterfaces().FirstOrDefault(matchesInterface); + } + + public static object GetDefaultValue(Type type) + { + return (TypeAllowsNullValue(type)) ? null : Activator.CreateInstance(type); + } + + public static bool IsCompatibleObject<T>(object value) + { + return (value is T || (value == null && TypeAllowsNullValue(typeof(T)))); + } + + public static bool IsNullableValueType(Type type) + { + return Nullable.GetUnderlyingType(type) != null; + } + + private static bool StrongTryGetValueImpl<TKey, TValue>(object dictionary, string key, out object value) + { + IDictionary<TKey, TValue> strongDict = (IDictionary<TKey, TValue>)dictionary; + + TValue strongValue; + bool retVal = strongDict.TryGetValue((TKey)(object)key, out strongValue); + value = strongValue; + return retVal; + } + + private static bool TryGetValueFromNonGenericDictionary(object dictionary, string key, out object value) + { + IDictionary weakDict = (IDictionary)dictionary; + + bool containsKey = weakDict.Contains(key); + value = (containsKey) ? weakDict[key] : null; + return containsKey; + } + + public static bool TypeAllowsNullValue(Type type) + { + return (!type.IsValueType || IsNullableValueType(type)); + } + } +} diff --git a/src/System.Web.Mvc/UnvalidatedRequestValuesAccessor.cs b/src/System.Web.Mvc/UnvalidatedRequestValuesAccessor.cs new file mode 100644 index 00000000..0c4f775e --- /dev/null +++ b/src/System.Web.Mvc/UnvalidatedRequestValuesAccessor.cs @@ -0,0 +1,4 @@ +namespace System.Web.Mvc +{ + internal delegate IUnvalidatedRequestValues UnvalidatedRequestValuesAccessor(ControllerContext controllerContext); +} diff --git a/src/System.Web.Mvc/UnvalidatedRequestValuesWrapper.cs b/src/System.Web.Mvc/UnvalidatedRequestValuesWrapper.cs new file mode 100644 index 00000000..260093a7 --- /dev/null +++ b/src/System.Web.Mvc/UnvalidatedRequestValuesWrapper.cs @@ -0,0 +1,32 @@ +using System.Collections.Specialized; +using System.Web.Helpers; + +namespace System.Web.Mvc +{ + // Concrete implementation for the IUnvalidatedRequestValues helper interface + + internal sealed class UnvalidatedRequestValuesWrapper : IUnvalidatedRequestValues + { + private readonly UnvalidatedRequestValues _unvalidatedValues; + + public UnvalidatedRequestValuesWrapper(UnvalidatedRequestValues unvalidatedValues) + { + _unvalidatedValues = unvalidatedValues; + } + + public NameValueCollection Form + { + get { return _unvalidatedValues.Form; } + } + + public NameValueCollection QueryString + { + get { return _unvalidatedValues.QueryString; } + } + + public string this[string key] + { + get { return _unvalidatedValues[key]; } + } + } +} diff --git a/src/System.Web.Mvc/UrlHelper.cs b/src/System.Web.Mvc/UrlHelper.cs new file mode 100644 index 00000000..804879f3 --- /dev/null +++ b/src/System.Web.Mvc/UrlHelper.cs @@ -0,0 +1,222 @@ +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Web.Mvc.Properties; +using System.Web.Routing; +using System.Web.WebPages; + +namespace System.Web.Mvc +{ + public class UrlHelper + { + public UrlHelper(RequestContext requestContext) + : this(requestContext, RouteTable.Routes) + { + } + + public UrlHelper(RequestContext requestContext, RouteCollection routeCollection) + { + if (requestContext == null) + { + throw new ArgumentNullException("requestContext"); + } + if (routeCollection == null) + { + throw new ArgumentNullException("routeCollection"); + } + RequestContext = requestContext; + RouteCollection = routeCollection; + } + + public RequestContext RequestContext { get; private set; } + + public RouteCollection RouteCollection { get; private set; } + + public string Action(string actionName) + { + return GenerateUrl(null /* routeName */, actionName, null, (RouteValueDictionary)null /* routeValues */); + } + + public string Action(string actionName, object routeValues) + { + return GenerateUrl(null /* routeName */, actionName, null /* controllerName */, new RouteValueDictionary(routeValues)); + } + + public string Action(string actionName, RouteValueDictionary routeValues) + { + return GenerateUrl(null /* routeName */, actionName, null /* controllerName */, routeValues); + } + + public string Action(string actionName, string controllerName) + { + return GenerateUrl(null /* routeName */, actionName, controllerName, (RouteValueDictionary)null /* routeValues */); + } + + public string Action(string actionName, string controllerName, object routeValues) + { + return GenerateUrl(null /* routeName */, actionName, controllerName, new RouteValueDictionary(routeValues)); + } + + public string Action(string actionName, string controllerName, RouteValueDictionary routeValues) + { + return GenerateUrl(null /* routeName */, actionName, controllerName, routeValues); + } + + public string Action(string actionName, string controllerName, object routeValues, string protocol) + { + return GenerateUrl(null /* routeName */, actionName, controllerName, protocol, null /* hostName */, null /* fragment */, new RouteValueDictionary(routeValues), RouteCollection, RequestContext, true /* includeImplicitMvcValues */); + } + + public string Action(string actionName, string controllerName, RouteValueDictionary routeValues, string protocol, string hostName) + { + return GenerateUrl(null /* routeName */, actionName, controllerName, protocol, hostName, null /* fragment */, routeValues, RouteCollection, RequestContext, true /* includeImplicitMvcValues */); + } + + public string Content(string contentPath) + { + return GenerateContentUrl(contentPath, RequestContext.HttpContext); + } + + [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings", Justification = "As the return value will used only for rendering, string return value is more appropriate.")] + public static string GenerateContentUrl(string contentPath, HttpContextBase httpContext) + { + if (String.IsNullOrEmpty(contentPath)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "contentPath"); + } + + if (httpContext == null) + { + throw new ArgumentNullException("httpContext"); + } + + if (contentPath[0] == '~') + { + return PathHelpers.GenerateClientUrl(httpContext, contentPath); + } + else + { + return contentPath; + } + } + + //REVIEW: Should we have an overload that takes Uri? + [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", Justification = "Needs to take same parameters as HttpUtility.UrlEncode()")] + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "For consistency, all helpers are instance methods.")] + public string Encode(string url) + { + return HttpUtility.UrlEncode(url); + } + + private string GenerateUrl(string routeName, string actionName, string controllerName, RouteValueDictionary routeValues) + { + return GenerateUrl(routeName, actionName, controllerName, routeValues, RouteCollection, RequestContext, true /* includeImplicitMvcValues */); + } + + [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings", Justification = "As the return value will used only for rendering, string return value is more appropriate.")] + public static string GenerateUrl(string routeName, string actionName, string controllerName, string protocol, string hostName, string fragment, RouteValueDictionary routeValues, RouteCollection routeCollection, RequestContext requestContext, bool includeImplicitMvcValues) + { + string url = GenerateUrl(routeName, actionName, controllerName, routeValues, routeCollection, requestContext, includeImplicitMvcValues); + + if (url != null) + { + if (!String.IsNullOrEmpty(fragment)) + { + url = url + "#" + fragment; + } + + if (!String.IsNullOrEmpty(protocol) || !String.IsNullOrEmpty(hostName)) + { + Uri requestUrl = requestContext.HttpContext.Request.Url; + protocol = (!String.IsNullOrEmpty(protocol)) ? protocol : Uri.UriSchemeHttp; + hostName = (!String.IsNullOrEmpty(hostName)) ? hostName : requestUrl.Host; + + string port = String.Empty; + string requestProtocol = requestUrl.Scheme; + + if (String.Equals(protocol, requestProtocol, StringComparison.OrdinalIgnoreCase)) + { + port = requestUrl.IsDefaultPort ? String.Empty : (":" + Convert.ToString(requestUrl.Port, CultureInfo.InvariantCulture)); + } + + url = protocol + Uri.SchemeDelimiter + hostName + port + url; + } + } + + return url; + } + + [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings", Justification = "As the return value will used only for rendering, string return value is more appropriate.")] + public static string GenerateUrl(string routeName, string actionName, string controllerName, RouteValueDictionary routeValues, RouteCollection routeCollection, RequestContext requestContext, bool includeImplicitMvcValues) + { + if (routeCollection == null) + { + throw new ArgumentNullException("routeCollection"); + } + + if (requestContext == null) + { + throw new ArgumentNullException("requestContext"); + } + + RouteValueDictionary mergedRouteValues = RouteValuesHelpers.MergeRouteValues(actionName, controllerName, requestContext.RouteData.Values, routeValues, includeImplicitMvcValues); + + VirtualPathData vpd = routeCollection.GetVirtualPathForArea(requestContext, routeName, mergedRouteValues); + if (vpd == null) + { + return null; + } + + string modifiedUrl = PathHelpers.GenerateClientUrl(requestContext.HttpContext, vpd.VirtualPath); + return modifiedUrl; + } + + [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "0#", Justification = "Response.Redirect() takes its URI as a string parameter.")] + public bool IsLocalUrl(string url) + { + // TODO this should call the System.Web.dll API once it gets added to the framework and MVC takes a dependency on it. + return RequestExtensions.IsUrlLocalToHost(RequestContext.HttpContext.Request, url); + } + + [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings", Justification = "As the return value will used only for rendering, string return value is more appropriate.")] + public string RouteUrl(object routeValues) + { + return RouteUrl(null /* routeName */, routeValues); + } + + [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings", Justification = "As the return value will used only for rendering, string return value is more appropriate.")] + public string RouteUrl(RouteValueDictionary routeValues) + { + return RouteUrl(null /* routeName */, routeValues); + } + + [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings", Justification = "As the return value will used only for rendering, string return value is more appropriate.")] + public string RouteUrl(string routeName) + { + return RouteUrl(routeName, (object)null /* routeValues */); + } + + [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings", Justification = "As the return value will used only for rendering, string return value is more appropriate.")] + public string RouteUrl(string routeName, object routeValues) + { + return RouteUrl(routeName, routeValues, null /* protocol */); + } + + [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings", Justification = "As the return value will used only for rendering, string return value is more appropriate.")] + public string RouteUrl(string routeName, RouteValueDictionary routeValues) + { + return RouteUrl(routeName, routeValues, null /* protocol */, null /* hostName */); + } + + [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings", Justification = "As the return value will used only for rendering, string return value is more appropriate.")] + public string RouteUrl(string routeName, object routeValues, string protocol) + { + return GenerateUrl(routeName, null /* actionName */, null /* controllerName */, protocol, null /* hostName */, null /* fragment */, new RouteValueDictionary(routeValues), RouteCollection, RequestContext, false /* includeImplicitMvcValues */); + } + + [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings", Justification = "As the return value will used only for rendering, string return value is more appropriate.")] + public string RouteUrl(string routeName, RouteValueDictionary routeValues, string protocol, string hostName) + { + return GenerateUrl(routeName, null /* actionName */, null /* controllerName */, protocol, hostName, null /* fragment */, routeValues, RouteCollection, RequestContext, false /* includeImplicitMvcValues */); + } + } +} diff --git a/src/System.Web.Mvc/UrlParameter.cs b/src/System.Web.Mvc/UrlParameter.cs new file mode 100644 index 00000000..0b567667 --- /dev/null +++ b/src/System.Web.Mvc/UrlParameter.cs @@ -0,0 +1,20 @@ +using System.Diagnostics.CodeAnalysis; + +namespace System.Web.Mvc +{ + public sealed class UrlParameter + { + [SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification = "This type is immutable.")] + public static readonly UrlParameter Optional = new UrlParameter(); + + // singleton constructor + private UrlParameter() + { + } + + public override string ToString() + { + return String.Empty; + } + } +} diff --git a/src/System.Web.Mvc/UrlRewriterHelper.cs b/src/System.Web.Mvc/UrlRewriterHelper.cs new file mode 100644 index 00000000..265f7202 --- /dev/null +++ b/src/System.Web.Mvc/UrlRewriterHelper.cs @@ -0,0 +1,45 @@ +using System.Collections.Specialized; + +namespace System.Web.Mvc +{ + internal class UrlRewriterHelper + { + private const string UrlWasRewrittenServerVar = "IIS_WasUrlRewritten"; + private const string UrlRewriterEnabledServerVar = "IIS_UrlRewriteModule"; + + private object _lockObject = new object(); + private bool _urlRewriterIsTurnedOnValue; + private bool _urlRewriterIsTurnedOnCalculated = false; + + private static bool WasThisRequestRewritten(HttpContextBase httpContext) + { + NameValueCollection serverVars = httpContext.Request.ServerVariables; + bool requestWasRewritten = (serverVars != null && serverVars[UrlWasRewrittenServerVar] != null); + return requestWasRewritten; + } + + private bool IsUrlRewriterTurnedOn(HttpContextBase httpContext) + { + // Need to do double-check locking because a single instance of this class is shared in the entire app domain (see PathHelpers) + if (!_urlRewriterIsTurnedOnCalculated) + { + lock (_lockObject) + { + if (!_urlRewriterIsTurnedOnCalculated) + { + NameValueCollection serverVars = httpContext.Request.ServerVariables; + bool urlRewriterIsEnabled = (serverVars != null && serverVars[UrlRewriterEnabledServerVar] != null); + _urlRewriterIsTurnedOnValue = urlRewriterIsEnabled; + _urlRewriterIsTurnedOnCalculated = true; + } + } + } + return _urlRewriterIsTurnedOnValue; + } + + public virtual bool WasRequestRewritten(HttpContextBase httpContext) + { + return IsUrlRewriterTurnedOn(httpContext) && WasThisRequestRewritten(httpContext); + } + } +} diff --git a/src/System.Web.Mvc/ValidatableObjectAdapter.cs b/src/System.Web.Mvc/ValidatableObjectAdapter.cs new file mode 100644 index 00000000..88c0137e --- /dev/null +++ b/src/System.Web.Mvc/ValidatableObjectAdapter.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using System.Linq; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + public class ValidatableObjectAdapter : ModelValidator + { + public ValidatableObjectAdapter(ModelMetadata metadata, ControllerContext context) + : base(metadata, context) + { + } + + public override IEnumerable<ModelValidationResult> Validate(object container) + { + // NOTE: Container is never used here, because IValidatableObject doesn't give you + // any way to get access to your container. + + object model = Metadata.Model; + if (model == null) + { + return Enumerable.Empty<ModelValidationResult>(); + } + + IValidatableObject validatable = model as IValidatableObject; + if (validatable == null) + { + throw new InvalidOperationException( + String.Format( + CultureInfo.CurrentCulture, + MvcResources.ValidatableObjectAdapter_IncompatibleType, + typeof(IValidatableObject).FullName, + model.GetType().FullName)); + } + + ValidationContext validationContext = new ValidationContext(validatable, null, null); + return ConvertResults(validatable.Validate(validationContext)); + } + + private IEnumerable<ModelValidationResult> ConvertResults(IEnumerable<ValidationResult> results) + { + foreach (ValidationResult result in results) + { + if (result != ValidationResult.Success) + { + if (result.MemberNames == null || !result.MemberNames.Any()) + { + yield return new ModelValidationResult { Message = result.ErrorMessage }; + } + else + { + foreach (string memberName in result.MemberNames) + { + yield return new ModelValidationResult { Message = result.ErrorMessage, MemberName = memberName }; + } + } + } + } + } + } +} diff --git a/src/System.Web.Mvc/ValidateAntiForgeryTokenAttribute.cs b/src/System.Web.Mvc/ValidateAntiForgeryTokenAttribute.cs new file mode 100644 index 00000000..b948a0b0 --- /dev/null +++ b/src/System.Web.Mvc/ValidateAntiForgeryTokenAttribute.cs @@ -0,0 +1,40 @@ +using System.Diagnostics; +using System.Web.Helpers; + +namespace System.Web.Mvc +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public sealed class ValidateAntiForgeryTokenAttribute : FilterAttribute, IAuthorizationFilter + { + private string _salt; + + public ValidateAntiForgeryTokenAttribute() + : this(AntiForgery.Validate) + { + } + + internal ValidateAntiForgeryTokenAttribute(Action<HttpContextBase, string> validateAction) + { + Debug.Assert(validateAction != null); + ValidateAction = validateAction; + } + + public string Salt + { + get { return _salt ?? String.Empty; } + set { _salt = value; } + } + + internal Action<HttpContextBase, string> ValidateAction { get; private set; } + + public void OnAuthorization(AuthorizationContext filterContext) + { + if (filterContext == null) + { + throw new ArgumentNullException("filterContext"); + } + + ValidateAction(filterContext.HttpContext, Salt); + } + } +} diff --git a/src/System.Web.Mvc/ValidateInputAttribute.cs b/src/System.Web.Mvc/ValidateInputAttribute.cs new file mode 100644 index 00000000..67d43afe --- /dev/null +++ b/src/System.Web.Mvc/ValidateInputAttribute.cs @@ -0,0 +1,26 @@ +using System.Diagnostics.CodeAnalysis; + +namespace System.Web.Mvc +{ + [SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "No compelling performance reason to seal this type.")] + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)] + public class ValidateInputAttribute : FilterAttribute, IAuthorizationFilter + { + public ValidateInputAttribute(bool enableValidation) + { + EnableValidation = enableValidation; + } + + public bool EnableValidation { get; private set; } + + public virtual void OnAuthorization(AuthorizationContext filterContext) + { + if (filterContext == null) + { + throw new ArgumentNullException("filterContext"); + } + + filterContext.Controller.ValidateRequest = EnableValidation; + } + } +} diff --git a/src/System.Web.Mvc/ValueProviderCollection.cs b/src/System.Web.Mvc/ValueProviderCollection.cs new file mode 100644 index 00000000..2cb85e92 --- /dev/null +++ b/src/System.Web.Mvc/ValueProviderCollection.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +namespace System.Web.Mvc +{ + public class ValueProviderCollection : Collection<IValueProvider>, IValueProvider, IUnvalidatedValueProvider, IEnumerableValueProvider + { + public ValueProviderCollection() + { + } + + public ValueProviderCollection(IList<IValueProvider> list) + : base(list) + { + } + + public virtual bool ContainsPrefix(string prefix) + { + return this.Any(vp => vp.ContainsPrefix(prefix)); + } + + public virtual ValueProviderResult GetValue(string key) + { + return GetValue(key, skipValidation: false); + } + + public virtual ValueProviderResult GetValue(string key, bool skipValidation) + { + return (from provider in this + let result = GetValueFromProvider(provider, key, skipValidation) + where result != null + select result).FirstOrDefault(); + } + + public virtual IDictionary<string, string> GetKeysFromPrefix(string prefix) + { + return (from provider in this + let result = GetKeysFromPrefixFromProvider(provider, prefix) + where result != null && result.Any() + select result).FirstOrDefault() ?? new Dictionary<string, string>(); + } + + internal static ValueProviderResult GetValueFromProvider(IValueProvider provider, string key, bool skipValidation) + { + // Since IUnvalidatedValueProvider is a superset of IValueProvider, it's always OK to use the + // IUnvalidatedValueProvider-supplied members if they're present. Otherwise just call the + // normal IValueProvider members. + + IUnvalidatedValueProvider unvalidatedProvider = provider as IUnvalidatedValueProvider; + return (unvalidatedProvider != null) ? unvalidatedProvider.GetValue(key, skipValidation) : provider.GetValue(key); + } + + internal static IDictionary<string, string> GetKeysFromPrefixFromProvider(IValueProvider provider, string prefix) + { + IEnumerableValueProvider enumeratedProvider = provider as IEnumerableValueProvider; + return (enumeratedProvider != null) ? enumeratedProvider.GetKeysFromPrefix(prefix) : null; + } + + protected override void InsertItem(int index, IValueProvider item) + { + if (item == null) + { + throw new ArgumentNullException("item"); + } + base.InsertItem(index, item); + } + + protected override void SetItem(int index, IValueProvider item) + { + if (item == null) + { + throw new ArgumentNullException("item"); + } + base.SetItem(index, item); + } + } +} diff --git a/src/System.Web.Mvc/ValueProviderDictionary.cs b/src/System.Web.Mvc/ValueProviderDictionary.cs new file mode 100644 index 00000000..6ea7b6d8 --- /dev/null +++ b/src/System.Web.Mvc/ValueProviderDictionary.cs @@ -0,0 +1,217 @@ +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Web.Routing; + +namespace System.Web.Mvc +{ + [Obsolete("The recommended alternative is to use one of the specific ValueProvider types, such as FormValueProvider.")] + public class ValueProviderDictionary : IDictionary<string, ValueProviderResult>, IValueProvider + { + private readonly Dictionary<string, ValueProviderResult> _dictionary = new Dictionary<string, ValueProviderResult>(StringComparer.OrdinalIgnoreCase); + + public ValueProviderDictionary(ControllerContext controllerContext) + { + ControllerContext = controllerContext; + if (controllerContext != null) + { + PopulateDictionary(); + } + } + + public ControllerContext ControllerContext { get; private set; } + + public int Count + { + get { return ((ICollection<KeyValuePair<string, ValueProviderResult>>)Dictionary).Count; } + } + + internal Dictionary<string, ValueProviderResult> Dictionary + { + get { return _dictionary; } + } + + public bool IsReadOnly + { + get { return ((ICollection<KeyValuePair<string, ValueProviderResult>>)Dictionary).IsReadOnly; } + } + + public ICollection<string> Keys + { + get { return Dictionary.Keys; } + } + + public ValueProviderResult this[string key] + { + get + { + ValueProviderResult result; + Dictionary.TryGetValue(key, out result); + return result; + } + set { Dictionary[key] = value; } + } + + public ICollection<ValueProviderResult> Values + { + get { return Dictionary.Values; } + } + + public void Add(KeyValuePair<string, ValueProviderResult> item) + { + ((ICollection<KeyValuePair<string, ValueProviderResult>>)Dictionary).Add(item); + } + + public void Add(string key, object value) + { + string attemptedValue = Convert.ToString(value, CultureInfo.InvariantCulture); + ValueProviderResult valueProviderResult = new ValueProviderResult(value, attemptedValue, CultureInfo.InvariantCulture); + Add(key, valueProviderResult); + } + + public void Add(string key, ValueProviderResult value) + { + Dictionary.Add(key, value); + } + + private void AddToDictionaryIfNotPresent(string key, ValueProviderResult result) + { + if (!String.IsNullOrEmpty(key)) + { + if (!Dictionary.ContainsKey(key)) + { + Dictionary.Add(key, result); + } + } + } + + public void Clear() + { + ((ICollection<KeyValuePair<string, ValueProviderResult>>)Dictionary).Clear(); + } + + public bool Contains(KeyValuePair<string, ValueProviderResult> item) + { + return ((ICollection<KeyValuePair<string, ValueProviderResult>>)Dictionary).Contains(item); + } + + public bool ContainsKey(string key) + { + return Dictionary.ContainsKey(key); + } + + public void CopyTo(KeyValuePair<string, ValueProviderResult>[] array, int arrayIndex) + { + ((ICollection<KeyValuePair<string, ValueProviderResult>>)Dictionary).CopyTo(array, arrayIndex); + } + + public IEnumerator<KeyValuePair<string, ValueProviderResult>> GetEnumerator() + { + return ((IEnumerable<KeyValuePair<string, ValueProviderResult>>)Dictionary).GetEnumerator(); + } + + private void PopulateDictionary() + { + CultureInfo currentCulture = CultureInfo.CurrentCulture; + CultureInfo invariantCulture = CultureInfo.InvariantCulture; + + // We use this order of precedence to populate the dictionary: + // 1. Request form submission (should be culture-aware) + // 2. Values from the RouteData (could be from the typed-in URL or from the route's default values) + // 3. URI query string + + NameValueCollection form = ControllerContext.HttpContext.Request.Form; + if (form != null) + { + string[] keys = form.AllKeys; + foreach (string key in keys) + { + string[] rawValue = form.GetValues(key); + string attemptedValue = form[key]; + ValueProviderResult result = new ValueProviderResult(rawValue, attemptedValue, currentCulture); + AddToDictionaryIfNotPresent(key, result); + } + } + + RouteValueDictionary routeValues = ControllerContext.RouteData.Values; + if (routeValues != null) + { + foreach (var kvp in routeValues) + { + string key = kvp.Key; + object rawValue = kvp.Value; + string attemptedValue = Convert.ToString(rawValue, invariantCulture); + ValueProviderResult result = new ValueProviderResult(rawValue, attemptedValue, invariantCulture); + AddToDictionaryIfNotPresent(key, result); + } + } + + NameValueCollection queryString = ControllerContext.HttpContext.Request.QueryString; + if (queryString != null) + { + string[] keys = queryString.AllKeys; + foreach (string key in keys) + { + string[] rawValue = queryString.GetValues(key); + string attemptedValue = queryString[key]; + ValueProviderResult result = new ValueProviderResult(rawValue, attemptedValue, invariantCulture); + AddToDictionaryIfNotPresent(key, result); + } + } + } + + public bool Remove(KeyValuePair<string, ValueProviderResult> item) + { + return ((ICollection<KeyValuePair<string, ValueProviderResult>>)Dictionary).Remove(item); + } + + public bool Remove(string key) + { + return Dictionary.Remove(key); + } + + public bool TryGetValue(string key, out ValueProviderResult value) + { + return Dictionary.TryGetValue(key, out value); + } + + #region IEnumerable Members + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)Dictionary).GetEnumerator(); + } + + #endregion + + #region IValueProvider Members + + [SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Justification = "The declaring type is obsolete, so there is little benefit to exposing this as a virtual method.")] + bool IValueProvider.ContainsPrefix(string prefix) + { + if (prefix == null) + { + throw new ArgumentNullException("prefix"); + } + + return ValueProviderUtil.CollectionContainsPrefix(Keys, prefix); + } + + [SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Justification = "The declaring type is obsolete, so there is little benefit to exposing this as a virtual method.")] + ValueProviderResult IValueProvider.GetValue(string key) + { + if (key == null) + { + throw new ArgumentNullException("key"); + } + + ValueProviderResult valueProviderResult; + TryGetValue(key, out valueProviderResult); + return valueProviderResult; + } + + #endregion + } +} diff --git a/src/System.Web.Mvc/ValueProviderFactories.cs b/src/System.Web.Mvc/ValueProviderFactories.cs new file mode 100644 index 00000000..ee98658a --- /dev/null +++ b/src/System.Web.Mvc/ValueProviderFactories.cs @@ -0,0 +1,20 @@ +namespace System.Web.Mvc +{ + public static class ValueProviderFactories + { + private static readonly ValueProviderFactoryCollection _factories = new ValueProviderFactoryCollection() + { + new ChildActionValueProviderFactory(), + new FormValueProviderFactory(), + new JsonValueProviderFactory(), + new RouteDataValueProviderFactory(), + new QueryStringValueProviderFactory(), + new HttpFileCollectionValueProviderFactory(), + }; + + public static ValueProviderFactoryCollection Factories + { + get { return _factories; } + } + } +} diff --git a/src/System.Web.Mvc/ValueProviderFactory.cs b/src/System.Web.Mvc/ValueProviderFactory.cs new file mode 100644 index 00000000..332b4ae0 --- /dev/null +++ b/src/System.Web.Mvc/ValueProviderFactory.cs @@ -0,0 +1,7 @@ +namespace System.Web.Mvc +{ + public abstract class ValueProviderFactory + { + public abstract IValueProvider GetValueProvider(ControllerContext controllerContext); + } +} diff --git a/src/System.Web.Mvc/ValueProviderFactoryCollection.cs b/src/System.Web.Mvc/ValueProviderFactoryCollection.cs new file mode 100644 index 00000000..84b57759 --- /dev/null +++ b/src/System.Web.Mvc/ValueProviderFactoryCollection.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +namespace System.Web.Mvc +{ + public class ValueProviderFactoryCollection : Collection<ValueProviderFactory> + { + private IResolver<IEnumerable<ValueProviderFactory>> _serviceResolver; + + public ValueProviderFactoryCollection() + { + _serviceResolver = new MultiServiceResolver<ValueProviderFactory>(() => Items); + } + + public ValueProviderFactoryCollection(IList<ValueProviderFactory> list) + : base(list) + { + _serviceResolver = new MultiServiceResolver<ValueProviderFactory>(() => Items); + } + + internal ValueProviderFactoryCollection(IResolver<IEnumerable<ValueProviderFactory>> serviceResolver, params ValueProviderFactory[] valueProviderFactories) + : base(valueProviderFactories) + { + _serviceResolver = serviceResolver ?? new MultiServiceResolver<ValueProviderFactory>(() => Items); + } + + public IValueProvider GetValueProvider(ControllerContext controllerContext) + { + var valueProviders = from factory in _serviceResolver.Current + let valueProvider = factory.GetValueProvider(controllerContext) + where valueProvider != null + select valueProvider; + + return new ValueProviderCollection(valueProviders.ToList()); + } + + protected override void InsertItem(int index, ValueProviderFactory item) + { + if (item == null) + { + throw new ArgumentNullException("item"); + } + base.InsertItem(index, item); + } + + protected override void SetItem(int index, ValueProviderFactory item) + { + if (item == null) + { + throw new ArgumentNullException("item"); + } + base.SetItem(index, item); + } + } +} diff --git a/src/System.Web.Mvc/ValueProviderResult.cs b/src/System.Web.Mvc/ValueProviderResult.cs new file mode 100644 index 00000000..3a15980b --- /dev/null +++ b/src/System.Web.Mvc/ValueProviderResult.cs @@ -0,0 +1,163 @@ +using System.Collections; +using System.ComponentModel; +using System.Globalization; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + [Serializable] + public class ValueProviderResult + { + private static readonly CultureInfo _staticCulture = CultureInfo.InvariantCulture; + private CultureInfo _instanceCulture; + + // default constructor so that subclassed types can set the properties themselves + protected ValueProviderResult() + { + } + + public ValueProviderResult(object rawValue, string attemptedValue, CultureInfo culture) + { + RawValue = rawValue; + AttemptedValue = attemptedValue; + Culture = culture; + } + + public string AttemptedValue { get; protected set; } + + public CultureInfo Culture + { + get + { + if (_instanceCulture == null) + { + _instanceCulture = _staticCulture; + } + return _instanceCulture; + } + protected set { _instanceCulture = value; } + } + + public object RawValue { get; protected set; } + + private static object ConvertSimpleType(CultureInfo culture, object value, Type destinationType) + { + if (value == null || destinationType.IsInstanceOfType(value)) + { + return value; + } + + // if this is a user-input value but the user didn't type anything, return no value + string valueAsString = value as string; + if (valueAsString != null && valueAsString.Trim().Length == 0) + { + return null; + } + + TypeConverter converter = TypeDescriptor.GetConverter(destinationType); + bool canConvertFrom = converter.CanConvertFrom(value.GetType()); + if (!canConvertFrom) + { + converter = TypeDescriptor.GetConverter(value.GetType()); + } + if (!(canConvertFrom || converter.CanConvertTo(destinationType))) + { + // EnumConverter cannot convert integer, so we verify manually + if (destinationType.IsEnum && value is int) + { + return Enum.ToObject(destinationType, (int)value); + } + + // In case of a Nullable object, we try again with its underlying type. + Type underlyingType = Nullable.GetUnderlyingType(destinationType); + if (underlyingType != null) + { + return ConvertSimpleType(culture, value, underlyingType); + } + + string message = String.Format(CultureInfo.CurrentCulture, MvcResources.ValueProviderResult_NoConverterExists, + value.GetType().FullName, destinationType.FullName); + throw new InvalidOperationException(message); + } + + try + { + object convertedValue = (canConvertFrom) + ? converter.ConvertFrom(null /* context */, culture, value) + : converter.ConvertTo(null /* context */, culture, value, destinationType); + return convertedValue; + } + catch (Exception ex) + { + string message = String.Format(CultureInfo.CurrentCulture, MvcResources.ValueProviderResult_ConversionThrew, + value.GetType().FullName, destinationType.FullName); + throw new InvalidOperationException(message, ex); + } + } + + public object ConvertTo(Type type) + { + return ConvertTo(type, null /* culture */); + } + + public virtual object ConvertTo(Type type, CultureInfo culture) + { + if (type == null) + { + throw new ArgumentNullException("type"); + } + + CultureInfo cultureToUse = culture ?? Culture; + return UnwrapPossibleArrayType(cultureToUse, RawValue, type); + } + + private static object UnwrapPossibleArrayType(CultureInfo culture, object value, Type destinationType) + { + if (value == null || destinationType.IsInstanceOfType(value)) + { + return value; + } + + // array conversion results in four cases, as below + Array valueAsArray = value as Array; + if (destinationType.IsArray) + { + Type destinationElementType = destinationType.GetElementType(); + if (valueAsArray != null) + { + // case 1: both destination + source type are arrays, so convert each element + IList converted = Array.CreateInstance(destinationElementType, valueAsArray.Length); + for (int i = 0; i < valueAsArray.Length; i++) + { + converted[i] = ConvertSimpleType(culture, valueAsArray.GetValue(i), destinationElementType); + } + return converted; + } + else + { + // case 2: destination type is array but source is single element, so wrap element in array + convert + object element = ConvertSimpleType(culture, value, destinationElementType); + IList converted = Array.CreateInstance(destinationElementType, 1); + converted[0] = element; + return converted; + } + } + else if (valueAsArray != null) + { + // case 3: destination type is single element but source is array, so extract first element + convert + if (valueAsArray.Length > 0) + { + value = valueAsArray.GetValue(0); + return ConvertSimpleType(culture, value, destinationType); + } + else + { + // case 3(a): source is empty array, so can't perform conversion + return null; + } + } + // case 4: both destination + source type are single elements, so convert + return ConvertSimpleType(culture, value, destinationType); + } + } +} diff --git a/src/System.Web.Mvc/ValueProviderUtil.cs b/src/System.Web.Mvc/ValueProviderUtil.cs new file mode 100644 index 00000000..4db98104 --- /dev/null +++ b/src/System.Web.Mvc/ValueProviderUtil.cs @@ -0,0 +1,119 @@ +using System.Collections.Generic; + +namespace System.Web.Mvc +{ + internal static class ValueProviderUtil + { + // Given "foo.bar[baz].quux", this method will return: + // - "foo.bar[baz].quux" + // - "foo.bar[baz]" + // - "foo.bar" + // - "foo" + public static IEnumerable<string> GetPrefixes(string key) + { + yield return key; + for (int i = key.Length - 1; i >= 0; i--) + { + switch (key[i]) + { + case '.': + case '[': + yield return key.Substring(0, i); + break; + } + } + } + + public static bool CollectionContainsPrefix(IEnumerable<string> collection, string prefix) + { + foreach (string key in collection) + { + if (key != null) + { + if (prefix.Length == 0) + { + return true; // shortcut - non-null key matches empty prefix + } + + if (key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + if (key.Length == prefix.Length) + { + return true; // exact match + } + else + { + switch (key[prefix.Length]) + { + case '.': // known separator characters + case '[': + return true; + } + } + } + } + } + + return false; // nothing found + } + + // Given "foo.bar", "foo.hello", "something.other", foo[abc].baz and asking for prefix "foo" will return: + // - "bar"/"foo.bar" + // - "hello"/"foo.hello" + // - "abc"/"foo[abc]" + public static IDictionary<string, string> GetKeysFromPrefix(IEnumerable<string> collection, string prefix) + { + IDictionary<string, string> keys = new Dictionary<string, string>(); + foreach (var entry in collection) + { + if (entry != null) + { + string key = null; + string fullName = null; + + if (entry.Length == prefix.Length) + { + // No key in this entry + continue; + } + + if (entry.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + int keyPosition = prefix.Length + 1; + switch (entry[prefix.Length]) + { + case '.': + int dotPosition = entry.IndexOf('.', keyPosition); + if (dotPosition == -1) + { + dotPosition = entry.Length; + } + + key = entry.Substring(keyPosition, dotPosition - keyPosition); + fullName = entry.Substring(0, dotPosition); + break; + case '[': + int bracketPosition = entry.IndexOf(']', keyPosition); + if (bracketPosition == -1) + { + // Malformed for dictionary + continue; + } + + key = entry.Substring(keyPosition, bracketPosition - keyPosition); + fullName = entry.Substring(0, bracketPosition + 1); + break; + } + + if (!keys.ContainsKey(key)) + { + keys.Add(key, fullName); + } + } + } + } + + return keys; + } + } +} diff --git a/src/System.Web.Mvc/ViewContext.cs b/src/System.Web.Mvc/ViewContext.cs new file mode 100644 index 00000000..87e5200e --- /dev/null +++ b/src/System.Web.Mvc/ViewContext.cs @@ -0,0 +1,281 @@ +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Web.WebPages.Scope; + +namespace System.Web.Mvc +{ + public class ViewContext : ControllerContext + { + private const string ClientValidationScript = @"<script type=""text/javascript""> +//<![CDATA[ +if (!window.mvcClientValidationMetadata) {{ window.mvcClientValidationMetadata = []; }} +window.mvcClientValidationMetadata.push({0}); +//]]> +</script>"; + + internal static readonly string ClientValidationKeyName = "ClientValidationEnabled"; + internal static readonly string UnobtrusiveJavaScriptKeyName = "UnobtrusiveJavaScriptEnabled"; + + // Some values have to be stored in HttpContext.Items in order to be propagated between calls + // to RenderPartial(), RenderAction(), etc. + private static readonly object _formContextKey = new object(); + private static readonly object _lastFormNumKey = new object(); + + private Func<IDictionary<object, object>> _scopeThunk; + private IDictionary<object, object> _transientScope; + + private DynamicViewDataDictionary _dynamicViewDataDictionary; + private Func<string> _formIdGenerator; + + // We need a default FormContext if the user uses html <form> instead of an MvcForm + private FormContext _defaultFormContext = new FormContext(); + + // parameterless constructor used for mocking + public ViewContext() + { + } + + [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "The virtual property setters are only to support mocking frameworks, in which case this constructor shouldn't be called anyway.")] + public ViewContext(ControllerContext controllerContext, IView view, ViewDataDictionary viewData, TempDataDictionary tempData, TextWriter writer) + : base(controllerContext) + { + if (controllerContext == null) + { + throw new ArgumentNullException("controllerContext"); + } + if (view == null) + { + throw new ArgumentNullException("view"); + } + if (viewData == null) + { + throw new ArgumentNullException("viewData"); + } + if (tempData == null) + { + throw new ArgumentNullException("tempData"); + } + if (writer == null) + { + throw new ArgumentNullException("writer"); + } + + View = view; + ViewData = viewData; + Writer = writer; + TempData = tempData; + } + + public virtual bool ClientValidationEnabled + { + get { return GetClientValidationEnabled(Scope, HttpContext); } + set { SetClientValidationEnabled(value, Scope, HttpContext); } + } + + public virtual FormContext FormContext + { + get + { + // Never return a null form context, this is important for validation purposes + return HttpContext.Items[_formContextKey] as FormContext ?? _defaultFormContext; + } + set { HttpContext.Items[_formContextKey] = value; } + } + + internal Func<string> FormIdGenerator + { + get + { + if (_formIdGenerator == null) + { + _formIdGenerator = DefaultFormIdGenerator; + } + return _formIdGenerator; + } + set { _formIdGenerator = value; } + } + + internal static Func<IDictionary<object, object>> GlobalScopeThunk { get; set; } + + private IDictionary<object, object> Scope + { + get + { + if (ScopeThunk != null) + { + return ScopeThunk(); + } + if (_transientScope == null) + { + _transientScope = new Dictionary<object, object>(); + } + return _transientScope; + } + } + + internal Func<IDictionary<object, object>> ScopeThunk + { + get { return _scopeThunk ?? GlobalScopeThunk; } + set { _scopeThunk = value; } + } + + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "The property setter is only here to support mocking this type and should not be called at runtime.")] + public virtual TempDataDictionary TempData { get; set; } + + public virtual bool UnobtrusiveJavaScriptEnabled + { + get { return GetUnobtrusiveJavaScriptEnabled(Scope, HttpContext); } + set { SetUnobtrusiveJavaScriptEnabled(value, Scope, HttpContext); } + } + + public virtual IView View { get; set; } + + public dynamic ViewBag + { + get + { + if (_dynamicViewDataDictionary == null) + { + _dynamicViewDataDictionary = new DynamicViewDataDictionary(() => ViewData); + } + return _dynamicViewDataDictionary; + } + } + + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "The property setter is only here to support mocking this type and should not be called at runtime.")] + public virtual ViewDataDictionary ViewData { get; set; } + + public virtual TextWriter Writer { get; set; } + + private string DefaultFormIdGenerator() + { + int formNum = IncrementFormCount(HttpContext.Items); + return String.Format(CultureInfo.InvariantCulture, "form{0}", formNum); + } + + internal static bool GetClientValidationEnabled(IDictionary<object, object> scope = null, HttpContextBase httpContext = null) + { + return ScopeCache.Get(scope, httpContext).ClientValidationEnabled; + } + + internal FormContext GetFormContextForClientValidation() + { + return (ClientValidationEnabled) ? FormContext : null; + } + + internal static bool GetUnobtrusiveJavaScriptEnabled(IDictionary<object, object> scope = null, HttpContextBase httpContext = null) + { + return ScopeCache.Get(scope, httpContext).UnobtrusiveJavaScriptEnabled; + } + + private static int IncrementFormCount(IDictionary items) + { + object lastFormNum = items[_lastFormNumKey]; + int newFormNum = (lastFormNum != null) ? ((int)lastFormNum) + 1 : 0; + items[_lastFormNumKey] = newFormNum; + return newFormNum; + } + + public void OutputClientValidation() + { + FormContext formContext = GetFormContextForClientValidation(); + if (formContext == null || UnobtrusiveJavaScriptEnabled) + { + return; // do nothing + } + + string scriptWithCorrectNewLines = ClientValidationScript.Replace("\r\n", Environment.NewLine); + string validationJson = formContext.GetJsonValidationMetadata(); + string formatted = String.Format(CultureInfo.InvariantCulture, scriptWithCorrectNewLines, validationJson); + + Writer.Write(formatted); + } + + internal static void SetClientValidationEnabled(bool enabled, IDictionary<object, object> scope = null, HttpContextBase httpContext = null) + { + ScopeCache.Get(scope, httpContext).ClientValidationEnabled = enabled; + } + + internal static void SetUnobtrusiveJavaScriptEnabled(bool enabled, IDictionary<object, object> scope = null, HttpContextBase httpContext = null) + { + ScopeCache.Get(scope, httpContext).UnobtrusiveJavaScriptEnabled = enabled; + } + + private static TValue ScopeGet<TValue>(IDictionary<object, object> scope, string name, TValue defaultValue = default(TValue)) + { + object result; + if (scope.TryGetValue(name, out result)) + { + return (TValue)Convert.ChangeType(result, typeof(TValue), CultureInfo.InvariantCulture); + } + return defaultValue; + } + + private sealed class ScopeCache + { + private static readonly object _cacheKey = new object(); + private bool _clientValidationEnabled; + private IDictionary<object, object> _scope; + private bool _unobtrusiveJavaScriptEnabled; + + private ScopeCache(IDictionary<object, object> scope) + { + _scope = scope; + + _clientValidationEnabled = ScopeGet(scope, ClientValidationKeyName, false); + _unobtrusiveJavaScriptEnabled = ScopeGet(scope, UnobtrusiveJavaScriptKeyName, false); + } + + public bool ClientValidationEnabled + { + get { return _clientValidationEnabled; } + set + { + _clientValidationEnabled = value; + _scope[ClientValidationKeyName] = value; + } + } + + public bool UnobtrusiveJavaScriptEnabled + { + get { return _unobtrusiveJavaScriptEnabled; } + set + { + _unobtrusiveJavaScriptEnabled = value; + _scope[UnobtrusiveJavaScriptKeyName] = value; + } + } + + public static ScopeCache Get(IDictionary<object, object> scope, HttpContextBase httpContext) + { + if (httpContext == null && Web.HttpContext.Current != null) + { + httpContext = new HttpContextWrapper(Web.HttpContext.Current); + } + + ScopeCache result = null; + scope = scope ?? ScopeStorage.CurrentScope; + + if (httpContext != null) + { + result = httpContext.Items[_cacheKey] as ScopeCache; + } + + if (result == null || result._scope != scope) + { + result = new ScopeCache(scope); + + if (httpContext != null) + { + httpContext.Items[_cacheKey] = result; + } + } + + return result; + } + } + } +} diff --git a/src/System.Web.Mvc/ViewDataDictionary.cs b/src/System.Web.Mvc/ViewDataDictionary.cs new file mode 100644 index 00000000..d104b33f --- /dev/null +++ b/src/System.Web.Mvc/ViewDataDictionary.cs @@ -0,0 +1,387 @@ +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + // TODO: Unit test ModelState interaction with VDD + + public class ViewDataDictionary : IDictionary<string, object> + { + private readonly Dictionary<string, object> _innerDictionary = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); + private readonly ModelStateDictionary _modelState = new ModelStateDictionary(); + private object _model; + private ModelMetadata _modelMetadata; + private TemplateInfo _templateMetadata; + + public ViewDataDictionary() + : this((object)null) + { + } + + [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "See note on SetModel() method.")] + public ViewDataDictionary(object model) + { + Model = model; + } + + [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "See note on SetModel() method.")] + public ViewDataDictionary(ViewDataDictionary dictionary) + { + if (dictionary == null) + { + throw new ArgumentNullException("dictionary"); + } + + foreach (var entry in dictionary) + { + _innerDictionary.Add(entry.Key, entry.Value); + } + foreach (var entry in dictionary.ModelState) + { + ModelState.Add(entry.Key, entry.Value); + } + + Model = dictionary.Model; + TemplateInfo = dictionary.TemplateInfo; + + // PERF: Don't unnecessarily instantiate the model metadata + _modelMetadata = dictionary._modelMetadata; + } + + public int Count + { + get { return _innerDictionary.Count; } + } + + public bool IsReadOnly + { + get { return ((IDictionary<string, object>)_innerDictionary).IsReadOnly; } + } + + public ICollection<string> Keys + { + get { return _innerDictionary.Keys; } + } + + public object Model + { + get { return _model; } + set + { + _modelMetadata = null; + SetModel(value); + } + } + + public virtual ModelMetadata ModelMetadata + { + get + { + if (_modelMetadata == null && _model != null) + { + _modelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => _model, _model.GetType()); + } + return _modelMetadata; + } + set { _modelMetadata = value; } + } + + public ModelStateDictionary ModelState + { + get { return _modelState; } + } + + public TemplateInfo TemplateInfo + { + get + { + if (_templateMetadata == null) + { + _templateMetadata = new TemplateInfo(); + } + return _templateMetadata; + } + set { _templateMetadata = value; } + } + + public ICollection<object> Values + { + get { return _innerDictionary.Values; } + } + + public object this[string key] + { + get + { + object value; + _innerDictionary.TryGetValue(key, out value); + return value; + } + set { _innerDictionary[key] = value; } + } + + public void Add(KeyValuePair<string, object> item) + { + ((IDictionary<string, object>)_innerDictionary).Add(item); + } + + public void Add(string key, object value) + { + _innerDictionary.Add(key, value); + } + + public void Clear() + { + _innerDictionary.Clear(); + } + + public bool Contains(KeyValuePair<string, object> item) + { + return ((IDictionary<string, object>)_innerDictionary).Contains(item); + } + + public bool ContainsKey(string key) + { + return _innerDictionary.ContainsKey(key); + } + + public void CopyTo(KeyValuePair<string, object>[] array, int arrayIndex) + { + ((IDictionary<string, object>)_innerDictionary).CopyTo(array, arrayIndex); + } + + public object Eval(string expression) + { + ViewDataInfo info = GetViewDataInfo(expression); + return (info != null) ? info.Value : null; + } + + public string Eval(string expression, string format) + { + object value = Eval(expression); + return FormatValueInternal(value, format); + } + + internal static string FormatValueInternal(object value, string format) + { + if (value == null) + { + return String.Empty; + } + + if (String.IsNullOrEmpty(format)) + { + return Convert.ToString(value, CultureInfo.CurrentCulture); + } + else + { + return String.Format(CultureInfo.CurrentCulture, format, value); + } + } + + public IEnumerator<KeyValuePair<string, object>> GetEnumerator() + { + return _innerDictionary.GetEnumerator(); + } + + public ViewDataInfo GetViewDataInfo(string expression) + { + if (String.IsNullOrEmpty(expression)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "expression"); + } + + return ViewDataEvaluator.Eval(this, expression); + } + + public bool Remove(KeyValuePair<string, object> item) + { + return ((IDictionary<string, object>)_innerDictionary).Remove(item); + } + + public bool Remove(string key) + { + return _innerDictionary.Remove(key); + } + + // This method will execute before the derived type's instance constructor executes. Derived types must + // be aware of this and should plan accordingly. For example, the logic in SetModel() should be simple + // enough so as not to depend on the "this" pointer referencing a fully constructed object. + protected virtual void SetModel(object value) + { + _model = value; + } + + public bool TryGetValue(string key, out object value) + { + return _innerDictionary.TryGetValue(key, out value); + } + + internal static class ViewDataEvaluator + { + public static ViewDataInfo Eval(ViewDataDictionary vdd, string expression) + { + //Given an expression "foo.bar.baz" we look up the following (pseudocode): + // this["foo.bar.baz.quux"] + // this["foo.bar.baz"]["quux"] + // this["foo.bar"]["baz.quux] + // this["foo.bar"]["baz"]["quux"] + // this["foo"]["bar.baz.quux"] + // this["foo"]["bar.baz"]["quux"] + // this["foo"]["bar"]["baz.quux"] + // this["foo"]["bar"]["baz"]["quux"] + + ViewDataInfo evaluated = EvalComplexExpression(vdd, expression); + return evaluated; + } + + private static ViewDataInfo EvalComplexExpression(object indexableObject, string expression) + { + foreach (ExpressionPair expressionPair in GetRightToLeftExpressions(expression)) + { + string subExpression = expressionPair.Left; + string postExpression = expressionPair.Right; + + ViewDataInfo subTargetInfo = GetPropertyValue(indexableObject, subExpression); + if (subTargetInfo != null) + { + if (String.IsNullOrEmpty(postExpression)) + { + return subTargetInfo; + } + + if (subTargetInfo.Value != null) + { + ViewDataInfo potential = EvalComplexExpression(subTargetInfo.Value, postExpression); + if (potential != null) + { + return potential; + } + } + } + } + return null; + } + + private static IEnumerable<ExpressionPair> GetRightToLeftExpressions(string expression) + { + // Produces an enumeration of all the combinations of complex property names + // given a complex expression. See the list above for an example of the result + // of the enumeration. + + yield return new ExpressionPair(expression, String.Empty); + + int lastDot = expression.LastIndexOf('.'); + + string subExpression = expression; + string postExpression = String.Empty; + + while (lastDot > -1) + { + subExpression = expression.Substring(0, lastDot); + postExpression = expression.Substring(lastDot + 1); + yield return new ExpressionPair(subExpression, postExpression); + + lastDot = subExpression.LastIndexOf('.'); + } + } + + private static ViewDataInfo GetIndexedPropertyValue(object indexableObject, string key) + { + IDictionary<string, object> dict = indexableObject as IDictionary<string, object>; + object value = null; + bool success = false; + + if (dict != null) + { + success = dict.TryGetValue(key, out value); + } + else + { + TryGetValueDelegate tgvDel = TypeHelpers.CreateTryGetValueDelegate(indexableObject.GetType()); + if (tgvDel != null) + { + success = tgvDel(indexableObject, key, out value); + } + } + + if (success) + { + return new ViewDataInfo() + { + Container = indexableObject, + Value = value + }; + } + + return null; + } + + private static ViewDataInfo GetPropertyValue(object container, string propertyName) + { + // This method handles one "segment" of a complex property expression + + // First, we try to evaluate the property based on its indexer + ViewDataInfo value = GetIndexedPropertyValue(container, propertyName); + if (value != null) + { + return value; + } + + // If the indexer didn't return anything useful, continue... + + // If the container is a ViewDataDictionary then treat its Model property + // as the container instead of the ViewDataDictionary itself. + ViewDataDictionary vdd = container as ViewDataDictionary; + if (vdd != null) + { + container = vdd.Model; + } + + // If the container is null, we're out of options + if (container == null) + { + return null; + } + + // Second, we try to use PropertyDescriptors and treat the expression as a property name + PropertyDescriptor descriptor = TypeDescriptor.GetProperties(container).Find(propertyName, true); + if (descriptor == null) + { + return null; + } + + return new ViewDataInfo(() => descriptor.GetValue(container)) + { + Container = container, + PropertyDescriptor = descriptor + }; + } + + private struct ExpressionPair + { + public readonly string Left; + public readonly string Right; + + public ExpressionPair(string left, string right) + { + Left = left; + Right = right; + } + } + } + + #region IEnumerable Members + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_innerDictionary).GetEnumerator(); + } + + #endregion + } +} diff --git a/src/System.Web.Mvc/ViewDataDictionary`1.cs b/src/System.Web.Mvc/ViewDataDictionary`1.cs new file mode 100644 index 00000000..966cc77b --- /dev/null +++ b/src/System.Web.Mvc/ViewDataDictionary`1.cs @@ -0,0 +1,60 @@ +namespace System.Web.Mvc +{ + public class ViewDataDictionary<TModel> : ViewDataDictionary + { + public ViewDataDictionary() + : + base(default(TModel)) + { + } + + public ViewDataDictionary(TModel model) + : + base(model) + { + } + + public ViewDataDictionary(ViewDataDictionary viewDataDictionary) + : + base(viewDataDictionary) + { + } + + public new TModel Model + { + get { return (TModel)base.Model; } + set { SetModel(value); } + } + + public override ModelMetadata ModelMetadata + { + get + { + ModelMetadata result = base.ModelMetadata; + if (result == null) + { + result = base.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(TModel)); + } + return result; + } + set { base.ModelMetadata = value; } + } + + protected override void SetModel(object value) + { + bool castWillSucceed = TypeHelpers.IsCompatibleObject<TModel>(value); + + if (castWillSucceed) + { + base.SetModel((TModel)value); + } + else + { + InvalidOperationException exception = (value != null) + ? Error.ViewDataDictionary_WrongTModelType(value.GetType(), typeof(TModel)) + : Error.ViewDataDictionary_ModelCannotBeNull(typeof(TModel)); + throw exception; + } + } + } +} diff --git a/src/System.Web.Mvc/ViewDataInfo.cs b/src/System.Web.Mvc/ViewDataInfo.cs new file mode 100644 index 00000000..c508ffda --- /dev/null +++ b/src/System.Web.Mvc/ViewDataInfo.cs @@ -0,0 +1,42 @@ +using System.ComponentModel; + +namespace System.Web.Mvc +{ + public class ViewDataInfo + { + private object _value; + private Func<object> _valueAccessor; + + public ViewDataInfo() + { + } + + public ViewDataInfo(Func<object> valueAccessor) + { + _valueAccessor = valueAccessor; + } + + public object Container { get; set; } + + public PropertyDescriptor PropertyDescriptor { get; set; } + + public object Value + { + get + { + if (_valueAccessor != null) + { + _value = _valueAccessor(); + _valueAccessor = null; + } + + return _value; + } + set + { + _value = value; + _valueAccessor = null; + } + } + } +} diff --git a/src/System.Web.Mvc/ViewEngineCollection.cs b/src/System.Web.Mvc/ViewEngineCollection.cs new file mode 100644 index 00000000..6dcd254b --- /dev/null +++ b/src/System.Web.Mvc/ViewEngineCollection.cs @@ -0,0 +1,133 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + public class ViewEngineCollection : Collection<IViewEngine> + { + private IResolver<IEnumerable<IViewEngine>> _serviceResolver; + + public ViewEngineCollection() + { + _serviceResolver = new MultiServiceResolver<IViewEngine>(() => Items); + } + + public ViewEngineCollection(IList<IViewEngine> list) + : base(list) + { + _serviceResolver = new MultiServiceResolver<IViewEngine>(() => Items); + } + + internal ViewEngineCollection(IResolver<IEnumerable<IViewEngine>> serviceResolver, params IViewEngine[] engines) + : base(engines) + { + _serviceResolver = serviceResolver ?? new MultiServiceResolver<IViewEngine>(() => Items); + } + + private IEnumerable<IViewEngine> CombinedItems + { + get { return _serviceResolver.Current; } + } + + protected override void InsertItem(int index, IViewEngine item) + { + if (item == null) + { + throw new ArgumentNullException("item"); + } + base.InsertItem(index, item); + } + + protected override void SetItem(int index, IViewEngine item) + { + if (item == null) + { + throw new ArgumentNullException("item"); + } + base.SetItem(index, item); + } + + private ViewEngineResult Find(Func<IViewEngine, ViewEngineResult> cacheLocator, Func<IViewEngine, ViewEngineResult> locator) + { + // First, look up using the cacheLocator and do not track the searched paths in non-matching view engines + // Then, look up using the normal locator and track the searched paths so that an error view engine can be returned + return Find(cacheLocator, trackSearchedPaths: false) + ?? Find(locator, trackSearchedPaths: true); + } + + private ViewEngineResult Find(Func<IViewEngine, ViewEngineResult> lookup, bool trackSearchedPaths) + { + // Returns + // 1st result + // OR list of searched paths (if trackSearchedPaths == true) + // OR null + ViewEngineResult result; + + List<string> searched = null; + if (trackSearchedPaths) + { + searched = new List<string>(); + } + + foreach (IViewEngine engine in CombinedItems) + { + if (engine != null) + { + result = lookup(engine); + + if (result.View != null) + { + return result; + } + + if (trackSearchedPaths) + { + searched.AddRange(result.SearchedLocations); + } + } + } + + if (trackSearchedPaths) + { + // Remove duplicate search paths since multiple view engines could have potentially looked at the same path + return new ViewEngineResult(searched.Distinct().ToList()); + } + else + { + return null; + } + } + + public virtual ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName) + { + if (controllerContext == null) + { + throw new ArgumentNullException("controllerContext"); + } + if (String.IsNullOrEmpty(partialViewName)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "partialViewName"); + } + + return Find(e => e.FindPartialView(controllerContext, partialViewName, true), + e => e.FindPartialView(controllerContext, partialViewName, false)); + } + + public virtual ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName) + { + if (controllerContext == null) + { + throw new ArgumentNullException("controllerContext"); + } + if (String.IsNullOrEmpty(viewName)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "viewName"); + } + + return Find(e => e.FindView(controllerContext, viewName, masterName, true), + e => e.FindView(controllerContext, viewName, masterName, false)); + } + } +} diff --git a/src/System.Web.Mvc/ViewEngineResult.cs b/src/System.Web.Mvc/ViewEngineResult.cs new file mode 100644 index 00000000..7e9e081a --- /dev/null +++ b/src/System.Web.Mvc/ViewEngineResult.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; + +namespace System.Web.Mvc +{ + public class ViewEngineResult + { + public ViewEngineResult(IEnumerable<string> searchedLocations) + { + if (searchedLocations == null) + { + throw new ArgumentNullException("searchedLocations"); + } + + SearchedLocations = searchedLocations; + } + + public ViewEngineResult(IView view, IViewEngine viewEngine) + { + if (view == null) + { + throw new ArgumentNullException("view"); + } + if (viewEngine == null) + { + throw new ArgumentNullException("viewEngine"); + } + + View = view; + ViewEngine = viewEngine; + } + + public IEnumerable<string> SearchedLocations { get; private set; } + + public IView View { get; private set; } + + public IViewEngine ViewEngine { get; private set; } + } +} diff --git a/src/System.Web.Mvc/ViewEngines.cs b/src/System.Web.Mvc/ViewEngines.cs new file mode 100644 index 00000000..090f20bc --- /dev/null +++ b/src/System.Web.Mvc/ViewEngines.cs @@ -0,0 +1,16 @@ +namespace System.Web.Mvc +{ + public static class ViewEngines + { + private static readonly ViewEngineCollection _engines = new ViewEngineCollection + { + new WebFormViewEngine(), + new RazorViewEngine(), + }; + + public static ViewEngineCollection Engines + { + get { return _engines; } + } + } +} diff --git a/src/System.Web.Mvc/ViewMasterPage.cs b/src/System.Web.Mvc/ViewMasterPage.cs new file mode 100644 index 00000000..c3d7c66b --- /dev/null +++ b/src/System.Web.Mvc/ViewMasterPage.cs @@ -0,0 +1,68 @@ +using System.Globalization; +using System.Web.Mvc.Properties; +using System.Web.UI; + +namespace System.Web.Mvc +{ + [FileLevelControlBuilder(typeof(ViewMasterPageControlBuilder))] + public class ViewMasterPage : MasterPage + { + public AjaxHelper<object> Ajax + { + get { return ViewPage.Ajax; } + } + + public HtmlHelper<object> Html + { + get { return ViewPage.Html; } + } + + public object Model + { + get { return ViewData.Model; } + } + + public TempDataDictionary TempData + { + get { return ViewPage.TempData; } + } + + public UrlHelper Url + { + get { return ViewPage.Url; } + } + + public dynamic ViewBag + { + get { return ViewPage.ViewBag; } + } + + public ViewContext ViewContext + { + get { return ViewPage.ViewContext; } + } + + public ViewDataDictionary ViewData + { + get { return ViewPage.ViewData; } + } + + internal ViewPage ViewPage + { + get + { + ViewPage viewPage = Page as ViewPage; + if (viewPage == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, MvcResources.ViewMasterPage_RequiresViewPage)); + } + return viewPage; + } + } + + public HtmlTextWriter Writer + { + get { return ViewPage.Writer; } + } + } +} diff --git a/src/System.Web.Mvc/ViewMasterPageControlBuilder.cs b/src/System.Web.Mvc/ViewMasterPageControlBuilder.cs new file mode 100644 index 00000000..a41556ef --- /dev/null +++ b/src/System.Web.Mvc/ViewMasterPageControlBuilder.cs @@ -0,0 +1,18 @@ +using System.CodeDom; +using System.Web.UI; + +namespace System.Web.Mvc +{ + internal sealed class ViewMasterPageControlBuilder : FileLevelMasterPageControlBuilder, IMvcControlBuilder + { + public string Inherits { get; set; } + + public override void ProcessGeneratedCode(CodeCompileUnit codeCompileUnit, CodeTypeDeclaration baseType, CodeTypeDeclaration derivedType, CodeMemberMethod buildMethod, CodeMemberMethod dataBindingMethod) + { + if (!String.IsNullOrWhiteSpace(Inherits)) + { + derivedType.BaseTypes[0] = new CodeTypeReference(Inherits); + } + } + } +} diff --git a/src/System.Web.Mvc/ViewMasterPage`1.cs b/src/System.Web.Mvc/ViewMasterPage`1.cs new file mode 100644 index 00000000..d514c7d7 --- /dev/null +++ b/src/System.Web.Mvc/ViewMasterPage`1.cs @@ -0,0 +1,50 @@ +namespace System.Web.Mvc +{ + public class ViewMasterPage<TModel> : ViewMasterPage + { + private AjaxHelper<TModel> _ajaxHelper; + private HtmlHelper<TModel> _htmlHelper; + private ViewDataDictionary<TModel> _viewData; + + public new AjaxHelper<TModel> Ajax + { + get + { + if (_ajaxHelper == null) + { + _ajaxHelper = new AjaxHelper<TModel>(ViewContext, ViewPage); + } + return _ajaxHelper; + } + } + + public new HtmlHelper<TModel> Html + { + get + { + if (_htmlHelper == null) + { + _htmlHelper = new HtmlHelper<TModel>(ViewContext, ViewPage); + } + return _htmlHelper; + } + } + + public new TModel Model + { + get { return ViewData.Model; } + } + + public new ViewDataDictionary<TModel> ViewData + { + get + { + if (_viewData == null) + { + _viewData = new ViewDataDictionary<TModel>(ViewPage.ViewData); + } + return _viewData; + } + } + } +} diff --git a/src/System.Web.Mvc/ViewPage.cs b/src/System.Web.Mvc/ViewPage.cs new file mode 100644 index 00000000..79a4fec7 --- /dev/null +++ b/src/System.Web.Mvc/ViewPage.cs @@ -0,0 +1,427 @@ +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Text; +using System.Web.UI; + +namespace System.Web.Mvc +{ + [FileLevelControlBuilder(typeof(ViewPageControlBuilder))] + public class ViewPage : Page, IViewDataContainer + { + [ThreadStatic] + private static int _nextId; + + private DynamicViewDataDictionary _dynamicViewData; + private string _masterLocation; + + private ViewDataDictionary _viewData; + + public AjaxHelper<object> Ajax { get; set; } + + public HtmlHelper<object> Html { get; set; } + + public string MasterLocation + { + get { return _masterLocation ?? String.Empty; } + set { _masterLocation = value; } + } + + public object Model + { + get { return ViewData.Model; } + } + + public TempDataDictionary TempData + { + get { return ViewContext.TempData; } + } + + public UrlHelper Url { get; set; } + + public dynamic ViewBag + { + get + { + if (_dynamicViewData == null) + { + _dynamicViewData = new DynamicViewDataDictionary(() => ViewData); + } + return _dynamicViewData; + } + } + + public ViewContext ViewContext { get; set; } + + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This is the mechanism by which the ViewPage gets its ViewDataDictionary object.")] + public ViewDataDictionary ViewData + { + get + { + if (_viewData == null) + { + SetViewData(new ViewDataDictionary()); + } + return _viewData; + } + set { SetViewData(value); } + } + + public HtmlTextWriter Writer { get; private set; } + + public virtual void InitHelpers() + { + Ajax = new AjaxHelper<object>(ViewContext, this); + Html = new HtmlHelper<object>(ViewContext, this); + Url = new UrlHelper(ViewContext.RequestContext); + } + + internal static string NextId() + { + return (++_nextId).ToString(CultureInfo.InvariantCulture); + } + + protected override void OnPreInit(EventArgs e) + { + base.OnPreInit(e); + + if (!String.IsNullOrEmpty(MasterLocation)) + { + MasterPageFile = MasterLocation; + } + } + + public override void ProcessRequest(HttpContext context) + { + // Tracing requires IDs to be unique. + ID = NextId(); + + base.ProcessRequest(context); + } + + protected override void Render(HtmlTextWriter writer) + { + Writer = writer; + try + { + base.Render(writer); + } + finally + { + Writer = null; + } + } + + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "The object is disposed in the finally block of the method")] + public virtual void RenderView(ViewContext viewContext) + { + ViewContext = viewContext; + InitHelpers(); + + bool createdSwitchWriter = false; + SwitchWriter switchWriter = viewContext.HttpContext.Response.Output as SwitchWriter; + + try + { + if (switchWriter == null) + { + switchWriter = new SwitchWriter(); + createdSwitchWriter = true; + } + + using (switchWriter.Scope(viewContext.Writer)) + { + if (createdSwitchWriter) + { + // It's safe to reset the _nextId within a Server.Execute() since it pushes a new TraceContext onto + // the stack, so there won't be an ID conflict. + int originalNextId = _nextId; + try + { + _nextId = 0; + viewContext.HttpContext.Server.Execute(HttpHandlerUtil.WrapForServerExecute(this), switchWriter, true /* preserveForm */); + } + finally + { + // Restore the original _nextId in case this isn't actually the outermost view, since resetting + // the _nextId may now cause trace ID conflicts in the outer view. + _nextId = originalNextId; + } + } + else + { + ProcessRequest(HttpContext.Current); + } + } + } + finally + { + if (createdSwitchWriter) + { + switchWriter.Dispose(); + } + } + } + + [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "textWriter", Justification = "This method existed in MVC 1.0 and has been deprecated.")] + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This method existed in MVC 1.0 and has been deprecated.")] + [Obsolete("The TextWriter is now provided by the ViewContext object passed to the RenderView method.", true /* error */)] + public void SetTextWriter(TextWriter textWriter) + { + // this is now a no-op + } + + protected virtual void SetViewData(ViewDataDictionary viewData) + { + _viewData = viewData; + } + + internal class SwitchWriter : TextWriter + { + public SwitchWriter() + : base(CultureInfo.CurrentCulture) + { + } + + public override Encoding Encoding + { + get { return InnerWriter.Encoding; } + } + + public override IFormatProvider FormatProvider + { + get { return InnerWriter.FormatProvider; } + } + + internal TextWriter InnerWriter { get; set; } + + public override string NewLine + { + get { return InnerWriter.NewLine; } + set { InnerWriter.NewLine = value; } + } + + public override void Close() + { + InnerWriter.Close(); + } + + public override void Flush() + { + InnerWriter.Flush(); + } + + public IDisposable Scope(TextWriter writer) + { + WriterScope scope = new WriterScope(this, InnerWriter); + + try + { + if (writer != this) + { + InnerWriter = writer; + } + + return scope; + } + catch + { + scope.Dispose(); + throw; + } + } + + public override void Write(bool value) + { + InnerWriter.Write(value); + } + + public override void Write(char value) + { + InnerWriter.Write(value); + } + + public override void Write(char[] buffer) + { + InnerWriter.Write(buffer); + } + + public override void Write(char[] buffer, int index, int count) + { + InnerWriter.Write(buffer, index, count); + } + + public override void Write(decimal value) + { + InnerWriter.Write(value); + } + + public override void Write(double value) + { + InnerWriter.Write(value); + } + + public override void Write(float value) + { + InnerWriter.Write(value); + } + + public override void Write(int value) + { + InnerWriter.Write(value); + } + + public override void Write(long value) + { + InnerWriter.Write(value); + } + + public override void Write(object value) + { + InnerWriter.Write(value); + } + + public override void Write(string format, object arg0) + { + InnerWriter.Write(format, arg0); + } + + public override void Write(string format, object arg0, object arg1) + { + InnerWriter.Write(format, arg0, arg1); + } + + public override void Write(string format, object arg0, object arg1, object arg2) + { + InnerWriter.Write(format, arg0, arg1, arg2); + } + + public override void Write(string format, params object[] arg) + { + InnerWriter.Write(format, arg); + } + + public override void Write(string value) + { + InnerWriter.Write(value); + } + + public override void Write(uint value) + { + InnerWriter.Write(value); + } + + public override void Write(ulong value) + { + InnerWriter.Write(value); + } + + public override void WriteLine() + { + InnerWriter.WriteLine(); + } + + public override void WriteLine(bool value) + { + InnerWriter.WriteLine(value); + } + + public override void WriteLine(char value) + { + InnerWriter.WriteLine(value); + } + + public override void WriteLine(char[] buffer) + { + InnerWriter.WriteLine(buffer); + } + + public override void WriteLine(char[] buffer, int index, int count) + { + InnerWriter.WriteLine(buffer, index, count); + } + + public override void WriteLine(decimal value) + { + InnerWriter.WriteLine(value); + } + + public override void WriteLine(double value) + { + InnerWriter.WriteLine(value); + } + + public override void WriteLine(float value) + { + InnerWriter.WriteLine(value); + } + + public override void WriteLine(int value) + { + InnerWriter.WriteLine(value); + } + + public override void WriteLine(long value) + { + InnerWriter.WriteLine(value); + } + + public override void WriteLine(object value) + { + InnerWriter.WriteLine(value); + } + + public override void WriteLine(string format, object arg0) + { + InnerWriter.WriteLine(format, arg0); + } + + public override void WriteLine(string format, object arg0, object arg1) + { + InnerWriter.WriteLine(format, arg0, arg1); + } + + public override void WriteLine(string format, object arg0, object arg1, object arg2) + { + InnerWriter.WriteLine(format, arg0, arg1, arg2); + } + + public override void WriteLine(string format, params object[] arg) + { + InnerWriter.WriteLine(format, arg); + } + + public override void WriteLine(string value) + { + InnerWriter.WriteLine(value); + } + + public override void WriteLine(uint value) + { + InnerWriter.WriteLine(value); + } + + public override void WriteLine(ulong value) + { + InnerWriter.WriteLine(value); + } + + private sealed class WriterScope : IDisposable + { + private SwitchWriter _switchWriter; + private TextWriter _writerToRestore; + + public WriterScope(SwitchWriter switchWriter, TextWriter writerToRestore) + { + _switchWriter = switchWriter; + _writerToRestore = writerToRestore; + } + + public void Dispose() + { + _switchWriter.InnerWriter = _writerToRestore; + } + } + } + } +} diff --git a/src/System.Web.Mvc/ViewPageControlBuilder.cs b/src/System.Web.Mvc/ViewPageControlBuilder.cs new file mode 100644 index 00000000..0dd309a3 --- /dev/null +++ b/src/System.Web.Mvc/ViewPageControlBuilder.cs @@ -0,0 +1,18 @@ +using System.CodeDom; +using System.Web.UI; + +namespace System.Web.Mvc +{ + internal sealed class ViewPageControlBuilder : FileLevelPageControlBuilder, IMvcControlBuilder + { + public string Inherits { get; set; } + + public override void ProcessGeneratedCode(CodeCompileUnit codeCompileUnit, CodeTypeDeclaration baseType, CodeTypeDeclaration derivedType, CodeMemberMethod buildMethod, CodeMemberMethod dataBindingMethod) + { + if (!String.IsNullOrWhiteSpace(Inherits)) + { + derivedType.BaseTypes[0] = new CodeTypeReference(Inherits); + } + } + } +} diff --git a/src/System.Web.Mvc/ViewPage`1.cs b/src/System.Web.Mvc/ViewPage`1.cs new file mode 100644 index 00000000..2e5be343 --- /dev/null +++ b/src/System.Web.Mvc/ViewPage`1.cs @@ -0,0 +1,47 @@ +using System.Diagnostics.CodeAnalysis; + +namespace System.Web.Mvc +{ + public class ViewPage<TModel> : ViewPage + { + private ViewDataDictionary<TModel> _viewData; + + public new AjaxHelper<TModel> Ajax { get; set; } + + public new HtmlHelper<TModel> Html { get; set; } + + public new TModel Model + { + get { return ViewData.Model; } + } + + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This is settable for unit testing purposes")] + public new ViewDataDictionary<TModel> ViewData + { + get + { + if (_viewData == null) + { + SetViewData(new ViewDataDictionary<TModel>()); + } + return _viewData; + } + set { SetViewData(value); } + } + + public override void InitHelpers() + { + base.InitHelpers(); + + Ajax = new AjaxHelper<TModel>(ViewContext, this); + Html = new HtmlHelper<TModel>(ViewContext, this); + } + + protected override void SetViewData(ViewDataDictionary viewData) + { + _viewData = new ViewDataDictionary<TModel>(viewData); + + base.SetViewData(_viewData); + } + } +} diff --git a/src/System.Web.Mvc/ViewResult.cs b/src/System.Web.Mvc/ViewResult.cs new file mode 100644 index 00000000..c462d6df --- /dev/null +++ b/src/System.Web.Mvc/ViewResult.cs @@ -0,0 +1,36 @@ +using System.Globalization; +using System.Text; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + public class ViewResult : ViewResultBase + { + private string _masterName; + + public string MasterName + { + get { return _masterName ?? String.Empty; } + set { _masterName = value; } + } + + protected override ViewEngineResult FindView(ControllerContext context) + { + ViewEngineResult result = ViewEngineCollection.FindView(context, ViewName, MasterName); + if (result.View != null) + { + return result; + } + + // we need to generate an exception containing all the locations we searched + StringBuilder locationsText = new StringBuilder(); + foreach (string location in result.SearchedLocations) + { + locationsText.AppendLine(); + locationsText.Append(location); + } + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, + MvcResources.Common_ViewNotFound, ViewName, locationsText)); + } + } +} diff --git a/src/System.Web.Mvc/ViewResultBase.cs b/src/System.Web.Mvc/ViewResultBase.cs new file mode 100644 index 00000000..d7650aa8 --- /dev/null +++ b/src/System.Web.Mvc/ViewResultBase.cs @@ -0,0 +1,105 @@ +using System.Diagnostics.CodeAnalysis; +using System.IO; + +namespace System.Web.Mvc +{ + public abstract class ViewResultBase : ActionResult + { + private DynamicViewDataDictionary _dynamicViewData; + private TempDataDictionary _tempData; + private ViewDataDictionary _viewData; + private ViewEngineCollection _viewEngineCollection; + private string _viewName; + + public object Model + { + get { return ViewData.Model; } + } + + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This entire type is meant to be mutable.")] + public TempDataDictionary TempData + { + get + { + if (_tempData == null) + { + _tempData = new TempDataDictionary(); + } + return _tempData; + } + set { _tempData = value; } + } + + public IView View { get; set; } + + public dynamic ViewBag + { + get + { + if (_dynamicViewData == null) + { + _dynamicViewData = new DynamicViewDataDictionary(() => ViewData); + } + return _dynamicViewData; + } + } + + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This entire type is meant to be mutable.")] + public ViewDataDictionary ViewData + { + get + { + if (_viewData == null) + { + _viewData = new ViewDataDictionary(); + } + return _viewData; + } + set { _viewData = value; } + } + + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This entire type is meant to be mutable.")] + public ViewEngineCollection ViewEngineCollection + { + get { return _viewEngineCollection ?? ViewEngines.Engines; } + set { _viewEngineCollection = value; } + } + + public string ViewName + { + get { return _viewName ?? String.Empty; } + set { _viewName = value; } + } + + public override void ExecuteResult(ControllerContext context) + { + if (context == null) + { + throw new ArgumentNullException("context"); + } + if (String.IsNullOrEmpty(ViewName)) + { + ViewName = context.RouteData.GetRequiredString("action"); + } + + ViewEngineResult result = null; + + if (View == null) + { + result = FindView(context); + View = result.View; + } + + TextWriter writer = context.HttpContext.Response.Output; + ViewContext viewContext = new ViewContext(context, View, ViewData, TempData, writer); + View.Render(viewContext, writer); + + if (result != null) + { + result.ViewEngine.ReleaseView(context, View); + } + } + + protected abstract ViewEngineResult FindView(ControllerContext context); + } +} diff --git a/src/System.Web.Mvc/ViewStartPage.cs b/src/System.Web.Mvc/ViewStartPage.cs new file mode 100644 index 00000000..437943c1 --- /dev/null +++ b/src/System.Web.Mvc/ViewStartPage.cs @@ -0,0 +1,43 @@ +using System.Web.Mvc.Properties; +using System.Web.WebPages; + +namespace System.Web.Mvc +{ + public abstract class ViewStartPage : StartPage, IViewStartPageChild + { + private IViewStartPageChild _viewStartPageChild; + + public HtmlHelper<object> Html + { + get { return ViewStartPageChild.Html; } + } + + public UrlHelper Url + { + get { return ViewStartPageChild.Url; } + } + + public ViewContext ViewContext + { + get { return ViewStartPageChild.ViewContext; } + } + + internal IViewStartPageChild ViewStartPageChild + { + get + { + if (_viewStartPageChild == null) + { + IViewStartPageChild child = ChildPage as IViewStartPageChild; + if (child == null) + { + throw new InvalidOperationException(MvcResources.ViewStartPage_RequiresMvcRazorView); + } + _viewStartPageChild = child; + } + + return _viewStartPageChild; + } + } + } +} diff --git a/src/System.Web.Mvc/ViewTemplateUserControl.cs b/src/System.Web.Mvc/ViewTemplateUserControl.cs new file mode 100644 index 00000000..fa6f0096 --- /dev/null +++ b/src/System.Web.Mvc/ViewTemplateUserControl.cs @@ -0,0 +1,6 @@ +namespace System.Web.Mvc +{ + public class ViewTemplateUserControl : ViewTemplateUserControl<object> + { + } +} diff --git a/src/System.Web.Mvc/ViewTemplateUserControl`1.cs b/src/System.Web.Mvc/ViewTemplateUserControl`1.cs new file mode 100644 index 00000000..5f3243fd --- /dev/null +++ b/src/System.Web.Mvc/ViewTemplateUserControl`1.cs @@ -0,0 +1,10 @@ +namespace System.Web.Mvc +{ + public class ViewTemplateUserControl<TModel> : ViewUserControl<TModel> + { + protected string FormattedModelValue + { + get { return ViewData.TemplateInfo.FormattedModelValue.ToString(); } + } + } +} diff --git a/src/System.Web.Mvc/ViewType.cs b/src/System.Web.Mvc/ViewType.cs new file mode 100644 index 00000000..2bf9c01a --- /dev/null +++ b/src/System.Web.Mvc/ViewType.cs @@ -0,0 +1,19 @@ +using System.ComponentModel; +using System.Web.UI; + +namespace System.Web.Mvc +{ + [ControlBuilder(typeof(ViewTypeControlBuilder))] + [NonVisualControl] + public class ViewType : Control + { + private string _typeName; + + [DefaultValue("")] + public string TypeName + { + get { return _typeName ?? String.Empty; } + set { _typeName = value; } + } + } +} diff --git a/src/System.Web.Mvc/ViewTypeControlBuilder.cs b/src/System.Web.Mvc/ViewTypeControlBuilder.cs new file mode 100644 index 00000000..01a0a2aa --- /dev/null +++ b/src/System.Web.Mvc/ViewTypeControlBuilder.cs @@ -0,0 +1,29 @@ +using System.CodeDom; +using System.Collections; +using System.Web.UI; + +namespace System.Web.Mvc +{ + internal sealed class ViewTypeControlBuilder : ControlBuilder + { + private string _typeName; + + public override void Init(TemplateParser parser, ControlBuilder parentBuilder, Type type, string tagName, string id, IDictionary attribs) + { + base.Init(parser, parentBuilder, type, tagName, id, attribs); + + _typeName = (string)attribs["typename"]; + } + + public override void ProcessGeneratedCode( + CodeCompileUnit codeCompileUnit, + CodeTypeDeclaration baseType, + CodeTypeDeclaration derivedType, + CodeMemberMethod buildMethod, + CodeMemberMethod dataBindingMethod) + { + // Override the view's base type with the explicit base type + derivedType.BaseTypes[0] = new CodeTypeReference(_typeName); + } + } +} diff --git a/src/System.Web.Mvc/ViewTypeParserFilter.cs b/src/System.Web.Mvc/ViewTypeParserFilter.cs new file mode 100644 index 00000000..3d1caecf --- /dev/null +++ b/src/System.Web.Mvc/ViewTypeParserFilter.cs @@ -0,0 +1,109 @@ +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Web.UI; + +namespace System.Web.Mvc +{ + internal class ViewTypeParserFilter : PageParserFilter + { + private static Dictionary<string, Type> _directiveBaseTypeMappings = new Dictionary<string, Type> + { + { "page", typeof(ViewPage) }, + { "control", typeof(ViewUserControl) }, + { "master", typeof(ViewMasterPage) }, + }; + + private string _inherits; + + [SuppressMessage("Microsoft.Security", "CA2141:TransparentMethodsMustNotSatisfyLinkDemandsFxCopRule", Justification = "System.Web.Mvc is SecurityTransparent and requires medium trust to run, so this downstream link demand is fine")] + public ViewTypeParserFilter() + { + } + + public override bool AllowCode + { + get { return true; } + } + + public override int NumberOfControlsAllowed + { + get { return -1; } + } + + public override int NumberOfDirectDependenciesAllowed + { + get { return -1; } + } + + public override int TotalNumberOfDependenciesAllowed + { + get { return -1; } + } + + [SuppressMessage("Microsoft.Security", "CA2141:TransparentMethodsMustNotSatisfyLinkDemandsFxCopRule", Justification = "System.Web.Mvc is SecurityTransparent and requires medium trust to run, so this downstream link demand is fine")] + public override void PreprocessDirective(string directiveName, IDictionary attributes) + { + base.PreprocessDirective(directiveName, attributes); + + Type baseType; + if (_directiveBaseTypeMappings.TryGetValue(directiveName, out baseType)) + { + string inheritsAttribute = attributes["inherits"] as string; + + // Since the ASP.NET page parser doesn't understand native generic syntax, we + // need to swap out whatever the user provided with the default base type for + // the given directive (page vs. control vs. master). We stash the old value + // and swap it back in inside the control builder. Our "is this generic?" + // check here really only works for C# and VB.NET, since we're checking for + // < or ( in the type name. + // + // We only change generic directives, because doing so breaks back-compat + // for property setters on @Page, @Control, and @Master directives. The user + // can work around this breaking behavior by using a non-generic inherits + // directive, or by using the CLR syntax for generic type names. + + if (inheritsAttribute != null && inheritsAttribute.IndexOfAny(new[] { '<', '(' }) > 0) + { + attributes["inherits"] = baseType.FullName; + _inherits = inheritsAttribute; + } + } + } + + [SuppressMessage("Microsoft.Security", "CA2141:TransparentMethodsMustNotSatisfyLinkDemandsFxCopRule", Justification = "System.Web.Mvc is SecurityTransparent and requires medium trust to run, so this downstream link demand is fine")] + public override void ParseComplete(ControlBuilder rootBuilder) + { + base.ParseComplete(rootBuilder); + + IMvcControlBuilder builder = rootBuilder as IMvcControlBuilder; + if (builder != null) + { + builder.Inherits = _inherits; + } + } + + // Everything else in this class is unrelated to our 'inherits' handling. + // Since PageParserFilter blocks everything by default, we need to unblock it + + public override bool AllowBaseType(Type baseType) + { + return true; + } + + public override bool AllowControl(Type controlType, ControlBuilder builder) + { + return true; + } + + public override bool AllowVirtualReference(string referenceVirtualPath, VirtualReferenceType referenceType) + { + return true; + } + + public override bool AllowServerSideInclude(string includeVirtualPath) + { + return true; + } + } +} diff --git a/src/System.Web.Mvc/ViewUserControl.cs b/src/System.Web.Mvc/ViewUserControl.cs new file mode 100644 index 00000000..a5f7e075 --- /dev/null +++ b/src/System.Web.Mvc/ViewUserControl.cs @@ -0,0 +1,213 @@ +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Web.Mvc.Properties; +using System.Web.UI; + +namespace System.Web.Mvc +{ + [FileLevelControlBuilder(typeof(ViewUserControlControlBuilder))] + public class ViewUserControl : UserControl, IViewDataContainer + { + private AjaxHelper<object> _ajaxHelper; + private DynamicViewDataDictionary _dynamicViewData; + private HtmlHelper<object> _htmlHelper; + private ViewContext _viewContext; + private ViewDataDictionary _viewData; + private string _viewDataKey; + + public AjaxHelper<object> Ajax + { + get + { + if (_ajaxHelper == null) + { + _ajaxHelper = new AjaxHelper<object>(ViewContext, this); + } + return _ajaxHelper; + } + } + + public HtmlHelper<object> Html + { + get + { + if (_htmlHelper == null) + { + _htmlHelper = new HtmlHelper<object>(ViewContext, this); + } + return _htmlHelper; + } + } + + public object Model + { + get { return ViewData.Model; } + } + + public TempDataDictionary TempData + { + get { return ViewPage.TempData; } + } + + public UrlHelper Url + { + get { return ViewPage.Url; } + } + + public dynamic ViewBag + { + get + { + if (_dynamicViewData == null) + { + _dynamicViewData = new DynamicViewDataDictionary(() => ViewData); + } + return _dynamicViewData; + } + } + + [Browsable(false)] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public ViewContext ViewContext + { + get { return _viewContext ?? ViewPage.ViewContext; } + set { _viewContext = value; } + } + + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This is the mechanism by which the ViewUserControl gets its ViewDataDictionary object.")] + [Browsable(false)] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public ViewDataDictionary ViewData + { + get + { + EnsureViewData(); + return _viewData; + } + set { SetViewData(value); } + } + + [DefaultValue("")] + public string ViewDataKey + { + get { return _viewDataKey ?? String.Empty; } + set { _viewDataKey = value; } + } + + internal ViewPage ViewPage + { + get + { + ViewPage viewPage = Page as ViewPage; + if (viewPage == null) + { + throw new InvalidOperationException(MvcResources.ViewUserControl_RequiresViewPage); + } + return viewPage; + } + } + + public HtmlTextWriter Writer + { + get { return ViewPage.Writer; } + } + + protected virtual void SetViewData(ViewDataDictionary viewData) + { + _viewData = viewData; + } + + protected void EnsureViewData() + { + if (_viewData != null) + { + return; + } + + // Get the ViewData for this ViewUserControl, optionally using the specified ViewDataKey + IViewDataContainer vdc = GetViewDataContainer(this); + if (vdc == null) + { + throw new InvalidOperationException( + String.Format( + CultureInfo.CurrentCulture, + MvcResources.ViewUserControl_RequiresViewDataProvider, + AppRelativeVirtualPath)); + } + + ViewDataDictionary myViewData = vdc.ViewData; + + // If we have a ViewDataKey, try to extract the ViewData from the dictionary, otherwise + // return the container's ViewData. + if (!String.IsNullOrEmpty(ViewDataKey)) + { + object target = myViewData.Eval(ViewDataKey); + myViewData = target as ViewDataDictionary ?? new ViewDataDictionary(myViewData) { Model = target }; + } + + SetViewData(myViewData); + } + + private static IViewDataContainer GetViewDataContainer(Control control) + { + // Walk up the control hierarchy until we find someone that implements IViewDataContainer + while (control != null) + { + control = control.Parent; + IViewDataContainer vdc = control as IViewDataContainer; + if (vdc != null) + { + return vdc; + } + } + return null; + } + + public virtual void RenderView(ViewContext viewContext) + { + using (ViewUserControlContainerPage containerPage = new ViewUserControlContainerPage(this)) + { + RenderViewAndRestoreContentType(containerPage, viewContext); + } + } + + internal static void RenderViewAndRestoreContentType(ViewPage containerPage, ViewContext viewContext) + { + // We need to restore the Content-Type since Page.SetIntrinsics() will reset it. It's not possible + // to work around the call to SetIntrinsics() since the control's render method requires the + // containing page's Response property to be non-null, and SetIntrinsics() is the only way to set + // this. + string savedContentType = viewContext.HttpContext.Response.ContentType; + containerPage.RenderView(viewContext); + viewContext.HttpContext.Response.ContentType = savedContentType; + } + + [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "textWriter", Justification = "This method existed in MVC 1.0 and has been deprecated.")] + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This method existed in MVC 1.0 and has been deprecated.")] + [Obsolete("The TextWriter is now provided by the ViewContext object passed to the RenderView method.", true /* error */)] + public void SetTextWriter(TextWriter textWriter) + { + // this is now a no-op + } + + private sealed class ViewUserControlContainerPage : ViewPage + { + private readonly ViewUserControl _userControl; + + public ViewUserControlContainerPage(ViewUserControl userControl) + { + _userControl = userControl; + } + + public override void ProcessRequest(HttpContext context) + { + _userControl.ID = NextId(); + Controls.Add(_userControl); + + base.ProcessRequest(context); + } + } + } +} diff --git a/src/System.Web.Mvc/ViewUserControlControlBuilder.cs b/src/System.Web.Mvc/ViewUserControlControlBuilder.cs new file mode 100644 index 00000000..e09e2982 --- /dev/null +++ b/src/System.Web.Mvc/ViewUserControlControlBuilder.cs @@ -0,0 +1,18 @@ +using System.CodeDom; +using System.Web.UI; + +namespace System.Web.Mvc +{ + internal sealed class ViewUserControlControlBuilder : FileLevelUserControlBuilder, IMvcControlBuilder + { + public string Inherits { get; set; } + + public override void ProcessGeneratedCode(CodeCompileUnit codeCompileUnit, CodeTypeDeclaration baseType, CodeTypeDeclaration derivedType, CodeMemberMethod buildMethod, CodeMemberMethod dataBindingMethod) + { + if (!String.IsNullOrWhiteSpace(Inherits)) + { + derivedType.BaseTypes[0] = new CodeTypeReference(Inherits); + } + } + } +} diff --git a/src/System.Web.Mvc/ViewUserControl`1.cs b/src/System.Web.Mvc/ViewUserControl`1.cs new file mode 100644 index 00000000..357888f4 --- /dev/null +++ b/src/System.Web.Mvc/ViewUserControl`1.cs @@ -0,0 +1,58 @@ +using System.Diagnostics.CodeAnalysis; + +namespace System.Web.Mvc +{ + public class ViewUserControl<TModel> : ViewUserControl + { + private AjaxHelper<TModel> _ajaxHelper; + private HtmlHelper<TModel> _htmlHelper; + private ViewDataDictionary<TModel> _viewData; + + public new AjaxHelper<TModel> Ajax + { + get + { + if (_ajaxHelper == null) + { + _ajaxHelper = new AjaxHelper<TModel>(ViewContext, this); + } + return _ajaxHelper; + } + } + + public new HtmlHelper<TModel> Html + { + get + { + if (_htmlHelper == null) + { + _htmlHelper = new HtmlHelper<TModel>(ViewContext, this); + } + return _htmlHelper; + } + } + + public new TModel Model + { + get { return ViewData.Model; } + } + + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This is settable for unit testing purposes")] + public new ViewDataDictionary<TModel> ViewData + { + get + { + EnsureViewData(); + return _viewData; + } + set { SetViewData(value); } + } + + protected override void SetViewData(ViewDataDictionary viewData) + { + _viewData = new ViewDataDictionary<TModel>(viewData); + + base.SetViewData(_viewData); + } + } +} diff --git a/src/System.Web.Mvc/VirtualPathProviderViewEngine.cs b/src/System.Web.Mvc/VirtualPathProviderViewEngine.cs new file mode 100644 index 00000000..9cf5d4d4 --- /dev/null +++ b/src/System.Web.Mvc/VirtualPathProviderViewEngine.cs @@ -0,0 +1,345 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Web.Hosting; +using System.Web.Mvc.Properties; +using System.Web.WebPages; + +namespace System.Web.Mvc +{ + public abstract class VirtualPathProviderViewEngine : IViewEngine + { + // format is ":ViewCacheEntry:{cacheType}:{prefix}:{name}:{controllerName}:{areaName}:" + private const string CacheKeyFormat = ":ViewCacheEntry:{0}:{1}:{2}:{3}:{4}:"; + private const string CacheKeyPrefixMaster = "Master"; + private const string CacheKeyPrefixPartial = "Partial"; + private const string CacheKeyPrefixView = "View"; + private static readonly string[] _emptyLocations = new string[0]; + private DisplayModeProvider _displayModeProvider; + + private VirtualPathProvider _vpp; + internal Func<string, string> GetExtensionThunk = VirtualPathUtility.GetExtension; + + protected VirtualPathProviderViewEngine() + { + if (HttpContext.Current == null || HttpContext.Current.IsDebuggingEnabled) + { + ViewLocationCache = DefaultViewLocationCache.Null; + } + else + { + ViewLocationCache = new DefaultViewLocationCache(); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "This is a shipped API")] + public string[] AreaMasterLocationFormats { get; set; } + + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "This is a shipped API")] + public string[] AreaPartialViewLocationFormats { get; set; } + + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "This is a shipped API")] + public string[] AreaViewLocationFormats { get; set; } + + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "This is a shipped API")] + public string[] FileExtensions { get; set; } + + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "This is a shipped API")] + public string[] MasterLocationFormats { get; set; } + + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "This is a shipped API")] + public string[] PartialViewLocationFormats { get; set; } + + public IViewLocationCache ViewLocationCache { get; set; } + + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "This is a shipped API")] + public string[] ViewLocationFormats { get; set; } + + protected VirtualPathProvider VirtualPathProvider + { + get + { + if (_vpp == null) + { + _vpp = HostingEnvironment.VirtualPathProvider; + } + return _vpp; + } + set { _vpp = value; } + } + + protected internal DisplayModeProvider DisplayModeProvider + { + get { return _displayModeProvider ?? DisplayModeProvider.Instance; } + set { _displayModeProvider = value; } + } + + private string CreateCacheKey(string prefix, string name, string controllerName, string areaName) + { + return String.Format(CultureInfo.InvariantCulture, CacheKeyFormat, + GetType().AssemblyQualifiedName, prefix, name, controllerName, areaName); + } + + internal static string AppendDisplayModeToCacheKey(string cacheKey, string displayMode) + { + // key format is ":ViewCacheEntry:{cacheType}:{prefix}:{name}:{controllerName}:{areaName}:" + // so append "{displayMode}:" to the key + return cacheKey + displayMode + ":"; + } + + protected abstract IView CreatePartialView(ControllerContext controllerContext, string partialPath); + + protected abstract IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath); + + protected virtual bool FileExists(ControllerContext controllerContext, string virtualPath) + { + return VirtualPathProvider.FileExists(virtualPath); + } + + public virtual ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache) + { + if (controllerContext == null) + { + throw new ArgumentNullException("controllerContext"); + } + if (String.IsNullOrEmpty(partialViewName)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "partialViewName"); + } + + string[] searched; + string controllerName = controllerContext.RouteData.GetRequiredString("controller"); + string partialPath = GetPath(controllerContext, PartialViewLocationFormats, AreaPartialViewLocationFormats, "PartialViewLocationFormats", partialViewName, controllerName, CacheKeyPrefixPartial, useCache, out searched); + + if (String.IsNullOrEmpty(partialPath)) + { + return new ViewEngineResult(searched); + } + + return new ViewEngineResult(CreatePartialView(controllerContext, partialPath), this); + } + + public virtual ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache) + { + if (controllerContext == null) + { + throw new ArgumentNullException("controllerContext"); + } + if (String.IsNullOrEmpty(viewName)) + { + throw new ArgumentException(MvcResources.Common_NullOrEmpty, "viewName"); + } + + string[] viewLocationsSearched; + string[] masterLocationsSearched; + + string controllerName = controllerContext.RouteData.GetRequiredString("controller"); + string viewPath = GetPath(controllerContext, ViewLocationFormats, AreaViewLocationFormats, "ViewLocationFormats", viewName, controllerName, CacheKeyPrefixView, useCache, out viewLocationsSearched); + string masterPath = GetPath(controllerContext, MasterLocationFormats, AreaMasterLocationFormats, "MasterLocationFormats", masterName, controllerName, CacheKeyPrefixMaster, useCache, out masterLocationsSearched); + + if (String.IsNullOrEmpty(viewPath) || (String.IsNullOrEmpty(masterPath) && !String.IsNullOrEmpty(masterName))) + { + return new ViewEngineResult(viewLocationsSearched.Union(masterLocationsSearched)); + } + + return new ViewEngineResult(CreateView(controllerContext, viewPath, masterPath), this); + } + + private string GetPath(ControllerContext controllerContext, string[] locations, string[] areaLocations, string locationsPropertyName, string name, string controllerName, string cacheKeyPrefix, bool useCache, out string[] searchedLocations) + { + searchedLocations = _emptyLocations; + + if (String.IsNullOrEmpty(name)) + { + return String.Empty; + } + + string areaName = AreaHelpers.GetAreaName(controllerContext.RouteData); + bool usingAreas = !String.IsNullOrEmpty(areaName); + List<ViewLocation> viewLocations = GetViewLocations(locations, (usingAreas) ? areaLocations : null); + + if (viewLocations.Count == 0) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, + MvcResources.Common_PropertyCannotBeNullOrEmpty, locationsPropertyName)); + } + + bool nameRepresentsPath = IsSpecificPath(name); + string cacheKey = CreateCacheKey(cacheKeyPrefix, name, (nameRepresentsPath) ? String.Empty : controllerName, areaName); + + if (useCache) + { + // Only look at cached display modes that can handle the context. + IEnumerable<IDisplayMode> possibleDisplayModes = DisplayModeProvider.GetAvailableDisplayModesForContext(controllerContext.HttpContext, controllerContext.DisplayMode); + foreach (IDisplayMode displayMode in possibleDisplayModes) + { + string cachedLocation = ViewLocationCache.GetViewLocation(controllerContext.HttpContext, AppendDisplayModeToCacheKey(cacheKey, displayMode.DisplayModeId)); + + if (cachedLocation != null) + { + if (controllerContext.DisplayMode == null) + { + controllerContext.DisplayMode = displayMode; + } + + return cachedLocation; + } + } + + // GetPath is called again without using the cache. + return null; + } + else + { + return nameRepresentsPath + ? GetPathFromSpecificName(controllerContext, name, cacheKey, ref searchedLocations) + : GetPathFromGeneralName(controllerContext, viewLocations, name, controllerName, areaName, cacheKey, ref searchedLocations); + } + } + + private string GetPathFromGeneralName(ControllerContext controllerContext, List<ViewLocation> locations, string name, string controllerName, string areaName, string cacheKey, ref string[] searchedLocations) + { + string result = String.Empty; + searchedLocations = new string[locations.Count]; + + for (int i = 0; i < locations.Count; i++) + { + ViewLocation location = locations[i]; + string virtualPath = location.Format(name, controllerName, areaName); + DisplayInfo virtualPathDisplayInfo = DisplayModeProvider.GetDisplayInfoForVirtualPath(virtualPath, controllerContext.HttpContext, path => FileExists(controllerContext, path), controllerContext.DisplayMode); + + if (virtualPathDisplayInfo != null) + { + string resolvedVirtualPath = virtualPathDisplayInfo.FilePath; + + searchedLocations = _emptyLocations; + result = resolvedVirtualPath; + ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, AppendDisplayModeToCacheKey(cacheKey, virtualPathDisplayInfo.DisplayMode.DisplayModeId), result); + + if (controllerContext.DisplayMode == null) + { + controllerContext.DisplayMode = virtualPathDisplayInfo.DisplayMode; + } + + // Populate the cache with the existing paths returned by all display modes. + // Since we currently don't keep track of cache misses, if we cache view.aspx on a request from a standard browser + // we don't want a cache hit for view.aspx from a mobile browser so we populate the cache with view.Mobile.aspx. + IEnumerable<IDisplayMode> allDisplayModes = DisplayModeProvider.Modes; + foreach (IDisplayMode displayMode in allDisplayModes) + { + if (displayMode.DisplayModeId != virtualPathDisplayInfo.DisplayMode.DisplayModeId) + { + DisplayInfo displayInfoToCache = displayMode.GetDisplayInfo(controllerContext.HttpContext, virtualPath, virtualPathExists: path => FileExists(controllerContext, path)); + + if (displayInfoToCache != null && displayInfoToCache.FilePath != null) + { + ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, AppendDisplayModeToCacheKey(cacheKey, displayInfoToCache.DisplayMode.DisplayModeId), displayInfoToCache.FilePath); + } + } + } + break; + } + + searchedLocations[i] = virtualPath; + } + + return result; + } + + private string GetPathFromSpecificName(ControllerContext controllerContext, string name, string cacheKey, ref string[] searchedLocations) + { + string result = name; + + if (!(FilePathIsSupported(name) && FileExists(controllerContext, name))) + { + result = String.Empty; + searchedLocations = new[] { name }; + } + + ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, cacheKey, result); + return result; + } + + private bool FilePathIsSupported(string virtualPath) + { + if (FileExtensions == null) + { + // legacy behavior for custom ViewEngine that might not set the FileExtensions property + return true; + } + else + { + // get rid of the '.' because the FileExtensions property expects extensions withouth a dot. + string extension = GetExtensionThunk(virtualPath).TrimStart('.'); + return FileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase); + } + } + + private static List<ViewLocation> GetViewLocations(string[] viewLocationFormats, string[] areaViewLocationFormats) + { + List<ViewLocation> allLocations = new List<ViewLocation>(); + + if (areaViewLocationFormats != null) + { + foreach (string areaViewLocationFormat in areaViewLocationFormats) + { + allLocations.Add(new AreaAwareViewLocation(areaViewLocationFormat)); + } + } + + if (viewLocationFormats != null) + { + foreach (string viewLocationFormat in viewLocationFormats) + { + allLocations.Add(new ViewLocation(viewLocationFormat)); + } + } + + return allLocations; + } + + private static bool IsSpecificPath(string name) + { + char c = name[0]; + return (c == '~' || c == '/'); + } + + public virtual void ReleaseView(ControllerContext controllerContext, IView view) + { + IDisposable disposable = view as IDisposable; + if (disposable != null) + { + disposable.Dispose(); + } + } + + private class AreaAwareViewLocation : ViewLocation + { + public AreaAwareViewLocation(string virtualPathFormatString) + : base(virtualPathFormatString) + { + } + + public override string Format(string viewName, string controllerName, string areaName) + { + return String.Format(CultureInfo.InvariantCulture, _virtualPathFormatString, viewName, controllerName, areaName); + } + } + + private class ViewLocation + { + protected string _virtualPathFormatString; + + public ViewLocation(string virtualPathFormatString) + { + _virtualPathFormatString = virtualPathFormatString; + } + + public virtual string Format(string viewName, string controllerName, string areaName) + { + return String.Format(CultureInfo.InvariantCulture, _virtualPathFormatString, viewName, controllerName); + } + } + } +} diff --git a/src/System.Web.Mvc/WebFormView.cs b/src/System.Web.Mvc/WebFormView.cs new file mode 100644 index 00000000..d8651656 --- /dev/null +++ b/src/System.Web.Mvc/WebFormView.cs @@ -0,0 +1,72 @@ +using System.Globalization; +using System.IO; +using System.Web.Mvc.Properties; + +namespace System.Web.Mvc +{ + public class WebFormView : BuildManagerCompiledView + { + public WebFormView(ControllerContext controllerContext, string viewPath) + : this(controllerContext, viewPath, null, null) + { + } + + public WebFormView(ControllerContext controllerContext, string viewPath, string masterPath) + : this(controllerContext, viewPath, masterPath, null) + { + } + + public WebFormView(ControllerContext controllerContext, string viewPath, string masterPath, IViewPageActivator viewPageActivator) + : base(controllerContext, viewPath, viewPageActivator) + { + MasterPath = masterPath ?? String.Empty; + } + + public string MasterPath { get; private set; } + + protected override void RenderView(ViewContext viewContext, TextWriter writer, object instance) + { + ViewPage viewPage = instance as ViewPage; + if (viewPage != null) + { + RenderViewPage(viewContext, viewPage); + return; + } + + ViewUserControl viewUserControl = instance as ViewUserControl; + if (viewUserControl != null) + { + RenderViewUserControl(viewContext, viewUserControl); + return; + } + + throw new InvalidOperationException( + String.Format( + CultureInfo.CurrentCulture, + MvcResources.WebFormViewEngine_WrongViewBase, + ViewPath)); + } + + private void RenderViewPage(ViewContext context, ViewPage page) + { + if (!String.IsNullOrEmpty(MasterPath)) + { + page.MasterLocation = MasterPath; + } + + page.ViewData = context.ViewData; + page.RenderView(context); + } + + private void RenderViewUserControl(ViewContext context, ViewUserControl control) + { + if (!String.IsNullOrEmpty(MasterPath)) + { + throw new InvalidOperationException(MvcResources.WebFormViewEngine_UserControlCannotHaveMaster); + } + + control.ViewData = context.ViewData; + control.RenderView(context); + } + } +} diff --git a/src/System.Web.Mvc/WebFormViewEngine.cs b/src/System.Web.Mvc/WebFormViewEngine.cs new file mode 100644 index 00000000..2827e736 --- /dev/null +++ b/src/System.Web.Mvc/WebFormViewEngine.cs @@ -0,0 +1,62 @@ +namespace System.Web.Mvc +{ + public class WebFormViewEngine : BuildManagerViewEngine + { + public WebFormViewEngine() + : this(null) + { + } + + public WebFormViewEngine(IViewPageActivator viewPageActivator) + : base(viewPageActivator) + { + MasterLocationFormats = new[] + { + "~/Views/{1}/{0}.master", + "~/Views/Shared/{0}.master" + }; + + AreaMasterLocationFormats = new[] + { + "~/Areas/{2}/Views/{1}/{0}.master", + "~/Areas/{2}/Views/Shared/{0}.master", + }; + + ViewLocationFormats = new[] + { + "~/Views/{1}/{0}.aspx", + "~/Views/{1}/{0}.ascx", + "~/Views/Shared/{0}.aspx", + "~/Views/Shared/{0}.ascx" + }; + + AreaViewLocationFormats = new[] + { + "~/Areas/{2}/Views/{1}/{0}.aspx", + "~/Areas/{2}/Views/{1}/{0}.ascx", + "~/Areas/{2}/Views/Shared/{0}.aspx", + "~/Areas/{2}/Views/Shared/{0}.ascx", + }; + + PartialViewLocationFormats = ViewLocationFormats; + AreaPartialViewLocationFormats = AreaViewLocationFormats; + + FileExtensions = new[] + { + "aspx", + "ascx", + "master", + }; + } + + protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath) + { + return new WebFormView(controllerContext, partialPath, null, ViewPageActivator); + } + + protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath) + { + return new WebFormView(controllerContext, viewPath, masterPath, ViewPageActivator); + } + } +} diff --git a/src/System.Web.Mvc/WebViewPage.cs b/src/System.Web.Mvc/WebViewPage.cs new file mode 100644 index 00000000..3799c602 --- /dev/null +++ b/src/System.Web.Mvc/WebViewPage.cs @@ -0,0 +1,115 @@ +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Web.Mvc.Properties; +using System.Web.WebPages; + +namespace System.Web.Mvc +{ + public abstract class WebViewPage : WebPageBase, IViewDataContainer, IViewStartPageChild + { + private ViewDataDictionary _viewData; + private DynamicViewDataDictionary _dynamicViewData; + private HttpContextBase _context; + + public AjaxHelper<object> Ajax { get; set; } + + public override HttpContextBase Context + { + // REVIEW why are we forced to override this? + get { return _context ?? ViewContext.HttpContext; } + set { _context = value; } + } + + public HtmlHelper<object> Html { get; set; } + + public object Model + { + get { return ViewData.Model; } + } + + internal string OverridenLayoutPath { get; set; } + + public TempDataDictionary TempData + { + get { return ViewContext.TempData; } + } + + public UrlHelper Url { get; set; } + + public dynamic ViewBag + { + get + { + if (_dynamicViewData == null) + { + _dynamicViewData = new DynamicViewDataDictionary(() => ViewData); + } + return _dynamicViewData; + } + } + + public ViewContext ViewContext { get; set; } + + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This is the mechanism by which the ViewPage gets its ViewDataDictionary object.")] + public ViewDataDictionary ViewData + { + get + { + if (_viewData == null) + { + SetViewData(new ViewDataDictionary()); + } + return _viewData; + } + set { SetViewData(value); } + } + + protected override void ConfigurePage(WebPageBase parentPage) + { + var baseViewPage = parentPage as WebViewPage; + if (baseViewPage == null) + { + // TODO : review if this check is even necessary. + // When this method is called by the framework parentPage should already be an instance of WebViewPage + // Need to review what happens if this method gets called in Plan9 pointing at an MVC view + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, MvcResources.CshtmlView_WrongViewBase, parentPage.VirtualPath)); + } + + // Set ViewContext and ViewData here so that the layout page inherits ViewData from the main page + ViewContext = baseViewPage.ViewContext; + ViewData = baseViewPage.ViewData; + InitHelpers(); + } + + public override void ExecutePageHierarchy() + { + // Change the Writer so that things like Html.BeginForm work correctly + TextWriter oldWriter = ViewContext.Writer; + ViewContext.Writer = Output; + + base.ExecutePageHierarchy(); + + // Overwrite LayoutPage so that returning a view with a custom master page works. + if (!String.IsNullOrEmpty(OverridenLayoutPath)) + { + Layout = OverridenLayoutPath; + } + + // Restore the old View Context Writer + ViewContext.Writer = oldWriter; + } + + public virtual void InitHelpers() + { + Ajax = new AjaxHelper<object>(ViewContext, this); + Html = new HtmlHelper<object>(ViewContext, this); + Url = new UrlHelper(ViewContext.RequestContext); + } + + protected virtual void SetViewData(ViewDataDictionary viewData) + { + _viewData = viewData; + } + } +} diff --git a/src/System.Web.Mvc/WebViewPage`1.cs b/src/System.Web.Mvc/WebViewPage`1.cs new file mode 100644 index 00000000..1b25f6b4 --- /dev/null +++ b/src/System.Web.Mvc/WebViewPage`1.cs @@ -0,0 +1,47 @@ +using System.Diagnostics.CodeAnalysis; + +namespace System.Web.Mvc +{ + public abstract class WebViewPage<TModel> : WebViewPage + { + private ViewDataDictionary<TModel> _viewData; + + public new AjaxHelper<TModel> Ajax { get; set; } + + public new HtmlHelper<TModel> Html { get; set; } + + public new TModel Model + { + get { return ViewData.Model; } + } + + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This is the mechanism by which the ViewPage gets its ViewDataDictionary object.")] + public new ViewDataDictionary<TModel> ViewData + { + get + { + if (_viewData == null) + { + SetViewData(new ViewDataDictionary<TModel>()); + } + return _viewData; + } + set { SetViewData(value); } + } + + public override void InitHelpers() + { + base.InitHelpers(); + + Ajax = new AjaxHelper<TModel>(ViewContext, this); + Html = new HtmlHelper<TModel>(ViewContext, this); + } + + protected override void SetViewData(ViewDataDictionary viewData) + { + _viewData = new ViewDataDictionary<TModel>(viewData); + + base.SetViewData(_viewData); + } + } +} diff --git a/src/System.Web.Mvc/packages.config b/src/System.Web.Mvc/packages.config new file mode 100644 index 00000000..f143a04f --- /dev/null +++ b/src/System.Web.Mvc/packages.config @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<packages> + <package id="Microsoft.Web.Infrastructure" version="1.0.0.0" /> +</packages>
\ No newline at end of file |