I have the following action methods:
public ActionResult ProfileSettings()
{
Context con = new Context();
ProfileSettingsViewModel model = new ProfileSettingsViewModel();
model.Cities = con.Cities.ToList();
model.Countries = con.Countries.ToList();
model.UserProfile = con.Users.Find(Membership.GetUser().ProviderUserKey);
return View(model); // Here model is full with all needed data
}
[HttpPost]
public ActionResult ProfileSettings(ProfileSettingsViewModel model)
{
// Passed model is not good
Context con = new Context();
con.Entry(model.UserProfile).State = EntityState.Modified;
con.SaveChanges();
return RedirectToAction("Index", "Home");
}
#using (Html.BeginForm("ProfileSettings", "User", FormMethod.Post, new { id = "submitProfile" }))
{
<li>
<label>
First Name</label>
#Html.TextBoxFor(a => a.UserProfile.FirstName)
</li>
<li>
<label>
Last Name</label>
#Html.TextBoxFor(a => a.UserProfile.LastName)
</li>
...
<input type="submit" value="Save" />
...
When I hit submit received model in POST method is incomplete. It contains FirstName, LastName etc. But UserID is null. So I can't update object. What am I doing wrong here?
MVC reconstructs your model only based on what's coming in the request. In your particular case, you are only submitting the FirstName and the LastName, because those are the only #Html.TextBoxFor() calls included in your View. MVC models don't behave like ViewState, it isn't stored anywhere.
You also don't want to include your entire Entity in your view-model. If all you need is the ID then that should be all you include. Then you'd load your entity again from your DAL, update the properties that need to be altered, and then save your changes.
You should store the UserId as a hidden field in the form.
Add a html tag HiddenFor in your view, and make sure that you are populating UserId in your Get action :
#using (Html.BeginForm("ProfileSettings", "User", FormMethod.Post, new { id = "submitProfile" }))
{
#Html.HiddenFor(a => a.UserProfile.UserId)
// your code here..
}
Related
I am taking an ASP.NET course an Udemy. Unfortunately, it's an old course, and I don't believe to get an answer there.
Now, what exactly is going on.
At this stage of the course, I need to work with Customers. The part that should show the list of customers, or the details of a specific customer, are working fine.
However, when I am trying to add a new customer to the database, the app crashes.
The full quote of the error:
The parameters dictionary contains a null entry for parameter 'id' of non-nullable type 'System.Int32' for method 'System.Web.Mvc.ActionResult CustomerDetails(Int32)' in 'VidlyExercise1.Controllers.CustomersController'. An optional parameter must be a reference type, a nullable type, or be declared as an optional parameter.
Parameter name: parameters
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.
I've trying to copy the exact code from lessons, but still something doesn't match.
The Customers Controller code:
public class CustomersController : Controller
{
private ApplicationDbContext _context;
public CustomersController()
{
_context = new ApplicationDbContext();
}
protected override void Dispose(bool Disposing)
{
_context.Dispose();
}
// GET: Customers
[Route("customers")]
public ActionResult CustomersList()
{
var customers = _context.Customers.Include(c => c.MembershipType).ToList();
var viewModel = new CustomersIndexViewModel
{
Customers = customers
};
return View(viewModel);
}
[Route("customers/{id}")]
public ActionResult CustomerDetails(int id)
{
var customer = _context.Customers.Include(m => m.MembershipType)
.SingleOrDefault(c => c.Id == id); //Eager loading
var viewModel = new CustomerDetailsViewModel
{
Name = customer.Name,
MembershipType = customer.MembershipType,
Birthdate = customer.Birthdate
};
return View(viewModel);
}
[Route("customers/new")]
public ActionResult New()
{
var membershipTypes = _context.MembershipTypes.ToList();
var viewModel = new NewCustomerViewModel()
{
MembershipTypes = membershipTypes,
Customer = new Customer()
};
return View("New", viewModel);
}
[HttpPost]
public ActionResult Create(Customer customer)
{
_context.Customers.Add(customer);
_context.SaveChanges();
return RedirectToAction("CustomersList", "Customers");
}
Now, when I click the button just to enter the View for adding a new Customer, it opens up fine.
But when I try to click the "Save" button, I get an error I posted above.
I even tried, changing the code in "Create" method, even just to post 404.
return HttpNotFound();
So, as I understand the Create method doesn't even get to the part of doing anything, it just crashes.
Here's the View code:
#model VidlyExercise1.ViewModels.NewCustomerViewModel
#{
ViewBag.Title = "New";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>New Customer</h2>
#using (Html.BeginForm("Create", "Customers", FormMethod.Get))
{
<div class="form-group">
#Html.LabelFor(m => m.Customer.Name)
#Html.TextBoxFor(m => m.Customer.Name, new { #class = "form-control" })
</div>
<div class="form-group">
#Html.LabelFor(m => m.Customer.Birthdate)
#Html.TextBoxFor(m => m.Customer.Birthdate, new { #class = "form-control" })
</div>
<div class="form-group">
<label>
#Html.CheckBoxFor(m => m.Customer.IsSubscribedToNewsLetter) Subscribed to newsletter
</label>
</div>
<div class="form-group">
#Html.LabelFor(m => m.Customer.MembershipTypeId)
#Html.DropDownListFor(m => m.Customer.MembershipTypeId,
new SelectList(Model.MembershipTypes, "Id", "Name"),
"Select Membership Types", new { #class = "form-control" })
</div>
#Html.HiddenFor(m=>m.Customer.Id)
<button type="submit" class="btn btn-primary">Save</button>
}
I think the example code from mentor on GitHub is almost identical. The View html is different, because it also includes further lessons, but the Controller code seems to be correct.
I know the there is a need to post something I already tried. I did google for potential fixes, and did try some of them, but the problem is, I don't know what I am looking for.
Some examples of what I found in similar questions, but which didn't help:
#using (Html.BeginForm("Search", "Person",FormMethod.Get))
Don't use a variable "Id" in ActionResult CustomerList: I never used one
Maybe something else I don't remember now.
One more thing: When I click the button, the path shows: "localhost\Customers\Create"
There is no View suited for this, and I am not sure that it's what supposed to happen.
Can you please help to find what's wrong? Again, it's hard to find an error myself, since I only recently started learning ASP.NET (with the said course) and new to it.
And one more question: The "create" button should get a "Customer" from somewhere, but where in the View code I actually "send" it?
Thank you in advance,
Evgenie
I apologize for trouble, I seems I found what was the problem.
Upon clicking "Save" button, the route was trying to redirect to "customer/save".
And there was a line:
public ActionResult CustomerDetails(int)
So, the program was trying to open route "customer/save" as if the word "save" was an Id!
So, instead of even getting to the code in the ActionResult Save, it tried to find a customer with non-existing Id="Save".
To fix that, all I needed is to change the line declaring the ActionResult CustomerDetails, that it will only accept integers as an Id's.
Like this:
[Route("customers/{id:int}")]
public ActionResult CustomerDetails(int id)
I didn't have to do any changes to "Save" method in CustomersController, or to the relevant View.
This is my View:
#model test2.Models.ChatModel
#{
ViewBag.Title = "Channel";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<center>
<h2>Channel: #Model.channelName</h2>
#{
foreach (string line in Model.chatLog) {
<div>#line</div>
}
}
<br />
#using (Html.BeginForm("sendMessage", "Home", FormMethod.Post)) {
#Html.TextBoxFor(model => model.message)
<button type="submit"> Send Message </button>
}
</center>
Here is my Controller:
public ActionResult sendMessage(ChatModel model) {
//send message somewhere
//this is not working
return RedirectToAction("Channel", "Home", new { channel = model.channelName });
//this is working
return RedirectToAction("Channel", "Home", new { channel = "test" });
}
The error happens in the redirectToAction method. Somehow "model.channelName" is empty, but #Model.channelName in my view is correctly displaying the channel name.
It looks like when you send a Model to a view, and "resend" this model back to a controller, the informations are lost.
Is there an easy way to solve this?
PS Step by step:
Model gets channelName
Model is send to view
View correctly displays data from model
adding message to Model
sending model to controller
model does NOT contain information from step 1
You need to include model.channelName in the form. Try adding a:
#Html.HiddenFor(model => model.channelName)
Anything not posted by the form, will be null in your model (including your chatlog)
Actually the values model properties should be rendered as input elements within the form that is posted back to controller action. The properties which are not included would loose their values.
What you can do is create a hidden field for those to post :
#using (Html.BeginForm("sendMessage", "Home", FormMethod.Post)) {
#Html.TextBoxFor(model => model.message)
#Html.HiddenFor(model => model.channelName)
<button type="submit"> Send Message </button>
}
You would need to add same way other properties too that are posting null at action and you need those for some processing.
Hope it helps.
I am getting the following error when trying to select an item from my drop down list and submit this.
An exception of type 'System.InvalidOperationException' occurred in
System.Web.Mvc.dll but was not handled in user code Additional
information: There is no ViewData item of type
'IEnumerable' that has the key 'RoleName'.
If anyone can help me figure out how to fix this error I would really appreciate it as I haven't been able to fix it yet and been stuck for quite a long time and google hasn't provided a solution yet!
This is my controller code
[AllowAnonymous]
public ActionResult Index()
{
var roles = context.Roles.ToList();
return View(roles);
}
[Authorize(Roles = "canEdit")]
public ActionResult ManageUserRoles()
{
var list = context.
Roles.OrderBy(r => r.Name).ToList().Select(rr => new SelectListItem { Value = rr.Name.ToString(), Text = rr.Name }).ToList();
ViewBag.Roles = list;
return View();
}
public ActionResult RoleAddToUser(string UserName, string RoleName)
{
ApplicationUser user = context.Users.FirstOrDefault(u => u.UserName.Equals(UserName, StringComparison.CurrentCultureIgnoreCase));
if (user != null)
{
UserManager.AddToRole(user.Id, RoleName);
}
return View("ManageUserRoles");
}
This is my ManageUserRoles View
#{
ViewBag.Title = "ManageUserRoles";
}
<h2>Manage User Roles</h2>
#Html.ActionLink("Create New Role", "Create") | #Html.ActionLink("View User Roles", "Index")
<hr />
<h2>Role Add to User</h2>
#using (Html.BeginForm("RoleAddToUser", "Roles"))
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<p>
User Name : #Html.TextBox("UserName")
Role Name: #Html.DropDownList("RoleName", (IEnumerable<SelectListItem>) ViewBag.Roles, "Select ...")
</p>
<input type="submit" value="Save" />
}
<hr />
Problem is (I assume) that you are visiting View("ManageUserRoles") from your ActionResult RoleAddToUser and not from ManageUserRoles. So ViewBag does not exists.
return View("ManageUserRoles");
starts to render view. However on the view you have dropdown which wants ViewBag.Roles ... but ... where do you set data into it? This exception is thrown because ViewBag.Roles just not exist.
If you want run ActionResult ManageUserRoles before returning the view, you have to call redirect.
return RedirectToAction("ManageUserRoles");
If you want render view without execute code in that method, you have to create ViewBag.Roles in RoleAddToUser. Please, note that ViewBag data are deleted after every request.
Verify that your model (whether its a domain model or some other viewmodel) has a field of string type called "RoleName", this needs to match the first argument of your DropDownList call so the page knows what variable to post the data back in on form submit.
What I want to do
I am very new to MVC.
I'm trying to create a page that allows users to perform the following actions on the same page:
View the list (table)
Add a new item (Filling the form and clicking the Add button should update the table)
Delete an item from the list (Clicking the Delete button in a row should update the table)
A simple example looks like this but I actually have two lists on one page (Fees and Costs):
Question
What would be the best way to achieve this?
Should I go with Dylan Beattie's method posted here which would look something like this?
public ActionResult MyAction(string submitButton, MyViewModel form)
{
switch (submitButton)
{
case "AddFee":
return (AddFee(form));
case "AddCost":
return (AddCost(form));
case "RemoveFee":
return (RemoveFee(form));
case "RemoveCost":
return (RemoveCost(form));
}
}
public ActionResult AddFee(MyViewModel form)
{
Fee newFee = ....; // Get entered data from `form`
_repository.InsertFee(newFee);
return View("Create"); //Back to the original page
}
Or is there any other recommended methods to handle this such as using JavaScript?
You could create the table as a partial view and re render this via ajax.
Wrap the partial view in a div and Wrap the form in #using (Ajax.BeginForm(.... and target the wrapper div. Your controller action that is targeted by the ajax request will need to return a partial view.
Here is a simple example
public class HomeController : Controller
{
public ActionResult Index()
{
MYvm vm = new MYvm() { id = 1, name = "This is my View Model" };
return View(vm);
}
public ActionResult DA(MYvm vm)
{
vm.name = "CHANGED";
return PartialView("Part", vm);
}
View:
#model MvcApplication1.Controllers.HomeController.MYvm
#{
ViewBag.Title = "Home Page";
}
#using (Ajax.BeginForm("DA", "Home", new AjaxOptions() { UpdateTargetId = "cont", HttpMethod = "Get" }))
{
<div>
Id: #Html.EditorFor(model => model.id)
</div>
<div>
Name: #Html.EditorFor(model => model.name)
</div>
<input type="submit" value="SubmitForm" />
}
<div id="cont">
#{Html.RenderPartial("part", Model);}
</div>
Partial View
#model MvcApplication1.Controllers.HomeController.MYvm
#{
ViewBag.Title = "part";
}
<h2>part</h2>
#Model.name
Should I go with [previous SO answer]
No. That answer was for a different scenario where the question had a form with two submit buttons that wanted to do two different actions (and wasn't even the accepted answer to that question).
Your sample screenshot indicates that some javascript/jquery and ajax would solve the issue cleanly.
As you're new to MVC, try to keep it relatively simple. Break up the page into separate parts:
the containing page
the edit form
the list with remove
the edit/list work independently and should be written in a way that they could be put on any other page - the page is just there to contain them and doesn't do much else (obviously your real page will contain more, but add those parts as separate components as well).
1 Create actions for your list and edit forms that return partialviews - just the parts that are needed for that view (self-contained)
controller:
[HttpGet]
public ActionResult AddCost()
{
var model = new Cost();
return PartialView(model);
}
[HttpPost]
public void AddCost(Cost model)
{
if (ModelState.IsValid) {
db.SaveCost(model);...
}
}
form Views/Home/AddCost.cshtml:
#using (Ajax.BeginForm(...
{
<div class='editor-label'>#Html.LabelFor(model=>model.Description)</div>
...etc...
}
I'll leave you to set the Ajax.BeginForm properties. But make sure the on-success calls reloadCostList() (see below)
controller
public ActionResult CostList()
{
var model = db.loadCosts(); ...
return PartialView(model);
}
list, Views/Home/CostList.cshtml
#model IEnumerable<ViewModels.Cost>
<table>
<thead>
<tr>
<th>Cost Description</th>
...
<tbody>
#foreach (var cost in Model.Costs)
{
<tr data-id='#cost.Id'>
<td>#Html.DisplayFor(x=>cost.Description)</td>
...
<td><a href='#' class='remove-button'>Remove</a></td>
}
...
2 Create an action + view for the main page with placeholder for the form and calls the list partial-action, eg:
<div id="body">
<div id="formWrapper">
#Html.Action("AddCost")
</div>
<div id="listWrapper">
#Html.Action("ListView")
</div>
</div>
if you already load the data for the page, you can pass it directly to the partial, but there's no need:
#Html.Partial("ListView", Model.Costs)
this allows you to refresh the list via an ajax call, something like:
function reloadCostList() {
$(".listWrapper").load("Home/CostList");
}
(ideally, $.ajax and add some fancy UI to indicate loading)
3 Add a remove action to your controller
[HttpPost]
public void RemoveCost(int id)
{
}
4 Wire up the Remove link
$(function() {
$(".remove-button").click(function() {
var id = $(this).closest("tr").attr("id");
$.post("/Home/RemoveCost/" + id, null, function() {
$(".listWrapper").load("Home/CostList");
// or reloadCostList(); from above
// or:
//$(".listWrapper tr[id=" + id + "]").hide();
});
});
}
rather than re-load the entire list, you could just remove the row (add some fancy UI like fade-out...)
I have a page which represents data from ICollection<> model, it generates #Html.BeginForm() for each item in ICollection<> and shows data with #Html.Labels, and I want to create a link from each form to item details, so it will be like, when I press form with item with id=4, it sends Model.ElementAt(4) as a model to new page, and display it. How can I do that?
EDIT: I guess I need to add something like #Html.ActionLink("DetailsPage","Shops",shop)
#using WebShops
#model ICollection<WebShops.Shop>
#foreach (Shop shop in Model)
{
using (Html.BeginForm())
{
#Html.Label(shop.name)
#Html.Display(shop.name)
<br />
#Html.Label(shop.image)
#Html.Display(shop.image)
<hr />
}
}
To display a specific item there is no need for Html.BeginForm because it makes a POST request and you need to make a GET request.
You need to create a new Action that will make use of GET request.
public ActionResult Shop(string id)
{
var shop = //get shop by id from database
return View(shop)
}
You call the new action like below.
#using WebShops
#model ICollection<WebShops.Shop>
#foreach (Shop shop in Model)
{
#Html.Label(shop.name)
#Html.Display(shop.name)
<br />
#Html.Label(shop.image)
#Html.Display(shop.image)
<hr />
#Html.ActionLink("Display", "Shop","ControllerName", new {id = shop.id})
}
You can do that using the object routeValues from this overload of Html.ActionLink:
#Html.ActionLink("DetailsPage","Shops", new { id = shop.ID })
This doesn't "send the model to the new page", it makes a link to Shops/DetailsPage/4, causing GET request when clicked.
So in the DetailsPage action method you'll have to look up the shop on ID again in order to display it.