Impossible de conserver ActionContext lors de l'exécution de HTML.Action dans .Net Core 3.1

Sep 17 2020

Je comprends que dans .Net Core 3.1 que le html.action a été supprimé en faveur de ViewComponents. Malheureusement, le code que j'ai ne se prête pas à un ViewComponent car il s'agit d'un contrôle PeoplePicker personnalisé qui aura une interaction avec l'utilisateur. Gardez à l'esprit que ce contrôle PeoplePicker fonctionne correctement dans .Net 4.7.2. J'ai cherché en ligne et trouvé des méthodes pour ré-implémenter la fonctionnalité html.action. Le problème que je rencontre est que lorsque le code atteint l'invocateur d'attente, invokeAsync (); dans le code, l'ActionContext qui a été défini est écrasé par les appels suivants aux propriétés get / set du modèle sous-jacent. Je vais parcourir le code et ce qui se passe. Voici la ligne qui appelle le PeoplePicker:

 @Html.Action("PeoplePicker", "PeoplePicker", new EDAD.Models.PeoplePickerViewModel { PickerId = 20, UserProfile = Model.CurrentUser })

La prochaine étape qui se produit est les HTMLHelperViewExtensions que j'ai implémentées pour autoriser le html.Action sont appelés:

        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));
                        }
                    }
                }
            }
        }

Tout fonctionne à ce stade. Le code arrive à la ligne suivante où il appelle ensuite le modèle de sélecteur de personnes

  routeData.PushState(null, new RouteValueDictionary(parameters ?? new { }), null);

Cela va au modèle et obtient correctement les 2 variables avec les données qui ont été transmises:

public class PeoplePickerViewModel 
{
    public int? PickerId { get; set; }
    public  UserModel UserProfile { get; set; }
}

Le code continue à travers le code HTMLHelper. Sur la ligne juste avant d'attendre invoker.InvokeAsync (), je peux afficher les données dans les deux variables (PickerID et UserProfile). C'est là que le problème se produit. Lorsqu'il atteint l'invocateur wait.InvokeAsync (), il retourne au modèle et obtient le UserProfile (qui est maintenant NULL), obtient le PickerID qui a conservé la valeur, puis obtient le UserProfile AGAIN une troisième fois (il est toujours nul ). Il transmet ensuite les informations au contrôleur PeoplePicker où la variable «modèle» est utilisée pour définir le PeoplePicker. Étant donné que le UserProfile a été défini sur null par les deuxième / troisième appels, le model.UserProfile est défini sur un nouveau UserModel () au lieu d'utiliser celui qui a été démarré avec.

    public PartialViewResult PeoplePicker(PeoplePickerViewModel model)
    {
        model.UserProfile = model.UserProfile ?? new UserModel();
        model.PickerId = model.PickerId ?? 0;
        return PartialView(model);
    }

Permettez-moi d'ajouter que le PeoplePicker fonctionne dans tous les autres aspects de sa fonctionnalité. Cela ne fonctionne tout simplement pas lorsqu'un profil utilisateur est transmis au début.

Donc, voici mes questions:

  1. Pourquoi appelle-t-il le modèle plus d'une fois?
  2. Existe-t-il un moyen de résoudre ce problème autre que ce que j'ai fait jusqu'à présent?
  3. Existe-t-il une meilleure façon de faire cela dans Core 3.1?

Réponses

1 FeiHan Sep 17 2020 at 11:28

le UserProfile a été défini sur null

J'ai fait un test et je peux reproduire le même problème. Dans votre code, nous pouvons constater que la UserProfilepropriété de votre PeoplePickerViewModelclasse est un type complexe, ce qui semble causer ce problème.

Pour résoudre ce problème, vous pouvez essayer la solution de contournement suivante.

routeData.PushState(null, new RouteValueDictionary(new { controller = controller, action = action, area = area }), null);


if (parameters == null)
{
    routeData.PushState(null, new RouteValueDictionary(new { }), null);
}
else
{
    var type = parameters.GetType();

    if (parameters.GetType() == typeof(PeoplePickerViewModel))
    {
        //dynamically generate and populate values based on your model class

        var mdata = parameters as PeoplePickerViewModel;

        var routeValDict = new RouteValueDictionary();
        routeValDict.Add("PickerId", mdata.PickerId);
        routeValDict.Add("UserProfile.Id", mdata.UserProfile.Id);
        routeValDict.Add("UserProfile.Name", mdata.UserProfile.Name);

        routeData.PushState(null, routeValDict, null);
    }
    else
    {
        routeData.PushState(null, new RouteValueDictionary(parameters), null);
    }
}

Test du code de la UserModelclasse

public class UserModel
{
    public int Id { get; set; }
    public string Name { get; set; }
}