ComboBox Validation without Binding - c#

This question is based on this solution, but I somehow cannot get this to work using a ComboBox. I would like to validate that upon clicking a submit button, the combobox has a selected item and is not null. Please note that I am not binding to anything on purpose, and not because I don't know how to. But if the answer is that there is no way I can use the validation rules without binding (to ComboBoxes specifically, I've done it to textboxes via the linked solution), please let me know.
Here is what I have so far:
XAML:
<ComboBox DataContext="{StaticResource ChargeAssigneeViewSource}" Name="ChargeAssigneeBox" ItemsSource="{Binding}" Width="85">
<ComboBox.SelectedItem>
<Binding RelativeSource="{RelativeSource Self}" Path="GetType" Mode="TwoWay">
<Binding.ValidationRules>
<my:ComboBoxValidationRule ErrorMessage="Please select an Assignee" />
</Binding.ValidationRules>
</Binding>
</ComboBox.SelectedItem>
</ComboBox>
Validation Rule:
class ComboBoxValidationRule : ValidationRule
{
private string errorMessage;
public string ErrorMessage
{
get { return errorMessage; }
set { errorMessage = value; }
}
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
if (value == null)
return new ValidationResult(false, ErrorMessage);
return new ValidationResult(true, null);
}
}
Button Click:
private void AddAnHours()
{
Employee currEmployee;
if (!ValidateElement.HasError(ChargeAssigneeBox))
{
if (!ValidateElement.HasError(analystTimeTxtBox))
{
currEmployee = ChargeAssigneeBox.SelectedItem as Employee;
item.AddTime(currEmployee, DateTime.Now, double.Parse(analystTimeTxtBox.Text));
analystTimeTxtBox.Clear();
ChargeAssigneeBox.SelectedItem = null;
}
}
UpdateTotals();
}
The error that I get is in this line:
currEmployee = ChargeAssigneeBox.SelectedItem as Employee;
but my ItemsSource is binding properly so even though I have selected an item, the selecteditem is not converting it to an employee object. I suspect it has something to do with what I am binding it to:
<Binding RelativeSoruce="{RelativeSource Self}" Path="GetType"....>
Help would be greatly appreciated, thanks.

You can't bind a property to the selectedItem on the combobox and then check if thats property is null?
Public string SelectedComboboxItem { get; set; }
<ComboBox DataContext="{StaticResource ChargeAssigneeViewSource}" SelectedItem="{Binding SelectedComboboxItem}" Name="ChargeAssigneeBox" ItemsSource="{Binding}" Width="85">

Related

Bind custom class with Validation in WPF

I noticed that it is possible to bind variables of the type DateTime to a textbox in WPF. If I enter a wrong value it will not validate and show a red border.
How can I implement my own class, that I can bind to a textbox without having to bind to a property of the class? The Textbox should show a string and the class will validate the input.
Is this possible?
My current solution is this:
In the Model:
public string DefaultLanguageValue
{
get
{
return _defaultLanguageValue;
}
set
{
if (value != this._defaultLanguageValue)
{
ValidateLanguage(value);
this._defaultLanguageValue = value;
NotifyPropertyChanged();
}
}
}
private void ValidateLanguage(string value)
{
string rx = "([a-zA-Z]{2}|[iI]-[a-zA-Z]+|[xX]-[a-zA-Z]{1,8})(-[a-zA-Z]{1,8})*";
if (!Regex.IsMatch(value, rx))
{
throw new ArgumentException();
}
}
In the XAML:
<TextBox Text="{Binding TreeViewModel.Model.DefaultLanguageValue, UpdateSourceTrigger=PropertyChanged, ValidatesOnExceptions=True}" BorderThickness="0" MinWidth="100"/>
It would be nice to have a Class that I can just bind like a String, Int or DateTime for examlpe. Any Ideas?
You could bind to the Tag property of the TextBox itself and validate using a ValidationRule:
public class DateValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
if (!DateTime.TryParse(value as string, out DateTime _))
return new ValidationResult(false, "Invalid date...");
return ValidationResult.ValidResult;
}
}
XAML:
<TextBox>
<TextBox.Text>
<Binding Path="Tag" RelativeSource="{RelativeSource Self}"
UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:DateValidationRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
This doesn't require you to bind to a view model.
I finally tried the solution suggested by mm8.
The only issue I have now is that if one enters an invalid value into the textbox, it will not update the textbox when I programmatically change the value of the source after clicking a button.
I tried Validation after update, but this allows the user to save invalid values.
<TreeViewItem>
<TreeViewItem.Header>
<StackPanel Orientation="Horizontal">
<CheckBox x:Name="chkDefaultLanguage" IsChecked="{Binding TreeViewModel.TreeModel.DefaultLanguage, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Text="DefaultLanguage: " />
<TextBox BorderThickness="0" MinWidth="100">
<TextBox.Text>
<Binding Path="TreeViewModel.TreeModel.DefaultLanguageValue" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<validationrules:LanguageCodeValidationRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
</StackPanel>
</TreeViewItem.Header>
</TreeViewItem>
class LanguageCodeValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
string rx = "([a-zA-Z]{2}|[iI]-[a-zA-Z]+|[xX]-[a-zA-Z]{1,8})(-[a-zA-Z]{1,8})*";
if (!Regex.IsMatch(value.ToString(), rx))
{
return new ValidationResult(false, "Invalid Language Codee.");
}
return ValidationResult.ValidResult;
}
}

