Failing to implement voting system with Asp.net MVC 5 controller - c#

I'm trying to basically have users "like" a post, which I call "voting" for a post in my application.
I have a vote class which records the userid, the postid (called a story), and the rest is boilerplate.
public class Vote
{
public int Id { get; set; }
public string VoterId { get; set; }
public virtual ApplicationUser Voter { get; set; }
public int StoryId { get; set; }
public Story Story { get; set; }
public DateTime CreatedAt { get; set; }
}
In the story model, I have a reference to a collection of votes which I theoretically will call a .Count() and print the number of "votes" to the view when it comes time to render the post details page.
public virtual ICollection<Vote> Votes { get; set; }
Inside the razor view, when a user is looking at a post(story), deciding whether or not to vote for it, I have this form. So if the user logged in isn't the author of the story, then they can vote for the story.
#if (!Model.IsStoryOwner)
{
<div class="row mt-1">
<div class="col-3">
#using (Html.BeginForm("New", "Vote"))
{
#Html.AntiForgeryToken()
#Html.HiddenFor(x => x.Story.Id, new { StoryId = Model.Story.Id })
<div class="btn-group">
<button type="submit" class="btn btn-info voteBtn" id="LikeBtn">
<i class="fa fa-heart mr-2"></i>
Vote
</button>
</div>
}
</div>
</div>
}
This hits the following controller:
//POST /vote/new
//FOR adding a vote to a story
[Authorize]
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult New(Vote vote)
{
var voterId = User.Identity.GetUserId();
var newVote = new Vote
{
VoterId = voterId,
StoryId = vote.StoryId,
CreatedAt = DateTime.Now
};
dbContext.Votes.Add(newVote);
dbContext.SaveChanges();
return View();
}
however the storyId is coming through as null even though I've verified that it's not on the client-side when I run the application.

Because your current view code will generate the HTML markup for a hidden input element with name attribute value Story.Id.
<input name="Story.Id" type="hidden" value="1">
When the form is submitted, model binder will not be able to map the value of that input to StoryId property of Vote object. For model binder to properly map the values, the input element name should match with the property name of the class you are using as the parameter of your action method.
You can create a hidden input with the name attribute value matching to your property name
#using (Html.BeginForm("New", "Vote"))
{
#Html.AntiForgeryToken()
<input type="hidden" name="StoryId" value="#Model.Story.Id" />
<div class="btn-group">
<button type="submit" class="btn btn-info voteBtn" id="LikeBtn">
<i class="fa fa-heart mr-2"></i>Vote
</button>
</div>
}
Or you can use the Html.Hidden helper method which generates the same markup as above.
#Html.Hidden("StoryId",Model.Story.Id)

You are using HiddenFor the Story.Id, not StoryId.
If your Model contains a value for StoryId, you can simply use
#Html.HiddenFor(x => x.StoryId)

Related

ASP.NET MVC cannot pass my hidden model back to controller

model class
public int Year { get; set; } = 0;
public int Odometer { get; set; }
public string ImageURL { get; set; } = "NA";
public string Category { get; set; } = "NA";
My View
<div class="form-group">
<label class="control-label AutoLText">Has Vehicle Documents</label>
#Html.DropDownListFor(Model => Model.VehDocuments, new SelectList(Model.GetYesNo()),new { #class = "form-control AutoL" })
<span asp-validation-for="VehDocuments" class="text-danger"></span>
</div>
<div class="form-group">
#Html.HiddenFor(m => m.ImageURL)
<input type="submit" value="Add New Vehicle" class="btn MVButton_2" />
</div>
so if i dont have the "#Html.HiddenFor(m => m.ImageURL)" in my view then the "ImageURL" will not be passed to the controller.
using the "HiddenFor" is kind of a security issue ? as if they change the string it will mess-up the image path to the controller and save it to the DB.
How can i go around this ?
Don't have ImageURL in the ViewModel if the user isn't supposed to see or change it.
What you are actually asking is "How to preserve model's properties' original values when they were not edited". There are multiple options, and yes, using hidden fields is one of them. Take a look at this post on social msdn.

Fill #Html.DropDownListFor with data from ViewModel

