Dependency Property not updating - c#

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

Related

Binding dynamic list of CheckBoxes' IsChecked property

I have a situation where I load a list of objects from an SQL database. For each of the objects, I want to display a CheckBox in an ItemsControl and have its IsChecked property bound to true if and only if a member of the window's DataContext contains that object in a list.
Let's call my window MyWindow. The DataContext of MyWindow is an object of type MyContext which has a list of objects (loaded from a database) of type DataObject, and an object of type Item:
public class MyContext {
public Item CurrentItem { get; set; }
public List<DataObject> Data { get; set; }
}
public class Item {
public List<DataObject> CheckedDataObjects { get; set; }
}
In MyWindow.xaml I have my ItemsControl which is bound to the Data list. The ItemTemplate defines that each DataObject should be displayed with a CheckBox, which should have its IsChecked bound to true if and only if the particular DataObject is contained in MyWindow.DataContext.CurrentItem.CheckedDataObjects.
My best idea is to use an IMultiValueConverter approach, however I get an XamlParseException with an inner InvalidOperationException, saying that two-way binding requires Path or XPath (loosely translated). Please advise!
<ItemsControl ItemsSource="{Binding Data}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<CheckBox>
<CheckBox.IsChecked>
<MultiBinding Converter="{StaticResource MyItemHasDataObjectConverter}">
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type local:MyWindow}}" Path="DataContext.CurrentItem"/>
<Binding RelativeSource="{RelativeSource Self}"/>
</MultiBinding>
</CheckBox.IsChecked>
</CheckBox>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
public class ItemHasDataObjectConverter : IMultiValueConverter {
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) {
// values should contain two objects: the CurrentItem object and an object of type DataObject
if (values.Length == 2 && values[0] is Item && values[1] is DataObject) {
return (values[0] as Item).CheckedDataObjects.Contains(values[1] as DataObject);
}
return false;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) {
return null;
}
}
EDIT
After Aabid's answer below, the converter now seems to work correctly. Further, I've added Checked and Unchecked event handlers to the CheckBox objects, which add/remove the corresponding DataObject object from CurrentItem.CheckedDataObjects. However, if I reset CurrentItem.CheckedDataObjects via code behind, either by calling Clear() or setting CheckedDataObjects = new List<DataObject>(), the CheckBox does not get updated in the UI (they stay checked).
I have made sure both MyContext and Item implement INotifyPropertyChanged and fire the corresponding OnPropertyChanged methods.
Add the Path=DataContext property to your second Binding in your multivalue binding, i.e
<Binding Path=DataContext RelativeSource="{RelativeSource Self}"/>

Validation based on existing data in WPF

