MVC adding items to sessions and displaying in view - asp.net

I'm working on a ASP.NET web shop but I'm stuck on a particular part. I'm trying to create a session whenever a user (not logged in) adds a CD or a DVD to their shopping cart. That way they can go to their cart (/shopping_cart) and see all of the products they have added.
However, I can't make it work. Whenever I click on 'add product' nothing really happens.
Here's my code:
ProductsController:
// POST: Producten/Details/9
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult AddToCart(int id)
{
Product p = db.Producten.Find(id);
if (p != null)
{
Models.Winkelkar kar;
if(Session["kar"] == null)
{
kar = new Models.Winkelkar();
}else
{
kar = (Models.Winkelkar)Session["winkelkar"];
}
kar.AddProduct(p);
Session["winkelkar"] = kar;
//var products = Session["kar"] as List<Product>;
}
return View();
}
My Models.Winkelkar looks like this:
public partial class Winkelkar
{
private List<Product> _List = new List<Product>();
public List<Product> GetList()
{
// geeft een lijst van pbjecten terug
return _List;
}
public void AddProduct(Product p)
{
_List.Add(p);
}
}
And my view form looks like this:
<form action="" method="post>
#Html.AntiForgeryToken()
<input type="hidden" name="id" value="#Model.ID" />
<input type="submit" name="toevoegen" value="Toevoegen" />
</form>
Now I'm trying to display the session on the same page, I'll move it to /shopping_cart once I know it works, but I'm trying to display it like this:
#Session["winkelkar"]
I've looked through many other solutions here, but nothing really seems to work.
EDIT
As pointed out below, I didn't have an action in my form. I changed it to:
<form action="#Url.Action("AddToCart", "Producten")" method="POST">
...
</form>
Getting one step closer.

You used the name "winkelkar" as your session key everywhere, expect on this line:
if(Session["kar"] == null)
That means you will never find your object, and always recreate a new one.
For this reason, you should avoid using a string literal for session keys, and declare a constant instead.
And as mason pointed out in his comment, your form has an empty action attribute. You should have something like that:
<form action="#Url.Action("AddToCart", "Products")" method="post">
....
</form>

Related

How to POST from one Page Handler to another?

I am working on a ASP.NET Core 2.0 project using Razor Pages (not MVC).
I have the following flow:
User fills out form and POSTs
This page's POST handler validates the info and returns Page() if there are issues. If not, handler saves data to database.
From here, I want the handler to POST to a different page's POST handler with the validated and saved data from Step 2.
How do I POST to another page from within a page handler? Is this the appropriate way to do this kind of thing? The reason I don't want to RedirectToPage() is because I don't want the final page in the sequence to be navigable via GET. The final page should not be accessible via a direct link but should only return on a POST.
I thought about validating/saving the data and setting a boolean "IsValid" and returning the page, checking for that IsValid, and immediately POSTing to the final page via JS. However this feels dirty.
Set the "asp-page" property of the form to your other page. Then set values in the standard way.
<form method="post" asp-page="/pathto/otherpage">
Select Example:<select name="DataForOtherPage">
Then in your controller, bind the value...
[BindProperty]
public string DataForOtherPage { get; set; }
You don't need to cross post!
If possible, you should avoid the cross-post. Do it all under the original action. The action can return a different view by specifying the view name in the View call.
If the target of the cross-post contains complicated logic that you don't want to duplicate, extract it to a common library, and call it from both actions.
For example, instead of
ActionResult Action1()
{
if (canHandleItMyself)
{
return View("View1");
}
else
{
return //Something that posts to action2
}
}
ActionResult Action2()
{
DoSomethingComplicated1();
DoSomethingComplicated2();
DoSomethingComplicated3();
DoSomethingComplicated4();
return View("View2");
}
Do something like this:
class CommonLibrary
{
static public void DoSomethingComplicated()
{
DoSomethingComplicated1();
DoSomethingComplicated2();
DoSomethingComplicated3();
DoSomethingComplicated4();
}
}
ActionResult Action1()
{
if (canHandleItMyself)
{
return View("View1");
}
else
{
CommonLibrary.DoSomethingComplicated();
return View("View2");
}
}
ActionResult Action2()
{
CommonLibrary.DoSomethingComplicated();
return View("View2");
}
If you really want to cross-post
If you insist on using a cross-post, you will have to render a page that does the post, e.g.
<HTML>
<BODY>
<IMG Src="/Images/Spinner.gif"> <!-- so the user doesn't just see a blank page -->
<FORM name="MyForm" Action="Action2" Method="Post">
<INPUT type="hidden" Name="Argument1" Value="Foo">
<INPUT type="hidden" Name="Argument2" Value="Bar">
</FORM>
<SCRIPT type="text/javascript>
document.getElementById("MyForm").submit(); //Automatically submit
</SCRIPT>
</BODY>
</HTML>

