From b51fd08bab86038ff0c348d3162e98730d5a4685 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 4 Feb 2014 16:33:03 -0800 Subject: [PATCH] Update ViewDataOfT.SetModel to check for type compatibility when setting model instance. --- WebFx.sln | 11 +- .../Internal/TypeExtensions.cs | 23 ++++ .../Resources.Designer.cs | 81 +++++++++++ .../Resources.resx | 126 ++++++++++++++++++ .../ViewContext.cs | 2 +- .../ViewData.cs | 7 +- .../ViewDataOfTModel.cs | 29 +++- src/Microsoft.AspNet.Mvc.Razor/RazorView.cs | 1 + .../RazorViewOfT.cs | 1 + .../Html/HtmlHelper.cs | 1 + .../Html/HtmlHelperOfT.cs | 1 + .../View/IView.cs | 1 + .../ActionResultHelper.cs | 1 + .../ActionResults/ViewResult.cs | 1 + src/Microsoft.AspNet.Mvc/Controller.cs | 1 + .../IActionResultHelper.cs | 3 +- .../ViewDataOfTTest.cs | 51 +++++++ .../ViewDataTest.cs | 16 +++ .../project.json | 12 ++ 19 files changed, 361 insertions(+), 8 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Internal/TypeExtensions.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Resources.Designer.cs create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Resources.resx create mode 100644 test/Microsoft.AspNet.Mvc.ModelBinding.Test/ViewDataOfTTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.ModelBinding.Test/ViewDataTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.ModelBinding.Test/project.json diff --git a/WebFx.sln b/WebFx.sln index 2b2692e7c1..2aefd06076 100644 --- a/WebFx.sln +++ b/WebFx.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 2013 -VisualStudioVersion = 12.0.21005.1 +VisualStudioVersion = 12.0.30110.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}" EndProject @@ -39,6 +39,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "net45", "net45", "{222CA408 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "k10", "k10", "{CE037E26-9EB5-48E2-B73B-06C6FF6CC9F5}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.Mvc.ModelBinding.Test.net45", "test\Microsoft.AspNet.Mvc.ModelBinding.Test\Microsoft.AspNet.Mvc.ModelBinding.Test.net45.csproj", "{42195A56-42C0-4CFF-A982-B6E24EFC6356}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -93,6 +97,10 @@ Global {236CDB04-8FDA-4152-9A5B-7F98C19C663A}.Debug|Any CPU.Build.0 = Debug|Any CPU {236CDB04-8FDA-4152-9A5B-7F98C19C663A}.Release|Any CPU.ActiveCfg = Release|Any CPU {236CDB04-8FDA-4152-9A5B-7F98C19C663A}.Release|Any CPU.Build.0 = Release|Any CPU + {42195A56-42C0-4CFF-A982-B6E24EFC6356}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {42195A56-42C0-4CFF-A982-B6E24EFC6356}.Debug|Any CPU.Build.0 = Debug|Any CPU + {42195A56-42C0-4CFF-A982-B6E24EFC6356}.Release|Any CPU.ActiveCfg = Release|Any CPU + {42195A56-42C0-4CFF-A982-B6E24EFC6356}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -114,5 +122,6 @@ Global {BA88E212-5889-48DC-823F-A3A67DDEF123} = {4EB70D0E-E27E-4C42-AB58-BC8B325EDFB3} {501817DD-8143-4A50-888D-99896A82CD12} = {222CA408-93EE-473A-9325-D04989EC9FEF} {A7D7CD66-A407-4144-8AB7-07F895F87137} = {CE037E26-9EB5-48E2-B73B-06C6FF6CC9F5} + {42195A56-42C0-4CFF-A982-B6E24EFC6356} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} EndGlobalSection EndGlobal diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/TypeExtensions.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/TypeExtensions.cs new file mode 100644 index 0000000000..96086d2c19 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/TypeExtensions.cs @@ -0,0 +1,23 @@ +using System; +using System.Reflection; + +namespace Microsoft.AspNet.Mvc.ModelBinding.Internal +{ + public static class TypeExtensions + { + public static bool IsCompatibleObject(this object value) + { + return (value is T || (value == null && TypeAllowsNullValue(typeof(T)))); + } + + public static bool IsNullableValueType(this Type type) + { + return Nullable.GetUnderlyingType(type) != null; + } + + public static bool TypeAllowsNullValue(this Type type) + { + return (!type.GetTypeInfo().IsValueType || IsNullableValueType(type)); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Resources.Designer.cs new file mode 100644 index 0000000000..9bd504bf3c --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Resources.Designer.cs @@ -0,0 +1,81 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.34003 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.AspNet.Mvc.ModelBinding { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // 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 Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [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("Microsoft.AspNet.Mvc.ModelBinding.Resources", System.Reflection.IntrospectionExtensions.GetTypeInfo(typeof(Resources)).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The model item passed into the ViewData is of type '{0}', but this ViewData instance requires a model item of type '{1}'.. + /// + internal static string ViewData_WrongTModelType { + get { + return ResourceManager.GetString("ViewData_WrongTModelType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The model item passed into the ViewData is null, but this ViewData instance requires a non-null model item of type '{0}'.. + /// + internal static string ViewDataDictionary_ModelCannotBeNull { + get { + return ResourceManager.GetString("ViewDataDictionary_ModelCannotBeNull", resourceCulture); + } + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Resources.resx b/src/Microsoft.AspNet.Mvc.ModelBinding/Resources.resx new file mode 100644 index 0000000000..2a5225f9e2 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Resources.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The model item passed is null, but this ViewData instance requires a non-null model item of type '{0}'. + + + The model item passed into the ViewData is of type '{0}', but this ViewData instance requires a model item of type '{1}'. + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/ViewContext.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/ViewContext.cs index 06b4ac6bfc..00574dfce9 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/ViewContext.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/ViewContext.cs @@ -2,7 +2,7 @@ using Microsoft.AspNet.Abstractions; using Microsoft.AspNet.Mvc.Routing; -namespace Microsoft.AspNet.Mvc +namespace Microsoft.AspNet.Mvc.ModelBinding { public class ViewContext : RequestContext { diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/ViewData.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/ViewData.cs index 7254075056..94b0aa91f0 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/ViewData.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/ViewData.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Dynamic; -namespace Microsoft.AspNet.Mvc +namespace Microsoft.AspNet.Mvc.ModelBinding { public class ViewData : DynamicObject { @@ -16,6 +16,11 @@ namespace Microsoft.AspNet.Mvc public ViewData(ViewData source) { + if (source == null) + { + throw new ArgumentNullException("source"); + } + _data = source._data; SetModel(source.Model); } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/ViewDataOfTModel.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/ViewDataOfTModel.cs index e2c99eddc0..ff1c07054a 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/ViewDataOfTModel.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/ViewDataOfTModel.cs @@ -1,5 +1,8 @@ - -namespace Microsoft.AspNet.Mvc +using System; +using System.Globalization; +using Microsoft.AspNet.Mvc.ModelBinding.Internal; + +namespace Microsoft.AspNet.Mvc.ModelBinding { public class ViewData : ViewData { @@ -21,8 +24,26 @@ namespace Microsoft.AspNet.Mvc protected override void SetModel(object value) { - // TODO: Add checks for cast - base.SetModel((TModel)value); + // IsCompatibleObject verifies if the value is either an instance of TModel or if value happens to be null that TModel is nullable type. + bool castWillSucceed = value.IsCompatibleObject(); + + if (castWillSucceed) + { + base.SetModel(value); + } + else + { + string message; + if (value == null) + { + message = String.Format(CultureInfo.CurrentCulture, Resources.ViewDataDictionary_ModelCannotBeNull, typeof(TModel)); + } + else + { + message = String.Format(CultureInfo.CurrentCulture, Resources.ViewData_WrongTModelType, value.GetType(), typeof(TModel)); + } + throw new InvalidOperationException(message); + } } } } diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs index 142fa07391..acc9fef15f 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorView.cs @@ -6,6 +6,7 @@ using System.Text; using System.Threading.Tasks; using Microsoft.AspNet.Abstractions; using Microsoft.AspNet.DependencyInjection; +using Microsoft.AspNet.Mvc.ModelBinding; namespace Microsoft.AspNet.Mvc.Razor { diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorViewOfT.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorViewOfT.cs index 6c85992183..b979834035 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorViewOfT.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorViewOfT.cs @@ -1,5 +1,6 @@ using System.IO; using System.Threading.Tasks; +using Microsoft.AspNet.Mvc.ModelBinding; namespace Microsoft.AspNet.Mvc.Razor { diff --git a/src/Microsoft.AspNet.Mvc.Rendering/Html/HtmlHelper.cs b/src/Microsoft.AspNet.Mvc.Rendering/Html/HtmlHelper.cs index 91d2588804..2c9fee03a9 100644 --- a/src/Microsoft.AspNet.Mvc.Rendering/Html/HtmlHelper.cs +++ b/src/Microsoft.AspNet.Mvc.Rendering/Html/HtmlHelper.cs @@ -8,6 +8,7 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Net; using System.Text; +using Microsoft.AspNet.Mvc.ModelBinding; namespace Microsoft.AspNet.Mvc { diff --git a/src/Microsoft.AspNet.Mvc.Rendering/Html/HtmlHelperOfT.cs b/src/Microsoft.AspNet.Mvc.Rendering/Html/HtmlHelperOfT.cs index f9e8d24a31..eb330ced60 100644 --- a/src/Microsoft.AspNet.Mvc.Rendering/Html/HtmlHelperOfT.cs +++ b/src/Microsoft.AspNet.Mvc.Rendering/Html/HtmlHelperOfT.cs @@ -1,4 +1,5 @@ using System; +using Microsoft.AspNet.Mvc.ModelBinding; namespace Microsoft.AspNet.Mvc { diff --git a/src/Microsoft.AspNet.Mvc.Rendering/View/IView.cs b/src/Microsoft.AspNet.Mvc.Rendering/View/IView.cs index f0ca1f9372..c7d6f22a58 100644 --- a/src/Microsoft.AspNet.Mvc.Rendering/View/IView.cs +++ b/src/Microsoft.AspNet.Mvc.Rendering/View/IView.cs @@ -1,5 +1,6 @@ using System.IO; using System.Threading.Tasks; +using Microsoft.AspNet.Mvc.ModelBinding; namespace Microsoft.AspNet.Mvc { diff --git a/src/Microsoft.AspNet.Mvc/ActionResultHelper.cs b/src/Microsoft.AspNet.Mvc/ActionResultHelper.cs index d23c6b99df..8fa66787d8 100644 --- a/src/Microsoft.AspNet.Mvc/ActionResultHelper.cs +++ b/src/Microsoft.AspNet.Mvc/ActionResultHelper.cs @@ -1,4 +1,5 @@ using System; +using Microsoft.AspNet.Mvc.ModelBinding; namespace Microsoft.AspNet.Mvc { diff --git a/src/Microsoft.AspNet.Mvc/ActionResults/ViewResult.cs b/src/Microsoft.AspNet.Mvc/ActionResults/ViewResult.cs index 314d4142ff..55a84d59a6 100644 --- a/src/Microsoft.AspNet.Mvc/ActionResults/ViewResult.cs +++ b/src/Microsoft.AspNet.Mvc/ActionResults/ViewResult.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.IO; using System.Text; using System.Threading.Tasks; +using Microsoft.AspNet.Mvc.ModelBinding; namespace Microsoft.AspNet.Mvc { diff --git a/src/Microsoft.AspNet.Mvc/Controller.cs b/src/Microsoft.AspNet.Mvc/Controller.cs index fa64596870..f16aa5fb66 100644 --- a/src/Microsoft.AspNet.Mvc/Controller.cs +++ b/src/Microsoft.AspNet.Mvc/Controller.cs @@ -1,4 +1,5 @@ using Microsoft.AspNet.Abstractions; +using Microsoft.AspNet.Mvc.ModelBinding; namespace Microsoft.AspNet.Mvc { diff --git a/src/Microsoft.AspNet.Mvc/IActionResultHelper.cs b/src/Microsoft.AspNet.Mvc/IActionResultHelper.cs index 29d3835328..41dfef7e20 100644 --- a/src/Microsoft.AspNet.Mvc/IActionResultHelper.cs +++ b/src/Microsoft.AspNet.Mvc/IActionResultHelper.cs @@ -1,4 +1,5 @@ - +using Microsoft.AspNet.Mvc.ModelBinding; + namespace Microsoft.AspNet.Mvc { public interface IActionResultHelper diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ViewDataOfTTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ViewDataOfTTest.cs new file mode 100644 index 0000000000..eabb75bd4b --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ViewDataOfTTest.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNet.Mvc.ModelBinding.Test +{ + public class ViewDataOfTTest + { + [Fact] + public void SettingModelThrowsIfTheModelIsNull() + { + // Arrange + var viewDataOfT = new ViewData(); + ViewData viewData = viewDataOfT; + + // Act and Assert + Exception ex = Assert.Throws(() => viewData.Model = null); + Assert.Equal("The model item passed is null, but this ViewData instance requires a non-null model item of type 'System.Int32'.", ex.Message); + } + + [Fact] + public void SettingModelThrowsIfTheModelIsIncompatible() + { + // Arrange + var viewDataOfT = new ViewData(); + ViewData viewData = viewDataOfT; + + // Act and Assert + Exception ex = Assert.Throws(() => viewData.Model = DateTime.UtcNow); + Assert.Equal("The model item passed into the ViewData is of type 'System.DateTime', but this ViewData instance requires a model item of type 'System.String'.", ex.Message); + } + + [Fact] + public void SettingModelWorksForCompatibleTypes() + { + // Arrange + string value = "some value"; + var viewDataOfT = new ViewData(); + ViewData viewData = viewDataOfT; + + // Act + viewData.Model = value; + + // Assert + Assert.Same(value, viewDataOfT.Model); + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ViewDataTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ViewDataTest.cs new file mode 100644 index 0000000000..13e2d0ae80 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ViewDataTest.cs @@ -0,0 +1,16 @@ +using System; +using Xunit; + +namespace Microsoft.AspNet.Mvc.ModelBinding.Test +{ + public class ViewDataDictionaryTest + { + [Fact] + public void ConstructorThrowsIfParameterIsNull() + { + // Act & Assert + ArgumentNullException ex = Assert.Throws(() => new ViewData(source: null)); + Assert.Equal("source", ex.ParamName); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/project.json b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/project.json new file mode 100644 index 0000000000..f011cf2948 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/project.json @@ -0,0 +1,12 @@ +{ + "version" : "0.1-alpha-*", + "dependencies": { + "Microsoft.AspNet.Mvc.ModelBinding" : "0.1-alpha-*", + "Moq": "4.0.10827", + "Xunit": "1.9.1", + "Xunit.extensions": "1.9.1" + }, + "configurations": { + "net45": { } + } +} \ No newline at end of file