CRCase_RowSelecting fires with next case on subsequent save - c#

I created an extension for CRCaseMaint, and added the event CRCase_RowSelecting. Here is the code I am currently using:
protected virtual void CRCase_RowSelecting(PXCache sender, PXRowSelectingEventArgs e)
{
CRCase row = e.Row as CRCase;
if (row == null) return;
PXDatabase.ResetSlot<List<CRCase>>("OriginalCase");
List<CRCase> originalCaseSlot = PXDatabase.GetSlot<List<CRCase>>("OriginalCase");
if (originalCaseSlot.Count == 0)
{
originalCaseSlot.Add(sender.CreateCopy(row) as CRCase);
}
else
{
originalCaseSlot[0] = sender.CreateCopy(row) as CRCase;
}
}
When I first open a case, this event will fire a couple times, and the last time it fires, the current case is correctly stored in e.Row, so this code works great. When I click Save, I have a RowPersisting event that compares the case stored in the originalCaseSlot with the updated case. At the end, it sets the original case slot to the updated case. This also works well.
However, when I make another change without leaving the case, and click save, e.Row on the RowSelecting event now has the next case stored on it rather than the current case. Since I am not touching the next case in any way, I am surprised that this is happening.
My question is, should I be using a different event instead of RowSelecting, or is there something else I am missing?
Thank you all for your help.

Sometimes when the primary record gets updated or the user clicks on a form toolbar button, the framework selects 2 records from database: the current primary record and the next one. This is why RowSelecting is invoked 2nd time for the next CRCase record.
Honestly, using PXDatabase Slots to store user session-specific records is not a good idea. PXDatabase Slots are shared among all user sessions and should only be used to cache frequently used data from database, which is not prone to frequent updates. This makes the main purpose of PXDatabase Slots to reduce number of database queries to widely and very often used configurable data, like Segment Key or Attribute configurations.
With that said, using the RowSelecting handler is definitely a step in the right direction. Besides, the RowSelecting handler, you should additionally define a separate PrevVersionCase data view to store the original CRCase record(s) and also override the Persist method to report about changes. The Locate method used on PXCache objects searches the cache for a data record that has the same key fields as the provided data record. This approach allows to compare changes between the originally cached and modified CRCase records having identical key field values.
public class CRCaseMaintExt : PXGraphExtension<CRCaseMaint>
{
[Serializable]
public class CRPrevVersionCase : CRCase
{ }
public PXSelect<CRPrevVersionCase> PrevVersionCase;
protected virtual void CRCase_RowSelecting(PXCache sender, PXRowSelectingEventArgs e)
{
CRCase row = e.Row as CRCase;
if (row == null || e.IsReadOnly) return;
var versionCase = new CRPrevVersionCase();
var versionCache = PrevVersionCase.Cache;
sender.RestoreCopy(versionCase, row);
if (versionCache.Locate(versionCase) == null)
{
versionCache.SetStatus(versionCase, PXEntryStatus.Held);
}
}
[PXOverride]
public void Persist(Action del)
{
var origCase = Base.Case.Current;
var origCache = Base.Case.Cache;
CRPrevVersionCase versionCase;
if (origCache.GetStatus(origCase) == PXEntryStatus.Updated)
{
versionCase = new CRPrevVersionCase();
origCache.RestoreCopy(versionCase, origCase);
versionCase = PrevVersionCase.Cache.Locate(versionCase) as CRPrevVersionCase;
if (versionCase != null)
{
foreach (var field in Base.Case.Cache.Fields)
{
if (!Base.Case.Cache.FieldValueEqual(origCase, versionCase, field))
{
PXTrace.WriteInformation(string.Format(
"Field {0} was updated", field));
}
}
}
}
del();
if (origCase != null)
{
PrevVersionCase.Cache.Clear();
versionCase = new CRPrevVersionCase();
Base.Case.Cache.RestoreCopy(versionCase, origCase);
PrevVersionCase.Cache.SetStatus(versionCase, PXEntryStatus.Held);
}
}
}
public static class PXCacheExtMethods
{
public static bool FieldValueEqual(this PXCache cache,
object a, object b, string fieldName)
{
return Equals(cache.GetValue(a, fieldName), cache.GetValue(b, fieldName));
}
}

Related

In relation to acumatica, is there a way to grab a variable from one table and use it as part of an id for another table?

