I am using MVVM via Caliburn Micro. I have a datagrid bound to a list of view models. I am running a process in a background thread that updates the datagrid's item viewmodels one at a time.
Everything is working great, the data grid's items update in real time as each of the item viewmodels get updated, just as you would expect.
One small problem: When an item's view model update occurs, the datagrid's layout get mucked up. For example, if I update an item's status from "OK" to "Oh no, something bad happened", that column get re-sized appropriately, but the subsequent columns get shrunk, obscuring the content in them.
If I refresh the entire grid, then everything get laid out appropriately.
Since I am using MVVM, I don't have access to the datagrid control itself, so I cannot use the datagrid's UpdateLayout method directly.
I could use CM's ViewAware view model, but that seems like it should be unnecessary.
Any ideas?
Related
Background:
I use converters to acquire values for most of my binding statement because the bindings are so complicated that even multi-binding cannot satisfy. I have to calculate the value in the converters and return the value. Also I use OneWay or OneTime binding just to show the correct value. When user changes a value, I use Handlers to set the value. The Handlers are also complex program which cannot be simply replaced by TwoWay or OneWayToSource binding. Actually in this case the DataContext does not have any meaning. I use converters. Another reason of using converters is that all Controls are loaded dynamically using many DataTemplates and it's hard to create dynamic DataContext for each Controls in each DataTemplate.
With above background, my application works fine. I'm looking for solutions for our new problem below.
I have many group of Buttons each represent a warehouse containing different type of items.
When double-click a Button, a detail window pops up and user can modify the items. Those items can be represented by CheckBox, Combobox, TextBlocks, TextBox, etc.
For user's convenience, I duplicated some of the frequently-modified Controls from the popup window onto the Button itself (WPF allows Button to contain sub-controls), so that user can directly modify the items without double-click and popup the detail window.
Each Button could contain unknown number of sub-controls such as CheckBox, Combobox, TextBlocks, etc. Here "unknown" means that in the future developer can duplicate any controls onto the Button if the Controls for those items are deemed frequently-modified.
Everything works fine so far.
When user modifies an item in the popup window and closes the window, I used to reload the DataTemplate for the whole window so that everything is refreshed and the controls duplicated onto the Button can synch up with the value modified from inside the popup window.
Everything still works fine so far.
The problem happens when the application runs on machine with slow hardware, where performance is an issue. On a much slower machine, reloading the whole DataTemplate for the whole application that contains many Buttons is quite slow.
So I'm looking for ways to just refresh the Button that is double-clicked, not all Buttons. However, I searched a couple of days and could not find ideal solution of refreshing a WPF sub-tree.
I tried to travel the sub-tree of the Button to assign null to the DataContext property and then assign back the old DataContext, but the binding seems not triggered and the converters were not called.
I saw someone suggested to use something like below:
((ComboBox)sender).GetBindingExpression(ComboBox.ItemsSourceProperty)
.UpdateTarget();
That demands that I know the Control and its property that has bindings. I think I can do the same for all possible Controls and properties but it does not seem a future-proof solution.
Anybody knows an effective way of refreshing a WPF sub-tree without knowing what is in the sub-tree?
I am using LinqToSql as my datasource, let’s say I query a list of programmers from a table and then loop through that data populating an ObservableCollection. (First question, is this part wrong? It just seems weird to query data into a list and then loop through it to populate another list)
Next, when I databind my ObservableCollection of programmers to a ListBox. Then I bind the selectedItem of the Listbox to a TextBox. I understand that when I select an item in the ListBox the textbox will be updated and when I make a change in the textbox, the ListBox will get updated and as a result the ObservableCollection that the listbox is bound to will also be updated.
What I don’t completely understand is what the best practice is to get the data from the OC back into my original datasource. Do I just loop through the observable collection commiting my changes by updating each record? That doesn’t seem terribly efficient.
Thanks so much for your help!
-Josh
This is purely a view-model issue. You have complete control over all the objects and properties on the data side of the fence. Sometimes you have a simple arrangement where the user can only edit a single record, databinding does all the work, and you just write it back to the database when the user clicks save.
If you have a lot more data being displayed and any one piece of it can be modified by the user, then it is probably wise for you to keep a dirty flag in the object itself that records whether any of the properties have been changed. That way, you can efficiently save back just the modified entries.
Since your objects probably support INotifyPropertyChanged and your collections are observable, you can even automatically detect and manage the dirty flag. Or it might be simpler to just set dirty to true in all of your setters.
In any case, this information can even be useful to the user. For example, a user-interface can show unsaved records in bold or with some other convention.
The ObservableCollection is not the only one collection which you could use in wpf. But it is standard collection allows wpf to automatically update ui item containers like ListBox on collection changes like addition or removal of an item. You can't do it with List.
When the user modifies a textbox in ListBoxItem the ObservableCollection is not updated cause you don't add or remove or reorder items in the collection. You change the property in one of the items.
The ListBox contains a list of ListBoxItem containers, one for each item in collection specified as an ItemsSource.
The DataContext of each ListBoxItem is set to the corresponding item stored in ObservableCollection. So, if you change the text in TextBox in code, the binding engine will change the property of that item specified for TextBox.Text in binding. Nothing to change or update for the ObservableCollection object.
In the setter of such item's property the PropertChanged event of INotifyPropertyChanged interface are usually raised. That is also the usual the place to set up a dirty flag. Then you could also commit changes immediately from there, keep some list of dirty objects or search for them on commit like:
items.Where(item => item.IsDirty)
Also there are good tools like Snoop and WPFInspector, which greatly help you to understand the wpf app visual tree and datacontexts for each element
When i use DataGridComboBoxColumn in my WPF DataGrid, the DataGrid SelectionChanged event is triggering multiple times based on the number of rows while loading the rows in the WPF DataGrid.
How can i stop this? Because of this I am facing Performance issue.
It depends on exactly how you have your bindings set up.
If for instance you have an ObservableCollection and you add items to it when you load data, you might run into this kind of problem. There are multiple solutions but I'd rather pinpoint the problem than typing kilometers of text, so if you can provide a bit more details I'll reply with my best guess at a solution.
Edit: After seeing the sample I figured out what the problem is: there's a DataGridComboBoxColumn in the DataGrid, with a SelectedValue binding to a property; when the binding is executed, the SelectionChanged event of the ComboBox is fired, and is caught by the handler on the DataGrid. There are several options to prevent this... one is to check the OriginalSource in the EventHandler, and the other is to handle the event on the ComboBox and to set its Handled property to true so it doesn't get caught by the DataGrid handler as well.
An alternative, much better solution would be to not handle the selection events in the code-behind unless there's a very solid reason. It's best to bind the ItemsSource of the DataGrid to an ICollectionView (ListCollectionView for example) which represents the original collection; the ICollectionView's CurrentItem is automatically synchronized with the selected row in the DataGrid and you can handle selection changed events on the ICollectionView, making it much easier (and unit-testable, somewhat separate from the UI implementation etc.). This doesn't work with multiple selections, but if you can only select a single row at a time it should work quite well.
I am using an MVVM approach.
I have a ViewModel and View called AllSomethingViewModel and AllSomethingView. The View Model contains a list of SomethingViewModels and a SelectedViewModel. The View contains a usercontol bound to the AllSomethingViewModel's SelectedVM property and a listbox control that lets me select a VM. Basically when I pick a new VM the usercontrol's DataContext changes and so the view associated with SomethingViewModel updates with new information.
SomethingViewModel contains a list of objects called ObservableCollection(DataPoints) data.
I have a DataGrid bound to data and columns defined that are bound to data's members. This works fine. I can change views and this datagrid updates and everything is nice.
The issue I am running into is that I would like whatever sorting is applyed to the datagrid to persist when the datacontext changes.
On the View associated with SomethingViewModel, I can subscribe to DataContextChanged event but I'm not sure what To do from there to get the sorting to apply.
For Example. I have 2 SomethingViewModels. So in my list there are 2 options. When i pick the first one I get my datagrid with my data. In the datagrid I decide to sort by DateCreated Ascending order. Then I go to my second VM, the datacontext changes so the data in the grid is updated but it is no longer sorted!
If your sorting is done by the DataGrid then it is stored in the ICollectionView that the DataGrid uses to display its data.
ICollectionView view = CollectionViewSource.GetDefaultView(myDataGrid.ItemsSource);
// Sorting is found in view.SortDescriptions
There's an example of setting your sorting in code here. Hope that's enough to get you going in the right direction
I am using the PagedCollectionView for grouping. I have a DataGrid and a textbox with a search button. The ItemSource of the grid is my PagedCollectionView, and the PagedCollectionView wraps an ObservableCollection because items in the grid can have their bound objects updated by a background process. When you click search, I first clear by ObservableCollection then load it with data from a db. The moment I call clear on my collection, the contents on of the DataGrid disappear, INCLUDING THE COLUMNS. They reappear when items are added to my collection. I would really like the columns and their headers to remain, as when they disappear it is quite jarring, not to mention sort orders, etc are lost. I believe the items disappear instantaneously because it is an ObservableCollection, but I need it to be so that the rows can be updated by the aforementioned background process. If I remove the PagedCollectionView as a wrapper and simply set the ItemSource of the DataGrid to the ObservableCollection, none of this behavior occurs, my columns and headers persist even when the collection is cleared, and my rows update instantaneously by the background process. Has anyone else observed this behavior? Does anyone know any workarounds? Or am I just doing something wrong?
The problem occurred because I was using AutoGenerated columns. Everytime I cleared my collection the PageCollectionView regenerated the columns for me. By turning that property off and using Blend to "pre-generate" the columns for me, I was able to get around the issue.