diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/DefaultEditorTemplates.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/DefaultEditorTemplates.cs index f54c2c7a70..848fda53ae 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/DefaultEditorTemplates.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/DefaultEditorTemplates.cs @@ -212,6 +212,17 @@ namespace Microsoft.AspNet.Mvc.Rendering return htmlAttributes; } + public static string MultilineTemplate(IHtmlHelper html) + { + var htmlString = html.TextArea( + name: string.Empty, + value: html.ViewContext.ViewData.TemplateInfo.FormattedModelValue.ToString(), + rows: 0, + columns: 0, + htmlAttributes: CreateHtmlAttributes(html, "text-box multi-line")); + return htmlString.ToString(); + } + public static string ObjectTemplate(IHtmlHelper html) { var viewData = html.ViewData; diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/TemplateRenderer.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/TemplateRenderer.cs index 6698000d1a..4cbd322094 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/TemplateRenderer.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/TemplateRenderer.cs @@ -8,7 +8,6 @@ using System.Globalization; using System.IO; using System.Linq; using Microsoft.AspNet.Mvc.Core; -using Microsoft.AspNet.Mvc.Internal; using Microsoft.Framework.DependencyInjection; namespace Microsoft.AspNet.Mvc.Rendering @@ -21,32 +20,32 @@ namespace Microsoft.AspNet.Mvc.Rendering private static readonly Dictionary> _defaultDisplayActions = new Dictionary>(StringComparer.OrdinalIgnoreCase) { + { "Collection", DefaultDisplayTemplates.CollectionTemplate }, { "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 }, }; - // TODO: Add DefaultEditorTemplates.MultilineTextTemplate and place in this dictionary. private static readonly Dictionary> _defaultEditorActions = new Dictionary>(StringComparer.OrdinalIgnoreCase) { - { "HiddenInput", DefaultEditorTemplates.HiddenInputTemplate }, - { "Password", DefaultEditorTemplates.PasswordTemplate }, - { "Text", DefaultEditorTemplates.StringTemplate }, { "Collection", DefaultEditorTemplates.CollectionTemplate }, - { "PhoneNumber", DefaultEditorTemplates.PhoneNumberInputTemplate }, - { "Url", DefaultEditorTemplates.UrlInputTemplate }, { "EmailAddress", DefaultEditorTemplates.EmailAddressInputTemplate }, + { "HiddenInput", DefaultEditorTemplates.HiddenInputTemplate }, + { "MultilineText", DefaultEditorTemplates.MultilineTemplate }, + { "Password", DefaultEditorTemplates.PasswordTemplate }, + { "PhoneNumber", DefaultEditorTemplates.PhoneNumberInputTemplate }, + { "Text", DefaultEditorTemplates.StringTemplate }, + { "Url", DefaultEditorTemplates.UrlInputTemplate }, + { "Date", DefaultEditorTemplates.DateInputTemplate }, { "DateTime", DefaultEditorTemplates.DateTimeInputTemplate }, { "DateTime-local", DefaultEditorTemplates.DateTimeLocalInputTemplate }, - { "Date", DefaultEditorTemplates.DateInputTemplate }, { "Time", DefaultEditorTemplates.TimeInputTemplate }, { typeof(byte).Name, DefaultEditorTemplates.NumberInputTemplate }, { typeof(sbyte).Name, DefaultEditorTemplates.NumberInputTemplate }, diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/DefaultEditorTemplatesTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/DefaultEditorTemplatesTests.cs index 3881b80085..419a3f40f1 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/DefaultEditorTemplatesTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/DefaultEditorTemplatesTests.cs @@ -2,6 +2,7 @@ // 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.Mvc.ModelBinding; @@ -14,6 +15,59 @@ namespace Microsoft.AspNet.Mvc.Core { public class DefaultEditorTemplatesTests { + // Mappings from templateName to expected result when using StubbyHtmlHelper. + public static TheoryData TemplateNameData + { + get + { + return new TheoryData + { + { null, "__TextBox__" }, + { string.Empty, "__TextBox__" }, + { "EmailAddress", "__TextBox__" }, + { "emailaddress", "__TextBox__" }, + { "HiddenInput", "True__Hidden__" }, // Hidden also generates value by default. + { "HIDDENINPUT", "True__Hidden__" }, + { "MultilineText", "__TextArea__" }, + { "multilinetext", "__TextArea__" }, + { "Password", "__Password__" }, + { "PASSWORD", "__Password__" }, + { "PhoneNumber", "__TextBox__" }, + { "phonenumber", "__TextBox__" }, + { "Text", "__TextBox__" }, + { "TEXT", "__TextBox__" }, + { "Url", "__TextBox__" }, + { "url", "__TextBox__" }, + { "Date", "__TextBox__" }, + { "DATE", "__TextBox__" }, + { "DateTime", "__TextBox__" }, + { "datetime", "__TextBox__" }, + { "DateTime-local", "__TextBox__" }, + { "DATETIME-LOCAL", "__TextBox__" }, + { "Time", "__TextBox__" }, + { "time", "__TextBox__" }, + { "Byte", "__TextBox__" }, + { "BYTE", "__TextBox__" }, + { "SByte", "__TextBox__" }, + { "sbyte", "__TextBox__" }, + { "Int32", "__TextBox__" }, + { "INT32", "__TextBox__" }, + { "UInt32", "__TextBox__" }, + { "uint32", "__TextBox__" }, + { "Int64", "__TextBox__" }, + { "INT64", "__TextBox__" }, + { "UInt64", "__TextBox__" }, + { "uint64", "__TextBox__" }, + { "Boolean", "__CheckBox__" }, // String is not a Nullable type. + { "BOOLEAN", "__CheckBox__" }, + { "Decimal", "__TextBox__" }, + { "decimal", "__TextBox__" }, + { "String", "__TextBox__" }, + { "STRING", "__TextBox__" }, + }; + } + } + [Fact] public void ObjectTemplateEditsSimplePropertiesOnObjectByDefault() { @@ -183,6 +237,219 @@ Environment.NewLine; Assert.Equal(expected, result); } + [Fact] + public void MultilineTextTemplate_ReturnsTextArea() + { + // Arrange + var expected = + ""; + + var model = "Model string"; + var html = DefaultTemplatesUtilities.GetHtmlHelper(model); + var templateInfo = html.ViewData.TemplateInfo; + templateInfo.HtmlFieldPrefix = "FieldPrefix"; + + // TemplateBuilder sets FormattedModelValue before calling TemplateRenderer and it's used below. + templateInfo.FormattedModelValue = "Formatted string"; + + // Act + var result = DefaultEditorTemplates.MultilineTemplate(html); + + // Assert + Assert.Equal(expected, result); + } + + [Theory] + [MemberData(nameof(TemplateNameData))] + public void Editor_CallsExpectedHtmlHelper(string templateName, string expectedResult) + { + // Arrange + var model = new DefaultTemplatesUtilities.ObjectTemplateModel { Property1 = "True" }; + var viewEngine = new Mock(); + viewEngine + .Setup(v => v.FindPartialView(It.IsAny(), It.IsAny())) + .Returns(ViewEngineResult.NotFound("", Enumerable.Empty())); + var helper = DefaultTemplatesUtilities.GetHtmlHelper( + model, + viewEngine.Object, + innerHelper => new StubbyHtmlHelper(innerHelper)); + helper.ViewData["Property1"] = "True"; + + // TemplateBuilder sets FormattedModelValue before calling TemplateRenderer and it's used in most templates. + helper.ViewData.TemplateInfo.FormattedModelValue = "Formatted string"; + + // Act + var result = helper.Editor( + "Property1", + templateName, + htmlFieldName: null, + additionalViewData: null); + + // Assert + Assert.Equal(expectedResult, result.ToString()); + } + + [Theory] + [MemberData(nameof(TemplateNameData))] + public void EditorFor_CallsExpectedHtmlHelper(string templateName, string expectedResult) + { + // Arrange + var model = new DefaultTemplatesUtilities.ObjectTemplateModel { Property1 = "True" }; + var viewEngine = new Mock(); + viewEngine + .Setup(v => v.FindPartialView(It.IsAny(), It.IsAny())) + .Returns(ViewEngineResult.NotFound("", Enumerable.Empty())); + var helper = DefaultTemplatesUtilities.GetHtmlHelper( + model, + viewEngine.Object, + innerHelper => new StubbyHtmlHelper(innerHelper)); + + // TemplateBuilder sets FormattedModelValue before calling TemplateRenderer and it's used in most templates. + helper.ViewData.TemplateInfo.FormattedModelValue = "Formatted string"; + + // Act + var result = helper.EditorFor( + anotherModel => anotherModel.Property1, + templateName, + htmlFieldName: null, + additionalViewData: null); + + // Assert + Assert.Equal(expectedResult, result.ToString()); + } + + [Theory] + [MemberData(nameof(TemplateNameData))] + public void Editor_CallsExpectedHtmlHelper_DataTypeName(string templateName, string expectedResult) + { + // Arrange + var model = new DefaultTemplatesUtilities.ObjectTemplateModel { Property1 = "True" }; + var viewEngine = new Mock(); + viewEngine + .Setup(v => v.FindPartialView(It.IsAny(), It.IsAny())) + .Returns(ViewEngineResult.NotFound("", Enumerable.Empty())); + var helper = DefaultTemplatesUtilities.GetHtmlHelper( + model, + viewEngine.Object, + innerHelper => new StubbyHtmlHelper(innerHelper)); + helper.ViewData["Property1"] = "True"; + var metadata = + helper.ViewData.ModelMetadata.Properties.First(m => string.Equals(m.PropertyName, "Property1")); + metadata.DataTypeName = templateName; + + // TemplateBuilder sets FormattedModelValue before calling TemplateRenderer and it's used in most templates. + helper.ViewData.TemplateInfo.FormattedModelValue = "Formatted string"; + + // Act + var result = helper.Editor( + "Property1", + templateName, + htmlFieldName: null, + additionalViewData: null); + + // Assert + Assert.Equal(expectedResult, result.ToString()); + } + + [Theory] + [MemberData(nameof(TemplateNameData))] + public void EditorFor_CallsExpectedHtmlHelper_DataTypeName(string templateName, string expectedResult) + { + // Arrange + var model = new DefaultTemplatesUtilities.ObjectTemplateModel { Property1 = "True" }; + var viewEngine = new Mock(); + viewEngine + .Setup(v => v.FindPartialView(It.IsAny(), It.IsAny())) + .Returns(ViewEngineResult.NotFound("", Enumerable.Empty())); + var helper = DefaultTemplatesUtilities.GetHtmlHelper( + model, + viewEngine.Object, + innerHelper => new StubbyHtmlHelper(innerHelper)); + var metadata = + helper.ViewData.ModelMetadata.Properties.First(m => string.Equals(m.PropertyName, "Property1")); + metadata.DataTypeName = templateName; + + // TemplateBuilder sets FormattedModelValue before calling TemplateRenderer and it's used in most templates. + helper.ViewData.TemplateInfo.FormattedModelValue = "Formatted string"; + + // Act + var result = helper.EditorFor( + anotherModel => anotherModel.Property1, + templateName, + htmlFieldName: null, + additionalViewData: null); + + // Assert + Assert.Equal(expectedResult, result.ToString()); + } + + [Theory] + [MemberData(nameof(TemplateNameData))] + public void Editor_CallsExpectedHtmlHelper_TemplateHint(string templateName, string expectedResult) + { + // Arrange + var model = new DefaultTemplatesUtilities.ObjectTemplateModel { Property1 = "True" }; + var viewEngine = new Mock(); + viewEngine + .Setup(v => v.FindPartialView(It.IsAny(), It.IsAny())) + .Returns(ViewEngineResult.NotFound("", Enumerable.Empty())); + var helper = DefaultTemplatesUtilities.GetHtmlHelper( + model, + viewEngine.Object, + innerHelper => new StubbyHtmlHelper(innerHelper)); + helper.ViewData["Property1"] = "True"; + var metadata = + helper.ViewData.ModelMetadata.Properties.First(m => string.Equals(m.PropertyName, "Property1")); + metadata.TemplateHint = templateName; + + // TemplateBuilder sets FormattedModelValue before calling TemplateRenderer and it's used in most templates. + helper.ViewData.TemplateInfo.FormattedModelValue = "Formatted string"; + + // Act + var result = helper.Editor( + "Property1", + templateName, + htmlFieldName: null, + additionalViewData: null); + + // Assert + Assert.Equal(expectedResult, result.ToString()); + } + + [Theory] + [MemberData(nameof(TemplateNameData))] + public void EditorFor_CallsExpectedHtmlHelper_TemplateHint(string templateName, string expectedResult) + { + // Arrange + var model = new DefaultTemplatesUtilities.ObjectTemplateModel { Property1 = "True" }; + var viewEngine = new Mock(); + viewEngine + .Setup(v => v.FindPartialView(It.IsAny(), It.IsAny())) + .Returns(ViewEngineResult.NotFound("", Enumerable.Empty())); + var helper = DefaultTemplatesUtilities.GetHtmlHelper( + model, + viewEngine.Object, + innerHelper => new StubbyHtmlHelper(innerHelper)); + var metadata = + helper.ViewData.ModelMetadata.Properties.First(m => string.Equals(m.PropertyName, "Property1")); + metadata.TemplateHint = templateName; + + // TemplateBuilder sets FormattedModelValue before calling TemplateRenderer and it's used in most templates. + helper.ViewData.TemplateInfo.FormattedModelValue = "Formatted string"; + + // Act + var result = helper.EditorFor( + anotherModel => anotherModel.Property1, + templateName, + htmlFieldName: null, + additionalViewData: null); + + // Assert + Assert.Equal(expectedResult, result.ToString()); + } + [Fact] public void Editor_FindsViewDataMember() { @@ -411,5 +678,251 @@ Environment.NewLine; var ex = Assert.Throws(() => helper.EditorFor(m => m.Property1)); Assert.Equal(expectedMessage, ex.Message); } + + private class StubbyHtmlHelper : IHtmlHelper, ICanHasViewContext + { + private readonly IHtmlHelper _innerHelper; + + public StubbyHtmlHelper(IHtmlHelper innerHelper) + { + _innerHelper = innerHelper; + } + + public Html5DateRenderingMode Html5DateRenderingMode + { + get { return _innerHelper.Html5DateRenderingMode; } + set { _innerHelper.Html5DateRenderingMode = value; } + } + + public string IdAttributeDotReplacement + { + get { return _innerHelper.IdAttributeDotReplacement; } + set { _innerHelper.IdAttributeDotReplacement = value; } + } + + public IModelMetadataProvider MetadataProvider + { + get { return _innerHelper.MetadataProvider; } + } + + public dynamic ViewBag + { + get { return _innerHelper.ViewBag; } + } + + public ViewContext ViewContext + { + get { return _innerHelper.ViewContext; } + } + + public ViewDataDictionary ViewData + { + get { return _innerHelper.ViewData; } + } + + public void Contextualize([NotNull] ViewContext viewContext) + { + (_innerHelper as ICanHasViewContext)?.Contextualize(viewContext); + } + + public HtmlString ActionLink( + [NotNull] string linkText, + string actionName, + string controllerName, + string protocol, + string hostname, + string fragment, + object routeValues, + object htmlAttributes) + { + throw new NotImplementedException(); + } + + public HtmlString AntiForgeryToken() + { + throw new NotImplementedException(); + } + + public MvcForm BeginForm( + string actionName, + string controllerName, + object routeValues, + FormMethod method, + object htmlAttributes) + { + throw new NotImplementedException(); + } + + public HtmlString CheckBox(string name, bool? isChecked, object htmlAttributes) + { + return new HtmlString("__CheckBox__"); + } + + public HtmlString Display( + string expression, + string templateName, + string htmlFieldName, + object additionalViewData) + { + throw new NotImplementedException(); + } + + public string DisplayName(string expression) + { + throw new NotImplementedException(); + } + + public string DisplayText(string name) + { + throw new NotImplementedException(); + } + + public HtmlString DropDownList( + string name, + IEnumerable selectList, + string optionLabel, + object htmlAttributes) + { + return new HtmlString("__DropDownList__"); + } + + public HtmlString Editor( + string expression, + string templateName, + string htmlFieldName, + object additionalViewData) + { + return _innerHelper.Editor(expression, templateName, htmlFieldName, additionalViewData); + } + + public string Encode(string value) + { + throw new NotImplementedException(); + } + + public string Encode(object value) + { + return _innerHelper.Encode(value); + } + + public void EndForm() + { + throw new NotImplementedException(); + } + + public string FormatValue(object value, string format) + { + throw new NotImplementedException(); + } + + public string GenerateIdFromName([NotNull] string name) + { + throw new NotImplementedException(); + } + + public IEnumerable GetClientValidationRules(ModelMetadata metadata, string name) + { + return Enumerable.Empty(); + } + + public HtmlString Hidden(string name, object value, object htmlAttributes) + { + return new HtmlString("__Hidden__"); + } + + public string Id(string name) + { + throw new NotImplementedException(); + } + + public HtmlString Label(string expression, string labelText, object htmlAttributes) + { + return new HtmlString("__Label__"); + } + + public HtmlString ListBox(string name, IEnumerable selectList, object htmlAttributes) + { + throw new NotImplementedException(); + } + + public string Name(string name) + { + throw new NotImplementedException(); + } + + public Task PartialAsync( + [NotNull] string partialViewName, + object model, + ViewDataDictionary viewData) + { + throw new NotImplementedException(); + } + + public HtmlString Password(string name, object value, object htmlAttributes) + { + return new HtmlString("__Password__"); + } + + public HtmlString RadioButton(string name, object value, bool? isChecked, object htmlAttributes) + { + return new HtmlString("__RadioButton__"); + } + + public HtmlString Raw(object value) + { + throw new NotImplementedException(); + } + + public HtmlString Raw(string value) + { + throw new NotImplementedException(); + } + + public Task RenderPartialAsync([NotNull] string partialViewName, object model, ViewDataDictionary viewData) + { + throw new NotImplementedException(); + } + + public HtmlString RouteLink( + [NotNull] string linkText, + string routeName, + string protocol, + string hostName, + string fragment, + object routeValues, + object htmlAttributes) + { + throw new NotImplementedException(); + } + + public HtmlString TextArea(string name, string value, int rows, int columns, object htmlAttributes) + { + return new HtmlString("__TextArea__"); + } + + public HtmlString TextBox(string name, object value, string format, object htmlAttributes) + { + return new HtmlString("__TextBox__"); + } + + public HtmlString ValidationMessage(string modelName, string message, object htmlAttributes, string tag) + { + return new HtmlString("__ValidationMessage__"); + } + + public HtmlString ValidationSummary( + bool excludePropertyErrors, + string message, + object htmlAttributes, + string tag) + { + throw new NotImplementedException(); + } + + public string Value(string name, string format) + { + throw new NotImplementedException(); + } + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/DefaultTemplatesUtilities.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/DefaultTemplatesUtilities.cs index 5a7ad1318b..71f3b7caa4 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/DefaultTemplatesUtilities.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/DefaultTemplatesUtilities.cs @@ -43,12 +43,16 @@ namespace Microsoft.AspNet.Mvc.Rendering public static HtmlHelper GetHtmlHelper() { - return GetHtmlHelper(null); + return GetHtmlHelper(model: null); } public static HtmlHelper GetHtmlHelper(IUrlHelper urlHelper) { - return GetHtmlHelper(null, urlHelper, CreateViewEngine(), CreateModelMetadataProvider()); + return GetHtmlHelper( + model: null, + urlHelper: urlHelper, + viewEngine: CreateViewEngine(), + provider: CreateModelMetadataProvider()); } public static HtmlHelper GetHtmlHelper(TModel model) @@ -58,7 +62,7 @@ namespace Microsoft.AspNet.Mvc.Rendering public static HtmlHelper GetHtmlHelper(IModelMetadataProvider provider) { - return GetHtmlHelper(null, provider); + return GetHtmlHelper(model: null, provider: provider); } public static HtmlHelper GetHtmlHelper(TModel model, IModelMetadataProvider provider) @@ -71,11 +75,34 @@ namespace Microsoft.AspNet.Mvc.Rendering return GetHtmlHelper(model, CreateUrlHelper(), viewEngine, CreateModelMetadataProvider()); } + public static HtmlHelper GetHtmlHelper( + TModel model, + ICompositeViewEngine viewEngine, + Func innerHelperWrapper) + { + return GetHtmlHelper( + model, + CreateUrlHelper(), + viewEngine, + CreateModelMetadataProvider(), + innerHelperWrapper); + } + public static HtmlHelper GetHtmlHelper( TModel model, IUrlHelper urlHelper, ICompositeViewEngine viewEngine, IModelMetadataProvider provider) + { + return GetHtmlHelper(model, urlHelper, viewEngine, provider, innerHelperWrapper: null); + } + + public static HtmlHelper GetHtmlHelper( + TModel model, + IUrlHelper urlHelper, + ICompositeViewEngine viewEngine, + IModelMetadataProvider provider, + Func innerHelperWrapper) { var viewData = new ViewDataDictionary(provider); viewData.Model = model; @@ -121,14 +148,19 @@ namespace Microsoft.AspNet.Mvc.Rendering var viewContext = new ViewContext(actionContext, Mock.Of(), viewData, new StringWriter()); // TemplateRenderer will Contextualize this transient service. + var innerHelper = (IHtmlHelper)new HtmlHelper( + viewEngine, + provider, + urlHelper, + GetAntiForgeryInstance(), + actionBindingContextProvider.Object); + if (innerHelperWrapper != null) + { + innerHelper = innerHelperWrapper(innerHelper); + } serviceProvider .Setup(s => s.GetService(typeof(IHtmlHelper))) - .Returns(() => new HtmlHelper( - viewEngine, - provider, - urlHelper, - GetAntiForgeryInstance(), - actionBindingContextProvider.Object)); + .Returns(() => innerHelper); var htmlHelper = new HtmlHelper( viewEngine,