Razor Pages keep data between class methods - c#

The problem that I have faced lately when trying to write a site on razor pages - when I use variables in a class method, it doesn't keep that data inside. In example: I have a method which creates data when the page is created. And when I press submit button: The data isn't remembered inside the class and thus it returns null.
I've tried to work with Data binding, Temp data, Private classes. Neither of them kept data for a future use inside one class. The current code is:
`
namespace TestSite.Pages.Shared
{
public class Registration_2Model : PageModel
{
private readonly TestSite.Data.ApplicationDbContext _context;
public UserManager<IdentityUser> _user;
public string _ID { get; set; }
public string _Code { get; set; }
public bool _Validated;
public Registration_2Model(UserManager<IdentityUser> UserManager, ApplicationDbContext context)
{
_context = context;
_user = UserManager;
}
public IActionResult OnGet()
{
var CurrentUser = _context.Simple_User.FirstOrDefault(m => m.ID == Guid.Parse(_user.GetUserId(User)));
if (CurrentUser == null)
{
_ID = _user.GetUserId(User);
_Code = GenerateCode();
_Validated = false;
TempData["ID"] = _ID;
TempData["Code"] = _Code;
return Page();
} else { return Redirect("/Index"); }
}
[BindProperty]
public Simple_User Simple_User { get; set; }
public async Task<IActionResult> OnPostAsync()
{
Simple_User.ID = Guid.Parse((string)TempData["ID"]);
Simple_User.Code = (string)TempData["Code"];
Simple_User.Validated = false;
if (!ModelState.IsValid)
{
return Page();
}
_context.Simple_User.Add(Simple_User);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
private string GenerateCode()
{
Random _random = new Random();
return $"{_random.Next(1000, 9999).ToString()}-{DateTime.Now.Year}";
}
}
}
`
and HTML:
`
#{
ViewData["Title"] = "Second registration";
}
<h2>Second registration</h2>
<h4>One step left. After your initial registration, you must fill in some blanks, after which, our moderator will check and add you to our list.</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label class="control-label">ID</label>
<input class="form-control" type="text" placeholder="#Model._ID" readonly />
</div>
<div class="form-group">
<label asp-for="Simple_User.Name" class="control-label"></label>
<input asp-for="Simple_User.Name" class="form-control" />
<span asp-validation-for="Simple_User.Name" class="text-danger"></span>
<span asp-validation-for="Simple_User.Code" class="text-danger"></span>
</div>
<div class="form-group">
<label class="control-label">Code</label>
<input class="form-control" type="text" placeholder="#Model._Code" readonly />
</div>
<div class="form-group">
<label asp-for="Simple_User.Address" class="control-label"></label>
<input asp-for="Simple_User.Address" class="form-control" />
<span asp-validation-for="Simple_User.Address" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="Index">Back to List</a>
</div>
#section Scripts {
#{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}`
Basically, site displays some value in an inaccessible field and should use it when it creates a Simple_User in database. But so far I got only nulls

HTTP is stateless™. If you want to set properties on objects during the execution of one request, and have those values available for a subsequent request, you need to implement your own state management strategy.
There are a lot of options available to you, depending on what you wan to do. If security is important, you should look at session variables. In the meantime, here are all of the options available: https://www.learnrazorpages.com/razor-pages/state-management

Try scaffolding the pages. You can throw out the Create/Details/Delete pages and keep only the Edit page if that is what your looking for. You can do this from the command line or in Visual Studio when creating a new page. Something like the following from the CLI (see here for more details)
dotnet aspnet-codegenerator razorpage -m SimpleUser -dc ApplicationDbContext
Also if you want your _ID or _Code properties to be sent in the post add the [BindProperty] attribute to them. See here for more info about Model Binding.

Related

Values from model not being passed back to view - .NET