Set custom error message in ValidationRules in WPF

I have set everything perfectly. If I set some string in ErrorMessage then it shows without error.
What I want is, I want to set ErrorMessage dynamically/programmatically. something
MyValidation.ErrorMessage = "some new message";
username.Update() //something
XAML Code
<TextBox Margin="5" Name="userName">
<TextBox.Text>
<Binding RelativeSource="{RelativeSource Self}" Path="Tag" Mode="OneWayToSource" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:MyValidation ErrorMessage="Static String" />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
C# Class Code
public class MyValidation : ValidationRule {
public string ErrorMessage { get; set; }
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
if (ErrorMessage.Length > 0) {
return new ValidationResult(false, ErrorMessage);
}
return ValidationResult.ValidResult;
}
}
If you give the ValidationRule a name in the XAML markup:
<Binding.ValidationRules>
<local:MyValidation x:Name="val" ErrorMessage="Static String" />
</Binding.ValidationRules>
...you could set its ErrorMessage property directly and then just explicitly update the binding:
val.ErrorMessage = "some new message";
userName.GetBindingExpression(TextBox.TextProperty)?.UpdateSource();
You can implement INotifyDataErrorInfo in your viewmodel. Implement GetErrors(string) so it returns different error messages based on your condition. You can even return multiple messages at once and they will be displayed below each other.
Here's a nice tutorial, but feel free to implement it on your own. Keep in mind that there's not just one correct approach and the interface gives you a lot of freedom.

WPF Multibinding: OneWayToSource binding from TextBox updated via another binding doesnt work?

I have a DataGrid bound to the People collection. Also I have a TextBox that should accept the Name value from the selected row. User can then edit the value or can leave it as is. The key point is: the text shown in the TextBox no matter whether it originates from collection or user typing must be propagated to the property NewName.
I've set two bindings for the NewNameTextBox: OneWay'ed to the CollectionView behind the DataGrid, and OneWayToSource'ed to the property:
<Window.Resources>
<CollectionViewSource x:Key="PeopleCollection"
Source="{Binding Path=People, Mode=OneWay}" />
<local:ConverterNewNamePrefill x:Key="ConverterNewNamePrefill" />
</Window.Resources>
<Grid>
<StackPanel>
<DataGrid ItemsSource="{Binding Source={StaticResource PeopleCollection}}"
AutoGenerateColumns="True"
IsReadOnly="True"
Margin="10">
</DataGrid>
<StackPanel Orientation="Horizontal" Margin="10">
<TextBox>
<TextBox.Text>
<MultiBinding Converter="{StaticResource ConverterNewNamePrefill}" >
<Binding Source="{StaticResource PeopleCollection}" Path="Name" Mode="OneWay" />
<Binding Path="NewName" Mode="OneWayToSource" />
</MultiBinding>
</TextBox.Text>
</TextBox>
</StackPanel>
</StackPanel>
</Grid>
I suppose the property should be updated when user changes selection in the DataGrid, but this doesn't happen. The TextBox gets updated and shows the selected Name value, but the property bound via OneWayToSource remains unchanged.
If the user types into the TextBox, the property gets updated as expected.
So the question is how can I update a property from both the sources via multi-bound TextBox without code behind view?
Here is the code behind window:
public class Person
{
public string Name { get; set; }
public string Surname { get; set; }
}
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private ObservableCollection<Person> _people = new ObservableCollection<Person> {
new Person() {Name = "Mitchell", Surname = "Sofia" },
new Person() {Name="Bush", Surname="Ethan" },
new Person() {Name="Ferrero", Surname="Emma" },
new Person() {Name="Thompson", Surname="Aiden" }
};
public ObservableCollection<Person> People => _people;
public string NewName { get; set; } = "Jackson";
}
public class ConverterNewNamePrefill : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return values[0];
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
return new[] { value, value };
}
}
The converter's method ConvertBack() is called only when user types, but not when the TextBox.Text updated from collection.
Thank you!
This is just how bindings work. The source or sources are not updated unless the target changes by means other than the binding itself. I.e. it's assumed that if the target was just updated from the source(s), then the source(s) is(are) already up-to-date and do not need updating.
Without more details it's difficult to know for sure what you want. But it seems like you might either want for NewName to actually be the target of a second binding, where the source is the same Name property being used as the source for the TextBox.Text property, or you want subscribe to the TextBox.TextChanged event and your handler explicitly write back the value to the NewName property when that event is raised.
In the former case, you'll have to make NewName a dependency property of MainWindow. That's a complication you may or may not want to deal with. If not, then I'd recommend the latter approach.