I have working code for adding role to user. Now i want to replace text input by dropdownlist with available (all) roles in application. I now that there is html tag #Html.DropDownListFor so I created new ViewModel containing every data I need but now i'am in dead end. I try foreach and fill data for every item on list but then it doesn't work. Trying with #Htm.DropDownListFor have the same effect. Can someone can show me the right way how to do that ? Thanks !
Controller:
public async Task<IActionResult> AddRoleToUser(string id, string role)
{
var user = await _userManager.FindByIdAsync(id);
if (await _roleManager.RoleExistsAsync(role))
{
await _userManager.AddToRoleAsync(user, role);
}
return RedirectToAction("UserGroups", new { id });
}
ViewModel:
public class UserGroupsViewModel
{
public ApplicationUser ApplicationUser { get; set; }
public IList<string> RolesList { get; set; }
public IQueryable<ApplicationRole> AllRoles { get; set; }
}
View
#model AdminMVC.Models.Admin.UserGroupsViewModel
#using (Html.BeginForm("AddRoleToUser", "Admin", new { id = Model.ApplicationUser.Id }))
{
<div classs="input-group">
<p><b>Name new role for user:</b></p>
<input class="form-control form-control-sm" type="text" name="role" placeholder="Role name">
<span class="input-group-btn">
<input type="submit" value="Add Role" class="btn btn-primary btn-sm" />
</span>
</div>
}
My question has been identified as a possible duplicate of another question. But i saw that question and still cant do it.
Can I must change my IList to list containing two values id and string ? And add additional position in viewmodel to store the result?
Change your RolesList to a SelectList and add a role property:
public SelectList RolesList { get; set; }
public string Role{get;set;}
Build the SelectList with
model.RolesList = new SelectList(listOfRoles);
Then in your view
#Html.DropDownListFor(x => x.Role, Model.RolesList)
If everything is in order, your post should contain the role populated.

How to select an item from <select> tag and save it as Foreign Key in ASP .NET Core 2.0

