I'm trying to get to grips with kendo binding in MVVM.
I have a Razor page that looks like this...
Index.cshtml
#page
#model IndexModel
#{
ViewData["Title"] = "Index";
}
<div id="frm">
#using (Html.BeginForm("Index", "Home", FormMethod.Post))
{
<div class="form-group">
<label><input type="text" class="form-control" data-bind="value: Username"/></label>
</div>
<button type="submit" class="btn btn-primary">Click</button>
}
<label>
<input type="text" class="form-control" data-bind="value: Username" />
</label>
</div>
<script>
var raw = #Html.Raw(Model.Me.ToJson());
var vm = new kendo.observable(raw);
kendo.bind($("#frm"), vm);
</script>
Index.cshtml.cs...
public class IndexModel : PageModelBase
{
[BindProperty]
public Person Me { get; set; }
public void OnGet()
{
Me = new Person { Username = "Bobby Brown" };
}
public void OnPost()
{
var p = Me;
p.Username += ".";
}
public class Person
{
public string Username { get; set; }
public string ToJson() => JsonConvert.SerializeObject(this);
}
}
When I render the page, the 2 inputs are, properly bound to the passed in value from the server-side model.
When I change the value in one of the inputs client-side and change focus, the other input changes.
I expect all of this.
When I click the button, the control returns to the server and executes the code in OnPost().
What doesn't happen is for Me to be set to something other than null.
I've tried it as is shown above,
I've tried refactoring the OnPost() method to OnPost(Person me) but me isn't set.
I've tried assessing the Request.Form object but there is nothing there.
I'm sure it must be simpler than I'm trying to make it.
Can anyone offer any advice about that I'm doing wrong, please?
Basically, I'm dim.
I worked it out. I was trying to fit a square peg into a round hole.
I added a hidden input in the form with the name Me to match the property name I was binding to.
I changed the button to a regular button (from a submit button) and added an onClick handler that did this...
function submitForm() {
$("#Me").val({ Username: vm.get("Username") });
$("#frm").submit();
}
And "lo, there was light".
Thanks for listening
Related
I am coding a solution where the user will submit a form, posting the values back to my ASP.NET MVC controller. My model is complex and the form fields are contained in a nested object (I'm using CQRS via MediatR). When I submit the form, the values come across as null. How can I get the complex model to recognize the form fields?
Here is my code:
Controller:
[HttpPost]
[Route("edit")]
public async Task<IActionResult> Edit(UpdateApplicationCommand command)
{
await _mediator.Send(command)
.ConfigureAwait(false);
return RedirectToAction("Index");
}
Models:
public class UpdateApplicationCommand : IRequest<Unit>
{
public ApplicationEditGeneralViewModel ApplicationEditGeneralViewModel { get; set; } = null!;
}
public class ApplicationEditGeneralViewModel
{
[Required]
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
}
View:
#model ApplicationEditGeneralViewModel
<form method="post" asp-action="Edit" asp-controller="Applications">
<div class="form-floating mb-3">
#Html.TextBoxFor(m => m.Name, new { #class = "form-control", placeholder = "Application Name"})
<label for="Name">Application Name</label>
</div>
<div class="form-floating mb-3">
#Html.TextBoxFor(m => m.Description, new { #class = "form-control", placeholder = "Application Description"})
<label for="Description">Application Description</label>
</div>
<div class="d-flex flex-row-reverse bd-highlight">
<input type="submit" value="Submit" class="btn btn-primary mt-2" />
</div>
</form>
I've tried to reduce the complex model to its fields, by placing the contents of the ApplicationEditGeneralViewModel directly into the UpdateApplicationCommand class. This worked, but I'd really like to keep the nested structure so that I can reuse the ApplicationEditGeneralViewModel object.
I saw this solution here:
How to bind nested model in partial view
But I'd rather avoid adding the name as a route object (if possible) for every form field. Is there another, more simple way that I can do this?
The first way, you can custom model binding like below:
public class CustomModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
var model = new UpdateApplicationCommand()
{
ApplicationEditGeneralViewModel = new ApplicationEditGeneralViewModel()
{
Description = bindingContext.ValueProvider.GetValue("Description").ToString(),
Name = bindingContext.ValueProvider.GetValue("Name").ToString()
}
};
bindingContext.Result = ModelBindingResult.Success(model);
return Task.CompletedTask;
}
}
Apply the custom model binding like below:
[HttpPost]
public async Task<IActionResult> Edit([ModelBinder(typeof(CustomModelBinder))]UpdateApplicationCommand model)
{
//.....
}
The second way, just change your razor view like below:
#model UpdateApplicationCommand
<form method="post">
<div class="form-floating mb-3">
#Html.TextBoxFor(m => m.ApplicationEditGeneralViewModel.Name, new { #class = "form-control", placeholder = "Application Name"})
<label for="Name">Application Name</label>
</div>
<div class="form-floating mb-3">
#Html.TextBoxFor(m => m.ApplicationEditGeneralViewModel.Description, new { #class = "form-control", placeholder = "Application Description"})
<label for="Description">Application Description</label>
</div>
<div class="d-flex flex-row-reverse bd-highlight">
<input type="submit" value="Submit" class="btn btn-primary mt-2" />
</div>
</form>
I am trying to create a form that can be submitted multiple times with different information, while retaining a common value in one field.
I have a list view from a SQL table in ASP.NET Core Razor that is a list of construction projects. For each row in the list I have a link that goes to a "create" template page where users can create a bid entry for the project which is stored in a different table. The Project Number is assigned to a route value (asp-route-Number = "the project number from the previous list")and populates a hidden field in the "create new bid" form.
Using the default code for the razor page, everything works great. You click submit and are taken back to the list of projects.
What I want to do is have another option on the "create new bid" form that will allow you to save and enter another bid for the same project. I created another button and handler to do this but I am stuck on actually implementing it. If I use return Page() the form posts and the page is returned with route data intact, but the text fields still contain the previous data and the drop-down list is empty. If I use return RedirectToPage(CreateNewBid, Route data) the form posts but the route data does not seem to be passed along and creates a null value error.
This is the link from the Projects list (inside the foreach table), which takes you to the "Create Bid" form and works fine.
<a asp-page="CreateBid" asp-route-Number="#item.ProjectNumber" asp-route-opwid="#item.Id">New Bid</a>
The Create Bid form has the following to submit and create another entry
int num = int.Parse(Request.Query["Number"]);
int idnum = int.Parse(Request.Query["opwid"]);
<input type="submit" value="Save and enter another"
asp-page-handler="Another" asp-route-opwid="#idnum"
asp-route-Number="#num" class="btn btn-primary"/>
And the handler:
public async Task<IActionResult> OnPostAnotherAsync(int Number, int opwid)
{
if (!ModelState.IsValid)
{
return Page();
}
_context.OpwBids.Add(OpwBids);
await _context.SaveChangesAsync();
return Page();
//return RedirectToPage("./CreateBid", (Number == num, opwid == idnum));
}
I have also tried several things in the route parameters (as opposed to using the variables) in the "Redirect to Page" and nothing seems to work.
Is there an easier way, or am I just missing something?
This is the cshtml file:
#page
#model Authorization_AD.Pages.GenSvc.BidEntry.CreateBidModel
#{
ViewData["Title"] = "CreateBid";
}
#{ int num = int.Parse(Request.Query["Number"]);
int idnum = int.Parse(Request.Query["opwid"]);
}
<h1>Create Bid</h1>
<h4>OPW number #num</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<input asp-for="OpwBids.OpwProject" value="#idnum" hidden class="form-control" />
</div>
<div class="form-group">
<label asp-for="OpwBids.OpeningDate" class="control-label"></label>
<input asp-for="OpwBids.OpeningDate" class="form-control" />
<span asp-validation-for="OpwBids.OpeningDate" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="OpwBids.Contractor" class="control-label"></label>
<select asp-for="OpwBids.Contractor" class="form-control" asp-items="ViewBag.Contractor">
<option disabled selected>--- SELECT ---</option>
</select>
</div>
<div class="form-group">
<label asp-for="OpwBids.BidAmount" class="control-label"></label>
<input asp-for="OpwBids.BidAmount" class="form-control" />
<span asp-validation-for="OpwBids.BidAmount" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save and enter another"
asp-page-handler="Another" asp-route-opwid="#idnum"
asp-route-Number="#num" class="btn btn-primary"/>
<input type="submit" value="Save and return to list" asp-page-handler="Done" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="Index">Back to List</a>
</div>
#section Scripts {
#{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
This is the C# file:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Authorization_AD.Models;
namespace Authorization_AD.Pages.GenSvc.BidEntry
{
public class CreateBidModel : PageModel
{
private readonly Authorization_AD.Models.OPWContext _context;
public CreateBidModel(Authorization_AD.Models.OPWContext context)
{
_context = context;
}
public IActionResult OnGet()
{
ViewData["Contractor"] = new SelectList(_context.Contractors, "Id", "ContractorName");
ViewData["OpwProject"] = new SelectList(_context.MainProjectsListing, "Id", "ProjectNumber");
return Page();
}
[BindProperty]
public OpwBids OpwBids { get; set; }
public async Task<IActionResult> OnPostDoneAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_context.OpwBids.Add(OpwBids);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
public async Task<IActionResult> OnPostAnotherAsync(int Number, int opwid)
{
if (!ModelState.IsValid)
{
return Page();
}
_context.OpwBids.Add(OpwBids);
await _context.SaveChangesAsync();
return Page();
//return RedirectToPage("./CreateBid", (Number == OpwBids.OpwProjectNavigation.ProjectNumber, opwid == OpwBids.OpwProject));
}
}
}
You can add a property to your page that will be used to bind the value of the clicked button.
public class CreateBidModel : PageModel {
//...
//Add this property to your page.
[BindProperty]
public string Button {get;set;}
public void OnGet(int number,string opwid){
//Set the number and opwid to the target properties
}
public Task<IActionResult> OnPostAsync(){
if (!ModelState.IsValid)
{
return Page();
}
_context.OpwBids.Add(OpwBids);
await _context.SaveChangesAsync();
if(Button == "finish"){
return RedirectToPage("./Index");
}
else {
return RedirectToPage("./CreateBid", (Number == OpwBids.OpwProjectNavigation.ProjectNumber, opwid == OpwBids.OpwProject));
}
}
}
To the view you need to add two buttons that have the same name and that value will be mapped to the Button property.
<form method="post">
... Other content goes here
<button name="#Html.NameFor(m => m.Button)" value="another">Create another</button>
<button name="#Html.NameFor(m => m.Button)" value="finish">Finish</button>
</form>
The value of the clicked button will be parsed to the Button property of the Pagemodel. Based on the value you can decide how to further handle the response of the request (Finish / Create another one in your case).
Thanks for everyone's help. I got it to do what I want by adding the following to the "OnPostAnotherAsync" task:
public async Task<IActionResult> OnPostAnotherAsync(int Number, int opwid)
{
if (!ModelState.IsValid)
{
return Page();
}
_context.OpwBids.Add(OpwBids);
await _context.SaveChangesAsync();
ViewData["Contractor"] = new SelectList(_context.Contractors, "Id", "ContractorName");
ModelState.SetModelValue("OpwBids.BidAmount", new ValueProviderResult(string.Empty, CultureInfo.InvariantCulture));
ModelState.SetModelValue("OpwBids.Contractor", new ValueProviderResult(string.Empty, CultureInfo.InvariantCulture));
return Page();
}
After the "Save Changes" I needed to re-load the view data for the "Contractor" drop down list. Then it was just a matter of clearing the form fields before returning the page.
I'm currently trying to update some HTML based on two properties in my viewmodel which are hooked to two form groups. This is ASP.NET Core.
The two form groups:
<div class="form-group">
<label asp-for="ParticipantAmount">Antal deltagere</label>
<input asp-for="ParticipantAmount" onchange="Group()" class="form-control" type="number" max="1000" min="1" />
</div>
<div class="form-group">
<label asp-for="GroupAmount">Antal grupper</label>
<input asp-for="GroupAmount" class="form-control" onchange="Group()" type="number" max="20" min="1" />
<p id="GroupSize">0</p>
</div>
The properties in the viewmodel:
public int ParticipantAmount { get; set; }
public int GroupAmount { get; set; }
The javascript method in the script section:
function Group() {
var groups = Number('#Model.GroupAmount');
var participants = Number('#Model.ParticipantAmount');
var size = participants / groups;
document.getElementById('GroupSize').innerHTML = size;
}
When I try to log stuff to the console, it seems like the properties are not being updated in the viewmodel when the form inputs change.
I think I'm missing some basic knowledge about an important aspect of what I'm trying to do, but I can't seem to google the correct keywords. Hope you can lead me in the right direction.
Just to illustrate how AJAX calls work I made a minimalistic web application using jQuery which should be used for learning only, the code is not production-ready.
Razor view. I've added default values and IDs to your the input fields to identify them easier in JavaScript code.
<div class="form-group">
<label asp-for="ParticipantAmount">Antal deltagere</label>
<input asp-for="ParticipantAmount" onchange="Group()" class="form-control" type="number" max="1000" min="1" value="#Model.ParticipantAmount" id="ParticipantAmount" />
</div>
<div class="form-group">
<label asp-for="GroupAmount">Antal grupper</label>
<input asp-for="GroupAmount" class="form-control" onchange="Group()" type="number" max="20" min="1" value="#Model.GroupAmount" id="GroupAmount" />
<p id="GroupSize">0</p>`enter code here`
</div>
JavaScript code using jQuery to get/update values and make AJAX request.
<script>
function Group() {
$.ajax({
method: "POST",
url: "/Home/UpdateAmounts",
data: { ParticipantAmount: $('#ParticipantAmount').val(), GroupAmount: $('#GroupAmount').val() }
}).done(function (response) {
$('#GroupSize').html(response);
});
}
</script>
Controller and view model. Added a method for AJAX calls.
public class HomeController : Controller
{
public IActionResult Index()
{
return View(new ParticipantViewModel());
}
// You can call this method via AJAX to update your view model
[HttpPost]
public IActionResult UpdateAmounts(int participantAmount, int groupAmount)
{
// Validation for negative numbers, and groupAmount cannot be zero
if (participantAmount < 0 || groupAmount <= 0)
{
return Ok(0);
}
return Ok(participantAmount / groupAmount);
}
}
public class ParticipantViewModel
{
public int ParticipantAmount { get; set; }
public int GroupAmount { get; set; }
}
To make things nicer and more modern you could look into following example of a simple web application using database to store data, and using Knockout for UI.
I have build my entities via database-first, since I have to use an existing db.
Now, let's say I've got the entity customer and country and I want to edit the customer.
My View should contain something like First Name, Last Name and Country which is a DropdownList from all countries in the entity.
For that I created a CustomerViewModel which uses these two entities. But I dont' know if all of this is right, since some things don't work.
First the code of my CustomerViewModel:
public class CustomerViewModel
{
public CUSTOMER customer { get; set; }
public COUNTRY country { get; set; }
public IEnumerable<SelectListItem> countryList { get; set; }
MyEFEntities db = new MyEFEntities();
public CustomerViewModel(int id)
{
IEnumerable<CUSTOMER> customerList = from c in db.CUSTOMER
where c.CUSTOMERNO== id
select c;
customer = customerList.First();
var countryListTemp = new List<String>();
var countryListQry = from s in db.COUNTRY
select s.COUNTRY_ABBREVIATION;
countryListTemp.AddRange(countryListQry);
countryList = new SelectList(countryListTemp);
}
}
Then the CustomerController.cs:
public ViewResult CustomerData(int id = 0)
{
// if (id == 0) { ... }
var viewModel = new CustomerViewModel(id);
return View(viewModel);
}
[HttpPost]
public ActionResult CustomerData(CustomerViewModel model)
{
db.Entry(model.customer).State = System.Data.EntityState.Modified;
db.SaveChanges();
return View(model);
}
And last but not least the CustomerData.cshtml:
#model MyApp.ViewModels.CustomerViewModel
#{
ViewBag.Title = "Customer";
}
<h2>
Customer</h2>
#using (Html.BeginForm("CustomerData", "Customer"))
{
#Html.ValidationSummary(true)
<div id="tabs">
<ul>
<li>Data</li>
<li>Hobbies</li>
<li>Stuff</li>
</ul>
<div id="data">
<div class="editor-label">
#Html.Encode("Country:")
</div>
<div class="editor-field">
#Html.DropDownListFor(m => m.country.COUNTRY_ABBREVIATION, Model.countryList, Model.customer.ADDRESS.COUNTRY.COUNTRY_ABBREVIATION)
</div>
<div class="editor-label">
#Html.Encode("Last name:")
#Html.TextBox("lastName", #Model.customer.ADDRESS.NAME)
</div>
<div class="editor-label">
#Html.Encode("First Name:")
#Html.TextBox("firstName", #Model.customer.ADDRESS.FIRSTNAME)
</div>
</div>
<div id="hobbies">
<p>
Hobbies
</p>
</div>
<div id="stuff">
<p>
Stuff
</p>
</div>
</div>
<p>
<input type="submit" value="Save" />
</p>
}
Viewing works great. I get to the URL http://localhost:12345/Customer/CustomerData/4711 and can see the current values for that customer.
Now, I'm missing some stuff.
The Save-button doesn't work. If I click it, I got an error message, that no parameterless constructor was found for this object. But I want to pass the ViewModel back to the controller. How to overload the Html.BeginForm() method?!
How to I store the changed values from the customer? Is is done by editing the text-fields or do I have to use Html.TextboxFor() instead of Html.Textbox? This is so complicated for a beginner. I'm not into the LINQ style at all.
The Dropdownlist doesn't work as supposed to. The country the customer has already is twice in it. It seems, that the third parameter does not preselct an item but add a default one. Would Html.Dropdownlist be better?
This design is bad, I'm afraid. Never do DB access from the model, that is what the Controller is for. The model should just hold data that the Controller feeds to it.
If you factor out DB access, or any type of logic, from your model, you will find that your system becomes much easier to set up and maintain, and it probably even solves the problems you mention.
i am trying to do a simple image upload using MVC2. in my view i have:
<% using (Html.BeginForm("Upload","Home")) { %>
<input type="file" name="upload" id="ImageUpload" value="Upload Image"/>
<input type="submit" value="Upload" />
<% } %>
In my controller (Home), how do i get this Image uploaded and save it to a database? I am very new to ASP.Net MVC and this thing has got me stuck. Thanks in advance for your help and time.
Edit:
okay, i guess my question is vague from the answer i got to provide more detail , This is what i have:
The image model is simple as below --
public class ImageModel
{
public Image image;
public string ImageName;
public ImageModel(Image image, string name)
{
this.image = image;
ImageName = name;
}
}
the view is like this:
<%using (Html.BeginForm("Upload","Home", FormMethod.Post, new {enctype = "multipart/form-data"}))
{%>
<input type="text" id="ImageName" />
<input type="file" name="upload" id="ImageUpload" value="Upload Image"/>
<input type="submit" value="Upload" />
<%} %>
the controller is where i want create a new ImageModel instance, validate it and if valid save it to the database: So i have:
public ActionResult Upload(ImageModel image)
{
//this is where i am stuck?
//how to get the supplied image as part of the ImageModel object
//whats the best way to retrieve the supplied image
ImageModel temp = image;
if(!temp.IsValid()){
//get errors
//return error view
}
uploadrepository.SaveImage(temp);
return View();
}
The question is how to get the supplied image and save it to the database
based on your View code try changing your model to this...
public class ImageModel
{
public HttpPostedFileWrapper upload { get; set; }
public string ImageName { get; set; }
}
also, you'll need to name that text input element (not just id)...
<input type="text" id="ImageName" name="ImageName" />