Apply conversion to all elements in a ComboBox drop down menu

I am trying to change the text only for contents of all items in a ComboBox based on a specific property in the ViewModel. I’ve created a DataTemplate with the Binding values as SelectedValue and the specific property I want to base the conversion on SomeProperty:
<ComboBox ItemsSource="{Binding Path=ChannelValues}"
SelectedValue="{Binding Path=Channel, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource ResourceKey=ChannelNumConverter}">
<Binding Path="SelectedValue"
RelativeSource="{RelativeSource AncestorType={x:Type ComboBox}}" />
<Binding Path="DataContext.SomeProperty"
ElementName="DataContextView" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
This appears to work, expect all the values in the drop down get changed to the translated SelectedValue. I’ve tried replacing SelectedValue with Text, but that doesn’t work either. Is there a way to apply this conversion to all values in the drop down (again, only changing the displayed values, not the underlying data)?
Update - ViewModel
// Populate somewhere with values
private ObservableCollection<ushort> mChannelValues = new ObservableCollection<ushort>();
public ObservableCollection<ushort> ChannelValues
{
get
{
return mChannelValues;
}
}
private ushort mChannelNum;
public ushort Channel
{
get
{
return mChannelNum;
}
set
{
if (mChannelNum != value)
{
mChannelNum = value;
OnPropertyChanged(new PropertyChangedEventArgs("Channel"));
}
}
}
private ushort mSomeProperty;
public ushort SomeProperty
{
get
{
return mSomeProperty;
}
set
{
if (mSomeProperty!= value)
{
mSomeProperty= value;
OnPropertyChanged(new PropertyChangedEventArgs("SomeProperty"));
}
}
}
Update 2 - Simple Converter
public object Convert(
object[] values,
Type targetType,
object parameter,
CultureInfo culture)
{
if (targetType != typeof(string))
throw new InvalidOperationException("The target must be a string");
if ((values[0] != null) && (!(values[0] is ushort)))
throw new InvalidOperationException("The channel must be an short");
if ((values[1] != null) && (!(values[1] is ushort)))
throw new InvalidOperationException("The some property must be a ushort");
ushort ushort_val = ushort.Parse((string)values[0]);
ushort ushort_some_property = ushort.Parse((string)values[1]);
switch (ushort_some_property)
{
case 0:
return (ushort_val + 1).ToString();
case 1:
return (ushort_val + 7).ToString();
case 2:
return (ushort_val + 2).ToString();
default:
return ushort_val.ToString();
}
}
Instead of using a MultiBinding, you could use SomeProperty as a ConverterParameter
<TextBlock Text="{Binding Converter={StaticResource ResourceKey=ChannelNumConverter}, ConverterParameter={Binding SomeProperty}}"/>
In converter:
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var someProperty = parameter as SomeType;
...
The problem is in fact that your itemTemplate is applied to ALL! Items in the combobox, whose converter actually processes the currently selected item and someproperty leading to equal values in all items.
The approach at this point is not the problem. You just have to bind the current text's value in the viewmodel instead of the selected value.
This will combine two values from your viewmodel into the result displayed in the textbox without any recursive updates.
Finally figured out how to do this. Below is the xaml to apply the converter to all elements in the drop down list:
<ComboBox ItemsSource="{Binding Path=ChannelValues}"
SelectedValue="{Binding Path=Channel, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource ResourceKey=ChannelNumConverter}">
<Binding />
<Binding Path="DataContext.SomeProperty"
RelativeSource="{RelativeSource Mode=FindAncestor,
AncestorType=local:DataContextView}" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>

