[Fixes #2337] Added support for file types in input taghelper and

htmlhelper
This commit is contained in:
Ajay Bhargav Baaskaran 2015-04-08 16:54:48 -07:00
parent a9f2c937df
commit 4951235eef
14 changed files with 297 additions and 107 deletions

View File

@ -8,6 +8,7 @@ using System.Globalization;
using System.Text;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.Rendering.Internal;
using Microsoft.Framework.DependencyInjection;
namespace Microsoft.AspNet.Mvc.Rendering

View File

@ -5,11 +5,12 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.Rendering.Internal;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.Internal;
namespace Microsoft.AspNet.Mvc.Rendering
{
@ -374,6 +375,20 @@ namespace Microsoft.AspNet.Mvc.Rendering
return GenerateTextBox(htmlHelper, inputType: "number");
}
public static string FileInputTemplate([NotNull] IHtmlHelper htmlHelper)
{
return GenerateTextBox(htmlHelper, inputType: "file");
}
public static string FileCollectionInputTemplate([NotNull] IHtmlHelper htmlHelper)
{
var htmlAttributes =
CreateHtmlAttributes(htmlHelper, className: "text-box single-line", inputType: "file");
htmlAttributes["multiple"] = "multiple";
return GenerateTextBox(htmlHelper, htmlHelper.ViewData.TemplateInfo.FormattedModelValue, htmlAttributes);
}
private static void ApplyRfc3339DateFormattingIfNeeded(IHtmlHelper htmlHelper, string format)
{
if (htmlHelper.Html5DateRenderingMode != Html5DateRenderingMode.Rfc3339)
@ -405,6 +420,11 @@ namespace Microsoft.AspNet.Mvc.Rendering
var htmlAttributes =
CreateHtmlAttributes(htmlHelper, className: "text-box single-line", inputType: inputType);
return GenerateTextBox(htmlHelper, value, htmlAttributes);
}
private static string GenerateTextBox(IHtmlHelper htmlHelper, object value, object htmlAttributes)
{
return htmlHelper.TextBox(
current: null,
value: value,

View File

@ -11,6 +11,7 @@ using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.ModelBinding.Validation;
using Microsoft.AspNet.Mvc.Rendering.Expressions;
using Microsoft.AspNet.Mvc.Rendering.Internal;
using Microsoft.Framework.Internal;
using Microsoft.Framework.WebEncoders;

View File

@ -5,9 +5,9 @@ using System.Globalization;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.Framework.Internal;
namespace Microsoft.AspNet.Mvc.Rendering
namespace Microsoft.AspNet.Mvc.Rendering.Internal
{
internal class TemplateBuilder
public class TemplateBuilder
{
private IViewEngine _viewEngine;
private ViewContext _viewContext;

View File

@ -7,16 +7,19 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.Internal;
namespace Microsoft.AspNet.Mvc.Rendering
namespace Microsoft.AspNet.Mvc.Rendering.Internal
{
internal class TemplateRenderer
public class TemplateRenderer
{
private static readonly string DisplayTemplateViewPath = "DisplayTemplates";
private static readonly string EditorTemplateViewPath = "EditorTemplates";
private const string DisplayTemplateViewPath = "DisplayTemplates";
private const string EditorTemplateViewPath = "EditorTemplates";
public const string IEnumerableOfIFormFileName = "IEnumerable`" + nameof(IFormFile);
private static readonly Dictionary<string, Func<IHtmlHelper, string>> _defaultDisplayActions =
new Dictionary<string, Func<IHtmlHelper, string>>(StringComparer.OrdinalIgnoreCase)
@ -62,6 +65,8 @@ namespace Microsoft.AspNet.Mvc.Rendering
{ typeof(decimal).Name, DefaultEditorTemplates.DecimalTemplate },
{ typeof(string).Name, DefaultEditorTemplates.StringTemplate },
{ typeof(object).Name, DefaultEditorTemplates.ObjectTemplate },
{ typeof(IFormFile).Name, DefaultEditorTemplates.FileInputTemplate },
{ IEnumerableOfIFormFileName, DefaultEditorTemplates.FileCollectionInputTemplate },
};
private ViewContext _viewContext;
@ -136,7 +141,7 @@ namespace Microsoft.AspNet.Mvc.Rendering
metadata.DataTypeName
};
foreach (string templateHint in templateHints.Where(s => !string.IsNullOrEmpty(s)))
foreach (var templateHint in templateHints.Where(s => !string.IsNullOrEmpty(s)))
{
yield return templateHint;
}
@ -146,14 +151,27 @@ namespace Microsoft.AspNet.Mvc.Rendering
var modelType = _viewData.ModelExplorer.ModelType;
var fieldType = Nullable.GetUnderlyingType(modelType) ?? modelType;
yield return fieldType.Name;
foreach (var typeName in GetTypeNames(_viewData.ModelExplorer.Metadata, fieldType))
{
yield return typeName;
}
}
public static IEnumerable<string> GetTypeNames(ModelMetadata modelMetadata, Type fieldType)
{
// Not returning type name here for IEnumerable<IFormFile> since we will be returning
// a more specific name, IEnumerableOfIFormFileName.
if (typeof(IEnumerable<IFormFile>) != fieldType)
{
yield return fieldType.Name;
}
if (fieldType == typeof(string))
{
// Nothing more to provide
yield break;
}
else if (!metadata.IsComplexType)
else if (!modelMetadata.IsComplexType)
{
// IsEnum is false for the Enum class itself
if (fieldType.IsEnum())
@ -167,36 +185,44 @@ namespace Microsoft.AspNet.Mvc.Rendering
}
yield return "String";
yield break;
}
else if (fieldType.IsInterface())
else if (!fieldType.IsInterface())
{
if (typeof(IEnumerable).IsAssignableFrom(fieldType))
{
yield return "Collection";
}
yield return "Object";
}
else
{
var isEnumerable = typeof(IEnumerable).IsAssignableFrom(fieldType);
var type = fieldType;
while (true)
{
fieldType = fieldType.BaseType();
if (fieldType == null)
type = type.BaseType();
if (type == null || type == typeof(object))
{
break;
}
if (isEnumerable && fieldType == typeof(Object))
{
yield return "Collection";
}
yield return fieldType.Name;
yield return type.Name;
}
}
if (typeof(IEnumerable).IsAssignableFrom(fieldType))
{
if (typeof(IEnumerable<IFormFile>).IsAssignableFrom(fieldType))
{
yield return IEnumerableOfIFormFileName;
// Specific name has already been returned, now return the generic name.
if (typeof(IEnumerable<IFormFile>) == fieldType)
{
yield return fieldType.Name;
}
}
yield return "Collection";
}
else if (typeof(IFormFile) != fieldType && typeof(IFormFile).IsAssignableFrom(fieldType))
{
yield return nameof(IFormFile);
}
yield return "Object";
}
private static IHtmlHelper MakeHtmlHelper(ViewContext viewContext, ViewDataDictionary viewData)

View File

@ -2,11 +2,12 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Mvc.Rendering.Internal;
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
namespace Microsoft.AspNet.Mvc.TagHelpers
@ -47,6 +48,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
{ nameof(Boolean), InputType.CheckBox.ToString().ToLowerInvariant() },
{ nameof(Decimal), InputType.Text.ToString().ToLowerInvariant() },
{ nameof(String), InputType.Text.ToString().ToLowerInvariant() },
{ nameof(IFormFile), "file" },
{ TemplateRenderer.IEnumerableOfIFormFileName, "file" },
};
// Mapping from <input/> element's type to RFC 3339 date and time formats.
@ -271,13 +274,22 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
format = GetFormat(modelExplorer, inputTypeHint, inputType);
}
object htmlAttributes = null;
if (string.Equals(inputType, "file") && string.Equals(inputTypeHint, TemplateRenderer.IEnumerableOfIFormFileName))
{
htmlAttributes = new Dictionary<string, object>
{
{ "multiple", "multiple" }
};
}
return Generator.GenerateTextBox(
ViewContext,
modelExplorer,
For.Name,
value: modelExplorer.Model,
format: format,
htmlAttributes: null);
htmlAttributes: htmlAttributes);
}
// Get a fall-back format based on the metadata.
@ -353,55 +365,9 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
}
}
yield return fieldType.Name;
if (fieldType == typeof(string))
foreach (string typeName in TemplateRenderer.GetTypeNames(modelExplorer.Metadata, fieldType))
{
// Nothing more to provide
yield break;
}
else if (!modelExplorer.Metadata.IsComplexType)
{
// IsEnum is false for the Enum class itself
if (fieldType.IsEnum())
{
// Same as fieldType.BaseType.Name in this case
yield return "Enum";
}
else if (fieldType == typeof(DateTimeOffset))
{
yield return "DateTime";
}
yield return "String";
}
else if (fieldType.IsInterface())
{
if (typeof(IEnumerable).IsAssignableFrom(fieldType))
{
yield return "Collection";
}
yield return "Object";
}
else
{
var 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;
}
yield return typeName;
}
}
}

