How to keep properties of separate collections in sync? - c#

I have a WPF application with a screen containing a tab control with two tabs. On each tab is a datagrid, each one bound to an ObservableCollection of Part objects. The part has a few "quantity" properties, which need to be synchronized between the grids. For example, if the user changes the quantity of partABC on grid1, partABC either needs to be added to grid2 with the same quantity, or if grid2 already contains partABC, then its quantity must be changed to reflect grid1.
My problem is that this must work in both directions. If I set a PropertyChanged handler on every part in both grids, I end up with an infinite loop as they constantly update each other's quantities. Up until now, I was handling this during the tab control selection changed event, just iterating through one of the lists and setting quantities one-by-one. This worked well enough until I realized that the users could potentially add thousands of parts to their lists, and at that point this process takes an unacceptable amount of time to complete (around 25 seconds for 4500 part objects).
edit
The first grid contains every part in the database, serving as sort of a "pick-list" where users simply scroll to the part they are looking for and enter quantities. The second grid contains only parts which have been manually entered by the user, in the event that they prefer to type in the numbers of the parts they want. Grid2 is always a subset of grid1.

You can accomplish this through databinding. You should not create duplicate Part objects. Instead duplicate the collections that hold the parts.
Part sharedPart = new Part();
Part onlyInTabA = new Part();
Part onlyInTabB = new Part();
ObservableCollection<Part> tabAParts = new ObservableCollection<Part>() { sharedPart, onlyinTabA };
ObservableCollection<Part> tabBParts = new ObservableCollection<Part>() { sharedPar, onlyInTabB };
Now use tabAParts to databind to the grid on tab A and tabBParts to databind to the grid on tab B
If your Part class implements INotifyPropertyChanged then changing a property of sharedPart will update both grids on both tabs. When you add a new part you can choose to make it shared (add it to both collections) or to keep it tab-specific

Related

Use DataBinding to maintain the alphabetic short between listBoxes

I've been programming some little project in Visual Studio using c# (.NET FRAMEWORK) and in some of my windows forms I want to show in different lisboxes the information about some object.
In my program I got a class named Client that has some Properties: an int called DNI (this is the one that identifies a Client), a string called Name and an int called Telephone. I want a form to show 3 different listboxes, each one with the list of elements of a collection of Client objects. The point is that a Client object has it's Properties showed in the same index of each listBox (to be said, reading horizontally you see the three values of the properties). I have implemented a button in top of each one that alphabetically sorts the elements of the listBox, but I would like to make a Binding between them so When you alphabetically sort one listbox, the other two sort their elements to match the new order of elements in the one alphabetically sorted.
I've been told the class DataBindingscan be used to do this. I've tried searching on the internet about it but achieved nothing and reading the documentation didn't help either, so I ended up posting it here. How can I use DataBindings to solve this? Any help or hint will be appreciated, thanks in advance.
What must I write to bind the indexes of them?
I recommended a datagridview; you seem to have tabular data that you want to show in a tabular fashion.. However, we'll demo all of it.
Easiest route:
Make a new winforms project
Add a DataSet type of file, open it, right click the design surface and choose Add .. DataTable
Name the table Client
Right click it and add column, call it DNI, use the props grid to make its type Int32
Add a string column Name
Add an int column Telephone
Save, switch to Form1 designer
Open Data Sources window (View menu, Other Windows)
Drag the Client node onto the form. A datagridview appears along with some stuff in the tray. Remove the navigator; it's not so useful in this context
Add 3 listboxes to the form
For each listbox:
Use the props grid to set its DataSource to the bindingsource
Set the DisplayMember to a different column - one DNI, the other Name and the third Telephone
That's it for now, run the app. Type some data in the datagridview. Note that when you sort by clicking the DGV header, the listboxes follow; this is because the sort instruction is being carried out by the bindingsource, and all controls bind through the bindingsource:
So that's thngs bound through a bindingsource to a strongly typed datatable, which is conceptually not really any different to what you have. The bit of work making the table and its columns in the design surface is the notional equivalent of making a class with properties. Indeed if you were to open the DataSetBlahBlah.Designer.cs file you'd find a class ClientRow with 3 properties DNI, Name, Telephone
You could change up everything you've made to be based on e.g. a List<Client> but your sorting life becomes slightly more awkward because a BindingSource doesn't understand how to sort it. As such you'd end up with something like:
//class property
private List<Client> Clients = new();
//in constructor
clientBindingSource.DataSource = Clients;
//in button that does sorting:
clientBindingSource.DataSource = Clients.OrderBy(c => c.name); //etc
Personally I'd leave it as a strongly typed datatable; for what you'd want out of a List<Client> a ClientDataTable collection of ClientRow are surface similar enough..
Clients.Where(c => c.Name == "John"); //how you might search a List<client>
ClientsDT.Where(c => c.Name == "John"); //how you might search a ClientDataTable
Clients.Add(new Client(){ DNI = 1, Name = "a", Telephone = 1 } ); //add to a List<Client>
ClientsDT.AddClientsRow(1, "a", 1); //how you might add to a ClientDataTable
etc
If you have already implemented a button for each, you've almost finished.
Every ListBox have to be bound with a List of Client, showing a specific property as DisplayMember.
The button of each ListBox can sort the List and refresh all of the ListBoxes.
If you want more information, please post some of your code.