I have these two classes:
State and Station :
public class State
{
public int Id { get; set; }
[Required]
public string Code { get; set; }
[Required]
public string Name { get; set; }
public virtual ICollection<Station> Stations { get; set; }
}
and
public class Station
{
public int Id { get; set; }
[Required]
public string Code { get; set; }
[Required]
public string Name { get; set; }
[Required]
public virtual State State { get; set; }
}
The design was "Code First" and I used migration to set up the database and it was like:
The station table saves StateId as foreign Key as it should
My StationsController.cs file has a Create Method in which I use ViewBag for listing the state names like this:
// GET: Stations/Create
public IActionResult Create()
{
ViewBag.StatesList = _context.States.ToList();
return View();
}
And finally my HttpPost Create method
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Id,Code,Name,State")] Station station)
{
if (ModelState.IsValid)
{
_context.Add(station);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(station);
}
My create.cshtml file has <select> tag like this:
<div class="form-group">
<label asp-for="State" class="control-label"></label>
<select asp-for="State" asp-items="#(new SelectList(ViewBag.StatesList, "Id", "Name","State"))" ></select>
<span asp-validation-for="State" class="text-danger"></span>
</div>
The Issue I am facing is that after clicking submit, my ModelState.isValid remains false and the State field is null as shown in this image:
The State Field is null
The Controller has been autogenerated and only two things I have changed: 1st is that I have added the ViewBag.StateList in the Create() method and second is that I have added a State field in Create([Bind("Id,Code,Name,State")].
Any help will be greatly appreciated and sorry for the long post..
regards
Ashutosh
I don't know how many times I have said in SO that you shouldn't send your entity model directly from database to the view, and listen to its postback. You should only generate a model (we call it ViewModel) that represents what the view needs.
Here is what I will do (I wrote everything by hand, not tested).
Create a view model for station creation view:
public class CreateStationViewModel
{
// You shouldn't have Station ID here as it's creation
[Required]
public string Code { get; set; }
[Required]
public string Name { get; set; }
[Display(Name = "State")]
public int SelectedStateId { get; set; }
public IDictionary<int, string> AvailableStates { get; set; }
}
Initialize this view model on the get method:
public IActionResult Create()
{
var vm = new CreateStationViewModel
{
// Construct a list of available states.
// We will use it as the dropdown options.
AvailableStates = _context.States
.ToDictionary(x => x.Id, x => $"{ x.Name }({ x.Code })")
};
return View(vm);
}
Build the form on the view:
#model CreateStationViewModel
#{
// You can define a variable here for the dictionary-to-selectListItem
// conversion.
// Or you can write an extension method on IDictionary for that purpose.
var availableStatesSelectListItems = Model.AvailableStates
.Select(x => new SelectListItem
{
Text = x.Value.ToString(),
Value = x.Key.ToString()
});
}
<form asp-area="" asp-controller="station" asp-action="create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Code" class="control-label"></label>
<input type="text" class="form-control" asp-for="Code" />
<span class="form-text" asp-validation-for="Code"></span>
</div>
<div class="form-group">
<label asp-for="Name" class="control-label"></label>
<input type="text" class="form-control" asp-for="Name" />
<span class="form-text" asp-validation-for="Name"></span>
</div>
<div class="form-group">
<label asp-for="SelectedStateId" class="control-label"></label>
<select class="form-control" asp-for="SelectedStateId"
asp-items="availableStatesSelectListItems">
<option value="">- select -</option>
</select>
<span class="form-text" asp-validation-for="SelectedStateId"></span>
</div>
<div class="form-group">
<button type="submit" class="btn btn-success">Create</button>
</div>
</form>
Listen to the view model on postback:
[HttpPost]
public IActionResult Create(CreateStationViewModel model)
{
if (ModelState.IsValid)
{
// You build the entity model from the view model
_context.Stations.Add(new Station
{
Code = model.Code,
Name = model.Name,
StateId = model.SelectedStateId
});
_context.SaveChanges();
return RedirectToAction("index");
}
// Rebuild the available states, or
// better, you can use ajax for the whole form (different topic)
model.AvailableStates = _context.States
.ToDictionary(x => x.Id, x => $"{ x.Name }({ x.Code })");
return View(model);
}
AFAIK you can't pass a State object to be used as the value of the <option> elements inside your <select>. Instead, you have to use a simpler datatype. In practice you need to use StateId as the value.
Add the property StateId to your Station class:
public class Station
{
public int Id { get; set; }
[Required]
public string Code { get; set; }
[Required]
public string Name { get; set; }
public virtual State State { get; set; }
[Required]
public int StateId { get; set; }
}
Also, your SelectList constructor is wrong. The fourth argument ("State" in your example) is supposed to define the default selected item. You don't need to define it here, so leave it out:
<select asp-for="StateId" asp-items="#(new SelectList(ViewBag.StatesList, "Id", "Name"))" ></select>
Now your <select> element should be able to select the correct Id and populate your StateId field. Note that in the [HttpPost] method, the Statewill still be null, but your database should be updated with the correct StateId.

html.hidden for value not set in asp.net MVC core Razor view

I am working on an asp.net MVc core application. I have a popup with a form element like this:
#using (Html.BeginForm("AddIVR", "ITPVoice", FormMethod.Post, new { role = "form" }))
{
#*#Html.HiddenFor(m =>m.m_newIVR.Account, new { #value= Model.accountID})*#
#Html.Hidden("m.m_newIVR.Account", Model.accountID)
}
I have a viewmodel like this:
public class AccountDetailsViewModel
{
public IVRS m_newIVR { get; set; }
}
and IVRS model class like this:
public class IVRS
{
[JsonProperty("_id")]
public string Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("description")]
public string Description { get; set; }
[JsonProperty("account")]
public string Account { get; set; }
}
I am trying to populate it in my view like this:
#Html.HiddenFor(m =>m.m_newIVR.Account, new { #value= Model.accountID})
but when i see view source, Value is null
I tried using:
#Html.Hidden("m.m_newIVR.Account", Model.accountID)
and it shows m_newIVR.Account populated.
Then I am posting the form to controller this action
[HttpPost]
public ActionResult AddIVR(AccountDetailsViewModel model)
{
return RedirectToAction("AccountDetails", "mycontroller")
}
Although I see that AccountId is populated in view ( using viewsource), but in post action method value of model.m_newIVR.Account is null.
HTML output looks like this:
<div id="edit-ivrs-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true" style="display: none;">
<div class="modal-dialog">
<form action="/ITPVoice/AddIVR" method="post" role="form"> <div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Add IVR</h4>
<input id="m_newIVR_Account" name="m_newIVR.Account" type="hidden" value="" />
<input id="AccountId" name="AccountId" type="hidden" value="56f5e3d77ea022a042665be1" />
</div>
<div class="modal-body">
</div>
</div>
My Questions are:
Why html.hiddenfor is not setting value of the model variable?
Although html.hidden is setting value, why it is not accessible in post action method ?
Please suggest.
Now I am able to answer your question why does it works for Html.Hidden but not for Html.HiddenFor.
When you Html.HiddenFor with m =>m.m_newIVR.Account then it always try to set value for hidden field value whatever value available in property m.m_newIVR.Account not the value that you specify using #value = Model.AccountId.
If you want to use HiddenFor the set m_newIVR.Account in ViewModel just use following thing.
#Html.HiddenFor(m =>m.m_newIVR.Account)
Html.Hidden is not strongly type so it not depend on name. You can specify different name and value parameter. In this case It is your responsibility to generate proper name for HiddenField.
My Working Sample
Model
public class AccountDetailsViewModel
{
public string AccountId { get; set; }
public IVRS m_newIVR { get; set; }
}
public class IVRS
{
[JsonProperty("_id")]
public string Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("description")]
public string Description { get; set; }
[JsonProperty("account")]
public string Account { get; set; }
}
Controller Action
[HttpGet]
public IActionResult Index1()
{
AccountDetailsViewModel model = new AccountDetailsViewModel();
//model.AccountId = "1222222";
model.m_newIVR = new IVRS();
model.m_newIVR.Account = "122222";
return View(model);
}
[HttpPost]
public IActionResult Index1(AccountDetailsViewModel model)
{
return View(model);
}
View (Index1.cshtml)
#model WebApplication2.Controllers.AccountDetailsViewModel
#using (Html.BeginForm())
{
#Html.HiddenFor(m =>m.m_newIVR.Account)
<input type="submit" />
}
// Sample Out

