ASP.NET display ModelState.AddModelError - asp.net

I have this method here, which is a login method and if the username and password is incorrect a ModelError gets added.
[HttpPost]
public ActionResult Login(LoginClass model, string ReturnUrl)
{
if (ModelState.IsValid)
{
if (Membership.ValidateUser(model.UserName, model.Password))
{
FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
if (Url.IsLocalUrl(ReturnUrl) && ReturnUrl.Length > 1 && ReturnUrl.StartsWith("/")
&& !ReturnUrl.StartsWith("//") && !ReturnUrl.StartsWith("/\\"))
{
return Redirect(ReturnUrl);
}
else
{
return RedirectToAction("Index", "Home");
}
}
else
{
ModelState.AddModelError("", "The user name or password provided is incorrect");
}
}
return RedirectToAction("Index", "Home");
}
My question is how would I display the ModelError to my View Index.cshtml?

... how would I display the ModelError to my View Index.cshtml?
Showing Error message
I initially made the assumption that you wanted to redirect to the home page (HomeController action Index) based on your call to return RedirectToAction("Index", "Home"); at the bottom of the Login action. Now I am thinking maybe this is a mistake in your flow and you are actually trying to show the error message to the user without redirecting and that you only want to redirect if everything succeeds. IF this is the case then just read this part and skip the rest about how to persist model state across RedirectToAction calls. All you need to do is call View instead of RedirectToAction if there is a failure.
[HttpPost]
public ActionResult Login(LoginClass model, string ReturnUrl)
{
if (ModelState.IsValid)
{
if (Membership.ValidateUser(model.UserName, model.Password))
{
FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
if (Url.IsLocalUrl(ReturnUrl) && ReturnUrl.Length > 1 && ReturnUrl.StartsWith("/")
&& !ReturnUrl.StartsWith("//") && !ReturnUrl.StartsWith("/\\"))
{
return Redirect(ReturnUrl);
}
else
{
return RedirectToAction("Index", "Home");
}
}
else
{
ModelState.AddModelError("", "The user name or password provided is incorrect");
}
}
return View(model); // goes back to the login view with the existing model and validation error
// return RedirectToAction("Index", "Home");
}
Login.cshtml include the following somewhere
#Html.ValidationSummary()
Working with RedirectToAction
The reason it is not working is the ViewData, which includes the validation messages, is lost when you execute the RedirectToAction. There are a couple of options for a solution.
Keep either persist the ViewData across the RedirectToAction and then restore it.
Only persist the ModelState and then merge it with the ModelState once the new action specified in the RedirectToAction has executed.
Persist ViewData
In most scenarios this works just fine but could cause problems if your redirected to action (in this case Index on the HomeController) has its own ViewData that it depends on.
LoginController.cs
// simplified code to just show the relevant parts to reproduce the problem/solution
[HttpPost]
public ActionResult Login(LoginClass model, string ReturnUrl)
{
// ... some other code
ModelState.AddModelError("", "The user name or password provided is incorrect");
// ... some other code
if (!ModelState.IsValid)
TempData["ViewData"] = ViewData;
return RedirectToAction("Index", "Home");
}
HomeController.cs
public ActionResult Index()
{
if (TempData["ViewData"] != null)
{
// restore the ViewData
ViewData = (ViewDataDictionary)TempData["ViewData"];
}
return View();
}
Home\Index.cshtml
#Html.ValidationSummary()
Merge the ModelState
This is would be my recommended approach because you define how you want this to happen once on a custom ActionFilter attribute and then apply where you want it to occur. You could also put this code directly into your controller but that would violate the DRY principle as soon as you need to do this on multiple controllers.
The approach here is to write the model state to the TempData if the TempData does not already contain a "ModelState" key. If there is already a key present that means that the current request has just written to it and we can read from it and merge that with our existing model state. This will prevent the code from unintentionally overwriting the ViewState or the ModelState as the ModelState is now merged. I can only see this going wrong if there are multiple RedirectToActions that all choose to write to the ModelState but I do not think this is a likely scenario.
ModelStateMergeFilterAttribute.cs
public class ModelStateMergeFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
// write to the temp data if there is modelstate BUT there is no tempdata key
// this will allow it to be merged later on redirect
if (filterContext.Controller.TempData["ModelState"] == null && filterContext.Controller.ViewData.ModelState != null)
{
filterContext.Controller.TempData["ModelState"] = filterContext.Controller.ViewData.ModelState;
}
// if there is tempdata (from the previous action) AND its not the same instance as the current model state THEN merge it with the current model
else if (filterContext.Controller.TempData["ModelState"] != null && !filterContext.Controller.ViewData.ModelState.Equals(filterContext.Controller.TempData["ModelState"]))
{
filterContext.Controller.ViewData.ModelState.Merge((ModelStateDictionary)filterContext.Controller.TempData["ModelState"]);
}
base.OnActionExecuted(filterContext);
}
}
LoginController.cs
// simplified the code to just show the relevant parts
[HttpPost]
[ModelStateMergeFilter]
public ActionResult Login(LoginClass model, string ReturnUrl)
{
ModelState.AddModelError("", "The user name or password provided is incorrect");
return RedirectToAction("Index", "Home");
}
HomeController.cs
[ModelStateMergeFilter]
public ActionResult Index()
{
return View();
}
Home\Index.cshtml
#Html.ValidationSummary()
References
Here are some references that also detail some of these approaches. I also relied on some of the input from these previous answers for my answer above.
How do I maintain ModelState errors when using RedirectToAction?
How can I maintain ModelState with RedirectToAction?

