diff --git a/samples/MvcSample.Web/Controllers/FormUrlEncodedController.cs b/samples/MvcSample.Web/Controllers/FormUrlEncodedController.cs new file mode 100644 index 0000000000..a9b696ae74 --- /dev/null +++ b/samples/MvcSample.Web/Controllers/FormUrlEncodedController.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using Microsoft.AspNet.Mvc; + +namespace MvcSample.Web.Controllers +{ + public class FormUrlEncodedController : Controller + { + [Route("[controller]")] + public IActionResult Index() + { + return View(); + } + + [Route("[controller]/[action]")] + public bool IsValidPerson(Person person) + { + return ModelState.IsValid && person.PastJobs.Any(); + } + } +} \ No newline at end of file diff --git a/samples/MvcSample.Web/Models/Address.cs b/samples/MvcSample.Web/Models/Address.cs new file mode 100644 index 0000000000..0614d6c06e --- /dev/null +++ b/samples/MvcSample.Web/Models/Address.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.ComponentModel.DataAnnotations; + +namespace MvcSample.Web +{ + public class Address + { + [Required] + public string Street { get; set; } + + [Required] + public string City { get; set; } + + [Required] + public string State { get; set; } + + [Required] + public int ZipCode { get; set; } + } +} \ No newline at end of file diff --git a/samples/MvcSample.Web/Models/Job.cs b/samples/MvcSample.Web/Models/Job.cs new file mode 100644 index 0000000000..e613b508a4 --- /dev/null +++ b/samples/MvcSample.Web/Models/Job.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.ComponentModel.DataAnnotations; + +namespace MvcSample.Web +{ + public class Job + { + [Required] + public string JobTitle { get; set; } + + [Required] + public string EmployerName { get; set; } + + [Required] + public int Years { get; set; } + } +} \ No newline at end of file diff --git a/samples/MvcSample.Web/Models/Person.cs b/samples/MvcSample.Web/Models/Person.cs new file mode 100644 index 0000000000..70e531d4cb --- /dev/null +++ b/samples/MvcSample.Web/Models/Person.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace MvcSample.Web +{ + public class Person + { + [Required] + public string Name { get; set; } + + [Required] + public Address Address { get; set; } + + [Required] + public IEnumerable PastJobs { get; set; } + } +} \ No newline at end of file diff --git a/samples/MvcSample.Web/Views/FormUrlEncoded/Index.cshtml b/samples/MvcSample.Web/Views/FormUrlEncoded/Index.cshtml new file mode 100644 index 0000000000..62a0d42ad2 --- /dev/null +++ b/samples/MvcSample.Web/Views/FormUrlEncoded/Index.cshtml @@ -0,0 +1,58 @@ + + + + + + JQuery FormUrlEncoded example + + + + + +

+