I have the following form in a .NET Core application, consisting of a text input field and a "Submit" button.
I'd like the text from the text input field to re-appear in the form after submission, being passed to the controller action from the form and then being passed back to the view.
However, when I test the application, although the inputted values from the view appear when they are bound to the model in the controller, when they are passed back to the view they are wiped and I receive an "Object reference set to null" exception error.
I wondered if there's something missing from my code or what the potential cause of this may be?
Any advice would be great here,
Thanks,
Robert
// This is my view, featuring a simple form
// Values from the view are successfully being passed into the Controller
// This is the exception I receive when the values are passed back to the view:
My code:
#page
#model Models.StringModel
<div class="text-left">
<form asp-controller="Home" asp-action="Alter">
<span class="form-control">
<label asp-for="Name">Alter string:</label>
#if (#Model != null)
{
<input type="text" asp-for="Name" class="changeString" value="#Model.Name"/>
} else
{
<input type="text" asp-for="Name" class="changeString"/>
}
<input class="btn btn-primary" type="submit" value="Update" action="Update" />
</span>
</form>
</div>
StringModel.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace SplitString.Models
{
public class StringModel
{
public int ID { get; set; }
public string Name { get; set; }
}
}
HomeController.cs
using Microsoft.AspNetCore.Mvc;
using SplitString.Models;
using System.Threading.Tasks;
namespace SplitString.Controllers
{
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Alter([Bind("Id, Name")] StringModel stringModel)
{
stringModel.ID = 1;
return View("~/Pages/Index.cshtml", stringModel);
}
}
}
Thanks,
Robert
It looks like you are using a razor page project,so that you don't need to use mvc in it.You only need to use page handler:
Index.cshtml:
#page
#model IndexModel
<div class="text-left">
<form method="post">
<span class="form-control">
<label asp-for="stringModel.Name">Alter string:</label>
#if (#Model != null)
{
<input type="text" asp-for="stringModel.Name" class="changeString" value="#Model.stringModel.Name"/>
} else
{
<input type="text" asp-for="stringModel.Name" class="changeString"/>
}
<input class="btn btn-primary" type="submit" value="Update" action="Update" />
</span>
</form>
</div>
Index.cshtml.cs:
public class IndexModel : PageModel
{
private readonly ILogger<IndexModel> _logger;
[BindProperty]
public StringModel stringModel { get; set; } = new StringModel();
public IndexModel(ILogger<IndexModel> logger)
{
_logger = logger;
}
public void OnGet()
{
}
public void OnPost()
{
stringModel.ID = 1;
//you can do something here
}
}
result:
By looking at this part of your code:
#if (#Model != null)
{
<input type="text" asp-for="Name" class="changeString" value="#Model.Name"/>
}
I saw that you are trying to access the Name property without checking if it has any value.
If you change your code to this it shouldn't throw an exception anymore.
#if (#Model != null && !string.IsNullOrEmpty(Model.Name))
{
<input type="text" asp-for="Name" class="changeString" value="#Model.Name"/>
}
please remove #page from your .cshtml file.
If you use View Engine to render cshtml, please don't use #page.
If you want to use #page, please use razor pages.
// #page
#model Models.StringModel
<div class="text-left">
<form asp-controller="Home" asp-action="Alter">
<span class="form-control">
<label asp-for="Name">Alter string:</label>
#if (#Model != null)
{
<input type="text" asp-for="Name" class="changeString" value="#Model.Name"/>
} else
{
<input type="text" asp-for="Name" class="changeString"/>
}
<input class="btn btn-primary" type="submit" value="Update" action="Update" />
</span>
</form>
</div>

ReturnUrl is always null on POST using ASP.NET Core 6 MVC

