I'm relatively new to ASP.NET so please bear with me.
I'm trying to code a straightforward auction site for a charity, using MVC 5 and Entity Framework with Code-First.
I have created an Item model and controller. The Item model holds fields like the title, description, starting bid, current bid, number of bids, high bidder, etc.
public class Item
{
public int ID { get; set; }
public string Code { get; set; }
public string Title { get; set; }
public string Description { get; set; }
[DisplayName("Starting Bid")] public int StartingBid { get; set; }
public int Increment {get; set;}
public int Bids { get; set; }
[DisplayName("Current Bid")] public int CurrentBid { get; set; }
public string HighBidder { get; set; }
public int CurrentOrStartingBid
{ // The price that is displayed next to an item
get
{
return Bids > 0 ? CurrentBid : StartingBid;
}
}
public int NextBid
{ // The minimum amount that is valid for a new bid
get
{
return Bids > 0 ? CurrentBid + Increment : StartingBid;
}
}
}
(What I have tried to do in the code above is to add these properties CurrentOrStartingBid and NextBid which are not intended to be part of the database record, they are just derivative properties rather than columns in the DB. So hopefully having these as read-only properties will do that...)
I am now making a view for the item detail. This shows the item description, and also features form controls for placing a bid, similar to what you would see on eBay. My question is about how to wire up the logic for the Bid button correctly.
I figure that in the view I should use an HTML form with a submit button for the bidding. This does an HTTP post when the button is hit, and allows me to write a method in the Item controller that gets called at that time.
So my Razor code in the view currently looks like this:
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<div class="form-group">
<div class="input-group">
<span class="input-group-addon">£</span>
<input type="number" class="form-control" value="#Model.NextBid" id="CurrentBid" name="CurrentBid"/>
</div>
<br />
<button type="submit" class="btn btn-primary btn-lg">Place bid</button>
</div>
}
Using this code allows me to write a controller method with this signature:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Bid([Bind(Include = "ID,CurrentBid")] Item item)
This kind of works so far. But note that I am having to pass the amount that has been bid in the CurrentBid field of the item, before it has been validated server-side. This doesn't feel quite right to me. Is there a way of writing the method so it just takes a signature like this?
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Bid(int itemID, int bidAmount)
Maybe there's a way to do that with query strings or something?
Anyway, once inside the method, things again seem a little weird. Protecting from over-posting, the only fields in the item variable that are valid are ID and CurrentBid. So I then do a lookup in the DbContext to find the actual item that corresponds with that ID and update it:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Bid([Bind(Include = "ID,CurrentBid")] Item item)
{
if (ModelState.IsValid)
{
Item i2 = db.Items.Find(item.ID);
if (item.CurrentBid >= i2.NextBid)
{
i2.Bids++;
i2.CurrentBid = item.CurrentBid;
i2.HighBidder = HttpContext.User.Identity.Name;
db.Entry(i2).State = EntityState.Modified;
db.SaveChanges();
}
return View(i2);
}
return RedirectToAction("Auction");
}
This seems to work. But it does not feel right. I think I am missing some important concepts/patterns here. If any experienced MVC folks could sketch how they would wire up the client/server logic for this simple button, that would be great. (You would also be helping a good cause!)
Is there a way of writing the method so it just takes a signature like
this?
[HttpPost] [ValidateAntiForgeryToken]
public ActionResult Bid(int itemID, int bidAmount)
Sure there is a way, lets say you have a simple model like this -
public class Item
{
public int ID { get; set; }
public int NextBib { get; set; }
public int CurrentBid { get; set; }
public string Description { get; set; }
}
Then your Action is creating an Item and sending to View as shown below -
public ActionResult BidForm()
{
Item i = new Item();
i.ID = 100;
i.CurrentBid = 10;
return View(i);
}
And your view is as follows -
#model MVC.Controllers.Item
#{
ViewBag.Title = "BidForm";
}
<h2>BidForm</h2>
#using (Html.BeginForm("Bid", "sample", FormMethod.Post))
{
#Html.AntiForgeryToken()
<input type="hidden" name="itemId" value="#Model.ID" />
<input type="number" class="form-control" value="#Model.NextBib" id="CurrentBid" name="bidAmount" />
<input type="submit" value="Create" />
}
And your NextBid action would be -
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Bid(int itemID, int bidAmount)
{
return View();
}
As you see we have an HiddenField with name = itemID and a input type=number field with name = `bidAmount. Those are mapped to parameters of the action as shown below -
Some recommendations which you can consider -
Instead of using regular HTml.BeginForm(), you can go for Ajax.BeginForm() to give more intuitive user experience. Alternatively you can use JQuery POST operation too.
To prevent OVER posting, you need to create specific ViewModels for specific activities like increasing a bid, displaying a product etc. And you post only required viewModels for required activities, so that you can prevent over posting of form.
Related
I am trying to create a blog site with asp.net core 3.1. I want to add a comment to the blog post, but I cannot get a query string to which blog post I posted.
I shared my codes as follows. Now, what is the reason why the id from the query string always returns 0?
Or what is the situation I am constantly doing wrong?
There is probably a very simple solution but it took me all day please can you help?
Entity;
public class Content : IEntity
{
public int Id { get; set; }
public int CategoryId { get; set; }
public int UserId { get; set; }
public virtual List<ContentComment> ContentComments { get; set; }
}
public class ContentComment : IEntity
{
public int id { get; set; }
public int UserId { get; set; }
public int ContentId { get; set; }
public string Comment { get; set; }
public DateTime CommnetCreateTime{ get; set; }
public virtual Content Contents { get; set; }
}
Controller :
public class BlogContentController : Controller
{
private IBlogService _blogService;
private IContentCommentsServices _contentCommentsServices;
public BlogContentController(IBlogService blogService, IContentCommentsServices contentCommentsServices)
{
_blogService = blogService;
_contentCommentsServices = contentCommentsServices;
}
public IActionResult Index(int id)
{
var blogModel = new ContentViewModel
{
Blogs = _blogService.GetById(id)
};
return View(blogModel);
}
public IActionResult CommentAdd()
{
var model = new ContentCommendViewModel()
{
ContentComments = new List<ContentComment>()
};
return Ok();
}
[HttpPost]
public IActionResult CommentAdd(ContentComment contentComment)
{
contentComment.ContentId = Convert.ToInt32(HttpContext.Request.Query["id"]);
_contentCommentsServices.Add(contentComment);
return RedirectToAction("CommentAdd");
}
}
View Page :
<div class="media-body mt-2" id="yorumyap">
<h5>Yorum Bırakın</h5>
<form asp-controller="BlogContent" asp-action="CommentAdd">
<div class="form-group">
<textarea name="Comment" class="form-control form-control-sm" rows="3" style="resize: none;" id="commenttext"></textarea>
</div>
<div class="text-right">
<button type="submit" class="btn btn-tekisimbilsim mb-2" id="commendAdd">Yorum Yap</button>
</div>
</form>
</div>
You are trying to get the 'id' from the query string of the request. This would require your post URL to look something like: https://example.com/BlogContent/CommentAdd?id=42. Based on the HTML you have provided, I bet your URL looks like: https://example.com/BlogContent/CommentAdd (you can determine what your URL looks like by using your browsers inspection tools to look at the form tag in the HTML). In order to get the desired behavior you will need to update your form tag to look something like this:
<form asp-controller="BlogContent" asp-action="CommentAdd" asp-route-id="#Model.BlogId">
the important part in this is the addition of the 'asp-route-id' tag helper. You can find information about this tag helper (listed as 'asp-route-{value}') and others at:
https://learn.microsoft.com/en-us/aspnet/core/mvc/views/working-with-forms?view=aspnetcore-3.1#the-form-tag-helper
But wait, there's more! In this example I just gave more than likely your URL will turn out to be https://example.com/BlogContent/CommentAdd/42 (I say more than likely because this is determined by your route configuration). This will not give you the expected result and the id will again be zero! So you have a couple of options. The easiest is to change the 'asp-route-{value}' tag helper to be something like 'asp-route-contentId'. This would give you html that looks like:
<form asp-controller="BlogContent" asp-action="CommentAdd" asp-route-contentId="#Model.BlogId">
which would generate a URL that would be https://example.com/BlogContent/CommentAdd?contentId=42. You would then need to update your controller code to retrieve the id to look like this:
contentComment.ContentId = Convert.ToInt32(HttpContext.Request.Query["contentId"]);
There are a myriad of other options we could go into but I will stop this essay for the time being here.
I'm new to asp.mvc and I'm trying to figure out why when I try to update values of a model through EditorFor helpers it won't send the updated value.
I have to follow a strict code convention and my parameters must start with a prefix (bln/str/int and so on)
If I leave the other parameters (permissionKey/stationKey/username) without the prefix, the values received are the updated one (the ones I need).
[HttpPost, ActionName("Save")]
public ActionResult Save(int intId, string permissionKey, string stationKey, string username)
{
var perm = _repository
.Get(row => row.Id == intId)
.First();
perm.PermissionKey = permissionKey;
perm.StationKey = stationKey;
perm.Username = username;
If I change the definition to strPermissionKey/strStationKey/strUsername, I will receive the old values of the model.
Model:
public class EditablePermissionRowModel
{
public int Id { get; set; }
public string PermissionKey { get; set; }
public string StationKey { get; set; }
public string Username { get; set; }
public bool EditPressed { get; set; } = false;
}
my Html looks like this:
#using (Html.BeginForm("Save", "Permissions"))
{
#*#Html.AntiForgeryToken()*#
#Html.HiddenFor(m => m.Id)
.... more html....
<button type="submit" class="btn btn-link">
<span class="glyphicon glyphicon-ok"></span>
</button>
I also tried to include all parameters that I need as hidden.
#Html.Hidden("strPermissionKey", Model.PermissionKey);
But this still didn't work. (Also tried #Html.Editor("strPermissionKey", Model.PermissionKey) but not luck)
What am I missing?
EDIT:
#StephenMuecke helped and pointed out that I only need to pass the model back as a parameter... and this did it. Problem solved.
this:
public ActionResult Save(EditablePermissionRowModel udtModel) {
... code ....
I have one model called ProductSupplier
I am passing #model IEnumerable to my View
and showing it from view
Now when i submit the form i m not getting list of IEnumerable in my http post method. I want to know the selected supplier from user.
Below is my model
public sealed class ProductSupplier
{
public int CountryId { get; set; }
public int UserId { get; set; }
public bool IsProductSupplier { get; set; }
public string CountryName { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
}
This is my HttpGet method
public ActionResult ManageSupplier(int id)
{
var supplier = App.UsersRepo.GetSupplierForProduct(id);
return View(supplier);
}
And I am binding it via following way (U can suggest me best way I am new bee to MVC)
#model IEnumerable<ProductSupplier>
#using (Html.BeginForm("ManageSupplier", "Products", FormMethod.Post, new { role = "form") })
{ #Html.AntiForgeryToken()
foreach (var item in Model)
{
<div class="checkbox">
<label>
#Html.CheckBoxFor(x => item.IsProductSupplier, new { id = item.Email }) #item.FirstName #item.LastName (#item.Email)
</label>
</div>
}
}
And finally my HttpPost method
[HttpPost]
public ActionResult ManageSupplier(IEnumerable<ProductSupplier> obj)
{ // I m getting obj null in my argument
//I want to Get selected id from obj and want to pass in selectedSupplier
var returnVal = App.ProductRepo.AssigneSupplierForProduct(productId, selectedSupplier);
return Json(new { success = true }, JsonRequestBehavior.DenyGet);
}
can anyone suggest me where i m making mistake.
I am new to MVC any kind of suggestion highly appreciated.
Thank you in advance.
Firstable u cant do it like this.One way to do that is something like this.Here is the basic step how u do that.
1-assign for all checkbox ,checkbox change event with the unique id.
(take a look at here)
2-Cretae a jquery object and store the data when ever the checkbox clicked ,via versa
var ListProductSuppliers ={ {ProductSupplier_info_here },{ProductSupplier_info_here } };
3-later via ajax request,serilize this object(ListProductSuppliers ) and send to your method
4-on server side deserilize this to the IEnumerable<ProductSupplier>
5 later do it whatever u want with those selected suppliars
Please bear with my noobness, I'm super new to the MVC pattern.
What I'm trying to do
I am building a profile information page for registered users on my site. This page would list data about the user, such as date of birth, telephone number, subscription status, etc.. You get the idea. I would also like to have a form to let users change their password, email address, personal information on the same page.
My problem
The user's data comes from my controller via a passed model variable:
public ActionResult Profil()
{
var model = db.Users.First(e => e.UserName == WebSecurity.CurrentUserName);
return View(model);
}
The output looks like this in my view:
<label>Phone number: </label>
#if (Model.PhoneNumber != null)
{
#Model.PhoneNumber
}
else
{
<span class="red">You haven't set up your phone number yet. </span>
}
The form in which the user could change his info would use another model, ProfileModel. So basiccaly I need to use two models in my view, one for outputting information and one for posting data. I thought that using a partial view I can achieve this, but I get this error:
The model item passed into the dictionary is of type
'Applicense.Models.User', but this dictionary requires a model item of
type 'Applicense.Models.ProfileModel'.
Here's what my call to the partial view looks like:
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
#Html.ValidationSummary()
#Html.Partial("_ModifyProfileInfo")
}
Here's the partial view:
#model Applicense.Models.ProfileModel
<ul>
<li>
#Html.LabelFor(m => m.Email)
#Html.EditorFor(m => m.Email)
</li>
<li>
#Html.LabelFor(m => m.ConfirmEmail)
#Html.EditorFor(m => m.ConfirmEmail)
</li>
<input type="submit" value="Update e-mail" />
</ul>
And finally here's my ProfileModel:
public class ProfileModel
{
[Required]
[DataType(DataType.EmailAddress)]
[Display(Name = "New e-mail address")]
public string Email { get; set; }
[DataType(DataType.EmailAddress)]
[Display(Name = "Confirm new e-mail address")]
[Compare("Email", ErrorMessage = "The e-mail and it's confirmation field do not match.")]
public string ConfirmEmail { get; set; }
}
Am I missing something? What's the proper way to do this?
Edit:
I remade my code reflecting Nikola Mitev's answer, but now I have another problem. Here's the error I get:
Object reference not set to an instance of an object. (#Model.UserObject.LastName)
This only occurs when I'm posting the changed e-mail address values. Here's my ViewModel (ProfileModel.cs):
public class ProfileModel
{
public User UserObject { get; set; }
[Required]
[DataType(DataType.EmailAddress)]
[Display(Name = "Új e-mail cím")]
public string Email { get; set; }
[DataType(DataType.EmailAddress)]
[Display(Name = "Új e-mail cím megerősítése")]
[Compare("Email", ErrorMessage = "A két e-mail cím nem egyezik.")]
public string ConfirmEmail { get; set; }
[DataType(DataType.EmailAddress)]
[Display(Name= "E-mail cím")]
public string ReferEmail { get; set; }
}
Controller:
public ActionResult Profil()
{
var User = db.Users.First(e => e.UserName == WebSecurity.CurrentUserName);
var ProfileViewModel = new ProfileModel
{
UserObject = User
};
return View(ProfileViewModel);
}
And finally here's my user.cs model class:
[Table("UserProfile")]
public class User
{
[Key]
[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
public int UserId { get; set; }
[Column("UserName")]
public string UserName { get; set; }
[Column("Email")]
[Required]
public string Email { get; set; }
[Column("FirstName")]
public string FirstName { get; set; }
[Column("LastName")]
public string LastName { get; set; }
[Column("PhoneNumber")]
public string PhoneNumber { get; set; }
... You get the idea of the rest...
I'm thinking it's happening because the model is trying to put data in each required columns into the database.
Edit2:
The httppost method of my Profil action:
[HttpPost]
[Authorize]
[ValidateAntiForgeryToken]
public ActionResult Profil(ProfileModel model)
{
if (ModelState.IsValid)
{
//insert into database
return Content("everything's good");
}
else
{
//outputs form errors
return View(model);
}
}
The best way to handle this situation is to use and pass viewModel to your Profile controller, viewModel is wrapper class for multiple objects that you want to pass to your view.
public class ProfileUserViewModel
{
public ProfileModel ProfileModelObject {get; set;}
public UserModel UserModelObject {get; set;}
}
Your controller should look like:
public ActionResult Profil()
{
var profileModel = db.Users.First(e => e.UserName == WebSecurity.CurrentUserName);
var userModel = //fetch from db.
var pmViewModel = new ProfileUserViewModel
{
ProfileModelObject = profileModel,
UserModelObject = userModel
};
return View(pmViewModel);
}
And finally your view :
#model Applicense.Models.ProfileUserViewModel
<label>Phone number: </label>
#if (Model.ProfileModelObject.PhoneNumber != null)
{
#Model.PhoneNumber
}
else
{
<span class="red">You haven't set up your phone number yet. </span>
}
There is an overload of #Html.Partial which allows you to send ViewData as defined in your controller - this is the method I generally use for partial views.
In your controller define ViewData["mypartialdata"] as ViewDataDictionary. Then in your view
#Html.Partial("_ModifyProfileInfo",ViewData["mypartialdata"])
In your [HttpPost] profil function, if modelstate.isvalid is false, you return your edit view, but you need to define your pmViewModel again , other wise your partial view will not have an object to display. Try using the following and let us know what happens
[HttpPost]
[Authorize]
[ValidateAntiForgeryToken]
public ActionResult Profil(ProfileModel model)
{
if (ModelState.IsValid)
{
//insert into database
return Content("everything's good");
}
else
{
//outputs form errors
var pmViewModel = new ProfileUserViewModel
{
ProfileModelObject = profileModel,
UserModelObject = userModel
};
return View(model);
}
}
While I know this question has been asked longtime ago however some people might still face a similar problem. One easy solution I use to pass or have more than one view model on a page is to use a ViewBag to hold the second object and refer to it in the view. See example bellow.
In your controller do this:
Obj2 personalDets = new Obj2();
DbContext ctx = new DbContext();
var details = ctx.GetPersonalInformation;
foreach(var item in details) {
personalDets.Password = item.Password;
personalDets .EmailAddress = item.EmailAddress;
}
ViewBag.PersonalInformation = personalDets;
Then in your view those properties become readily available for you
I have a simple model which uses a multi select listbox for a many-many EF relationship.
On my Create action, I'm getting the error
The parameter conversion from type 'System.String' to type 'MyProject.Models.Location' failed because no type converter can convert between these types.
I have 2 models, an Article and a Location:
Article.cs
namespace MyProject.Models
{
public class Article
{
public Article()
{
Locations = new List<Location>();
}
[Key]
public int ArticleID { get; set; }
[Required(ErrorMessage = "Article Title is required.")]
[MaxLength(200, ErrorMessage = "Article Title cannot be longer than 200 characters.")]
public string Title { get; set; }
public virtual ICollection<Location> Locations { get; set; }
}
Location.cs:
namespace MyProject.Models
{
public class Location
{
[Key]
public int LocationID { get; set; }
[Required(ErrorMessage = "Location Name is required.")]
[MaxLength(100, ErrorMessage = "Location Name cannot be longer than 100 characters.")]
public string Name { get; set; }
public virtual ICollection<Article> Articles { get; set; }
}
}
I have a ViewModel:
namespace MyProject.ViewModels
{
public class ArticleFormViewModel
{
public Article article { get; set; }
public virtual List<Location> Locations { get; set; }
public ArticleFormViewModel(Article _article, List<Location> _locations)
{
article = _article;
Locations = _locations;
}
}
}
create.cshtml:
#model MyProject.ViewModels.ArticleFormViewModel
<h2>Create</h2>
#using (Html.BeginForm()) {
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<fieldset>
<legend>Article</legend>
<div class="editor-label">
#Html.LabelFor(model => model.article.Title)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.article.Title)
#Html.ValidationMessageFor(model => model.article.Title)
</div>
<h3>Locations</h3>
#Html.ListBoxFor(m=>m.article.Locations,new MultiSelectList(Model.Locations,"LocationID","Name"))
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
Finally my controller actions:
// GET: /Article/Create
public ActionResult Create()
{
var article = new Article();
var AllLocations = from l in db.Locations
select l;
ArticleFormViewModel viewModel = new ArticleFormViewModel(article, AllLocations.ToList());
return View(viewModel);
}
//
// POST: /Article/Create
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(Article article)
{
var errors = ModelState.Values.SelectMany(v => v.Errors);
if (ModelState.IsValid)
{
var locations = Request.Form["article.Locations"];
if (locations != null)
{
var locationIDs = locations.Split(',');
foreach (var locationID in locationIDs)
{
int id = int.Parse(locationID);
Location location = db.Locations.Where(l => l.LocationID == id).First();
article.Locations.Add(location);
}
}
db.Articles.Add(article);
db.SaveChanges();
return RedirectToAction("Index");
}
var AllLocations = from l in db.Locations
select l;
ArticleFormViewModel viewModel = new ArticleFormViewModel(article, AllLocations.ToList());
return View(viewModel);
}
This all works relatively well, my Locations listbox is populated properly:
If I do not select a Location then my model is saved properly. If I select one or more locations then my Model.IsValid check fails with the exception
The parameter conversion from type 'System.String' to type 'MyProject.Models.Location' failed because no type converter can convert between these types.
However if I remove the ModelState.IsValid check then despite the error my values are all correctly saved into the database - just that I lose validation for things such as the model title.
Hope someone can help!
Unless you create a type converter, you cannot directly bind the results of your list box directly to a complex object like that. The reason lies in the fact that MVC can only deal with posted HTTP values, which in this case are an array of strings that contain the selected ID's. Those strings do not directly map to your Locations object (ie the number 1 cannot be directly converted to a Locations object with an ID of 1).
Your best bet is to have a list of location ID's in your View Model of type string or int to accept the posted values, then in your post method create the Location objects and fill them with the correct ID's.
FYI, the reason your code works is because you are bypassing the model binding and going directly to the Request.Form collection. You will notice that the bound Article object will not have any Location objects.
EDIT:
I don't even see how your code would work even without this problem. Your ArticleFormViewModel does not have a parameterless constructor, so that will fail in model binding (unless you have a custom model binder).
In any event, what you want to do is this (note, you will have to populate SelectedLocationIDs if you want them to be selected when the view is rendered):
public class ArticleFormViewModel
{
...
List<int> SelectedLocationIDs { get; set; }
...
}
Then, in your view you have:
#Html.ListBoxFor(m=>m.SelectedLocationIDs,
new MultiSelectList(Model.Locations,"LocationID","Name"))
In your Post method, instead of the code that calls Request.Form, you have something like this:
foreach(var locationID in article.SelectedLocationIDs) {
... // look up your locations and add them to the model
}