I understand that in .Net Core 3.1 that the html.action was removed in favor of ViewComponents. Unfortunately the code I have does not lend itself for a ViewComponent since it is a custom PeoplePicker control that will have user interaction. Keep in mind that this PeoplePicker control works correctly in .Net 4.7.2. I've looked on line and found methods on how to re-implement the html.action functionality. The problem I'm having is that when the code hits the await invoker.InvokeAsync(); line in the code the ActionContext that was set gets overwritten by subsequent calls to the underlying model's get/set properties. I'll walk through the code and what is happening. Here is the line that calls the PeoplePicker:
@Html.Action("PeoplePicker", "PeoplePicker", new EDAD.Models.PeoplePickerViewModel { PickerId = 20, UserProfile = Model.CurrentUser })
The next step that happens is the HTMLHelperViewExtensions that I implemented to allow the html.Action are called:
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.IO;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Mvc.Rendering
{
public static class HtmlHelperViewExtensions
{
public static IHtmlContent Action(this IHtmlHelper helper, string action, object parameters = null)
{
var controller = (string)helper.ViewContext.RouteData.Values["controller"];
return Action(helper, action, controller, parameters);
}
public static IHtmlContent Action(this IHtmlHelper helper, string action, string controller, object parameters = null)
{
var area = (string)helper.ViewContext.RouteData.Values["area"];
return Action(helper, action, controller, area, parameters);
}
public static IHtmlContent Action(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");
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 serviceProvider = helper.ViewContext.HttpContext.RequestServices;
var actionContextAccessor = helper.ViewContext.HttpContext.RequestServices.GetRequiredService<IActionContextAccessor>();
var httpContextAccessor = helper.ViewContext.HttpContext.RequestServices.GetRequiredService<IHttpContextAccessor>();
var actionSelector = serviceProvider.GetRequiredService<IActionSelector>();
// creating new action invocation context
var routeData = new RouteData();
foreach (var router in helper.ViewContext.RouteData.Routers)
{
routeData.PushState(router, null, null);
}
routeData.PushState(null, new RouteValueDictionary(new { controller = controller, action = action, area = area }), null);
routeData.PushState(null, new RouteValueDictionary(parameters ?? new { }), null);
//get the actiondescriptor
RouteContext routeContext = new RouteContext(helper.ViewContext.HttpContext) { RouteData = routeData };
var candidates = actionSelector.SelectCandidates(routeContext);
var actionDescriptor = actionSelector.SelectBestCandidate(routeContext, candidates);
var originalActionContext = actionContextAccessor.ActionContext;
var originalhttpContext = httpContextAccessor.HttpContext;
try
{
var newHttpContext = serviceProvider.GetRequiredService<IHttpContextFactory>().Create(helper.ViewContext.HttpContext.Features);
if (newHttpContext.Items.ContainsKey(typeof(IUrlHelper)))
{
newHttpContext.Items.Remove(typeof(IUrlHelper));
}
newHttpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(newHttpContext, routeData, actionDescriptor);
actionContextAccessor.ActionContext = actionContext;
var invoker = serviceProvider.GetRequiredService<IActionInvokerFactory>().CreateInvoker(actionContext);
await invoker.InvokeAsync();
newHttpContext.Response.Body.Position = 0;
using (var reader = new StreamReader(newHttpContext.Response.Body))
{
return new HtmlString(reader.ReadToEnd());
}
}
catch (Exception ex)
{
return new HtmlString(ex.Message);
}
finally
{
actionContextAccessor.ActionContext = originalActionContext;
httpContextAccessor.HttpContext = originalhttpContext;
if (helper.ViewContext.HttpContext.Items.ContainsKey(typeof(IUrlHelper)))
{
helper.ViewContext.HttpContext.Items.Remove(typeof(IUrlHelper));
}
}
}
}
}
Everything is working at this point. The code gets to the following line where it then calls the People Picker model
routeData.PushState(null, new RouteValueDictionary(parameters ?? new { }), null);
This goes to the model and correctly gets the 2 variables with the data that was passed in:
public class PeoplePickerViewModel
{
public int? PickerId { get; set; }
public UserModel UserProfile { get; set; }
}
The code continues through the HTMLHelper code. On the line just prior to await invoker.InvokeAsync() I can view the data in both of the 2 variables (PickerID and UserProfile). This is where the problem happens. When it hits the await invoker.InvokeAsync() it goes BACK to the model and gets the UserProfile (which is now NULL), gets the PickerID which has retained the value, then gets the UserProfile AGAIN for a third time (it is still null). It then passes the info to the PeoplePicker controller where the "model" variable is used to set the PeoplePicker. Since the UserProfile was set to null by the second/third calls the model.UserProfile is set to a new UserModel() instead of using the one that was started with.
public PartialViewResult PeoplePicker(PeoplePickerViewModel model)
{
model.UserProfile = model.UserProfile ?? new UserModel();
model.PickerId = model.PickerId ?? 0;
return PartialView(model);
}
Let me add that the PeoplePicker works in all other aspects of its functionality. It's just not working when a userprofile is passed in at the start.
So here are my questions:
- Why is it calling the model more than once?
- Is there a way I can troubleshoot this other than what I've done to this point?
- Is there a better way to do this in Core 3.1?