Binding to different property of bound object in Validation Rule - c#

Given the following View Model example
public class MyViewModel
{
public ObservableCollection<MyObjType> BoundItems { get; }
}
and MyObjType
public class MyObjType
{
public string Name { get; set; }
public int Id { get; set; }
}
I have added a Validation rule to a DataGrid Column, where the DataGrid is bound to the BoundItems collection in my ViewModel, and the Text property in the Template Column is bound to the Name.
<DataGrid ItemsSource="{Binding BoundItems}">
<DataGrid.Columns>
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TexBox>
<TextBox.Text>
<Binding Path="Name" ValidatesOnDataErrors="True">
<Binding.ValidationRules>
<xns:MyValidationRule>
<xns:MyValidationRule.SomeDependencyProp>
<xns:SomeDependencyProp SubProp={Binding Id} /> <!-- Not Working -->
</xns:MyValidationRule.SomeDependencyProp>
</xns:MyValidationRule>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
...
</DataGrid.Columns>
</DataGrid>
I want to pass another property Id of my collection type (MyObjType) to the validation rule, how do I access that from the rule. I know about the freezable and getting the context of the view model, but i need another property of my collection type that is bound to the Datagrid.
The ValidationRule and SomeDependencyProp is modeled after the example here: https://social.technet.microsoft.com/wiki/contents/articles/31422.wpf-passing-a-data-bound-value-to-a-validation-rule.aspx
public class SomeDependencyProp : DependencyObject
{
public static readonly SubPropProperty =
DependencyProperty.Register("SubProp", typeof(int),
typeof(SomeDependencyProp), new FrameworkPropertyMetadata(0));
public int SubProp{
get { return (int)GetValue(SubPropProperty ); }
set { SetValue(SubPropProperty, value); }
}
}
public class MyValidationRule: System.Windows.Controls.ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
...
}
public SomeDependencyProp SomeDependencyProp { get; set; }
}

The solution to this situation is to use a BindingProxy.

Related

Bind ComboBox Text to a ValidationRule inside another ComboBox?

I would like to bind the text from ComboBox CB1 to the Validation Rule (CommunicationMode) in ComboBox CB2.
My source code looks like this, but I get the error: "Binding" can only be set for a "DependencyProperty" of a "DependencyObject".
Is there a way to solve this?
public string CommunicationMode { get; set; }
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
return new ValidationResult(true, null);
}
<ComboBox Name="CB1">
<ComboBox.Text>
<Binding Path="CB1" UpdateSourceTrigger="PropertyChanged"/>
</ComboBox.Text>
</ComboBox>
<ComboBox Name="CB2">
<ComboBox.Text>
<Binding Path="CB2" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<valid:ComboboxValidationRule CommunicationMode="{Binding ElementName=CB1, Path=Name}" ValidatesOnTargetUpdated="True"/>
</Binding.ValidationRules>
</Binding>
</ComboBox.Text>
</ComboBox>
You could create a Wrapper class with a dependency property:
public class ComboboxValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
//your validation logic...
return new ValidationResult(true, null);
}
public Wrapper Wrapper { get; set; }
}
public class Wrapper : DependencyObject
{
public static readonly DependencyProperty CommunicationModeProperty =
DependencyProperty.Register(nameof(CommunicationMode), typeof(string), typeof(Wrapper));
public string CommunicationMode
{
get { return (string)GetValue(CommunicationModeProperty); }
set { SetValue(CommunicationModeProperty, value); }
}
}
XAML:
<ComboBox Name="CB2">
<ComboBox.Text>
<Binding Path="CB2" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<valid:ComboboxValidationRule ValidatesOnTargetUpdated="True">
<valid:ComboboxValidationRule.Wrapper>
<valid:Wrapper CommunicationMode="{Binding Source={x:Reference CB1}, Path=Name}" />
</valid:ComboboxValidationRule.Wrapper>
</valid:ComboboxValidationRule>
</Binding.ValidationRules>
</Binding>
</ComboBox.Text>
</ComboBox>

WPF ItemsControl data binding with variable path

