I've got a DataGrid that I'm binding to an ObservableCollection of "Customer" classes, implementing IDataErrorInfo. One of the properties on the Customer class is an int, and in my IDataErrorInfo implementation I check that it's within a valid range, e.g.:-
public class Customer : IDataErrorInfo
{
public int PercentDiscount { get; set; }
... other properties & methods removed for clarity
public string this[columnName]
{
get
{
if (PercentDiscount < 0 || PercentDiscount > 10)
return "Percent Discount is invalid";
}
}
}
In my XAML code-behind I handle a couple of events. In the PreparingCellForEdit event I store a reference to the row being edited:-
private void DataGrid_PreparingCellForEdit(object sender, DataGridPreparingCellForEditEventArgs e)
{
_rowBeingEdited = e.Row;
}
Then in the RowEditEnding event, I take some action if the row is in an invalid state (in my case I revert the Customer properties back to their previous "good" values):-
private void DataGrid_RowEditEnding(object sender, DataGridRowEditEndingEventArgs e)
{
if (_rowBeingEdited != null)
{
var errors = Validation.GetErrors(_rowBeingEdited);
if (errors.Count > 0)
{
.. do something
}
}
}
This works fine if the user enters a numeric value that fails my validation rule, but if the user enters a non-numeric value then the RowEditEnding event never fires and the cell remains in an edit state. I assume it's because WPF fails to bind the non-numeric value to the int property. Is there any way I can detect/handle when this happens?
Last resort is to change the PercentDiscount property to a string, but I'm trying to avoid going down this route.
Edit - I've just found that I can successfully handle both types of error using the CellEditEnding event instead of RowEditEnding. A new problem has appeared though - if I enter an invalid value into the cell then press Enter, the underlying property doesn't get updated, so when CellEditEnding fires Validation.GetErrors is empty. The end result is that the row leaves edit mode but still shows the invalid value in the cell with red border. Any idea what's going on now?
This may not be much of an answer especially since you already mentioned it, but I've fought with DataGrid validation for a while and ended up resorting to making my backing values be strings. You'll notice in the output window of the debugger that a binding or conversion exception happens when you type an alpha character into a DataGridColumn bound to an int.
You can get different behavior by changing the UpdateSourceTrigger, or by putting a converter in between the binding and the property, but I never got exactly what I needed until I backed the values with strings.
I suppose you could also try creating your own DataGridNumericColumn derived from DataGridTextColumn and maybe you'd have more control over the binding/validation behavior.
I struggled to find a good solution for this, but I saw some other people messing with the CellEditEnding event and I ended up, coming up with this code to revert values if they fail conversion:
private void CellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
{
if (e.EditingElement is TextBox)
{
var cellTextBox = (TextBox)e.EditingElement;
var cellTextBoxBinding = cellTextBox.GetBindingExpression(TextBox.TextProperty);
if (cellTextBoxBinding != null && !cellTextBoxBinding.ValidateWithoutUpdate())
{
cellTextBoxBinding.UpdateTarget();
}
}
}
Calling ValidateWithoutUpdate on the editing elements binding returns false if the conversion of the value fails, then calling the UpdateTarget forces the value to be reverted to the current 'model' value.
Related
OK, so I'm using a typical Binding to my ViewModel. It works beautifully, to source or to target, or so it seems. The vm collection is an ObservableCollection which is initialized and never modified (no setter).
public ObservableCollection<Statement> StatementsList { get; } = new();
#region SelectedStatement
private Statement _selectedStatement;
public Statement SelectedStatement
{
get => _selectedStatement;
set => Set(ref _selectedStatement, value, nameof(SelectedStatement));
}
#endregion SelectedStatement
I can set SelectedStatement from the ViewModel, and the UI updates fine. I can watch the SelectionChanged event of the DataGrid and confirm the added items and removed items are exactly as expected.
Then, I select a different row USING THE MOUSE, and use my search function to select another row using SelectedItem = some statement, which visually selects the row perfectly (again), confirmed by the SelectionChanged event again. SelectedStatement in my view model has the correct value!
Then, the weirdness starts. I press the down arrow the keyboard.
You'd expect the next line after the selected statement to be selected, but instead the next line after the previously selected item (using the mouse) is selected. It's like the keyboard responding code in the DataGrid is not recognizing the prior new row selection via the VM.
Has anyone seen this behavior? I've done WPF development for many years, and I've seen many weird WPF bugs, but this one I've never noticed!
Note that IsSynchronizedWithCurrentItem="True" on the DataGrid. I tried setting it to false just as a stab in the dark, but no change in behavior. I also tried changing my SelectedItem property to wrap a call to GetDefaultCollectionView() and getting/changing the selected item via the collection view instead of using a binding to SelectedItem. The behavior is identical.
Selecting an item is essentially setting IsSelected = true.
And setting this property does not affect the Focus transition to the selected element in any way.
And when controlling from the keyboard, the transition occurs from the element with Focus.
You can add the SelectionChanged processing to the Selector (ListBox, DataGrid,...) and in it perform the Focus transition to the selected item (by the index in the SelectedIndex).
An example of such a handler:
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (sender is Selector selector)
{
int index = selector.SelectedIndex;
if (index >=0)
{
var element = selector.ItemContainerGenerator.ContainerFromIndex(index);
if (element is UIElement uiElement)
uiElement.Focus();
}
}
}
I'm having some problems with ListBox databinding and immutability. I have a model that provides a List of some elements and a ViewModel that takes these elements and puts them to an ObservableCollection which is bound to the ListBox.
The elements, however, are not mutable so when they change - which happens when user changes ListBox's selection or in a few other scenarios - the model fires up an event and the ViewModel retrieves a new List of new elements instances and repopulates the ObservableCollection.
This approach works quite well - despite being obviously not optimal - when user interacts with the ListBox via mouse (clicking) but fails horribly when using keyboard (tab to focus current element and then using mouse arrows or further tabbing). For some reason the ActiveSchema gets always reset to the first element of the Schemas[*].
The ActiveSchema setter gets called for the schema user switched to, then for null, and finally for the first value again. For some reason the two last events don't happen when invoked via mouse.
PS: Full code can be found here
PPS: I know I should probably rework the model so it exposes ObservableCollection that mutates but there're reasons why trashing everything and creating it from scratch is just a bit more reliable.
//ListBox's Items source is bound to:
public ObservableCollection<IPowerSchema> Schemas { get; private set; }
//ListBox's Selected item is bound to:
public IPowerSchema ActiveSchema
{
get { return Schemas.FirstOrDefault(sch => sch.IsActive); }
set { if (value != null) { pwrManager.SetPowerSchema(value); } }
}
//When model changes:
private void Model_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if(e.PropertyName == nameof(IPowerManager.PowerSchemas))
{
updateCurrentSchemas();
}
}
private void updateCurrentSchemas()
{
Schemas.Clear();
var currSchemas = pwrManager.PowerSchemas;
currSchemas.ForEach(sch => Schemas.Add(sch));
RaisePropertyChangedEvent(nameof(ActiveSchema));
}
I have two textboxes in WPF. named txt1 and txt2.
In the lostFocus of txt1 I write
If txt1.Text is nothing then
txt1.Focus
End If
In the lostFocus event of txt2 I write
If txt2.Text is nothing then
txt2.Focus
End If
Now, If txt1 and txt2 are both empty and user presses TAB key in txt1 the problem occurs. Program goes in infinite loop. I mean cursor comes to txt1 and goes to txt2 infinite times.I know This is normal behavior according to my code.
So I want to have validating event to avoid the problems like above. But I cannot find one in WPF.
So which event should I use?
I am not a VB coder so can't write exact code for you but here is what you should do. Add event handler for event PreviewLostKeyboardFocus. inside the event handler set e.Handled to true if the text is empty.
Sample C# code. I have writter a generic handler.
private void TextBox_PreviewLostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
if (string.IsNullOrEmpty((sender as TextBox).Text))
{
e.Handled = true;
}
}
A better solution might be to allow the user to navigate away from the empty/null text box but either revert the text to the initial value (if there was one) or provide a validation error. Providing validation errors is relatively easy with IDataErrorInfo.
As a software user myself I get annoyed when an application prevents me from navigating away from a field.
Reset Value Approach
See this stackoverflow appraoch for how to maintain and get the previous value. In the LostFocus event you can set your member variable back to _oldValue if the current value is invalid.
determine a textbox's previous value in its lost focused event? WPF
Validation Approach
Those two dates are stored in a model or a view model class. In that class implement IDataErrorInfo (http://msdn.microsoft.com/en-us/library/system.componentmodel.idataerrorinfo(v=vs.95).aspx). Then in your xaml you can show the validation errors.
//This is your model/viewmodel validation logic
public string this[string columnName]
{
get
{
string result = null;
if (columnName == "FirstName")
{
if (string.IsNullOrEmpty(FirstName))
result = "Please enter a First Name";
}
if (columnName == "LastName")
{
if (string.IsNullOrEmpty(LastName))
result = "Please enter a Last Name";
}
if (columnName == "Age")
{
if (Age < = 0 || Age >= 99)
result = "Please enter a valid age";
}
return result;
}
}
//Here is a sample of a xaml text block
<textbox x:Name="tbFirstName" Grid.Row="0" Grid.Column="1" Validation.Error="Validation_Error" Text="{Binding UpdateSourceTrigger=LostFocus, Path=FirstName, ValidatesOnDataErrors=true, NotifyOnValidationError=true}" />
You can also look at these other StackOverflow posts-
What is IDataErrorInfo and how does it work with WPF?
IDataErrorInfo notification
I have a problem with DataGrid for WPF. I want to get the current cell's text while editing it. The problem is you can't get the value without committing the cell first. I want to validate the text first before committing it.
Thanks.
You have different ways to do it.
The cleanest one would be to implement IDataErrorInfo in your model and to set ValidatesOnDataError=true in your bindings. (And if you don't know about IDataErrorInfo, I'd really encourage you to take some time to lean it because it is a VERY useful tool and is very easy to use)
Another alternative solution, which requires less code, but may be a bit less clean:
You can just add an event handler to CellEditEnding (or override DataGrid.OnCellEditEnding ) and validate the data entered before it commits. If the validation fails, you just cancel the CellEditEnding event.
Here's the code if you override OnCellEditEnding (which is almost exactly the same as just adding an event handler)
I took an easy example for you, here I just try to parse the text entered.
You can have access to the text entered by the user with EditingElement property of a DataGridCellEditEndingEventArgs
Here is the code:
protected override void OnCellEditEnding(DataGridCellEditEndingEventArgs e)
{
try
{
// Try to parse the text
double test = Double.Parse((e.EditingElement as TextBox).Text);
}
catch (FormatException)
{
// Mark the current editing element as invalid and cancel the event
TextBox text = e.EditingElement as TextBox;
BindingExpression bindingExpression =
BindingOperations.GetBindingExpression(text, TextBox.TextProperty);
BindingExpressionBase bindingExpressionBase =
BindingOperations.GetBindingExpressionBase(text, TextBox.TextProperty);
ValidationError validationError =
new ValidationError(new ExceptionValidationRule(), bindingExpression);
Validation.MarkInvalid(bindingExpressionBase, validationError);
e.Cancel = true;
}
base.OnCellEditEnding(e);
}
Hope this was clear enough :)
I have some issue with WPF databinding, and I hope to be clear in my explaination because I fear that the problem is very subtle.
I have essentially a WPF UserControl with a bunch of ComboBox, each one is chained to each other. I mean that the first combobox is filled with some elements, and when the user select and item, the second combobox is filled with elements based on the previous selection, and so on with other combox.
All combobox are binded with UpdateSourceTrigger=LostFocus.
The code for a ItemsSource property of a combo looks like this:
private ICollectionView allTicketTypesView;
public IEnumerable<TicketTypeBase> AllTicketTypes
{
get { return this.allTicketTypesView.SourceCollection.Cast<TicketTypeBase>(); }
private set
{
IEnumerable<TicketTypeBase> enumerable = value ?? new TicketTypeBase[0];
ObservableCollection<TicketTypeBase> source = new ObservableCollection<TicketTypeBase>(enumerable);
this.allTicketTypesView = CollectionViewSource.GetDefaultView(source);
this.OnPropertyChanged("AllTicketTypes");
}
}
The code for a SelectedItem property of a combo is similar to this code:
private TicketTypeBase ticketType;
public TicketTypeBase TicketType
{
get { return this.ticketType; }
set
{
this.ticketType = value;
this.OnPropertyChanged("TicketType");
this.UpdateConcessions();
}
}
I'm experiencing a subtle problem:
when i move with keyboard and/or mouse over my combo, I see that often propertychanged is called also when I actually don't change any of the items of the list.
I mean that a combo is filled with elements, and an item is selected: moving over the combo with the keyboard trigger the propertychanged (and let the other combo to be updated, that is an indesidered behavior), but the element itself is the same.
I see this behavior in a combobox that is binded with a list of strings (so I suppose no error on Equals/GetHashCode implementation) and this behavior happens everytime except the first time.
I've fixed the code with this:
private string category;
public string Category
{
get { return this.category; }
set
{
bool actuallyChanged = !String.Equals(this.category, value);
this.category = value;
this.OnPropertyChanged("Category");
if (!actuallyChanged)
{
string format = String.Format("Category '{0}' isn't changed actually", value);
Trace.WriteLine(format);
}
else this.UpdateTicketTypes():
}
}
But of course I don't like this code that add logic to the setters.
Any suggestion about how to avoid this behavior?
I hope to be clear, and I'm ready to explain better my problem if someone don't understand clearly.
It is not unreasonable for your model to check whether a value used in a property setter is actually different from the current value. However a more 'standard' implementation would look like the following:
private string category;
public string Category
{
get { return this.category; }
set
{
// check this is a new value
if(Object.Equals(this.category, value))
return;
// set the value
this.category = value;
// raise change notification
this.OnPropertyChanged("Category");
// compute related changes
this.UpdateTicketTypes():
}
}
Just a guess but can you implement SelectedValue binding instead of SelectedItem? SelectedValue (for value types like int, string, bool etc.) do no refresh upon keyboard or mouse focuses and even when ItemsSource (with CollectionView) changes coz the change notifications in the source (or model) not fire as value types do not change by reference.