Populate a viewmodel with a newly added database ID

This is a follow up to a question that was asked yesterday.
I have a viewmodel, which shows a list of objectives. Using jquery I can add a new objectives line to the screen (the ID is set to 0 for any new objectives listed). When I click on the Save button to Post the objective list back to the controller, the controller loops through the objective list, and checks the ID against the database. If the ID is NOT found, it creates a new objective, adds this to the DB context, and saves te changes. It then retreives the ID, and returns the View(model) to the View.
The problem is, although the ID in the model, is updated to the database ID - when the model is rendered in the View again, it's ID is still 0. So if I click Save again, it again, re-adds the "new objective added previously" to the database again.
My controller is shown below:
//
// POST: /Objective/Edit/model
[HttpPost]
public ActionResult Edit(ObjectivesEdit model)
{
if (model.Objectives != null)
{
foreach (var item in model.Objectives)
{
// find the database row
Objective objective = db.objectives.Find(item.ID);
if (objective != null) // if database row is found...
{
objective.objective = item.objective;
objective.score = item.score;
objective.possscore = item.possscore;
objective.comments = item.comments;
db.SaveChanges();
}
else // database row not found, so create a new objective
{
Objective obj = new Objective();
obj.comments=item.comments;
obj.objective = item.objective;
obj.possscore = item.possscore;
obj.score = item.score;
db.objectives.Add(obj);
db.SaveChanges();
// now get the newly created ID
item.ID = obj.ID;
}
}
}
return View(model);
}
My ID is being set in the controller:
EDIT: Another example here, showing model.Objectives1.ID being updated:
However when the view renders it, it reverts to 0:
The Objectives list is determined as follows:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace MvcObjectives2.Models
{
public class ObjectivesEdit
{
public IEnumerable<Objective> Objectives { set; get; }
public ObjectivesEdit()
{
if (Objectives == null)
Objectives = new List<Objective>();
}
}
}
The View has:
#model MvcObjectives2.Models.ObjectivesEdit
#using (Html.BeginForm())
{
#Html.EditorFor(x=>x.Objectives)
<button type="submit" class="btn btn-primary"><i class="icon-ok icon-white"></i> Save</button>
}
and in my EditorTemplate (objective.cshtml):
#model MvcObjectives2.Models.Objective
<div class="objec">
<div>
#Html.TextBoxFor(x => x.objective})
</div>
<div>
#Html.TextBoxFor(x => x.score})
</div>
<div>
#Html.TextBoxFor(x => x.possscore})
</div>
<div>
#Html.TextBoxFor(x => x.comments})
#Html.HiddenFor(x => x.ID) // This is the ID where it should now show the new ID from the database, but shows 0
</div>
</div>
I suspect the issue is somewhere in my controller - but I would appreciate any advise on how to get my View to render the new ID of the added objective.
After rewording my search, I came across several posts which say this is by design. A Posted form expects to display what it sent to the controller, if the same page is shown again.
However, you can add this, which will flush ModelState, and apparantly show the updated values from the model, updated in the controller:
ModelState.Clear();
return View(model);
I'm not certain if this has any other effect yet - but for now, it appears to work ok.
Thanks, Mark
The Html.HiddenFor has bitten me before in a similar scenario. The problem is when using this Html helper the hidden value is not updated on the re-post.
If you post something from the form and change it inside your controller, when you re-render the page using it will use the value which was originally posted to the action.
Instead use
<input type="hidden" name="ID" id="ID" value="#Html.Encode(Model.ID)" />

