Prevent invalid textbox input from being bound to business object property? - c#

I have a simple winforms app with one form, a few controls and a business object defined like this:
public class BusinessObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null)
{
PropertyChanged(this, e);
}
}
private string _phoneNumber;
public string PhoneNumber
{
get { return _phoneNumber; }
set
{
if (_phoneNumber == value)
{
return;
}
_phoneNumber = value;
OnPropertyChanged(new PropertyChangedEventArgs("PhoneNumber"));
}
}
On my form, I have a textbox that is bound to the PhoneNumber property via a binding source and the data source update mode is set to OnPropertyChanged. This all works as expected. I need to do some validation on the text before it gets assigned to the PhoneNumber property on my business object. I thought that I would do this in the Validating event handler for the textbox and, if the input is invalid, I display my error provider and set e.Cancel = true. Unfortunately, this doesn't prevent the invalid input from being bound to the PhoneNumber property. Is there an easy way to do this?

Data Validation might be just the thing you are looking for. Should keep invalid input from changing your objects.

As suggested by mrlucmorin, I've changed my update data source mode from "OnPropertyChanged" to "OnValidation" so that the binding only occurs when the textbox loses and gets validated. I did implement validation in the Validating() event handler for my textbox and set e.Cancel = true when the data is invalid. Unfortunately, clicking buttons on my toolbar doesn't seem to cause the textbox to lose focus so the Validating() event never fires but I was able to work around that by calling ValidateChildren() when a toolbar button is clicked. Thanks again to mrlucmorin and ImGreg for the suggestions that ultimately solved my problem!

According to the msdn, the event you are using is occurring after the value has changed. One option is to store a backup of the data and restore the value that changed. However, this is not an ideal approach.
I would change the how you are validating the controls.
I'm not certain when to do this as it depends on how your code works. Maybe perform your own validation when you lose focus on the textbox control or do the validation when the datasource is to be updated.
EDIT: Perhaps you are looking for the ErrorProvider Class. This can be used to handle validation like you want.

Related

Multiple views sharing same data with two-way data binding between multiple threads

