I'm wanting to capture the old values within a model so I can compare with the new values after submission, and create audit logs of changes a user makes.
My guess is doing it with hidden input boxes with duplicated old value properties would be one way. But wondering if there are any other good alternatives?
Thanks
In the save method, just go and get the original object from the database before saving the changes, then you have your old and new values to compare against? :)
This sounds like standard auditing. You should not worry about what has changed just capture EVERYTHING and who made the change. Unless there is some sort of real time reporting that needs to be done.
Possible auditing implementations:
CQRS, in a nutshell it tracks every change to a given object. The downside is it's an architecture that is more involved to implement.
The Rolling ledger. Each insert is a new row in the database. The most current row is used for display purposes, but with each update, a new row is inserted into the database.
Yet another approach is to save it off into an audit table.
All get the job done.
You could also store the original model in the view bag and do something like this...
// In the controller
public ActionResult DoStuff()
{
// get your model
ViewBag.OriginalModel = YourModel;
return View(YourModel);
}
// In the View
<input type="hidden" name="originalModel" value="#Html.Raw(Json.Encode(ViewBag.OriginalModel));" />
// In the controller's post...
[HttpPost]
public ActionResult DoStuff(YourModel yourModel, string originalModel)
{
// yourModel will be the posted data.
JavaScriptSerializer JSS = new JavaScriptSerializer();
YourModel origModel = JSS.Deserialize<YourModel>(originalModel);
}
I didn't get a chance to test this, just a theory :)
Exactly what mattytommo says is the preferred method all around
Instantiate new view model for creating a new entity
public ActionResult Edit(int id) {
var entity = new Entity(id); // have a constructor in your entity that will populate itself and return the instance of what is in the db
// map entity to ViewModel using whatever means you use
var model = new YourViewModel();
return View(model);
}
Post changes back
[HttpPost]
public ActionResult Edit(YourViewModel model) {
if (ModelState.IsValid) {
var entity = new YourEntity(model.ID); // re-get from db
// make your comparison here
if(model.LastUserID != entity.LastUserID // do whatever
... etc...
}
return View(model);
}
Related
I have an edit/create page for a model. When the user submits the form, it'll be added or updated to the database.
After that happens, I want to redirect them back to the same form while keeping the same data, with a cleared out id as well as some other values.
public ActionResult AddProduct(MyModel myModel)
{
// Save
if (myModel.AuditID != 0) {
Update(myModel);
} else {
Add(myModel);
}
// Set some values so it's seen as new, as well as some
// other values that need to be cleared
myModel.ID = 0;
myModel.Product = "";
// Edit/Create page
ActionResult ret = EditCreateKnownRow(myModel);
return ret;
}
I want it to be treated as a completely new entity, but I get an InvalidOperationException with these details:
The property 'ID' is part of the object's key information and cannot be modified.
I get that entity framework doesn't want to deal with the foreign key constraints that may exist, but that has nothing to do with what I'm looking for. Is there a way to treat it as a new entity without having to create a copy constructor?
Thanks.
Would doing something to the ModelState help? I've tried ModelState.Remove(myModel.ID.ToString()); and ModelState.Clear(); before modifying the key, but it didn't work.
I have a model with a property called "datetime_inclusion" and I need to set a value for this ONLY when I save the first time, there is a way to do this treatment in the model?
I use C# MVC5 and entity framework 5
When you save the first time your object won't have an ID until it gets put into the DB. You can check against this to set your value.
if(myEntity.ObjectID <= 0)
{
myEntity.DateAdded = DateTime.Now;
}
Shoe's answer is perfect. But if you are worry that property can be change in edit time, control that. In create action use Shoe's code and in Edit action use:
[HttpPost]
public ActionResult Edit(int id, Model model)
{
using (var db = new YourEntities())
{
//control that, it does not change
model.datetime_inclusion = db.YourTable.Find(id).datetime_inclusion;
db.Entry(model).State = System.Data.EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
}
I am writing a asp.net mvc4 app and I am using entity framework 5. Each of my entities have fields like EnteredBy, EnteredOn, LastModifiedBy and LastModifiedOn.
I am trying to auto-save them by using the SavingChanges event. The code below has been put together from numerous blogs, SO answeres etc.
public partial class myEntities : DbContext
{
public myEntities()
{
var ctx = ((IObjectContextAdapter)this).ObjectContext;
ctx.SavingChanges += new EventHandler(context_SavingChanges);
}
private void context_SavingChanges(object sender, EventArgs e)
{
ChangeTracker.DetectChanges();
foreach (ObjectStateEntry entry in
((ObjectContext)sender).ObjectStateManager
.GetObjectStateEntries
(EntityState.Added | EntityState.Modified))
{
if (!entry.IsRelationship)
{
CurrentValueRecord entryValues = entry.CurrentValues;
if (entryValues.GetOrdinal("LastModifiedBy") > 0)
{
HttpContext currContext = HttpContext.Current;
string userName = "";
DateTime now = DateTime.Now;
if (currContext.User.Identity.IsAuthenticated)
{
if (currContext.Session["userId"] != null)
{
userName = (string)currContext.Session["userName"];
}
else
{
userName = currContext.User.Identity.Name;
}
}
entryValues.SetString(
entryValues.GetOrdinal("LastModifiedBy"), userName);
entryValues.SetDateTime(
entryValues.GetOrdinal("LastModifiedOn"), now);
if (entry.State == EntityState.Added)
{
entryValues.SetString(
entryValues.GetOrdinal("EnteredBy"), userName);
entryValues.SetDateTime(
entryValues.GetOrdinal("EnteredOn"), now);
}
else
{
string enteredBy =
entry.OriginalValues.GetString(entryValues.GetOrdinal("EnteredBy"));
DateTime enteredOn =
entry.OriginalValues.GetDateTime(entryValues.GetOrdinal("EnteredOn"));
entryValues.SetString(
entryValues.GetOrdinal("EnteredBy"),enteredBy);
entryValues.SetDateTime(
entryValues.GetOrdinal("EnteredOn"), enteredOn);
}
}
}
}
}
}
My problem is that entry.OriginalValues.GetString(entryValues.GetOrdinal("EnteredBy")) and entry.OriginalValues.GetDateTime(entryValues.GetOrdinal("EnteredOn")) are not returning the original values but rather the current values which is null. I tested with other fields in the entity and they are returning the current value which were entered in the html form.
How do I get the original value here?
I think the problem may be that you are using the instance provided by the model binder as the input to your controller method, so EF does not know anything about that entity and its original state. Your code may look like this:
public Review Update(Review review)
{
_db.Entry(review).State = EntityState.Modified;
_db.SaveChanges();
return review;
}
In that case, EF knows nothing about the Review instance that is being saved. It is trusting you and setting it as modified, so it will save all of its properties to the database, but it does not know the original state\values of that entity.
Check the section named Entity States and the Attach and SaveChanges Methods of this tutorial. You can also check the first part of this article, that shows how EF does not know about the original values and will update all properties in the database.
As EF will need to know about the original properties, you may first load your entity from the database and then update its properties with the values received in the controller. Something like this:
public Review Update(Review review)
{
var reviewToSave = _db.Reviews.SingleOrDefault(r => r.Id == review.Id);
//Copy properties from entity received in controller to entity retrieved from the database
reviewToSave.Property1 = review.Property1;
reviewToSave.Property2 = review.Property2;
...
_db.SaveChanges();
return review;
}
This has the advantage that only modified properties will be send and updated in the database and that your views and view models don't need to expose every field in your business objects, only those that can be updated by the users. (Opening the door for having different classes for viewModels and models\business objects). The obvious disadvantage is that you will incur an additional hit to the database.
Another option mentioned in the tutorial I referenced above is for you to save the original values somehow (hidden fields, session, etc) and on save use the original values to attach the entity to the database context as unmodified. Then update that entity with the edited fields. However I would not recommend this approach unless you really need to avoid that additional database hit.
Hope that helps!
I was running into a similar problem when trying to audit log the Modified values of an Entity.
It turns out during the post back the ModelBinder doesn't have access to the original values so the Model received is lacking the correct information. I fixed my problem by using this function which clones the current values, relods the object, and then reset the current values.
void SetCorrectOriginalValues(DbEntityEntry Modified)
{
var values = Modified.CurrentValues.Clone();
Modified.Reload();
Modified.CurrentValues.SetValues(values);
Modified.State = EntityState.Modified;
}
You can gain access to the DbEntityEntry though the change tracker, or the entry function from your context.
I have a view that I pass a viewmodel object to that contains an IQueryable<> object.
This object is used to populate an mvccontrib grid. The view also contains other partial views that allow the user to filter the data within the grid.
Once the grid is filtered I would like the user to be able to export the Iqueryable object to another controller actionresult method which then calls another viewmodel that exports the data to Excel.
Here is the snippet of the view that calls the Export actionresult() method:
#using (Html.BeginForm("Export", "Home", FormMethod.Post, new { Model }))
{
<p>
<input class="button" value="Export to Excel" type="submit" />
</p>
}
Model does contain the IQueryable object.
When I debug the code I can view the viewmodel object, and of course in order to populate the IQueryable I must enumerate the object.
I have also created another viewmodel object that, once the Model object is passed back to the actionresult method attempts to enumerate the IQueryable object by either using the .ToList() method or the AsEnumerable() method.
But in all cases the IQueryable object is pass to the controller as a null object.
Here is the action result method that is being called from the view:
[HttpPost]
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Export(PagedViewModel<NPSProcessed> NPSData)
{
string message = "";
NPSData Query = new Models.NPSData(NPSData);
Query.IData = NPSData.Query.ToList();
// Opening the Excel template...
FileStream fs =
new FileStream(Server.MapPath(#"\Content\NPSProcessedTemplate.xls"), FileMode.Open, FileAccess.Read);
MemoryStream ms = new MemoryStream();
ee.ExportData(fs, ms, Query.IData, message);
// Sending the server processed data back to the user computer...
return File(ms.ToArray(), "application/vnd.ms-excel", "NPSProcessedNewFile.xls");
}
Any assistance would be greatly appreciated.
Thanks
Joe
You cannot pass complex objects around like this: new { Model }. It would have been to easy :-). You will have to send them one by one:
new { prop1 = Model.Prop1, prop2 = Model.Prop2, ... }
Obviously this could get quite painful. So what I would recommend you is to send only an id:
new { id = Model.id }
and then inside your controller action that is supposed to export to Excel use this id to fetch the object from wherever you fetched it initially in the GET action (presumably a database or something). If you want to preserve the paging, and stuff that the user could have performed on the grid, you could send them as well to the server:
new { id = Model.id, page = Model.CurrentPage, sortColumn = Model.SortBy }
Another possibility (which I don't recommend) consists into saving this object into the session so that you can fetch it back later.
Yet another possibility (which I still don't recommend) is to use the MVCContrib's Html.Serialize helper which allows you to serialize an entire object graph into a hidden field and it will be sent to the server when the form is submitted and you will be able to fetch it as action argument.
The simple answer is: don't put IQueryable properties in your model. The model should be purely simple objects and validation attributes. Keep the queryability in your controller.
I'm struggling myself trying to get the contents of a form which is a complex model and then update the model with that complex model.
My account model has many individuals
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult OpenAnAccount(string area,[Bind(Exclude = "Id")]Account account, [Bind(Prefix="Account.Individuals")] EntitySet<Individual> individuals){
var db = new DB();
account.individuals = invdividuals;
db.Accounts.InsertOnSubmit(account);
db.SubmitChanges();
}
So it works nicely for adding new Records, but not for update them like:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult OpenAnAccount(string area,[Bind(Exclude = "Id")]Account account, [Bind(Prefix="Account.Individuals")] EntitySet<Individual> individuals){
var db = new DB();
var record = db.Accounts.Single(a => a.Reference == area);
account.individuals = invdividuals;
try{
UpdateModel(record, account); // I can't convert account ToValueProvider()
db.SubmitChanges();
}
catch{
return ... //Error Message
}
}
My problem is being how to use UpdateModel with the account model since it's not a FormCollection.
How can I convert it? How can I use ToValueProvider with a complex model?
I hope I was clear enough
Thanks a lot :)
UPDATE
That's what I was looking for:
http://goneale.com/2009/07/27/updating-multiple-child-objects-and-or-collections-in-asp-net-mvc-views/
This scenario is not supported unless you have your Account type implement IValueProvider.
That would be some pretty strange MVC though. The model binder should make sense of the HTTP request and translate that to your model not take bind your entities to your other entities.
Upon further inspection I think you're looking for:
http://msdn.microsoft.com/en-us/library/dd487246.aspx
Try this:
try{
db.Accounts.ApplyCurrentValues(record);
db.SubmitChanges();
}