If your are using MVC, you can use Validation Summary.
#Html.ValidationSummary();
https://msdn.microsoft.com/en-CA/library/dd5c6s6h%28v=vs.71%29.aspx
Also,pass your model back to your view:
return RedirectToAction("Index", "Home", model);

Related

How to show a successful login popup message when the successful login can go to a redirect url?

I have the following login post action in the controller:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
[Auditing]
public async Task<ActionResult> Login(LoginModel details, string returnUrl)
{
if (ModelState.IsValid)
{
AppUser user = await UserManager.FindAsync(details.Name,
details.Password);
if (user == null)
{
ModelState.AddModelError("", "Invalid name or password.");
}
else
{
ClaimsIdentity ident = await UserManager.CreateIdentityAsync(user,
DefaultAuthenticationTypes.ApplicationCookie);
ident.AddClaims(LocationClaimsProvider.GetClaims(ident));
ident.AddClaims(ClaimsRoles.CreateRolesFromClaims(ident));
AuthManager.SignOut();
AuthManager.SignIn(new AuthenticationProperties
{
IsPersistent = false
}, ident);
//Persist login into DB upon successful login
Loginrecord login = new Loginrecord();
login.Username = user.UserName;
login.SessionId = HttpContext.Session.SessionID;
Session["sessionid"] = HttpContext.Session.SessionID;
login.Date = DateTime.Now;
SQLLoginrecord sqlLogin = new SQLLoginrecord();
sqlLogin.PutOrPostLogin(login);
//End addition
return Redirect(returnUrl);
}
}
ViewBag.returnUrl = returnUrl;
return View(details);
}
Since a successful login from this action can go to any authorization-requiring page the user types the URL for in the address bar of the browser, how can I show a popup message that indicates a successful login? If I were to go with the ViewBag approach and add a Success variable, do I have to access that ViewBag from every View (including some that do not have a shared Layout) of the application that requires authentication?
Redirect method is going to issue a 302 response to the browser with the new url in the location header and browser will make a totally new Http request for that url. So ViewBag is not going to help you.
You might consider using TempData (which works for the next call) to share data from your Login action method to any other action method. You can write an action filter which checks the TempData item and set the approriate ViewBag entry so that you can do whatever you want in your other views.
public class LoginMsg : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var msg=filterContext.Controller.TempData["LoginMsg"] as string;
if (!String.IsNullOrEmpty(msg))
{
filterContext.Controller.ViewBag.LoginMsg= msg;
}
base.OnActionExecuting(filterContext);
}
}
Now in your Login class, before redirecting, set the TempData
public async Task<ActionResult> Login(LoginModel details, string returnUrl)
{
// do stuff
TempData["LoginMsg"]="Logged in successfully";
return Redirect(returnUrl);
}
Now decorate your other action methods (or you can do it on controller level) with our action filter.
[LoginMsg]
public ActionResult Dashboard()
{
return View();
}
In your view or the _Layout, you can access the ViewBag item and call the code to the trigger your popup control.
<script>
var msg = "#ViewBag.Msg";
if (msg.length) {
// alert(msg);
// Call the code to fire the popup now
// yourMagicPopupPlugin.Popup(msg);
}
</script>

