Start of with empty View Model MVC - c#

I would like to create a view which is linked to multiple tables. From what I understand I need to create a View Model and link that to the page.
I get a couple of errors using the below
'PaymentViewModel' is a type, which is not valid in the given context.
An expression tree may not contain a dynamic operation (related to first error?)
I am new to MVC - come from ASP....Any help is appreciated
public class PaymentViewModel
{
public string playername { get; set; }
public DateTime dob { get; set; }
public string phone { get; set; }
public string email { get; set; }
public string clubname { get; set; }
public string productname { get; set; }
public decimal amount { get; set; }
public int transactionID { get; set; }
public bool approved { get; set; }
public string subtype { get; set; }
public DateTime subdate { get; set; }
}
Controller
I need to start with a blank view as this is the first step to register a player so the information is not in the database.
Below is the code I use to get a populated View.
public ActionResult Payment()
{
DateTime blank = Convert.ToDateTime("01-01-1900");
var prod = from p in db.Product
join c in db.Club on p.clubname equals c.clubname
where p.clubname == "Club1"
select new PaymentViewModel
{
productname = p.prodname,
clubname = c.clubname,
playername = c.add1,
dob = blank,
phone = c.phone,
email = c.email,
transactionID = 0,
amount = p.amount,
approved = Convert.ToBoolean("1"),
subtype = c.city,
subdate = blank
};
return View(prod);
}
View
#S4C.BAL.PaymentViewModel;
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>Player Name</h4>
<hr />
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
<div class="form-group">
<b class="control-label col-md-2" style="">Full Name</b>
<div class="col-md-10">
#Html.EditorFor(model => model.playername, new { htmlAttributes = new { autofocus = "autofocus", #maxlength = "25", #class = "form-control" } })
#Html.ValidationMessageFor(model => model.playername, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
<br /><br />
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</div>
</div>
</div>
}
<div>#Html.ActionLink("Back to List", "Index")</div>
#section Scripts {#Scripts.Render("~/bundles/jqueryval")}

You cannot do approved = Convert.ToBoolean("1") in your select because the whole projection will happen at the database side and it does not know what Convert.ToBoolean() is. You need to do this in your view model:
public class PaymentViewModel {
// other properties ...
public string approved { get; set; }
public bool IsApproved {get {return this.approved == "1" }}
}
Also change the first line in your view to this:
#model S4C.BAL.PaymentViewModel

Not sure if I am understanding this correctly so please tell me if I'm wrong here.
Sounds like you know how to get a view filled with data from your database and you want to get an empty view without the data filled. To get an empty view request with just return a view without the model.
// Must request with /{Controller}/PaymentEmpty
Public ActionResult PaymentEmpty()
{
return View("Payment", new PaymentViewModel());
}
If you look at the default templates for ASP MVC applications the controller contains actions for Index, Details, Create, Edit and Delete. Thinking of actions in this manner can help with structuring your requests. Maybe place Payment into its own controller named PaymentsController and having the actions from the controller follow the default template.

Related

DropDownListFor with ViewBag does not pass values

I want to pass a new booking via a create view where the user can select different locations. I can select the locations in the view, but the location attribute is still set to null, when I click submit, everything else works.
The locations are stored in a List in an infrastructure model, I am guessing, the problem is, that my values are LocationIds and not Location objects, but I don't know how to create an selected list with object values, since all the examples are with ids/names.
These are my models:
public class Infrastructure
{
public int InfrastructureId { get; set; }
[Required]
public List<Location> Locations { get; set; }
}
public class Location
{
public int LocationId { get; set; }
public Address Address { get; set; }
public int CountEmployee { get; set; }
public GPS Coordinates { get; set; }
public List<ChargingStation> ChargingStations { get; set; }
}
this is the Create in the Controller:
public IActionResult Create()
{
Infrastructure infrastructure = _infrastructure.GetRealInfrastructure();
List<Location> locations = infrastructure.Locations;
ViewBag.Locations = new SelectList(locations, "LocationId", "Address.City");
return View();
}
and this is the view:
<div class="form-group">
<label asp-for="end" class="control-label"></label>
<input asp-for="end" class="form-control" id="endTime"/>
<span asp-validation-for="end" class="text-danger"></span>
</div>
</div>
<div class="col-3">
<div class="form-group">
<label asp-for="location" class="control-label"></label>
#Html.DropDownListFor(model => model.location, (IEnumerable<SelectListItem>)ViewBag.Locations, "Choose Location", new { #class = "form-control" })
<span asp-validation-for="location" class="text-danger"></span>
</div>
</div>
The values for endtime for example are saved, but location is still set too null, so I am guessing I am missing some select/submit tag? Or as written before its because I pass an string value (LocationId) instead of an actual location object?