I am trying to create a UserControl that contains an ItemsControl which should display Parameters and their values. The values must be editable and these values should be transferred back to the ViewModel.
It should be possible to define which property represents the parameter name and which property the parameter value.
Parameter class:
public class Parameter
{
public string Name { get; set; }
public string Value { get; set; }
}
ViewModel:
public class MyViewModel : INotifyPropertyChanged
{
...
public ObservableCollection<Parameter> Parameters { get; set; }
...
}
UserControl ("ParameterList.xaml"):
<UserControl x:Name="ParameterList" ...>
<Border BorderBrush="Black" BorderThickness="1" Height="100">
<!-- I don't know if this binding expression is correct -->
<ItemsControl ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type controls:ParameterList}}, Path=Parameters}">
<ItemsControl.ItemTemplate>
<Border>
<!-- The property path defined via "ParameterNameMember" should be bound. -->
<TextBlock Text="{Binding ???}" />
<!-- The property path defined via "ParameterValueMember" should be bound. -->
<!-- The value edited in this TextBox should be transferred to the ViewModel. -->
<TextBox Text="{Binding ???}" />
</Border>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
</UserControl>
UserControl code behind:
public partial class ParameterList : UserControl
{
public IEnumerable Parameters
{
get => (IEnumerable)GetValue(ParametersProperty);
set => SetValue(ParametersProperty, value);
}
public string ParameterNameMember
{
get => (string)GetValue(ParameterNameMemberProperty);
set => SetValue(ParameterNameMemberProperty, value);
}
public string ParameterValueMember
{
get => (string)GetValue(ParameterValueMemberProperty);
set => SetValue(ParameterValueMemberProperty, value);
}
public static readonly DependencyProperty ParametersProperty =
DependencyProperty.Register("Parameters", typeof(object),
typeof(ParameterList), new PropertyMetadata(default(IEnumerable)));
public static readonly DependencyProperty ParameterNameMemberProperty =
DependencyProperty.Register("ParameterNameMember", typeof(string),
typeof(ParameterList), new PropertyMetadata(""));
public static readonly DependencyProperty ParameterValueMemberProperty =
DependencyProperty.Register("ParameterValueMember", typeof(string),
typeof(ParameterList), new PropertyMetadata(""));
public ParameterList()
{
InitializeComponent();
}
}
I want to use the control as follows:
<uc:ParameterList
x:Name="ParameterList"
Parameters="{Binding Parameters}"
ParameterNameMember="Name"
ParameterValueMember="Value" />
Since I don't have that much experience with WPF, I need some help with the data binding. I would be very grateful if I could get some useful suggestions.

WPF DataGrid binding to property in subclass

I'm trying to wrap my head around how to properly bind a datagrid column with data in a subclass. To clear things up, I've done a little sample which, if resolved, would greatly help in making the code work.
Here are the classes defined:
public class SubItem
{
public string Data { get; set; }
}
public class Item
{
public int Value { get; set; }
public SubItem Data { get; set; }
}
I then create an observablecollection as follows:
public class IntData : ObservableCollection<Item>
{
public IntData() : base()
{
Item i = new Item() { Value = 56, Data = new SubItem() { Data = "testdata" } };
Add(i);
}
}
And here is my MainWindow code:
public partial class MainWindow : Window
{
public IntData Integers { get; set; }
public MainWindow()
{
Integers = new IntData();
InitializeComponent();
dataGrid1.ItemsSource = Integers; // This is an important line
}
}
The XAML code is kept simple:
<DataGrid Name="dataGrid1" AutoGenerateColumns="False" Margin="12">
<DataGrid.Columns>
<DataGridTextColumn Header="Integers" Binding="{Binding Value}"/>
<DataGridTextColumn Header="Data" Binding="{Binding Data}"/>
</DataGrid.Columns>
</DataGrid>
Running the above, you will notice that the Integers is working as it should but not the Data column.
Any ideas from anyone on how to make that column show the Data property?
Thanks in advance!
The easiest way to fix it is to override ToString() method in SubItem class and return Data property
public class SubItem
{
public string Data { get; set; }
public override string ToString()
{
return Data;
}
}
Another option is update DataGridTextColumn binding and use Data sub-property of Data property from Item class (you'll probably need to update a naming:))
<DataGridTextColumn Header="Data" Binding="{Binding Data.Data}"/>
More complicated option is to use DataGridTemplateColumn and define your own template to represent a data
<DataGridTemplateColumn Header="Data">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding Data.Data}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>

IMultiValue Converter Issue