About Response.Redirect, FormsAuthentication and MVC

I want to understand the difference in the behavior of a program when we call FormsAuthentication.RedirectFromLoginPage vs. when we call Response.Redirect(FormsAuthentication.GetRedirectUrl()) and manually redirect.
Please see the comments below.
I have a LoginController/Index (two actions, one for HttpGet and one for HttpPost). The View of this controller represents the application's login page.
I also have a home page or landing page, i.e. the page that the user must be taken to after a successful login. This is represented in my application by the HomeController's Index action and the ~Views/Home/Index.cshtml view.
I have presented three scenarios. I understand scenario 1 and I expect it to work the way it does, but I noted a difference in scenarios 2 and 3.
Scenario 1
namespace Controllers
{
[AllowAnonymous]
public class LoginController : Controller
{
[HttpPost]
public ActionResult Index(Login loginViewModel)
{
if (ModelState.IsValid)
{
var user = ValidateUser(loginViewModel);
if (user != null)
{
// Other stuff: set cookies, session state, etc.
return RedirectToAction("Index", "Home");
}
else
{
ModelState.AddModelError("", "Invalid password. Please try again.");
}
}
// If the user was a valid user, the flow-of-control won't reach here
// as expected and the user will be taken to the view that is served
// by the HomeController::Index() action. If it is by convention, it will
// be the ~Views/Home/Index.cshtml view. This is fine.
return View();
}
}
}
Scenario 2
namespace Controllers
{
[AllowAnonymous]
public class LoginController : Controller
{
[HttpPost]
public ActionResult Index(Login loginViewModel)
{
if (ModelState.IsValid)
{
var user = ValidateUser(loginViewModel);
if (user != null)
{
// Other stuff: set cookies, session state, etc.
Response.Redirect(FormsAuthentication.GetRedirectUrl(loginViewModel.UserName,
loginViewModel.RememberMe));
}
else
{
ModelState.AddModelError("", "Invalid password. Please try again.");
}
}
// If the user was a valid user, the flow-of-control still reaches here
// as expected. And as expected, it renders the same View, i.e. the View
// associated with the controller we are in, which is ~Views/Login/Index,
// which represents the login page. This is wrong. I shouldn't redirect here.
// I understand this. My question here is two fold:
// 1) I am simply trying to understand the difference in behaviors of the three
// scenarios described in this question.
// 2) Given this, the right way would be to not use Response.Redirect here but instead
// use RedirectToAction. However, if I wanted to use Response.Redirect, what should
// I do?
return View();
}
}
}
Scenario 3
namespace Controllers
{
[AllowAnonymous]
public class LoginController : Controller
{
[HttpPost]
public ActionResult Index(Login loginViewModel)
{
if (ModelState.IsValid)
{
var user = ValidateUser(loginViewModel);
if (user != null)
{
// Other stuff: set cookies, session state, etc.
FormsAuthentication.RedirectFromLoginPage(loginViewModel.UserName,
loginViewModel.RememberMe);
}
else
{
ModelState.AddModelError("", "Invalid password. Please try again.");
}
}
// If the user was a valid user, the flow-of-control still reaches here
// as expected. However, magically, somehow, even though the statement below
// suggests that the user must be taken to the View of the same controller and
// action that we are currently in, i.e. the View of the LoginController::Index()
// action, i.e. the ~Views/Login/Index.cshtml, it magically takes me to the
// ~Views/Home/Index.cshtml instead, which is what is specified as the LoginPage
// attribute of the <authentication>/<forms> element in the web.config.
// I want to know how this happens.
return View();
}
}
}
Update
I am at my wit's end now. Now, even Scenario 1 that uses RedirectToAction is calling the Index() action on the LoginController class.
The actual difference is that FormsAuthentication.RedirectFromLoginPage() sets cookies and then makes redirects but FormsAuthentication.GetRedirectUrl() only returns redirect url.
The funny thing is that implementation of FormsAuthentication.GetRedirectUrl() is like this:
public static String GetRedirectUrl(String userName, bool createPersistentCookie)
{
if (userName == null)
return null;
return GetReturnUrl(true);
}
So actually userName and createPersistentCookie parameters are completely ignored. You must call FormsAuthentication.SetAuthCookie( userName, true/false ) manually before calling GetRedirectUrl.
Agree with Vasily.
RedirectFromLoginPage issues an authentication ticket and places it in the default cookie using the SetAuthCookie method.
You can read something about this behavior here.
If you want to have a better control over the cookie creation you should (encryption, expiration, extending the principal) you should create the cookie yourself.
I explained the whole process here and here.