I'm not able to get ReturnUrl to work on HttpPost using ASP.NET Core 6 MVC.
When adding a breakpoint to the POST method, returnurl is always null. But with .NET 5, it works with the same code setup except that with .NET 6, I need to make the returnurl parameter nullable so that I won't get an error "returnurl field is required".
This is the code I'm using - any help would be much appreciated.
Thanks.
Model:
namespace IdentityManagerDotNet6.Models
{
public class LoginViewModel
{
[Required]
[EmailAddress]
public string Email { get; set; } = string.Empty;
[Required]
[DataType(DataType.Password)]
public string Password { get; set; } = string.Empty;
[Display(Name = "Remember me?")]
public bool RememberMe { get; set; }
}
}
Controller
[HttpGet]
public IActionResult Login(string? returnurl)
{
ViewData["ReturnUrl"] = returnurl;
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginViewModel loginViewModel, string? returnurl)
{
ViewData["ReturnUrl"] = returnurl;
returnurl = returnurl ?? Url.Content("~/");
if (ModelState.IsValid)
{
var result = await _signInManager.PasswordSignInAsync(loginViewModel.Email, loginViewModel.Password, loginViewModel.RememberMe, lockoutOnFailure: true);
if (result.Succeeded)
{
return LocalRedirect(returnurl);
}
if (result.IsLockedOut)
{
return View("Lockout");
}
else
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return View(loginViewModel);
}
}
return View(loginViewModel);
}
View:
#model LoginViewModel
<h1 class="text-info">Log in</h1>
<div class="row">
<div class="col-md-8">
<form asp-controller="Account" asp-action="Login" asp-route-returnurl="#ViewData["ReturnUrl"]" method="post" role="form">
<h4>Use a local account to log in</h4>
<hr />
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Email" class="col-md-2"></label>
<div class="col-md-10">
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
</div>
<div class="form-group mt-3">
<label asp-for="Password" class="col-md-2"></label>
<div class="col-md-10">
<input asp-for="Password" class="form-control" />
<span asp-validation-for="Password" class="text-danger"></span>
</div>
</div>
<div class="form-check mt-3">
<input class="form-check-input" asp-for="RememberMe" type="checkbox" value="" id="flexCheckChecked">
<label class="form-check-label" asp-for="RememberMe" for="flexCheckChecked">
Remember me?
</label>
</div>
<div class="form-group">
<div class=" col-1 my-3">
<button type="submit" asp-controller="Account" asp-action="Login" class="btn btn-success form-control">Login</button>
</div>
</div>
<p>
<a asp-action="Register">Register as a new user?</a>
</p>
<p>
<a asp-action="ForgotPassword">Forgot your passord?</a>
</p>
</form>
</div>
</div>
"I'm not able to get ReturnUrl to work on HttpPost using ASP.NET Core 6 MVC.":
I have checked your code between the line. It doesn't has anything
wrong with [FromQuery] So you don't need to do anything on [FromQuery] as other answer I've seen, may be deleted now.
Issue Replication:
I have reproduced your issue successfully as you can see below:
What Causing the Issue:
If you investigate your code again you would noticed that you are
using asp-controller="Login" asp-action="Login" twice on your
Login.cshtml at the begining of the form and at the point of submit button this causing the data loss while you are submitting the form.
At the starting on form:
<form asp-controller="Login" asp-action="Login" asp-route-returnurl="#ViewData["ReturnUrl"]" method="post" role="form">
At your button submit::
<button type="submit" asp-controller="Login" asp-action="Login" class="btn btn-success form-control">Login</button>
Solution:
The easiest solution is just modify your submit button code like below which will resolve your issue:
<div class="form-group">
<div class=" col-1 my-3">
<button type="submit" class="btn btn-success form-control">Login</button>
</div>
</div>
Output:
Hope it will resolve your returnurl null issue completely.
Controller
[HttpGet]
public IActionResult Login(string? returnurl) <-- you don't need it
{
ViewData["ReturnUrl"] = returnurl; <-- you don't need it
return View();
}
View
<form method="post" role="form"> <-- you can do that and the returnUrl
will be posted to you anyway
Or
<form method="post" role="form" new { returnUrl = Context.Request.QueryString["ReturnUrl"]} >
it will work either way

ASP.NET Core MVC custom validation ignored

