Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ActionResult extension doesn't work with Page() ActionResult method

I have an extension for ActionResult that adds a toast to TempData when returning a page:

public static IActionResult WithMessage(this ActionResult result, InformMessage msg)
{
    return new InformMessageResult(result, msg);
}

and this is InformMessageResult:

public class InformMessageResult : ActionResult
    {
        public ActionResult InnerResult { get; set; }
        public InformMessage InformMessage { get; set; }

        public InformMessageResult (ActionResult innerResult, InformMessage informMsg)
        {
            InnerResult = innerResult;
            InformMessage = informMsg;
        }

        public override async Task ExecuteResultAsync(ActionContext context)
        {
            ITempDataDictionaryFactory factory = context.HttpContext.RequestServices.GetService(typeof(ITempDataDictionaryFactory)) as ITempDataDictionaryFactory;
            ITempDataDictionary tempData = factory.GetTempData(context.HttpContext);

            tempData.Put("InformMessage", InformMessage);

            await InnerResult.ExecuteResultAsync(context);
        }
    }

This works well with

return RedirectToPage(etc).WithMessage(etc)

and the like, but fails with

return Page().WithMessage(etc)

and the debugger highlights

await InnerResult.ExecuteResultAsync(context);

saying InnerResult not set to an instance of an object.

Is there a way I can make this work with Return Page()?

Edit for additional info:

I tested what was being sent in as the "InnerResult" and it looks like with Return Page(), everything is null (by design, I'd say, as I do nothing to it before that point):

with RedirectToPage():

enter image description here

With Page():

enter image description here

like image 292
linus72982 Avatar asked May 05 '19 05:05

linus72982


2 Answers

This is an older question, but I needed functionality like this myself, and dug deep to find the reason.

As you can see from your debugging, the Page method generates a completely blank PageResult. Being as every property is null, calling ExecuteResultAsync on it fails as it obviously can't do anything with all-null values.

The reason Page() otherwise works the rest of the time is due to behind-the-scenes magic in PageActionInvoker, specifically in its InvokeResultAsync method. It will detect that your ViewData and Page are blank and populate them before it, itself, calls the pageResult.ExecuteResultAsync method.

Hence, you can still get your InformMessageResult to work if you do the same work as PageActionInvoker does. Here's how:

public override async Task ExecuteResultAsync(ActionContext context)
{
    /* temp data work goes here */

    if (InnerResult is PageResult pageResult)
    {
        var pageContext = context as PageContext
            ?? throw new ArgumentException("context must be a PageContext if your InnerResult is a PageResult.", "context");

        var pageFactoryProvider = pageContext.HttpContext.RequestServices.GetRequiredService<IPageFactoryProvider>();
        var pageFactory = pageFactoryProvider.CreatePageFactory(pageContext.ActionDescriptor);

        var viewContext = new ViewContext(
            pageContext,
            NullView.Instance,
            pageContext.ViewData,
            tempData,
            TextWriter.Null,
            new Microsoft.AspNetCore.Mvc.ViewFeatures.HtmlHelperOptions()
        );
        viewContext.ExecutingFilePath = pageContext.ActionDescriptor.RelativePath;

        pageResult.ViewData = viewContext.ViewData;
        pageResult.Page = (PageBase)pageFactory(pageContext, viewContext);
    }

    await InnerResult.ExecuteResultAsync(context);
}

private class NullView : IView
{
    public static readonly NullView Instance = new NullView();

    public string Path => string.Empty;

    public Task RenderAsync(ViewContext context)
    {
        if (context == null) throw new ArgumentNullException("context");
        return Task.CompletedTask;
    }
}
like image 129
Ber'Zophus Avatar answered Oct 10 '22 19:10

Ber'Zophus


The problem I suspect is that Page() and RedirectToPage() inherit from different base classes.

RedirectToPage() as per this documentation. It has the following Inheritance:

Object -> ActionResult -> RedirectToPageResult

This is exposed by some inheritance of the controller. So you're extension of ActionResult is available to be used.

However the Page() method is part of a RazorPages class as per this documentation. So it's inheritance is as follows:1

Object -> RazorPageBase -> PageBase -> Page

Now the Page() method of that class does return a PageResult which looks to inherit from ActionResult as defined here.

So with that in mind I'd suggest casting it to the base ActionResult first, and then using your extension method. Something like this perhaps:

var baseClass = (Page() as ActionResult);
return baseClass.WithMessage(etc);

1 You can see the base type in the second image the OP supplied.

like image 34
JoeTomks Avatar answered Oct 10 '22 19:10

JoeTomks