Good Afternoon,
I'm currently attempting to work with the Entity Framework DbContext/Code First inside a winforms application. My class, PrintQueueItem, is mapped to a view inside the database that only returns PrintQueueItem that have a PrintStatus equal to 'Pending'. This is displayed to the end-user via a DataGridView so they can view what's to be batch printed at the end of the day. Users are able to add instances of PrintQueueItem and this is displayed inside the DataGridView as well.
My issue arises when I attempt to cancel an item that's set to print. I set the PrintStatus to Cancelled and then run the ExecuteSqlCommand to update the database. After this, I try reload the information but the item is still displayed inside the DataGridView. I'm unsure why this is as when I get the count (via Local) after the information has been loaded again it has been decremented by one. Any idea why this would be as I thought the BindingList provides a 2-way sync for data-binding and the UI would be updated with the change in data?
PrintQueueItem (Model)
public class PrintQueueItem
{
public PrintQueueItem()
{
this.PrintQueueID = Guid.NewGuid();
this.PrintStatus = "Pending";
this.DateAdded = DateTime.Now;
}
public Guid PrintQueueID { get; set; }
public Guid DocumentID { get; set; }
public string PrintStatus { get; set; }
public DateTime DateAdded { get; set; }
public virtual Documentation Document { get; set; }
}
Initial Binding
this.printQueueBinding.DataSource = db.PrintQueue.Local.ToBindingList();
this.printQueueGridView.AutoGenerateColumns = false;
this.printQueueGridView.DataSource = printQueueBinding;
Update PrintStatus on PrintQueueItem and Reload
PrintQueueItem item = printQueueBinding.Current as PrintQueueItem;
if (item == null)
{
throw new ArgumentNullException("Could not obtain instance of selected 'PrintQueueItem'");
}
item.PrintStatus = "Cancelled";
this.db.Database.ExecuteSqlCommand("exec usp_PrintQueue_Update {0}, {1}",item.PrintQueueID, item.PrintStatus);
this.db.PrintQueue.Load();
Some Additional Notes
Instead of a call to the ExecuteSqlCommand, I tried calling SaveChanges but encountered an exception stating:
"Store update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded."
I'm assuming this is because the item is no longer found in the view when it's updated. Please let me know if this is not the case.
The reason for using a filtered view rather than filtering in the application is because BindingList doesn't implement IBindingListView and I'm unable to implement the BindingList.Filter method to filter the data displayed inside the DataGridView. Any suggestions on this are welcome as well.
Thank you for your help.
After a couple beers and some time to let the jumbled mess of logic I had in my brain clear out from earlier today; I began to think of what was actually happening and then it hit me that when I attempted to call SaveChanges a concurrency exception was happening.
I failed to pay attention to the type of exception that was raised and thus failed to properly handle it. I've since updated my code to the following so when a DbUpdateConcurrencyException occurs (expected, since the view no longer contains the record) the entities that experience the issue are refreshed from the database:
try
{
if (Program.YesNoQuestion(string.Format("Cancel print job for the selected document?", printQueueGridView.SelectedRows.Count)) != System.Windows.Forms.DialogResult.Yes)
{
return;
}
PrintQueueItem item = printQueueBinding.Current as PrintQueueItem;
if (item == null)
{
throw new ArgumentNullException("Could not obtain an instance of 'PrintQueueItem'");
}
item.PrintStatus = "Cancelled";
this.db.Database.ExecuteSqlCommand("exec usp_PrintQueue_Update {0}, {1}", item.PrintQueueID,
item.PrintStatus);
//this.db.PrintQueue.Load();
this.db.SaveChanges();
}
catch (DbUpdateConcurrencyException ex)
{
foreach (DbEntityEntry entry in ex.Entries)
{
entry.Reload();
}
}
Please let me know if there are better ways to go about this as I'm always open to other ideas and always interested in doing things the proper way. Have a great night!
Related
This happens on a Xamarin.Forms app.
It is a simple app about making lists.
I got two pages: one with the lists and another to show the list's items. The error happens on the latter when I try to add a new item to an ObservableCollection.
This is a simplified version of the ViewModel:
public ObservableCollection<ListItem> Items { get; }
public Command AddItemCommand { get; }
public ItemsViewModel()
{
Items = new ObservableCollection<ListItem>();
AddItemCommand = new Command(OnAddItem);;
}
private async void OnAddItem()
{
await Device.InvokeOnMainThreadAsync(async () =>
{
if (string.IsNullOrEmpty(NewItemText))
return;
ListItem listITem = new ListItem()
{
ListId = _currentList.ListId,
Id = Guid.NewGuid().ToString(),
Text = NewItemText
};
Items.Add(listITem);
_currentList.ListItems.Add(listITem);
await DataStore.UpdateItemAsync(_currentList);
NewItemText = string.Empty;
});
}
The error happens on the Items.Add(listITem); call.
Tried wrapping the call on Device.InvokeOnMainThreadAsync with no luck.
The curious thing is it just happens on the second time I access the page.
The full project can be found on my GitHub:
https://github.com/JeffersonAmori/ListApp
It wouldn't surprise me if the error stopped happening if you lessen the chance for XForms to intervene before you've done the Add. (I'm hypothesizing that the underlying problem is a latent XForms bug.):
private async void OnAddItem()
{
// --- ASSUME we are already on MainThread. ---
// --- Avoid "await" (and any "..Invoke..Async") until after "Items.Add". ---
if (string.IsNullOrEmpty(NewItemText))
return;
ListItem listITem = new ListItem()
{
ListId = _currentList.ListId,
Id = Guid.NewGuid().ToString(),
Text = NewItemText
};
Items.Add(listITem);
_currentList.ListItems.Add(listITem);
NewItemText = string.Empty;
// ----- await Potentially slow operation(s) AFTER all quick UI calls. -----
await DataStore.UpdateItemAsync(_currentList);
}
CAVEAT #1: This doesn't fix any underlying problem, it just might make it happen less often. If its an XF problem, you might have to wrap your code in try..catch. In catch, determine if the item got added. If not try adding it again. Messy.
CAVEAT #2: This assumes OnAddItem is only called from MainThread. That will be true, if you never call it directly yourself - UI code will invoke the command on main thread.
CAVEAT #3: Assumes that all of the types involved (especially ListItem and _currentList.ListItems) are not UI types - they have no dependencies on Xamarin.Forms View classes.
I think the problem was the way the app was navigating between pages.
Before - when the error occurred - it was navigating using flyout routes via the flyout menu. Now I'm explicit registering the routes on code behind and pushing them into the navigation stack programmatically when the user clicks one of the items on the ColletcionView. This way I'm not reusing pages anymore.
public AppShell()
{
InitializeComponent();
Routing.RegisterRoute(nameof(ItemsPage), typeof(ItemsPage));
}
and then
async private void OnListSelected(List list)
{
if (list == null)
return;
await Shell.Current.GoToAsync($"{nameof(ItemsPage)}?{nameof(ItemsViewModel.ListId)}={list.ListId}");
}
This way I seems to get the threads to behavior correctly and always get a new list when navigating between pages as they are pushed/popped into/from the navigation stack.
Bear in mind there's some guess work on this answer as I'm just getting back into Xamari.Forms and I'm fairly new to Shell.
Thanks again toolmakersteve for the insights.
Cheers.
So,
In my WPF application, I want my users to be able to open previews of invoices, so that they may either verify or discard them. I am letting them check rows (each row representing a invoice) in a DataGridCheckBoxColumn in my DataGrid, then clicking a button (which runs my CreateInvoicePreview() method, see bottom of post), having all of the invoice previews be opened in new windows (one window for each invoice).
Well.. What happens now, is: User checks InvoiceA and InvoiceB. Two invoices are opened, but they are the same: InvoiceC. The correct amount of invoices are always opened, but not the correct instance. If I open the temp folder specified in my file path, I see that all invoices in the datagrid has been saved: InvoiceA through InvoiceJ.
Let me take you through the code.
This is the method that creates that builds and saves the actual PDF's, which the WebView2 control uses as source, so that it can display them in-app. It is heavily abbreviated.
I have kept the structure with the nested foreach loops in case that is relevant.
public void CreatePreviewInvoice() {
/* SQL SERVER CODE
* SQL SERVER CODE
* SQL SERVER CODE */
List<PaidTrip> paidTrips = PaidTrips.ToList();
tripsGroupedByCompany = paidTrips.GroupBy(pt => pt.LicenseHolderID);
foreach (IGrouping<string, PaidTrip> companyGroup in tripsGroupedByCompany) {
/* SQL SERVER CODE
* SQL SERVER CODE
* SQL SERVER CODE */
List<LicenseHolder> licenseHolders = LicenseHolders.ToList();
IEnumerable<IGrouping<string, PaidTrip>> groupedByVehicle = companyGroup.GroupBy(n => n.VehicleID);
foreach (IGrouping<string, PaidTrip> vehicleGroup in groupedByVehicle) {
// Iterating over all data pertaining to each vehicle
foreach (PaidTrip trip in vehicleGroup) {
}
try {
string userName = System.Security.Principal.WindowsIdentity.GetCurrent().Name.Split('\\')[1];
string fileName = $"FORHÅNDSVISNING - MÅ IKKE SENDES! {LicenseHolderID + "_" + "Faktura_" + InvoiceID}.pdf";
string filePath = $#"C:\Users\{userName}\AppData\Local\Temp\";
PdfFilePath = $"{filePath}{fileName}";
//if (LicenseHolderID == PreviewInvoiceViewModel.SelectedRow.LicenseHolderID) {
document.Save($"{PdfFilePath}");
//} else {
// return;
//}
} catch (Exception ex) {
MessageBox.Show(ex.Message);
}
}
}
As you see, towards the end of the method I have commented out a bit of code, which was me trying to implement a way to filter based on the checked rows only. It did not work.
This is the XAML for the WebView2:
<Wpf:WebView2
x:Name="wv_preview_invoice" Loaded="{s:Action CreatePreviewInvoice}"
Height="997" Width="702" Canvas.Left="20" Canvas.Top="71"
Source="{Binding PdfFilePath}"></Wpf:WebView2>
PdfFilePath is a property, which is referenced within the method above.
It's given a value within the method, and when Source (for the WebView2) is called, it is able to get the value from PdfFilePath, and thus display the PDF.
But as I said initially, it just creates X amount of instances/windows of the same invoice. Always the same one, because of in what order they are queried from the database.
And finally, here is the method that run when they click whichever invoices they want to preview, it's to open the new window with the WebView2 control:
public void PreviewInvoices() {
bool success = false;
foreach (PaidTrip item in PaidTrips) {
if (item.IsChecked == true) {
ShowPreviewInvoiceDetailed(item);
success = true;
}
}
if (!success) {
MessageBox.Show("You must chose an invoice to preview first.");
}
}
The method that opens the next window where the WebView2 is, looks like this:
public void ShowPreviewInvoiceDetailed() {
PreviewInvoiceDetailedViewModel viewModel = new(windowManager);
windowManager.ShowWindow(viewModel);
}
What part (or several parts) of the picture am I missing?
I managed to solve this by doing the following:
I made a property; public static string PreviewedInvoice { get; set; } in the ViewModel of the parent window. In my method that opens the child window (where the preview invoices are to be displayed) I bind it to LicenseHolderID of the rows that have a checked CheckBox, via foreach loop, like such:
public void PreviewInvoices() {
bool success = false;
foreach (PaidTrip item in PaidTrips) {
if (item.IsChecked == true) {
PreviewedInvoice = item.LicenseHolderID;
ShowPreviewInvoiceDetailed();
success = true;
}
}
if (!success) {
MessageBox.Show("You must chose an invoice to preview first.");
}
}
So now I have a way to filter only the checked rows, and make sure only the LicenseHolderID which match those in the row with a checked CheckBox, are actually saved. I updated my main method:
if (LicenseHolderID == PreviewInvoiceViewModel.PreviewedInvoice) {
document.Save($"{fullPath}");
SourcePath = fullPath;
}
And I bound SourcePath to the the source of the WebView2 in the XAML.
I feel like this is a clunky way of doing it, and I am going back and forth between layers, as a comment (since removed) mentioned.
If anyone can show me a better way, I'm all ears..
I'm relatively new to XAML / Xamarin and I'm running into something I'm hoping someone can help me clarify.
I know the title is misleading, but I couldn't put it any different.
I have the following property in my ViewModel:
private ObservableCollection<Schedule> _scheduleList;
public ObservableCollection<Schedule> ScheduleList
{
get
{
return _scheduleList;
}
set
{
value = _scheduleList;
}
}
Somewhere down the line, I do something like this:
private void DayFilter(string week)
{
try
{
var list = _scheduleList.Where(x => x.ScheduleDate.DayOfWeek.ToString() == week);
ObservableCollection<Schedule> newlist = new ObservableCollection<Schedule>(list);
ScheduleList.Clear(); // <- this line clears out _scheduleList as well
foreach (var item in newlist)
{
ScheduleList.Add(item);
}
}
catch (Exception ex)
{
throw ex;
}
}
Whenever ScheduleList.Clear() is called, it also clears out _scheduleList which is the private field.
I know this has something to do with the fact that this is ObservableCollection, but the requirement is that it should be, and I could not find a way to retain the value on _scheduleList, as I need this field populated throughout the lifetime of the application.
Is there away that the field _scheduleList does not get cleared out?
This is just how C# works
ScheduleList.Clear();
returns a reference to _scheduleList (that's what the public get does) and then calls Clear on it.
In your scenario, you probably need to maintain two completely separate copies of your data - the original, as well as one that you use for filtering/displaying the data.
I've bound my DataGridView to my ComboBox so that whatever value is selected in the ComboBox, the corresponding valued for SID and Mark will appear in the DataGridView. The DataGridView is editable when I do this but the data is not saved in the database when it is input. Is there a way to update it? If there's another method, I have to first warn that I only need SID and Mark in the DataGridView, if I try to bind the whole "Student_Course" table to the DataGridView I get other columns I don't need.
private void cboeCID_SelectedIndexChanged_1(object sender, EventArgs e)
{
var CID = Convert.ToInt32(cboeCID.Text);
using (var db = new Entities2())
{
var course = from c in db.Student_Course
where c.CID == CID
select new Class1
{
SID = c.SID,
Mark = c.Mark
};
editDataGridView.DataSource = course.ToList();
Validate();
editDataGridView.EndEdit();
editDataGridView.Update();
}
}
class Class1
{
public int SID { get; set; }
public int Mark { get; set; }
}
There are some important issues in above code:
You shaped the result of query to a custom Class1 which is not your entity type.
You used a DbContext in using statement which means db is disposed after the using statement and will not track changes.
You called SaveChanges on another instance of your DbContext which is not aware of changes, so nothing happens.
To solve above issues consider these tips:
Create db as a field of Form and instantiate it in Load event of Form and use it for both loading and saving data.
You can load data entity this way:
db = new Entities2();
db.Student_Course.Where(x => c.CID== CID).ToList();
editDataGridView.DataSource = db.Student_Course.Local;
You can save data this way:
editDataGridView.EndEdit();
db.SaveChanges();
If you need to use a view model different than your entity for edit, when saving changes you should first load original entities from database using another instance of your context, then for each entity set the value of changed field and then call SaveChanges method.
For more information take a look at these resources:
Entity Framework Databinding with WinForms
Entity Framework Add/Attach Entity States
I have a datagridview which we will call dataGridViewExample.
My object (the uncommon datatypes is because my database is SQLite):
class MyObject
{
public Int64 Vnr { get; set; }
public string Name { get; set; }
public Single Price { get; set; }
public int Amount { get; set; }
}
Here is the relevant code:
//This form gets called with a .ShowDialog(); in my form1.
private List<MyObjecte> ExampleList = new List<MyObject>();
public MyForm()
{
dataGridViewExample.DataSource = OrdreInkøbsListe;
}
private void AddtoDataGridViewExample()
{
//Add a new MyObject to the list
ExampleList.Add(new myObject()
{
Vnr = newVnr,
Amount = newAmount,
Price = newPrice,
Name = newName
});
//refresh datasource
dataGridViewExample.DataSource = null;
dataGridViewExample.Refresh();
dataGridViewExample.DataSource = OrdreInkøbsListe;
ddataGridViewExample.Refresh();
}
When MyForm gets called with a .ShowDialog, it shows up fine and displays my DataGridView example just fine. As you can read from the code, the ExampleListis initially empty, so it just shows an empty datagridview with 4 columns: Vnr, Name, Price & Amount. If I click inside it etc. nothing happens - so everything is working as planned, so far.
Everytime I call AddtoDataGridViewExample() it adds the new object to the Datagridview, and the datagridview does update, listing all the objects added so far (they show themself as rows, again according to plan).
Now, remember that I just said that nothing happened if you clicked inside DataGridViewExample before I have called AddtoDataGridViewExample()?
Well, after having called AddtoDataGridViewExample() once or more, the program will crash if I click inside DataGridViewExample (for example: the users wants to select a row). It throws an IndexOutOfRangeException and talks about an -1 index.
It also throws the exception in the other form, on the line where I call MyForm with .ShowDialog();
I really am stuck on this, do you guys have any idea what is wrong??
My only clue is that I do believe the refresh of DataGridViewExample's datasource might be the cause of the problem.
Another important note: I have yet bound any events to my DataGridViewExample. So you can rule that idea out.
Here is all DataGridViewExample's properties:
this.dataGridViewExample.AllowUserToAddRows = false;
this.dataGridViewExample.AllowUserToDeleteRows = false;
this.dataGridViewExample.AllowUserToResizeColumns = false;
this.dataGridViewExample.AllowUserToResizeRows = false;
this.dataGridViewExample.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.Fill;
this.dataGridViewExample.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.dataGridViewExample.Location = new System.Drawing.Point(591, 53);
this.dataGridViewExample.MultiSelect = false;
this.dataGridViewExample.Name = "dataGridViewExample";
this.dataGridViewExample.ReadOnly = true;
this.dataGridViewExample.RowHeadersVisible = false;
this.dataGridViewExample.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect;
this.dataGridViewExample.ShowEditingIcon = false;
this.dataGridViewExample.Size = new System.Drawing.Size(240, 150);
this.dataGridViewExample.TabIndex = 31;
I guess the click event tries to get the currently selected row and do something with it, while dataGridViewExample.DataSource = null; clears the datasource, and the currently selected row becomes null.
If you set the DataGridView.DataSource to the list, you don't need to reset it to null, refresh, and reset it to the list again (and refresh again) to see the changes. It will be enough to just refresh the DataGridView.
You can also just try using an BindingList<T> object instead of a List<T>, which will automatically notify your grid of its internal changes (Adding and removing elements), and there's also an INotifyPropertyChanged interface you can implement on your MyObject class, that will make every property change in an object show on the grid (For any changes made to the object in the code, and not through the grid itself).
Have you tried running the debugger and break when InedxOutOfRangeException is thrown to see where the exception is thrown?
Select Debug > Exceptions then there's a Find button on the dialog so you don't have to browse through all of the possibilities.
I had similar situation. I assigned generic list of certain object to DataGridView. Then I was setting null to DataSource and after that refresh. After that I assign list of objects to DataSource. While clicked on grid while runtime error occured IndexOutOfRange. My solution was to assign new empty list of my object to that grid and refresh and after changes on my working list I do assign to DataSource and call Refresh. Now, it is working without any crashes. Please look on my code before:
grid.DataSource = null;
grid.Refresh();
if(cases.Count() > 0)
{
grid.DataSource = cases;
grid.Refresh();
}
And now on my code after:
grid.DataSource = new List<MyCase>();
grid.Refresh();
//do something with cases
if(cases.Count() > 0)
{
grid.DataSource = cases;
grid.Refresh();
}