Make WPF DataGrid create new item only after full-row commit

The code I am working on uses a WPF DataGrid to store a table of entries, bound to an ObservableCollection of items, with the possibility for the user to create new items using the blank row at the bottom of the table.
When the user selects a cell in the new row, right as they type their first keystroke, the DataGrid creates a new blank item and inserts it in the ObservableCollection. Once the user hits Enter or takes focus away from the cell, the new text is committed as an edit to the item.
Is there any way to change the behavior of the DataGrid so that it does not create a blank item before the user is done typing? Or, at least, does not add the blank item to the ObservableCollection until after the row is committed.
I'd like to make it so that new items are only added to the ObservableCollection after a full-row commit has occurred, meaning that each new item added to the Collection will already have data in it, instead of being inserted blank and then edited later.
I've searched high and low for an answer, but it appears that no one else is having this problem.
The reason why I want this functionality is that I am implementing an "Undo" feature in this DataGrid, but when new rows get added as blanks and then edited later, that always ends up counting as two changes, meaning the user needs to perform two "Undo" operations to remove a row they have just barely created. And that's not intuitive at all.
Thanks.
In general it's normal behavior of DataGrid.
DataGrid calls Add() for the new collection item when you start editing the row
DataGrid automatically calls Remove() for the item when you hit Escape key or call CancelEdit() in any other way
There's nothing strange to support (ignore in code) collection items with blank/default values. As alternative you may add some proxy collection and commit its changes to main collection when Datagrid saves the Row data. As for me, first way is preferrable.

Placing the last row used for adding new elements at the top of the data gris

In the data grid control, there's an empty row at the bottom. When a user fills the cells, I can store the contents, hence creating an additional element in the database.
However, when the number of pre-existing rows grows large, the user'd have to scroll each time to access that row. Is there a smooth way to move it up to the top?
The solution I can think of is placing other controls in a panel right above the data grid. But that's more work than I'm willing to spend. Still, it'd be nice to let the users not be forced to scroll their mouses off.
Assuming the collection view returned by your DataGrid's Items property implements IEditableCollectionView (which, in my experience, does for an editable DataGrid), then you should be able to use the NewItemPlaceholderPosition property through the explicit interface:
// Assume myDataGrid is the DataGrid control holding your results.
// You can do this inside your window/control's constructor after its
// call to InitializeComponent and after myDataGrid's ItemsSource
// property has been set.
var collView = myDataGrid.Items as IEditableCollectionView;
if( collView != null )
collView.NewItemPlaceholderPosition = NewItemPlaceholderPosition.AtBeginning

Datagrid on demand load off