I need to create a validation node that will return an error if value entered already exists. I have GUI with items that can have their name set. I want to enforce the names to be unique.
So for each validation, I need following two parameters:
List of all names of all items, or some predicate that will tell me a name exists
Current items name, to exclude it from the above validation (changing the name to the same value should not be an error)
The data contexts look like this (just the interface for illustration):
class AppMainContext
{
public IEnumerable<string> ItemNames {get;}
public Item SelectedItem {get;}
}
class Item
{
public string Name {get;}
}
The field in WPF looks like this and its parent is bound to `{SelectedItem}:
<DockPanel DockPanel.Dock="Top">
<Label Content="Name: "/>
<TextBox DockPanel.Dock="Top">
<TextBox.Text>
<Binding Path="Name" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<vmvalidation:UniqueNameRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
</DockPanel>
The validator looks like this:
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Windows.Controls;
namespace MyApp.Validation
{
public class UniqueNameRule : ValidationRule
{
public IEnumerable<string> ExistingNames { get; set; }
public string MyName { get; set; }
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
if(value is string newValue)
{
// name changed
if(!value.Equals(MyName))
{
if(ExistingNames.Contains(newValue))
{
return new ValidationResult(false, "Name already exists!");
}
}
return new ValidationResult(true, null);
}
else
{
return new ValidationResult(false, "Invalid value type. Is this validator valid for the given field?");
}
}
}
}
I tried to at least bind current name to the validator. The text box already exists in current items data context, so a correct binding would be:
<Binding.ValidationRules>
<vmvalidation:UniqueNameRule MyName="{Binding Name}" />
</Binding.ValidationRules>
Except that this gives an error:
The member MyName is not recognized or is not accessible.
The list of all items is in the windows data context, accessible through ItemNames. I suppose it could be accessed like this:
{Binding Path=DataContext.ItemNames, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}
I tried correct binding using an answer below, but I then get an error:
A 'Binding' cannot be set on the 'MyName' property of type MyProject_Validation_UniqueNameRule_9_468654. A 'Binding' can only be set on a DependencyProperty of a DependencyObject.
Looks like bindings are not supported at all.
So how can I put this together, so that the validation rule can access both of these variables?
The binding is failing due to the nature of how the validation rule falls on the visual tree, and maybe is what you suspect.
There are other flavors of RelativeSource (see the properties section in that document) on bindings.
Ultimately one wants the parent node, here is one used on styles which might be relevant:
<vmvalidation:UniqueNameRule
MyName="{Binding Name, RelativeSource={RelativeSource TemplatedParent}}"/>
Or work your way up the chain, instead of x:Type Window how the more likely binding to the parent such as x:Type TextBox:
<vmvalidation:UniqueNameRule
MyName="{Binding Name, RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType={x:Type TextBox}}"/>

Binding to different property of bound object in Validation Rule

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.

List<Object> as dependecy property in WPF

I am trying to create a dependency property to pass a List<Object> to my custom validation class but I get the following error:
A 'Binding' cannot be set on the 'TimeSheetRowList' property of type 'WpfApp1_Services_Wrapper_2_252121760'. A 'Binding' can only be set on a DependencyProperty of a DependencyObject.
Here is my XAML code:
<UserControl x:Class="WpfApp1.Views.Installer"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApp1.Views"
xmlns:validation="clr-namespace:WpfApp1.Services"
mc:Ignorable="d"
>
<Grid>
<DataGrid >
<DataGrid.Columns>
<DataGridTextColumn Header="Time from" >
<DataGridTextColumn.Binding>
<Binding Path="TimeFrom" StringFormat="HH:mm" ValidatesOnNotifyDataErrors="True">
<Binding.ValidationRules>
<validation:TimeIntervalValidation>
<validation:TimeIntervalValidation.Wrapper >
<validation:Wrapper TimeSheetRowList="{Binding DataList}"/>
</validation:TimeIntervalValidation.Wrapper>
</validation:TimeIntervalValidation>
</Binding.ValidationRules>
</Binding>
</DataGridTextColumn.Binding>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
</StackPanel>
</Grid>
public class TimeIntervalValidation:ValidationRule
{
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
// method implementation
}
public Wrapper Wrapper { get; set; }
}
public class Wrapper : DependencyObject
{
public static readonly DependencyProperty TimeSheetRowCollectionProperty =
DependencyProperty.Register("TimeSheetRowList", typeof(IList<TimeSheetRow>),
typeof(Wrapper),
new FrameworkPropertyMetadata(
new ObservableCollection<TimeSheetRow>()
));
public ObservableCollection<TimeSheetRow> TimeSheetRowList
{
get { return (ObservableCollection<TimeSheetRow>)GetValue(TimeSheetRowCollectionProperty); }
set { SetValue(TimeSheetRowCollectionProperty, value); }
}
}
Tried to close project then clean and rebuild solution but doesn't work. To mention I am only getting the error mentioned above only when entering in XAML, otherwise if I just build solution I do not get any errors before going to XAML.
When you use typeof(IList<TimeSheetRow>) as property type argument of the Register method, you must also use that type for the property wrapper.
Besides that, you must not set any other value than null for the default value of collection-type properties. Otherwise all instances of the Wrapper class would use the same single default collection object.
Also adhere to naming conventions for the depenency property identifier field.
public static readonly DependencyProperty TimeSheetRowListProperty =
DependencyProperty.Register(
nameof(TimeSheetRowList),
typeof(IList<TimeSheetRow>),
typeof(Wrapper),
new PropertyMetadata(null));
public IList<TimeSheetRow> TimeSheetRowList
{
get { return (IList<TimeSheetRow>)GetValue(TimeSheetRowListProperty); }
set { SetValue(TimeSheetRowListProperty, value); }
}

Change the visibility of elements in ComboBox dynamically using MVVM framework

Ok,
I have seen a few similar questions but have not been able to figure out this problem for the past couple days. I have two Comboboxes and I want each one to hide the selected element in the other one. For example, if I select a value in ComboBox 1 that selected item should be removed as an option in ComboBox 2.
I thought about using a command but ComboBoxes don't have commands. I have pasted below the comboboxes' XAML and ViewModel code. I would appreciate any help with this. I know the code below is wrong but I think that the logic for this should be in the setters of the bounded to ItemSource.
<ComboBox Margin="0,7,0,0"
Name="ComboBoxA"
HorizontalAlignment="Stretch"
Header="{Binding AccountHeader}"
ItemTemplate="{StaticResource ComboBoxTemplate}"
ItemsSource="{Binding ChargedAccounts,
Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}"
SelectedItem="{Binding SelectedAccount,
Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}" />
<ComboBox x:Uid="TargetAccountTextBox"
Name="ComboBoxB"
Margin="0,7,0,0"
HorizontalAlignment="Stretch"
Header="target account"
ItemTemplate="{StaticResource ComboBoxTemplate}"
ItemsSource="{Binding TargetAccounts,
Mode=TwoWay,
namespace MoneyFox.Shared.ViewModels
{
[ImplementPropertyChanged]
public class ModifyPaymentViewModel : BaseViewModel, IDisposable
{
private readonly IDefaultManager defaultManager;
private readonly IDialogService dialogService;
private readonly IPaymentManager paymentManager;
//this token ensures that we will be notified when a message is sent.
private readonly MvxSubscriptionToken token;
private readonly IUnitOfWork unitOfWork;
// This has to be static in order to keep the value even if you leave the page to select a category.
private double amount;
private Payment selectedPayment;
public ModifyPaymentViewModel(IUnitOfWork unitOfWork,
IDialogService dialogService,
IPaymentManager paymentManager,
IDefaultManager defaultManager)
{
this.unitOfWork = unitOfWork;
this.dialogService = dialogService;
this.paymentManager = paymentManager;
this.defaultManager = defaultManager;
TargetAccounts = unitOfWork.AccountRepository.Data;
ChargedAccounts = unitOfWork.AccountRepository.Data;
token = MessageHub.Subscribe<CategorySelectedMessage>(ReceiveMessage);
}
ObservableCollection<Account> _SelectedAccount;
ObservableCollection<Account> SelectedAccount
{
get
{
return _SelectedAccount;
}
set
{
_SelectedAccount = value;
for(int i = 0; i < ChargedAccounts.Count; i++)
{
if(ChargedAccounts[i].ToString() == _SelectedAccount.ToString())
{
ChargedAccounts.Remove(ChargedAccounts[i]);
}
}
}
}
ObservableCollection<Account> _TargetAccount;
ObservableCollection<Account> Targetccount
{
get
{
return _SelectedAccount;
}
set
{
_SelectedAccount = value;
for (int i = 0; i < TargetAccounts.Count; i++)
{
if (TargetAccounts[i].ToString() == _SelectedAccount.ToString())
{
TargetAccounts.Remove(ChargedAccounts[i]);
}
}
}
}
While I do agree with a lot of the points in the answer provided by Ed, there is a simpler way to do this without DataTriggers or Converters. There is already a filterable CollectionViewSource in the framework that is your friend (Scott Hanselman loves it)
I would bind ComboBoxA to your regular ChargedAccounts property, but I would modify ComboBoxB to:
bind to a property in the code behind of the View that returns a ICollectionView
in a SelectionChanged event handler for ComboBoxA (also in the code behind of the view) I would adjust the filter for the ICollectionView to exclude the currently selected item
Roughly, this can be done in just a couple of lines:
public ICollectionView FilteredData { get; set; }
private void ComboBoxA_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var z = new CollectionViewSource {Source = ViewModel.ChargedAccounts.Where(p => p != ViewModel.SelectedAccount) };
FilteredData = z.View;
}
Of course this assumes you've done the right thing by having a ViewModel property in the code behind of your view preferably exposed as an interface, and that the ChargedAccounts and SelectedAccount properties are available via that interface.
You could also cobble these couple of lines together in your viewmodel and trigger it via a property change on SelectedAccount - I just have the opinion that a filter operation in response to a UI action should go in the code behind of the UI, but that decision is really up to you.
Give the comboboxes an ItemContainerStyle (TargetType="ComboBoxItem") with a data trigger. For ComboBoxA, that'll look like this:
<ComboBox
...
x:Name="ComboBoxA"
...
>
<ComboBox.ItemContainerStyle>
<Style TargetType="ComboBoxItem">
<Style.Triggers>
<DataTrigger Value="True">
<DataTrigger.Binding>
<MultiBinding
Converter="{local:ObjectEquals}"
>
<Binding
Path="SelectedItem"
ElementName="ComboBoxB" />
<!-- Binding with no properties just binds to the DataContext -->
<Binding />
</MultiBinding>
</DataTrigger.Binding>
<Setter
Property="Visibility"
Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.ItemContainerStyle>
</ComboBox>
ComboBoxB gets the same deal, but ElementName="ComboBoxA" in the SelectedItem binding.
And we'll need to write that multi-value converter. It's as easy as they come:
public class ObjectEquals : MarkupExtension, IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return values.Length == 2 && values[0] == values[1];
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
}
It'd be so convenient if you could bind DataTrigger.Value to {Binding}, but it's not a dependency property.
You *could^ also do this purely in the viewmodel by temporarily removing SelectedAccount from TargetAccounts -- you'd have a private full _targetAccountsFull list, and a public filtered one. The setter for SelectedAccount would filter the list. Were you trying to do that already?
But that's not my idea of a good solution. Hiding combo box items is UI design stuff; the viewmodel shouldn't be involved in it, and in fact shouldn't even be aware that such things take place. One of the pleasures of WPF/MVVM is that you can separate that stuff out into pure UI code in the view. The viewmodel has its own complexities to worry about.
By the way, you bind SelectedItem to SelectedAccount, but SelectedAccount is an ObservableCollection. That makes no sense. There's one selected account. Make it a single Account, not a collection of them.

Categories

Resources