I'm writing a WPF calculator app. In this application I have TextBox that get and set the input according to the OnWindowKeyDown event (input insert/validated and returned to the TextBox as the user type).
for example if the user type:
3-> validate == true--> print to TextBox '3'
y-> validate == false -> ignore
3-> validate == true --> print to TeextBox "33"
I want to get the input char by char (I have a State Pattern class that react to each char added), but I need to return the result as a string.
How can I transfer my data from and to the textBox, with the MVVM design?
It should be enough to bind a Text
property of TextBox
to ViewModel
's property.
For example:
//ViewModel
public class MyViewModel : INotifyPropertyChanged
{
private string textBoxText = string.Empty;
public string TextBoxText
{
get {return textBoxText;}
set {
textBoxText = value;
OnPropertyChanged("TextBoxText "..);
}
}
}
Bind MyViewModel
to a DataContext
of your Form
, TextBox
or whatever... in short make it available to your TextBox
in some way.
Define a Converter
whom methods will be invoked every time Text
property of TextBox
set.
public class TextContenyConverter: IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
// validate input and return appropriate value
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
After binding in XAML
considering that DataContext
is an object of type MyViewModel
where TextContenyConverterObject
is an object of type TextContenyConverter
defined like a static resource.
This just an example.
This is another good explantion on ValueConverters
.
You could bind your TextBoxes
to Properties
on the ViewModel
and use INotifyPropertyChanged
events to do your validation / modification / whatever.
You can update the properties from either the UI or background and it will be updated automatically for the user.
The problem with using a ValueConverter is that you are relying on the Presentation layer for what seems like domain logic. You say you have some sort of State pattern class which does indeed sound like part of a Model (the first 'M' in MVVM).
If you set your binding to have {Binding .... UpdateSourceTrigger=PropertyChanged} you will get the value sent to you View Model each time a user enters a single character. You then need to validate each call to the setter.
Next, there is a feature/bug in the TextBox control. The TextBox wont listen to PropertyChanged events for the binding if it was the source of the change. This means if you type in "y" and your setter actually sets the property to "" and then raises the PropertyChanged event you will still see "y" :(
There is a post that looks at this (http://stackoverflow.com/questions/3905227/coerce-a-wpf-textbox-not-working-anymore-in-net-4-0) but as they use events, they are not doing MVVM.
Having just done this for a WPF project, I ended up having an Attached Property. All the logic was in my Model that my ViewModel wrapped. I was able to unit test my logic and also add the attached property to a style so I could reuse in many times.
The code I wrote looks like this.
public sealed class TextBoxBehaviour : DependencyObject
{
#region CoerceValue Attached property
public static bool GetCoerceValue(DependencyObject obj)
{
return (bool)obj.GetValue(CoerceValueProperty);
}
public static void SetCoerceValue(DependencyObject obj, bool value)
{
obj.SetValue(CoerceValueProperty, value);
}
/// <summary>
/// Gets or Sets whether the TextBox should reevaluate the binding after it pushes a change (either on LostFocus or PropertyChanged depending on the binding).
/// </summary>
public static readonly DependencyProperty CoerceValueProperty =
DependencyProperty.RegisterAttached("CoerceValue", typeof(bool), typeof(TextBoxBehaviour), new UIPropertyMetadata(false, CoerceValuePropertyChanged));
static void CoerceValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var textbox = d as TextBox;
if (textbox == null)
return;
if ((bool)e.NewValue)
{
if (textbox.IsLoaded)
{
PrepareTextBox(textbox);
}
else
{
textbox.Loaded += OnTextBoxLoaded;
}
}
else
{
textbox.TextChanged -= OnCoerceText;
textbox.LostFocus-= OnCoerceText;
textbox.Loaded -= OnTextBoxLoaded;
}
}
static void OnTextBoxLoaded(object sender, RoutedEventArgs e)
{
var textbox = (TextBox)sender;
PrepareTextBox(textbox);
textbox.Loaded -= OnTextBoxLoaded;
}
static void OnCoerceText(object sender, RoutedEventArgs e)
{
var textBox = (TextBox)sender;
var selectionStart = textBox.SelectionStart;
var selectionLength = textBox.SelectionLength;
textBox.GetBindingExpression(TextBox.TextProperty).UpdateTarget();
if (selectionStart < textBox.Text.Length) textBox.SelectionStart = selectionStart;
if (selectionStart + selectionLength < textBox.Text.Length) textBox.SelectionLength = selectionLength;
}
private static void PrepareTextBox(TextBox textbox)
{
var binding = textbox.GetBindingExpression(TextBox.TextProperty).ParentBinding;
var newBinding = binding.Clone();
newBinding.ValidatesOnDataErrors = true;
textbox.SetBinding(TextBox.TextProperty, newBinding);
if (newBinding.UpdateSourceTrigger == UpdateSourceTrigger.PropertyChanged)
{
textbox.TextChanged += OnCoerceText;
}
else if (newBinding.UpdateSourceTrigger == UpdateSourceTrigger.LostFocus || newBinding.UpdateSourceTrigger == UpdateSourceTrigger.Default)
{
textbox.LostFocus += OnCoerceText;
}
}
#endregion
}
you then just have to implement the setter (as it seems you already are) and add the attached property to you textbox that is bound to your ViewModel.
<TextBox Text="{Binding Number, UpdateSourceTrigger=PropertyChanged}"
myNamespace:TextBoxBehaviour.CoerceValue="True"/>