MVVM WPF datagrid data entry problem - wpf

I have a datagrid in my MVVM application which, because of the way the client wants the data displayed, needs use template columns. They want some typical data-entry functionality though (pressing enter performs data checking, commits the row if valid, focuses on the first textbox of the next row; pressing tab focuses the next textbox...). Also, data is often imported from an external source into the grid, usually thousands of records at a time.
Right now, I have a Loaded event hooked up to each textbox which I am using to set focus after new rows are added. My problem is that the grid goes haywire when I import a lot of rows. As the user scrolls, the Loaded events are fired, and the grid becomes basically unusable. I disabled virtualization to prevent this, and find my grid taking up a gig of RAM in certain configurations, which is unacceptable. I can't figure out a way to get this grid to work the way they require without using a huge amount of memory. It seems as if I just need to be able to focus a textbox within a newly added row, but since the data validation is performed in the viewmodel, I don't have access to the new row in codebehind, so I can't just call "newtextbox.focus()" or whatever. I'm getting pretty desperate here, any suggestions would be massively appreciated.

Put an event listener in the code behind that can call your newtextbox.focus() (and whatever else you want to do). In the validation in the view model, fire the event with args that indicate what you want your grid to do.
Edit: OK, new approach. Try trapping the keystrokes, and on enter or tab do the things you want it to do.
This would be in your xaml
<Grid KeyUp="myDataGrid_KeyUp" >
This would go in your code-behind
private void myDataGrid_KeyUp(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
// do your enter stuff here, manipulate the view model for validation, etc.
}
else if (e.Key == Key.Tab)
{
// do your tab stuff here (get selected row, determine cell focus, focus on next cell)
}
}

Related

Detecting a BindingSource SortChanged Event in order to implement 2 way binding in winforms

I am trying to write a 2 way binding application using C# winforms
I have a BindingNavigator and a DataGridView bound to the same data source
So that I can have 2 way binding ( yes in winforms ) I want to be able to detect when the bindingSource sort order has changed.
When I click the column header of the DataGridView the sort order of the grid does change
and the BindingSource.ListChanged event does fire
However the bindingSource.Sort remains null
When the grid column header is clicked the BindingSource.ListChanged event does fire
private void bindingSource_ListChanged(object sender, ListChangedEventArgs e)
{
if (e.ListChangedType == ListChangedType.Reset)
{
if (bindingSource.Sort != null)
{
controller.Sort = bindingSource.Sort; // never gets here. Why?
}
}
}
Why isn't clicking the DataGridView Column header setting the bindingSource.Sort property?
What event should I be listening to in order to detect that the bindingSource sort has changed?
Having come across this problem (for a different reason as in multi column sorting) I found it amazing that the Bindingsource doesn't have a sort changed event nor that the Datagridview's own Sorted event doesn't fire with it (it just seems to fire when it does the sorting).
The datagridview "knows" and even changes its sort glyph position.
The way I solved this won't suit most because it is specific to the task I am coding. Within my multi column sort routine I keep track of the bindingsource.sort string and then on a regular event within the Datagridview (currently trying SelectionChanged) I compare my sort string with the current Bindingsource one and switch off my multi-column sort indicators when it is different. This is ok for me because all my Datagrids are based on a common custom control.
The only other way I thought of would be to create a custom control based upon the Bindingsource and add a sort method (that calls the bindingsource sort method) and then trigger my own event. This would be fine if starting out on a new project but changing a large program becomes a pain.
It is sad given that I made a custom control from the start for every UI control (Winforms) but never thought to make a general one for Data Access.
Hope this helps someone facing the same issues.

DataGridViewCheckboxColumn not setting underlying data

