Customize Razor Pages Handlers

Customize Razor Pages Handlers

Jul 08, 2017     Viewed 5474 times 0 Comments
Posted in #Razor Pages 

I blogged many times about the Razor Pages and some of its features that will be shipped in ASP.NET Core 2.0.

One of the new concepts that Razor Pages come up with is what it called Page Handlers or Handler Methods, which one of the nice features in the Razor Pages which is similar to WebForms events but not exactly. For more information about the page handlers you can have a look to the Mike Brind blog post Razor Pages - Understanding Handler Methods.

If your already work with the Razor Page you will notice that the handler methods follow a specific naming conventions:

  • OnGet()
  • OnGetAsync()
  • OnPost()
  • OnPostAsync()
  • OnPost{Handler Name}()
  • OnPost{Handler Name}Async()

where {Handler Name} something like DeleteCustomer, CreateProduct .. etc, where the handler name come from the querystring - which the thing I personally dislike - that's generated by asp-page-handler.

So names such as OnPostDeleteCustomer(), OnPostCreateProduct() is not good, because we already know from the method names that they will be executed on the HTTP POST, it would be nice if we can customize those names to something like: DeleteCustomerAsync(), OnCreatePost() or any name you would like instead of forcing yourself to add the OnPost at the beginning of the method name.

We can achieve this by introducing a new convention-based names, instead I come up with little bit crazy idea :) that let you specify the handler method directly into the markup, and when the page is posted it will automatically figure out which methods should execute based on the specified handler.

First step I need to create a CommandTagHelper that allows you to specify your command aka handler method.

[HtmlTargetElement("button", Attributes = CommandNameAttributeName)]
[HtmlTargetElement("button", Attributes = CommandArgumentsAttributeName + "*")]
public class CommandTagHelper : TagHelper
{
    private const string CommandNameAttributeName = "asp-command";
    private const string CommandArgumentsAttributeName = "asp-command-";
    public const string CommandNameParam = "__command";

    [HtmlAttributeName(CommandNameAttributeName)]
    public string Name { get; set; }

    [HtmlAttributeName(DictionaryAttributePrefix = CommandArgumentsAttributeName)]
    public IDictionary<string, string> Arguments { get; set; } =
        new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

    [HtmlAttributeNotBound]
    [ViewContext]
    public ViewContext ViewContext { get; set; }

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        var pagePath = ViewContext.HttpContext.Request.Path.ToString();
        output.Attributes.Add("name", CommandNameParam);
        output.Attributes.Add("value", Name);

        if (Arguments != null && Arguments.Count != 0)
        {
            var queryString = string.Join("&", Arguments.Select(r => $"{r.Key}={r.Value}"));
            var formActionAttribute = output.Attributes["formaction"];

            if (formActionAttribute != null)
            {
                output.Attributes.Remove(formActionAttribute);
            }

            output.Attributes.Add("formaction", $"{pagePath}?{queryString}");
        }
    }
}

In the code snippet above I created a simple tag helper which something similar to asp-page-handler and asp-route-, instead I used asp-command and asp-command- the main difference here I personally don't like to pass the handler name in the querystring, because it's visible to the user, instead I post its value in form posted values, also the user able to add some command arguments to be passed in the executed command method using asp-command-{Command Argument}.

Then we need some guy to look into the posted values to specify which command name is specified to look after that for the suitable command method to invoke. For that I created a CommandPageModel class which responsible for this as the following:

public abstract class CommandPageModel : PageModel
{
    public Task<IActionResult> OnPostAsync()
    {
        var commandName = HttpContext.Request.Form[CommandTagHelper.CommandNameParam];
        var commandArgs = Request.Query.ToDictionary(x => x.Key, x => x.Value.ToString(),
            StringComparer.OrdinalIgnoreCase);

        return InvokeCommandMethod(this, commandName, commandArgs) as Task<IActionResult>;
    }

    private static object InvokeCommandMethod(PageModel model, string name, IDictionary<string, string> args)
    {
        var commandMethod = model.GetType().GetMethod(name);
        var commandMethodParams = commandMethod.GetParameters();
        var @params = new List<object>();

        if (commandMethodParams.Any())
        {
            for (int i = 0; i < commandMethodParams.Count(); i++)
            {
                var param = commandMethodParams[i];

                if (args.ContainsKey(param.Name))
                {
                    var formValue = args[param.Name].ToString();
                    var value = Convert.ChangeType(formValue, param.ParameterType);
                    @params.Add(value);
                }
            }
        }

        return commandMethod.Invoke(model, @params.ToArray());       
    }
}

The above code in not complicated at all, when the page is posted we figure out the command name using the CommandTagHelper after that using reflection we look for the method with the same name and pass their parameters if there is, and invoke it to execute the business logic that written in the page model.

Finally our page model need to inherit from CommandPageModel instead of PageModel, after that you can using the CommandTagHelper in the Razor as the following:

<button type="submit" asp-command="OnDeletePerson" asp-command-id="@person.Id" class="btn btn-xs btn-danger">delete</button>

You can download the source code for this post from my RazorPagesCommands repository on GitHub.


Leave a Comment