Based on the edit of the question, it is impractical for the O/P to change the viewmodel(s) to add additional string properties for the serial number parts.
A custom IValueConverter
In this situation, a custom IValueConverter can provide the required functionality. Let's call this custom converter SerialNumberConverter.
As already hinted at by IL_Agent's very brief answer, you would use the converter in XAML simliar to the following:
<StackPanel Orientation="Horizontal"> <StackPanel.Resources> <My:SerialNumberConverter x:Key="SerialNumberConverter" /> </StackPanel.Resources> <TextBox Text="{Binding SerialNumber, ConverterParameter=0, Converter={StaticResource SerialNumberConverter}}"/> <TextBox Text="{Binding SerialNumber, ConverterParameter=1, Converter={StaticResource SerialNumberConverter}}"/> <TextBox Text="{Binding SerialNumber, ConverterParameter=2, Converter={StaticResource SerialNumberConverter}}"/> <TextBox Text="{Binding SerialNumber, ConverterParameter=3, Converter={StaticResource SerialNumberConverter}}"/> <TextBox Text="{Binding SerialNumber, ConverterParameter=4, Converter={StaticResource SerialNumberConverter}}"/> </StackPanel>
The implementation of the SerialNumberConverter looks somewhat unconventional:
public class SerialNumberConverter : IValueConverter { private readonly string[] _serialNumberParts = new string[5]; public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { int serialPartIndex; if (!int.TryParse(parameter.ToString(), out serialPartIndex) || serialPartIndex < 0 || serialPartIndex >= _serialNumberParts.Length ) return Binding.DoNothing; string completeSerialNumber = (string) value; if (string.IsNullOrEmpty(completeSerialNumber)) { for (int i = 0; i < _serialNumberParts.Length; ++i) _serialNumberParts[i] = null; return ""; } _serialNumberParts[serialPartIndex] = completeSerialNumber.Substring(serialPartIndex * 6, 5); return _serialNumberParts[serialPartIndex]; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { int serialPartIndex; if (!int.TryParse(parameter.ToString(), out serialPartIndex) || serialPartIndex < 0 || serialPartIndex >= _serialNumberParts.Length ) return Binding.DoNothing; _serialNumberParts[serialPartIndex] = (string)value; return (_serialNumberParts.Any(string.IsNullOrEmpty)) ? Binding.DoNothing : string.Join("-", _serialNumberParts); } }
How does it work? For the following explanation, the reader is required to have a basic understanding of how the binding mechanism of WPF utilizes IValueConverters.
Convert method
First, let's take a look at the Convert method. The value passed to that method is obviously coming from the view models SerialNumber property and thus is a complete serial number.
Based on the ConverterParameter - which specifies the serial number part to be used for a particular binding - the appropriate portion of the serial number string will be extracted. In the example converter given here, i assumed a serial number format of five parts with 5 characters each, and each part being separated from another by a hyphen - character (i.e., a serial number would look like "11111-22222-33333-44444-55555").
Obviously the Convert method will return this serial number part, but before doing so it will memorize it in a private string array _serialNumberParts. The reason for doing this becomes clear when looking at the ConvertBack method.
Another responsibility of the Convert method is erasing the _serialNumberParts array in case the bound SerialNumber property provides an empty string or null.
ConvertBack method
The ConvertBack method essentially converts the data from the text box before it is being assigned to the SerialNumber property of the view model. However, the text box will only provide one part of the serial number -- but the SerialNumber property needs to receive a complete serial number.
To create a complete serial number, ConvertBack relies on the serial number parts memorized in the _serialNumberParts array. However, before composing the complete serial number the _serialNumberParts array will be updated with the new data provided by the text box.
In case your UI starts with empty text boxes, the ConvertBack method will not return a serial number until all text boxes have provided their data (i.e., until the user has typed something into all text boxes). Instead, the method will return Binding.DoNothing in case a complete serial number cannot be composed yet. (Binding.DoNothing instructs the binding to do (erm...) nothing.)
Considerations regarding SerialNumberConverter
For this converter to work without troubles, the following considerations need to be taken into account:
- The bindings of each text box belonging to the same serial number need to use the same converter instance (so that the _serialNumberParts array will be able to keep track of the complete serial number)
- If the UI provides several text box groups for entering multiple serial numbers, then each of these text box groups need to use a separate converter instance (otherwise, serial number parts of different serial numbers could mix in the same _serialNumberParts array). In my XAML example above, i ensured this by placing the converter instance in the resource dictionary of the parent StackPanel of the text boxes (which makes the converter instance local to this StackPanel and its descendent elements).
- It is required to use data bindings for all serial number parts. Otherwise, the _serialNumberParts array will never be populated fully, which in turn will prevent ConvertBack from returning any complete serial number.
A ValidationRule for the text boxes
If validation of input should be handled for each text box individually, a custom ValidationRule is required:
public class SerialNumberValidationRule : ValidationRule { public override ValidationResult Validate(object value, CultureInfo cultureInfo) { string serialNumberPart = value.ToString(); return (serialNumberPart.All(c => '0' <= c && c <= '9')) ? (serialNumberPart.Length == 5) ? ValidationResult.ValidResult : new ValidationResult(false, "Serial number part must be 5 numbers") : new ValidationResult(false, "Invalid characters in serial number part"); } }
In the example SerialNumberValidationRule given here i assume that only number characters are valid characters for a serial number (you would of course implement the ValidationRule differently depending on the specification of your serial number format...)
While implementing such a ValidationRule is rather easy and straightforward, attaching it to the data bindings in XAML is unfortunately not as elegant:
<StackPanel Orientation="Horizontal"> <StackPanel.Resources> <My:SerialNumberConverter x:Key="SerialNumberConverter" /> </StackPanel.Resources> <TextBox> <TextBox.Text> <Binding Path="SerialNumber" ConverterParameter="0" Converter="{StaticResource SerialNumberConverter}"> <Binding.ValidationRules> <My:SerialNumberValidationRule /> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox> <TextBox> <TextBox.Text> <Binding Path="SerialNumber" ConverterParameter="1" Converter="{StaticResource SerialNumberConverter}"> <Binding.ValidationRules> <My:SerialNumberValidationRule /> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox> ...here follow the remaining text boxes... </StackPanel>
The reason for this convoluted XAML is Binding.ValidationRules being a read-only property. Unfortunately that means we cannot simply write something like
<Binding ValidationRules="{StaticResource MyValidator}" ... />
but instead need to resort to this kind of verbose XAML shown above to add our SerialNumberValidationRule to the Binding.ValidationRules collection.
Final notes
For the sake of readability, i omitted any sanity checks in my example converter code which are not required to get an understanding of how the code works. Depending on your requirements and application scenario you might need to add sanity checks to the converter code to prevent it from going haywire if the view model's SerialNumber property could possibly provide improper data.
The validation as depicted above will just show a slim red rectangle around a text box if the ValidationRule fails (this is default behavior for a text box). If your UI should present a more elaborate validation error response, most certainly you will need to do much more than just only adding the ValidationRule to the bindings...