Where is @Html.Action
in Asp.net Core?
I can see @Html.ActionLink
but not a direct call to an Action as before.
Was it replaced by ViewComp
ViewComponents are great, but not so great for Ajax.
If you really miss the @Html.RenderAction method, then here's a quick implementation I threw together for AspNetCore.
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Routing;
namespace Microsoft.AspNetCore.Mvc.Rendering {
public static class HtmlHelperViewExtensions
{
public static IHtmlContent RenderAction(this IHtmlHelper helper, string action, object parameters = null)
{
var controller = (string)helper.ViewContext.RouteData.Values["controller"];
return RenderAction(helper, action, controller, parameters);
}
public static IHtmlContent RenderAction(this IHtmlHelper helper, string action, string controller, object parameters = null)
{
var area = (string)helper.ViewContext.RouteData.Values["area"];
return RenderAction(helper, action, controller, area, parameters);
}
public static IHtmlContent RenderAction(this IHtmlHelper helper, string action, string controller, string area, object parameters = null)
{
if (action == null)
throw new ArgumentNullException("action");
if (controller == null)
throw new ArgumentNullException("controller");
if (area == null)
throw new ArgumentNullException("area");
var task = RenderActionAsync(helper, action, controller, area, parameters);
return task.Result;
}
private static async Task<IHtmlContent> RenderActionAsync(this IHtmlHelper helper, string action, string controller, string area, object parameters = null)
{
// fetching required services for invocation
var currentHttpContext = helper.ViewContext?.HttpContext;
var httpContextFactory = GetServiceOrFail<IHttpContextFactory>(currentHttpContext);
var actionInvokerFactory = GetServiceOrFail<IActionInvokerFactory>(currentHttpContext);
var actionSelector = GetServiceOrFail<IActionSelectorDecisionTreeProvider>(currentHttpContext);
// creating new action invocation context
var routeData = new RouteData();
var routeParams = new RouteValueDictionary(parameters ?? new { });
var routeValues = new RouteValueDictionary(new { area = area, controller = controller, action = action });
var newHttpContext = httpContextFactory.Create(currentHttpContext.Features);
newHttpContext.Response.Body = new MemoryStream();
foreach (var router in helper.ViewContext.RouteData.Routers)
routeData.PushState(router, null, null);
routeData.PushState(null, routeValues, null);
routeData.PushState(null, routeParams, null);
var actionDescriptor = actionSelector.DecisionTree.Select(routeValues).First();
var actionContext = new ActionContext(newHttpContext, routeData, actionDescriptor);
// invoke action and retreive the response body
var invoker = actionInvokerFactory.CreateInvoker(actionContext);
string content = null;
await invoker.InvokeAsync().ContinueWith(task => {
if (task.IsFaulted)
{
content = task.Exception.Message;
}
else if (task.IsCompleted)
{
newHttpContext.Response.Body.Position = 0;
using (var reader = new StreamReader(newHttpContext.Response.Body))
content = reader.ReadToEnd();
}
});
return new HtmlString(content);
}
private static TService GetServiceOrFail<TService>(HttpContext httpContext)
{
if (httpContext == null)
throw new ArgumentNullException(nameof(httpContext));
var service = httpContext.RequestServices.GetService(typeof(TService));
if (service == null)
throw new InvalidOperationException($"Could not locate service: {nameof(TService)}");
return (TService)service;
}
}
}
You can invoke from your view using one of these methods:
@Html.RenderAction("action", "controller", "area", new { id = 1})
@Html.RenderAction("action", "controller", new { id = 1})
@Html.RenderAction("action", new { id = 1})
Note:
Controller name, and optionally area name, will default to the corresponding values from the ActionContext if not provided.
Yes, ViewComponents would be the new way of doing this, but they are not exactly the same as what @Html.Action
was doing before though...for example, in MVC5 and prior, invoking 'child actions' would also execute any filters (for example, if the controller had filters decorated on them) giving them the appearance as regular actions...but this is not true with ViewComponents and they are executed in the context of actual request...
More info on view components: https://docs.microsoft.com/en-us/aspnet/core/mvc/views/view-components
M.R.T2017 said : ... The following examples were successfully tested: ...
First thanks for your sharing.
But this overload method may be cause "HTTP ERROR 500" :
@Html.RenderAction("About")
Because 'controller' maybe lower case controller name, ex. "home", "grid", etc. :
helper.ViewContext.RouteData.Values["controller"]
You need capitalize the controller name, ex. "grid" -> "Grid", because the controller class name is case-sensitive in Assembly, action name is the same.
*Visual Studio 2019/NET Core 2.2.
Workaround by Aries for the Helper Extension is no more workable for Net Core 2.0 as IActionSelectorDecisionTreeProvider has been removed from the newer version. See the link below.
https://github.com/Microsoft/aspnet-api-versioning/issues/154
@Html.Action was replaced by ViewComponents. I dislike ViewComponents for multiple reasons.
However I am using alternative pattern to @Html.Action
First I create Action on controller that is returning partial view with a content that I want to display in page i.e.
[HttpGet]
public async Task<IActionResult> GetFoo()
{
return PartialView("_Foo", new Foo());
}
Then I place div on page where the foo view should be loaded and include IIFE at the bottom of that page. I.e. code bellow will load GetFoo view and then insert that html to div with id foo-view.
<div class="page">
<div id="foo-view" data-url="@Url.Action(action: "GetFoo", controller: "Home")"></div>
</div>
<script>
$(document).ready(function () {
(function () {
var url = $("#foo-view").data("url");
$("#foo-view").load(url);
})();
});
</script>
You may also want to display spinner while view is beign fetched from server.
For Net Core 2.0
using Microsoft.AspNetCore.Mvc.Infrastructure;
replace
// var actionSelector = GetServiceOrFail<IActionSelectorDecisionTreeProvider>(currentHttpContext);
var actionSelector = GetServiceOrFail<IActionDescriptorCollectionProvider>(currentHttpContext);
and
// var actionDescriptor = actionSelector.DecisionTree.Select(routeValues).First();
var actionDescriptor = actionSelector.ActionDescriptors.Items.Where(i => i.RouteValues["Controller"] == controller && i.RouteValues["Action"] == action).First();