Search code examples
c#asp.net-corerazor-pagesasp.net-core-identity

Why don't values persist on Postback?


My home page has a list of product options each giving the user a different account level on sign up. Each option presents a "Sign Up" button which takes the user to the /Identity/Account/Register page to sign up.

I need to communicate to the Register page which option the user selected.

  1. I can't use Sessions because that's apparently been taken away
  2. I can't use Cookies because that's apparently been taken away
  3. ViewData values don't persist when I submit the form
  4. Querystring values don't persist when I submit the form
  5. Global variable values don't persist when I submit the form
  6. I can't set properties of the viewmodel when the page is loaded initially (NullReferenceException)

When the Register page loads, the value is there, but when I submit the form, it disappears.

I'm at a loss. By what mechanism am I meant to get this required information across?

For the most part my code is pretty much just standard boilerplate stuff:

[AllowAnonymous]
public class RegisterModel : PageModel
{
    private readonly SignInManager<ApplicationUser> _signInManager;
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly ILogger<RegisterModel> _logger;
    private readonly IEmailSender _emailSender;

    public RegisterModel(
        UserManager<ApplicationUser> userManager,
        SignInManager<ApplicationUser> signInManager,
        ILogger<RegisterModel> logger,
        IEmailSender emailSender)
    {
        _userManager = userManager;
        _signInManager = signInManager ?? throw new ArgumentNullException(nameof(signInManager));
        _logger = logger;
        _emailSender = emailSender;
    }

    [BindProperty]
    public InputModel Input { get; set; }
    [BindProperty]
    public int AccountLevel { get; set; }
    public string ReturnUrl { get; set; }

    public void OnGet(string returnUrl = null, int acclevel = 1)
    {
        AccountLevel = acclevel;
        ReturnUrl = returnUrl;
    }

    public async Task<IActionResult> OnPostAsync(string returnUrl = null, int acclevel = 0)
    {
        returnUrl = returnUrl ?? Url.Content("~/");

        if (ModelState.IsValid)
        {
            if (acclevel == 0) throw new ArgumentException(nameof(acclevel));

            Input.LicenseCount = acclevel * 10;

            var user = new ApplicationUser { UserName = Input.Email, Email = Input.Email, Name = Input.FirstName, Surname = Input.Surname, PhoneNumber = Input.PhoneNumber, SaIdNumber = Input.IdNumber, LicensesCount = Input.LicenseCount };

            var result = await _userManager.CreateAsync(user, Input.Password);
            await _userManager.AddToRoleAsync(user, nameof(SystemRoles.AppUser));
            if (result.Succeeded)
            {
                _logger.LogInformation("User created a new account with password.");

                var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
                var callbackUrl = Url.Page(
                    "/Account/ConfirmEmail",
                    pageHandler: null,
                    values: new { userId = user.Id, code = code },
                    protocol: Request.Scheme);

                await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
                    $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");

                //await _signInManager.SignInAsync(user, isPersistent: false);
                return LocalRedirect(returnUrl);
            }
            foreach (var error in result.Errors)
            {
                ModelState.AddModelError(string.Empty, error.Description);
            }
        }

        // If we got this far, something failed, redisplay form
        return Page();
    }
}

Here's the Page (this is using Pages with the PageModel for some reason, rather than Views and Controllers - it scaffolded this way when I added Identity).

@page
@model RegisterModel
@{
    ViewData["Title"] = "Register";
}

<h1>@ViewData["Title"]</h1>

<div class="row">
    <div class="col-md-4">
        <form asp-route-returnUrl="@Model.ReturnUrl" method="post">
            <h4>Create a new account.</h4>
            <hr />
            <div asp-validation-summary="All" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Input.FirstName"></label>
                <input asp-for="Input.FirstName" class="form-control" />
                <span asp-validation-for="Input.FirstName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Input.Surname"></label>
                <input asp-for="Input.Surname" class="form-control" />
                <span asp-validation-for="Input.Surname" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Input.PhoneNumber"></label>
                <input asp-for="Input.PhoneNumber" class="form-control" />
                <span asp-validation-for="Input.PhoneNumber" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Input.IdNumber"></label>
                <input asp-for="Input.IdNumber" class="form-control" />
                <span asp-validation-for="Input.IdNumber" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Input.Email"></label>
                <input asp-for="Input.Email" class="form-control" />
                <span asp-validation-for="Input.Email" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Input.Password"></label>
                <input asp-for="Input.Password" class="form-control" />
                <span asp-validation-for="Input.Password" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Input.ConfirmPassword"></label>
                <input asp-for="Input.ConfirmPassword" class="form-control" />
                <span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
            </div>
            <button type="submit" class="btn btn-primary">Register</button>
        </form>
    </div>
</div>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}

Solution

  • Note: I don't work with Razor Pages, so someone with knowledge in that technology might have a better answer.

    You can use a hidden field in the form to keep the value on POST-back:

    <form asp-route-returnUrl="@Model.ReturnUrl" method="post">
        @Html.HiddenFor(model => model.AccountLevel)
        @*OR, TagHelper way*@
        <input asp-for="AccountLevel" type="hidden"/>
        <h4>Create a new account.</h4>
        ....
    </form>
    

    This is also fairly easy to do with sessions after you enable them:

    public void OnGet(string returnUrl = null, int acclevel = 1)
    {
        HttpContext.Session.SetInt32("AccountLevel", acclevel);
        ReturnUrl = returnUrl;
    }
    
    public async Task<IActionResult> OnPostAsync(string returnUrl = null)
    {
        int accountLevel = HttpContext.Session.GetInt32("AccountLevel");
        ...
    }