diff --git a/Build.ps1 b/Build.ps1 index 26f3adb..259efff 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -38,7 +38,7 @@ exec { & dotnet build -c Release --version-suffix=$buildSuffix -v q /nologo } Push-Location -Path .\test\HtmlTags.Testing try { - exec { & dotnet xunit -configuration Release -nobuild } + exec { & dotnet xunit -configuration Release -nobuild -fxversion 2.0.0 } } finally { Pop-Location @@ -47,7 +47,7 @@ finally { Push-Location -Path .\test\HtmlTags.AspNetCore.Testing try { - exec { & dotnet xunit -configuration Release -nobuild } + exec { & dotnet xunit -configuration Release -nobuild -fxversion 2.0.0 } } finally { Pop-Location diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..ef1851e --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,17 @@ + + + + Easy generation of html with a jquery inspired object model + Copyright Jeremy D. Miller, Josh Arnold, Joshua Flanagan, Jimmy Bogard, et al. All rights reserved. + 7.0.0 + Jeremy D. Miller;Joshua Flanagan;Josh Arnold;Jimmy Bogard + latest + true + $(NoWarn);1701;1702;1591 + true + https://raw.githubusercontent.com/HtmlTags/htmltags/master/logo/FubuHtml_256.png + https://github.com/HtmlTags/htmltags + https://github.com/HtmlTags/htmltags/raw/master/license.txt + + + diff --git a/HtmlTags.sln b/HtmlTags.sln index 45c0c64..2bbf93d 100644 --- a/HtmlTags.sln +++ b/HtmlTags.sln @@ -9,7 +9,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .gitignore = .gitignore appveyor.yml = appveyor.yml Build.ps1 = Build.ps1 - NuGet.config = NuGet.config + Directory.Build.props = Directory.Build.props ..\readme.md = ..\readme.md EndProjectSection EndProject diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 0000000..3f0e003 --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/NuGet.config b/NuGet.config deleted file mode 100644 index de5b6ae..0000000 --- a/NuGet.config +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/UpgradeLog.htm b/UpgradeLog.htm deleted file mode 100644 index 666769e..0000000 Binary files a/UpgradeLog.htm and /dev/null differ diff --git a/src/HtmlTags.AspNetCore.TestSite/Controllers/HomeController.cs b/src/HtmlTags.AspNetCore.TestSite/Controllers/HomeController.cs index 1cb01d9..841d4be 100644 --- a/src/HtmlTags.AspNetCore.TestSite/Controllers/HomeController.cs +++ b/src/HtmlTags.AspNetCore.TestSite/Controllers/HomeController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; @@ -19,8 +20,12 @@ public HomeIndexModel() } public class BlargModel { + [Required] + [MinLength(10)] public string Blorg { get; set; } } + [Required] + [MaxLength(20)] public string Value { get; set; } public Blarg Blarg { get; set; } diff --git a/src/HtmlTags.AspNetCore.TestSite/Views/Home/Index.cshtml b/src/HtmlTags.AspNetCore.TestSite/Views/Home/Index.cshtml index 4f5e0e1..f56984d 100644 --- a/src/HtmlTags.AspNetCore.TestSite/Views/Home/Index.cshtml +++ b/src/HtmlTags.AspNetCore.TestSite/Views/Home/Index.cshtml @@ -67,6 +67,7 @@ Next + @@ -86,26 +87,37 @@ @Html.Lookup(nameof(Model.Value)).RemoveClass("readonly") - @(Html.ButtonGroup(Html.PrimaryButton("Save"), - Html.LinkButton("Cancel", nameof(HomeController.Index)))) - + @(Html.ButtonGroup(Html.PrimaryButton("Save"), + Html.LinkButton("Cancel", nameof(HomeController.Index)))) + Input Tag value: + Editor for value: @Html.EditorFor(m => m.Value) + Input tag blarg: + Editor for blarg: @Html.EditorFor(m => m.Blarg) + Input tag blorg.blorg: + Editor for blorg.blorg: @Html.EditorFor(m => m.Blorg.Blorg) + Input tag Blorgs[0].Blarg: + Editor for Blorgs[0].Blarg: @Html.EditorFor(m => m.Blorgs[0].Blorg) @Html.Input(m => m.Value).AddClass("foo") + Display tag for Value: + Display for Vlaue: @Html.Display(m => m.Value) + Label tag for Value: + Label for Value: @Html.Label(m => m.Value) - + @{ - var tag = new HtmlTag("canvas"); + var tag = new HtmlTag("canvas"); } @tag diff --git a/src/HtmlTags.AspNetCore.TestSite/web.config b/src/HtmlTags.AspNetCore.TestSite/web.config index dc0514f..8700b60 100644 --- a/src/HtmlTags.AspNetCore.TestSite/web.config +++ b/src/HtmlTags.AspNetCore.TestSite/web.config @@ -1,14 +1,12 @@  - - - + - + - + \ No newline at end of file diff --git a/src/HtmlTags.AspNetCore/ElementName.cs b/src/HtmlTags.AspNetCore/ElementName.cs new file mode 100644 index 0000000..dda50fc --- /dev/null +++ b/src/HtmlTags.AspNetCore/ElementName.cs @@ -0,0 +1,9 @@ +namespace HtmlTags +{ + public class ElementName + { + public ElementName(string value) => Value = value; + + public string Value { get; } + } +} \ No newline at end of file diff --git a/src/HtmlTags.AspNetCore/HtmlHelperExtensions.cs b/src/HtmlTags.AspNetCore/HtmlHelperExtensions.cs index 134219f..d4b2631 100644 --- a/src/HtmlTags.AspNetCore/HtmlHelperExtensions.cs +++ b/src/HtmlTags.AspNetCore/HtmlHelperExtensions.cs @@ -1,4 +1,9 @@ -namespace HtmlTags +using System.Linq; +using HtmlTags.Reflection; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; + +namespace HtmlTags { using System; using System.Linq.Expressions; @@ -9,39 +14,51 @@ public static class HtmlHelperExtensions { - public static HtmlTag Input(this IHtmlHelper helper, Expression> expression) + private static readonly DotNotationElementNamingConvention NamingConvention = new DotNotationElementNamingConvention(); + + public static HtmlTag Input(this IHtmlHelper helper, Expression> expression) where T : class { - var generator = GetGenerator(helper); + var generator = GetGenerator(helper, expression); return generator.InputFor(expression); } - public static HtmlTag Label(this IHtmlHelper helper, Expression> expression) + public static HtmlTag Label(this IHtmlHelper helper, Expression> expression) where T : class { - var generator = GetGenerator(helper); + var generator = GetGenerator(helper, expression); return generator.LabelFor(expression); } - public static HtmlTag Display(this IHtmlHelper helper, Expression> expression) + public static HtmlTag Display(this IHtmlHelper helper, Expression> expression) where T : class { - var generator = GetGenerator(helper); + var generator = GetGenerator(helper, expression); return generator.DisplayFor(expression); } - public static HtmlTag Tag(this IHtmlHelper helper, Expression> expression, string category) + public static HtmlTag Tag(this IHtmlHelper helper, Expression> expression, string category) where T : class { - var generator = GetGenerator(helper); + var generator = GetGenerator(helper, expression); return generator.TagFor(expression, category); } - public static IElementGenerator GetGenerator(IHtmlHelper helper) where T : class + public static IElementGenerator GetGenerator(IHtmlHelper helper, Expression> expression) where T : class { - var library = helper.ViewContext.HttpContext.RequestServices.GetService(); - return ElementGenerator.For(library, t => helper.ViewContext.HttpContext.RequestServices.GetService(t), helper.ViewData.Model); + var modelExplorer = + ExpressionMetadataProvider.FromLambdaExpression(expression, helper.ViewData, helper.MetadataProvider); + + var elementName = new ElementName(NamingConvention.GetName(typeof(T), expression.ToAccessor())); + + return GetGenerator(helper, modelExplorer, helper.ViewContext, elementName); } + public static IElementGenerator GetGenerator(IHtmlHelper helper, params object[] additionalServices) where T : class + { + var library = helper.ViewContext.HttpContext.RequestServices.GetService(); + object ServiceLocator(Type t) => additionalServices.FirstOrDefault(t.IsInstanceOfType) ?? helper.ViewContext.HttpContext.RequestServices.GetService(t); + return ElementGenerator.For(library, ServiceLocator, helper.ViewData.Model); + } } } \ No newline at end of file diff --git a/src/HtmlTags.AspNetCore/HtmlTagTagHelper.cs b/src/HtmlTags.AspNetCore/HtmlTagTagHelper.cs index 7119839..204e09f 100644 --- a/src/HtmlTags.AspNetCore/HtmlTagTagHelper.cs +++ b/src/HtmlTags.AspNetCore/HtmlTagTagHelper.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; namespace HtmlTags { @@ -34,7 +35,16 @@ public override void Process(TagHelperContext context, TagHelperOutput output) var library = ViewContext.HttpContext.RequestServices.GetService(); - var tagGenerator = new TagGenerator(library.TagLibrary, new ActiveProfile(), t => ViewContext.HttpContext.RequestServices.GetService(t)); + var additionalServices = new object[] + { + For.ModelExplorer, + ViewContext, + new ElementName(For.Name) + }; + + object ServiceLocator(Type t) => additionalServices.FirstOrDefault(t.IsInstanceOfType) ?? ViewContext.HttpContext.RequestServices.GetService(t); + + var tagGenerator = new TagGenerator(library.TagLibrary, new ActiveProfile(), ServiceLocator); var tag = tagGenerator.Build(request, Category); diff --git a/src/HtmlTags.AspNetCore/HtmlTags.AspNetCore.csproj b/src/HtmlTags.AspNetCore/HtmlTags.AspNetCore.csproj index dda8927..799ec56 100644 --- a/src/HtmlTags.AspNetCore/HtmlTags.AspNetCore.csproj +++ b/src/HtmlTags.AspNetCore/HtmlTags.AspNetCore.csproj @@ -1,18 +1,12 @@ - Easy generation of html with a jquery inspired object model - Copyright 2008-2016 Jeremy D. Miller, Josh Arnold, Joshua Flanagan, et al. All rights reserved. - 6.0.0 - Jeremy D. Miller;Joshua Flanagan;Josh Arnold netstandard2.0 $(DefineConstants);ASPNETCORE HtmlTags.AspNetCore + HtmlTags HtmlTags.AspNetCore html;ASP.NET MVC - https://raw.githubusercontent.com/HtmlTags/htmltags/master/logo/FubuHtml_256.png - https://github.com/HtmlTags/htmltags - https://github.com/HtmlTags/htmltags/raw/master/license.txt @@ -25,10 +19,8 @@ + - - - diff --git a/src/HtmlTags.AspNetCore/ModelMetadataTagExtensions.cs b/src/HtmlTags.AspNetCore/ModelMetadataTagExtensions.cs new file mode 100644 index 0000000..f9e3fe2 --- /dev/null +++ b/src/HtmlTags.AspNetCore/ModelMetadataTagExtensions.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using HtmlTags.Conventions; +using HtmlTags.Conventions.Elements; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewFeatures; + +namespace HtmlTags +{ + public static class ModelMetadataTagExtensions + { + public static void ModelMetadata(this HtmlConventionRegistry registry) + { + registry.Labels.Modifier(); + registry.Displays.Modifier(); + registry.Editors.Modifier(); + registry.Editors.Modifier(); + registry.Editors.Modifier(); + registry.Editors.Modifier(); + } + + private class DisplayNameElementModifier : IElementModifier + { + public bool Matches(ElementRequest token) + => token.Get()?.Metadata.DisplayName != null; + + public void Modify(ElementRequest request) + => request.CurrentTag.Text(request.Get().Metadata.DisplayName); + } + + private static object BuildFormattedModelValue(ElementRequest request, Func formatStringFinder) + { + var modelMetadata = request.Get().Metadata; + + var formattedModelValue = request.RawValue; + + if (request.RawValue == null) + { + formattedModelValue = modelMetadata.NullDisplayText; + } + + var formatString = formatStringFinder(modelMetadata); + + if (formatString != null && formattedModelValue != null) + { + formattedModelValue = string.Format(CultureInfo.CurrentCulture, formatString, formattedModelValue); + } + + return formattedModelValue; + } + + private class MetadataModelDisplayModifier : IElementModifier + { + public bool Matches(ElementRequest token) + => token.Get() != null; + + public void Modify(ElementRequest request) + => request.CurrentTag.Text(BuildFormattedModelValue(request, m => m.DisplayFormatString)?.ToString()); + } + + private class MetadataModelEditModifier : IElementModifier + { + public bool Matches(ElementRequest token) + => token.Get() != null; + + public void Modify(ElementRequest request) + => request.CurrentTag.Value(BuildFormattedModelValue(request, m => m.EditFormatString)?.ToString()); + } + + private class PlaceholderElementModifier : IElementModifier + { + public bool Matches(ElementRequest token) + => token.Get()?.Metadata.Placeholder != null; + + public void Modify(ElementRequest request) + => request.CurrentTag.Attr("placeholder", request.Get().Metadata.Placeholder); + } + + private class ModelStateErrorsModifier : IElementModifier + { + public bool Matches(ElementRequest token) + => token.TryGet(out ViewContext viewContext) + && token.TryGet(out ElementName elementName) + && viewContext.ViewData.ModelState.TryGetValue(elementName.Value, out var entry) + && entry.Errors.Count > 0; + + public void Modify(ElementRequest request) + => request.CurrentTag.AddClass(HtmlHelper.ValidationInputCssClassName); + } + + private class ClientSideValidationModifier : IElementModifier + { + public bool Matches(ElementRequest token) + => token.TryGet(out ViewContext viewContext) + && viewContext.ClientValidationEnabled; + + public void Modify(ElementRequest request) + { + var validationProvider = request.Get(); + var viewContext = request.Get(); + var modelExplorer = request.Get(); + var attributes = new Dictionary(); + + validationProvider.AddValidationAttributes(viewContext, modelExplorer, attributes); + + request.CurrentTag.MergeAttributes(attributes); + } + } + } +} \ No newline at end of file diff --git a/src/HtmlTags.AspNetCore/ServiceCollectionExtensions.cs b/src/HtmlTags.AspNetCore/ServiceCollectionExtensions.cs index 14fd1f0..e6574c6 100644 --- a/src/HtmlTags.AspNetCore/ServiceCollectionExtensions.cs +++ b/src/HtmlTags.AspNetCore/ServiceCollectionExtensions.cs @@ -6,30 +6,28 @@ public static class ServiceCollectionExtensions { - public static void AddHtmlTags(this IServiceCollection services, HtmlConventionLibrary library) - { - services.AddSingleton(library); - } + public static IServiceCollection AddHtmlTags(this IServiceCollection services, HtmlConventionLibrary library) => services.AddSingleton(library); - public static void AddHtmlTags(this IServiceCollection services, params HtmlConventionRegistry[] registries) + public static IServiceCollection AddHtmlTags(this IServiceCollection services, params HtmlConventionRegistry[] registries) { var library = new HtmlConventionLibrary(); foreach (var registry in registries) { registry.Apply(library); } - services.AddHtmlTags(library); + return services.AddHtmlTags(library); } - public static void AddHtmlTags(this IServiceCollection services, Action config) + public static IServiceCollection AddHtmlTags(this IServiceCollection services, Action config) { var registry = new HtmlConventionRegistry(); config(registry); registry.Defaults(); + registry.ModelMetadata(); - services.AddHtmlTags(registry); + return services.AddHtmlTags(registry); } } } \ No newline at end of file diff --git a/src/HtmlTags/Conventions/ElementGenerator.cs b/src/HtmlTags/Conventions/ElementGenerator.cs index 57caf4b..3c2deae 100644 --- a/src/HtmlTags/Conventions/ElementGenerator.cs +++ b/src/HtmlTags/Conventions/ElementGenerator.cs @@ -27,16 +27,16 @@ public static ElementGenerator For(HtmlConventionLibrary library, Func> expression, string profile = null, T model = null) + public HtmlTag LabelFor(Expression> expression, string profile = null, T model = null) => Build(expression, ElementConstants.Label, profile, model); - public HtmlTag InputFor(Expression> expression, string profile = null, T model = null) + public HtmlTag InputFor(Expression> expression, string profile = null, T model = null) => Build(expression, ElementConstants.Editor, profile, model); - public HtmlTag DisplayFor(Expression> expression, string profile = null, T model = null) + public HtmlTag DisplayFor(Expression> expression, string profile = null, T model = null) => Build(expression, ElementConstants.Display, profile, model); - public HtmlTag TagFor(Expression> expression, string category, string profile = null, T model = null) + public HtmlTag TagFor(Expression> expression, string category, string profile = null, T model = null) => Build(expression, category, profile, model); public T Model @@ -45,7 +45,7 @@ public T Model set { _model = new Lazy(() => value); } } - public ElementRequest GetRequest(Expression> expression, T model = null) + public ElementRequest GetRequest(Expression> expression, T model = null) { return new ElementRequest(expression.ToAccessor()) { @@ -53,7 +53,7 @@ public ElementRequest GetRequest(Expression> expression, T model }; } - private HtmlTag Build(Expression> expression, string category, string profile = null, T model = null) + private HtmlTag Build(Expression> expression, string category, string profile = null, T model = null) { ElementRequest request = GetRequest(expression, model); return _tags.Build(request, category, profile); diff --git a/src/HtmlTags/Conventions/ElementRequest.cs b/src/HtmlTags/Conventions/ElementRequest.cs index 26880d4..c1b5bcd 100644 --- a/src/HtmlTags/Conventions/ElementRequest.cs +++ b/src/HtmlTags/Conventions/ElementRequest.cs @@ -1,8 +1,6 @@ namespace HtmlTags.Conventions { using System; - using System.Linq.Expressions; - using System.Reflection; using Elements; using Formatting; using Reflection; @@ -13,24 +11,6 @@ public class ElementRequest private object _rawValue; private Func _services; - public static ElementRequest For(object model, PropertyInfo property) - { - return new ElementRequest(new SingleProperty(property)) - { - Model = model - }; - } - - public static ElementRequest For(Expression> expression) => new ElementRequest(expression.ToAccessor()); - - public static ElementRequest For(T model, Expression> expression) - { - return new ElementRequest(expression.ToAccessor()) - { - Model = model - }; - } - public ElementRequest(Accessor accessor) { Accessor = accessor; @@ -79,6 +59,8 @@ public void ReplaceTag(HtmlTag tag) public T Get() => (T)_services(typeof(T)); + public bool TryGet(out T service) => (service = (T) _services(typeof(T))) != null; + // virtual for mocking public virtual HtmlTag BuildForCategory(string category, string profile = null) => Get().Build(this, category, profile); diff --git a/src/HtmlTags/Conventions/Elements/IElementGenerator.cs b/src/HtmlTags/Conventions/Elements/IElementGenerator.cs index 5510c94..ef97b48 100644 --- a/src/HtmlTags/Conventions/Elements/IElementGenerator.cs +++ b/src/HtmlTags/Conventions/Elements/IElementGenerator.cs @@ -6,10 +6,10 @@ namespace HtmlTags.Conventions.Elements public interface IElementGenerator where T : class { T Model { get; set; } - HtmlTag LabelFor(Expression> expression, string profile = null, T model = null); - HtmlTag InputFor(Expression> expression, string profile = null, T model = null); - HtmlTag DisplayFor(Expression> expression, string profile = null, T model = null); - HtmlTag TagFor(Expression> expression, string category, string profile = null, T model = null); + HtmlTag LabelFor(Expression> expression, string profile = null, T model = null); + HtmlTag InputFor(Expression> expression, string profile = null, T model = null); + HtmlTag DisplayFor(Expression> expression, string profile = null, T model = null); + HtmlTag TagFor(Expression> expression, string category, string profile = null, T model = null); HtmlTag LabelFor(ElementRequest request, string profile = null, T model = null); HtmlTag InputFor(ElementRequest request, string profile = null, T model = null); diff --git a/src/HtmlTags/Conventions/TagGenerator.cs b/src/HtmlTags/Conventions/TagGenerator.cs index 0c6dbdb..d6c879f 100644 --- a/src/HtmlTags/Conventions/TagGenerator.cs +++ b/src/HtmlTags/Conventions/TagGenerator.cs @@ -40,6 +40,8 @@ public HtmlTag Build(ElementRequest request, string category = null, string prof var token = request.ToToken(); + token.Attach(_serviceLocator); + var plan = _library.PlanFor(token, profile, category); request.Attach(_serviceLocator); diff --git a/src/HtmlTags/HtmlTag.cs b/src/HtmlTags/HtmlTag.cs index 2674a97..0d53994 100755 --- a/src/HtmlTags/HtmlTag.cs +++ b/src/HtmlTags/HtmlTag.cs @@ -372,6 +372,15 @@ public HtmlTag AppendText(string text) return this; } + public HtmlTag MergeAttributes(IDictionary attributes) + { + foreach (var attribute in attributes) + { + Attr(attribute.Key, attribute.Value); + } + + return this; + } public HtmlTag Modify(Action action) { @@ -799,6 +808,7 @@ public HtmlTag TextIfEmpty(string defaultText) public HtmlTag Name(string name) => Attr("name", name); public HtmlTag Value(string value) => Attr("value", value); + public string Value() => Attr("value"); } } diff --git a/src/HtmlTags/HtmlTags.csproj b/src/HtmlTags/HtmlTags.csproj index 36073a5..993e019 100644 --- a/src/HtmlTags/HtmlTags.csproj +++ b/src/HtmlTags/HtmlTags.csproj @@ -1,17 +1,10 @@ - Easy generation of html with a jquery inspired object model - Copyright 2008-2016 Jeremy D. Miller, Josh Arnold, Joshua Flanagan, et al. All rights reserved. - 6.0.0 - Jeremy D. Miller;Joshua Flanagan;Josh Arnold;Jimmy Bogard net45 HtmlTags HtmlTags html;ASP.NET MVC - https://raw.githubusercontent.com/HtmlTags/htmltags/master/logo/FubuHtml_256.png - https://github.com/HtmlTags/htmltags - https://github.com/HtmlTags/htmltags/raw/master/license.txt diff --git a/src/HtmlTags/Reflection/ReflectionExtensions.cs b/src/HtmlTags/Reflection/ReflectionExtensions.cs index 740f6eb..8e685f3 100644 --- a/src/HtmlTags/Reflection/ReflectionExtensions.cs +++ b/src/HtmlTags/Reflection/ReflectionExtensions.cs @@ -37,7 +37,7 @@ public static void ForAttribute(this Accessor accessor, Action action) whe public static bool HasAttribute(this Accessor provider) where T : Attribute => provider.InnerProperty.GetCustomAttribute() != null; - public static Accessor ToAccessor(this Expression> expression) => ReflectionHelper.GetAccessor(expression); + public static Accessor ToAccessor(this Expression> expression) => ReflectionHelper.GetAccessor(expression); public static string GetName(this Expression> expression) => ReflectionHelper.GetAccessor(expression).Name; diff --git a/test/HtmlTags.AspNetCore.Testing/HtmlTags.AspNetCore.Testing.csproj b/test/HtmlTags.AspNetCore.Testing/HtmlTags.AspNetCore.Testing.csproj index b9f3de1..ba995fd 100644 --- a/test/HtmlTags.AspNetCore.Testing/HtmlTags.AspNetCore.Testing.csproj +++ b/test/HtmlTags.AspNetCore.Testing/HtmlTags.AspNetCore.Testing.csproj @@ -22,11 +22,13 @@ - - - - - + + + + + + + diff --git a/test/HtmlTags.AspNetCore.Testing/ModelMetadataTagExtensionsTester.cs b/test/HtmlTags.AspNetCore.Testing/ModelMetadataTagExtensionsTester.cs new file mode 100644 index 0000000..94d758c --- /dev/null +++ b/test/HtmlTags.AspNetCore.Testing/ModelMetadataTagExtensionsTester.cs @@ -0,0 +1,614 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using HtmlTags.Conventions; +using Microsoft.AspNetCore.Antiforgery; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.DataAnnotations; +using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal; +using Microsoft.AspNetCore.Mvc.Internal; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Mvc.ViewEngines; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.WebEncoders.Testing; +using Moq; +using Shouldly; +using Xunit; + +namespace HtmlTags.Testing +{ + public class ModelMetadataTagExtensionsTester + { + class Subject + { + [Display(Name = "Hello", Prompt = "Value Here")] + [DisplayFormat(DataFormatString = "Foo {0} Bar", ApplyFormatInEditMode = true, NullDisplayText = "Bunny")] + [Required] + [MaxLength(10)] + public string Value { get; set; } + } + + [Fact] + public void ShouldBuildLabelFromDisplayAttribute() + { + var subject = new Subject {Value = "Value"}; + var helper = GetHtmlHelper(subject); + + var label = helper.Label(s => s.Value); + label.Text().ShouldBe("Hello"); + } + + [Fact] + public void ShouldBuildDisplayFromDisplayFormat() + { + var subject = new Subject {Value = "Value"}; + var helper = GetHtmlHelper(subject); + + var display = helper.Display(s => s.Value); + display.Text().ShouldBe("Foo Value Bar"); + } + + [Fact] + public void ShouldBuildInputValueFromEditFormat() + { + var subject = new Subject {Value = "Value"}; + var helper = GetHtmlHelper(subject); + + var editor = helper.Input(s => s.Value); + editor.Value().ShouldBe("Foo Value Bar"); + } + + [Fact] + public void ShouldSetPlaceholderForInput() + { + var subject = new Subject {Value = "Value"}; + var helper = GetHtmlHelper(subject); + + var editor = helper.Input(s => s.Value); + editor.Attr("placeholder").ShouldBe("Value Here"); + } + + [Fact] + public void ShouldUseNullDisplayTextForDisplay() + { + var subject = new Subject {Value = null}; + var helper = GetHtmlHelper(subject); + + var editor = helper.Display(s => s.Value); + editor.Text().ShouldBe("Foo Bunny Bar"); + } + + [Fact] + public void ShouldUseNullDisplayTextForEdit() + { + var subject = new Subject { Value = null }; + var helper = GetHtmlHelper(subject); + + var editor = helper.Input(s => s.Value); + editor.Value().ShouldBe("Foo Bunny Bar"); + } + + [Fact] + public void ShouldAddValidationClassForInvalidValues() + { + var subject = new Subject { Value = null }; + var helper = GetHtmlHelper(subject); + + helper.ViewData.ModelState.IsValid.ShouldBeFalse(); + + var editor = helper.Input(s => s.Value); + editor.HasClass(HtmlHelper.ValidationInputCssClassName).ShouldBeTrue(); + } + + [Fact] + public void ShouldAddClientSideValidationClasses() + { + var subject = new Subject { Value = "value" }; + var helper = GetHtmlHelper(subject); + + var editor = helper.Input(s => s.Value); + editor.Attr("data-val").ShouldBe("true"); + editor.Attr("data-val-maxlength").ShouldNotBeNullOrEmpty(); + editor.Attr("data-val-maxlength-max").ShouldNotBeNullOrEmpty(); + } + + + [Fact] + public void ShouldNotAddClientSideValidationClassesWhenNoClientValidationEnabled() + { + var subject = new Subject { Value = "value" }; + var helper = GetHtmlHelper(subject); + helper.ViewContext.ClientValidationEnabled = false; + + var editor = helper.Input(s => s.Value); + editor.Attr("data-val").ShouldBeNullOrEmpty(); + editor.Attr("data-val-maxlength").ShouldBeNullOrEmpty(); + editor.Attr("data-val-maxlength-max").ShouldBeNullOrEmpty(); + } + + public static HtmlHelper GetHtmlHelper(TModel model) + { + return GetHtmlHelper(model, CreateViewEngine()); + } + + public static HtmlHelper GetHtmlHelper( + TModel model, + ICompositeViewEngine viewEngine, + IStringLocalizerFactory stringLocalizerFactory = null) + { + return GetHtmlHelper( + model, + CreateUrlHelper(), + viewEngine, + TestModelMetadataProvider.CreateDefaultProvider(stringLocalizerFactory)); + } + + 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, new ModelStateDictionary()); + viewData.Model = model; + + return GetHtmlHelper( + viewData, + urlHelper, + viewEngine, + provider, + innerHelperWrapper, + htmlGenerator: null, + idAttributeDotReplacement: null); + } + + private static HtmlHelper GetHtmlHelper( + ViewDataDictionary viewData, + IUrlHelper urlHelper, + ICompositeViewEngine viewEngine, + IModelMetadataProvider provider, + Func innerHelperWrapper, + IHtmlGenerator htmlGenerator, + string idAttributeDotReplacement) + { + var httpContext = new DefaultHttpContext(); + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor(), viewData.ModelState); + + var options = new MvcViewOptions(); + if (!string.IsNullOrEmpty(idAttributeDotReplacement)) + { + options.HtmlHelperOptions.IdAttributeDotReplacement = idAttributeDotReplacement; + } + var localizationOptionsAccesor = new Mock>(); + + localizationOptionsAccesor.SetupGet(o => o.Value).Returns(new MvcDataAnnotationsLocalizationOptions()); + + options.ClientModelValidatorProviders.Add(new DataAnnotationsClientModelValidatorProvider( + new ValidationAttributeAdapterProvider(), + localizationOptionsAccesor.Object, + stringLocalizerFactory: null)); + var optionsAccessor = new Mock>(); + optionsAccessor + .SetupGet(o => o.Value) + .Returns(options); + + var valiatorProviders = new[] +{ + new DataAnnotationsModelValidatorProvider( + new ValidationAttributeAdapterProvider(), + new TestOptionsManager(), + stringLocalizerFactory: null), + }; + + var validator = new DefaultObjectValidator(provider, valiatorProviders); + + validator.Validate(actionContext, validationState: null, prefix: string.Empty, viewData.Model); + + var urlHelperFactory = new Mock(); + urlHelperFactory + .Setup(f => f.GetUrlHelper(It.IsAny())) + .Returns(urlHelper); + + var expressionTextCache = new ExpressionTextCache(); + + var attributeProvider = new DefaultValidationHtmlAttributeProvider( + optionsAccessor.Object, + provider, + new ClientValidatorCache()); + + if (htmlGenerator == null) + { + htmlGenerator = new DefaultHtmlGenerator( + Mock.Of(), + optionsAccessor.Object, + provider, + urlHelperFactory.Object, + new HtmlTestEncoder(), + attributeProvider); + } + + // TemplateRenderer will Contextualize this transient service. + var innerHelper = (IHtmlHelper)new HtmlHelper( + htmlGenerator, + viewEngine, + provider, + new TestViewBufferScope(), + new HtmlTestEncoder(), + UrlEncoder.Default); + + if (innerHelperWrapper != null) + { + innerHelper = innerHelperWrapper(innerHelper); + } + + var registry = new HtmlConventionRegistry(); + registry.ModelMetadata(); + registry.Defaults(); + + var library = new HtmlConventionLibrary(); + registry.Apply(library); + + var serviceCollection = new ServiceCollection(); + + serviceCollection + .AddSingleton(viewEngine) + .AddSingleton(urlHelperFactory.Object) + .AddSingleton(Mock.Of()) + .AddSingleton(innerHelper) + .AddSingleton() + .AddSingleton(attributeProvider) + .AddHtmlTags(library); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + + httpContext.RequestServices = serviceProvider; + + var htmlHelper = new HtmlHelper( + htmlGenerator, + viewEngine, + provider, + new TestViewBufferScope(), + new HtmlTestEncoder(), + UrlEncoder.Default, + expressionTextCache); + + var viewContext = new ViewContext( + actionContext, + Mock.Of(), + viewData, + new TempDataDictionary( + httpContext, + Mock.Of()), + new StringWriter(), + options.HtmlHelperOptions) + { + ClientValidationEnabled = true + }; + + htmlHelper.Contextualize(viewContext); + + return htmlHelper; + } + + private static ICompositeViewEngine CreateViewEngine() + { + var view = new Mock(); + view + .Setup(v => v.RenderAsync(It.IsAny())) + .Callback(async (ViewContext v) => + { + view.ToString(); + await v.Writer.WriteAsync(FormatOutput(v.ViewData.ModelExplorer)); + }) + .Returns(Task.FromResult(0)); + + var viewEngine = new Mock(MockBehavior.Strict); + viewEngine + .Setup(v => v.GetView(/*executingFilePath*/ null, It.IsAny(), /*isMainPage*/ false)) + .Returns(ViewEngineResult.NotFound("MyView", Enumerable.Empty())) + .Verifiable(); + viewEngine + .Setup(v => v.FindView(It.IsAny(), It.IsAny(), /*isMainPage*/ false)) + .Returns(ViewEngineResult.Found("MyView", view.Object)) + .Verifiable(); + + return viewEngine.Object; + } + + private static IUrlHelper CreateUrlHelper() + { + return Mock.Of(); + } + + private static string FormatOutput(ModelExplorer modelExplorer) + { + var metadata = modelExplorer.Metadata; + return string.Format( + CultureInfo.InvariantCulture, + "Model = {0}, ModelType = {1}, PropertyName = {2}, SimpleDisplayText = {3}", + modelExplorer.Model ?? "(null)", + metadata.ModelType == null ? "(null)" : metadata.ModelType.FullName, + metadata.PropertyName ?? "(null)", + modelExplorer.GetSimpleDisplayText() ?? "(null)"); + } + + public class TestOptionsManager : IOptions + where TOptions : class, new() + { + public TestOptionsManager() + : this(new TOptions()) + { + } + + public TestOptionsManager(TOptions value) + { + Value = value; + } + + public TOptions Value { get; } + } + + public class TestViewBufferScope : IViewBufferScope + { + public IList CreatedBuffers { get; } = new List(); + + public IList ReturnedBuffers { get; } = new List(); + + public ViewBufferValue[] GetPage(int size) + { + var buffer = new ViewBufferValue[size]; + CreatedBuffers.Add(buffer); + return buffer; + } + + public void ReturnSegment(ViewBufferValue[] segment) + { + ReturnedBuffers.Add(segment); + } + + public PagedBufferedTextWriter CreateWriter(TextWriter writer) + { + return new PagedBufferedTextWriter(ArrayPool.Shared, writer); + } + } + + public class TestModelMetadataProvider : DefaultModelMetadataProvider + { + // Creates a provider with all the defaults - includes data annotations + public static IModelMetadataProvider CreateDefaultProvider(IStringLocalizerFactory stringLocalizerFactory = null) + { + var detailsProviders = new IMetadataDetailsProvider[] + { + new DefaultBindingMetadataProvider(), + new DefaultValidationMetadataProvider(), + new DataAnnotationsMetadataProvider( + new TestOptionsManager(), + stringLocalizerFactory), + new DataMemberRequiredBindingMetadataProvider(), + }; + + var compositeDetailsProvider = new DefaultCompositeMetadataDetailsProvider(detailsProviders); + return new DefaultModelMetadataProvider(compositeDetailsProvider, new TestOptionsManager()); + } + + public static IModelMetadataProvider CreateDefaultProvider(IList providers) + { + var detailsProviders = new List() + { + new DefaultBindingMetadataProvider(), + new DefaultValidationMetadataProvider(), + new DataAnnotationsMetadataProvider( + new TestOptionsManager(), + stringLocalizerFactory: null), + new DataMemberRequiredBindingMetadataProvider(), + }; + + detailsProviders.AddRange(providers); + + var compositeDetailsProvider = new DefaultCompositeMetadataDetailsProvider(detailsProviders); + return new DefaultModelMetadataProvider(compositeDetailsProvider, new TestOptionsManager()); + } + + public static IModelMetadataProvider CreateProvider(IList providers) + { + var detailsProviders = new List(); + if (providers != null) + { + detailsProviders.AddRange(providers); + } + + var compositeDetailsProvider = new DefaultCompositeMetadataDetailsProvider(detailsProviders); + return new DefaultModelMetadataProvider(compositeDetailsProvider, new TestOptionsManager()); + } + + private readonly TestModelMetadataDetailsProvider _detailsProvider; + + public TestModelMetadataProvider() + : this(new TestModelMetadataDetailsProvider()) + { + } + + private TestModelMetadataProvider(TestModelMetadataDetailsProvider detailsProvider) + : base( + new DefaultCompositeMetadataDetailsProvider(new IMetadataDetailsProvider[] + { + new DefaultBindingMetadataProvider(), + new DefaultValidationMetadataProvider(), + new DataAnnotationsMetadataProvider( + new TestOptionsManager(), + stringLocalizerFactory: null), + detailsProvider + }), + new TestOptionsManager()) + { + _detailsProvider = detailsProvider; + } + + public IMetadataBuilder ForType(Type type) + { + var key = ModelMetadataIdentity.ForType(type); + + var builder = new MetadataBuilder(key); + _detailsProvider.Builders.Add(builder); + return builder; + } + + public IMetadataBuilder ForType() + { + return ForType(typeof(TModel)); + } + + public IMetadataBuilder ForProperty(Type containerType, string propertyName) + { + var property = containerType.GetRuntimeProperty(propertyName); + Assert.NotNull(property); + + var key = ModelMetadataIdentity.ForProperty(property.PropertyType, propertyName, containerType); + + var builder = new MetadataBuilder(key); + _detailsProvider.Builders.Add(builder); + return builder; + } + + public IMetadataBuilder ForProperty(string propertyName) + { + return ForProperty(typeof(TContainer), propertyName); + } + + private class TestModelMetadataDetailsProvider : + IBindingMetadataProvider, + IDisplayMetadataProvider, + IValidationMetadataProvider + { + public List Builders { get; } = new List(); + + public void CreateBindingMetadata(BindingMetadataProviderContext context) + { + foreach (var builder in Builders) + { + builder.Apply(context); + } + } + + public void CreateDisplayMetadata(DisplayMetadataProviderContext context) + { + foreach (var builder in Builders) + { + builder.Apply(context); + } + } + + public void CreateValidationMetadata(ValidationMetadataProviderContext context) + { + foreach (var builder in Builders) + { + builder.Apply(context); + } + } + } + + public interface IMetadataBuilder + { + IMetadataBuilder BindingDetails(Action action); + + IMetadataBuilder DisplayDetails(Action action); + + IMetadataBuilder ValidationDetails(Action action); + } + + private class MetadataBuilder : IMetadataBuilder + { + private List> _bindingActions = new List>(); + private List> _displayActions = new List>(); + private List> _valiationActions = new List>(); + + private readonly ModelMetadataIdentity _key; + + public MetadataBuilder(ModelMetadataIdentity key) + { + _key = key; + } + + public void Apply(BindingMetadataProviderContext context) + { + if (_key.Equals(context.Key)) + { + foreach (var action in _bindingActions) + { + action(context.BindingMetadata); + } + } + } + + public void Apply(DisplayMetadataProviderContext context) + { + if (_key.Equals(context.Key)) + { + foreach (var action in _displayActions) + { + action(context.DisplayMetadata); + } + } + } + + public void Apply(ValidationMetadataProviderContext context) + { + if (_key.Equals(context.Key)) + { + foreach (var action in _valiationActions) + { + action(context.ValidationMetadata); + } + } + } + + public IMetadataBuilder BindingDetails(Action action) + { + _bindingActions.Add(action); + return this; + } + + public IMetadataBuilder DisplayDetails(Action action) + { + _displayActions.Add(action); + return this; + } + + public IMetadataBuilder ValidationDetails(Action action) + { + _valiationActions.Add(action); + return this; + } + } + } + + } + +} \ No newline at end of file diff --git a/test/HtmlTags.Testing/HtmlTags.Testing.csproj b/test/HtmlTags.Testing/HtmlTags.Testing.csproj index 1455f80..1d7162f 100644 --- a/test/HtmlTags.Testing/HtmlTags.Testing.csproj +++ b/test/HtmlTags.Testing/HtmlTags.Testing.csproj @@ -1,7 +1,7 @@ - net451 + net452 true HtmlTags.Testing HtmlTags.Testing @@ -13,12 +13,12 @@ - - - - + + + + - +