View File

@ -8,9 +8,11 @@ using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.ModelBinding.Validation;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Mvc.Rendering.Internal;
using Microsoft.AspNet.Testing;
using Microsoft.Framework.Internal;
using Microsoft.Framework.WebEncoders;
@ -78,6 +80,9 @@ namespace Microsoft.AspNet.Mvc.Core
{ "decimal", "__TextBox__ class='text-box single-line'" },
{ "String", "__TextBox__ class='text-box single-line'" },
{ "STRING", "__TextBox__ class='text-box single-line'" },
{ typeof(IFormFile).Name, "__TextBox__ class='text-box single-line' type='file'" },
{ TemplateRenderer.IEnumerableOfIFormFileName,
"__TextBox__ class='text-box single-line' type='file' multiple='multiple'" },
};
}
}

View File

@ -0,0 +1,67 @@
// 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.Net;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Core;
using Microsoft.AspNet.Http.Core.Collections;
using Microsoft.AspNet.Mvc.ModelBinding;
using Xunit;
namespace Microsoft.AspNet.Mvc.Rendering.Internal
{
public class TemplateRendererTest
{
public static TheoryData<Type, string[]> TypeNameData
{
get
{
return new TheoryData<Type, string[]>
{
{ typeof(string), new string[] { "String" } },
{ typeof(bool), new string[] { "Boolean", "String" } },
{ typeof(DateTime), new string[] { "DateTime", "String" } },
{ typeof(float), new string[] { "Single", "String" } },
{ typeof(double), new string[] { "Double", "String" } },
{ typeof(Guid), new string[] { "Guid", "String" } },
{ typeof(TimeSpan), new string[] { "TimeSpan", "String" } },
{ typeof(int), new string[] { "Int32", "String" } },
{ typeof(ulong), new string[] { "UInt64", "String" } },
{ typeof(Enum), new string[] { "Enum", "String" } },
{ typeof(HttpStatusCode), new string[] { "HttpStatusCode", "Enum", "String" } },
{ typeof(FormFile), new string[] { "FormFile", "IFormFile", "Object" } },
{ typeof(IFormFile), new string[] { "IFormFile", "Object" } },
{ typeof(FormFileCollection), new string[] { "FormFileCollection", typeof(List<IFormFile>).Name,
TemplateRenderer.IEnumerableOfIFormFileName, "Collection", "Object" } },
{ typeof(IFormFileCollection), new string[] { "IFormFileCollection",
TemplateRenderer.IEnumerableOfIFormFileName, "Collection", "Object" } },
{ typeof(IEnumerable<IFormFile>), new string[] { TemplateRenderer.IEnumerableOfIFormFileName,
typeof(IEnumerable<IFormFile>).Name, "Collection", "Object" } },
};
}
}
[Theory]
[MemberData(nameof(TypeNameData))]
public void GetTypeNames_ReturnsExpectedResults(Type fieldType, string[] expectedResult)
{
// Arrange
var metadataProvider = new TestModelMetadataProvider();
var metadata = metadataProvider.GetMetadataForType(fieldType);
// Act
var typeNames = TemplateRenderer.GetTypeNames(metadata, fieldType);
// Assert
var collectionAssertions = expectedResult.Select<string, Action<string>>(expected =>
actual => Assert.Equal(expected, actual));
Assert.Collection(typeNames, collectionAssertions.ToArray());
}
}
}