MVC 3 ReturnUrl redirection not working

I have a probably very silly question but I'll has it anyway.
Here is the code in my controller for logging in
[HttpPost]
public ActionResult Index(LogonModel model, string ReturnUrl)
{
ReturnUrl = Request.QueryString["ReturnUrl"];
if (ModelState.IsValid)
{
if (UserRepository.validLogin(model.Username, model.Password))
{
UserLogRepository.createLogEntry("Log On", " has logged on to the Staff Portal.", "Entry/Exit");
if (ReturnUrl.Length > 1)
{
return Redirect(Request.QueryString["ReturnUrl"]);
}
else
{
return RedirectToAction("Dashboard", "Home");
}
}
else
{
ModelState.AddModelError("", Session["Error"].ToString());
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
As you can see i'm just checking if the returnurl has a length for testing purposes before I lock it down more. My issue is I get an "Object reference not set to an instance of an object." pointing to this line "if (ReturnUrl.Length > 1)"
Now the URL I have when a user has timed out from the site is this:
http://localhost/Dispatch2012/Staff/Home?ReturnUrl=Dispatch2012%2FStaff%2FCredential
As you can see, this is the standard redirect created by MVC 3 and i've tried to read the ReturnUrl as a standard query string but every time it says that object doesn't exist. What am I missing?
The way your controller is set up is strange to me, but let's dive into it:
[HttpPost]
public ActionResult Index(LogonModel model, string returnUrl) //changed
{
ReturnUrl = returnUrl; //changed
if (ModelState.IsValid)
{
if (UserRepository.validLogin(model.Username, model.Password))
{
UserLogRepository.createLogEntry("Log On", string.Format("{0} has logged on to the Staff Portal.", model.Username, "Entry/Exit"); //changed
if (ReturnUrl.Length > 1) //this should use IsLocalUrl
{
return Redirect(Request.QueryString["ReturnUrl"]);
}
else
{
return RedirectToAction("Dashboard", "Home");
}
}
else
{
ModelState.AddModelError("", Session["Error"].ToString());
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
A few things:
Your returnUrl needs to be checked to make sure it's a Local URL. There are a number of ways of doing this, and since you're using ASP.NET MVC 3, it's built in.
Why are you pulling the ReturnUrl out of the querystring when (if you've set up your view correctly), it's already passed in?
Do you have the following in your view?
<%= Html.Hidden("returnUrl", Url.Encode(Url.Action("ActionToRedirectTo", "ControllerName", new { id = Model.Whatever}))) %>
If so, when it posts, it will automatically get sent to the Index Action as the returnUrl parameter.
I'm betting since it's not working, you aren't actually sending the ReturnUrl back correctly, check what I said about the view. Oh, and make sure you're URL encoding the ReturnUrl.
Also, since it's a HttpPost, the querystring wouldn't have the return Url in it.

Redirecting to mobile page in Asp.Net MVC 3

I'm trying to use a different login page for an Asp.Net MVC application that is modified to fit mobile devices, primarily iPhone/Android. All I basically need is to modify the login view, because the actual content is in a particular part of the application, I'm not trying to make a mobile version of the entire site.
So I tried to follow this: http://www.asp.net/learn/whitepapers/add-mobile-pages-to-your-aspnet-web-forms-mvc-application
But I don't know the authentication well enough to know exactly how to do the logon action methods for the mobile version. I feel like I'm probably missing a specific mobile post action,and I don't understand what to do with the url passed in the redirect. Here's what I've got so far:
public ActionResult LogOn()
{
string returnUrl = Request.QueryString["ReturnUrl"];
if ((returnUrl != null) && returnUrl.StartsWith("/Mobile/",
StringComparison.OrdinalIgnoreCase))
{
return RedirectToAction("LogOnMobile", "Account",
new { ReturnUrl = returnUrl });
}
return View();
}
public ActionResult LogOnMobile(string returnurl)
{
return View();
}
[HttpPost]
public ActionResult LogOn(LogOnModel model, string returnUrl)
{
if (ModelState.IsValid)
{
if (MembershipService.ValidateUser(model.UserName, model.Password))
{
FormsService.SignIn(model.UserName, model.RememberMe);
if (Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction("Index", "Home");
}
}
else
{
ModelState.AddModelError("", "The user name or password provided is incorrect.");
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
And this doesn't work. I get to the mobile login page (or actually so far I've just tried out that the action method works by commenting out the if clause), but when I try to login I just get to the same page again, but strangely without the fields...
What do I need to do to get this to work?
Do you keep the account controller in the Mobile Area? Better practice will be using the mobile Area to keep all your mobile site stuff and redirect the authorization to the AccountController.
public ActionResult Login()
{
string returnUrl = Request.QueryString["ReturnUrl"];
if ((returnUrl != null) && returnUrl.StartsWith("/Mobile/", StringComparison.OrdinalIgnoreCase))
return RedirectToAction("Login", "Account", new { Area = "Mobile", ReturnUrl = returnUrl });
return ContextDependentView();
}
I don't see anywhere you are actually checking to see if it is a mobile device to re-direct:
if (Request.Browser.IsMobileDevice){ }

asp mvc session variables

Need to store some auth information of client on server, to share between 2 pages.
Dont matter how, session, cookie, tempdata, i tried everything, and nothing works, for exampe:
public ActionResult CheckIn(string pass)
{
if (System.Configuration.ConfigurationManager.AppSettings["pass"] == pass)
{
HttpContext.Session.Add("admin", "yes");
}
return View();
}
public ActionResult Helper() {
if (HttpContext.Session["admin"] != null)
{
if (HttpContext.Session["admin"].ToString() == "yes")
return PartialView("InitConfig");
else
return PartialView("StationLogics");
}
else
return PartialView("StationLogics");
}
and i get always null in session in helper method. what i'm doing wrong?
Have you tried HttpContext.Current.Session instead ?
ps. doing that you do is actually not good desing you should reconsider it in your application.
Are you sure that all those if conditions are met? Try with a simple example:
public ActionResult CheckIn()
{
Session["foo"] = "bar";
return View();
}
public ActionResult Helper()
{
var foo = Session["foo"];
if (foo == null)
{
// this won't be null if you called the CheckIn method first
}
return View();
}
Call the CheckIn action first to store a value in the session and then call the Helper action. Unless you have disabled session for this ASP.NET application or cookies in your browser you should get correct value.

Resources