Using Entity Framework with a many-to-many relationship, I'm confused how to update that relationship from an ASP.NET MVC controller where the model is bound.
For example, a blog: where posts have many tags, and tags have many posts.
Posts controller edit action fails to lazy load tags entities:
public class PostsController : Controller
{
[Route("posts/edit/{id}")]
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Edit([Bind(Include = "Id,Title,Tags")] Post post)
{
// Post tags is null
Post.tags.ToList();
}
}
Above, the HTTP Post binds properties to the model; however, the Post.tags relationship is null.
There's no way for me to query, .Include(p => p.Tags), or Attach() the post to retrieve the related tag entities using this [Bind()].
On the view side, I'm using a tokenizer and passing formdata - I'm not using a MVC list component. So, the issue is not binding the view formdata - the issue is that the .tags property is not lazy loading the entities.
This relationship is functional - from the Razor cshtml view I am able to traverse the collection and view children tags.
From the Post View, I can view tags (this works)
#foreach (var tag in Model.Tags) {
}
From the Tag View, I can view posts (this works)
#foreach (var post in Model.Posts) {
}
Likewise on create action from the Posts controller, I can create new tag entities and persist their relationship.
public class PostsController : Controller
{
[Route("posts/create")]
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Create([Bind(Include = "Title,Content")] Post post)
{
string[] tagNames = this.Request.Form["TagNames"].Split(',').Select(tag => tag.Trim()).ToArray();
post.Tags = new HashSet<Tag>();
Tag tag = new Tag();
post.Tags.Add(tag);
if (ModelState.IsValid)
{
db.posts.Add(post);
await db.SaveChangesAsync();
return RedirectToAction("Admin");
}
return View(post);
}
These relationships work everywhere except for this edit HTTP Post. For reference, they are defined as:
public class Post
{
public virtual ICollection<Tag> Tags { get; set; }
}
public class Tag
{
public virtual ICollection<Post> Posts { get; set; }
}
Only on HTTP Post of edit action can I not access tags of posts.
How can I load related tags to alter that collection?
Your problem has nothing to do with Entity framework. It is basically an issue when you post a model/viewmodel with a collection property from your form, the collection property becomes null.
You can solve this by using EditorTemplates.
It looks like you are using the entity classes generated by Entity framework in your views. Generally this is not a good idea because now your UI layer is tightly coupled to EF entities. What if tomorrow you want to change your data access code implemenation form EF to something else for any reasons ?
So Let's create some viewmodels to be used in the UI layer. Viewmodels are simple POCO classes. The viewmodels will have properties which the view absolutely need. Do not just copy all your entity class properties and paste in your viewmodels.
public class PostViewModel
{
public int Id { set; get; }
public string Title { set; get; }
public List<PostTagViewModel> Tags { set; get; }
}
public class PostTagViewModel
{
public int Id { set; get; }
public string TagName { set; get; }
public bool IsSelected { set; get; }
}
Now in your GET action, you will create an object of your PostViewModel class, Initialize the Tags collection and send to the view.
public ActionResult Create()
{
var v =new PostViewModel();
v.Tags = GetTags();
return View(v);
}
private List<PostTagViewModel> GetTags()
{
var db = new YourDbContext();
return db.Tags.Select(x=> new PostTagViewModel { Id=x.Id, TagName=x.Name})
.ToList();
}
Now, Let's create an editor template. Go to the ~/Views/YourControllerName directory and create a sub directory called EditorTemplates. Create a new view there with the name PostTagViewModel.cshtml.
Add the below code to the new file.
#model YourNamespaceHere.PostTagViewModel
<div>
#Model.TagName
#Html.CheckBoxFor(s=>s.IsSelected)
#Html.HiddenFor(s=>s.Id)
</div>
Now, in our main view (create.cshtml) which is strongly typed to PostViewModel, we will call Html.EditorFor helper method to use the editor template.
#model YourNamespaceHere.PostViewModel
#using (Html.BeginForm())
{
<label>Post title</label>
#Html.TextBoxFor(s=>s.Title)
<h3>Select tags</h3>
#Html.EditorFor(s=>s.Tags)
<input type="submit"/>
}
Now in your HttpPost action method, you can inspect the posted model for Tags collection.
[HttpPost]
public ActionResult Create(PostViewModel model)
{
if (ModelState.IsValid)
{
// Put a break point here and inspect model.
foreach (var tag in model.Tags)
{
if (tag.IsSelected)
{
// Tag was checked from UI.Save it
}
}
// to do : Save Post,Tags and then redirect.
}
model.Tags = GetTags(); //reload tags again
return View(model);
}
So since our HttpPost action's parameter is an object of PostViewModel, we need to map it to your Entity classes to save it using entity framework.
var post= new Post { Title = model.Title };
foreach(var t in model.Tags)
{
if(t.IsSelected)
{
var t = dbContext.Tags.FirstOrDefault(s=>s.Id==t.Id);
if(t!=null)
{
post.Tags.Add(t);
}
}
}
dbContext.Posts.Add(post);
await dbContext.SaveChangesAsync();
Your create method that missing an Id parametres of post that you want to insert tags.
You have to declare Post Id at the begining of the method.
[Route("posts/create")]
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Create([Bind(Include = "Title,Content")] Post post)
{
Post nPost= db.Posts.FirstOrDefault(x => x.Id == post.Id);
nPost.Id= post.Id;
nPost.Title= post.Title;
string[] tagNames = this.Request.Form["TagNames"].Split(',').Select(tag => tag.Trim()).ToArray();
foreach(string item in tagNames)
{
//First check if you got tag in Tags table
Tag tg= db.Tags.FirstOrDefault(x => x.Name.ToLower() == item.ToLower().Trim());
// If no row than create one.
if (tg== null)
{
tg= new Tag();
tg.Name= item;
db.Tags.Add(tg);
await db.SaveChanges();
}
// Now adding those tags to Post Tags.
if (nPost.Tags.FirstOrDefault(x => x.Id == tg.Id) == null)
{
nPost.Tags.Add(etk);
await db.SaveChanges();
}
}
if (ModelState.IsValid)
{
await db.SaveChangesAsync();
return RedirectToAction("Admin");
}
return View(post);
}
That's it.
Related
My task is to upload file.
class FileUploadController : Controller {
public ActionResult Index(HttpPostedFileBase postedFile) {
// When and how to validate it and return appropriate view and model data
// How to store the file in database and appropriately return view and model data
}
}
Validation requires to check if filename already exists(database access) and if file extension(database access) is supported.
So far I architected it like this:
class FileUploadController : Controller {
public ActionResult Index(HttpPostedFileBase postedFile) {
FileUploadModel model=new FileUploadModel();
model.UploadedFile = postedFile;
FileUploadService service = new FileUploadService();
bool valid = service.Validate(postedFile);
if (valid) {
FileUploadViewModel viewModel = service.Save(postedFile);
return View("some_view", viewModel);
}
else {
return View("some_view", service.ViewModel);
}
}
}
public class FileUploadModel
{
public HttpPostedFileBase UploadedFile { get; set; }
}
class FileUploadViewModel {
public ModelState ModelState;
public String Filename;
}
public class FileUploadService
{
private FileUploadViewModel viewModel = new FileUploadViewModel();
public FileUploadViewModel Save(FileUploadModel fileUploadModel)
{
// here i will just save it to the database
// and return viewModel with valid state
}
public bool Validate(FileUploadModel fileUploadModel)
{
// I do the filename, size, etc validation here together with database validation if the file exists and appropriately attach errors to viewModel.ModelState so views can render the error
}
}
As you can see my validate method populates viewModel.ModelState and my Save method returns new FileUploadViewModel. I really can't make up my mind how to design this so it can grow.
My questions are:
- If suddenly update action is added and my service serve update method, I will need to return different data as ViewModel and the validation would be different, should I create new ViewModel class and new Validation..?
- Does my validation occur at valid place?
Obviously there are a number of ways to do this, but I thought I'd ask for a little feedback on benefits and drawbacks of the approaches.
First of all, the NerdDinner tutorial's Edit Action is in the form (say Form A):
[HttpPost]
public ActionResult Edit(int id, FormCollection collection) {
It seems to me that if you shape your ViewModels well to match your views, that the approach Form B:
[HttpPost]
public ActionResult Edit(MyViewModel mvm) {
just seems like a better, cleaner approach. I then just map the VM properties to the Model properties and save. However, if this ViewModel has other entities embedded in it that are initialized via the constructor (for example in the nerddinner tutorial), then this edit action fails if there is no default constructor and you'd have to use the first approach.
So, the first question is do you agree that generally Form B is usually better? Are there drawbacks?
Secondly, it seems then if Form B is used, the decorator type validation would need to be in the ViewModel. Are there advantages of embedding entities in ViewModels and keeping the validation at the entity level only?
This is a pretty general SO question.
the first question is do you agree that generally Form B is usually better?
The only time I do not use Form B is when I upload files. Otherwise, I don't believe anyone should ever need to use Form A. The reason I think people use Form A is a lack of understanding of the abilities of ASP.Net's version of MVC.
Secondly, it seems then if Form B is used, the decorator type validation would need to be in the ViewModel.
Sort of / it Depends. I'll give you an example:
public IValidateUserName
{
[Required]
string UserName { get; set; }
}
public UserModel
{
string UserName { get; set; }
}
[MetadataType(typeof(IValidateUserName))]
public UserValiationModel : UserModel
{
}
The validation decorator is in an interface. I'm using the MetadataType on a derived class to validate the derived type. I personally like this practice because it allows reusable validation and the MetadataType/Validation is NOT part of the ASP.NET core functionality, so it can be used outside of ASP.Net (MVC) application.
Are there advantages of embedding entities in ViewModels ..
Yes, I do my absolute best to never pass a basic model to the view. This is an example of what I don't do:
public class person { public Color FavoriteColor { get; set; } }
ActionResult Details()
{
Person model = new Person();
return this.View(model);
}
What happens when you want to pass more data to your view (for partials or layout data)? That information is not Person relevant most of the time so adding it to the Person model makes no sense. Instead, my models typically look like:
public class DetailsPersonViewModel()
{
public Person Person { get; set; }
}
public ActionResult Details()
{
DetailsPersonViewModel model = new DetailsPersonViewModel();
model.Person = new Person();
return this.View(model);
}
Now I can add required data the DetailsPersonViewModel that view needs beyond what a Person knows. For example, lets say this is going to display a for with all the colors for the Person to pick a favorite. All the possible colors aren't part of a person and shouldn't be part of the person Model, so I'd add them to the DetailPersonViewModel.
public class DetailsPersonViewModel()
{
public Person Person { get; set; }
public IEnumerable<Color> Colors { get; set; }
}
.. and keeping the validation at the entity level only?
System.ComponentModel.DataAnnotations weren't designed to validate properties' properties, so doing something like:
public class DetailsPersonViewModel()
{
[Required(property="FavoriteColor")]
public Person Person { get; set; }
}
Doesn't exist and doesn't make sense. Why ViewModel shouldn't contain the validation for the entity that needs validation.
this edit action fails if there is no default constructor and you'd have to use the first approach.
Correct, but why would a ViewModel or a Entity in a ViewModel not have a parameterless constructor? Sounds like a bad design and even if there is some requirement for this, it's easily solved by ModelBinding. Here's an example:
// Lets say that this person class requires
// a Guid for a constructor for some reason
public class Person
{
public Person(Guid id){ }
public FirstName { get; set; }
}
public class PersonEditViewModel
{
public Person Person { get; set; }
}
public ActionResult Edit()
{
PersonEditViewModel model = new PersonEditViewModel();
model.Person = new Person(guidFromSomeWhere);
return this.View(PersonEditViewModel);
}
//View
#Html.EditFor(m => m.Person.FirstName)
//Generated Html
<input type="Text" name="Person.FirstName" />
Now we have a form that a user can enter a new first name. How do we get back the values in this constructor? Simple, the ModelBinder does NOT care what model it is binding to, it just binds HTTP values to matching class properties.
[MetadataType(typeof(IPersonValidation))]
public class UpdatePerson
{
public FirstName { get; set; }
}
public class PersonUpdateViewModel
{
public UpdatePerson Person { get; set; }
}
[HttpPost]
public ActionResult Edit(PersonUpdateViewModel model)
{
// the model contains a .Person with a .FirstName of the input Text box
// the ModelBinder is simply populating the parameter with the values
// pass via Query, Forms, etc
// Validate Model
// AutoMap it or or whatever
// return a view
}
I have not yet taken a look at the NerDinner project, however, I generally try to avoid having a ViewModel in the POST of an action and instead, only have the elements of the "form" submitted.
For instance, if the ViewModel has a Dictionary that is used in some kind of dropdown, the entire dropdown will not be submitted, only the selected value.
My general approach is:
[HttpGet]
public ActionResult Edit(int id)
{
var form = _service.GetForm(id);
var pageViewModel = BuildViewModel(form);
return View(pageViewModel);
}
[HttpPost]
public ActionResult Edit(int id, MyCustomForm form)
{
var isSuccess = _service.ProcessForm(id);
if(isSuccess){
//redirect
}
//There was an error. Show the form again, but preserve the input values
var pageViewModel = BuildViewModel(form);
return View(pageViewModel);
}
private MyViewModel BuildViewModel(MyCustomForm form)
{
var viewModel = new MyViewModel();
viewModel.Form = form;
viewModel.StateList = _service.GetStateList();
return viewModel;
}
A lot of my plain content is in the database, accessed by a custom CMS. Around the application I display simple "thank you" messages, etc. which consist of a controller action (simplified):
public ActionResult DetailsUpdated()
{
return View();
}
and my view:
#Html.GetContent("DetailsUpdated")
I have quite a few of these and its quite annoying having a lot of view files with one-liners in. I want to be able to return that content as a View, I can do return ContentResult(ContentRepository.GetContent("KEY")); but this returns as plain-text and there is no master view rendered.
So, basically, grab the content from the DB via ContentRepository.GetContent("KEY") (returns a string) and inject it into a master view, where RenderBody() is called. I'd like to have a custom ActionResult so I can just do:
public ActionResult DetailsUpdated()
{
return DbContentResult();
}
and then the DbContentResult ActionResult will find the content key relative to the action and controller name, go to the database and retrieve the content and display it within its master view, no physical file view needed. Is this possible?
You may have one view file and refer to that view file from several actions:
public class FooBarController : Controller {
public ViewResult Foo() {
return View("FooView", ContentRepository.GetContent("KEY"));
}
}
In this case, you will be able to render the view whose path is ~/Views/Shared/FooView.cshtml (unless you override the default convention of course).
Edit:
As you indicated, you can make a custom ViewResult which does this for you:
public class DbContentResult : ViewResult {
public DbContentResult() {
this.ViewName = "FooView";
this.ViewData.Model = "Foo Model";
}
}
Usage:
public ActionResult Index() {
return new DbContentResult();
}
Or even better, write an extension method for Controller class which integrates with DbContentResult:
public static class ControllerExtensions {
public static ViewResult DbContentResult(this Controller controller) {
return new DbContentResult();
}
}
Usage:
public ActionResult Index() {
return this.DbContentResult();
}
for more detail about creating custom actionresult go here:-
http://www.professionals-helpdesk.com/2012/06/create-custom-actionresult-in-mvc-3.html
I want to update a "Post" and change relationships with "Categories" that already created before. Post entity has ICollection of Categories. But categories are not changed. It seems, that EF does not track entity relations. By the way I have no problem with creating of new Posts with assigning of Categories.
There are two models:
public class Post
{
public virtual int PostId { get; set; }
...
public virtual ICollection<Category> Categories { get; set; }
}
public class Category
{
public virtual int CategoryId { get; set; }
...
public virtual ICollection<Post> Posts { get; set; }
}
The Add controller, that works as expected:
public ActionResult Create(Post model)
{
var c = Request.Form["CategoryID"].Split(',');
model.Categories = c.Select ... .ToList(); //here I assign relationships with attached objects
_repository.Add(model);
_repository.SaveChanges();
...
}
Repository Add method:
T IRepository.Add<T>(T entity)
{
return Set<T>().Add(entity);
}
The Edit controller does not save changed categories, only post props.
public ActionResult Edit(Post model)
{
var c = Request.Form["CategoryID"].Split(',');
model.Categories = c.Select ... .ToList(); //here I update relationships with attached objects
_repository.Attach(model);
_repository.SaveChanges();
...
}
Repository Edit method:
T IRepository.Attach<T>(T entity)
{
var entry = Entry(entity);
entry.State = System.Data.EntityState.Modified;
return entity;
}
Am I doing something wrong?
Thanks in advance
Solution:
public ActionResult Edit(Post model)
{
model = _repository.Attach(model);
var post = _repository.Posts.Include(p => p.Categories).Single(s => s.PostId == model.PostId);
post.Categories.Clear();
model.Categories = GetCategories();
_repository.SaveChanges();
}
First Attach the object (EntityState.Modified)
Query the object with Include or other method for Loading Related Entities
Clear the existent relations. I don like this, but I cannot find another way
Assign new relations from the view and SaveChanges.
Entity Framework won't track relationship changes this way. It only tracks the states of objects, so the proper way would be to load the "Post" that you want with all categories and then to modify the loaded collection - this way changes are going to be reflected in all objects' states.
Manipulate category collection i.e. (add, remove, edit) in post class. If you are using same DbContext then changes will be tracked. It should be worked.
add a category
_post.Categories.Add(category1);
delete category
_post.Categories.Remove(category1);
edit category
_post.Categories[0].Name = "TEST Name";
udapte a object
string ImgID = CompCert.CertID.ToString() + "ComID" + CompId + ext;
CompCert.CertImageFile = ImgID;
db.ObjectStateManager.ChangeObjectState(CompCert, EntityState.Modified);
db.SaveChanges();
There are a lot of articles devoted to working with data in MVC, and nothing about MVC 2.
So my question is: what is the proper way to handle POST-query and validate it.
Assume we have 2 actions. Both of them operates over the same entity, but each action has its own separated set of object properties that should be bound in automatic manner. For example:
Action "A" should bind only "Name" property of object, taken from POST-request
Action "B" should bind only "Date" property of object, taken from POST-request
As far as I understand - we cannot use Bind attribute in this case.
So - what are the best practices in MVC2 to handle POST-data and probably validate it?
UPD:
After Actions performed - additional logic will be applied to the objects so they become valid and ready to store in persistent layer. For action "A" - it will be setting up Date to current date.
I personally don't like using domain model classes as my view model. I find it causes problems with validation, formatting, and generally feels wrong. In fact, I'd not actually use a DateTime property on my view model at all (I'd format it as a string in my controller).
I would use two seperate view models, each with validation attributes, exposed as properties of your primary view model:
NOTE: I've left how to combining posted view-models with the main view model as an exercise for you, since there's several ways of approaching it
public class ActionAViewModel
{
[Required(ErrorMessage="Please enter your name")]
public string Name { get; set; }
}
public class ActionBViewModel
{
[Required(ErrorMessage="Please enter your date")]
// You could use a regex or custom attribute to do date validation,
// allowing you to have a custom error message for badly formatted
// dates
public string Date { get; set; }
}
public class PageViewModel
{
public ActionAViewModel ActionA { get; set; }
public ActionBViewModel ActionB { get; set; }
}
public class PageController
{
public ActionResult Index()
{
var viewModel = new PageViewModel
{
ActionA = new ActionAViewModel { Name = "Test" }
ActionB = new ActionBViewModel { Date = DateTime.Today.ToString(); }
};
return View(viewModel);
}
// The [Bind] prefix is there for when you use
// <%= Html.TextBoxFor(x => x.ActionA.Name) %>
public ActionResult ActionA(
[Bind(Prefix="ActionA")] ActionAViewModel viewModel)
{
if (ModelState.IsValid)
{
// Load model, update the Name, and commit the change
}
else
{
// Display Index with viewModel
// and default ActionBViewModel
}
}
public ActionResult ActionB(
[Bind(Prefix="ActionB")] ActionBViewModel viewModel)
{
if (ModelState.IsValid)
{
// Load model, update the Date, and commit the change
}
else
{
// Display Index with viewModel
// and default ActionAViewModel
}
}
}
One possible way to handle POST data and add validation, is with a custom model binder.
Here is a small sample of what i used recently to add custom validation to POST-form data :
public class Customer
{
public string Name { get; set; }
public DateTime Date { get; set; }
}
public class PageController : Controller
{
[HttpPost]
public ActionResult ActionA(Customer customer)
{
if(ModelState.IsValid) {
//do something with the customer
}
}
[HttpPost]
public ActionResult ActionB(Customer customer)
{
if(ModelState.IsValid) {
//do something with the customer
}
}
}
A CustomerModelBinder will be something like that:
public class CustomerModelBinder : DefaultModelBinder
{
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor)
{
if (propertyDescriptor.Name == "Name") //or date or whatever else you want
{
//Access your Name property with valueprovider and do some magic before you bind it to the model.
//To add validation errors do (simple stuff)
if(string.IsNullOrEmpty(bindingContext.ValueProvider.GetValue("Name").AttemptedValue))
bindingContext.ModelState.AddModelError("Name", "Please enter a valid name");
//Any complex validation
}
else
{
//call the usual binder otherwise. I noticed that in this way you can use DataAnnotations as well.
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
}
}
and in the global.asax put
ModelBinders.Binders.Add(typeof(Customer), new CustomerModelBinder());
If you want not to bind Name property (just Date) when you call ActionB, then just make one more custom Model Binder and in the "if" statement, put to return the null, or the already existing value, or whatever you want. Then in the controller put:
[HttpPost]
public ActionResult([ModelBinder(typeof(CustomerAModelBinder))] Customer customer)
[HttpPost]
public ActionResult([ModelBinder(typeof(CustomerBModelBinder))] Customer customer)
Where customerAmodelbinder will bind only name and customerBmodelbinder will bind only date.
This is the easiest way i have found, to validate model binding, and i have achieved some very cool results with complex view models. I bet there is something out there that i have missed, and maybe a more expert can answer.
Hope i got your question right...:)