Editing only part of object in view but pass whole obj to ActionListener

I have a form with a submit button that should pass through the item to the actionlistener. I thought it might be similar to the question in #Html.HiddenFor does not work on Lists in ASP.NET MVC but none of the answers seem to work. You can even see my for-loop taken from one of the answers in there.
[
EDIT: I have gotten rid of the mass of hidden loops and replaced with #Html.EditorFor so that you can see, even if not hidden, the flags list does not get to the actionlistener. This is a problem because when someone edits the flags, there is no way to update the db as I cannot get the ID of the flag updated.
]
The ModelState in the controller is never valid, regardless whether I keep the "[Bind(Include =" there or not. That's just there because of the tutorial for
ASP.NET MVC Tutorial: Web application development with Azure Cosmos DB.
ItemController.cs:
[HttpPost]
[ActionName("ProductEdit")]
[ValidateAntiForgeryToken]
public async Task<ActionResult> EditProductAsync( [Bind(Include = "Id, Name, Flags")] Item model)
{
Item product = await DocDBRepo<Item>.GetItem(model.Id);
model.Organisations = product.Organisations;
if (ModelState.IsValid) //Checks item validation via "required" set on properties
{
await DocDBRepo<Item>.UpdateItemAsync(model.Id, model);
return RedirectToAction("Index");
}
return View(model);
}
[HttpGet]
[ActionName("ProductEdit")]
public async Task<ActionResult> EditProductAsync(string id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Item item = await DocDBRepo<Item>.GetItem(id);
if (item == null)
{
return HttpNotFound();
}
return View(item);
}
ProductEdit.cs:
#model RRPortal.Models.Item
#{
ViewBag.Title = "ProductEdit";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>ProductEdit</h2>
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
<hr />
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
#Html.HiddenFor(model => model.Id)
<div class="form-group">
#Html.LabelFor(model => model.Name, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.Name, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.Name, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.Flags, htmlAttributes: new { #class = "control-label col-md-2 " })
</div>
#*Flags list*#
#for (int i = 0; i < Model.Flags.Count; i++) //foreach (var flag in Model.Flags)
{
<div class="form-group">
//#Html.HiddenFor(modelItem => Model.Flags[i].Id)
#Html.Label(Model.Flags[i].Name, htmlAttributes: new { #class = "control-label col-md-3" })
#Html.LabelFor(modelItem => Model.Flags[i].Enabled, htmlAttributes: new { #class = "control-label col-md-1" })
<div class="col-md-8">
#Html.EditorFor(modelItem => Model.Flags[i].Enabled, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(modelItem => Model.Flags[i].Enabled, "", new { #class = "text-danger" })
</div>
</div>
}
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
}
<div>
#Html.ActionLink("Back to List", "Index")
</div>
#section Scripts {
#Scripts.Render("~/bundles/jqueryval")
}
Item.cs:
public class Item
{
[JsonProperty(PropertyName = "id")]
public string Id { get; set; }
[Required]
[JsonProperty(PropertyName = "name")]
public string Name { get; set; }
[JsonProperty(PropertyName = "flags")]
public List<Flag> Flags { get; set; }
[JsonProperty(PropertyName = "organisations")]
public List<Organisation> Organisations { get; set; }
}
public class Flag
{
[JsonProperty(PropertyName = "id")]
public int Id { get; set; }
[Required]
[JsonProperty(PropertyName = "name")]
public string Name { get; set; }
[Required]
[JsonProperty(PropertyName = "enabled")]
public bool Enabled { get; set; }
}
public class Organisation
{
[JsonProperty(PropertyName = "id")]
public int Id { get; set; }
[JsonProperty(PropertyName = "name")]
public string Name { get; set; }
[JsonProperty(PropertyName = "users")]
[Display(Name ="Users")]
public List<User> UserStore { get; set; }
}
public class User
{
[JsonProperty(PropertyName = "id")]
public int Id { get; set; }
[Required]
[JsonProperty(PropertyName = "fname")]
public string FName { get; set; }
[Required]
[JsonProperty(PropertyName = "lname")]
public string LName { get; set; }
[Required]
[Display(Name = "Admin?")]
[JsonProperty(PropertyName = "isadmin")]
public bool IsAdmin { get; set; }
}
The Item's Id and Name comes through and is not null when I debug the controller, but the Flags List is always empty. The ModelState shows the following exception: {"The parameter conversion from type 'System.String' to type 'RRPortal.Models.Flag' failed because no type converter can convert between these types."}
I have also been asked where the ModelState is showing the exception so below is a screenshot:
I will gladly update the question if anyone has any questions. I have been tweaking the view for 2 days now and still can't get the item to contain anything. The rendered HTML appears to contain the organisation and inner objects perfectly fine.
Any help is appreciated!
My guess is that in your HttpGet view you have something along the lines of:
[HttpGet]
public ActionResult EditProductAsync()
{
var model = new ProductViewModel()
{
Flags = _uow.Products.GetFlags(),
Organisations = _uow.Products.GetOrganisations()
};
return View(model);
}
Because these objects are not also returned as part of your form, they are returning to the server as empty which is throwing an error for you, thus invalidating the model. Before you check if the model is valid, you should first do something like this:
[HttpPost]
[ActionName("ProductEdit")]
[ValidateAntiForgeryToken]
public async Task<ActionResult> EditProductAsync( [Bind(Include = "Id, Name, Flags, Organisations")] Item model)
{
model.Organisations = _uow.Products.GetOrganisations();
model.Flags = _uow.Products.GetFlags();
if (ModelState.IsValid)
{
await DocDBRepo<Item>.UpdateItemAsync(model.Id, model);
return RedirectToAction("Index");
}
return View(model);
}
By populating those fields, any model errors you have are strictly your client's errors on submitting the form.