How do I pass this child model reference to a virtual ICollection in parent domain model in ASP.NET MVC?

I'm creating a commenting system for my ASP.NET MVC blog engine using a view form that triggers a basic controller action method:
FORM:
#if (User.Identity.IsAuthenticated)
{
//using (Html.BeginForm())
// {
<div class="new_comment">
<h6 id="shortcodes" class="page-header"><i class="fa fa-file-text-o"></i> Leave a comment</h6>
<div class="hline"></div>
<form class="form-horizontal" action="#Url.Action("CreateComment")" method="post" role="form">
<div class="form-group">
<div class="col-sm-4 col-md-4">
<textarea rows="7" class="form-control" name="Message" placeholder="Your Comment Here..."></textarea>
#Html.AntiForgeryToken()
<input type="hidden" name="Slug" value="#Model.Slug"/>
<input type="hidden" name="PostId" value="#Model.Id"/>
<br/>
</div>
</div>
<div class="form-group">
<input type="submit" value="Post Comment" class="btn btn-primary" style="margin-left: 12px"/>
</div>
</form>
</div>
//}
}
CONTROLLER:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult CreateComment([Bind(Include = "PostId,Message,Username,DatePosted")]Comment comment)
{
var post = db.BlogPosts.Find(comment.PostId);
if (post == null)
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
if (ModelState.IsValid)
{
comment.Username = User.Identity.GetUserId();
comment.DatePosted = DateTimeOffset.Now;
db.Comments.Add(comment);
db.SaveChanges();
}
return RedirectToAction("BlogPostDetails", new { slug = post.Slug});
}
I've set breakpoints aside each of the expressions contained within the if statement and confirmed that none of the data values being passed ("PostId, Message, Username, DatePosted") are null, and that db.SaveChances() is commiting changes to the database. Next, here isModels.BlogPosts...
MODELS:
public class BlogPosts
{
public BlogPosts()
{
this.Comments = new HashSet<Comment>();
}
public int Id { get; set; }
public DateTimeOffset Created { get; set; }
public DateTimeOffset? Updated { get; set; }
[AllowHtml]
[Required]
public string Title { get; set; }
public string Slug { get; set; }
[Required]
public string Category { get; set; }
[AllowHtml]
[Required]
public string Body { get; set; }
public string MediaURL { get; set; }
public bool Published { get; set; }
public virtual ICollection<Comment> Comments { get; set; }
}
public class Comment
{
public int Id { get; set; }
public int PostId { get; set; }
public string Username { get; set; }
[Required]
[DataType(DataType.MultilineText)]
public string Message { get; set; }
public DateTimeOffset DatePosted { get; set; }
public Nullable<System.DateTimeOffset> Edited { get; set; }
public virtual BlogPosts BlogPost { get; set; }
public virtual ApplicationUser Author { get; set; }
//public int? ParentId { get; set; }
//[ForeignKey("ParentId")]
//public virtual ICollection<Comment> Children { get; set; }
//public string ParentComment { get; internal set; }
}
And here is the view that fails to execute:
VIEW THAT DOES NOT EXECUTE
#foreach (var item in Model.Comments.OrderByDescending(c => c.DatePosted))
{
<div class="comment">
<p>
#if (item.Username == null)
{
<small>By: Anonymous</small><span>|</span>
}
else
{
<small>By: #Html.DisplayFor(modelItem => item.Username)</small><span>|</span>
}
<small>Date: #Html.DisplayFor(modelItem => item.DatePosted)</small>
#if (item.Edited != null)
{
<span>|</span><small>Updated: #Html.DisplayFor(modelItem => item.Edited)</small>
}
</p>
<div>
#Html.DisplayFor(modelItem => item.Message)
</div>
</div>
if (item.Username == User.Identity.GetUserId() || User.IsInRole("Admin") || User.IsInRole("Moderator"))
{
<div>
#Html.ActionLink("Edit", "_EditComment", new { id = item.Id }) <span>|</span>
#Html.ActionLink("Delete", "_DeleteComment", new { id = item.Id })
</div>
}
<br />
<!--<div class="hline"></div>-->
}
<div>
<input type="button" class="btn btn-primary" value="Return to Blog Roll" onclick="location.href = '#Url.Action("BlogIndex")'">
</div>
<br />
#if (User.Identity.IsAuthenticated || User.IsInRole("Admin") || User.IsInRole("Moderator"))
{
<input type="button" class="btn btn-primary" value="Modify Post" onclick="location.href = '#Url.Action("BlogAdmin")'">
<br />
<br />
}
When setting a breakpoint on the first line in the view above: #foreach (var item in Model.Comments.OrderByDescending(c => c.DatePosted)), the reference to
public virtual ICollection<Comment> Comments within the Models.BlogPosts class remains null (which obviously means the logic in my view fails to execute and no comment is posted).
I am new to ASP.NET MVC, EF Code-First, etc., and clearly do not understand how my controller is failing to pass the comment values in the child model to the public virtual ICollection<Comment> Comments in the parent... How is it that Models.Comment as referenced in my CommentCreate controller contains a value, and the very same virtual reference in my Models.BlogPosts does not?
EDIT: After great feedback from several users on both the cosmetic errors and critical errors in my code, as well as helpful ASP.NET MVC resource pointers, I determined that the null references being passed had to do with incorrect property-naming conventions in my domain models. See answer below.
Youre including PostId however the actual property name is Id. Also you need to show what model your view is receiving. If youre concerened with what youre exposing (e.g. Id, why dont you just mark it as a hidden field?).
In the controller pass in nothing but the model that you want to edit, e.g. keep a controller solely for url routing, keep your model solely for your object and your view should be only for the model youre passing in.
Finally figured out that the BlogPost property in public virtual BlogPosts BlogPost in my Models.Comment domain model needs to be renamed to match the domain model's foreign key: public int PostId. The solution was executed as follows:
Changing the property name to Post,
then manually deleting the null PostId values from the Comment table in my database,
and then running update-database -f command in NuGet.

Categories