I've been struggling with this for a while now. I'm constructing this view:
But when I hit the 'Update' button after do some changes the web refreshes to show the original values.
About the view: I get this view using an IEnumerable and looping thru each item in the model inside a form. Then, inside the form, there is a table that contains only 1 row. I do this in order to wrap all the items of the record in one form. This is part of code:
#foreach (var item in Model)
{
<form asp-action="Test" asp-route-id="#item.Id">
<table class="table">
<tbody>
<tr>
<td>
<input type="hidden" asp-for="#item.Id" />
<div class="form-group">
<div class="col-md-10">
<input asp-for="#item.MchName" readonly class="form-control" />
<span asp-validation-for="#item.MchName" class="text-danger"></span>
</div>
</div>
</td>
//more fields
<td>
<input type="submit" value="Update" class="btn btn-default" />
</td>
</tr>
</tbody>
</table>
</form>}
I declare an asp-action and a asp-route-id:
<form asp-action="Test" asp-route-id="#item.Id">
Question: Is this good enough? Is there something missing?
This is the Get Method:
public async Task<IActionResult> Test()
{
PopulateMachineTypeDropDownListStore();
return View(await _context.Machines.AsNoTracking().ToListAsync());
}
Question: I'm not passing any argument to the controller, yet the view will list the items following the given structure using an IEnumerable. Should I pass anything to the Get Method or is it fine as it is?
This is the Post Method:
#model IEnumerable<Application.Models.Machine>
[HttpPost, ActionName("Test")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> TestPost(int? id)
{
if (id == null)
{
return NotFound();
}
var machinetoUpdate = await _context.Machines
.SingleOrDefaultAsync(s => s.Id == id);
if (await TryUpdateModelAsync(
machinetoUpdate,
"",
s => s.MchName, s => s.StoreID, s => s.PUnit, s => s.Status))
{
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateException)
{
ModelState.AddModelError("", "Unable to save changes. " +
"Try again, and if the problem persists, " +
"see your system administrator.");
}
return RedirectToAction("Test");
}
PopulateMachineTypeDropDownListStore();
return View(await _context.Machines.AsNoTracking().ToListAsync());
}
Question: I don't know if because the entity I retrieve the id from (and that I use to update the model thru TryUpdateModelAsync()) is also being used to compare to the model that thru the view this might not been working properly.
Thanks in advance for any help.
Related
What is want to achieve is I have a form to adds rows with data to a html table, it's like a temporary table and all the data from it will be added in just one submit button. How can I possibly do this?
This is my sample table structure, data from it must be added to db
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<table class="table">
<thead>
<tr>
<!--some other fields-->
<th>Amount</th>
<th>Check #</th>
<th>Check Date</th>
</tr>
</thead>
<tbody>
<tr>
<!--some other fields-->
<td>
<input asp-for="Amount" class="form-control" />
<span asp-validation-for="Amount" class="text-danger"></span>
</td>
<td>
<input asp-for="Check_No" class="form-control" />
<span asp-validation-for="Check_No" class="text-danger"></span>
</td>
<td>
<input asp-for="Check_Date" class="form-control" />
<span asp-validation-for="Check_Date" class="text-danger"></span>
</td>
</tr>
<!--row 2-->
<!--row 3-->
<!--row 4-->
<!--etc..-->
</tbody>
</table>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-default" />
</div>
And here is my controller code so far, i don't know what do I need to change
// POST: Books/Create
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Id,Year,Month,Document_Code,GLA_Code,Expense_Code,Function_Code,Document_Reference_No,Document_Date,Quantity,Amount,Check_No,Check_Date,Remarks,Encoder")] Book book)
{
if (ModelState.IsValid)
{
_context.Add(book);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(book);
}
What must needed to be change in my view and controller, and help will is appreciated.
Thank you.
There's a couple of issues here.
First, if you've got a table of "books" (plural), the input names need to be indexed. You then also need to accept something like List<Book> instead of Book as the param to your action method. It's a little hard to tell with just the code provided, but I'd imagine you're repeating these inputs, all with the same names for each row. If that's the case, only the values for the last item will be posted. Making it a list of items enables you to post them all.
Simplistically, that means your inputs need to have names like [0].Amount, which Razor will generate for you if you use a for loop and render the inputs like:
<input asp-for="#Model[i].Amount" class="form-control" />
If you're adding the additional rows (and contained inputs) via JavaScript, you'll need to ensure that you're generating these indexed names properly yourself. A JS templating library may help in this regard.
Second, do not use Bind. Just don't. It's awful, horrible, and kills both puppies and kittens. For more explanation see my post, Bind is Evil. Use a view model instead. As a general rule you should never post an entity class. Your entity classes serve the database and its concerns, which are almost always different from the concerns of the view. Additionally, you should never just blindly save something posted by a user. Even if you insist on using your entity class to bind to, you can improve the security and safety of your code exponentially by literally mapping the values from the posted version of the class over to a new instance you create. Then, you know exactly what is being persisted to the database (without the godforsaken Bind) and you also have the opportunity to sanitize input as necessary.
I was facing a similar problem, but using ASP.NET Core 3.1 and Razor Pages. I was looking for a way to add and remove rows from a table with JavaScript, and then post it. My problem was to post the table without Ajax. Based in the question and in the accepted answer, I could do that.
Here is Index.cshtml.cs:
public class IndexViewModel {
public string Name { get; set; }
public IList<ResourceViewModel> Resources { get; set; }
}
public class ResourceViewModel {
public string Key { get; set; }
public string Value { get; set; }
}
public class IndexModel: PageModel {
[BindProperty]
public IndexViewModel ViewModel {get; set; }
public void OnGet() {
// You can fill your ViewModel property here.
}
public void OnPost() {
// You can read your posted ViewModel property here.
}
}
Here is Index.cshtml:
#page
#model IndexModel
#{
ViewData["Title"] = "Index";
}
<form method="post">
<div class="form-group">
<label asp-for="ViewModel.Name"></label>
<input asp-for="ViewModel.Name" class="form-control" />
</div>
<div class="form-group">
<table class="table">
<thead>
<th>Key</th>
<th>Value</th>
</thead>
<tbody>
#for(int i = 0; i < Model.ViewModel.Resources.Count; i++) {
<tr>
<td>
<input asp-for="ViewModel.Resources[i].Key" type="hidden" />
#Model.ViewModel.Resources[i].Key
</td>
<td>
<input asp-for="ViewModel.Resources[i].Value" type="hidden" />
#Model.ViewModel.Resources[i].Value
</td>
</tr>
}
</tbody>
</table>
</div>
<button type="submit" class="btn btn-primary">Send</button>
</form>
Notice I've used type="hidden" because I didn't want the user to edit the table directly.
I hope you find this useful!
I'm working with an MVC5 project, I have created a simple system that the user can upload a file "CV" for each Employee.
Now all thing work for me fine except "DELETING File".
I need to add action method for deleting uploaded file and the ability to replace it with another file.
in the model class I have created two property HttpPostedFileBase CV to save the uploaded file
and String cvName, to save the file name and use it to create a link to that file.
In the controller that what I have done:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult DeleteCV(string cvName)
{
//Session["DeleteSuccess"] = "No";
var CVName = "";
CVName = cvName;
string fullPath = Request.MapPath("~/Content/CVs/" + CVName);
if (System.IO.File.Exists(fullPath))
{
System.IO.File.Delete(fullPath);
//Session["DeleteSuccess"] = "Yes";
}
return RedirectToAction("Index");
}
and this is the view:
<div class="form-group">
#Html.LabelFor(model => model.CV, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#{
if (File.Exists(Server.MapPath("~/Content/CVs/"
+ Html.DisplayFor(model => model.cvName))))
{
#Html.DisplayFor(model => model.cvName) #Html.HiddenFor(model => model.cvName)
<a href="#Url.Action("DeleteCV", new { #Model.cvName })">
<img src="#Url.Content("~/Content/Images/Delete.jpg")" width="20" height="20" class="img-rounded" />
</a>
}
else
{
<input type="file" name="file" accept="pdf" />
}
}
</div>
</div>
I can't delete the file, each time this message appears
The resource cannot be found.
Description: HTTP 404. The resource you are looking for (or one of its dependencies) could have been removed, had its name changed, or is temporarily unavailable. Please review the following URL and make sure that it is spelled correctly.
Requested URL: /DeleteCV
You're sending a GET to a POST
Change the [HttpPost] to [HttpGet]
Or use JQuery and send a DELETE verb like I mentioned in the comments
You're using an , so your link will become /Controller/DeleteCV?cvName=SomeName, which will be executed as GET. You don't want that for many reasons, and frankly, the rest of the code is a mess too. Don't do business logic (like checking for a file) in your view, and you might want to add a few checks around that File.Delete().
Do the file check in your controller, saving the result in a model variable, and create a separate form to POST to your Delete method:
if (#Model.FileExists)
{
#using(Html.BeginForm("Cv", "DeleteCV", FormMethod.Post))
{
#Html.AntiForgeryToken()
#Html.HiddenFor(m => m.cvName)
<input type="submit" value="Delete" />
}
}
else
{
#using(Html.BeginForm("Cv", "UploadCV", FormMethod.Post))
{
#Html.AntiForgeryToken()
<input type="file" name="file" accept="pdf" />
<input type="submit" value="Upload" />
}
}
I have taken over support of an MVC application, but unfortunately have pretty much zero experience or knowledge of MVC, so I apologise in advance if this is a stupid question.
We are placing every single result into a listed item in the HTML, then hiding all but one record, so you can filter through the different entries using First/Prev/Next/Last buttons, all via jQuery:
$("a#next").on("click", function () {
var nextli = $("li.first");
if ($(nextli).is(":last-child")) {
return false;
}
nextli.removeClass("first").addClass("record").next().addClass("first").removeClass("record");
});
$("a#previous").on("click", function () {
var nextli = $("li.first");
if ($(nextli).is(":first-child")) {
return false;
}
nextli.removeClass("first").addClass("record").prev().addClass("first").removeClass("record");
});
This is fine, and displays the records without any problem, however when you try to edit any record but the first, you get a "System.ArgumentNullException: Value cannot be null." error.
Here's the code in the controller for the edit function:
[HttpPost]
public ActionResult Edit(RecordsView rv)
{
if (ModelState.IsValid)
{
repository.EditRecord(rv.Records.FirstOrDefault().DogIncident);
}
return RedirectToAction("Index");
}
This is defined at the start of the cshtml file:
#using (Html.BeginForm("Edit", "Home", FormMethod.Post,new {#class="record-view"}))
And finally, here is how the HTML is generated on the view:
<li class="first" id="record-1805">
<form action="/Home/Edit" class="record-view" method="post">
<ul>
[form elements]
<li><input type="submit" style="margin: 18px 0;" value="Save"></li>
</ul>
</form>
</li>
<li class="record" id="record-1804">
<form action="/Home/Edit" class="record-view" method="post">
<ul>
[form elements]
<li><input type="submit" style="margin: 18px 0;" value="Save"></li>
</ul>
</form>
</li>
<li class="record" id="record-1803">
<form action="/Home/Edit" class="record-view" method="post">
<ul>
[form elements]
<li><input type="submit" style="margin: 18px 0;" value="Save"></li>
</ul>
</form>
</li>
Does anyone please know why I can only edit the first record that is displayed and not the others? Even if I go through the records using the next/back buttons and back to the first record to submit it, it updates fine, but updating any other record generates an error.
Referencing Darin's post: Pass a parameter to a controller using jquery ajax takes you halfway. He's using an input val in jQuery and passing it to the Controller Action.
Typically you'd provide one input link or #Html.ActionLink per record to be clicked for editing and MVC will bind to the controls parameter to your action (ID or what ever you want) just use the same name on the action link as the parameter and it will be mapped during the post.
data: { input: $('#caption').val() },
Change the signature of your controller action , or add another that takes an 'int' as shown below.
[HttpPost]
public ActionResult Edit(int ID = 0)
{
if (ModelState.IsValid)
{
repository.EditRecord(rv.Records.Where(r => r.ID == ID).DogIncident);
}
return RedirectToAction("Index");
}
Can you post the complete view so we can complete a solution ?
Was expecting the cshtml view. Here's an example:
#model IEnumerable<yourModelName>
#{
ViewBag.Title = "Index";
}
<h2>Index</h2>
<p>
#Html.ActionLink("Create New", "Create")
</p>
<table>
<tr>
<th>
#Html.DisplayNameFor(model => model.NID)
</th>
<th>
#Html.DisplayNameFor(model => model.CreatedOn)
</th>
<th>
#Html.DisplayNameFor(model => model.ExtensionAttribute15)
</th>
<th></th>
</tr>
#foreach (var item in Model) {
<tr>
<td>
#Html.DisplayFor(modelItem => item.yourElement1)
</td>
<td>
#Html.DisplayFor(modelItem => item.yourElement2)
</td>
<td>
#Html.ActionLink("Edit", "Edit", new { id=item.ID }) |
</td>
</tr>
}
</table>
#section Scripts{
<script type="text/javascript">
$("a#next").on("click", function () {
var nextli = $("li.first");
if ($(nextli).is(":last-child")) {
return false;
}
nextli.removeClass("first").addClass("record").next().addClass("first").removeClass("record");
});
$("a#previous").on("click", function () {
var nextli = $("li.first");
if ($(nextli).is(":first-child")) {
return false;
}
nextli.removeClass("first").addClass("record").prev().addClass("first").removeClass("record");
});
</script>
}
I've a MVC application, whose SharedLayout view(Master Page) gives user capability to search. They could search their order by Order No or By Bill no. So there are two option buttons the Shared View along with the textbox. Code is somewhat like this
#using (Html.BeginForm("Track", "Tracking", FormMethod.Post))
{
<div style="text-align: center">
<textarea cols="20" id="txtNo" name="txtOrderNo" rows="2" ></textarea>
</div>
<div style="text-align: center">
<input type="radio" name="optOrderNo" checked="checked" value="tracking" />Order No <input type="radio" name="optRefNo" value="tracking" />Ref No
</div>
<div style="text-align: center">
<input type="submit" value="Track" />
</div>
}
So it'll go to TrackingController and Track Method in it and return the view. It works fine for a single search as a View is associated with a controller's methods. It works fine but how could i conditionally return the other view based on the radio button selection.
What i come up with is this
[HttpPost]
public ActionResult Track(FormCollection form)
{
string refNo = null;
if (form["optRefNo"] == null)
{
string OrderNo = form["txtOrderNo"];
var manager = new TrackingManager();
var a = manager.ConsignmentTracking(OrderNo);
var model = new TrackingModel();
if (OrderNo != null)
model.SetModelForConsNo(a, consNo);
return View(model);
}
refNo = form["txtConsNo"];
return TrackByRef(refNo);
}
public ActionResult TrackByRef(string refNo)
{
//what ever i want to do with reference no
return View();
}
Kindly guide.
Thanks
View has an overload where the first parameter is a string. This is the name (or path) to the view you want to use, rather than the default (which is a view that matches the action's name).
public ActionResult TrackByRef(string refNo)
{
//what ever i want to do with reference no
return View("Track");
// or, if you want to supply a model to Track:
// return View("Track", myModel);
}
I Have a View and Partial View. The layout for the view is something like this:
<html>
...
<div id="MainView">#RenderBody()</div>
<!--Partial View-->
<div id="partialView">#Html.Action("PartialViewForm", "Main")</div>
...
</html>
My Partial View (named as _Register) is something like this:
#model PartialViewModel
<div id="form">
#using (Html.BeginForm("PartialViewForm", "Main", FormMethod.Post))
{
#Html.ValidationSummary(true)
<table >
<tbody>
<tr>
<td align="left">#Html.LabelFor(model => model.Name)*</td>
<td>#Html.EditorFor(model => model.Name)</td>
<td align="left">#Html.ValidationMessageFor(model => model.Name, "")</td>
</tr>
<tr>
<td align="left"><input type="submit" value="Go" class="submit2"/></td>
</tr>
</tbody>
</table>
}
</div>
In my MainController I have methods like this:
public class MainController : Controller
{
public ActionResult Index()
{
return View();
}
[HttpGet]
public ActionResult PartialViewForm()
{
var partialViewModel= new PartialViewModel();
return PartialView("_Register", partialViewModel);
}
[HttpPost]
public ActionResult PartialViewForm(PartialViewModel partialViewModel )
{
// if Validation is not successfull
return PartialView("_Register", partialViewModel);
// else
....
}
}
This is what I want to do... when validation fails on the partial view I want to g back to the main view... however in my case on the post action when validation fails all I can see is the partialview... there is no main page content.
There are posts on the forum that show the same kind of behavior but I am not able to solve my issue. Can anyone please tell me how to fix it (it will be really helpful if you can modify my example and show it)
Thanks
I'm not sure if I totally understand what you are trying to do but if what I'm thinking is right, you should just use
[HttpPost]
public ActionResult PartialViewForm(PartialViewModel partialViewModel )
{
// if Validation is not successfull
model = _db.getBlah(); //get the original model for the main view
return View("MainView", model);
// else
....
}
However I think your issue might be that you really should have your form submission in your main view and not in your partial - the partial is just there to render the editors for your Create/Edit views, etc; the data should be submitted to the main view's action so that it can create/update the proper model.