Validation:
public class MustContainTags : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var msg = (Message)validationContext.ObjectInstance;
if (msg.Text == null)
return new ValidationResult("This field is required.");
var text = msg.Text;
if (Regex.Matches(text, #"($\w+)").Count == 0)
{
return new ValidationResult("Message must contain at least one tag.");
}
else
{
return ValidationResult.Success;
}
}
}
Model:
public class Message
{
...
[BsonElement("msg_text")]
[Required(ErrorMessage = "This field is required.")]
[Display(Name = "Message")]
[MinLength(32, ErrorMessage = "Must contain at least 32 characters.")]
[MaxLength(256, ErrorMessage = "Must contain no more than 256 characters.")]
[MustContainTags(ErrorMessage = "Message must contain at least one tag.")]
public string Text { get; set; }
}
Controller:
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult PostMessage(Message model)
{
if (ModelState.IsValid)
{
//do things and redirect
}
return View(model);
}
Technically looks fine, and it should work. Except it doesn't.
Built-in validation works as expected. When something is incorrect, red text appears under input and informs user something is wrong.
When all constraints are satisfied except [MustContainTags], there is no error message, and ModelState.IsValid = false, which redirects us to view, instead of inform user what is wrong.
Edit
View:
<form method="post" asp-action="NewMessage">
<div class="border p-3">
#*<div asp-validation-summary="ModelOnly" class="text-danger"></div>*#
<div class="form-group row">
<h2 class="text-info pl-3">Write new post</h2>
</div>
<div class="form-group row">
<label asp-for="Message.Title" class="ml-2"></label>
<input asp-for="Message.Title" class="form-control" />
<span asp-validation-for="Message.Title" class="text-danger"></span>
</div>
<div class="form-group row">
<label asp-for="Message.Text" class="ml-2"></label>
<textarea asp-for="Message.Text" class="form-control" rows="6"></textarea>
<span asp-validation-for="Message.Text" class="text-danger"></span>
</div>
</div>
<div class="form-group row">
<div class="col-8 offset-2 row">
<div class="col">
<button type="submit" class="btn btn-info w-100"><i class="fas fa-plus"></i> Post message</button>
</div>
<div class="col">
<a asp-action="Index" class="btn btn-success w-100"><i class="fas fa-sign-out-alt"></i> Back</a>
</div>
</div>
</div>
</form>
#section Scripts{
#{
<partial name="_ValidationScriptsPartial" />
}
}
In this case it might be better to use the override of IsValid that returns a bool.
protected override bool IsValid(object value)
{
if (value == null)
return false;
string valueString = value as string;
if (Regex.Matches(valueString, #"($\w+)").Count == 0)
{
return false;
}
else
{
return true;
}
}
It's not possible to create custom client side validation in ASP.NET. Only built in functions like [Required] are available. You can try bypassing it with javascript.

Using RoleManager ApplicationRole to insert new role

I have a method to add new roles like:
public async Task<IActionResult> CreateRole(ApplicationRoleModel model)
{
try
{
foreach (var role in model.RoleList)
{
var roleExists = await _roleManager.RoleExistsAsync(role.Name);
if (roleExists) continue;
var createRole = _roleManager.CreateAsync(role);
}
return Ok();
}
catch (Exception e)
{
return BadRequest();
}
}
So as you can see I use foreach clause to access model who have IEnumerable of ApplicationRole like:
public class ApplicationRoleModel : ClaimsToRoleModel
{
public IEnumerable<ApplicationRole> RoleList { get; set; }
}
So as you can see in foreach I can access to role.Name of RoleList (ApplicationRole), I tested it with postman and it works, it added new Role successfull, now instead postman I want to create view to create new role so I try:
#page
#model Security.Dto.Models.ApplicationRoleModel
<form asp-controller="security" asp-action="CreateRole" method="post">
<h4>Create new Role</h4>
<hr />
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="RoleList.Name">Role Name</label>
<input asp-for="RoleList.Name" class="form-control" />
</div>
<button type="submit" class="btn btn-default">Create</button>
</form>
But when I try to compile project it return:
'IEnumerable' does not contain a definition for
'Name' and no extension method 'Name' accepting a first argument of
type 'IEnumerable' could be found (are you missing a
using directive or an assembly reference?)
So, how can I access to Name property of ApplicationRole to assign to model from view? Regards
Note: I also try to access it on my view like:
#for(Int32 i = 0; i < this.Model.RoleList.Count; i++ ) {
<div class="form-group">
<label asp-for="RoleList[i].Name">Role Name</label>
<input asp-for="RoleList[i].Name" class="form-control" />
</div>
}
But I get:
Cannot apply indexing with [] to an expression of type
'IEnumerable'
The model in your view is IEnumerable<ApplicationRole> but you attempting to create controls for a single instance of ApplicationRole
You could use List<ApplicationRole> instead of IEnumerable<ApplicationRole>in your model and use jquery to add role lists as below
Model:
public class ApplicationRoleModel : ClaimsToRoleModel
{
public List<ApplicationRole> RoleList { get; set; }
}
View:
#page
#model Security.Dto.Models.ApplicationRoleModel
<form asp-controller="security" asp-action="CreateRole" method="post">
<h4>Create new Role</h4>
<hr />
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group" id="item-list">
<label>Role Name</label>
<input asp-for="RoleList[0].Name" class="form-control items" />
</div>
Add another//click to add
<br />
<button type="submit" class="btn btn-default">Create</button>
</form>
#section Scripts {
<script>
$(function () {
$("#add").click(function (e) {
e.preventDefault();
var i = ($(".items").length);
var n = '<input class="form-control items" name="RoleList[' + i + '].Name" />'
$("#item-list").append(n);
});
});
</script>
}