The ItemsSource of a DataGrid is bound to an ObservableCollection. Two of the DataGridColumns are bound to a DateTime property in the collection while a third column is used to display the date difference between them using an IMultiValue Converter. The behaviour I'm trying to understand and resolve is found in Scenario 2.
Scenario 1: No Issue
View is opened and the DataGrid already contains records because the collection is NOT empty. If a new object is added to the collection, it gets displayed in the DataGrid and the last column displays the date difference value correctly.
Scenario 2: Has Issue
View is opened but DataGrid contains no records because the collection is empty. If a new object is added to the collection, it gets displayed in the DataGrid but the last column (containing the converter) is empty. However, if the view is then closed and re-opened, the date difference displays correcty in the DataGrid.
I would like the date difference value to display in the DataGridcolumn when an object is first added to an empty collection. What am I missing?
Object Class
public class Evaluation
{
public int ID { get; set; }
public DateTime BirthDate { get; set; }
public DateTime TestDate { get; set; }
}
ViewModel
public class EvaluationViewModel : ViewModelBase
{
private ObservableCollection<Evaluation> evaluations;
public class EvaluationViewModel()
{
evaluations = Utility.Convert<Evaluation>(db.evaluationRepository.GetAllById(Subject.ID));
TestView = (CollectionView)new CollectionViewSource { Source = Evaluations }.View;
TestView.SortDescriptions.Add(new SortDescription("TestDate", ListSortDirection.Ascending));
}
public ObservableCollection<Evaluation> Evaluations
{
get { return evaluations; }
}
public CollectionView TestView { get; set; }
}
View
public class Evaluation
{
public int ID { get; set; }
public DateTime BirthDate { get; set; }
public DateTime TestDate { get; set; }
}
<Window.Resources>
<converters:DateDiffMonthMultiConverter x:Key="DateConverter"/>
</Window.Resources>
<DataGrid ItemsSource="{Binding TestView}">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Path=ID}" Visibility="Hidden"/>
<DataGridTextColumn Header="Birth Date" Binding="{Binding BirthDate}"/>
<DataGridTextColumn Header="Test Date" Binding="{Binding TestDate}"/>
<DataGridTemplateColumn Header="Age When Tested">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource DateConverter}">
<Binding Path="BirthDate"/>
<Binding Path="TestDate"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
Converter
public class DateDiffMonthMultiConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
string result = string.Empty;
if(values[0] is DateTime && values[1] is DateTime)
{
DateTime start = (DateTime)values[1];
DateTime end = (DateTime)values[0];
TimeSpan ts = start - end;
double avgDaysPerMonth = 30.4;
double months = (double)ts.Days / avgDaysPerMonth;
string suffix = months > 1 ? "mths" : "mth";
result = string.Format("{0} {1}", months.ToString("0.0"), suffix);
}
return result;
}
public object[] ConvertBack(object value, Type[] targetType, object parameter, CultureInfo culture)
{
return null;
}
}
I tried your code (adding an item to the ObservableCollection after 2 seconds), and it's working for me. Here is my code:
MainWindow.xaml.cs
public MainWindow()
{
InitializeComponent();
DataContext = new EvaluationViewModel();
Loaded += MainWindow_Loaded;
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
Task.Factory.StartNew(() => Thread.Sleep(2000))
.ContinueWith((t) =>
{
(DataContext as EvaluationViewModel).Evaluations.Add(
new Evaluation() { ID = 2, BirthDate = DateTime.Now.AddYears(-22), TestDate = DateTime.Now });
}, TaskScheduler.FromCurrentSynchronizationContext());
}
ViewModel
public EvaluationViewModel()
{
Evaluations = new ObservableCollection<Evaluation>();
TestView = (CollectionView)new CollectionViewSource { Source = Evaluations }.View;
TestView.SortDescriptions.Add(new SortDescription("TestDate", ListSortDirection.Ascending));
}
public ObservableCollection<Evaluation> Evaluations { get; }
public CollectionView TestView { get; set; }
As it turns out the converter was not the issue, but instead one of the values used by the converter is a DependencyProperty (my bad for not recognizing this until now) and was throwing a DependencyProperty.UnsetValue error. I was able to resolve the issue by using the CreateNew() method when adding a new entity to the collection, so the navigation property was known at the time of loading the object into the DataGrid.

Dependency Property not updating

I'm trying to add parameters to my custom validation rule. For this I defined a dependency object like this:
public class SettingsValueValidationDependencyObject : DependencyObject
{
public Custom.ValueType ValueTypeForValidation
{
get { return (Custom.ValueType)this.GetValue(ValueTypeForValidationProperty); }
set { this.SetValue(ValueTypeForValidationProperty, value); }
}
public static readonly DependencyProperty ValueTypeForValidationProperty = DependencyProperty.Register("ValueTypeForValidation", typeof(Custom.ValueType), typeof(SettingsValueValidationDependencyObject), new UIPropertyMetadata(Custom.ValueType.Int32Value));
}
My validation rule class looks like this:
public class SettingsValueValidationRule : ValidationRule
{
public SettingsValueValidationDependencyObject SettingsValueValidationDependencyObject
{
get;
set;
}
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
// validation...
}
}
xaml code:
<DataGridTextColumn Header="Value" Width="150">
<DataGridTextColumn.Binding>
<Binding Path="Value">
<Binding.ValidationRules>
<validators:SettingsValueValidationRule>
<validators:SettingsValueValidationRule.SettingsValueValidationDependencyObject>
<validators:SettingsValueValidationDependencyObject ValueTypeForValidation="{Binding ValueType}"/>
</validators:SettingsValueValidationRule.SettingsValueValidationDependencyObject>
</validators:SettingsValueValidationRule>
</Binding.ValidationRules>
</Binding>
</DataGridTextColumn.Binding>
</DataGridTextColumn>
The two properties Value and ValueType both belong to the same object and the DataGrid's ItemsSource is bound to a list of these object. When I edit the Value cell, the ValueTypeForValidation property is always the default value (I also have a column to display the ValueType and its definitely another value). I also tried to update the BindingExpression manually in the Validate method but it won't work. What am I doing wrong?
There is no Binding in ValidationRules.
ValidationRules are not part of LogicalTree and so there is no DataContext to serve as Source in your Binding.
There are however few tricks on the internet how to make a ValidationRule "bindable".
Take a look at this tut:
Binding on a Non-UIElement

Categories

Resources