I am trying to find the best way to use MVC for models which are only partially edited.
Below is an simple example.
Model
using System.ComponentModel.DataAnnotations;
public class SimpleModel
{
public int Id { get; set; }
public string Parent { get; set; }
[Required]
public string Name { get; set; }
}
View
using System.ComponentModel.DataAnnotations;
public class SimpleModel
{
public int Id { get; set; }
public string Parent { get; set; }
[Required]
public string Name { get; set; }
}
Controller
using System.Web.Mvc;
public class SimpleController : Controller
{
public ActionResult Edit(int id)
{ return View(Get(id)); }
[HttpPost]
public ActionResult Edit(int id, SimpleModel model)
{
if (model.Name.StartsWith("Child")) //Some test that is not done client-side.
{
Save(model);
//Get the saved data freshly.
//model = Get(id);
}
else
{
ModelState.AddModelError("", "Name should start with 'Child'");
}
//Is this the way to set the Parent property?
//var savedModel = Get(id);
//model.Parent = savedModel.Parent;
return View(model);
}
//Mock a database.
SimpleModel savedModel;
private void Save(SimpleModel model)
{ savedModel = new SimpleModel() { Id = model.Id, Name = model.Name }; }
private SimpleModel Get(int id)
{
if (savedModel == null)
{ return new SimpleModel() { Id = id, Parent = "Father", Name = "Child " + id.ToString() }; }
else
{ return new SimpleModel() { Id = savedModel.Id, Parent = "Father", Name = savedModel.Name }; }
}
}
The Name field is editable. The Parent field is only for reference and should not be updated. Therefore, it is rendered using DisplayFor.
Upon post, I receive a model with property Parent set to null. That's no problem as it will not be saved. However, when I simply return the received model to the view, the Parent field will no longer be displayed. When the model is valid, I can easily get it again from the database and thus get the Parent field's value back.
When the model is not valid, I would like to allow the user to correct input and attempt to save once more. There, I the received model's values that are input should be used, but the displayed values should be shown as well.
In reality, there are many more fields to be shown for reference, most often from different database entities than the one that is being edited.
I have seen suggestions to pass the fields as hidden fields in the view, but I feel very reluctant to read data from the client that should not be updated.
Is there a more elegant way to do this than copying these values into the model manually or passing them as hidden fields?
What about giving those un-editable properties to another model and let it take care of those properties?
View Model
public class PersonViewModel
{
public int Id { get; set; }
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
public PersonDetailsModel DetailsModel { get; set; }
}
Details Model
public class PersonDetailsModel
{
public string Mother { get; set; }
public string Father { get; set; }
public PersonDetailsModel() { }
public PersonDetailsModel(int personId)
{
// pull required model data from databases
var db = DBParentContext;
Mother = db.Parent.Where(m => m.ChildId == personId)
Father = db.Parent.Where(m => m.ChildId == personId)
}
}
Controller
public class PersonController : Controller
{
[HttpPost]
public ActionResult Edit(PersonViewModel viewModel)
{
viewModel.DetailsModel = new PersonDetailsModel(viewModel.Id)
if (ModelState.IsValid) {
// ~
}
return View(viewModel)
}
}
View
#model PersonViewModel
#Html.DisplayFor(m => m.DetailsModel.Mother)
#Html.DisplayFor(m => m.DetailsModel.Father)
#Html.TextBoxFor(m => m.FirstName)
#Html.TextBoxFor(m => m.LastName)
Since details like your Mother are un-editable then they're not really part of the "Edit" model, so I'd box like that away and try to let something else take care of them.
If you aren't going to update the Parent field, then it really doesn't matter if it's a hidden or not, since you won't update it on post.
I would use the hidden in this case, just make sure not to update that field.
Related
I'd like to know a good practice to update a many to many relationship when submit form.
I got these two entities and I use the default many to many relationship from EF core 5:
public class BlogEntry
{
public int Id { get; set; }
[Required]
[MaxLength(200)]
public string Title { get; set; }
[Required]
public string EntryText { get; set; }
[NotMapped]
public IEnumerable<string> CategoriesToPublish { get; set; }
public ICollection<Category> Categories { get; set; }
}
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<BlogEntry> BlogEntries { get; set; }
}
context:
public DbSet<BlogEntry> BlogEntries { get; set; }
public DbSet<Category> Categories { get; set; }
And I have a form witha multiselect field to represent this relationship. See Image
form
I'm not using the relation property on the form(maube I should, but I don't know), I have another property to convert the relationship into a list of strings called CategoriesToPublish so I can load the multiselect and retrieve the selection on post.
On the post action method, I want to iterate the this CategoriesToPublish and update all the relationships.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Publish(BlogEntry blogEntry)
{
if (ModelState.IsValid)
{
blogEntry.Categories = await _context.Categories.Where(x => x.BlogEntries.Any(x => x.Id == blogEntry.Id)).ToListAsync();
await UpdateCategories(blogEntry);
_context.Update(blogEntry);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(List));
}
return View(blogEntry);
}
But the problem that I'm facing is that the Categories relationship is not loaded on postback. And if I try to load it manually and save context, I get an error saying SqlException: Violation of PRIMARY KEY constraint 'PK_BlogEntryCategory'. Cannot insert duplicate key in object 'dbo.BlogEntryCategory'
I am not sure how to approach this problem. Any advice?
After lot of searching I found out a solution.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Publish(BlogEntry blogEntry)
{
if (ModelState.IsValid)
{
blogEntry = _context.Update(blogEntry).Entity;
blogEntry.Categories = await _context.Entry(blogEntry).Collection(u => u.Categories).Query().ToListAsync();
await UpdateCategories(blogEntry);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(List));
}
return View(blogEntry);
}
I retrieve the blogEntry after Update:
blogEntry = _context.Update(blogEntry).Entity;
At this point the Categories are still empty, but now we can load them from DB again:
blogEntry.Categories = await _context.Entry(blogEntry).Collection(u => u.Categories).Query().ToListAsync();
and boila, now we are good to iterate all the Categories change them if needed, it doesn't complain about duplicate keys.
I am new in MVC. I am working on a project where i have created a model class and also context class which is working good if i view the record in normal view.
but if i try to get the data in group by "Series_Name" and bind it into same model class it gives error. here is my code
Here is Model class and DBContextClass
[Table("tblvideo")]
public class TVSerial
{
[Key]
public Int64 Video_ID { get; set; }
public string Series_Name { get; set; }
public string Season_No { get; set; }
public string Episode_No { get; set; }
public string Episode_Name { get; set; }
public string Time_Duration { get; set; }
public string File_Url_480p { get; set; }
public string File_Url_720p { get; set; }
public string Description { get; set; }
public bool Is_Active { get; set; }
public string Image_Url_Small { get; set; }
public string Image_Url_Big { get; set; }
}
public class TvSerialContext : DbContext
{
public DbSet<TVSerial> TvSerials { get; set; }
}
Here is controller class:
public class TvSerialController : Controller
{
public ActionResult ListAllTvSerial()
{
try
{
TvSerialContext tvContext = new TvSerialContext();
List<TVSerial> tv = tvContext.TvSerials.ToList();
return View(tv);
}
catch (Exception ex)
{
return Content(ex.Message);
}
}
}
Above code works as expected, but if i am doing this :
public ActionResult ListAllSeason(string serial)
{
try
{
TvSerialContext tvContext = new TvSerialContext();
List<TVSerial> tv = tvContext.TvSerials.Where(tvs => tvs.Series_Name == serial).Distinct().ToList();
return View(tv);
}
catch (Exception ex)
{
return Content(ex.Message);
}
}
it return all rows , i just want single row from every series_name and custom field "Series_Name,Season_No,Image_Url_Big"
i don't know how to achieve this.
getting result :
Expected result:-
You could do this by creating a view model and using a .GroupBy() clause
public class TVSerialVM
{
public string SeriesName { get; set; }
public string SeasonNo { get; set; }
public string ImageUrl { get; set; }
}
and the query to project into your view model
List<TVSerialVM> model = tvContext.TvSerials.Where(t => t.Series_Name == serial)
.GroupBy(t => new { t.Series_Name, t.Season_No, t.Image_Url_Big })
.Select(t => new TVSerialVM
{
SeriesName = t.Key.Series_Name,
SeasonNo = t.Key.Season_No,
ImageUrl = t.Key.Image_Url_Big
}).ToList();
Side note: Your duplicating data in the database (the season number and the image url). You should consider moving the image urls to another table with a relationship to the season number.
The reason you are getting multiple values even though you are using distinct is the Distinct method does not know what "equal" is for TVSerial.
You can use Distinct with IEqualityComparer.
https://msdn.microsoft.com/en-us/library/vstudio/bb338049(v=vs.100).aspx
Distinct is not guaranteed to on custom objects it doesn't know what to compare. I have used this SO in the past to make my custom object work with Distinct.
Creating a distinct list of custom type in C#
We have the below model:
public class Sample
{
public int SampleId { get; set; }
public int ToTestId { get; set; }
public int Name { get; set; }
public virtual ICollection<SampleCondition> Child_SampleConditions { get; set; }
}
I am rendering them in my view as below:
#model Sample
#{
string cntlrName = ViewContext.RouteData.GetRequiredString("controller");
List<SampleCondition> sampleConditions = Model.Child_SampleConditions.ToList();
if (d2l.NullOrEmpty(sampleConditions))
{
Model.Child_SampleConditions .Add(new SampleCondition());
}
List<SampleCondition> sampleConditions = Model.Child_SampleConditions as List<SampleCondition>;
}
#Html.ValidationSummary(true)
#Html.d2_HiddenFor(O => O.SampleId)
<div class="#BOOTSTRAP.ROW">
#Html.ManyToOneFieldFor(Model, O => O.ToTestId, allowed: CrudFlag.Editable)
#Html.FieldFor(O => O.Name, allowed: CrudFlag.Editable)
#Html.FieldFor(O => sampleConditions[0].ConditionValue, allowed:CrudFlag.Editable)
</div>
Please look at the third filed in div, sampleConditions[0].ConditionValue. The html input field for this control is rendering with the name "[0].ConditionValue" instead of "Child_SampleConditions[0].ConditionValue". Thats why when I post this form, the Child_SampleConditions are not automatically binded to the model object into post method.
can somebody advise how to render ICollection input field names correctly so that they can automatically gets model binded.
NOTE: Please exclude the custom model binder options.
EDIT: I am providing some more information here.
In the ManyToOneFieldFor mehtod, the name retrieved using the below helper:
string fullName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);
It just returns, [0].ConditionValue but not Child_SampleConditions[0].ConditionValue.
Please advise..
Thanks.
Instead of :
O => sampleConditions[0].ConditionValue
write:
O => O.Child_SampleConditions[0].ConditionValue
EDIT:
Create a ViewModel with IList as ICollection cannot be indexed:
public class SampleVM
{
public int SampleId { get; set; }
public int ToTestId { get; set; }
public int Name { get; set; }
public virtual IList<SampleCondition> Child_SampleConditions { get; set; }
}
Reference link:Cannot apply indexing with [] to an expression of type ICollection
I have a MVC4 application and although I have get parameters for my DropDownList from the database, I encounter some kind of problems while posting the DropDownList value to the database. There is lots of samples for different approach, but I would like to apply a method without using an extra approach i.e. Ajax, Javascript, etc. On the other hand, I have run into "FormCollection" to pass data, but I am not sure if FormCollection is the best way in this scene. Here are some part of the view, controller and model I use:
View:
#using (Html.BeginForm("Add", "Product", FormMethod.Post,
new { enctype = "multipart/form-data" }))
{
<p>Product Type : #Html.DropDownListFor(m => m.SelectedLookupId, new SelectList(Model.Lookups.Where(x => x.LookupType == "Product Type"), "LookupID", "LookupValue"), "--- Select ---") </p>
Controller:
[HttpPost]
public ActionResult Add(Product product)
{
if (ModelState.IsValid)
{
product.ProductType = // ??? Cannot get the SelectedLookupId
...
repository.SaveProduct (product);
TempData["message"] = string.Format("{0} has been saved", product.Name);
return View("Completed");
}
else
{
//there is something wrong with the data values
return View(product);
}
}
ViewModel:
public class ProductViewModel
{
public IEnumerable<Product> Products { get; set; }
public IEnumerable<Lookup> Lookups { get; set; } //Lookup for Product Types
public int SelectedLookupId { get; set; }
public Product Product { get; set; }
}
Thanks in advance for your helps.
Your action method should be receiving the view model, not the Product itself, like so:
[HttpPost]
public ActionResult Add(ProductViewModel productViewModel)
Unless I'm confused. But I assume the view snippet you posted above is from the Add view and that view's model is of type ProductViewModel. In your action method you are returning the Add view when the model state is invalid however you are passing a Product to that view. Again I may be confused because this should give you a runtime error that the types don't match.
Thanks for reply. Actually by using ViewModel rather than View, I have managed to solve the problem. On the other hand, after some research, I have applied another effective method in order to populate Dropdownlist without needing ViewModel. Furthermore with this example, I could use multiple foreign keys on the same Lookup table as shown below. Here is an an Applicant entity having 3 foreign keys and Lookup entity related to these keys. What I wanted to achieve with this example is exactly to use a Lookup table for only several Dropdownlist parameters i.e. Gender, Yes/No, Status,... due to no needing to create a table for the several parameters (these parameters are distinguished LookupType property on Lookup table). Here is the full example (I have shorted unrelated properties for brevity) below:
Applicant Entity:
public class Applicant
{
[Key]
public int ApplicantID { get; set; }
public string Name { get; set; }
public string Surname { get; set; }
// for using "Multiple foreign keys within same table using Fluent API"
public int? HasDoneAnyProject { get; set; }
public int? IsInterestedAnyProgramme { get; set; }
public int? InterestedProgrammeId { get; set; }
public virtual Lookup PrimaryLookup { get; set; }
public virtual Lookup SecondaryLookup { get; set; }
public virtual Lookup TertiaryLookup { get; set; }
}
Lookup Entity:
public class Lookup
{
[Key]
public int LookupID { get; set; }
public string LookupType { get; set; }
public string LookupValue { get; set; }
// for using "Multiple foreign keys within same table using Fluent API"
public virtual ICollection<Applicant> PrimaryLookupFor { get; set; }
public virtual ICollection<Applicant> SecondaryLookupFor { get; set; }
public virtual ICollection<Applicant> TertiaryLookupFor { get; set; }
}
DbContext:
public class EFDbContext : DbContext
{
public DbSet<Applicant> Applicants { get; set; }
public DbSet<Lookup> Lookups { get; set; }
//for using "Multiple foreign keys within same table using Fluent API"
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Applicant>()
.HasOptional(b => b.PrimaryLookup)
.WithMany(a => a.PrimaryLookupFor)
.HasForeignKey(b => b.HasDoneAnyProject)
.WillCascadeOnDelete(false);
modelBuilder.Entity<Applicant>()
.HasOptional(b => b.SecondaryLookup)
.WithMany(a => a.SecondaryLookupFor)
.HasForeignKey(b => b.IsInterestedAnyProgramme)
.WillCascadeOnDelete(false);
modelBuilder.Entity<Applicant>()
.HasOptional(b => b.TertiaryLookup)
.WithMany(a => a.TertiaryLookupFor)
.HasForeignKey(b => b.InterestedProgrammeId)
.WillCascadeOnDelete(false);
}
}
Controller:
private void PopulateLookupsDropDownList(string lookupType, string foreignKey, object selectedLookups = null)
{
var lookupsQuery = repository.Lookups
.Select(x => x)
.Where(x => x.LookupType == lookupType)
.OrderBy(x => x.ParentLookupID).ToList();
ViewData[foreignKey] = new SelectList(lookupsQuery, "LookupID", "LookupValue", selectedLookups);
}
and for calling the Method for each of three Dropdownlist:
PopulateLookupsDropDownList("YesNo", "HasDoneAnyProject", applicant.HasDoneAnyProject);
PopulateLookupsDropDownList("YesNo", "IsInterestedAnyProgramme", applicant.IsInterestedAnyProgramme);
PopulateLookupsDropDownList("Programme", "InterestedProgrammeId", applicant.InterestedProgrammeId);
View: : Populating each of three Dropdownlist from the same Lookup table with different LookupType parameter:
<label>Has done any project before?</label>
#Html.DropDownList("HasDoneAnyProject", "---- Select ----")
<label>Are you interested in any programme?</label>
#Html.DropDownList("IsInterestedAnyProgramme", "---- Select ----")
<label>Interested programme name?</label>
#Html.DropDownList("InterestedProgrammeId", "---- Select ----")
I hope this approach will be useful for those who want to populate Dropdownlists from the same Lookup table. On the other hand, it is not only suitable for this, also can be used for populating Dropdownlists from different tables.
Regards.
I am trying to use the following ViewModel
public class ProjectViewModel
{
public Project Project { get; set; } //bulk of the information
public int SelectedGovernmentClientId { get; set; } //selected ID for the dropdown
public IEnumerable<SelectListItem> GovernmentClients { get; set; } //dropdown values
}
Here's my project class
public class Project
{
public int ID { get; set; }
public string Title { get; set; }
//omitting extra fields
public virtual GovernmentClient GovernmentClient { get; set; }
}
Here's the action that I have
[HttpPost]
public ActionResult Edit(ProjectViewModel projectViewModel)
{
if (ModelState.IsValid)
{
//i am getting the following from debugging
//projectViewModel.Project.GovernmentClient.Name is NULL
//projectViewModel.Project.GovernmentClient.ID is the correct ID from the dropdown
db.Entry(projectViewModel.Project).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(projectViewModel);
}
All the values are getting updated EXCEPT the government client. Why is this happening?
projectViewModel.Project.GovernmentClient =
db.GovernmentClients.Find(projectViewModel.SelectedGovernmentClientId);
You are retrieving the value from database and setting its state to modified. However, there is no modification at all.
After a whole day of playing around with this, I think I might've solved it
I added the following to my Project model
[ForeignKey("GovernmentClient")]
public int GovernmentClient_ID { get; set; }
To render the dropdown, I used this
#Html.DropDownListFor(model => model.Project.GovernmentClient_ID, Model.GovernmentClients)
To generate the GovernmentClients list, I used the following method
private IEnumerable<SelectListItem> GetGovernmentClientsList(int selectedItem = -1)
{
var defaultItem = Enumerable.Repeat(new SelectListItem
{
Value = "-1",
Text = " - Select a government client - ",
Selected = (selectedItem == -1)
}, count: 1);
var clients = db.GovernmentClients.ToList().Select(x => new SelectListItem
{
Value = x.ID.ToString(),
Text = x.ClientName,
Selected = (selectedItem == -1) ? false : (x.ID == selectedItem)
});
return defaultItem.Concat(clients);
}
Overall, I am happy with this, because I am not hardcoding any property names, which I know would come back and bite me.