I have a SDK:DATAGRID with 20 columns or so, when it opens up it only shows four fields/columns. Which is what I want and how I designed it
Basically I'm grabbing information based on the user click - EXAMPLE:
OWNERNAME.Text = ((TextBlock)EPICGrid.Columns[1].GetCellContent(EPICGrid.SelectedItem)).Text;
and/or
OWNERNAME2.Text = ((TextBlock)EPICGrid.Columns[16].GetCellContent(EPICGrid.SelectedItem)).Text;
What I'm running into it doesn't grab the information in the cell unless if I scroll and show the column so I can only grab the first 4 columns of data because they show when the grid becomes visible.
I can't grab data from columns 5 -20 unless I scoll over and make those columns visible. They don't have to be visible during the click...it just seems like the data doesn't really load until I view the column.
I guess I should say the first record/row loads all the data/cells/columns and I can grab any data from the first record but the problem happens with records 2 - *.
Just to clarify - my issue is not a visibility of my columns or rows. My issue is the SDK DataGrid seems like it is loading the data on demand. So if the column is not in view at one point or another the information in the cell is not available.
I don't want to show all columns and I don't want to give the user the ability to see all columns so I want to disable the scroll bars but when a user clicks on a certain row I need to grab information in certain cells and since the column is not load yet the information is not there.
How do I turn the feature load on demand off?
I did a search and found out that someone had a similar problem with rows loading and the suggestion was setting the VirtualizingStackPanel.VirtualizationMode = Standard
It almost like the problem is stemmed from VirtualizingStackPanel.VirtualizationMode but I set this property to standard and recycle and no luck.
Here's definition:
By default, a VirtualizingStackPanel creates an item container for
each visible item and discards it when it is no longer needed (such as
when the item is scrolled out of view). When an ItemsControl contains
a lot of items, the process of creating and discarding item containers
can negatively affect performance. When
VirtualizingStackPanel.VirtualizationMode is set to Recycling, the
VirtualizingStackPanel reuses item containers instead of creating a
new one each time.
On initial load, if the cell is not visible I can not grab the cells
content (unless it is the first record/row). Once and after the cell /
column is visible then the information is available.
I think you are expected to deal with the data that the row is bound to directly and not pull the data out of the controls. This makes sense since it is two way data binding so the data is updated as you are changing it (assuming it implements the INotifyPropertyChanged Interface).
An example would be where a datagrid is bound to a collection of type MyEntity.
private void DataGrid1_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (this.DataGrid1.SelectedItem == null)
return;
MyEntity myEntity = (MyEntity)this.DataGrid1.SelectedItem;
// at this point you have the (updated) data the row is bound to.
MessageBox.Show("You Selected: " + myEntity.name);
...
Another example is where there is a button on each row. The code to process when a button is clicked looks something like:
private void btnProcessEntity_Click(object sender, RoutedEventArgs e)
{
Button btn = sender as Button;
MyEntity myEntity = btn.DataContext as MyEntity;
// clicking a button in a row doesn't select the row, so select it.
this.DataGrid1.SelectedItem = myEntity;
MessageBox.Show("Will Process: " + myEntity.name);
...
}
If you are not familiar with some of the technologies that normally are used with Silverlight check out these Video Tutorials. It is VB.Net, but the code is really not the focus - it is focused on a Silverlight application architecture. I would start with "Intro to SL4 and WCF Ria Services" and then view one of the ones on MVVM.

How To Hide ListView ColumnHeader?

I am struggling to figure out the correct control to use for a list of predefined jobs in the included form. I currently have a ListBoxControl in the Predefined Job Name group that lists all of the predefined jobs for a marine service shop (i.e. oil change, tune up, etc...). Then, based on the item (i.e. job name) that is selected in my ListBox, I need to display the items that correspond to that job. For example, if oil change is the selected job I need to show 4 quarts oil, 1 oil filter, labor, etc...and so on.
Currently, when I load the form data I have a DAO that retrieves all of my jobs from the database using LINQ to SQL. Then I iterate over the results and put the job names into the ListBox. The problem that I am having is that there is no tag for ListBox items like there is for ListView items. So each time the user selects another item in the ListBox, I have to perform another LINQ query to get the job from the database again so that I can display its' corresponding items. If I could use a ListView and hide the column header I could set the entire job on the tag so that each time the user selects a new item I would have access to the details without having to make another call to the database. Is there a way that I can hide the column header of a ListView without hiding the entire column?
You can set the HeaderStyle member of the ListView to None.
listView1.HeaderStyle = System.Windows.Forms.ColumnHeaderStyle.None;
Checkout the ListView HeaderStyle property. It has the following options:
None
Nonclickable
Clickable
From MSDN:
The HeaderStyle property allows you to specify whether the column headers are visible or, if they are visible, whether they will function as clickable buttons. If the HeaderStyle property is set to ColumnHeaderStyle.None, the column headers are not displayed, although the items and subitems of the ListView control are still arranged in columns
You can also create simple object like ListItem which has two poperties: Text (string) and Tag (object). Then implement ListItem.ToString() and you can use these in the ListBox as well.
You can also check out Better ListView Express component, which is free and allows displaying items in Details view without columns. The advantage over ListBox and ListView is a native look and many extra features.
Easy way is using the ColumnWidthChanging event
private void listViewExtended1_ColumnWidthChanging(object sender, ColumnWidthChangingEventArgs e)
{
if (e.ColumnIndex == 0)
{
e.Cancel = true;
e.NewWidth = listViewExtended1.Columns[e.ColumnIndex].Width;
}
}
I found that if you know for a fact you are not displaying the headers it may be best to set the HeaderStyle property to None, as Rajesh mentions above.
When setting in the .CS when screen initially loads the headers are displayed until screen is fully rendered.

Categories

Resources