View File

@ -47,6 +47,8 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
[InlineData("Link", null)]
// Testing the ScriptTagHelper
[InlineData("Script", null)]
// Testing InputTagHelper with File
[InlineData("Input", null)]
public async Task MvcTagHelpers_GeneratesExpectedResults(string action, string antiForgeryPath)
{
// Arrange

View File

@ -0,0 +1,22 @@
<html>
<head>
<meta charset="utf-8" />
<title>File Input</title>
</head>
<body>
<h2>Input Tag Helper Test</h2>
<input name="InterfaceFile" type="file" id="InterfaceFile" value="" />
<input name="InterfaceFiles" type="file" id="InterfaceFiles" multiple="multiple" value="" />
<input name="ConcreteFile" type="file" id="ConcreteFile" value="" />
<input name="ConcreteFiles" type="file" id="ConcreteFiles" multiple="multiple" value="" />
<input name="EnumerableFiles" type="file" id="EnumerableFiles" multiple="multiple" value="" />
<input class="text-box single-line" id="InterfaceFile" name="InterfaceFile" type="file" value="" />
<input class="text-box single-line" id="InterfaceFiles" multiple="multiple" name="InterfaceFiles" type="file" value="" />
<input class="text-box single-line" id="ConcreteFile" name="ConcreteFile" type="file" value="" />
<input class="text-box single-line" id="ConcreteFiles" multiple="multiple" name="ConcreteFiles" type="file" value="" />
<input class="text-box single-line" id="EnumerableFiles" multiple="multiple" name="EnumerableFiles" type="file" value="" />
</body>
</html>

View File

@ -5,8 +5,10 @@ using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Mvc.Rendering.Internal;
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
using Microsoft.Framework.WebEncoders;
using Moq;
@ -618,33 +620,48 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
Assert.Equal(expectedTagName, output.TagName);
}
public static TheoryData<string, string, string> InputTypeData
{
get
{
return new TheoryData<string, string, string>
{
{ null, null, "text" },
{ "Byte", null, "number" },
{ null, null, "text" },
{ "Byte", null, "number" },
{ "custom-datatype", null, "text" },
{ "Custom-Datatype", null, "text" },
{ "date", null, "date" }, // No date/time special cases since ModelType is string.
{ "datetime", null, "datetime" },
{ "datetime-local", null, "datetime-local" },
{ "DATETIME-local", null, "datetime-local" },
{ "Decimal", "{0:0.00}", "text" },
{ "Double", null, "number" },
{ "Int16", null, "number" },
{ "Int32", null, "number" },
{ "int32", null, "number" },
{ "Int64", null, "number" },
{ "SByte", null, "number" },
{ "Single", null, "number" },
{ "SINGLE", null, "number" },
{ "string", null, "text" },
{ "STRING", null, "text" },
{ "text", null, "text" },
{ "TEXT", null, "text" },
{ "time", null, "time" },
{ "UInt16", null, "number" },
{ "uint16", null, "number" },
{ "UInt32", null, "number" },
{ "UInt64", null, "number" },
{ nameof(IFormFile), null, "file" },
{ TemplateRenderer.IEnumerableOfIFormFileName, null, "file" },
};
}
}
[Theory]
[InlineData(null, null, "text")]
[InlineData("Byte", null, "number")]
[InlineData("custom-datatype", null, "text")]
[InlineData("Custom-Datatype", null, "text")]
[InlineData("date", null, "date")] // No date/time special cases since ModelType is string.
[InlineData("datetime", null, "datetime")]
[InlineData("datetime-local", null, "datetime-local")]
[InlineData("DATETIME-local", null, "datetime-local")]
[InlineData("Decimal", "{0:0.00}", "text")]
[InlineData("Double", null, "number")]
[InlineData("Int16", null, "number")]
[InlineData("Int32", null, "number")]
[InlineData("int32", null, "number")]
[InlineData("Int64", null, "number")]
[InlineData("SByte", null, "number")]
[InlineData("Single", null, "number")]
[InlineData("SINGLE", null, "number")]
[InlineData("string", null, "text")]
[InlineData("STRING", null, "text")]
[InlineData("text", null, "text")]
[InlineData("TEXT", null, "text")]
[InlineData("time", null, "time")]
[InlineData("UInt16", null, "number")]
[InlineData("uint16", null, "number")]
[InlineData("UInt32", null, "number")]
[InlineData("UInt64", null, "number")]
[MemberData(nameof(InputTypeData))]
public async Task ProcessAsync_CallsGenerateTextBox_AddsExpectedAttributes(
string dataTypeName,
string expectedFormat,
@ -679,6 +696,15 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
metadataProvider: metadataProvider);
var tagBuilder = new TagBuilder("input", new HtmlEncoder());
Dictionary<string, object> htmlAttributes = null;
if (string.Equals(dataTypeName, TemplateRenderer.IEnumerableOfIFormFileName))
{
htmlAttributes = new Dictionary<string, object>
{
{ "multiple", "multiple" }
};
}
htmlGenerator
.Setup(mock => mock.GenerateTextBox(
tagHelper.ViewContext,
@ -686,7 +712,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
tagHelper.For.Name,
null, // value
expectedFormat,
null)) // htmlAttributes
htmlAttributes)) // htmlAttributes
.Returns(tagBuilder)
.Verifiable();