The 'Customer' form has a variable called AcctReferenceNbr (Variable that I'm trying to grab shown in yellow) which takes a two-letter abbreviation of the customer name. I am currently editing the Projects form, and I want to use this abbreviation as part of the External Ref. Nbr.
The attached image End Result I'm trying to achieve shows what the end result should look like. The number from the QuoteID is appended to the abbreviation.
I am able to successfully grab the QuoteID as it is part of the Projects table, but I am currently unable to grab the AcctReference Nbr from the Customer table.
I have a RowSelected event on the QuoteID field, which is shown below:
namespace PX.Objects.PM
{
public class ProjectEntry_Extension : PXGraphExtension<ProjectEntry>
{
#region Event Handlers
protected void PMProject_RowSelected(PXCache cache, PXRowSelectedEventArgs e)
{
PMProject row = (PMProject)e.Row;
if (row.ContractCD != null) {
PMProject item = PXSelectorAttribute.Select<PMProject.contractCD>(cache, row) as PMProject;
// The "UP" string is where the abbreviation is supposed to be,
// but I just added two letters to test if the appending works, which it does.
row.ExtRefNbr = "UP" + item.ContractCD;
}
}
#endregion
}
}
What I've tried so far:
Accessing the Customer table namespace to grab the value and pass it to the Projects form, which didn't work because it didn't accept the Customer type in the Projects form.
Adding a PXDefault attribute to the External Ref. Nbr which would try and grab the variable using SQL.
I'm a bit stuck on what else I can try. Any help would be appreciated :)
UPDATED
Below is how I went about trying to grab the AcctReferenceNbr value from the Customer table.
The reason why I tried using the PXSelectorAttribute method was that I added the AcctReferenceNbr as a column to the Quote ID selector (selector is shown in the link above called 'End Result I'm trying to achieve').
So I figured I could try and grab that value in the Customer namespace, as that is where the variable resides, and pass that up to the Project namespace above.
Then, I would call the public method below in the Project namespace to get the required abbreviation:
// instead of this
row.ExtRefNbr = "UP" + item.ContractCD;
// it would be this
row.ExtRefNbr = PX.Objects.AR.CustomerMaint_Extension.getAcctReferenceNbr(cache, e) + item.ContractCD;
namespace PX.Objects.AR
{
public class CustomerMaint_Extension : PXGraphExtension<CustomerMaint>
{
#region Event Handlers
public static string getAcctReferenceNbr(PXCache cache, PXRowSelectedEventArgs e)
{
BAccount row = (BAccount)e.Row;
BAccount item = PXSelectorAttribute.Select<BAccount.acctReferenceNbr>(cache, row) as BAccount;
return item.acctReferenceNbr;
}
}
#endregion
}
}
Is there a proper way to target the actual table?
try this. I haven't tested this but give it a go.
protected void PMProject_RowSelected(PXCache cache, PXRowSelectedEventArgs e)
{
PMProject row = (PMProject)e.Row;
if (row.ContractCD != null && row.CustomerID != null)
{
BAccount ba = (BAccount )PXSelectorAttribute.Select<PMProject.customerID>(cache, row) ;
row.ExtRefNbr = ba.AcctReferenceNbr+ row.ContractCD;
}
}
you certainly don't need to extend the CustomerMaint graph.

How do I make multiple if conditions, that returns true, execute in C#?

I have a checkedListBox where the user can select whatever they want to update. I want them to be able to freely update 1 up to 5 characteristics of a machine. So when they only want to update 1 thing, they do not have to provide the other 4 characteristics. Also, when they want to update 5 characteristics, they
can do it in one go. For that purpose I have the following if statements:
if (clbCharacteristicsToUpdate.CheckedItems.Count != 0)
{
if (clbCharacteristicsToUpdate.GetSelected(0))
{
currentITime = Convert.ToDouble(tbCurrentITime.Text);
MessageBox.Show(currentITime.ToString());
dh.UpdateCurrentITime(machineNr, currentITime);
}
if (clbCharacteristicsToUpdate.GetSelected(1))
{
cycleTime = Convert.ToDouble(tbCycleTime.Text);
MessageBox.Show(cycleTime.ToString());
dh.UpdateCycleTime(machineNr, cycleTime);
}
if (clbCharacteristicsToUpdate.GetSelected(2))
{
nrOfLinesPerCm = Convert.ToInt32(tbNrOfLinesPerCm.Text);
MessageBox.Show(nrOfLinesPerCm.ToString());
dh.UpdateNrOfLinesPerCm(machineNr, nrOfLinesPerCm);
}
if (clbCharacteristicsToUpdate.GetSelected(3))
{
heightOfLamallae = Convert.ToDouble(tbHeightOfLamallae.Text);
MessageBox.Show(heightOfLamallae.ToString());
dh.UpdateHeightOfLamallae(machineNr, heightOfLamallae);
}
if (clbCharacteristicsToUpdate.GetSelected(4))
{
if (rbLTB.Checked)
{
machineType = 2;
MessageBox.Show(machineType.ToString());
}
else if (rbSTB.Checked)
{
machineType = 1;
MessageBox.Show(machineType.ToString());
}
if(!rbLTB.Checked && !rbSTB.Checked)
{
MessageBox.Show("Select a machine type to update!");
return;
}
dh.UpdateType(machineNr, machineType);
}
}
My problem is that, when I choose and update 1 thing it works perfectly. But when I choose multiple ones, it only executes the last if statement that returns true. I thought about using if-else but then only the first one that returns true will be executed. I also thought about having if statements for each possibility. But since I have 5 characteristics I can update, this would make 25 possibilities and I do not want to have 25 if statements. Thanks in advance!
GetSelected does not check whether the item has been checked, but that it is actually selected.
For example, in the below image "Item 2" will will return true for GetSelected. It is selected, not checked.
Instead you could do something like checking the clbCharacteristicsToUpdate.CheckedItems property to get the items that are actually checked.
The GetSelected method actually comes from the ListBox class, which CheckedListBox inherits from.
Try this:
This is a helper method I created on a button.
private void button1_Click(object sender, EventArgs e)
{
CheckList();
}
This method looks if items are checked and iterates over the collection of checked ones, calling the last metod.
private void CheckList()
{
if (clbCharacteristicsToUpdate.SelectedItems.Count != 0)
{
foreach (int indexChecked in clbCharacteristicsToUpdate.CheckedIndices)
{
CheckSelectedItem(indexChecked);
}
}
}
And finally, here the appropriate actions are taken based on indexes.
private void CheckSelectedItem(int index)
{
if (index == 0)
{
//Do stuff on Zero
MessageBox.Show("Zero");
}
if (index == 3)
{
//Do stuff on One
MessageBox.Show("Three");
}
}

Determine whether there are changes on a form when you click a submit button

I have a form with two textboxes. I am retrieving data from the
database to populate the boxes. When my user clicks on submit button
and the content of the 2 textboxes does not change, I dont want to go through
the code.
How do I determine when the content of the boxes changes and when it does not change?
Do I need to make some kind of comparison to what I have in memory?
public ActionResult Edit(profile objprofiler)
{
if (ModelState.IsValid)
{
//Go fetch the existing profile from the database
var currentProfile = db.Profiles.FirstOrDefault(p => p.ProfileId == objprofiler.ProfileId);
//Update the database record with the values from your model
currentProfile.City = objprofiler.City;
currentProfile.State = objprofiler.State;
//Commit to the database!
db.SaveChanges();
ViewBag.success = "Your changes have been saved";
return View(profiler);
}
}
You can compare the values with a simple if condition. Something like this:
if ((currentProfile.City != objprofiler.City) || (currentProfile.State != objprofiler.State))
{
currentProfile.City = objprofiler.City;
currentProfile.State = objprofiler.State;
db.SaveChanges();
}
Or use whatever logic you're trying to achieve, really. Whether you want to compare for each field individually, use a && instead of an ||, etc. The logic you want to implement is up to you. But you'd perform the comparison in an if statement.
Note also that you can use string.Equals() instead of just the == operator to compare strings with some more options, such as case sensitivity options and other useful things.
If the comparison gets more complex, you might also encapsulate it in the profile object itself. Perhaps by overriding .Equals(), though that has other implications when testing for equality. Maybe just a simple helper function:
public bool IsEqualTo(profile obj)
{
return this.City == obj.City
&& this.State == obj.State;
}
Then in the controller you can just use that method:
if (!currentProfile.IsEqualTo(objprofiler))
db.SaveChanges();
The way I typically handle this is by setting a 'dirty' flag any time a data change event occurs on any of the form's controls.
When the user comes to submit the form, I just check the state of the flag to see whether any changes need to be saved. This avoids having to compare all data to their previous states, which can be a nuisance if there are a lot of input controls on the form.
For example:
bool isDirty;
private void textBox_TextChanged(object sender, EventArgs e)
{
// Possible validation here
SetDirty(true);
}
private void SetDirty(bool dirty)
{
// Possible global validation here
isDirty = dirty;
}
private void Submit()
{
if(isDirty)
{
// Save logic
}
}
This approach allows you to run any global validation logic whenever any data is changed.
Caveat: If a user makes a change then reverts it, the form will still submit the data.
On the client side you can check if the value has changed by running some js to compare the elements value to its initial value. Something like this.
function hasFormChanged() {
//textboxes, textareas
var els = document.querySelectorAll('input[type="text"], textarea, input[type="number"]');
for (i = 0; i < els.length; i++) {
var el = els[i];
if (el.value !== el.defaultValue) {
return true;
}
}
//checkboxes and radios
els = document.querySelectorAll('input[type="radio"], input[type="checkbox"]');
for (i = 0; i < els.length; i++) {
var el = els[i];
if (el.checked !== el.defaultChecked) {
return true;
}
}
//select
els = document.querySelectorAll('select');
for (i = 0; i < els.length; i++) {
var el = els[i];
if (el.options[el.selectedIndex].value != '') {
if (!el.options[el.selectedIndex].defaultSelected) {
return true;
}
}
}
//if we get here then nothing must have changed
return false;
}
and it that function return true indicating that something has changed you can set a hidden form value like this
<input type="hidden" value="false" id="AnyUpdates" name="AnyUpdates"/>
to true.
Then in your controller update read that field to determine if you need to do your db stuff.

WinForms (C#) input field with database-driven autocomplete

I am trying to create a text input field with autocomplete functionality. The list of available options is huge (50,000+) and will need to be queried on TextChanged (after the first 3 characters have been entered).
I have a 99%-working solution with TextBox, setting AutoCompleteCustomSource to my new AutoCompleteStringCollection in the TextChanged event, but that results in occasional memory access violations due to a well-documented bug in the underlying AutoComplete implementation...
Microsoft Support say "Do not modify the AutoComplete candidate list dynamically during key events"...
Several SO threads: 1, 2, 3
These threads have some suggestions on how to prevent the exceptions but nothing seems to completely eliminate them, so I'm looking for an alternative. have tried switching to a ComboBox-based solution but can't get it to behave as I want.
After the user types the third character, I update the ComboBox's DataSource but the first item is automatically selected. The user is not able to continue typing the rest of the name.
The ComboBox items are not visible until the user clicks the triangle to expand the list
If the user selects the text they have entered and starts typing, I set DataSource to null to remove the list of suggestions. Doing this puts the cursor at the start of the text, so their characters appear in completely the wrong order!
My View:
public event EventHandler SearchTextChanged;
public event EventHandler InstrumentSelected;
public Instrument CurrentInstrument
{
get { return comboBoxQuickSearch.SelectedItem as Instrument; }
}
public IEnumerable<Instrument> Suggestions
{
get { return comboBoxQuickSearch.DataSource as IEnumerable<Instrument>; }
set
{
comboBoxQuickSearch.DataSource = value;
comboBoxQuickSearch.DisplayMember = "Name";
}
}
public string SearchText
{
get { return comboBoxQuickSearch.Text; }
}
private void comboBoxQuickSearch_TextChanged(object sender, EventArgs e)
{
if (SearchTextChanged != null)
{
SearchTextChanged(sender, e);
}
}
private void comboBoxQuickSearch_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Enter && InstrumentSelected != null)
{
InstrumentSelected(sender, e);
}
}
My Presenter:
private void SearchTextChanged(object sender, EventArgs e)
{
lock (searchLock)
{
// Do not update list of suggestions if:
// 1) an instrument has already been selected
// (the user may be scrolling through suggestion list)
// 2) a search has taken place within the last MIN_SEARCH_INTERVAL
if (DateTime.Now - quickSearchTimeStamp < minimumSearchInterval
|| (view.Suggestions != null && view.Suggestions.Any(i => i.Name == view.SearchText)))
{
return;
}
string searchText = view.SearchText.Trim();
if (searchText.Length < SEARCH_PREFIX_LENGTH)
{
// Do not show suggestions
view.Suggestions = null;
searchAgain = false;
}
// If the prefix has been entered or changed,
// or another search is needed to display the full sublist
else if (searchText.Length == SEARCH_PREFIX_LENGTH
|| searchText.Substring(0, SEARCH_PREFIX_LENGTH) != searchTextPrefix
|| searchAgain)
{
// Record the current time and prefix
quickSearchTimeStamp = DateTime.Now;
searchTextPrefix = searchText.Substring(0, SEARCH_PREFIX_LENGTH);
// Query matches from DB
IList<Instrument> matches = QueryMatches(searchText);
// Update suggestions
view.Suggestions = matches;
// If a large number of results was received, search again on the next chararacter
// This ensures the full match list is presented
searchAgain = matches.Count() > MAX_RESULTS;
}
}
}
(The searchAgain bit is left over from the TextBox implementation, where the AutoCompleteCustomSource wouldn't always show the complete list if it contained too many items.)
Can I get the ComboBox to work as I want it to, providing suggestions as the user types, given my requirement to query those suggestions on TextChanged?
Is there some other combination of controls I should use for a better user experience, e.g. ListBox?

Trouble with EF "Save" method, modified collection of child entities

I have a parent entity (Treatment) with a collection of child entities (Segments). I have a save method that take a treatment, determines if it's new or existing, and then either adds it to the objectContext or attaches it to the object context based on whether it is new or existing.
It does the same thing with the children within the main entity. It iterates over the collection of child entities, and then adds or updates as appropriate.
What I'm trying to get it to do, is to delete any child objects that are missing. The problem is, when I'm updating the parent object and then I attach it to the object context, the parent object then has a collection of child objects from the DB. Not the collection I originally passed in. So if I had a Treatment with 3 segments, and I remove one segment from the collection, and then pass the Treatment into my save method, as soon as the Treatment object is attached to the objectcontext, the number of segments it has is changed from 2 to 3.
What am I doing wrong?
Here is the code of my save method:
public bool Save(Treatment myTreatment, modelEntities myObjectContext)
{
bool result = false;
if (myObjectContext != null)
{
if (myTreatment.Treatment_ID == 0)
{
myObjectContext.Treatments.AddObject(myTreatment);
}
else
{
if (myTreatment.EntityState == System.Data.EntityState.Detached)
{
myObjectContext.Treatments.Attach(myTreatment);
}
myObjectContext.ObjectStateManager.ChangeObjectState(myTreatment, System.Data.EntityState.Modified);
myObjectContext.Treatments.ApplyCurrentValues(myTreatment);
}
foreach (Segment mySegment in myTreatment.Segments)
{
if (mySegment.SegmentID == 0)
{
myObjectContext.ObjectStateManager.ChangeObjectState(mySegment, System.Data.EntityState.Added);
myObjectContext.Segments.AddObject(mySegment);
}
else
{
if (mySegment.EntityState == System.Data.EntityState.Detached)
{
myObjectContext.Segments.Attach(mySegment);
}
myObjectContext.ObjectStateManager.ChangeObjectState(mySegment, System.Data.EntityState.Modified);
myObjectContext.Segments.ApplyCurrentValues(mySegment);
}
}
}
result = (myObjectContext.SaveChanges(SaveOptions.None) != 0);
return result;
}
*EDIT****
Based on some of the feedback below, I have modified the "Save" method. The new method implementation is below. However, it still does not delete Segments that have been removed from the myTreatments.Segments collection.
public bool Save(Treatment myTreatment, tamcEntities myObjectContext)
{
bool result = false;
if (myObjectContext != null)
{
if (myTreatment.Treatment_ID == 0)
{
myObjectContext.Treatments.AddObject(myTreatment);
}
else
{
if (myTreatment.EntityState == System.Data.EntityState.Detached)
{
myObjectContext.Treatments.Attach(myTreatment);
}
myObjectContext.ObjectStateManager.ChangeObjectState(myTreatment, System.Data.EntityState.Modified);
}
foreach (Segment mySegment in myTreatment.Segments)
{
if (mySegment.SegmentID == 0)
{
myObjectContext.ObjectStateManager.ChangeObjectState(mySegment, System.Data.EntityState.Added);
}
else
{
myObjectContext.ObjectStateManager.ChangeObjectState(mySegment, System.Data.EntityState.Modified);
}
}
}
result = (myObjectContext.SaveChanges(SaveOptions.None) != 0);
return result;
}
FINAL EDIT
I have finally got it to work. Here is the updated Save method that is working properly. I had to save the initial list of Segments in a local variable and then compare it to the myTreatments.Segments list after it was attached to the DB, to determine a list of Segments to be deleted, and then iterate over that list and delete matching Segments from the newly attached myTreatment.Segments list. I also removed the passing in of the objectcontext per advice from several responders below.
public bool Save(Treatment myTreatment)
{
bool result = false;
List<Segment> myTreatmentSegments = myTreatment.Segments.ToList<Segment>();
using (tamcEntities myObjectContext = new tamcEntities())
{
if (myTreatment.Treatment_ID == 0)
{
myObjectContext.Treatments.AddObject(myTreatment);
}
else
{
if (myTreatment.EntityState == System.Data.EntityState.Detached)
{
myObjectContext.Treatments.Attach(myTreatment);
}
myObjectContext.ObjectStateManager.ChangeObjectState(myTreatment, System.Data.EntityState.Modified);
}
// Iterate over all the segments in myTreatment.Segments and update their EntityState to force
// them to update in the DB.
foreach (Segment mySegment in myTreatment.Segments)
{
if (mySegment.SegmentID == 0)
{
myObjectContext.ObjectStateManager.ChangeObjectState(mySegment, System.Data.EntityState.Added);
}
else
{
myObjectContext.ObjectStateManager.ChangeObjectState(mySegment, System.Data.EntityState.Modified);
}
}
// Create list of "Deleted" segments
List<Segment> myDeletedSegments = new List<Segment>();
foreach (Segment mySegment in myTreatment.Segments)
{
if (!myTreatmentSegments.Contains(mySegment))
{
myDeletedSegments.Add(mySegment);
}
}
// Iterate over list of "Deleted" segments and delete the matching segment from myTreatment.Segments
foreach (Segment mySegment in myDeletedSegments)
{
myObjectContext.ObjectStateManager.ChangeObjectState(mySegment, System.Data.EntityState.Deleted);
}
result = (myObjectContext.SaveChanges(SaveOptions.None) != 0);
}
return result;
}
Maybe I am missing something, but to me this code looks overly cumbersome.
Please bear with me, if I am on the wrong track here and misunderstood you.
Regarding the objects that should be deleted, I suggest that you store these in a separate collection that only holds the deleted items. You can the delete them from the ObjectContext.
Instead of calling ApplyCurrentValues I would, I would simply call myObjectContext.SaveChanges(). ApplyCurrentValues has, in this case, the downside that it does not take care of any other entity that has relations to the one you are saving.
MSDN documentation:
Copies the scalar values from the supplied object into the object in
the ObjectContext that has the same key.
As the other Segments are already attached to your Treatment, by using SaveChanges(), they will be added to the context automatically or updated if they were already added.
This should make all that manual handling of the EntityStates unnecessary.
EDIT:
Now I see where this is going...
Somewhere in you you code - outside of this Save() method - you are deleting the Segment instances. The trouble lies in the problem that your ObjectContext is totally unaware of this. And how should it be...?
You may have destroyed the instance of a certain Segment entity, but as the entities are detached, this means that they have no connection to the ObjectContext. Therefore the context has absolutely no idea of what you have done.
As a consequence, when you attach the treament to it, the context still believes that all Segments are alive because it does not know about the deletion and adds them again to the Treatment like nothing ever happened.
Solution:
Like I already said above, you need to keep track of your deleted entities.
In those spots, where you delete the Segments, do not actually delete them, but:
Remove() it from the Treatment instance.
Move the "deleted" Segment into a collection, e.g. List<Segment>. Let's call it deletedSegments.
Pass the deletedSegments collection into the Save() method
Loop through this collection and ObjectContect.Delete() them.
Do the rest of the remaining save logic as necessary.
Also, like Tomas Voracek mentioned, it is preferable to use contexts more locally. Create it only within the save method instead of passing it as an argument.
Ok try again.
When you say "remove" do you mean mark as deleted.
You are calling ChangeObjectState to change the state to modified.
So if you send 3 down, one deleted, one modified and one unchanged; then all will get marked as modified before save changes is called.

Categories