I want to create my own custom HTML Helper like the ones used in ASP.NET MVC, but I haven't been able to find how to implement them in the correct way.
I have found how to create custom Tag Helpers but not HTML Helpers. How do I create my own custom HTML Helpers?
For me I thought my HTML helpers weren't working until I spotted that the extension method is now on IHtmlHelper not HtmlHelper.
So for .net core:
public static IHtmlContent CheckboxListFor<TModel>(this IHtmlHelper<TModel> html,
Expression<Func<TModel, List<CheckboxListItem>>> expression) ...
Instead of for .net:
public static HtmlString CheckboxListFor<TModel>(this HtmlHelper<TModel> html,
Expression<Func<TModel, List<CheckboxListItem>>> expression) ...
EDIT: I've also updated the return type for .net core to be IHtmlContent as using something like HtmlContentBuilder is a nicer way to compose HTML content and returning that returns IHtmlContent
HTML Helpers look to be supported in ASP.NET Core and are awaiting documentation:
https://learn.microsoft.com/en-au/aspnet/core/mvc/views/html-helpers
[Edit:] Since answering, the above page no longer exists. I'd say HTML Helpers, while they work, are no longer "supported" in ASP.NET Core.
Looking at the ASP.NET Core source they work fairly similarly to older versions of ASP.NET MVC:
https://github.com/aspnet/AspNetCore/blob/master/src/Mvc/Mvc.ViewFeatures/src/Rendering/HtmlHelperDisplayExtensions.cs
Example
MyHTMLHelpers.cs:
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
using System;
namespace MyApp.Helpers
{
public static class MyHTMLHelpers
{
public static IHtmlContent HelloWorldHTMLString(this IHtmlHelper htmlHelper)
=> new HtmlString("<strong>Hello World</strong>");
public static String HelloWorldString(this IHtmlHelper htmlHelper)
=> "<strong>Hello World</strong>";
}
}
_ViewImports.cshtml (second line is the important change):
#using MyApp
#using MyApp.Helpers
#addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
MyView.cshtml:
<div>#Html.HelloWorldHTMLString()</div>
<div>#Html.HelloWorldString()</div>
Outputs:
Hello World
<strong>Hello World</strong>
Here is an example for .Net Core 2 using TagBuilders
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
using System.IO;
public static IHtmlContent HelloWorld(this IHtmlHelper html, string name)
{
var span = new TagBuilder("span");
span.InnerHtml.Append("Hello, " + name + "!");
var br = new TagBuilder("br") {TagRenderMode = TagRenderMode.SelfClosing};
string result;
using (var writer = new StringWriter())
{
span.WriteTo(writer, System.Text.Encodings.Web.HtmlEncoder.Default);
br.WriteTo(writer, System.Text.Encodings.Web.HtmlEncoder.Default);
result = writer.ToString();
}
return new HtmlString(result);
}
I was never able to get HtmlHelper extension methods to work, I always recieved:
'IHtmlHelper' does not contain a definition for 'MethodName' and no extension method 'MethodName' accepting a first argument of type 'IHtmlHelper' could be found (are you missing a using directive or an assembly reference?)
Even though I had the proper namespace in my _ViewImports.cshtml file. So I decided to use the ability of Razor pages to now support injecting services that have been registered for dependency injection. As an example I have the need to inject some values from my configuration file into my _Layout.cshtml file. So I did the following:
1) Defined a IConfigurationHelperService interface:
public interface IConfigurationHelperService
{
string GetApiUrl();
}
2) Defined an implementation of that interface in a ConfigurationHelperSerivce class (which itself is using dependency injection to get the regular configuration class):
public class ConfigurationHelperService : IConfigurationHelperService
{
public ConfigurationHelperService(IConfiguration configuration)
{
Configuration = configuration;
}
private IConfiguration Configuration { get; }
public string GetApiUrl()
{
return GetConfigurationValue(ApiUrl);
}
private string GetConfigurationValue(string key)
{
var value = Configuration[key];
if (value.IsNullOrEmpty()) throw new KeyNotFoundException($"Configruation does not contain an instance of {key}");
return value;
}
}
3) Registered the service for injection via ConfigureServices in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IConfigurationHelperService, ConfigurationHelperService>();
services.AddMvc();
}
4) Added the proper namespace as a using statement into my _ViewImports.cshtml file.
5) Used the #inject keyword to define it for use in the _Layout.cshtml file.
#inject IConfigurationHelperService ConfigHelper
<!DOCTYPE html>
<html>
...
#ConfigHelper.GetApiUrl()
...
</html>
It worked great for me, and I can see a lot more uses for this on simpler pages where defining models would be too much work.
Well i guess this answer won't be noticed but here's what i came up with using service registrations:
I hope it helps someone.
Register the service:
services.AddTransient<IHtmlHelperFactory, HtmlHelperFactory>();
Use the service:
var helper = HttpContext.RequestServices.GetRequiredService<IHtmlHelperFactory>().Create();
Interface:
public interface IHtmlHelperFactory
{
IHtmlHelper Create();
}
Implementation:
public class HtmlHelperFactory : IHtmlHelperFactory
{
private readonly IHttpContextAccessor _contextAccessor;
public class FakeView : IView
{
/// <inheritdoc />
public Task RenderAsync(ViewContext context)
{
return Task.CompletedTask;
}
/// <inheritdoc />
public string Path { get; } = "View";
}
public HtmlHelperFactory(IHttpContextAccessor contextAccessor)
{
_contextAccessor = contextAccessor;
}
/// <inheritdoc />
public IHtmlHelper Create()
{
var modelMetadataProvider = _contextAccessor.HttpContext.RequestServices.GetRequiredService<IModelMetadataProvider>();
var tempDataProvider = _contextAccessor.HttpContext.RequestServices.GetRequiredService<ITempDataProvider>();
var htmlHelper = _contextAccessor.HttpContext.RequestServices.GetRequiredService<IHtmlHelper>();
var viewContext = new ViewContext(
new ActionContext(_contextAccessor.HttpContext, _contextAccessor.HttpContext.GetRouteData(), new ControllerActionDescriptor()),
new FakeView(),
new ViewDataDictionary(modelMetadataProvider, new ModelStateDictionary()),
new TempDataDictionary(_contextAccessor.HttpContext, tempDataProvider),
TextWriter.Null,
new HtmlHelperOptions()
);
((IViewContextAware)htmlHelper).Contextualize(viewContext);
return htmlHelper;
}
}
This has been well explained by Danny van der Kraan in his blog post here. The answer below is an extract from this post:
ASP.NET Core 1.0 [MVC 6] comes with a new exciting feature called TagHelpers. In ASP.Net Core 1.0 there is no concept of HTML Helper like in MVC.
What are TagHelpers?
TagHelpers can be seen as the evolution of HTML helpers which were introduced with the launch of the first MVC framework. To provide context you have to imagine that with classic ASP the only way you could automate the generation of HTML is via custom subroutines. After that ASP.NET came with server controls, with view states as biggest plus, to simulate the look and feel of desktop applications and help with the transition for desktop developers. But we all know what happens when we try to jam squares in to round holes. We had to face the fact that web development is nothing like desktop development. To get in line with proper web development the ASP.NET MVC framework was launched with HTML helpers to automate the HTML output. But HTML helpers never really gelled, especially not with front end developers and designers. One of the main pet peeves was that it made you switch a lot from angle brackets (HTML, CSS) to C# (Razor syntax) during work on views, which made the experience unnecessarily uncomfortable. [MVC 6] wants to address this and some smaller issues by introducing TagHelpers. Example
HTML helper:
#Html.ActionLink(”Home”, ”Index”, ”Home”)
With the anchor TagHelper this would look like:
<a asp-action="Index" asp-controller="Home">Home</a>
PS: Please note that asp- is just a convention, but more on that later.
The output rendered in the browser is the same for both:
Home
PS: Provided the default route has not been altered.
For more information about TagHelpers click here
To create a custom HTML helper you have create a static class and static method.
below example is for a custom HTML helper for submit button.
namespace TagHelpers.Helpers
{
public static class CustomHtmlHelpers
{
public static IHtmlContent SubmitButton(this IHtmlHelper htmlHelper, string value, string name )
{
string str = "<input type='submit' value ='"+ value +"'name='"+ name +"' />";
return new HtmlString(str);
}
}
}
Make sure you add below using statements.
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
To access the helper everywhere on the page you need to add the namespace in to the viewimports.cshtml file
#using TagHelpers.Helpers
Now, You can now use it on the page where you want to define a button.
<div>
#Html.SubmitButton("Login", "Command")
#Html.SubmitButton("Cancel", "Command")
</div>
Here is an example to get Enum name based on the Enum value in view. Custom HTML Helper for Enum Type
public static IHtmlContent DisplayEnumFor(this IHtmlHelper htmlHelper, string value, Type enumType)
{
if (htmlHelper == null)
throw new ArgumentNullException(nameof(htmlHelper));
if (value == null)
throw new ArgumentNullException(nameof(value));
if (!enumType.IsEnum)
throw new ArgumentException("Type must be an enumerated type");
foreach (var item in Enum.GetValues(enumType))
if (((int)item).ToString().Equals(value.Trim()))
return new HtmlString(item.ToString());
return new HtmlString(value);
}
Kept it simple but renders as expected. Make sure you have the right attributes set for the right elements. Please add suggestions if needs improvement or give your votes if it looks good.
public static class IconExtensions
{
public static IHtmlContent CCIcon(string iconName, string? toolTip = null)
{
return new HtmlString($"<img src=\"/img/{iconName}.png\" alt=\"{iconName}\" class=\"img-ca-annexicons\" title=\"{toolTip??iconName}\" />");
}
}
Related
I am relatively new to working with DI containers and have hit a bit of a roadblock.
SimpleInjector has a method with the following signature:
Container.RegisterInitializer<TService>(Action<TService>)
In our code base we do use it like this:
// this is a property injection of the abstract file system
container.RegisterInitializer<IFileSystemInjection>(
fs => fs.FileSystem = new FileSystem());
I am wondering how I would achieve the same using the IServiceCollection parameter in the ConfigureServices method in the Startup.cs class. So far I have been able to register all my types using the services.AddTransient(); but I am not sure how what the equivalent simpleinjector.RegisterInitializer is within the IServiceCollection.
You'd use the factory overload(s) of AddSingleton, AddScoped, and AddTransient. I'm not sure what scope IFileSystemInjection should be in, but it sounds like something that could be a singleton. If not, change the method you call appropriately:
service.AddSingleton<IFileSystemInjection>(p =>
{
var fs = new FileSystemInjection();
fs.FileSystem = new FileSystem();
});
In short, if you provide a factory, then you're responsible for the entire object initialization, hence the new FileSystemInjection(), which I'm subbing as the actual implementation of IFileSystemInjection your using.
If that implementation has dependencies that need to be injected in order to create it, you can pull those from p, which is an instance of IServiceProvider:
var myDep = p.GetRequiredService<MyDep>();
var fs = new FileSystemImplementation(myDep);
You can use this nuget package, that extends standard Microsoft Dependency Injection and adds property injection:
https://www.nuget.org/packages/DJMJ.Extensions.DependencyInjection.Property/1.1.0
Mark property for injection
using Microsoft.Extensions.DependencyInjection;
public class FooService
{
[Inject]
public IBooService BooService { get; set; }
public void Foo()
{
// just start using injected property
BooService...
}
}
Add services scan method in ConfigureServices
using Microsoft.Extensions.DependencyInjection;
...
host.ConfigureServices((services)=>
{
services.AddTransient<IBooService, BooService>();
services.AddTransient<IFooService, FooService>();
// scan method
services.AddPropertyInjectedServices();
});
If you using this extension in asp net and want add property injection support in controllers too, you should add in ConfigureServices this statement:
services.AddControllers().AddControllersAsServices()
having a helper method like this:
public static IHtmlContent Source(this IHtmlHelper html, string s)
{
var path = ServerMapPath() + "Views\\" + s;
I need to get the equivalent of Server.MapPath in asp.net core
You need to inject IHostingEnvironment. Then:
var path = env.ContentRootPath + "Views\\" + s;
in an html helper you can do this:
((IHostingEnvironment)html.ViewContext.HttpContext.RequestServices.GetService(typeof(IHostingEnvironment))).ContentRootPath;
recommended Solution
I recommend Don't use a static class. You can keep something similar to your class, and register it as a singleton.
public class SourceClass
{
private readonly IWebHostEnvironment _webHostEnvironment;
public SourceClass (IWebHostEnvironment webHostEnvironment)
{
_webHostEnvironment= webHostEnvironment;
}
private IHtmlContent Source(this IHtmlHelper html, string s)
{
string contentRootPath = _webHostEnvironment.ContentRootPath;
Path.Combine(contentRootPath,"Views",s)
}
}
Then, in Startup.ConfigureServices:
services.AddSingleton<SourceClass>();
Finally, inject SourceClass where you need it, instead of just statically referencing the class.
Another Solution
(Note :#regnauld provided this solution, but it had some drawbacks, which I thought would be fully answered)
But if you want to use a static method you can do(Note: In .Net Core 3 IHostingEnvironment is absolute and IWebHostEnvironment must be used instead ):
for ServerPath ,use
((IWebHostEnvironment)html.ViewContext.HttpContext.RequestServices.GetService(typeof(IWebHostEnvironment))).ContentRootPath
OR
for WebRootPath (wwwroot) ,use
((IWebHostEnvironment)html.ViewContext.HttpContext.RequestServices.GetService(typeof(IWebHostEnvironment))).WebRootPath
in this answer you can find more details.
I also recommend that you use this code:
var path = Path.Combine(WebRootPath,"Views",s)
instead of
var path = WebRootPath + "Views\\" + s
to run the code on all operating systems.
Background
I have a web api project which uses complex types for GET requests, here is an example of a controller method, and its associated complex type
[RoutePrefix("api")]
public class MyController : ApiController
{
[Route("Something")]
public IHttpActionResult GetSomething([FromUri]RequestObject request)
{
// do something to get "data"
return Ok(data);
}
}
// elsewhere
public class RequestObject
{
[Required]
public string SomeValue{get;set;}
}
This works with a url such as http://domain.com/api/Something?SomeValue=foo.
I would like to use alias' for these parameters, for which I will do some complex stuff (once I have this working) but effectively I have defined an attribute AliasAttribute.
[AttributeUsage(AttributeTargets.Property,AllowMultiple=true)]
public class AliasAttribute : Attribute
{
public string ParameterName { get; private set; }
public AliasAttribute(string parameterName)
{
this.ParameterName = parameterName;
}
}
Which I would like to adorn onto my request model like so:
// elsewhere
public class RequestObject
{
[Required,Alias("val")]
public string SomeValue{get;set;}
}
Allowing my url to shorten to http://domain.com/api/Something?val=foo. This is a contrived and simplified example, but hopefully demonstrates the background.
Problem
ModelBinding in web api has become very complex compared to Mvc model binding. I am getting twisted up between IModelBinder, IValueProvider, HttpParameterBinding et al.
I would like an example of where I should hook in to the model binding to allow me to write the value to my model from the querystring - note that I only use this aliasing behaviour when the route uses the FromUri attribute (see MyController.GetSomething above).
Question title: Support aliased arguments in get requests for web api. I think you are re-inventing a wheel here AliasAttribute , and have not given a really good reason why you don't want to use community ways of doing this.
I have done something similar with Newtonsoft.Json serializer. But if you want something ootb I'd have to google around.
public class RequestObject
{
[Required]
[JsonProperty("vla")]
public string SomeValue{get;set;}
}
Example SO that uses it: .Net NewtonSoft Json Deserialize map to a different property name
Here is a more agnostic way to do it.
[DataContract]
public class RequestObject
{
[DataMember(Name="val", IsRequired=true)]
public string SomeValue{get;set;}
}
The default MVC template uses "#DateTime.Now.Year" to display the copyright year, but I'd much rather use NodaTime everywhere.
I'm currently using Ninject to inject an instance of IClock into Controllers that do time or date specific stuff. Is there a recommended way to access a "global IClock" in MVC similar to the "DateTime.Now"? I suppose I could inject the IClock into every Controller then pass it into every view, but it would be nice to have access to something global sometimes.
I know I could use SystemClock.Instance in the Layout template... it would be much nicer to reference a global, testable IClock instead.
You could use a child action.
Start by writing a controller in which you could use dependency injection as usual and which will contain a child action:
public class CopyrightController: Controller
{
private readonly IClock clock;
public CopyrightController(IClock clock)
{
this.clock = clock;
}
[ChildActionOnly]
public ActionResult Index()
{
// In this example I am directly passing the IClock instance
// to the partial view as model but in a real application
// you might want to use a view model here
return PartialView(this.clock);
}
}
and then you could have a corresponding partial view (~/Views/Copyright/Index.cshtml):
#model IClock
<div>Copyright ...</div>
and finally in your _Layout call this child action:
<footer>
#Html.Action("Copyright", "Index")
</footer>
If you're looking for a testable DateTime.Now, you could use SystemTime.Now(), as detailed in this post by Ayende Rahien. You can create a Func<DateTime> that defaults to DateTime.Now but that you can set to a specific date in your tests. It saves you introducing another dependency. Here's the func:
public static class SystemTime
{
public static Func<DateTime> Now = () => DateTime.Now;
}
Wherever you would use DateTime.Now you should use SystemTime.Now instead. You can set it in your tests like this:
SystemTime.Now = () => new DateTime(2012, 7, 1).Date;
Provided you have adequate test coverage of your usage of dates, you should catch any instances in which you are using DateTime.Now instead of SystemTime.Now
So MVC 4 introduces script and style bundling. Which allows for this:
public static void RegisterBundles(BundleCollection bundles)
{
bundles.Add(new ScriptBundle("~/bundles/mobile").Include(
"~/Scripts/jquery.mobile-*"));
then used in a razor view like this:
#Scripts.Render("~/bundles/mobile")
My question is why do I have to type "~/bundles/mobile"? Is there a way get intellisence to have a strongly typed object to pick up on? Otherwise I have to go look it up to make sure I call it the same thing.
I would like to do something like this: (I know this won't compile this way, it's just an example)
public static void RegisterBundles(BundleCollection bundles)
{
Bundle mobile = new Bundle("mobile");
mobile.AddFile("w/e")
bundles.Add(mobile);
//in page:
#Scripts.Render(BundleConfig.mobile)
or something to that affect.
Edit: the answer so simple. As #Hao Kung points out #Styles.Render simply takes a url string path. I created a class to hold the pathes.
public class bundles
{
#region Javascript
public static string scripts = "~/bundles/scripts";
...
#endregion
#region CSS
public static string css = "~/Content/css";
public static string jqueryUi = "~/Content/themes/base/css";
...
#endregion
}
in any page then you simply do
#Styles.Render(bundles.jqueryUi)
there you have it. A little extra effort on your part, but at least it's strongly typed now.
The Render Scripts/Styles Render helpers are not limited to rendering references to bundles, they resolve any urls, so the only way for the helper to detect that you mean to reference a bundle, is by passing in the virtual path of the bundle.