Fill ViewModel collection from multiple fields

I have a form in which user can select which shipping methods they want to support for they product that they are selling, e.g. first class letter, second class letter, parcel, etc. I only give users a collection of possible shipping methods, they declare how much each one will cost, so if someone wants to sell a toaster in a parcel, they will charge less than for a set of dumbbells.
My ProductViewModel:
public int Id { get; set; }
public ICollection<SelectedShippingMethodViewModel> SelectedShippingMethods { get; set; }
And SelectedShippingMethodViewModel:
public class SelectedShippingMethodViewModel
{
public string Name { get; set; }
public decimal Price { get; set; }
}
In my form I create a section with possible options like this:
<h3>Add new product</h3>
<hr />
#using (Html.BeginForm("AddNew", "ProductCreator", null, FormMethod.Post, new { #class = "form-horizontal", role = "form" }))
{
#Html.AntiForgeryToken()
<div class="form-group">
<label class="col-sm-2 control-label">Shipping methods</label>
<div class="col-sm-10">
#foreach (ShippingMethod shippingMethod in ViewBag?.ShippingMethods)
{
<div class="row">
<div class="col-md-3">
// I don't know what should be here
#Html.CheckBox("SelectedShippingMethods", false)
#shippingMethod.Name
</div>
<div class="col-md-2">
// I don't know what should be here
#Html.TextBox("SelectedShippingMethods.Price")
</div>
</div>
}
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-default">Add product</button>
</div>
</div>
}
I have a database table with every possible shipping method that I acquire like this:
[HttpGet]
public async Task<ActionResult> AddNew()
{
ViewBag.ShippingMethods = await _shippingService.GetAllShippingMethodsAsync();
return View();
}
The problem is if checkbox is selected I have to bind Price and Name for each individual SelectedShippingMethodViewModel and I have no idea how to make it work.
Your view models are incorrect. To allow users to select the shipping methods they want and add a price, that view model needs to be
public class ShippingMethodViewModel
{
public string Name { get; set; }
public decimal Price { get; set; }
public bool IsSelected { get; set; } // your checkbox binds to this property
}
and the ProductViewModel should be
public class ProductViewModel
{
public int Id { get; set; }
....
public List<ShippingMethodViewModel> ShippingMethods { get; set; }
}
Then in the GET method, initialize your ProductViewModel and populate the ShippingMethods based on all available ShippingMethods, for example
var shippingMethods = await _shippingService.GetAllShippingMethodsAsync()
ProductViewModel model = new ProductViewModel
{
....
ShippingMethods = shippingMethods.Select(x => new ShippingMethodViewModel
{
Name = x.Name
}).ToList()
};
return View(model);
and in the view, use a for loop or EditorTemplate for typeof ShippingMethodViewModel to correctly generate your form controls
#for (int i = 0; i < Model.ShippingMethods.Count; i++)
{
#Html.LabelFor(m => m.ShippingMethods[i].IsSelected, Model[0].ShippingMethods.Name)
#Html.CheckBoxFor(m => m.ShippingMethods[i].IsSelected)
#Html.LabelFor(m => m.ShippingMethods[i].Price)
#Html.TextBoxFor(m => m.ShippingMethods[i].Price)
#Html.HiddenFor(m => m.ShippingMethods[i].Name) // if you want this to be submitted as well
}
Then in the POST method
public ActionResult AddNew(ProductViewModel model)
{
// Get the selected Shipping Methods and the associated price
var selectedMethods = model.ShippingMethods.Where(x => x.Selected);

C# Model Binding to Custom Classes Not Working

I am having difficulty getting MVC to bind to a model I have created. I have done this quite a few times in the past successfully. As such, I am just not sure why it is not working in this project.
For example, I have the following View:
#model StoryWall.ViewModels.ViewPostViewModel
#{
ViewBag.Title = "View Post";
}
<article class="story">
<header>
<h1>#Model.story.Title</h1>
<spann class="text-muted">#Model.story.Store.StoreName</span>
<h2>Posted by #Model.story.PosterName</h2>
</header>
if(#Model.story.StoryImage != null) {
<div class="storyImageWrapper">
<img src="~/img/#Model.story.StoryImage" />
</div>
<p>#Model.story.StoryBody</p>
}
</article>
<div class="commentsSection">
<h2>Comments</h2>
<h3>Add a Comment</h3>
<form method="post" class="form-horizontal" name="CommentForm" action="/View/AddComment">
#Html.AntiForgeryToken()
<input type="hidden" name="newComment.StoryID" value="#Model.story.StoryID" />
<div class="form-group"><label class="control-label col-sm-2">Name </label><div class="col-sm-10">#Html.TextBoxFor(#m => m.newComment.CommenterName, new { #class = "form-control", #required = true, #ng_model = "CommenterName"}) <span class="text-warning" ng-show="CommentForm.newComment.CommenterName.$dirty && CommentForm.newComment.CommenterName.$invalid"> Required </span> <span class="text-warning"> #Html.ValidationMessageFor(#m => m.newComment.CommenterName) </span></div> </div>
<div class="form-group"><label class="control-label col-sm-2">Email </label><div class="col-sm-10">#Html.TextBoxFor(#m => m.newComment.CommenterEmail, new { #class = "form-control", #required = true, #ng_model = "CommenterEmail" }) <span class="text-warning" ng-show="CommentForm.newComment.CommenterEmail.$dirty && CommentForm.newComment.CommenterEmail.$invalid"> Required </span> <span class="text-warning"> #Html.ValidationMessageFor(#m => m.newComment.CommenterEmail) </span></div> </div>
<div class="form-group"><label class="control-label col-sm-2">Message </label><div class="col-sm-10">#Html.TextAreaFor(#m => m.newComment.CommentBody, new { #class = "form-control", #required = true, #ng_model = "CommentBody" }) <span class="text-warning" ng-show="CommentForm.newComment.CommentBody.$dirty && CommentForm.newComment.CommentBody.$invalid"> Required </span> <span class="text-warning"> #Html.ValidationMessageFor(#m => m.newComment.CommentBody) </span></div> </div>
<button type="submit" ng-disabled="CommentForm.$invalid">Submit</button>
</form>
<h3>Current Comments</h3>
#foreach(var comment in #Model.story.Comments) {
<blockquote>#comment.CommentBody</blockquote>
<span>Poster: #comment.CommenterName on #comment.DatePosted.ToString("MM-dd-yyyy")</span>
}
</div>
Even though I am specifically using Html.TextBoxFor() for my input boxes, the binding is still not working as expected.
This is my Controller. "comment" in the second Action method is not binding correctly; its properties are null.
public class ViewController : Controller
{
StoryModel dbContext = new StoryModel();
public ActionResult ViewPost(Int32 postID)
{
ViewPostViewModel vm = new ViewPostViewModel();
vm.story = dbContext.Stories.FirstOrDefault(s => s.StoryID == postID);
return View(vm);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult AddComment(Comment comment)
{
if (ModelState.IsValid)
{
dbContext.Comments.Add(comment);
dbContext.SaveChanges();
return RedirectToAction("ViewPost", new { storyID = comment.StoryID});
}
else
{
ViewPostViewModel vm = new ViewPostViewModel();
vm.story = dbContext.Stories.FirstOrDefault(s => s.StoryID == comment.StoryID);
vm.newComment = comment;
return View("ViewPost", vm);
}
}
}
I know this is not the first time a similar question has been asked, but I could not find a solution that solved my problem. Additionally, as stated, this is something I have done in the past with success.
The only "new" element in this scenaria for me is Angular.js. This is my first time using the framework. Could it be interfering with the binding somehow?
If it helps, the Comment model:
public partial class Comment
{
public int CommentID { get; set; }
public int? StoryID { get; set; }
public int UserID { get; set; }
[Required]
[StringLength(75)]
public string CommenterName { get; set; }
[Required]
[StringLength(75)]
public string CommenterEmail { get; set; }
[Required]
public DateTime DatePosted { get; set; }
[Required]
public string CommentBody { get; set; }
public virtual Story Story { get; set; }
public virtual User User { get; set; }
}
and the ViewPostViewModel
public class ViewPostViewModel
{
public Story story { get; set; }
public Comment newComment { get; set; }
}
}
Thanks much for any help.
One answer would be to use #Html.EditorFor()
#Html.EditorFor(m => m.newComment)
Then on the folder where the view is placed you create a new folder called EditorTemplates with a view that is named exactly as the object type. In this case, Comment.cshtml
The view could be something like this ->
#model StoryWall.ViewModels.Comment
#Html.TextBoxFor(m => m.CommenterName)
#Html.TextBoxFor(m => m.CommenterEmail)
#Html.TextAreaFor(m => m.CommentBody)
This approach is the one I normally use to work with lists (useful in surveys or tests) but it also works with a single item.
Another approach could be to just add everything to the viewmodel since the viewmodel doesn't need to be a one to one mapping of the business objects or the database models. :)
Edit: Forgot to add. I think using this approach the method that receives the post will have to receive the whole ViewModel instead of just the comment. ->
public ActionResult AddComment(ViewPostViewModel vm)