View File

@ -171,5 +171,10 @@ namespace MvcTagHelpersWebSite.Controllers
{
return View();
}
public IActionResult Input()
{
return View();
}
}
}

View File

@ -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.Collections.Generic;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Core;
using Microsoft.AspNet.Http.Core.Collections;
namespace MvcTagHelpersWebSite.Models
{
public class Folder
{
public IFormFile InterfaceFile { get; set; }
public IFormFileCollection InterfaceFiles { get; set; }
public FormFile ConcreteFile { get; set; }
public FormFileCollection ConcreteFiles { get; set; }
public IEnumerable<IFormFile> EnumerableFiles { get; set; }
}
}

View File

@ -0,0 +1,26 @@
@using MvcTagHelpersWebSite.Models
@model Folder
@addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers"
<html>
<head>
<meta charset="utf-8" />
<title>File Input</title>
</head>
<body>
<h2>Input Tag Helper Test</h2>
<input asp-for="InterfaceFile" name="InterfaceFile" />
<input asp-for="InterfaceFiles" name="InterfaceFiles" />
<input asp-for="ConcreteFile" name="ConcreteFile" />
<input asp-for="ConcreteFiles" name="ConcreteFiles" />
<input asp-for="EnumerableFiles" name="EnumerableFiles" />
@Html.EditorFor(m => m.InterfaceFile)
@Html.EditorFor(m => m.InterfaceFiles)
@Html.EditorFor(m => m.ConcreteFile)
@Html.EditorFor(m => m.ConcreteFiles)
@Html.EditorFor(m => m.EnumerableFiles)
</body>
</html>