I cannot come up with a solution to a problem that's best described verbally and with a little code. I am using VS 2013, MVC 5, and EF6 code-first; I am also using the MvcControllerWithContext scaffold, which generates a controller and views that support CRUD operations.
Simply, I have a simple model that contains a CreatedDate value:
public class WarrantyModel
{
[Key]
public int Id { get; set; }
public string Description { get; set; }
DateTime CreatedDate { get; set; }
DateTime LastModifiedDate { get; set; }
}
The included MVC scaffold uses the same model for its index, create, delete, details, and edit views. I want the CreatedDate in the 'create' view; I do not want it in the 'edit' view because I do not want its value to change when the edit view is posted back to the server and I don't want anyone to be able to tamper with the value during a form-post.
Ideally, I don't want the CreatedDate to ever get to the Edit view. I have found a few attributes I can place on the model's CreatedDate property (for example, [ScaffoldColumn(false)]) that prevent it from appearing on the Edit view, but then I'm getting binding errors on postback because the CreatedDate ends up with a value of 1/1/0001 12:00:00 AM. That's because the Edit view is not passing a value back to the controller for the CreatedDate field.
I don't want to implement a solution that requires any SQL Server changes, such as adding a trigger on the table that holds the CreatedDate value. If I wanted to do a quick-fix, I would store the CreatedDate (server-side, of course) before the Edit view is presented and then restore the CreatedDate on postback--that would let me change the 1/1/0001 date to the CreatedDate EF6 pulled from the database before rendering the view. That way, I could send CreatedDate as a hidden form field and then overwrite its value in the controller after postback, but I don't have a good strategy for storing server-side values (I don't want to use Session variable or the ViewBag).
I looked at using [Bind(Exclude="CreatedDate")], but that doesn't help.
The code in my controller's Edit post-back function looks like this:
public ActionResult Edit([Bind(Include="Id,Description,CreatedDate,LastModifiedDate")] WarrantyModel warrantymodel)
{
if (ModelState.IsValid)
{
db.Entry(warrantymodel).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(warrantymodel);
}
I thought I might be able to examine the db.Entry(warrantymodel) object within the if block above and examine at the OriginalValue for CreatedDate, but when I try to access that value (as shown next), I get an exception of type 'System.InvalidOperationException':
var originalCreatedDate = db.Entry(warrantymodel).Property("CreatedDate").OriginalValue;
If I could successfully examine the original CreatedDate value (i.e., the one that is already in the database) I could just overwrite whatever the CurrentValue is. But since the above line of code generates an exception, I don't know what else to do. (I thought about querying the database for the value but that's just silly since the database was already queried for the value before the Edit view was rendered).
Another idea I had was to change the IsModified value to false for the CreatedDate value but when I debug then I discover that it is already is set to false in my 'if' block shown earlier:
bool createdDateIsModified = db.Entry(warrantymodel).Property("CreatedDate").IsModified;
I am out of ideas on how to handle this seemingly simple problem. In summary, I do not want to pass a model field to an Edit view and I want that field (CreatedDate, in this example) to maintain its original value when the other Edit fields from the view are posted back and persisted to the database using db.SaveChanges().
Any help/thoughts would be most appreciated.
Thank you.
You should leverage ViewModels:
public class WarrantyModelCreateViewModel
{
public int Id { get; set; }
public string Description { get; set; }
DateTime CreatedDate { get; set; }
DateTime LastModifiedDate { get; set; }
}
public class WarrantyModelEditViewModel
{
public int Id { get; set; }
public string Description { get; set; }
DateTime LastModifiedDate { get; set; }
}
The intention of a ViewModel is a bit different than that of a domain model. It provides the view with just enough information it needs to render properly.
ViewModels can also retain information that doesn't pertain to your domain at all. It could hold a reference to the sorting property on a table, or a search filter. Those certainly wouldn't make sense to put on your domain model!
Now, in your controllers, you map properties from the ViewModels to your domain models and persist your changes:
public ActionResult Edit(WarrantyModelEditViewModel vm)
{
if (ModelState.IsValid)
{
var warrant = db.Warranties.Find(vm.Id);
warrant.Description = vm.Description;
warrant.LastModifiedDate = vm.LastModifiedDate;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(warrantymodel);
}
Furthermore, ViewModels are great for amalgamating data from multiple models. What if you had a details view for your warranties, but you also wanted to see all servicing done under that warranty? You could simply use a ViewModel like this:
public class WarrantyModelDetailsViewModel
{
public int Id { get; set; }
public string Description { get; set; }
DateTime CreatedDate { get; set; }
DateTime LastModifiedDate { get; set; }
List<Services> Services { get; set; }
}
ViewModels are simple, flexible, and very popular to use. Here is a good explantion of them: http://lostechies.com/jimmybogard/2009/06/30/how-we-do-mvc-view-models/
You're going to end up writing a lot of mapping code. Automapper is awesome and will do most of the heavy lifting: http://automapper.codeplex.com/
This is not an answer for the questions, but it might be critical for those who is using Bind() and facing different problems. When I was searching "why Bind() clears out all pre-existing but not-bound values", I found this:
(in the HttpPost Edit()) The scaffolder generated a Bind attribute and added the entity created by the model binder to the entity set with a Modified flag. That code is no longer recommended because the Bind attribute clears out any pre-existing data in fields not listed in the Include parameter. In the future, the MVC controller scaffolder will be updated so that it doesn't generate Bind attributes for Edit methods.
from a official page (last updated in 2015, March):
http://www.asp.net/mvc/overview/getting-started/getting-started-with-ef-using-mvc/implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application#overpost
According to the topic:
Bind is not recommended and will be removed in the future from the auto-generated codes.
TryUpdateModel() is now the official solution.
You can search "TryUpdateModel" in the topic for details.
It may solve your problem
In Model:
Use ?
public class WarrantyModel
{
[Key]
public int Id { get; set; }
public string Description { get; set; }
DateTime? CreatedDate { get; set; }
DateTime? LastModifiedDate { get; set; }
}
After form submit:
public ActionResult Edit([Bind(Include = "Id,Description,CreatedDate,LastModifiedDate")] WarrantyModel warrantymodel)
{
if (ModelState.IsValid)
{
db.Entry(warrantymodel).State = EntityState.Modified;
db.Entry(warrantymodel).Property("CreatedDate").IsModified=false
db.SaveChanges();
return RedirectToAction("Index");
}
return View(warrantymodel);
}
+1 for cheny's answer. Use TryUpdateModel instead of Bind.
public ActionResult Edit(int id)
{
var warrantymodel = db.Warranties.Find(id);
if (TryUpdateModel(warrantymodel, "", new string[] { "Id", "Description", "LastModifiedDate" }))
{
db.SaveChanges();
return RedirectToAction("Index");
}
return View(warrantymodel);
}
If you want to use View Model, you can use Automapper and configure it to skip null values so the existing data still exists in the domain model.
Example:
Model:
public class WarrantyModel
{
public int Id { get; set; }
public string Description { get; set; }
DateTime CreatedDate { get; set; }
DateTime? LastModifiedDate { get; set; }
}
ViewModel:
public class WarrantyViewModel
{
public int Id { get; set; }
public string Description { get; set; }
DateTime? CreatedDate { get; set; }
DateTime? LastModifiedDate { get; set; }
}
Controller:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include="Id,Description,LastModifiedDate")] WarrantyViewModel warrantyViewModel)
{
var warrantyModel = db.Warranties.Find(warrantyViewModel.Id);
Mapper.Map(warrantyViewModel, warrantyModel);
if (ModelState.IsValid)
{
db.Entry(warrantyModel).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(warrantyModel);
}
Automapper:
Mapper.CreateMap<WarrantyViewModel, WarrantyModel>()
.ForAllMembers(opt => opt.Condition(srs => !srs.IsSourceValueNull));
try to remove the Create date prompt text box in the Edit view. In my application, the scaffold generated Edit and Create Views contain the Primary key which is generated in the database.
Controller:
...
warrantymodel.CreatedDate = DateTime.Parse(Request.Form["CreatedDate"]);
...
i need to render list data in partial page... I have store procedure that is responsible to get list of menus and have master Layoutpage in view--> shared folder--> now i am rendering partial page in masterLayout page but i am not getting any result. I am aware that partial page doesn't go through controller so how i would pass the data in that?
Controller Action
public ActionResult DisplayFunctionsList()
{
var myList = F_UOF.GetAllFunctions();
return View(myList);
}
Store Procedure Mapping (Model) class
public class GetAllFunction_SP_Map
{
public GetAllFunction_SP_Map() { }
[Key]
public int Hierarchy_ID { get; set; }
[Required]
public int ParentID { get; set; }
[StringLength(250)]
[Required]
public string ParentName { get; set; }
[Required]
public int ChildID { get; set; }
[StringLength(250)]
[Required]
public string ChildName { get; set; }
[StringLength(250)]
[Required]
public string Controller { get; set; }
[StringLength(250)]
[Required]
public string Action { get; set; }
}
MasterLayout Page
#Html.Partial("_DisplayFunctionList_Partial")
Partial Page (_DisplayFunctionList_Partial)
#model IEnumerable<DatabaseLayer.StoreProcedures.StoreProceduresMapping.GetAllFunction_SP_Map>
#foreach(var item in Model)
{
#item.ParentName
}
In this example, since you are calling from the master page, you want to use the Html.RenderAction() method.
#{Html.RenderAction("DisplayFunctionsList", "Controller");}
Edit - the Html.RenderAction must be surrounded by curly braces with the semicolon (';') at the end of the function call. To avoid this, you can use the #Html.Action call, which returns an MvcHtmlString
#Html.Action("DisplayFunctionsList", "Controller")
Also, in using this method, you would need to change your view result slightly because the view you are trying to render is not the name of the action you are calling it from and you appear to be wanting to return a PartialViewResult
public ActionResult DisplayFunctionsList()
{
var myList = F_UOF.GetAllFunctions();
return PartialView("_DisplayFunctionList_Partial", myList);
}
If you are within a view (not a master page), then you would need to pass the model, assuming the model is of the correct type. If the model for the partial view is a property on your overall view model, then just send the model property instead of the entire model.
*Send Entire Model*
#Html.Partial("_DisplayFunctionList_Partial", Model)
*Send Model Property*
#{Html.Partial("_DisplayFunctionList_Partial", Model.MyList)}
The call to #Html.Partial takes a second parameter, which is the model to pass to the partial view. All you need to do is include the list in your View's Model and then pass it along appropriately.
A MVC controller takes a few form items passed to it.
Let's say Name and Address.
In the [Post] controller
It receives a Person Object.
The MVC magical mapping takes place and the Person Object is filled.
1) What is the correct term for this magical mapping?
MODEL BINDING
2) Why if my Person object has virtual object, it doesn't get magically filled up?
OK so here is some REAL code.
public class PackageItem
{
public int ProposalItemID { get; set; }
public virutal PackageByContract { get; set; }
public int Quantity { get; set; }
}
public class EquipmentItem
{
public int ProposalItemID { get; set; }
public virtual EquipmentByContract { get; set; }
public int Quantity { get; set; }
}
public class ProposalItem
{
public PackageItem PackageItem { get; set; }
public EquipmentItem EquipmentItem { get; set; }
}
EquipmentByContract
and
PackageByContract
objects both have
EquipmentByContractID
and
PackageByContractID
<select name="PackageItem.PackageByContract.PackageByContractID"...>
<select name="PackageItem.EquipmentByContract.EquipmentByContractID"...>
Post the controller
Upon Debugging PackageByContractID and EquipmentByContractID are both null
Valued being sent are int
In my controller
[HttpPost]
public ActionResult Index(ProposalItem Item)
{...}
Upon hovering over the Item, both objects appear.
When I drill through it both values are null.
MVC needs some very specific inputs with very specific ids to be posted in order to work its Model Binding magic.
If the model is coming back null, you either don't have an input corresponding to each property of your model, or your ids are wrong.
Check out this post for some ideas of what it should look like.
I'm trying to do something i that feels like a small task, but i cannot figure out a simple way to do it. All my approaches for doing this gets really complex for a simple task.
I have these models:
public class Blog
{
public int Id { get; set; }
public String Name { get; set; }
public virtual ICollection<Post> Posts { get; set; }
}
public class Post
{
public int Id { get; set; }
public String Name { get; set; }
public int BlogId { get; set; }
public virtual ICollection<Comment> Comments { get; set; }
}
public class Comment
{
public int Id { get; set; }
public String CommentText { get; set; }
public int PostId { get; set; }
public int UserProfileUserId { get; set; }
}
public class UserProfile
{
public int UserId { get; set; }
public string UserName { get; set; }
public String FirstName { get; set; }
public String LastName { get; set; }
}
In the Added Comments partial view, i want to show the full user name of the user that made a comment. If i just use my base classes in my views and partial views, i get everything i need except full user name on added comments. So far, i've thought of the following ways:
ViewModels - This will result in creating a ViewModel for each of my Classes and then populate / map them manually in my controller.
Code in Views - I have the UserProfileUserId so i can just ask the repository from the view but this Kills the MVC in MVC so i don't want to do it.
Actually Adding UserProfileFirstName and UserProfileLastName to the Comment Class as foreign keys - This feels like filling the database with view specific data. It doesn't belong in a relational database.
Using regular SQL and Query the database - Just because i know SQL, this -could- be a way to do it. but then again i'm killing the MVC in MVC.
How should i do this? Where is my silly overlooked option? I've searched a lot but could not find an answer, but this could be related to me not knowing all the technical terms yet. Sorry if this is answered 1000 times before.
Ideally i would change my domain model to include a Author property of type UserProfile and load that data as well using a JOIN (Comment table and User table)
public class Comment
{
public int Id { get; set; }
public String CommentText { get; set; }
public int PostId { get; set; }
public UserProfile Author { get; set; }
}
EDIT : As per the questions in the comment
This is how i will do this.
My Repositary method will have these methods
List<Comment> GetCommentsForPost(int postId);
BlogPost GetPost(int postId);
I would have ViewModel for representing a single blog post like this
public class PostViewModel
{
public int PostID { set;get;}
public string PostText { set;get;}
public string AuthorDisplayName { set;get;}
public List<CommentViewModel> Comments { set;get;}
public PostViewModel()
{
Comments=new List<CommentViewModel>();
}
}
public class CommentViewModel
{
public int CommentID {set;get;}
public string Text { set;get;}
public string AuthorDisplayName { set;get;}
}
Now in your GET Action, Get the data from your Repositary and Map that to ViewModel and send it to view
public ActionResult ViewPost(int id)
{
var post=repositary.GetPost(id);
if(post!=null)
{
PostViewModel vm=new PostViewModel { PostID=id };
vm.PostText=post.Name;
var comments=repo.GetCommentsForPost(id);
foreach(var item in comments)
{
vm.Comments.Add(new CommentViewModel { CommentID=item.Id,
AuthorDisplayName=item.Author.FirstName});
}
return View(vm);
}
return View("NotFound");
}
Now your view will be strongly typed to The PostViewModel
#model PostViewModel
<h2>#Model.PostText</h2>
#Html.Partial("Comments",Model.Comments)
And your partial view(Comments.cshtml) will be strongly typed to a collection of CommentViewModel
#model List<CommentViewModel>
#foreach(var item in Model)
{
<div>
#item.Text
<p>Written by #item.AuthorDisplayName</p>
</div>
}
Now our views are not depending directly to Domain models. This allows us to bring data from another source tomorrow if we need (Ex :Get comments from a web service) and simply map to our view model.
Some notes
Do not add too much of code to Views. Let's keep it pure HTML as much as possible. No data access calls directly from Views!
I manually mapped the domain model to viewmodel for your understanding. You may use a mapping library like Automapper to do so. Also you may move part of the code we have in the GET action method to another servier layer so that it can be reused in multiple places.
Let's say you have an object called Person that looks like this:
class Person
{
public int ID { get; set; }
public string Name { get; set; }
public int NumberOfCatsNamedEnder { get; set; }
}
I have a simple HTML form that exposes the properties that gets posted to an ASP.NET MVC action inside of my PersonController class. The issue I have is that if someone puts in the letter 'A' for NumberOfCatsNamedEnder, I get a The model of type 'Person' was not successfully updated. error. Since this happens while trying to update the Model, I can't find any way to check to see if someone passed in a non-integer value without resorting to
if(!IsInteger(formCollection["NumberOfCatsNamedEnder"]))
{
ModelState.AddModelError(
"NumberOfCatsNamedEnder",
"Ender count should be a number");
}
Is there a better way to do this? I was able to find some information on custom ModelBinders; is that what is needed?
I really like the approach of using a presentation model. I'd create a class like this:
class PersonPresentation
{
public int ID { get; set; }
public string Name { get; set; }
public string NumberOfCatsNamedEnder { get; set; }
public void FromPerson(Person person){ /*Load data from person*/ }
}
Then your controller action can bind the view to a PersonPresentation:
public ActionResult Index()
{
Person person = GetPerson();
PersonPresentation presentation = new PersonPresentation();
ViewData.Model = presentation.FromPerson(person);
return View();
}
...and then accept one in your Update method and perform validation:
public ActionResult Update(PersonPresentation presentation)
{
if(!IsInteger(presentation.NumberOfCatsNamedEnder))
{
ModelState.AddModelError(
"NumberOfCatsNamedEnder",
"Ender count should be a number");
}
...
}