I am currently making a website in ASP.NET MVC where i would like to use EditorTemplates to display a List inside my ViewModel. RowViewModel has a List of RowDataViewModels. I'm trying to make a Create View for RowViewModel, where i can use
#Html.EditorFor(model => model.RowData)
and therefore i need an EditorTemplate. I created and EditorTemplate under:
Views/Shared/EditorTemplates/RowDataViewModel.cshtml
And i also tried putting the EditorTemplates folder under the /Home/ view folder, but nothing seems to work. No breakpoints are being hit inside the editortemplate. I think i did it this way before, but i might be forgetting something.
Any ideas?
RowViewModel:
public class RowViewModel
{
[Required]
[Display(Name="Name")]
public string Name { get; set; }
public List<RowDataViewModel> RowData { get; set; }
}
RowDataViewModel:
public class RowDataViewModel
{
public string Name { get; set; }
public string Value { get; set; }
public string DataType { get; set; }
}
EditorTemplate - RowDataViewModel.cshtml:
#using iDealWebRole.ViewModels
#model RowDataViewModel
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
<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.Value, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.Value, "", new { #class = "text-danger" })
</div>
</div>
The problem is here: #Html.EditorFor(model => model.RowData)
You should use your editor for every row in RowData, like:
#for (int i = 0; i < Model.RowData.Count(); i++)
{
#Html.EditorFor(model => Model.RowData[i])
}
Related
I have the following model:
public class FormDimensions
{
[JsonProperty(PropertyName = "id")]
public string Id { get; set; }
[JsonProperty(PropertyName = "formID")]
public String FormID { get; set; }
[JsonProperty(PropertyName = "width")]
public float Width { get; set; }
[JsonProperty(PropertyName = "height")]
public float Height { get; set; }
[JsonProperty(PropertyName = "padding")]
public float Padding { get; set; }
[JsonProperty(PropertyName = "inspectedFields")]
public Dictionary<String, Dictionary<String,FieldDimension>> InspectedFields { get; set; }
}
The field "InspectedFields" is the field in question..
public class FieldDimension
{
[JsonProperty(PropertyName = "name")]
public String Name { get; set; }
[JsonProperty(PropertyName = "page")]
public int Page { get; set; }
[JsonProperty(PropertyName = "dimension")]
public String Dimension { get; set; }
}
I have been using the following code in my Edit Razor view for the model:
foreach (var entry in Model.InspectedFields)
{
<h3>#entry.Key</h3>
<div class="card-block">
#foreach (var page in entry.Value.Values)
{
<div class="form-group">
#Html.Label("Value Name", htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(x => page.Name, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessage(page.Name, new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.Label("Page Dimensions", htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(x => page.Dimension, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessage(page.Dimension, new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.Label("Page Number", htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(x => page.Page, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessage(page.Page.ToString(), new { #class = "text-danger" })
</div>
</div>
}
</div>
<hr />
}
So when I go to save my model, it actually nulls out the entire InspectedFeilds dictionary and cause my razor view to crash. However, it will save all the other values in the model. I am not sure if this is because my implementation or I am missing something about handling dictionaries in a razor view..
In order to post a dictionary you must both post the Key and Value. You also need to use a for loop and index the dictionary. For example:
#for (var i = 0; i < Model.InspectedFields.Count; i++)
{
#Html.HiddenFor(m => m.InspectedFields[i].Key)
for (var j = 0; j < Model.InspectedFields[i].Value.Count; j++)
{
#Html.HiddenFor(m => m.InspectedFields[i].Value[j].Key)
#Html.EditorFor(m => m.InspectedFields[i].Value[j].Value.Page)
}
}
HTML has been stripped from sample code to illustrate the format better.
I have the following two classes:
public class Game
{
public int ID { get; set; }
public string Title { get; set; }
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
public Console Console { get; set; }
}
public class Console
{
public int ID { get; set; }
public string Title { get; set; }
public DateTime ReleaseDate { get; set; }
public string Company { get; set; }
}
After this I added a GameController with scaffolding. Now I am working on the Create view of the Game Object and I am experiencing the problem that the MVC HTML.helpers are rendering input fields for all of the properties of a Console object instead of the just the title (which I guess is logical, given the fact that the property is an actual Console object).
I still want the user to choose a console to be taken along for the Game Object being created, so I tried solving it in the following way:
<div class="form-group">
#Html.LabelFor(model => model.Genre, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.Genre, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.Genre, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.Console, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.Console.Title, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.Console.Title, "", new { #class = "text-danger" })
</div>
</div>
<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>
I thought I would simply mention the title property of the model, but I'm still receiving input fields for all the Console properties in my Game's create view.
I understand this is simply the way MVC works. But how should I deal with something like this? I also tried things like:
Console: #Html.DropDownList("console", "All")
Of course the problem here is that the HTML form does not understand that this field is meant to be taken along as the console field for the game object being created. What is the correct solution for this problem?
EDIT:
I did the following:
Add a gameCreation viewModel:
public class GameCreation
{
public Game game { get; set; }
public SelectList Consoles { get; set; }
}
Then I built my ViewModel and passed it to view via controller:
public ActionResult Create()
{
var gc = new GameCreation();
gc.Consoles = cb.BuildList(db);
return View(gc);
}
Note that cb.BuildList(db) returns a SelectList item. Then in the view I tried:
<div class="form-group">
#Html.LabelFor(model => model.Console.Title, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.DropDownList("console", gc.Consoles)
#Html.ValidationMessageFor(model => model.Console.Title, "", new { #class = "text-danger" })
</div>
</div>
This does not work, as gc is not known. I also tried passing the data via the ViewBag but this item is also not known here. Also I then received the error:
HTMLHelper has no applicable method named 'dropdownlist'. How can I access the data of the viewmodel which I passed?
I Have the following in my razor view
#foreach (var group in Model.QuestionList.GroupBy(x => x.AreaName))
{
<h4>#group.Key</h4>
for (int i = 0; i < Model.QuestionList.Count(x => x.AreaName == group.Key); i++)
{
<div class="form-group">
<div class="row">
<div class="col-md-4">
#Html.DisplayFor(x => Model.QuestionList[i].Question)
</div>
<div class="col-md-2">
#Html.HiddenFor(x => Model.QuestionList[i].StnAssureQuestionId)
#Html.DropDownListFor(model => model.QuestionList[i].Score, new SelectList(Model.QuestionList[i].Scores, "ScoreId", "ScoreNum", 0), "Please Select", new { #class = "form-control" })
#Html.ValidationMessageFor(model => model.QuestionList[i].Score, "", new { #class = "text-danger" })
</div>
<div class="col-md-4">
#Html.EditorFor(x => Model.QuestionList[i].Comments, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.QuestionList[i].Comments, "", new { #class = "text-danger" })
</div>
</div>
</div>
}
}
I want to be able to display all the objects in QuestionList but group them by AreaName, which is the group key.
The current code displays the title of the first group then the questions in that group but after that all it does is display the next group name followed by the same questions then again for all the group.
It's a no brainer I'm sure but I'm still not skilled enough to spot it.
You might be able to get by with something like this, but I'd take other's advice about creating a specific view model for this view also.
#foreach (var group in Model.QuestionList.GroupBy(x => x.AreaName))
{
var questionList = group.ToList();
<h4>#group.Key</h4>
for (int i = 0; i < questionList.Count(); i++)
{
<div class="form-group">
<div class="row">
<div class="col-md-4">
#Html.DisplayFor(x => questionList[i].Question)
</div>
<div class="col-md-2">
#Html.HiddenFor(x => questionList[i].StnAssureQuestionId)
#Html.DropDownListFor(model => questionList[i].Score, new SelectList(questionList[i].Scores, "ScoreId", "ScoreNum", questionList[i].Score), "Please Select", new { #class = "form-control" })
#Html.ValidationMessageFor(model => questionList[i].Score, "", new { #class = "text-danger" })
</div>
<div class="col-md-4">
#Html.EditorFor(x => questionList[i].Comments, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => questionList[i].Comments, "", new { #class = "text-danger" })
</div>
</div>
</div>
}
}
Dot Net Fiddle Example
You should not be using complex queries in your view, and while the JamieD77's answer will solve the issue of correctly displaying the items, it will fail to bind to your model when you submit.
If you inspect the html you generating you will see that for each group you have inputs such as
<input type="hidden" name="questionList[0].StnAssureQuestionId" ... />
<input type="hidden" name="questionList[1].StnAssureQuestionId" ... />
but the DefaultModelBinder requires collection indexers to start at zero and be consecutive so when binding, it will correctly bind the inputs in the first group, but ignore those in all other groups because the indexers starts back at zero.
As always start with view models to represent waht you want to display/edit (What is ViewModel in MVC?). In this case I'm assuming the SelectList options associated with Score are common across all Questions
public class QuestionnaireVM
{
public IEnumerable<QuestionGroupVM> Groups { get; set; }
public SelectList Scores { get; set; }
}
public class QuestionGroupVM
{
public string Name { get; set; }
public IEnumerable<QuestionVM> Questions { get; set; }
}
public class QuestionVM
{
public int ID { get; set; }
public string Question { get; set; }
public int Score { get; set; }
public string Comments { get; set; }
}
While you could use nested loops in the view
for (int i = 0; i < Model.Groups.Count; i++)
{
<h4>Model.Groups[i].Name</h4>
for (int j = 0; j < Model.Groups[i].Count; j++)
{
#Html.DisplayFor(m => m.Groups[i].Questions[j].Question)
a better solution is to use EditorTemplates which give you a reusable component (and you would not have to change the collection properties to IList<T>). Note I have omitted <div> elements to keep it simple.
In /Views/Shared/EditorTemplates/QuestionVM.cshtml
#model QuestionVM
#Html.DisplayFor(m => m.Question)
#Html.HiddenFor(m => m.ID)
#Html.DropDownListFor(m => m.Score, (SelectList)ViewData["scores"], "Please Select", new { #class = "form-control" })
#Html.ValidationMessageFor(m => m.Score, "", new { #class = "text-danger" })
#Html.EditorFor(m => m.Comments, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(m => m.Comments, "", new { #class = "text-danger" })
In /Views/Shared/EditorTemplates/QuestionGroupVM.cshtml
#model QuestionGroupVM
<h4>#Html.DisplayFor(m => m.Name)</h4>
#Html.EditorFor(m => m.Questions, new { scores = ViewData["scores"] })
and the main view would be
#model QuestionnaireVM
#using (Html.BeginForm())
{
#Html.EditorFor(m => m.Groups, new { scores = Model.Scores })
<input type="submit" value="Save" />
}
Then in the get method, project your data model to the view model, for example
QuestionnaireVM model = new QuestionnaireVM
{
Groups = db.Questions.GroupBy(x => x.AreaName).Select(x => new QuestionGroupVM
{
Name = x.Key,
Questions = x.Select(y => new QuestionVM
{
ID = y.StnAssureQuestionId,
Question = y.Question,
Score = y.Score,
Comments = y.Comments
}
},
Scores = new SelectList(.....)
};
return View(model);
and the signature of the POST method would be
public ActionResult Edit(QuestionnaireVM model)
Side note: You do not currently have an input for the group name property which means if you needed to return the view because ModelState was invalid, you would need to run the query again, so consider adding #Html.HiddenFor(m => m.Name) to the QuestionGroupVM.cshtml template (and of course if you do retur the view, you also need to reassign the SelectList property.
When you displaying your questions you should work with your group object (grouped collection) but not with the initial collection.
I mean you should change your
Model.QuestionList[i]
To
group.Select(x => x.QuestionList)[i]
Anyway your solution really messy It's better to do such grouping on server side.
Please consider that a View should be completely agnostic to the business logic implemented by the service layer. View is just a dummy presentation mechanism which grabs data served by a Controller through a ViewModel and displays the data. It is the basic of the MVC architecture and my strong recommendation is that following the architecture is itself a very good reason to go the right way.
That being said the view you have is getting messy. Consider reconstructing it as something like this:
public class QuestionDataViewModel
{
public List<QuestionData> Data { get; set; }
}
public class QuestionData
{
public string AreaName { get; set; }
public List<Question> QuestionList { get; set; }
}
public class Question
{
public int StnAssureQuestionId { get; set; }
public int Score { get; set; }
public IEnumerable<SelectListItem> Scores { get; set; }
public List<Comment> Comments { get; set; }
}
Construct this server side and just render through simple razor foreach loops. This will not only benefit you with cleaner code but will also help avoid the model binding pain you are about to run into when you post the form back with your current implementation.
I am currently working on a project to model a bikestore. In my 'Order' object, I have a lis object for the Bike items on the order. How would I add bikes to this list? I.E I want to display a list of availiable bikes in the Create view, an add one or more of them to the order.
My Controller:
public ActionResult Create()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "OrderNumber,CustomerName,OrderDate,PickupDate,TotalCost,PaymentMethod")] Order order)
{
if (ModelState.IsValid)
{
db.Orders.Add(order);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(order);
}
My Inventory model
public class Inventory
{
public int Id { get; set; }
public string SerialNumber { get; set; }
public virtual Store Store { get; set; }
public int? StoreId { get; set; }
public string Model { get; set; }
public string Description { get; set; }
public Decimal InventoryCost { get; set; }
public Decimal RecSalePrice { get; set; }
public Decimal SalePrice { get; set; }
public string PaymentMethod { get; set; }
public virtual BikeCategory Category { get; set; }
public int? CategoryId { get; set; }
}
My Order model:
namespace BikeStore.Models
{
public class Order
{
public Order()
{
OrderedItems = new List<Inventory>();
}
public string CustomerName { get; set; } //FROM CONTROLLER User.Identity.Name
public virtual List<Inventory> OrderedItems { get; set; }
[Key, DatabaseGenerated(System.ComponentModel.DataAnnotations.Schema.DatabaseGeneratedOption.Identity)]
public int OrderNumber { get; set; }
In the create view for orders:
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>Order</h4>
<hr />
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
<div class="form-group">
#Html.LabelFor(model => model.CustomerName, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.CustomerName, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.CustomerName, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.OrderDate, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.OrderDate, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.OrderDate, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.PickupDate, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.PickupDate, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.PickupDate, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.TotalCost, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.TotalCost, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.TotalCost, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.PaymentMethod, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.PaymentMethod, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.PaymentMethod, "", new { #class = "text-danger" })
</div>
</div>
<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>
#Html.ActionLink("Back to List", "Index")
</div>
Start by creating view models to represent what you want to display/edit in the view (add display and validation attributes as appropriate)
public class InventoryVM
{
public int ID { get; set; }
public string Name { get; set; }
public bool IsSelected { get; set; }
}
public class OrderVM
{
public string PaymentMethod { get; set; }
public List<InventoryVM> Inventory { get; set; }
}
Note that CustomerName, OrderDate and Total are not appropriate (you don't want a user editing these - they should be set in the POST method immediately before saving the order). Not sure what PickupDate represents but if its the actual date then that's not appropriate either (it would be set separately when the order is collected). I would also suggest that PaymentMethod be an enum or collection of PaymentType's and that you and use a dropdownlist in the view for selection.
Then the GET method would be
public ActionResult Create()
{
// Get all available bikes, for example
var inventory = db.Inventory;
OrderVM model = new OrderVM
{
Inventory = inventory.Select(i => new
{
ID = i.ID,
Name = i.Model // modify this to suit what you want to display in the view
}).ToList()
};
return View(model);
}
And in the view
#model yourAssembly.OrderVM
#using (Html.BeginForm())
{
for(int i = 0; i < Model.Inventory.Count; i++)
{
#Html.HiddenFor(m => m.Inventory[i].ID)
#Html.CheckBoxFor(m => m.Inventory[i].IsSelected)
#Html.LabelFor(m => m.Inventory[i].IsSelected, Model.Inventory[i].Name)
}
#Html.TextBoxFor(m => m.PayentMethod)
<input type="submit" value="Create" />
}
And the POST method would be
public ActionResult Create(OrderVM model)
{
// Initialize a new Order and map properties from view model
var order = new Order
{
CustomerName = User.Identity.Name,
OrderDate = DateTime.Now,
....
PaymentMethod = model.PaymentMethod
}
// Save the order so that you now have its `ID`
IEnumerable<int> selectedItems = model.Inventory.Where(i => i.IsSelected).Select(i => i.ID);
foreach(var item in selectedItems)
{
// You have not shown the model for this so just guessing
var orderItem = new OrderItem{ OrderID = order.Id, InventoryId = item };
db.OrderItems.Add(orderItem);
}
db.SaveChanges();
}
Side notes:
If you want to be able to allow users to select more that one of any
item, the you could change bool IsSelected to say int Quantity
If you you want to display additional information about the items,
say Description and Cost you can include additional properties
in the InventoryVM view model and display them with
#Html.DisplayFor(m => m.Inventory[i].Description)
If you want to display a total cost for all selected items in the
view, you will need to use javascript/jquery
If ModelState could be invalid, you will need to repopulate the
properties of InventoryVM before you return the view (as shown
only the ID and IsSelected properties post back) or include
hidden inputs for the other properties in the view
Having some trouble understanding how to create and edit a collection of strings using a form. I've tried using EditorFor but it seems to have no luck and instead puts the following text into the form. I'm trying to edit the collection "Keywords".
System.Collections.Generic.HashSet`1[MVCModuleStarter.Models.Module]System.Collections.Generic.HashSet`1[MVCModuleStarter.Models.Module]
This is the Html I'm using the EditorFor in with a working EditorFor being used on a string for reference.
<div class="form-group">
#Html.LabelFor(model => model.Category, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.Category, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.Category, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.Keywords, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.Keywords, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.Keywords, "", new { #class = "text-danger" })
</div>
</div>
This is the Edit method inside my controller;
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include = "ModuleId,ModuleTitle,ModuleLeader,ModuleDescription,ImageURL,Category,Keywords")] Module module)
{
if (ModelState.IsValid)
{
int moduleId = module.ModuleId;
repository.UpdateModule(module);
repository.Save();
return RedirectToAction("Details", new { Id = moduleId });
}
return View(module);
}
This is the Model for reference;
[Required, StringLength(20), Display(Name = "Category")]
public string Category { get; set; }
public virtual ICollection<Keyword> Keywords { get; set; }
Model for Keyword
public class Keyword
{
[Key, Display(Name = "ID")]
public int KeywordId { get; set; }
[Required, StringLength(100), Display(Name = "Keyword")]
public string KeywordTerm { get; set; }
public virtual ICollection<Module> Modules { get; set; }
}
}
Any help would be amazing, still new to this! Thanks!
You need to create an EditorTemplate for Keyword, for example
In /Views/Shared/EditorTemplates/Keyword.cshtml (add divs, class names etc as required)
#model Keyword
#Html.HiddenFor(m => m.KeywordId)
#Html.LabelFor(m => m.KeywordTerm)
#Html.TextBoxFor(m => m.KeywordTerm)
#Html.ValidationMessageFor(m => m.KeywordTerm)
Then in the main view
Html.EditorFor(m=> m.Keywords)
Note I have omitted the collection property Modules, but if you also want to edit them, the add another EditorTemplate for Modules
Alternatively you can use a for loop in the main view. This would mean the collection need to be IList<T>
for(int i = 0; i < Model.Keywords.Count, i++)
{
#Html.HiddenFor(m => m.Keywords[i].KeywordId)
// other properties of Keyword
for (int j = 0; j < Model.Keywords[i].Modules.Count; j++)
{
#Html.TextBoxFor(m => m.Keywords[i].Modules[j].SomeProperty)
// other properties of Module
}
}