Bind multiple values to a single checkbox and post it to controller

Model.cs
A campaign can have multiple images, that's why IEnumerable<int> ImageIdList.
public class Campaign
{
public int Id { get; set; }
public int CreatedBy { get; set; }
public int UpdatedBy { get; set; }
public IEnumerable<int> ImageIdList { get; set; }
}
View.cshtml
I want to download all the images related to a campaign, based on the ImageIdList, that's why I need to post all these ImageIds when a particular Campaign is checked and download button is clicked.
#model Campaign
#{
Layout = "....";
var assets = Model.AssetsInCampaign.ToList();
}
#using (Html.BeginForm("action-method", "controller", FormMethod.Post))
{
<div class="btnSubmit">
<input type="submit" value="Download Asset(s)" />
</div>
#foreach(var i in assets)
{
<div class="s_checkcol">
<input type="checkbox" name="ids" />
#foreach (var imageId in i.Where(c => c.AssetId == doc.FileDataId).SelectMany(c => c.ImageIdList))
{
<input type="hidden" name="ids" value=#(imageId)>
}
</div>
}
}
Controller.cs
public ActionResult DownloadFiles(IEnumerable<int> ids)
{
// code
}
NOTE: Only a part of code(where I'm facing the problem) is provided here. Its a DB first approach and in no way I can alter that (ORDERS).
I tried the above, but all of the ids are posted to the controller no matter how many checkboxes are selected.
Question: How should I bind the IEnumerable<int> ImageIdList property to a checkbox in View.cs and post the data to Controller.cs so that only the ids of selected checkboxes are posted?
This is a nice practice... it will work and Iam working with such a
manner (Iam sure that it will work very well) but one thing you have to be very carefull while coding this, little bit
complicated
Iam taking this effort not only for as an answer to this particular question.
Its for all stackoverflow users. Because i never found the below method anyware in stackoverflow.
I get this method by a long search. You people can use this.
It will help you to avoid for loops to bind the Checkboxlist
Its the best good for re-usability (need a single line (max: 20-25 chars to bind a CheckBoxList in Razor))
CheckBoxListItem.cs
create a New Class CheckBoxListItem //you can use any other names
public class CheckBoxListItem
{
public int ID { get; set; }
public string Display { get; set; }
public bool IsChecked { get; set; }
}
MyModel.cs
This is modelclass
public class MyModel
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public IEnumerable<CheckBoxListItem> ChkList { get; set; }
}
HomeController.cs
This is controller
public ActionResult Index()
{
var model = new MyModel(){
Id = 0,
Name = "Your Name",
ChkList = dbContext.myTable.Select(x => new CheckBoxListItem { ID = x.MyTableFieldID, Display = x.MyTableFieldName, IsChecked = true })
//If you need only int part, then just avoid to bind data on Display field
};
return View(model);
}
[HttpPost]
public ActionResult Index(MyModel myModel) //Your model object on PostAction
{
IEnumerable<CheckBoxListItem> ChkList = myModel.ChkList;
// Here is your answer, You can see all your check box items (both checked and unchecked) in ChkList, it will shows all your checked items are true and non-checked items are false in IsChecked field
}
Here you have to give more patiance
Goto the Folder View>Shared>EditorTemplates and RightClick Add>View... and Create a new View with the same name CheckBoxListItem.cshtml
CheckBoxListItem.cshtml
#model Project.Models.CheckBoxListItem
<div class="">
#Html.HiddenFor(x => x.ID)
<div class="">
#Html.CheckBoxFor(x => x.IsChecked)
</div>
#Html.LabelFor(x => x.IsChecked, Model.Display, new { #class = "" })
</div>
Create your View
Index.cshtml
#model #model Project.Models.MyModel
<div class="form-group">
#Html.LabelFor(model => model.Id, htmlAttributes: new { #class = "" })
<div class="col-md-10">
#Html.EditorFor(model => model.Id, new { htmlAttributes = new { #class = "" } })
#Html.ValidationMessageFor(model => model.Id, "", new { #class = "" })
</div>
</div>
#Html.EditorFor(model => model.ChkList) //This only one line of code is enough to bind a checkBoxList in future
<input type="submit" value="Create" class="" />
You will get all these in your post action

Categories