UWP app ( mvvm architecture ) I have a MainView which has a collection in its ViewModel, used to bind to the GridView on MainView and each item has a TextBox with 2 way databinding with Description property of class Note.
Xaml of the TextBox of each gridviewitem.
<TextBlock Text="{x:Bind Description,Mode=TwoWay}"
Collection property used to bind to ItemSource of gridview.
public ObservableCollection<Note> Notes { get; }
and this is the class Note
public class Note : Observable
{
private string _description;
public string Description
{
get => _description;
set => Set(ref _description, value, nameof(Description));
}
}
the Observable class is for two way data binding help.
public class Observable : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void Set<T>(ref T storage, T value, [CallerMemberName]string propertyName = null)
{
if (Equals(storage, value))
{
return;
}
storage = value;
OnPropertyChanged(propertyName);
}
protected void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
Now everything uptil this point works perfectly, when I change the text in textbox, it changes the value of Description as well.
Second View
Now I have a feature where each GridViewItem has a button in it which opens the Note in new window. and this new window has nothing but only 1 TextBox, so now the secondary view and the GridViewItem which opened that view are using the same object of Note.
This TextBox in secondary view also has 2 way data binding with the Description of the Note.
The Problem
What I want is that whether the textbox in gridview or the textbox on the secondary view is edited, the value of description must remain synced between these 2 textboxes, that is why I tried to bind them 2 way with same object of Note hence the same Description object is bound to both of them.
Error here was expected to me which was Marshalling threading error, so whenever I try to change value of any textbox, it tried to update UI on other view ( which is another thread ) which is ofcourse not allowed.
I know about CoreDisptcher
I already know about the Dispatcher feature of UWP for safe cross thread communication, I already have it all setup and if I use it from a normal method I can easily use it for cross thread UI update and it totally works. But my issue is the following line :
protected void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));\
Exception occurs when it tried to invoke the PropertyChanged I tried to wrap following line in my Dispatcher :
OnPropertyChanged(propertyName);
but INotify interface does not allow me to have a Set<> method which returns a Task instead it needs to return just an object, this is the point where I am stuck and I dont know how to make use of Dispatcher in this scenario, please let me know if there is some better way to do this, it seems this way might not be so efficient.
Thanks.
The best solution in this case would be to have a separate set of INotifyPropertyChanged instances for each window and using some kind of messaging solution like EventHub in MvvmLight, which publishes message that the underlying model changed and all interested parties should update their instances.
Another option would be to create a base model class, which maintains a dictionary of INotifyPropertyChanged instances for each UI thread (so it would be a Dictionary<Dispatcher, YourModelClass>. Now the parent would subscribe to PropertyChanged event of each child instance and once it executes would propagate the event to other childs using the appropriate Dispatcher.
Also there is a very interesting utility class ViewSpecificBindableClass by Marian Dolinský on his GitHub which could potentially be a solution that would allow you to have "single" class in multiple views, aware of multiple dispatchers. I haven't tried it yet, but it seems promising.
So I finally had to take a totally different approach centralizing TextChanged events of MainView textbox and the one on the secondaryview.
I essentially passed the textbox on the mainpage through to the secondary page ( secondary view ) and then subscribed to its TextChanged event. I also subscribed to the TextChanged event of textbox on the secondary view, and then with help of reverse dispatchers I was able to sync the text between 2 windows without any problems.
Note : always make sure to unsubscribe to events when the secondary window closes to prevent memory leaks.
private async void PipBox_TextChanged(object sender, TextChangedEventArgs e)
{
string text = PipBox.Text;
await CoreApplication.MainView.Dispatcher.AwaitableRunAsync(() =>
{
if (parentBox.Text != text)
parentBox.Text = text;
});
}
private async void ParentBox_TextChanged(object sender, TextChangedEventArgs e)
{
string text = parentBox.Text;
// the awaitablerunasync extension method comes from "Windows Community Toolkit".
await _viewLifetimeControl.Dispatcher.AwaitableRunAsync(() =>
{
if (ViewModel.MyNote.Description != text)
ViewModel.MyNote.Description = text;
});
}
Notice that I still have 2 way data binding on both textboxes and it does not cause any exceptions because I am using 2 different instances of Note for both views.
<TextBox Text="{x:Bind ViewModel.MyNote.Description, Mode=TwoWay}"
x:Name="PipBox"/>
but because I have twoway data binding on both textboxes, that is how I can easily keep both instances of Note in sync as well on separate threads.
I will keep the github repo in case it can help anyone else : https://github.com/touseefbsb/MultiWindowBindingSync
P.S : A special thanks to Martin Zikmund who helped me a lot in figuring out this solution.

Handle event when Label Text Change

I'm developing App which communicate with RFID Reader, I have the following Label which takes it's value from the Reader (when I click on the read button on the hardware)
<Label Text="{Binding Statistics.TotalUniqueCount}" />
I want to handle and event when the text value changed, it looks like the label doesn't has such event, is there any text view control that can handle the text change event? I tried to use Entry control, but the problem that when I read the RFID tags, it gives me the first and second time correct values, but the third time it gives me wrong value , and that's happens only when I use Entry.
for example, I read 3 unique tags, it gives me first time 3, when I read more 2 tags, the number becomes 5, but when I read the third time another 3 tags, the number becomes 1.
I put Entry and Label in the same page with the same Binding, the Label shows correct values and the Entry shows wrong values.
is there any solution to handle event when this binding (Statistics.TotalUniqueCount) changes?
As was mentioned in the comments, it looks like the Statistics class must implement INotifyPropertyChanged (that's how the binding is able to update the UI when the data changes). If that's the case, you should just subscribe to that same event in your code. Wherever you have access to that Statistics variable (in code bebhind or viewmodel), just do
Statistics.PropertyChanged += (o,e)=>
{
if (e.PropertyName == "TotalUniqueCount")
{
//do something
}
}
What you can do is on whatever page/view your label is on create a new bindableproperty that is set to your binding. Then add a propertyChanged method that will get called if the text on your label changes.
public static BindableProperty TextChangedProperty = BindableProperty.Create(nameof(TextChanged), typeof(string), typeof(YourView), string.Empty, propertyChanged:OnTextChanged);
public string TextChanged
{
get { return (string)GetValue(TextChangedProperty); }
set { SetValue(TextChangedProperty, value); }
}
static void OnTextChanged(BindableObject bindable, object oldValue, object newValue)
{
//Do stuff
}

Can't deselect ListView Item in MVVM UWP

I want to be able to click ListView item, which then takes me to appropriate page. But since there doesn't exists anything like ClickedItem to go along with the ItemClick, I have to use the SelectedItem (to get the object of what the user clicked) and SelectionChanged to capture when it happens (because this is setup in a way that when user clicks, he makes a selection, which triggers this).
Since in MVVM I can't use events, I'm binding what would be events to methods in my ViewModel.
<GridView x:Name="MyGrid"
ItemsSource="{x:Bind ViewModel.myList, Mode=OneWay}"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
IsSwipeEnabled="false"
SelectedItem="{Binding mySelectedItem, Mode=TwoWay}" // Binding makes it easier to bind the whole object
SelectionChanged="{x:Bind ViewModel.SelectioMade}"
>
I fill up my list in the ViewModel. I'm using Template10 implementation of INotifyPropertyChanged.
private MyListItemClass _mySelectedItem;
public MyListItemClass mySelectedItem{
get { return _mySelectedItem; }
set { Set(ref _mySelectedItem, value); }
}
And this simple method pushes me to the next page when user clickes on an item.
public void SelectioMade() {
if (_mySelectedItem != null) {
NavigationService.Navigate(typeof(Views.DetailPage), _mySelectedItem.id);
}
}
This works.
Problem is that a selection is made and it persists. When I hit the back button on the DetailPage, I go back to this list as I left it and the clicked item is still selected. And hence, clicking it again doesn't actually make a selection and trigger the SelectionChanged.
Obvious choice seemed to be to just set mySelectedItem to null when I no longer need the value, but it doesn't work.
public void SelectioMade() {
if (_mySelectedItem != null) {
NavigationService.Navigate(typeof(Views.DetailPage), _mySelectedItem.id);
mySelectedItem = null;
}
}
I can't seem to be able to set it back to null. If I place a break point on the mySelectedItem = null; it just doesn't do anything. It does trigger the set { Set(ref _mySelectedItem, value); }, but the View doesn't update. Neither the clicked item becomes deselected, nor a TextBlock I bound to one of the mySelectedItem.id properties gets changed (or rather emptied).
I would like to know why doesn't this work and possibly how to fix it. My MVVM may not be perfect, I'm still learning. And while it may not be perfect, I'm not really looking for advice how to properly write MVVM. I want to know why this doesn't work, because in my opinion, it should work just fine.
It seems that GridView doesn't like the SelectedItem property being changed within the SelectionChanged handler (it could result in an infinite loop if guards are not used). You could instead set SelectedItem to null in the OnNavigatedTo handler for that page (or whatever the Template 10 equivalent of that is).
Also you don't really need to subscribe to the SelectionChanged event since you can detect this in the setter of your mySelectedItem property.
However, I think it is wrong to handle item clicks by listening for selection changed events because the selection can be changed by other means (up/down arrow key, or tab key, for example). All you want to do is to respond to an item click and obtain the clicked item, right? For this, you can x:Bind the ItemClick event to a method in your view model:
<GridView ItemClick="{x:Bind ViewModel.ItemClick}" SelectionMode="None" IsItemClickEnabled="True">
public void ItemClick(object sender, ItemClickEventArgs e)
{
var item = e.ClickedItem;
}
If you're uneasy about the ItemClick method signature in your view model, then you can make your own ItemClick behavior to execute a Command exposed in your view model with the command's parameter bound to the clicked item.
If you're not using behaviors for some reason, then you can make your own attached property instead, something like this:
public class ViewHelpers
{
#region ItemClickCommand
public static readonly DependencyProperty ItemClickCommandProperty =
DependencyProperty.RegisterAttached("ItemClickCommand", typeof(ICommand), typeof(ViewHelpers), new PropertyMetadata(null, onItemClickCommandPropertyChanged));
public static void SetItemClickCommand(DependencyObject d, ICommand value)
{
d.SetValue(ItemClickCommandProperty, value);
}
public static ICommand GetItemClickCommand(DependencyObject d)
{
return (ICommand)d.GetValue(ItemClickCommandProperty);
}
static void onItemClickCommandPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var listView = d as ListViewBase;
if (listView == null)
throw new Exception("Dependency object must be a ListViewBase");
listView.ItemClick -= onItemClick;
listView.ItemClick += onItemClick;
}
static void onItemClick(object sender, ItemClickEventArgs e)
{
var listView = sender as ListViewBase;
var command = GetItemClickCommand(listView);
if (command != null && command.CanExecute(e.ClickedItem))
command.Execute(e.ClickedItem);
}
#endregion
}
XAML doesn't require MVVM patterns to be used, which means there is lots of "missing" functionality that you need to write yourself to make MVVM easier for you (like the above ItemClick attached property). Maybe Template 10 provides some behaviors for you already? I'm not familiar with it.
My first instinct would be to check your Set method, to ensure that it is really sending the proper notification to the view. I am not familiar with the Template10 implementation, so it seems strange to me that you are not required to provide a property name with Set().
Beyond that, I would suggest that you go back to using Click rather than SelectionChanged, since that is the behavior you are actually interested in. You should read a bit about attached properties, which are a great way to accomplish tasks that would normally require code-behind without actually using code-behind. They make MVVM a lot more practical and a lot less hackish.
https://msdn.microsoft.com/en-us/library/ms749011(v=vs.110).aspx
An attached property, more or less, allows you to define a DependencyProperty that you can attach to any element in XAML, like your GridView. Because you get access to the Element in the setter, you are free to attach to its events. So, you can create an attached property with a delegate type, which will forward an event like click to the delegate. Back in the view, you bind it to your handler in the ViewModel like this:
<GridView something:MyAttachedProperties.ClickHandler="{Binding MyClickHandler}" />
Hope this helps!
SelectedIndex = -1, following your null set of the SelectedItem property? So yes another property would be required or make sure that caching is disabled for that page as well.

2-way binding in Winforms app makes entering text in a TextBox painfully slow

I've set-up 2-way binding between my form (it has 32 controls) and an instance of my class but each character entered in a TextBox has that 1/2 second delay which makes the application almost unusable.
When I use DataSourceUpdateMode.Never, the problem does not occur which clearly indicates the 2-way binding is the culprit.
Note that if I set DataSourceUpdateMode.Never for each control but one, the lag exists for that one control so it doesn't seem to be the number of bound controls that causes the issue.
parameterTagRecord = new PETParameterTagRecord(TagID);
baseTagNameTB.DataBindings.Add("Text", parameterTagRecord,
"BaseTagName", true, DataSourceUpdateMode.OnPropertyChanged);
And an extract of my class:
public class PETParameterTagRecord : PETBaseObject, INotifyPropertyChanged
{
private string _baseTagName = Constants.NullString;
public event PropertyChangedEventHandler PropertyChanged;
public string BaseTagName
{
get { return _baseTagName; }
set
{
_baseTagName = value;
NotifyPropertyChanged("BaseTagName");
}
}
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
What am I doing wrong?
It shouldn't be that slow, but there's an option where you can have the textbox change on key press or on lost focus. Try setting it to lost focus. Also in your setter, be sure to check that _baseTagName != value before setting and raising the event. That will slow things up a bunch as well.
So, first try changing your binding like this:
baseTagNameTB.DataBindings.Add("Text", parameterTagRecord,
"BaseTagName", true, DataSourceUpdateMode.OnValidation);
See this MSDN link: http://msdn.microsoft.com/en-us/library/system.windows.forms.datasourceupdatemode.aspx. This means that instead of every keypress causing the new string value to be pushed into the property, it will only do so on Validation (which happens as part of the control losing focus).
Second, change your property implementation to match this:
public string BaseTagName
{
get { return _baseTagName; }
set
{
if (_baseTagName != value) {
_baseTagName = value;
NotifyPropertyChanged("BaseTagName");
}
}
}
Right now you're raising the event whether the property has actually changed or not. That is also detrimental to performance.
I ran into the same exact issue with BindingSource. This has has nothing to do with the update mode or notifications being fired too often (though indirectly, it does). The current implementation causes every single bound element to refresh whenever any property changes. So the reason OnValidation is less of an issue is obvious, it happens less frequently.
Fairly easy to check, too. Add two counters, increase each whenever a getter is accessed or NotifyProperChanged is called. In my case, with roughly 40 elements, I'd be at 1/40 after loading the form. Add a character in a textbox, suddenly at 2/80. Keeping the key pressed, my app stopped being responsive. Once it finally caught up, the count stood at something ridiculous like 50/2000. All from one single element changing.
I might be wrong, but I don't see how this makes sense or could be the desired implementation. Why would I want to update the whole form when one element changes, defeats the point of binding specific elements in the first place.

WPF bindings for settings read before execution

I have an application where bindings work almost as they should, there is just one problem:
Some GUI elements update the underlying data type "on exit" i.e. when focusing on something else. However, this doesn't happend when I click "execute" or "save" (saving settings). So if the last setting that was set was a textbox and the user didn't click somewhere else, the updated setting value is not included into the execution / setting save.
Is there a way to do this manually at execute/save?
Why doesn't my clicking on execute/save work as a focus change? Maybe it does but it doesn't happend until after the event for the button is run?
You can use this to explicitly update the binding for a specific textbox element or the focused textbox element in your execute/save method and still use LostFocus for the UpdateSourceTrigger property:
public static void UpdateBinding()
{
UpdateBinding(Keyboard.FocusedElement as TextBox);
}
public static void UpdateBinding(TextBox element)
{
if (element != null)
{
var binding = element.GetBindingExpression(TextBox.TextProperty);
if (binding != null)
{
binding.UpdateSource();
}
}
}
Try using UpdateSourceTrigger="PropertyChanged" on your textbox binding
EDIT
or use UpdateSourceTrigger="Explicit" and call the update method in your bindings when handling your button click

Categories