Asp.net MVC cancel button with multiple redirect choices

I currently have a form with a submit and cancel button. Depending on some logic, each time the page loads, I want that same cancel button to redirect to different other pages in the application. This is the code I have at the moment in my aspx view that changes the location.href based on my property
<% if (Model.MyProperty.Equals("something"))
{ %>
<input class="btnCancel" type="button" value="" onclick="location.href='<%: Url.Action("MyAction","MyController", new {Area="MyArea"},null)%>'" />
<% } %>
<% else if (Model.MyProperty.Equals("somethingelse"))
{ %>
<input class="btnCancel" type="button" value="" onclick="location.href='<%: Url.Action("MyOtherAction","MyOtherController", new {Area="SomeOtherArea"},null)%>'" />
<% } %>
Is this the correct and elegant way to do this? I would rather reduce the multiple IF-ELSE conditions if there was a way to do it.
Thanks for your time.
The way I've always handled multiple redirect options is by setting the href value in the controller action.
The View is generic, but the controller action is specific to the context of the page your rendering. So in your model, make a property called CancelUrl. Now, in the controller action, set it to the link you want it to go to.
model.CancelUrl = Url.Action("action", "controller");
This way, all you have to do in your View is say
Text
You can create a cancel method that takes your property as a parameter and redirect appropriately within the controller. This logic should probably not be in your view anyway as views should have almost 0 logic anyway
I would put the property that will be used to decide the cancel action in the view model (as you already have), alongside any other required properties.
For example:
public class IndexModel
{
//any other properties you need to define here
public string MyProperty { get; set; }
}
Then your view would look similar to:
#model IndexModel
#using (Html.BeginForm())
{
//other information you may want to submit would go here and in the model.
#Html.HiddenFor(m => m.MyProperty)
<button type="submit" name="submit" value="submit">submit</button>
<button type="submit" name="cancel" value="cancel">cancel</button>
}
And finally, your post action should decide the next action that should be returned:
[HttpPost]
public ActionResult Index(IndexModel model)
{
if (!string.IsNullOrEmpty(Request["submit"]))
{
if (ModelState.IsValid)
{
//any processing of the model here
return RedirectToAction("TheNextAction");
}
return View();
}
if (model.MyProperty.Equals("something"))
{
return RedirectToAction("MyAction", "MyController", new { area = "MyArea" });
}
else //assumes the only other option is "somethingelse"
{
return RedirectToAction("MyOtherAction", "MyOtherController", new { area = "SomeOtherArea" });
}
}

Asp.Net MVC Return to page on error with fields populated

