I have a grid which i load the data as following
Method i without Sorting
public partial class frmSalesOrderDetails : BaseForm.TfrmList
{
// DECLARE OBJECTS
private BusinessObject.TransactionLayer.SalesOrderMaster SalesOrderMaster = new BusinessObject.TransactionLayer.SalesOrderMaster();
private ObservableCollection<BusinessObject.TransactionLayer.SalesOrderDetail> ocSalesOrderDetails { get; set; }
// DATA BINDING TO GRID
private void DataBind_grdSalesOrderDetail(int InvoiceId)
{
// FILL OBJECT SalesOrderMaster , it is based on POCO so it also contains SalesOrderMaster.SalesOrderDetails
SalesOrderMaster = (BusinessObject.TransactionLayer.SalesOrderMaster)(_Context.SalesOrderMaster.Where(t => t.InvoiceId == InvoiceId).FirstOrDefault());
//Fill the Grid's datasource
grdSalesOrderDetail.DataSource = SalesOrderMaster.SalesOrderDetails.ToBindingList().ToBindingList();
}
Mehod 2 with sorting
public partial class frmSalesOrderDetails : BaseForm.TfrmList
{
// DECLARE OBJECTS
private BusinessObject.TransactionLayer.SalesOrderMaster SalesOrderMaster = new BusinessObject.TransactionLayer.SalesOrderMaster();
private ObservableCollection<BusinessObject.TransactionLayer.SalesOrderDetail> ocSalesOrderDetails { get; set; }
// DATA BINDING TO GRID
private void DataBind_grdSalesOrderDetail(int InvoiceId)
{
// FILL OBJECT SalesOrderMaster , it is based on POCO so it also contains SalesOrderMaster.SalesOrderDetails
SalesOrderMaster = (BusinessObject.TransactionLayer.SalesOrderMaster)(_Context.SalesOrderMaster.Where(t => t.InvoiceId == InvoiceId).FirstOrDefault());
// SORT THE DATA on a temp ocSalesOrderDetails2
ObservableCollection<BusinessObject.TransactionLayer.SalesOrderDetail> ocSalesOrderDetails2 = new ObservableCollection<BusinessObject.TransactionLayer.SalesOrderDetail>(SalesOrderMaster.SalesOrderDetails.OrderBy(t => t.STFicheLineNo));
// Fill ocSalesOrderDetails with ocSalesOrderDetails2's data
ocSalesOrderDetails = new ObservableCollection<BusinessObject.TransactionLayer.SalesOrderDetail>(ocSalesOrderDetails2);
//Fill the Grid's datasource
grdSalesOrderDetail.DataSource = ocSalesOrderDetails.ToBindingList();
}
So the problem is as following : When I use Method 1 and use the following code to remove (delete ) a SalesOrderDetail :
base._Context.SalesOrderDetail.Remove(((BusinessObject.TransactionLayer.SalesOrderDetail)((DevExpress.XtraGrid.Views.Grid.GridView)grdSalesOrderDetail.MainView).GetFocusedRow()));
All is in sync, the record is removed from Context, and grid datasource When I use Method 2 and use delete the same way : There is no more sync, the record is not deleted from the grid datasource
Could anybody help please.
If you follow this link, ObservableCollection Constructor you'll see that the constructor actually creates a copy of the collection passed to it. So for your method1, when you delete a row, SalesOrderMaster from _context got changed which was instantly reflected in your object, SalesOrderMaster used as the DataSource.
For method2, the same is still happening, except, now in the memory, you have 2 copies of the same data. The _context value is still getting updated but that has got nothing to do with the object ocSalesOrderDetails which has it's own set of data used as DataSource. Actually, your grid is in sync with this variable instead of the actual result.
Easy way out is to call this method
DataBind_grdSalesOrderDetail(int InvoiceId)
after each delete operation to keep your grid both, in sync as well as sorted.
Related
I have a windows form with a ComboBox DisplayBox. In my ViewModel I now have a Property BindingList<MyObject> ObjectBindingList that I want to bind to the DisplayBox.
When I load the form, the DisplayBox does not show any text.
The property DataSource is set and holds a List of MyObjects when checking in the debug modus after the data download.
The property items always has a count of zero.
My code works as following:
On startup I set the databindings in the form class to a still empty List ObjectBindingList.
displayBox.DataSource = ObjectBindingList;
The DisplayMember and ValueMember were set in the ComboBox Properties in the GUI Designer.
Asynchrously the controller downloads some data (MyDataObjects) async. Then sets the BindingList<MyObject> ObjectBindingList in the ViewModel to the downloaded Objects through adding them.
Since I don't see all of the relevant code, I can only assume what's happening.
Probably, you don't see the data in the ComboBox, because you are creating a new BindingList when loading the data. But the ComboBox is still attached to the old empty list.
You initialize the data source with an empty list like this:
// Property
BindingList<MyObject> ObjectBindingList { get; set; }
Somewhere else
// Initializes data source with an empty `BindingList<MyObject>`.
ObjectBindingList = new BindingList<MyObject>();
displayBox.DataSource = ObjectBindingList;
Later, you load the data and replace the list:
ObjectBindingList = LoadData();
Now, you have two lists: the initial empty list assigned to displayBox.DataSource and a new filled one assigned to the property ObjectBindingList. Note that displayBox.DataSource does not have a reference to the property itself, therefore it does not see the new value of the property.
For a BindingList<T> to work as intended, you must add the items with
var records = LoadData();
foreach (var data in records) {
ObjectBindingList.Add(data);
}
I.e., keep the original BindingList<MyObject> assigned to the data source.
See also: How can I improve performance of an AddRange method on a custom BindingList?
To avoid the problem, I would be advisasble to make the property read-only (using C# 9.0's Target-typed new expressions).
BindingList<MyObject> ObjectBindingList { get; } = new();
It seems like when trying to update the ComboBox from a different thread than the main forms thread, the update did not reach the control.
I am now using the Invoke Method together with a BindingSource Object in between the Binding List and the control.
private void SetBindingSourceDataSource( BindingList<MyObject> myBindingList)
{
if (InvokeRequired)
{
Invoke(new Action<BindingList<MyObject>>(SetBindingSourceDataSource), myBindingList);
}
else {
this.BindingSource.DataSource = myBindingList;
}
}
I am expeciall calling the above function on a PropertyChanged event, that I trigger at the end of every call of the download Function.
I have bound a DataGridView to an SQL Server table in a .Net 5.0 WinForms project. Displaying the data works well.
I would like to update editions to the database as soon as I move to another row in the DataGridView. But I have not found a way to do this.
The solution presented here seems not to work with an OleDbDataAdapter. The Update method does not accept a DataRow. Examples in DOCS require a DataSet which I try to avoid. Other examples use a button to save changes.
The data gets loaded like this:
var dataAdapter = new OleDbDataAdapter(sqlQueryString, connString);
var dataTable = new DataTable();
dataAdapter.Fill(dataTable); // fill data table from SQL server
var bindingSource = new BindingSource();
bindingSource.PositionChanged += new System.EventHandler(bindingSource_PositionChanged);
bindingSource.DataSource = dataTable; // connect binding source to data table
dataGridView.DataSource = bindingSource; // connect DataGridView to binding source
For the update I finally have tried this:
private void bindingSource_PositionChanged(object sender, EventArgs e)
{
DataRow dataRow = ((DataRowView)((BindingSource)sender).Current).Row;
if (dataRow.RowState == DataRowState.Modified) // this is successful
{
dataAdapter.Update(dataRow); // compile error
}
}
I get the compile error
Cannot convert from 'System.Data.DataRow' to 'System.Data.DataRow[]'.
Any hint is appreciated.
MVVM
In modern programming, there is the tendency to separate the model from the view. This separation makes it easier to change the way that your data is displayed without having to change your model. You can also change parts of the model without having to change the display. It is easier to reuse the model and to unit test it without having to start a forms program.
In WPF this separation between model and view is almost enforced. When using winforms you have to take care that you do not mix them more than needed.
To keep these two separated, adapter code is needed to glue your model to your view. This adapter code is quite often called the viewmodel. the abbreviation of these three is quite often called MVVM. Consider to familiarize yourself with the ideas of MVVM.
Use a BindingList in your DataSource
If you want to separate your model from your view, you need methods to fetch the data that must be displayed from the database, and data to update items.
I don't know what you will be displaying in your DataGridView, but let's assume it is a sequence of Products, something like this:
class Product
{
public int Id {get; set;}
public string ProductCode {get; set;}
public string Name {get; set;}
public string Description {get; set;}
public decimal Price {get; set;}
...
}
You will have methods to fetch the Products that must be displayed, and to Update one Product, or maybe several Products at a time:
IEnumerable<Product> FetchProductsToDisplay(...)
{
// TODO: fetch the products from the database.
}
void UpdateProduct(Product product) {...}
void UpdateProducts(IEnumerable<Product> products) {...}
Implementation is out of scope of this question. By the way, did you notice, that because I put fetching and updating data in separate procedures, I hid where the Products are saved? It can be in an SQL server, but if you want it could also be a CSV or XML file, or even a dictionary, which could be handy for unit tests.
Besides: you can unit tests these methods without using your forms.
Using the visual studio designer you have added the columns and defined which column should show which Product property. You could also have done this in the constructor using property DataGridViewColumn.DataPropertyName
public MyForm()
{
InitializeComponents();
this.columnProductCode.DataPropertyName = nameof(Product.ProductCode);
this.columnName.DataPropertyName = nameof(Product.Name);
...
}
You don't need to set the DataPropertyName for properties that you won't show anyway.
Now to display the products, it is enough to assign the Products to the DataSource of the DataGridView:
var productsToDisplay = this.FetchProductsToDisplay(...);
this.dataGridView1.DataSource = productsToDisplay.ToList();
This will display the products. However, changes that the operator makes: Add / Remove / Edit rows are not updated. If you need this functionality, then the Products need to put in an object that implements IBindingList, like (surprise!) BindingList<Product>:
private BindingList<Product> DisplayedProducts
{
get => (BindingList<Product>)this.dataGridView1.DataSource;
set => this.dataGridView1.DataSource = value;
}
To Initialize the DataGridView:
private void DisplayProducts()
{
this.DisplayedProducts = new BindingList<Product>(this.FetchProductsToDisplay().ToList());
}
Now whenever the operator makes any change to the DataGridView: Add / Remove rows, or change the Displayed values in a row, these changes are reflected in DisplayedProducts.
If for instance the operator clicks Apply Now to indicate he has finished editing the products:
private void OnButtonApplyNow_Clicked(object sender, ...)
{
var products = this.DisplayedProducts;
// find out which Products are Added / Removed / Changed
this.ProcessEditedProducts(products);
}
Of course you can Add / Remove / Change displayed products programmatically:
void AddProductsToDisplay()
{
Product product = this.DisplayedProducts.AddNew();
this.FillNewProduct(product);
}
Back to your question
Ask yourself: Is it wise to update the database as soon as the position is changed?
If the operator starts typing, then remembers he can copy-paste items, he will stop typing, go to other controls to copy, and then continue editing the cell by pasting. Maybe he goes to other rows to look at information to decide what to put in the cell.
Another scenario: the Descriptions of Product A and Product B need to be exchanged. Think of the operator actions needed for this. When would it be wise to update the database? When are you certain that the operator is content with the new data?
Hence it is not wise to update the database as soon as a row is edited. The operator should explicitly indicate he has finished editing.
private void OnButtonOk_Clicked(object sender, ...)
{
var products = this.DisplayedProducts;
// find out which Products are Added / Removed / Changed
this.ProcessEditedProducts(products);
}
Further improvements
Once you've separated your data (model) from the way this data is displayed (view), using the DataSource, it is quite easy to access the Product that is displayed in the current row or in the selected rows:
Product CurrentProduct => (Product) this.dataGridView1.CurrentRow?.DataBoundItem;
IEnumerable<Product> SelectedProducts = this.dataGridView1.SelectedRows
.Cast<DataGridViewRow>()
.Select(row => row.DataBoundItem)
.Cast<Product>();
you can use foreach loop.
private void AddInfo()
{
// flag so we know if there was one dupe
bool updated = false;
// go through every row
foreach (DataGridViewRow row in dgv_Purchase.Rows)
{
// check if there already is a row with the same id
if (row.Cells["Pro_ID"].ToString() == txt_ProID.Text)
{
// update your row
row.Cells["Purchase_Qty"] = txt_Qty.Text;
updated = true;
break; // no need to go any further
}
}
// if not found, so it's a new one
if (!updated)
{
int index = dgv_Purchase.Rows.Add();
dgv_Purchase.Rows[index].Cells["Purchase_Qty"].Value = txt_Qty.Text;
}
}
Finally I've found the 2 missing lines:
private SqlCommandBuilder commandBuilder; // on UserControl level
commandBuilder = new SqlCommandBuilder(dataAdapter); // when loading data
A book has helped me: Michael Schmalz, C# Database Basics, O'Reilly
It is strange that the DOCS reference of SqlDataAdapter doesn't mention the SqlCommandBuilder.
Thanks to everybody who has spent precious time for a New contributor.
I just noticed that I can't refresh the values fetched from the DB. Storage (i.e. from the client to the DB) works as supposed to. Loading the first time works as charm as well.
However, if someone deletes a row in the DB (say, using SQL Management Studio), that change isn't in effect in my client until I reinstantiate the whole view model. Only calling Refresh() doesn't fetch the change. The same goes for altering the values of loaded in records.
However, additions to the table are brought in...
I (re)load the values shown in the view from the DB by calling the following method in the view model.
public ViewModel()
{
Reload();
...
}
public void Reload()
{
_data.Set<Order>().Load();
_data.Set<TimeFrame>().Load();
Orders = _data.Set<Order>().Local;
TimeFrames = _data.Set<TimeFrame>().Local;
...
}
readonly Data _data;
private ObservableCollection<Order> _orders;
private ObservableCollection<TimeFrame> _timeFrames;
public ObservableCollection<Order> Orders
{
get { return _orders; }
set { _orders = value; OnPropertyChanged("Orders"); }
}
public ObservableCollection<TimeFrame> TimeFrames
{
get { return _timeFrames; }
set { _timeFrames = value; OnPropertyChanged("TimeFrames"); }
}
What am I missing?
Here the problem is that you load your values, then you use the local property.
The Local collection contains all the values that have been loaded in your context.
if the first time, you load values (1,2,3,4) your Local collection will contain values (1,2,3,4). The second time you load it, you will perhaps load values (1, 4, 5) your Local collection will contain values (1,2,3,4,5)
you should do something like
Orders = new ObservableCollection(_data.Set<Order>());
if it is a read only scenario and you don't need to update and save your data, you should even load your data AsNoTracking, so you won't have caching issues and you will have less EF overhead as your entities won't be tracked
Orders = new ObservableCollection(_data.Set<Order>().AsNoTracking());
I have problem with ordering data for ListView. I have EventDisplay class which is an ObservableCollection for ListView(called Events)
private ObservableCollection<EventDisplay> currentEvents = new ObservableCollection<EventDisplay>();
private void Events_Loaded(object sender, RoutedEventArgs e)
{
sv = (ScrollViewer)VisualTreeHelper.GetChild(VisualTreeHelper.GetChild(this.Events, 0), 0);
Events.ItemsSource = currentEvents;
}
I then add new data by function :
private void LoadDataToList(List<EventDisplay> newItems)
{
foreach (EventDisplay ed in newItems)
{
//Set some additional data
currentEvents.Add(ed);
}
//When this line below is commented ListView data is updated
//but is not sorted, when i uncomment the listview data is not being updated
//currentEvents = new ObservableCollection<EventDisplay>(currentEvents.OrderByDescending(x => x.ed.date).ToList());
}
So what is the proper way of ordering data for ListView in Windows 8.1 apps ?
You can sort & filter the view of your ObservableCollection (explanation here)
public class ViewableCollection<T> : ObservableCollection<T>
{
private ListCollectionView _View;
public ListCollectionView View
{
get
{
if (_View == null)
{
_View = new ListCollectionView(this);
}
return _View;
}
}
}
Data structure for the example:
interface ICustomer
{
string CuctomerName{get;set;}
int Age{get;set;}
}
Example use of the code:
ViewableCollection<ICustomer> vCustomers = new ViewableCollection<ICustomer>();
// Sorting settings:
ViewableCollection<ICustomer> vCustomers.View.SortDescriptions.Add(new SortDescription("CustomerName", ListSortDirection.Ascending));
vCustomers.View.Filter = MyCustomFilterMethod;
// add data to collection
MyCustomers.ToList().ForEach(customer => vCustomers.Add(customer));
Examlpe filter method:
private bool MyCustomFilterMethod(object item)
{
ICustomer customer = item as ICustomer;
return customer.Age > 25;
}
when you need to refresh the filter, the only thing you need to do is call:
this.vCustomers.View.Refresh();
Then you bind your GUI to vCustomers.View
You don't need to reset binding sources etc.
Use this for your add items code:
foreach (EventDisplay ed in newItems.OrderByDescending(x => x.ed.date).ToList()
{
//Set some additional data
currentEvents.Add(ed);
}
The reason your doesn't work is that you are reassigned the currentEvents reference rather than updating the ObservableCollection.
You should do the following :
currentEvents = new ObservableCollection<EventDisplay>(currentEvents.OrderByDescending(x => x.ed.date).ToList());
Events.ItemsSource = currentEvents;
This forces the ListView to rebind to your new sorted observable collection.
Another option is to sort the Observable collection in place. However, it may introduce flickering as the ListView will constantly update as the sort progresses.
If you don't want the ScrollView to reset its position, you can save the scrollview position and then restore it after sorting the list.
I've had success with Implementing a custom ObservableCollection that supports sorting but prevents UI flickering by suspending change notification during sort and then issuing a reset notification. The ScrollView should stay at its current position even when confronted with the reset event.
I'm still in the learning Phase of WPF, EF and MVVM and now I got the following problem. I can delete and insert new items in my DataGridView but I don't know how to update my items.
All I do is select an emptyrow which already has a primary key and then I put the data into it. It's working (updating database) but the GridView is not refreshing. I Need to restart the program first to see my updated data.
My Execute Command to Update my Database. I'm in the ViewModel class
public void ExecuteUpdate(object obj)
{
try
{
SelectedIndex.Child_Update(new Farbe { FarbauswahlNr = SelectedIndex.FarbauswahlNr, Kurztext = SelectedIndex.Kurztext, Ressource = SelectedIndex.Ressource, Vari1 = SelectedIndex.Vari1, Vari2 = SelectedIndex.Vari2 });
//ListeAktualisieren --> Refreshing the List
ListeAktualisieren();
}
catch (Exception e)
{
MessageBox.Show(e.ToString());
}
}
Here is my Refresh Method which SHOULD Refresh the GridView. I'm in the ViewModel class
public void ListeAktualisieren()
{
farbliste.ListeAktualisieren(db);
farbliste.Model = farbliste.Model.Concat(farbliste.Addlist).ToList();
Model = farbliste.Model;
farbliste.Addlist.Clear();
}
The method is calling my Business List which also got a Refresh Method. Reading from my database here. I'm in the Business List class
public void ListeAktualisieren(TestDBEntities db)
{
Model.Clear();
foreach (var item in db.Farben)
{
//Insert and delete working
add = new Farbe { FarbauswahlNr = item.FarbauswahlNr, Kurztext = item.Kurztext, Ressource = item.Ressource, Vari1 = Convert.ToBoolean(item.Var1), Vari2 = item.Vari2 };
Addlist.Add(add);
}
}
Model is the Source of my GridView which is not Refreshing changed data when Updated but is showing new data rows when inserting or deleting.
You need Observablecollections and Classes with implemented INotifyPropertyChanged. Add the new element to the Observablecollection by insert and raise the event propertychanged by a change.
The rest should be done by WPF.
Edit: The Sourcecollection for the DataGrid needs to be the Observablecollection.
Edit2: To be nice I put the result of the comments here ;-)
Each row of the DataGrid is an element of the collection. Each cell of one row listens to a PropertyChangedEvent of its element (the String is Casesensitive so be carefull). If the getter of the property isn't called after the propertychangedevent the binding didn't receive the event.
This piece of Code can help asure that you don't call with nonexistent strings:
private void VerifyPropertyName(string PropertyName)
{
if (string.IsNullOrEmpty(PropertyName))
return;
if (TypeDescriptor.GetProperties(this)(PropertyName) == null) {
string msg = "Ungültiger PropertyName: " + PropertyName;
if (this.ThrowOnInvalidPropertyName) {
throw new isgException(msg);
} else {
Debug.Fail(msg);
}
}
}
Try adding this to your binding section
ItemsSource="{Binding Path=Model, UpdateSourceTrigger= PropertyChanged"}