I have a DGV bound to an IEnumerable. T has a boolean property which can be get/set. When loading the table, the get property is accessed to populate the DataGridViewCheckboxColumn. I can see it being hit in the debugger. When I click a cell in the checkbox column, the checkbox is checked as you'd expect, but the underlying data source isnt' updated, and the property setter isn't called. The checkbox column has the ReadOnly property set to false.
How can I get this to update the underlying bound data?
polygonListBindingSource.DataSource = m_displayPolygons.OrderBy(p => p.Name);
I've seen related questions, but they're not consistant in their answers. Some suggest calling EndEdit on the DGV, other suggest calling it on the binding source. Do I really have to subscribe to an event for when the cell value is changed to actually commit the change to the underlying data type? This seems unnatural.
This is down to the fact that the DataGridView only commits its edits when the current cell loses focus. Clicking on the checkbox isn't enough to commit the edit.
The standard was to do this is to use events, usually the CurrentCellDirtyStateChanged event:
void dataGridView1_CurrentCellDirtyStateChanged(object sender, EventArgs e)
{
if (dataGridView1.IsCurrentCellDirty)
{
dataGridView1.CommitEdit(DataGridViewDataErrorContexts.Commit);
}
}
It is a bit unnatural but is by design - this way your data source doesn't get flooded by edits that may not be final. Not necessarily such a big deal for a checkbox but a worthwhile design decision for the text box cell.

How to handle events generated by Grid Splitter in WPF?

I want an event handler that handles the event when the grid splitter is being moved, Im not sure if there is one, if not, I guess I can generated an event when the size of the rows are changed?
Thanks.
You could do the rows changing size, but GridSplitter itself is a Thumb and so has its own events such as DragStarted and DragCompleted. More details here.
Edit: If you make the GridSplitter focusable and allow it to be moved with the keyboard, read the answer by Benlitz for more information.
I didn't tested, but I'm pretty sure that the currently accepted answer from AresAvatar won't work if you're resizing the rows/columns using keyboard arrows (by giving the focus to the grid splitter). This is a rare but possible case that you should anticipate in your application.
When the grid splitter is moved (either by drag'n'drop or using keyboard arrows), it changes the Width/Height dependency properties of the ColumnDefinitions/RowDefinitions of the grid. You can easily register a handler on this property change:
var heightDescriptor = DependencyPropertyDescriptor.FromProperty(RowDefinition.HeightProperty, typeof(ItemsControl));
heightDescriptor.AddValueChanged(myGrid.RowDefinitions[0], HeightChanged);
(This snippet will for instance track size change in the first row of the grid).
Then you can handle resize in an handler that will work in every case.
private void HeightChanged(object sender, EventArgs e)
{
// TODO: handle row resize
}
Generally, it is really not advised to rely on the user input action (mouse dragging, keyboard inputs...) to handle a logical or visual actions/events, since there are almost always several ways to do the same actions using different inputs (mouse, keyboards, touchscreen, ease-of-use tools...).
Use DragDelta. Be aware that if you want to change the size, use as the Actual property as the current size like ActualWidth instead of Width.

Databinding falls behind event notification - discussion