I am starting a new project in Asp.net MVC 2.
I have been mostly a webforms developer and have limited exposure to Asp.Net MVC and hence this is probably a noob question.
My situation is as follows:
I have a create page for saving some data to the DB.
The view for this page is not strongly bound / typed - so the way I am extracting the data from the view is by looking at the POST parameters.
Incase there is an error (data validation, etc), I need to send the user back to the previous page with everything filled in the way it was and displaying the message.
On webforms, this got handled automatically due to the view state - but how can I go about doing the same here?
A code example can be as follows:
View:
<% using (Html.BeginForm("Create", "Question", FormMethod.Post)) { %>
<div>
Title: <%: Html.TextBox("Title", "", new { #style="width:700px" })%>
</div>
<div>
Question: <%: Html.TextBox("Question", "", new { #style="width:700px" })%>
</div>
<input type="submit" value="Submit" />
<% } %>
Controller:
[HttpPost]
[ValidateInput(false)]
public ActionResult Create() {
Question q = new Question();
q.Title = Request.Form["Title"];
q.Text = Request.Form["Question"];
if(q.Save()) {
return RedirectToAction("Details", new { id = q.Id });
}
else {
// Need to send back to Create page with data filled in
// Help needed here
}
}
Thanks.
You could simply return the View in case of error. This will preserve the context.
[HttpPost]
[ValidateInput(false)]
public ActionResult Create(Question q) {
if(q.Save()) {
return RedirectToAction("Details", new { id = q.Id });
}
else {
// Need to send back to Create page with data filled in
// Help needed here
return View();
// If the view is located on some other controller you could
// specify its location:
// return View("~/Views/Question/Create.aspx");
}
}
Also I would recommend you to use strongly typed views along with the strongly typed helpers. Notice how I used a Question object directly as action parameter. This is equivalent to the code you have written in which you were manually extracting and building this object. The model binder does this job automatically for you.

Retrieving data from Html.DropDownList() in controller (ASP MVC) | string returned?

I have the following problem:
I have a form in site/banen (currently local running webserver) which is using a SQL database. The link is made using ADO.net and is instantiated in the controller in the following way:
DBModelEntities _entities;
_entities = new DBModelEntities(); // this part is in the constructor of the controller.
Next, I use this database to fill a Html.DropDownList() in my view. This is done in two steps. At the controller side we have in the constructor:
ViewData["EducationLevels"] = this.GetAllEducationLevels();
and a helper method:
public SelectList GetAllEducationLevels()
{
List<EducationLevels> lstEducationLevels = _entities.EducationLevels.ToList();
SelectList slist = new SelectList(lstEducationLevels, "ID", "Name");
return slist;
}
In the view I have the following:
<% using (Html.BeginForm()) {%>
<fieldset>
<legend>Fields</legend>
<!-- various textfields here -->
<p>
<label for="EducationLevels">EducationLevels:</label>
<!-- <%= Html.DropDownList("EducationLevels", ViewData["EducationLevels"] as SelectList)%> -->
<%= Html.DropDownList("EducationLevels", "..select option..")%>
</p>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
<% } %>
Now, the form is rendered correctly when I browse to the create page. I can select etc. But when selected I have to use that value to save in my new model to upload to the database. This is where it goes wrong. I have the following code to do this in my controller:
//
// POST: /Banen/Create
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(FormCollection form)
{
// set rest of information which has to be set automatically
var vacatureToAdd = new Vacatures();
//vacatureToAdd.EducationLevels = form["EducationLevels"];
// Deserialize (Include white list!)
TryUpdateModel(vacatureToAdd);
// Validate
if (String.IsNullOrEmpty(vacatureToAdd.Title))
ModelState.AddModelError("Title", "Title is required!");
if (String.IsNullOrEmpty(vacatureToAdd.Content))
ModelState.AddModelError("Content", "Content is required!");
// Update the variables not set in the form
vacatureToAdd.CreatedAt = DateTime.Now; // Just created.
vacatureToAdd.UpdatedAt = DateTime.Now; // Just created, so also modified now.
vacatureToAdd.ViewCount = 0; // We have just created it, so no views
vacatureToAdd.ID = GetGuid(); // Generate uniqueidentifier
try
{
// TODO: Add insert logic here
_entities.AddToVacatures(vacatureToAdd);
_entities.SaveChanges();
// Return to listing page if succesful
return RedirectToAction("Index");
}
catch (Exception e)
{
return View();
}
}
#endregion
It gives the error:
alt text http://www.bastijn.nl/zooi/error_dropdown.png
I have found various topics on this but all say you can retrieve by just using:
vacatureToAdd.EducationLevels = form["EducationLevels"];
Though this returns a string for me. Since I'm new to ASP.net I think I am forgetting to tell to select the object to return and not a string. Maybe this is the selectedValue in the part where I make my SelectList but I can't figure out how to set this correctly. Of course I can also be complete on a sidetrack.
Sidenote: currently I'm thinking about having a seperate model like here.
Any help is appreciated.
You can't return an object from usual <SELECT> tag wich is rendered by Html.DropDownList() method, but only string variable could be returned. In your case ID of EducationLevels object will be send to the server. You should define and use one more custom helper method to reconstruct this object by ID.

Resources