How to use MultiBinding in a WPF ComboBox

This is driving me NUTS!!!
I have a ComboBox used to filter a query by employee which works fine but only displays the employees first name. I want to use a MultiValueConverter to display the employees full name (This would be less urgent if we did not have 2 Mikes and 2 Daves)
Below is my working code and the IMultiValueConverter Class (With unnecessary formatting trimmed out for brevity). I have tried everything I can think of to get the MultiConverter to work but I have had no luck.
<ComboBox ItemsSource="{Binding Path=EmployeesFilter}"
DisplayMemberPath="EmpFirstName"
SelectedValue="{Binding Path=EmployeeToShow, Mode=TwoWay}"/>
The ViewModel Properties it is bound to:
// This collection is used to populate the Employee Filter ComboBox
private ObservableCollection<Employee> employeesFilter;
public ObservableCollection<Employee> EmployeesFilter
{
get {
return employeesFilter;
}
set {
if (employeesFilter != value)
{
employeesFilter = value;
OnPropertyChanged("EmployeesFilter");
}
}
}
// This property is TwoWay bound to the EmployeeFilters SelectedValue
private Employee employeeToShow;
public Employee EmployeeToShow
{
get {
return employeeToShow;
}
set {
if (employeeToShow != value)
{
employeeToShow = value;
OnPropertyChanged("EmployeeToShow");
QueryIssues(); // Requery with new employee filter
}
}
}
The IMultiValueConverter:
class StringsToFullNameMultiConverter : IMultiValueConverter
{
public object Convert(object[] values,
Type targetType,
object parameter,
System.Globalization.CultureInfo culture)
{
// I added this because I kept getting DependecyProperty.UnsetValue
// Passed in as the program initializes
if (values[0] as string != null)
{
string firstName = (string)values[0];
string lastName = (string)values[1];
string fullName = firstName + " " + lastName;
return fullName;
}
return null;
}
public object[] ConvertBack(object value,
Type[] targetTypes,
object parameter,
System.Globalization.CultureInfo culture)
{
return null;
}
}
I tried a lot of different things but basically am using the following in the ComboBox
<ComboBox.SelectedValue>
<MultiBinding Converter="{StaticResource StringsToFullNameMultiConverter}"
Mode="OneWay" >
<Binding Path="EmpFirstName" />
<Binding Path="EmpLastName"/>
</MultiBinding>
</ComboBox.SelectedValue>
As it stands now the converter is called when the program initializes with the values set to DependencyProperty.UnsetValue. after that it is never called again, even when you select a name from the box. The names are still displayed as a first name.
Thanks for any help or pointers to good tutorials/samples you can provide. All the ones I keep finding on the web are for textboxes and I can use them just fine all day.
You're close! What you want to do though is ComboBox.ItemTemplate, not SelectedValue. Prepare for some XAML hell.
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource StringsToFillNameMultiConverter}">
<Binding Path="EmpFirstName" />
<Binding Path="EmpLastName" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
Also, if I recall correctly, you don't need to create your own converter if you're just formatting strings. I think you can do the following (someone please correct me if I'm wrong.)
<!-- "Last, First" -->
<MultiBinding StringFormat="{}{1}, {0}">
<Binding Path="EmpFirstName" />
<Binding Path="EmpLastName" />
</MultiBinding>
You may be better of using a data template for the items, so you have complete control over how each person is displayed in the dropdown list.
The type convert is OK provided you don’t have a need to control the formatting of the different fields.
I ended up by addig a Readonly Property to my Class and use Displaymemberpath in the Combobox
public class MyEmployee
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string DisplayName {
get { return FirstName + " " + LastName; }
}
}
Could something like this work for your situation...?
BR,
Daniel

Categories

Resources