Found an interesting problem that I first found in WinForms, and found again in Silverlight, and more than likely WPF as well when it comes to databinding.
I have a tab control with several tabs. As users click across the tabs, each time should be valid before allowing the user to switch from the tab.
For example, user is in a text box which is updated. Binding of text boxes is not flushed until the control loses focus. Loss of focus occurs when the cursor is moved from the control, and focus is given to another control.
In this scenario, the user tabs into a control (let's use text box for this example), and updates the text box. At this point the databinding has not flushed the control, and hence the VM has not yet seen the change. The user then uses their mouse to click the next tab of the control.
At this point things get interesting. I used the PreviewSelectionChanged (Telerik RadTabControl), as I want to check things out before the jump to the next tab occurs, and it also gives me the ability to cancel the event.
However, when I look at the VM, in this event, it still does not have the updated data. I see the VM is clean, and go ahead and allow the jump to the next tab.
As soon as this event is over however, the databindings flush, and the VM gets updated. what now? The events are out of sync! When the mouse was used to click the next tab, the textbox should have lost focus, flushed it's bindings, before the Preview of the Tab click! It's to late to jump back and say oops we didn't catch that in time!
I think I found an interesting work around to this issue - but I'm not 100% sure it will work 100% of the time. I cancel the current event, but then I use the Dispatcher and create a delegate pointing to another method with the same signature as the current event. The Dispatcher will add this message to the message pump, which by this time will now (hopefully?) be behind the messages of the VM updating...
My two questions are:
1) I'm assuming that the textbox control either didn't flush when the mouse left the control, or the process that was fired was too slow and hence the preview message was on the pump before the databinding - either way I see this to be a major issue.
2) Is the workaround a good solution?
Ok, first to answer question 1:
Just because the mouse left the textbox area, doesn't mean that the textbox lost focus. It only loses focus once something else gets focus. For example, if you moved the mouse out of the textbox and click on some other control on your page (it can be anything from a scroll viewer to another textbox, etc.) then your textbox will lose focus.
Now, based on that, the events do not happen in the wrong order. What happens is; your click event on the other tab triggers both the textbox to lose focus (and the data binding to take place) and the move to the next frame, and based on that, you basically get a race condition where the moving to the next tab happens before the databinding takes place.
About question 2:
What you can do is, set the UpdateSourceTrigger to Explicit, you will however be forced to then have some kind of text_changed event and manually update the binding.
You can read more about that here. It might not be the most complete explanation but is a good place to start.
On the other hand, you can associate some events to the textbox and force the textbox to lose focus on those events (e.g. mouse out).
Just an idea: Why not do everything in the VM's PropertyChanged event?
protected override void OnThisViewModelPropertyChanged(object sender, PropertyChangedEventArgs e) {
if(e.PropertyName == "WhateverProperty") {
//Do your magic here for whatever you want to set
}
}
Have your TabItems bound to a collection that will control is being disabled or not.
<sdk:TabControl>
<sdk:TabItem IsEnabled="{Binding SomeProperty, Converter={AmIDisabledOrWhatConverter}}" />
</sdk:TabControl>
That way, everything is triggered whenever a property is chaned in the vm. No more timing issues since everything is on the vm.
Just my two cents.
There's a design defect here, and you're trying to work around the defect instead of fixing it. You shouldn't have to figure out how to cancel the Click event on the tab. The tab shouldn't be processing Click events in the first place.
Generally speaking, if it's not legal for the user to click on a control, the control shouldn't be enabled. The tab should be disabled until the state of the view model is valid.
Your view model should be exposing a command for navigating to the next tab, and the tab should be bound to the command. The command's CanExecute method should only return true when the state of the view model on the current tab is valid.
This doesn't fix your other problem, which is that Silverlight doesn't support UpdateSourceTrigger="PropertyChanged" out of the box. But that's a solved problem (here is one example).
Note that if you implement commands to handle this wizard-like navigation in your application, you can, down the road, change the view to use something other than a tab control (e.g. to use navigation buttons like an actual wizard, or something like Telerik's PanelBar) without having to screw around with event handlers.
Change your bindings to include UpdateSourceTrigger="PropertyChanged".
This will ensure that your data sources are updated on every key stroke, not just LostFocus.
MyOwnTextBox()
{
this.TextChanged += (s, e) => UpdateText();
}
private void UpdateText()
{
BindingExpression be = GetBindingExpression(TextProperty);
if (be != null && be.ParentBinding.Mode == BindingModes.TwoWay)
{
be.UpdateSource();
}
}
I am using this class it updates my binding on the fly, however there is issue with empty string and null values, if your destination property is nullable string then this will update empty string in your destination property. You can get workaround by using some sort of string converter which will use null instead of empty string in case of nullable strings.

How to fix selected Item in a collection control on a user control (using the MVVM implementation) in WPF

I hope someone can help me...
Josh Smith did a great article on "WPF apps with the Model-View View-Model", and included in his article was the following code sample.
If you download the code sample and run the app and view all customers, then select a company (e.g. 4th company), then click "Create new customer" (which will open a tab of the new customer), and then click back on the "All Customers" tab, and then using the keyboard try and move the selected item up one to the item directly over the current selected item, it doesn't! Instead the selector starts at the top again.
I am not sure why this happens, but I want it so that when you click up, it goes one item up, rather than starting at the top of the list. I suspect this has to do with FocusManager, but am not sure.
Does anyone know why the control behaves in this manner? Is it possible, and what approach I should take to modify this code and get it to not "reset" the selected item?
I have implemented a project based off this template and for functionality reasons I need to get the keyboard to move the selector up & down without it resetting.
in the example you've provided setting IsSelected property does not affect the logical focus so the focus is set by default. Workaround I have in mind currently is to force focus for the element in codebehind. As an example add handler to selectionchanged of the listview like this:
private void ListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Count == 0 || e.RemovedItems.Count > 0) return;
var item = (CustomerViewModel) e.AddedItems[0];
var container = (UIElement) listView1.ItemContainerGenerator.ContainerFromItem(item);
container.Focus();
}

Resources