diff --git a/samples/MvcSample.Web/Views/Shared/MyView.cshtml b/samples/MvcSample.Web/Views/Shared/MyView.cshtml
index 16e4c3f574..0e2bc5ea94 100644
--- a/samples/MvcSample.Web/Views/Shared/MyView.cshtml
+++ b/samples/MvcSample.Web/Views/Shared/MyView.cshtml
@@ -56,7 +56,6 @@
-
diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/FormCollectionModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/FormCollectionModelBinder.cs
new file mode 100644
index 0000000000..50a2407e28
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/FormCollectionModelBinder.cs
@@ -0,0 +1,50 @@
+// 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;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNet.Http;
+using Microsoft.AspNet.Http.Core.Collections;
+
+namespace Microsoft.AspNet.Mvc.ModelBinding
+{
+ ///
+ /// Modelbinder to bind form values to .
+ ///
+ public class FormCollectionModelBinder : IModelBinder
+ {
+ ///
+ public async Task BindModelAsync([NotNull] ModelBindingContext bindingContext)
+ {
+ if (bindingContext.ModelType != typeof(IFormCollection) &&
+ bindingContext.ModelType != typeof(FormCollection))
+ {
+ return false;
+ }
+
+ var request = bindingContext.OperationBindingContext.HttpContext.Request;
+ if (request.HasFormContentType)
+ {
+ var form = await request.ReadFormAsync();
+ if (bindingContext.ModelType.IsAssignableFrom(form.GetType()))
+ {
+ bindingContext.Model = form;
+ }
+ else
+ {
+ var formValuesLookup = form.ToDictionary(p => p.Key,
+ p => p.Value);
+ bindingContext.Model = new FormCollection(formValuesLookup, form.Files);
+ }
+ }
+ else
+ {
+ bindingContext.Model = new FormCollection(new Dictionary());
+ }
+
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/project.json b/src/Microsoft.AspNet.Mvc.ModelBinding/project.json
index afa5fd79b1..db27165255 100644
--- a/src/Microsoft.AspNet.Mvc.ModelBinding/project.json
+++ b/src/Microsoft.AspNet.Mvc.ModelBinding/project.json
@@ -5,7 +5,7 @@
"warningsAsErrors": true
},
"dependencies": {
- "Microsoft.AspNet.Http": "1.0.0-*",
+ "Microsoft.AspNet.Http.Core": "1.0.0-*",
"Microsoft.AspNet.Http.Extensions": "1.0.0-*",
"Microsoft.AspNet.Mvc.Common": { "version": "6.0.0-*", "type": "build" },
"Microsoft.Framework.DependencyInjection": "1.0.0-*",
diff --git a/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs b/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs
index fdba761395..b1dd8eb837 100644
--- a/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs
+++ b/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs
@@ -37,6 +37,7 @@ namespace Microsoft.AspNet.Mvc
options.ModelBinders.Add(new CancellationTokenModelBinder());
options.ModelBinders.Add(new ByteArrayModelBinder());
options.ModelBinders.Add(new FormFileModelBinder());
+ options.ModelBinders.Add(new FormCollectionModelBinder());
options.ModelBinders.Add(typeof(GenericModelBinder));
options.ModelBinders.Add(new MutableObjectModelBinder());
options.ModelBinders.Add(new ComplexModelDtoModelBinder());
diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTest.cs
index 1da95943f7..abe6e66c3d 100644
--- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTest.cs
+++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTest.cs
@@ -11,6 +11,8 @@ using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNet.Builder;
+using Microsoft.AspNet.Http;
+using Microsoft.AspNet.Http.Core.Collections;
using Microsoft.AspNet.TestHost;
using ModelBindingWebSite;
using ModelBindingWebSite.ViewModels;
@@ -1530,6 +1532,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.Equal(expectedContent, body);
}
+ [Fact]
public async Task ModelBinder_FormatsDontMatch_ThrowsUserFriendlyException()
{
// Arrange
@@ -1601,5 +1604,93 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var dictionary = JsonConvert.DeserializeObject>(responseContent);
Assert.Equal(expectedDictionary, dictionary);
}
+
+ [Fact]
+ public async Task FormCollectionModelBinder_CanBind_FormValues()
+ {
+ // Arrange
+ var server = TestServer.Create(_services, _app);
+ var client = server.CreateClient();
+ var url = "http://localhost/FormCollection/ReturnValuesAsList";
+ var nameValueCollection = new List>
+ {
+ new KeyValuePair("field1", "value1"),
+ new KeyValuePair("field2", "value2"),
+ };
+ var formData = new FormUrlEncodedContent(nameValueCollection);
+
+ // Act
+ var response = await client.PostAsync(url, formData);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var valuesList = JsonConvert.DeserializeObject>(
+ await response.Content.ReadAsStringAsync());
+ Assert.Equal(new List { "value1", "value2" }, valuesList);
+ }
+
+ [Fact]
+ public async Task FormCollectionModelBinder_CanBind_FormValuesWithDuplicateKeys()
+ {
+ // Arrange
+ var server = TestServer.Create(_services, _app);
+ var client = server.CreateClient();
+ var url = "http://localhost/FormCollection/ReturnValuesAsList";
+ var nameValueCollection = new List>
+ {
+ new KeyValuePair("field1", "value1"),
+ new KeyValuePair("field2", "value2"),
+ new KeyValuePair("field1", "value3"),
+ };
+ var formData = new FormUrlEncodedContent(nameValueCollection);
+
+ // Act
+ var response = await client.PostAsync(url, formData);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var valuesList = JsonConvert.DeserializeObject>(
+ await response.Content.ReadAsStringAsync());
+ Assert.Equal(new List { "value1,value3", "value2" }, valuesList);
+ }
+
+ [Fact]
+ public async Task FormCollectionModelBinder_CannotBind_NonFormValues()
+ {
+ // Arrange
+ var server = TestServer.Create(_services, _app);
+ var client = server.CreateClient();
+ var url = "http://localhost/FormCollection/ReturnCollectionCount";
+ var data = new StringContent("Non form content");
+
+ // Act
+ var response = await client.PostAsync(url, data);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var collectionCount = JsonConvert.DeserializeObject(
+ await response.Content.ReadAsStringAsync());
+ Assert.Equal(0, collectionCount);
+ }
+
+ [Fact]
+ public async Task FormCollectionModelBinder_CanBind_FormWithFile()
+ {
+ // Arrange
+ var server = TestServer.Create(_services, _app);
+ var client = server.CreateClient();
+ var url = "http://localhost/FormCollection/ReturnFileContent";
+ var expectedContent = "Test Content";
+ var formData = new MultipartFormDataContent("Upload----");
+ formData.Add(new StringContent(expectedContent), "File", "test.txt");
+
+ // Act
+ var response = await client.PostAsync(url, formData);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var fileContent = await response.Content.ReadAsStringAsync();
+ Assert.Equal(expectedContent, fileContent);
+ }
}
}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/FormCollectionModelBinderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/FormCollectionModelBinderTest.cs
new file mode 100644
index 0000000000..e00e99d263
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/FormCollectionModelBinderTest.cs
@@ -0,0 +1,148 @@
+// 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.
+
+#if ASPNET50
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNet.Http;
+using Microsoft.AspNet.Http.Core.Collections;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNet.Mvc.ModelBinding
+{
+ public class FormCollectionModelBinderTest
+ {
+ [Fact]
+ public async Task FormCollectionModelBinder_ValidType_BindSuccessful()
+ {
+ // Arrange
+ var formCollection = new FormCollection(new Dictionary
+ {
+ { "field1", new string[] { "value1" } },
+ { "field2", new string[] { "value2" } }
+ });
+ var httpContext = GetMockHttpContext(formCollection);
+ var bindingContext = GetBindingContext(typeof(FormCollection), httpContext);
+ var binder = new FormCollectionModelBinder();
+
+ // Act
+ var result = await binder.BindModelAsync(bindingContext);
+
+ // Assert
+ Assert.True(result);
+ var form = Assert.IsAssignableFrom(bindingContext.Model);
+ Assert.Equal(2, form.Count);
+ Assert.Equal("value1", form["field1"]);
+ Assert.Equal("value2", form["field2"]);
+ }
+
+ [Fact]
+ public async Task FormCollectionModelBinder_InvalidType_BindFails()
+ {
+ // Arrange
+ var formCollection = new FormCollection(new Dictionary
+ {
+ { "field1", new string[] { "value1" } },
+ { "field2", new string[] { "value2" } }
+ });
+ var httpContext = GetMockHttpContext(formCollection);
+ var bindingContext = GetBindingContext(typeof(string), httpContext);
+ var binder = new FormCollectionModelBinder();
+
+ // Act
+ var result = await binder.BindModelAsync(bindingContext);
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Fact]
+ public async Task FormCollectionModelBinder_NoForm_BindSuccessful_ReturnsEmptyFormCollection()
+ {
+ // Arrange
+ var httpContext = GetMockHttpContext(null, hasForm: false);
+ var bindingContext = GetBindingContext(typeof(IFormCollection), httpContext);
+ var binder = new FormCollectionModelBinder();
+
+ // Act
+ var result = await binder.BindModelAsync(bindingContext);
+
+ // Assert
+ Assert.True(result);
+ Assert.IsType(typeof(FormCollection), bindingContext.Model);
+ Assert.Empty((FormCollection)bindingContext.Model);
+ }
+
+ [Fact]
+ public async Task FormCollectionModelBinder_CustomFormCollection_BindSuccessful()
+ {
+ // Arrange
+ var formCollection = new MyFormCollection(new Dictionary
+ {
+ { "field1", new string[] { "value1" } },
+ { "field2", new string[] { "value2" } }
+ });
+ var httpContext = GetMockHttpContext(formCollection);
+ var bindingContext = GetBindingContext(typeof(FormCollection), httpContext);
+ var binder = new FormCollectionModelBinder();
+
+ // Act
+ var result = await binder.BindModelAsync(bindingContext);
+
+ // Assert
+ Assert.True(result);
+ var form = Assert.IsAssignableFrom(bindingContext.Model);
+ Assert.Equal(2, form.Count);
+ Assert.Equal("value1", form["field1"]);
+ Assert.Equal("value2", form["field2"]);
+ }
+
+ private static HttpContext GetMockHttpContext(IFormCollection formCollection, bool hasForm = true)
+ {
+ var httpContext = new Mock();
+ httpContext.Setup(h => h.Request.ReadFormAsync(It.IsAny()))
+ .Returns(Task.FromResult(formCollection));
+ httpContext.Setup(h => h.Request.HasFormContentType).Returns(hasForm);
+ return httpContext.Object;
+ }
+
+ private static ModelBindingContext GetBindingContext(Type modelType, HttpContext httpContext)
+ {
+ var metadataProvider = new EmptyModelMetadataProvider();
+ var bindingContext = new ModelBindingContext
+ {
+ ModelMetadata = metadataProvider.GetMetadataForType(null, modelType),
+ ModelName = "file",
+ OperationBindingContext = new OperationBindingContext
+ {
+ ModelBinder = new FormCollectionModelBinder(),
+ MetadataProvider = metadataProvider,
+ HttpContext = httpContext,
+ }
+ };
+
+ return bindingContext;
+ }
+
+ private class MyFormCollection : ReadableStringCollection, IFormCollection
+ {
+ public MyFormCollection(IDictionary store) : this(store, new FormFileCollection())
+ {
+ }
+
+ public MyFormCollection(IDictionary store, IFormFileCollection files) : base(store)
+ {
+ Files = files;
+ }
+
+ public IFormFileCollection Files { get; private set; }
+ }
+ }
+}
+
+#endif
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.Test/MvcOptionsSetupTest.cs b/test/Microsoft.AspNet.Mvc.Test/MvcOptionsSetupTest.cs
index 07f420c24d..0c89c2243f 100644
--- a/test/Microsoft.AspNet.Mvc.Test/MvcOptionsSetupTest.cs
+++ b/test/Microsoft.AspNet.Mvc.Test/MvcOptionsSetupTest.cs
@@ -39,7 +39,7 @@ namespace Microsoft.AspNet.Mvc
// Assert
var i = 0;
- Assert.Equal(12, mvcOptions.ModelBinders.Count);
+ Assert.Equal(13, mvcOptions.ModelBinders.Count);
Assert.Equal(typeof(BinderTypeBasedModelBinder), mvcOptions.ModelBinders[i++].OptionType);
Assert.Equal(typeof(ServicesModelBinder), mvcOptions.ModelBinders[i++].OptionType);
Assert.Equal(typeof(BodyModelBinder), mvcOptions.ModelBinders[i++].OptionType);
@@ -49,6 +49,7 @@ namespace Microsoft.AspNet.Mvc
Assert.Equal(typeof(CancellationTokenModelBinder), mvcOptions.ModelBinders[i++].OptionType);
Assert.Equal(typeof(ByteArrayModelBinder), mvcOptions.ModelBinders[i++].OptionType);
Assert.Equal(typeof(FormFileModelBinder), mvcOptions.ModelBinders[i++].OptionType);
+ Assert.Equal(typeof(FormCollectionModelBinder), mvcOptions.ModelBinders[i++].OptionType);
Assert.Equal(typeof(GenericModelBinder), mvcOptions.ModelBinders[i++].OptionType);
Assert.Equal(typeof(MutableObjectModelBinder), mvcOptions.ModelBinders[i++].OptionType);
Assert.Equal(typeof(ComplexModelDtoModelBinder), mvcOptions.ModelBinders[i++].OptionType);
diff --git a/test/WebSites/ModelBindingWebSite/Controllers/FormCollectionController.cs b/test/WebSites/ModelBindingWebSite/Controllers/FormCollectionController.cs
new file mode 100644
index 0000000000..f616d4174a
--- /dev/null
+++ b/test/WebSites/ModelBindingWebSite/Controllers/FormCollectionController.cs
@@ -0,0 +1,40 @@
+// 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.IO;
+using Microsoft.AspNet.Http;
+using Microsoft.AspNet.Http.Core.Collections;
+using Microsoft.AspNet.Mvc;
+
+namespace ModelBindingWebSite.Controllers
+{
+ public class FormCollectionController : Controller
+ {
+ public IList ReturnValuesAsList(IFormCollection form)
+ {
+ var valuesList = new List();
+
+ valuesList.Add(form["field1"]);
+ valuesList.Add(form["field2"]);
+
+ return valuesList;
+ }
+
+ public int ReturnCollectionCount(IFormCollection form)
+ {
+ return form.Count;
+ }
+
+ public ActionResult ReturnFileContent(FormCollection form)
+ {
+ var file = form.Files.GetFile("File");
+ using (var reader = new StreamReader(file.OpenReadStream()))
+ {
+ var fileContent = reader.ReadToEnd();
+
+ return Content(fileContent);
+ }
+ }
+ }
+}