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 @@
-
-
-
-
+
+
+
+
-
+