Reputation: 1109
I was wondering if there is way to bind multiple textboxes to a single property in VM and a single validation logic. Perhaps a good example of that would be a typical Serial Number in any product activation process. Usually, when asked to enter serial number, the end user has, say, 5 text boxes with 5 max symbols.
Now lets imagine that in my VM I have only one property called SerialNumber. So my XAML would look like this:
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding SerialNumber}"/>
<TextBox Text="{Binding SerialNumber}"/>
<TextBox Text="{Binding SerialNumber}"/>
<TextBox Text="{Binding SerialNumber}"/>
<TextBox Text="{Binding SerialNumber}"/>
<StackPanel>
And my code will be like this:
class ViewModel
{
public string SerialNumber{get;set;}
}
Is there a way to bind these textboxes so that they each point ot the same property on VM and that property is updated only when the validation on all 5 textboxes passes?
EDIT: As some posters pointed out, yes, I could go with 5 separate properties for each textbox, the problem with that is that the actual situation is much more complex than in the example that I've provided. One of the reasons that the suggested approach is unfavorable is because this view will be reused in multiple places with different VM classes and going with the 5 properties approach I would have to copy them in each and every VM class that will use this View. If it were only as simple as taking five string properties and concatenating them, it would be tolerable. But in the real world scenario there is a very complicated verification, validation and combination logic behind these properties, that it makes it impractical to rewrite the same logic in each and every VM, which is why I'm looking for something reusable, something that could be done in XAML as much as possible. I was wondering if BindingGroup with some sort of ValidationRule and ValueConverter could be used in this case.
Upvotes: 1
Views: 3405
Reputation:
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.
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:
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.
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...
Upvotes: 1
Reputation: 747
I think the best way is using of 5 separate properties, but if you want only one you could use Converter and pass order number of each part as parameter.
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding SerialNumber, Converter={StaticResource SerialNumberConverter}, ConverterParameter=0}"/>
<TextBox Text="{Binding SerialNumber, Converter={StaticResource SerialNumberConverter}, ConverterParameter=1}"/>
<TextBox Text="{Binding SerialNumber, Converter={StaticResource SerialNumberConverter}, ConverterParameter=2}"/>
<TextBox Text="{Binding SerialNumber, Converter={StaticResource SerialNumberConverter}, ConverterParameter=3}"/>
<TextBox Text="{Binding SerialNumber, Converter={StaticResource SerialNumberConverter}, ConverterParameter=4}"/>
<StackPanel>
Upvotes: 1
Reputation: 69959
You cannot data bind different UI controls to one property and have them reflect different values. Instead, you will need to define additional properties to data bind to the other TextBox
es. It's stil a bit unclear as to exactly what you want, but if you want several TextBox
es that each show a few characters of a pass code, or something similar, then you'll need to do it more like this:
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding SerialNumber1}" />
<TextBox Text="{Binding SerialNumber2}" />
<TextBox Text="{Binding SerialNumber3}" />
<TextBox Text="{Binding SerialNumber4}" />
<TextBox Text="{Binding SerialNumber5}" />
<StackPanel>
...
string serialNumber = string.Concat(SerialNumber1, SerialNumber2, SerialNumber3,
SerialNumber4, SerialNumber5);
Alternatively, if you want to compare the supposedly identical values of two TextBox
es, as in a typical password entry field, you could do something like this:
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding SerialNumber1}" />
<TextBox Text="{Binding SerialNumber2}" />
<StackPanel>
...
bool isValid = SerialNumber1 == SerialNumber2;
In all cases, you will need to add further properties.
Upvotes: 2
Reputation: 31
You should try to bind it into separate properties and then use the + operator to link them togeather, in a separate property and use that after in the model.
Upvotes: 2