I have an #Html.ActionLink inside of a partial view that when clicked I'd like to have either send the user to another view or stay on the current view without changing anything. Is this possible?
Our controller looks like:
public ActionResult Edit(int id)
{
if (ShouldAllowEdit(id))
{
return this.View("Edit", ...edit stuff...)
}
return ????????
}
We tried return new EmptyResult(); but that just dumps the user to a blank page.
This is a little different approach to the issue, but it should do what you want.
Instead of giving the user a link to navigate to, do an ajax call on link/button click, and do the id check. Return either the url to navigate to in a JsonResult, or nothing if the id is invalid.
On return of the ajax call, navigate to the url if appropriate.
(swap out the hard coded ids and the == 0 with your ShouldAllowEdit function in the example of course)
In the View:
<div class="btn btn-danger" id="myButton">Button</div>
#section scripts{
<script>
$("#myButton").click(function () {
$.ajax("#Url.Action("Edit", new { id = 0 })", { type : "POST" })
.success(function (data) {
if (data.url !== "") {
window.location.href = data.url;
}
});
});
</script>
}
In the controller:
[HttpPost]
public JsonResult Edit(int id)
{
if (id == 0)
{
return Json(new {url = ""});
}
else
{
return Json(new { url = Url.Action("EditPage", new { id = id }) });
}
}
An answer is to redirect to the view action - and maybe give some feed back why they failed.
public ActionResult Edit(int id)
{
if (ShouldAllowEdit(id))
{
return this.View("Edit", ...edit stuff...)
}
ModelState.AddModelError("id", "Not allowed to edit this item");
return RedirectToAction(Edit(id));
}
If the user clicks a link they will be taken away. They might be sent back right to the same page, but the page will unload, be requested from the server again, and then re-rendered in the browser. If you don't want that to happen, you don't give the user the link in the first place. In other words, conditionally render the link or not based on the user's roles or whatever.
#if (userCanEdit)
{
#Html.ActionLink(...)
}
Where userCanEdit is whatever logic you need to make that determination.
If the user fails whatever check you determine, then they don't get the link. Simple.
However, since there's malicious people in the world, you can't just leave it entirely there. There's potential for the user to figure out the link to edit something and go there manually. So, to prevent that you check for the edit permission in your action (like you've already got in your code sample), but if the user is not allowed, then you just return a forbidden status code:
return new HttpStatusCodeResult(HttpStatusCode.Forbidden);
Or
return new HttpStatusCodeResult(403);
They both do the same thing.
UPDATE
Based on your comment above, it appears that the user is normally allowed to edit but can't in a particular instance because another user is editing. A 403 Forbidden is not appropriate in that case, so really all you've got is a simple redirect back to the page they were on, perhaps with a message explaining why they're back there.
TempData["EditErrorMessage"] = "Sorry another user is editing that right now.";
return RedirectToAction("Index");
Related
I am sending a success message in response from API to controller and setting its value in TempData["Message"], now redirecting to some other action method.
like this:
[HttpPost]
public ActionResult AddFeedbackCategory(IFeedbackCategoryModel modelData)
{
var message = JsonConvert.DeserializeObject<ApiResponse>(response);
TempData["Message"] = message.Message;
return RedirectToAction("CategoryList");
}
[HttpGet]
public ActionResult CategoryList()
{
var categoryData = JsonConvert.DeserializeObject<List<CategoryModel>>(data);
return View(categoryData);
}
and in the view of CategoryList I have written code something like this:
#if (TempData["Message"] != null)
{
<div id="snackbar" class="snack_success">#TempData["Message"].ToString()
</div>
TempData["Message"] = null;
}
It help to show a success message(retains for 2 second) whenever I add some record in my system like this.
Now the problem is when I hit back button and then again return to category list this success message appear again. Why its value not becoming null? is there any specific reason or I am doing something wrong?
Let's say you navigate from Page A to Page B. Then you click Back on Page B (to go back to Page A). If you do not explicitly set caching headers on Page A, the browser may decide to display the old version of Page A (that it rendered before going to Page B).
To solve this, one option is to disable browser caching. For example, use this against the GET endpoint's action method:
[OutputCache(NoStore = true, Duration = 0, VaryByParam = "*", Location = OutputCacheLocation.None)]
This will ensure that, if you press the Back button, the browser will be forced to hit the server again to re-render Page A (rather than just showing the old version of Page A).
I have a Controller with some actions on it as follows:
[HttpPost]
public ActionResult Create(CreateModel model)
{
if (model.SelectedCustomers.Count > 0 &&
model.SelectedVersions.Count > 0 &&
!string.IsNullOrWhiteSpace(model.ScriptName) &&
!string.IsNullOrWhiteSpace(model.ScriptText))
{
Script script;
...save to database...
return Edit(script.Id); //<---------Return other view here
}
else
{
...
}
}
[HttpGet]
public ActionResult Edit(int? scriptId)
{
return View();
}
After the Create action runs, and saves my model to the database successfully, I want to send the user to the Edit view for the newly created script. When I use the code above, specifically return Edit(script.Id); it just sends the user back to the Create view instead of the Edit view. When the user navigates to the Edit action directly, or through the result of an Html.ActionLink pointed at Edit everything works correctly.
What am I doing wrong?
This isn't doing what you think it does:
return Edit(script.Id)
It's not actually telling the framework to go to that action. It's just returning the return value of that method. Purely a C# concern before any components of the ASP.NET MVC Framework are involved at all. And what is that return value:
return View()
So the former is really functionally the same thing as the latter. And any time you use return View() in ASP.NET MVC, the framework will determine that view by examining the action currently being called, which in this case is Create.
What you want isn't to return the Edit view (even if you do, in this case, the user is still on the Create URL, which will cause confusion). What you want is to return a redirect to tell the client to request that next action:
return RedirectToAction("Edit", new { scriptId = script.Id });
You can always call RedirectToAction and return that action result. That will inform the browser to redirect to the different action.
I think you will need something like this:
return RedirectToAction("Edit", new { scriptId = script.Id });
Calling Edit directly is no different than calling a method.
You can do with this RedirecToAction with input parameters.
return RedirectToAction("Action", new { id = 12 });
In Your Case:
return RedirectToAction("Edit", new { scriptId = script.Id });
Question background:
I am implementing some basic 'shopping cart' logic to an MVC app. Currently when I click a link - denoted as 'Add To Cart' on the screen shot below this calls to an 'AddToCart' method in the 'ProductController' as shown:
Product.cshtml code:
#Html.ActionLink("Add To Cart", "AddToCart")
'AddToCart' method in the ProductController:
public void AddToCart()
{
//Logic to add item to the cart.
}
The issue:
Not an issue as such but currently when I click the 'Add To Cart' button on the ActionLink on the ProductDetail.cshtml view the page calls the 'AddToCart' method on the ProductController and gives a blank view on the page - as shown below. I want the view to stay on 'ProductDetail.cshtml' and just call the 'AddToCart' method, how do I do this?
Basically #Html.ActionLink() or <a></a> tag uses get request to locate the page. Hence whenever you clicked it, you request to your AddToCart action method in ProductController and if that action method returns null or void so a blank or empty page is shown as you experienced (because or #Html.ActionLink() get request by Default).
So if you want to add your value to cart then call AddToCart method using ajax i.e:
HTML:
#Html.ActionLink("Add To Cart", "AddToCart", null, new { id="myLink"})
Jquery or Javascript:
$("#myLink").click(function(e){
e.preventDefault();
$.ajax({
url:$(this).attr("href"), // comma here instead of semicolon
success: function(){
alert("Value Added"); // or any other indication if you want to show
}
});
});
'AddToCart' method in the ProductController:
public void AddToCart()
{
//Logic to add item to the cart.
}
Now this time when the call goes to AddToCart method it goes by using ajax hence the whole page will not redirect or change, but its an asynchronous call which execute the AddToCart action method in your ProductController and the current page will remains same. Hence the product will also added to cart and page will not change to blank.
Hope this helps.
The answer of Syed Muhammad Zeeshan is what you are looking for, however you may return an EmptyResult.
public ActionResult AddToCart()
{
//Logic to add item to the cart.
return new EmptyResult();
}
According to this it has no impact on your code ASP.Net MVC Controller Actions that return void
But maybe sometime you want to return data and then you could do something like this:
if (a)
{
return JSon(data);
}
else
{
return new EmptyResult();
}
As many people mentioned here you will need to use AJAX if your using asp.net MVC to hit a controller POST function without having to leave your view.
A good use case for this is if you want to upload a file without refreshing the page and save that on the server.
All of the
return new EmptyResult();
Wont work, they will still redirect you.
Here is how you do it, in your view have the follow form as an example:
<form enctype="multipart/form-data" id="my-form">
<p>
The CSV you want to upload:
</p>
<input type="file" class="file-upload" name="FileUpload" />
</div>
<div>
<button type="submit" class="btn btn-default" name="Submit" value="Upload">Upload</button>
</div>
</form>
Then in the JavaScript side you need to add this to your view with within Script tags.
$("#my-form").on('submit', function (event) {
event.preventDefault();
// create form data
var formData = new FormData();
//grab the file that was provided by the user
var file = $('.file-upload')[0].files[0];
// Loop through each of the selected files.
formData.append('file', file);
if (file) {
// Perform the ajax post
$.ajax({
url: '/YourController/UploadCsv',
data: formData,
processData: false,
contentType: false,
type: 'POST',
success: function (data) {
alert(data);
}
});
}
});
Your controller might look something like this to process this type of file:
[HttpPost]
public void UploadCsv()
{
var listOfObjects = new List<ObjectModel>();
var FileUpload = Request.Files[0]; //Uploaded file
//check we have a file
if (FileUpload.ContentLength > 0)
{
//Workout our file path
string fileName = Path.GetFileName(FileUpload.FileName);
string path = Path.Combine(Server.MapPath("~/App_Data/"), fileName);
//Try and upload
try
{
//save the file
FileUpload.SaveAs(path);
var sr = new StreamReader(FileUpload.InputStream);
string csvData = sr.ReadToEnd();
foreach (string r in csvData.Split('\n').Skip(1))
{
var row = r;
if (!string.IsNullOrEmpty(row))
{
//do something with your data
var dataArray = row.Split(',');
}
}
}
catch (Exception ex)
{
//Catch errors
//log an error
}
}
else
{
//log an error
}
}
There are many ways to accomplish what you want, but some of them require a lot more advanced knowledge of things like JavaScript than you seem aware of.
When you write ASP.NET MVC applications, you are required to have more intimate knowledge of how browsers interact with the web server. This happens over a protocol called HTTP. It's a simple protocol on the surface, but it has many subtle details that you need to understand to successfully write ASP.NET MVC apps. You also need to know more about Html, CSS, and JavaScript.
In your case, you are creating an anchor tag (<a href="..."/>), which when click upon, instructs the browser to navigate to the url in the href. That is why you get a different page.
If you don't want that, there are a number of ways change your application. The first would be, instead of using an ActionLink, you instead simply have a form and post values back to your current controller. And call your "add to cart" code from your post action method.
Another way would be have your AddToCart method look at the referrer header (again, part of that more subtle knowledge of http) and redirect back to that page after it has processed its work.
Yet another way would be to use Ajax, as suggested by Syed, in which data is sent to your controller asynchronously by the browser. This requires that you learn more about JavaScript.
Another option is to use an embedded iframe and have your "add to cart" be it's own page within that iframe. I wouldn't necessarily suggest that approach, but it's a possibility.
Controller should return ActionResult. In this case a redirect to the caller page.
using System.Web.Mvc;
using System.Web.Mvc.Html;
public ActionResult Index()
{
HtmlHelper helper = new HtmlHelper(new ViewContext(ControllerContext, new WebFormView(ControllerContext, "Index"), new ViewDataDictionary(), new TempDataDictionary(), new System.IO.StringWriter()), new ViewPage());
helper.RenderAction("Index2");
return View();
}
public void Index2(/*your arg*/)
{
//your code
}
I was struggling with this and couldn't get it working with ajax.
Eventually got a working solution by making my controller method return type ActionResult rather than void and returning a RedirectToAction() and inputting the action relating to the view I wanted to remain on when calling the controller method.
public ActionResult Method()
{
// logic
return RedirectToAction("ActionName");
}
I want to build a login form that displays in a sidebar across every page in my site. If the user enters an incorrect user/pass, I want the errors to be displayed above this form (with the rest of the page remaining as it was before), and if he logs in successfully, I want the form to change to a list of information about the user (again, with the rest of the page the same as before the login). I am using an MVC 3 web application project with the default Internet Application template. I have this:
_Layout.cshtml
#{
if (User.Identity.IsAuthenticated)
{
Html.RenderAction("ShowUserInfo", "User");
}
else
{
Html.RenderAction("LogIn", "User");
}
}
UserController
[ChildActionOnly]
public PartialViewResult ShowUserInfo()
{
// populate loggedInInfo from database based on
// User.Identity.Name
return PartialView("_LoggedInInfo", loggedInInfo);
}
private ActionResult RedirectToPrevious(string returnUrl)
{
if (Url.IsLocalUrl(returnUrl) && returnUrl.Length > 1 && returnUrl.StartsWith("/")
&& !returnUrl.StartsWith("//") && !returnUrl.StartsWith("/\\"))
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction("index", "");
}
}
[ChildActionOnly]
public PartialViewResult LogIn()
{
return PartialView("_LogInForm");
}
//
// POST: /User/LogIn
[HttpPost]
public ActionResult LogIn(LogInModel model, string returnUrl)
{
if (ModelState.IsValid)
{
if (Membership.ValidateUser(model.UserName, model.Password))
{
FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
return RedirectToPrevious(returnUrl);
}
else
{
ModelState.AddModelError("", "The user name or password provided is incorrect.");
}
}
return RedirectToPrevious(returnUrl);
}
_LogInForm
#model MyProject.Models.LogInModel
<h2>Login</h2>
<p>
Please enter your username and password. #Html.ActionLink("Register", "register", "user") if you don't have an account.<br />
#Html.ValidationSummary(true, "Login was unsuccessful. Please correct the errors and try again.")
</p>
#using (Html.BeginForm("LogIn", "user")) {
html stuff
}
This works almost as intended, except that when I enter a wrong username/password, the page just reloads with an empty form and no error is displayed. I have tried some other things too, but I either get errors about how I cannot issue a redirect from a partial view or I get the partial view (with the errors showing up) displayed as a whole view, so it shows up as a single page, separate from the rest of the site. If I log in correctly, everything works fine.
How can I get the errors to be correctly displayed above the form? I would rather not use any Ajax or JQuery to do this.
The problem seem to be that you are doing a redirect rather than just returning the appropriet view.
After you've added a model error, you need to return the view instead of performing a redirect :
return View("LoginViewNameGoesHere")
So you do not want to return the partial view here, but the entire view.
When doing, RedirectToAction and Redirect, current request ends with http status code for redirection - 3xx, which tells browser to make another request to specified url. This means, all the validation data of current request is lost, and all-new, pure request is made to login url. And you get empty form and no errors.
What you should do is render the login view within current request scope, not via doing redirection. General pattern for redisplaying invalid view
public ActionResult Login(LogInModel model)
{
if(ModelState.IsValid)
{
return RedirectToAction("Home");
}
return View(model); //! no redirection
}
How can I return the result of a different action or move the user to a different action if there is an error in my ModelState without losing my ModelState information?
The scenario is; Delete action accepts a POST from a DELETE form rendered by my Index Action/View. If there is an error in the Delete I want to move the user back to the Index Action/View and show the errors that are stored by the Delete action in the ViewData.ModelState. How can this be done in ASP.NET MVC?
[AcceptVerbs(HttpVerbs.Post | HttpVerbs.Delete)]
public ActionResult Delete([ModelBinder(typeof(RdfUriBinder))] RdfUri graphUri)
{
if (!ModelState.IsValid)
return Index(); //this needs to be replaced with something that works :)
return RedirectToAction("Index");
}
Store your view data in TempData and retrieve it from there in your Index action, if it exists.
...
if (!ModelState.IsValid)
TempData["ViewData"] = ViewData;
RedirectToAction( "Index" );
}
public ActionResult Index()
{
if (TempData["ViewData"] != null)
{
ViewData = (ViewDataDictionary)TempData["ViewData"];
}
...
}
[EDIT] I checked the on-line source for MVC and it appears that the ViewData in the Controller is settable, so it is probably easiest just to transfer all of the ViewData, including the ModelState, to the Index action.
Use Action Filters (PRG pattern) (as easy as using attributes)
Mentioned here and here.
Please note that tvanfosson's solution will not always work, though in most cases it should be just fine.
The problem with that particular solution is that if you already have any ViewData or ModelState you end up overwriting it all with the previous request's state. For example, the new request might have some model state errors related to invalid parameters being passed to the action, but those would end up being hidden because they are overwritten.
Another situation where it might not work as expected is if you had an Action Filter that initialized some ViewData or ModelState errors. Again, they would be overwritten by that code.
We're looking at some solutions for ASP.NET MVC that would allow you to more easily merge the state from the two requests, so stay tuned for that.
Thanks,
Eilon
In case this is useful to anyone I used #bob 's recommended solution using PRG:
see item 13 -> link.
I had the additional issue of messages being passed in the VeiwBag to the View being written and checked / loaded manually from TempData in the controller actions when doing a RedirectToAction("Action"). In an attempt to simplify (and also make it maintainable) I slightly extended this approach to check and store/load other data as well. My action methods looked something like:
[AcceptVerbs(HttpVerbs.Post)]
[ExportModelStateToTempData]
public ActionResult ChangePassword(ProfileViewModel pVM) {
bool result = MyChangePasswordCode(pVM.ChangePasswordViewModel);
if (result) {
ViewBag.Message = "Password change success";
else {
ModelState.AddModelError("ChangePassword", "Some password error");
}
return RedirectToAction("Index");
}
And my Index Action:
[ImportModelStateFromTempData]
public ActionResult Index() {
ProfileViewModel pVM = new ProfileViewModel { //setup }
return View(pVM);
}
The code in the Action Filters:
// Following best practices as listed here for storing / restoring model data:
// http://weblogs.asp.net/rashid/archive/2009/04/01/asp-net-mvc-best-practices-part-1.aspx#prg
public abstract class ModelStateTempDataTransfer : ActionFilterAttribute {
protected static readonly string Key = typeof(ModelStateTempDataTransfer).FullName;
}
:
public class ExportModelStateToTempData : ModelStateTempDataTransfer {
public override void OnActionExecuted(ActionExecutedContext filterContext) {
//Only export when ModelState is not valid
if (!filterContext.Controller.ViewData.ModelState.IsValid) {
//Export if we are redirecting
if ((filterContext.Result is RedirectResult) || (filterContext.Result is RedirectToRouteResult)) {
filterContext.Controller.TempData[Key] = filterContext.Controller.ViewData.ModelState;
}
}
// Added to pull message from ViewBag
if (!string.IsNullOrEmpty(filterContext.Controller.ViewBag.Message)) {
filterContext.Controller.TempData["Message"] = filterContext.Controller.ViewBag.Message;
}
base.OnActionExecuted(filterContext);
}
}
:
public class ImportModelStateFromTempData : ModelStateTempDataTransfer {
public override void OnActionExecuted(ActionExecutedContext filterContext) {
ModelStateDictionary modelState = filterContext.Controller.TempData[Key] as ModelStateDictionary;
if (modelState != null) {
//Only Import if we are viewing
if (filterContext.Result is ViewResult) {
filterContext.Controller.ViewData.ModelState.Merge(modelState);
} else {
//Otherwise remove it.
filterContext.Controller.TempData.Remove(Key);
}
}
// Restore Viewbag message
if (!string.IsNullOrEmpty((string)filterContext.Controller.TempData["Message"])) {
filterContext.Controller.ViewBag.Message = filterContext.Controller.TempData["Message"];
}
base.OnActionExecuted(filterContext);
}
}
I realize my changes here are a pretty obvious extension of what was already being done with the ModelState by the code # the link provided by #bob - but I had to stumble on this thread before I even thought of handling it in this way.
Please don't skewer me for this answer. It is a legitimate suggestion.
Use AJAX
The code for managing ModelState is complicated and (probably?) indicative of other problems in your code.
You can pretty easily roll your own AJAX javascript code. Here is a script I use:
https://gist.github.com/jesslilly/5f646ef29367ad2b0228e1fa76d6bdcc#file-ajaxform
(function ($) {
$(function () {
// For forms marked with data-ajax="#container",
// on submit,
// post the form data via AJAX
// and if #container is specified, replace the #container with the response.
var postAjaxForm = function (event) {
event.preventDefault(); // Prevent the actual submit of the form.
var $this = $(this);
var containerId = $this.attr("data-ajax");
var $container = $(containerId);
var url = $this.attr('action');
console.log("Post ajax form to " + url + " and replace html in " + containerId);
$.ajax({
type: "POST",
url: url,
data: $this.serialize()
})
.done(function (result) {
if ($container) {
$container.html(result);
// re-apply this event since it would have been lost by the form getting recreated above.
var $newForm = $container.find("[data-ajax]");
$newForm.submit(postAjaxForm);
$newForm.trigger("data-ajax-done");
}
})
.fail(function (error) {
alert(error);
});
};
$("[data-ajax]").submit(postAjaxForm);
});
})(jQuery);
Maybe try
return View("Index");
instead of
return Index();