+ + diff --git a/samples/MvcSample.Web/bower.json b/samples/MvcSample.Web/bower.json new file mode 100644 index 0000000000..6805ab323f --- /dev/null +++ b/samples/MvcSample.Web/bower.json @@ -0,0 +1,13 @@ +{ + "name": "MvcSample.Web", + "description": "Web site demonstrating various MVC features.", + "private": true, + "dependencies": { + "jquery": "1.11.2" + }, + "exportsOverride": { + "jquery": { + "": "dist/jquery.{js,min.js,min.map}" + } + } +} diff --git a/samples/MvcSample.Web/gruntfile.js b/samples/MvcSample.Web/gruntfile.js new file mode 100644 index 0000000000..34c745b143 --- /dev/null +++ b/samples/MvcSample.Web/gruntfile.js @@ -0,0 +1,21 @@ + +module.exports = function (grunt) { + grunt.initConfig({ + bower: { + install: { + options: { + targetDir: "wwwroot/lib", + layout: "byComponent", + cleanTargetDir: false + } + } + } + }); + + // This command registers the default task which will install bower packages into wwwroot/lib + grunt.registerTask("default", ["bower:install"]); + + // The following line loads the grunt plugins. + // This line needs to be at the end of this this file. + grunt.loadNpmTasks("grunt-bower-task"); +}; \ No newline at end of file diff --git a/samples/MvcSample.Web/package.json b/samples/MvcSample.Web/package.json new file mode 100644 index 0000000000..13d5d39599 --- /dev/null +++ b/samples/MvcSample.Web/package.json @@ -0,0 +1,10 @@ +{ + "version": "0.0.0", + "name": "MvcSample.Web", + "private": true, + "description": "Web site demonstrating various MVC features.", + "devDependencies": { + "grunt": "^0.4.5", + "grunt-bower-task": "^0.4.0" + } +} \ No newline at end of file diff --git a/samples/MvcSample.Web/project.json b/samples/MvcSample.Web/project.json index 12b55d5131..f3979d12a6 100644 --- a/samples/MvcSample.Web/project.json +++ b/samples/MvcSample.Web/project.json @@ -29,5 +29,17 @@ "dependencies": { } } }, + "exclude": [ + "wwwroot", + "node_modules", + "bower_components" + ], + "packExclude": [ + "node_modules", + "bower_components", + "**.kproj", + "**.user", + "**.vspscc" + ], "webroot": "wwwroot" } \ No newline at end of file diff --git a/samples/MvcSample.Web/wwwroot/Scripts/Mvc.js b/samples/MvcSample.Web/wwwroot/Scripts/Mvc.js new file mode 100644 index 0000000000..f5b692e8f4 --- /dev/null +++ b/samples/MvcSample.Web/wwwroot/Scripts/Mvc.js @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +var MVC = (function () { + // Takes the data which needs to be converted to form-url encoded format understadable by MVC. + // This does not depend on jQuery. Can be used independently. + var _stringify = function (data) { + // This holds the stringified result. + var result = ""; + + if (typeof data !== "object") + { + return result; + } + + for (var element in data) { + if (data.hasOwnProperty(element)) { + result += process(element, data[element]); + } + } + + // An '&' is appended at the end. Removing it. + return result.substring(0, result.length - 1); + } + + function process(key, value, prefix) { + // Ignore functions. + if (typeof value === "function") { + return; + } + + if (Object.prototype.toString.call(value) === '[object Array]') { + var result = ""; + for (var i = 0; i < value.length; i++) { + var tempPrefix = (prefix || key) + "[" + i + "]"; + result += process(key, value[i], tempPrefix); + } + + return result; + } + else if (typeof value === "object") { + var result = ""; + for (var prop in value) { + // This is to prevent looping through inherited proeprties. + if (value.hasOwnProperty(prop)) { + var tempPrefix = (prefix || key) + "." + prop; + result += process(prop, value[prop], tempPrefix); + } + } + + return result; + } + else { + return encodeURIComponent(prefix || key) + "=" + encodeURIComponent(value) + "&"; + } + } + + return { + // Converts a Json object into MVC understandable format + // when submitted as form-url-encoded data. + stringify: _stringify + }; +})() \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/MvcSampleTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/MvcSampleTests.cs index 5363b087c5..d0c1b1476c 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/MvcSampleTests.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/MvcSampleTests.cs @@ -6,6 +6,7 @@ using System.IO; using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Text; using System.Threading.Tasks; using Microsoft.AspNet.Builder; using Microsoft.AspNet.Mvc.Xml; @@ -51,6 +52,55 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests } } + [Theory] + [InlineData("Name=SamplePerson&Address.Street=SampleStreet&Address.City=SampleCity&" + + "Address.State=SampleState&Address.ZipCode=11&PastJobs[0].JobTitle=SampleJob1&" + + "PastJobs[0].EmployerName=Employer1&PastJobs[0].Years=2&PastJobs[1].JobTitle=SampleJob2&" + + "PastJobs[1].EmployerName=Employer2&PastJobs[1].Years=4&PastJobs[2].JobTitle=SampleJob3&" + + "PastJobs[2].EmployerName=Employer3&PastJobs[2].Years=1", "true")] + // Input with some special characters + [InlineData("Name=SamplePerson&Address.Street=SampleStre'et&Address.City=S\ampleCity&" + + "Address.State=SampleState&Address.ZipCode=11&PastJobs[0].JobTitle=S~ampleJob1&" + + "PastJobs[0].EmployerName=Employer1&PastJobs[0].Years=2&PastJobs[1].JobTitle=SampleJob2&" + + "PastJobs[1].EmployerName=Employer2&PastJobs[1].Years=4&PastJobs[2].JobTitle=SampleJob3&" + + "PastJobs[2].EmployerName=Employer3&PastJobs[2].Years=1", "true")] + [InlineData("Name=SamplePerson", "false")] + public async Task FormUrlEncoded_ReturnsAppropriateResults(string input, string expectedOutput) + { + using (TestHelper.ReplaceCallContextServiceLocationService(_services)) + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/FormUrlEncoded/IsValidPerson"); + request.Content = new StringContent(input, Encoding.UTF8, "application/x-www-form-urlencoded"); + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.Equal(expectedOutput, await response.Content.ReadAsStringAsync()); + } + } + + [Fact] + public async Task FormUrlEncoded_Index_ReturnSuccess() + { + using (TestHelper.ReplaceCallContextServiceLocationService(_services)) + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync("http://localhost/FormUrlEncoded"); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + } + [Fact] public async Task Home_NotFoundAction_Returns404() {