Display user data in ASP.NET core MVC

Trying to display authenticated user data in an MVC view.
Using ASP.NET Core 2.1
The following error occours:
An unhandled exception occurred while processing the request.
NullReferenceException: Object reference not set to an instance of an object.
AspNetCore.Views_Home_Index.ExecuteAsync() in Index.cshtml, line 6
There seems to be a problem with using #Model.id. What is the correct way of accessing properties of an authenticated user from within the view?
Models/LoginModel.cs
using Microsoft.AspNetCore.Identity;
namespace MyProject.Models
{
public class LoginModel
{
[Required]
[UIHint("email")]
public string Email { get; set; }
[Required]
[UIHint("password")]
public string Password { get; set; }
}
}
Views/Account/Login.cshtml
#model LoginModel
<h1>Login</h1>
<div class="text-danger" asp-validation-summary="All"></div>
<form asp-controller="Account" asp-action="Login" method="post">
<input type="hidden" name="returnUrl" value="#ViewBag.returnUrl" />
<div class="form-group">
<label asp-for="Email"></label>
<input asp-for="Email" class="form-control" />
</div>
<div class="form-group">
<label asp-for="Password"></label>
<input asp-for="Password" class="form-control" />
</div>
<button class="btn btn-primary" type="submit">Login</button>
</form>
Controllers/AccountController.cs
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginModel details, string returnUrl)
{
ApplicationUser user = new ApplicationUser();
if (ModelState.IsValid)
{
user = await userManager.FindByEmailAsync(details.Email);
if (user != null)
{
await signInManager.SignOutAsync();
Microsoft.AspNetCore.Identity.SignInResult result =
await signInManager.PasswordSignInAsync(
user, details.Password, false, false);
if (result.Succeeded)
{
return Redirect(returnUrl ?? "/");
}
}
ModelState.AddModelError(nameof(LoginModel.Email),
"Invalid user or password");
}
return View(details);
}
Views/Home/Index.cshtml
#model ApplicationUser
#if (User.Identity.IsAuthenticated)
{
#Model.Id
}
You can inject the UserManager into the view, and achieve the same result without having the pass a model into the view:
#using Microsoft.AspNetCore.Identity
#inject UserManager<ApplicationUser> UserManager
And then doing:
#await UserManager.GetUserIdAsync(User)

Categories