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"/>