I have these 3 relevant tables below :
I have a form. Say it is called fmEditPurchase. Inside that form, I can edit Purchase informations, and add / delete PurchasedProduct.
There can be many changes in that form, but the changes will only be applied if I click on the save button, which returns DialogResult.OK to parent form. Below method is located in the main form.
private void Purchase_EditPurchase(object sender, DataGridViewCellEventArgs e)
{
if (dgvPurchase.SelectedRows.Count == 1)
{
int index = dgvPurchase.SelectedCells[0].RowIndex;
DataGridViewRow selectedRow = dgvPurchase.Rows[index];
int id = (int)selectedRow.Cells["ID"].Value;
Purchase edit = null;
using (var context = new dbKrunchworkContext())
{
edit = context.Purchases.Where(x => x.ID == id).FirstOrDefault();
if (edit != null)
{
fmAddEditPurchase editForm = new fmAddEditPurchase(edit);
if (editForm.ShowDialog() == DialogResult.OK)
{
//Section 1
foreach (var item in editForm.DeletedPP)
{
context.Entry(item).State = EntityState.Deleted;
context.Entry(item.Product).State = EntityState.Unchanged;
}
//Section 2
context.Entry(editForm.Purchase).State = EntityState.Modified;
//Section 3
foreach (var item in editForm.Purchase.PurchasedProducts)
{
context.Entry(item.Product).State = EntityState.Unchanged;
}
tslbMessage.Text =
string.Format("Product Data Edited");
context.SaveChanges();
Purchase_RefreshDGVPurchase();
}
}
}
}
}
So, to summarize, the changes on a particular Purchase record can include the removal / addition of PurchasedProduct and every other fields, which everything will be applied at eafter the Save button is pressed.
The problem is, when I'm editing, and removing a PurchasedProduct and saving, below error popped up.
The problem persist even after I rearrange the order of section 1, 2, and 3 (Look at top code to see where are the section 1,2,3, which are marked in the comments.).
I also can't do the SaveChanges() in the fmEditPurchase, since it's able to be cancelled to make 0 changes.
How to fix it? Thanks
According to your comment:
a purchase record can't have 2 PurchasedProduct record with a same
product (just add the quantity instea)
You don't need to set the PK to Product_ID. FK should be enough. If you wanna increase the quantity just look for the record searching by Product_ID, update it and save. In that way deleting will be possible.
Finally solved it, while Rodolfo's answer contribute to the solution, but it's not enough.
These are what I did :
I add my database context as parameter to the edit dialog constructor, and also a field inside that to store the context.
private dbKrunchworkContext context;
public fmAddEditPurchase(dbKrunchworkContext context)
{
//Bla bla
this.context = context;
//Bla bla
}
Before I initialize & show the dialog, I initialize the context using using(var context = new dbKrunchworkContext) { } and pass the value to the dialog.
using (var context = new dbKrunchworkContext())
{
edit = context.Purchases.Where(x => x.ID == id).FirstOrDefault();
if (edit != null)
{
fmAddEditPurchase editForm = new fmAddEditPurchase(context, edit);
if (editForm.ShowDialog() == DialogResult.OK)
{
//Blabla
}
}
}
In the new dialog, every time I'm going to use a context, I use the context field instead of using a newly instatiated one.
private void RefreshPurchasedProduct()
{
BindingSource bi = new BindingSource();
//Bla bla
bi.DataSource = Purchase.PurchasedProducts.
Join(context.Products, x => x.Product.ID, y => y.ID, (x, y) =>
new { y.Product_Name, x.Price, x.Quantity }).ToList();
//Bla bla
}
This way, there'll be no (annoying) The relationship between the two objects cannot be defined because they are attached to different ObjectContext objects.
Hope this helps anyone stumbled upon this similar problem of mine.
P.S. Some of the solution out there may prefer solve it with the static context, never tried it, but if me, I prefer this way, since it'll be cleaner this way, and less code to maintain.
Related
I have a project where a user can add new products or modify existing ones. Products can have parts associated with them. I created a list in the class where my product constructor lives to hold parts for each product.
I'm trying to figure out how to set up that if a user makes changes to the parts that are associated with a product but hits the cancel button then the list reverts to the original list. If they hit the save button after making edits to the products parts list then the updated list is saved and when they open the product again then the updated list displays.
All the code in the file I have is quite a bit but the parts I thought would be most helpful for what I have are...
Filling in the product text boxes with the information on the product. Populating the datagridview CurrentPartsDataGrid with the tempParts list that is a copy of the product.Parts list. Or creating a new empty list if it's a new product that is created.
if (product != null)
{
ProductIdText.Text = product.ID.ToString();
ProductNameText.Text = product.Name.ToString();
InvText.Text = product.QOH.ToString();
PriceText.Text = product.Price.ToString();
InvMinText.Text = product.Min.ToString();
InvMaxText.Text = product.Max.ToString();
tempParts = new BindingList<Part>(product.Parts);
}
else
{
product = new Product();
tempParts = new BindingList<Part>();
}
CurrentPartsDataGrid.DataSource = tempParts;
In the save/cancel button click event methods I have tried doing a for loop or for each loop. I clear the list and then try to repopulate.
Code in save
product.Parts.Clear();
foreach (Part part in tempParts)
{
product.Parts.Add(part);
}
Code in Cancel
tempParts.Clear();
foreach (Part part in product.Parts)
{
tempParts.Add(part);
}
If there is anything else that would be helpful, let me know. I'm new to posting here so don't want to overload the post but also don't want to not provide enough.
Any help on how to fix this would be awesome.
Thanks!
I ended up not needing to do any of the EndEdit() stuff.
In my (if product != null)
I added
tempList = new BindingList<Part>();
for (int i = 0; i < product.Parts.Count; i++)
{
tempList.Add(product.Parts[i]);
}
This created a new templist that when the user selected cancel I set product.Parts = tempList which resolved the issue.
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));
}
}
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.
I am using c# and VS2012 on a lightswitch web-application,
I wish to export my data to CSV (on a search screen!), but can't reach any POC,
As i understand there are 2 main problems - a savefiledialog must be caused directly from a user button and in it must happened in the main dispatcher,
I used this code :
partial void mySearchScreen_Created()
{
var CSVButton = this.FindControl("ExportToCSV");
CSVButton.ControlAvailable += ExportCSV_ControlAvailable;
}
private void ExportCSV_ControlAvailable(object sender, ControlAvailableEventArgs e)
{
this.FindControl("ExportToCSV").ControlAvailable -= ExportCSV_ControlAvailable;
Button Button = (Button)e.Control;
Button.Click += ExportCSV_Click;
}
private void ExportCSV_Click(object sender, System.Windows.RoutedEventArgs e)
{
Microsoft.LightSwitch.Details.Client.IScreenCollectionProperty collectionProperty = this.Details.Properties.mySearch;
var intPageSize = collectionProperty.PageSize;
//Get the Current PageSize and store to variable
collectionProperty.PageSize = 0;
var dialog = new SaveFileDialog();
dialog.Filter = "CSV (*.csv)|*.csv";
if (dialog.ShowDialog() == true) {
using (StreamWriter stream = new StreamWriter(dialog.OpenFile())) {
string csv = GetCSV();
stream.Write(csv);
stream.Close();
this.ShowMessageBox("Excel File Created Successfully. NOTE: When you open excel file and if you receive prompt about invalid format then just click yes to continue.", "Excel Export", MessageBoxOption.Ok);
}
}
collectionProperty.PageSize = intPageSize;
//Reset the Current PageSize
}
private string GetCSV()
{
StringBuilder csv = new StringBuilder();
int i = 0;
foreach (var orderRow_loopVariable in mySearch) {
var orderRow = orderRow_loopVariable;
////HEADER
if (i == 0) {
int c = 0;
foreach (var prop_loopVariable in orderRow.Details.Properties.All().OfType<Microsoft.LightSwitch.Details.IEntityStorageProperty>()) {
var prop = prop_loopVariable;
if (c > 0) {
csv.Append(",");//Constants.vbTab
}
c = c + 1;
csv.Append(prop.DisplayName);
}
}
csv.AppendLine("");
////DATA ROWS
int c1 = 0;
foreach (var prop_loopVariable in orderRow.Details.Properties.All().OfType<Microsoft.LightSwitch.Details.IEntityStorageProperty>()) {
var prop = prop_loopVariable;
if (c1 > 0) {
csv.Append(",");//Constants.vbTab
}
c1 = c1 + 1;
csv.Append(prop.Value);
}
i = i + 1;
}
if (csv.Length > 0) {
return csv.ToString(0, csv.Length - 1);
} else {
return "";
}
}
This works, but it only get's me the first page items,
On another thing i had to do i solved that problem by using this code :
this.DataWorkspace.myDataContextData.MySearch(...).Execute();
Yet trying that instead of just using 'MySearch' gives me the following error :
t is not valid to call Execute() on a different Dispatcher than the ExecutableObject's Logic Dispatcher.
Why is it so difficult to do such a basic thing related to data (export to csv/excel) on a system build for handling data ?
Any ideas ?
The simplest workaround if this is the only use of the search screen would be to turn off paging. To do this go to the screen designer, highlight the query on the left, and in properties uncheck 'support paging.'
I'm not sure what the limitations are, but you can run some code in a different dispatcher using:
this.Details.Dispatcher.BeginInvoke(() =>
{
//This runs on main dispatcher
});
I don't think there's anything wrong with your code, but I've noticed that it takes a while to reset the page size on a large collection, in which time the rest of your code continues to execute. I think that's why you only get the first page. The only solution I've found is to wait.
When the "File Download - Security Warning" dialog pops up, keep an eye on the 'busy' indicator on the screen's tab and also the 'Page x of y' status at the bottom of the grid if you can see it. Only when the busy indicator has gone and the status just says 'Page' should you click OK to continue.
I haven't figured out a way of doing this programmatically so it's not a very helpful feature unless you have a very tightly controlled user population. But if it's just you and a couple of power users, it is workable. I'm also not sure if this has been improved on in versions after VS2012.
There can be a downside to the other answer of taking the paging off the query entirely. I've tried that workaround when the grid collection was being displayed in a modal window and the window became uncloseable if there were too many rows in the grid.
Phil
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.