I have made a CustomControl, with the DP Threshold, like so:
public class SymbolControl : Control
{
public static readonly DependencyProperty ThresholdProperty = DependencyProperty.Register("Threshold", typeof(IThreshold<SolidColorBrush>), typeof(SymbolControl));
public IThreshold<SolidColorBrush> Threshold
{
get { return (IThreshold<SolidColorBrush>)GetValue(ThresholdProperty); }
set { SetValue(ThresholdProperty, value); }
}
...
}
Here is where the Property is used in the xaml of the custom control:
...
<Border.Background>
<MultiBinding Converter="{StaticResource ThreshholdToReturnValueConverter}" NotifyOnTargetUpdated="True" >
<Binding Path="Threshold" RelativeSource="{RelativeSource TemplatedParent}" NotifyOnTargetUpdated="True" />
<Binding Path="SymbolValue" RelativeSource="{RelativeSource TemplatedParent}" NotifyOnTargetUpdated="True" />
<Binding Path="DefaultBackground" RelativeSource="{RelativeSource TemplatedParent}" NotifyOnTargetUpdated="True" />
</MultiBinding>
</Border.Background>
...
And here is How the CustomControl is used:
<controls:SymbolControl ... Threshold="{Binding Threshold, NotifyOnTargetUpdated=True, Converter={StaticResource DummyConverter}}" .../>
When I call NotifyPropertyChanged(nameof(Threshold)), the CustomControl does not update.
However I have placed a dummy converter, with a breakpoint in, in the Threshold binding when the custom control is instantiated and this breakpoint triggers when I call NotifyPropertyChanged(nameof(Threshold)), so it appears the binding isn't updating the target?
I have also tried adding a PropertyChangedCallback for the DP ThresholdProperty with a breakpoint in which only triggers when the original property is first instantiated.
I have also found doing this in the ViewModel causes the custom control to update:
var temp = Threshold;
Threshold = null;
Threshold = temp;
I have done lots of searching online and had no luck, any ideas of what the problem could be?
I found another work around as I was unable to use the other work around given in the question:
I added the code into Threshold.cs:
public Threshold : IThreshold<SolidColorBrush>, INotifyPropertyChanged
{
...
public Threshold()
{
...
this.PropertyChanged += delegate (object sender, PropertyChangedEventArgs e) { if (e.PropertyName != nameof(WorkAround)) { NotifyPropertyChanged(nameof(WorkAround)); } };
}
...
public bool WorkAround { set; get; }
}
In the xaml of the custom control I added the 4th binding to the MultiBinding:
...
<Border.Background>
<MultiBinding Converter="{StaticResource ThreshholdToReturnValueConverter}" NotifyOnTargetUpdated="True" >
<Binding Path="Threshold" RelativeSource="{RelativeSource TemplatedParent}" NotifyOnTargetUpdated="True" />
<Binding Path="SymbolValue" RelativeSource="{RelativeSource TemplatedParent}" NotifyOnTargetUpdated="True" />
<Binding Path="DefaultBackground" RelativeSource="{RelativeSource TemplatedParent}" NotifyOnTargetUpdated="True" />
<Binding Path="Threshold.WorkAround" RelativeSource="{RelativeSource TemplatedParent}" NotifyOnTargetUpdated="True" />
</MultiBinding>
</Border.Background>
...
However this workaround is not ideal, so I will not accept it, if anyone finds a better solution please let me know :)
Related
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.
I'm quite new with WPF and I might be missing something simple, but I've tried with some simple examples for Custom User Control Binding and they work, but when I try to apply them to our situation, the binding only works when I modify the value in the Custom User Control, but if I change it from other place the Custom User Control does not reflect that change.
We are using Prism and we have a ViewModel and a View to show and edit the properties of a Device. These properties are read from an XML at startup and added to the Device as a Dictionary of Settings. Both Settings and Device implement INotifyPropertyChanged and when a Setting's value changes, the Device raises the property Changed event for that Setting as well.
So when the program is started the values are shown like this, where the red arrow is the new custom user control and blue arrow points the working code directly in the view initial state:
If I change the value in the custom user control, its updated in the other one as well. Binding to source ok:
But if I change it on the other one, its not updated in the custom user control. Binding from source not ok:
Also, if I change the value of the str_pressureUnit condition, the conversion is only performed in the old code. Conversion condition binding not ok:
But the Enable/Disable Control, works correctly for both. Enable ok:
Simple examples with another custom user control and properties from Device that are not Dynamic work ok if I use the `Mode="TwoWay". I was thinking that maybe is a problem with the Multibindings or with the Dynamic Settings configuration, but as the one with EnableProperties work (which is a normal property of the ViewModel) I suspect it might be something related to the Dynamic properties.
This is how our Device class looks like:
public class Device : DynamicObject, INotifyPropertyChanged
{
#region Properties
...
public Dictionary<string, ISetting> DynamicSettings { get; private set; } = new Dictionary<string, ISetting>();
...
public Device(SettingDefinitionsProvider settingDefinitionsProvider, ICommunicationChannel communicationChannel, DeviceType deviceType)
{
...
foreach(ISetting s in DynamicSettings.Values)
{
s.PropertyChanged += OnSettingValueUpdated;
}
}
...
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName]string propertyname = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyname));
}
#endregion //INotifyPropertyChanged
#region EventHandlers
private void OnSettingValueUpdated(object sender, PropertyChangedEventArgs e)
{
if(sender is ISetting)
{
OnPropertyChanged((sender as ISetting).Name);
}
}
#endregion //EventHandlers
}
And the Setting class throws the PropertyChanged event when its value changes.
The viewModel has a SelectedDevice property and its set as DataContext of the view with the AutoWireViewModel functionality of Prism.
We are also using MultiBinding and some complex converters because some Settings shown value might change depending on other Setting Value, as well the control being enabled, etc. So one entry in the XAML for a Setting might get this big:
<TextBlock Grid.Row="4" Text="{Binding SelectedDevice.SafetyMargin.DisplayName}" Margin="5"/>
<TextBox Grid.Row="4" Grid.Column="1" Margin="5">
<TextBox.IsEnabled>
<MultiBinding Converter="{conv:EnableControlConverter}">
<Binding RelativeSource="{RelativeSource AncestorType={x:Type UserControl}, Mode=FindAncestor}" Path="DataContext.SelectedDevice.SafetyMargin"/>
<Binding RelativeSource="{RelativeSource AncestorType={x:Type UserControl}, Mode=FindAncestor}" Path="DataContext.EnableControls"/>
</MultiBinding>
</TextBox.IsEnabled>
<MultiBinding Converter="{conv:ConversionConverter}">
<Binding RelativeSource="{RelativeSource AncestorType={x:Type UserControl}, Mode=FindAncestor}" Path="DataContext.SelectedDevice.SafetyMargin"/>
<Binding RelativeSource="{RelativeSource AncestorType={x:Type UserControl}, Mode=FindAncestor}" Path="DataContext.SelectedDevice.PressureUnit"/>
</MultiBinding>
</TextBox>
<TextBlock Grid.Row="4" Grid.Column="2" Margin="5">
<TextBlock.Text>
<MultiBinding Converter="{conv:UnitLabelConverter}">
<Binding RelativeSource="{RelativeSource AncestorType={x:Type UserControl}, Mode=FindAncestor}" Path="DataContext.SelectedDevice.SafetyMargin"/>
<Binding RelativeSource="{RelativeSource AncestorType={x:Type UserControl}, Mode=FindAncestor}" Path="DataContext.SelectedDevice.PressureUnit"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
To simplify the addition of new Settings (this is the reason of the reading from xml and so on), I created a new user Control. Its XAML is:
<UserControl ...
x:Name="parent">
<Grid DataContext="{Binding ElementName=parent}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="{Binding Path=Setting.DisplayName}" Margin="5"/>
<TextBox Grid.Column="1" Margin="5">
<TextBox.IsEnabled>
<MultiBinding Converter="{conv:EnableControlConverter}">
<Binding Path="Setting"/>
<Binding Path="Enabled"/>
</MultiBinding>
</TextBox.IsEnabled>
<MultiBinding Mode="TwoWay" Converter="{conv:ConversionConverter}">
<Binding Path="Setting"/>
<Binding Path="ConditionSetting"/>
</MultiBinding>
</TextBox>
<TextBlock Grid.Column="2" Margin="5">
<TextBlock.Text>
<MultiBinding Converter="{conv:UnitLabelConverter}">
<Binding Path="Setting"/>
<Binding Path="ConditionSetting"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Grid>
And the code behind:
public partial class CustomTextBox : UserControl
{
protected static Logger logger = LogManager.GetCurrentClassLogger();
public static readonly DependencyProperty SettingProperty =
DependencyProperty.Register("Setting", typeof(object),
typeof(CustomTextBox), new PropertyMetadata(""));
public static readonly DependencyProperty ConditionSettingProperty =
DependencyProperty.Register("ConditionSetting", typeof(object),
typeof(CustomTextBox), new PropertyMetadata(""));
public static readonly DependencyProperty EnabledProperty =
DependencyProperty.Register("Enabled", typeof(object),
typeof(CustomTextBox), new PropertyMetadata(""));
public object Setting
{
get { return (object)GetValue(SettingProperty); }
set { SetValue(SettingProperty, value); }
}
public object ConditionSetting
{
get { return (object)GetValue(ConditionSettingProperty); }
set { SetValue(ConditionSettingProperty, value); }
}
public object Enabled
{
get { return (object)GetValue(EnabledProperty); }
set { SetValue(EnabledProperty, value); }
}
public CustomTextBox()
{
InitializeComponent();
}
}
And this is the new code in the view:
<controls:CustomTextBox Grid.Row="3"
Setting="{Binding SelectedDevice.SafetyMargin, Mode=TwoWay}"
ConditionSetting="{Binding SelectedDevice.PressureUnit, Mode=TwoWay}"
Enabled="{Binding EnableControls}"/>
Any advice will be highly appreciated, thanks in advance.
Did you debug ?
Is the callback OnSettingValueUpdated in your Device-Class called
in both cases and are there any differences ?
Are the setters of ConditionSetting/Setting in your UserControl
called ?
I would say that somehow the PropertyChanged is not executed properly or doesn't reach the Property ...
The problem was using references as values for the DependencyProperties. All the structure is working and the DependencyProperty hears the NotifyPropertyChanged event for the Setting, but it checks the equality before updating the value and then it finds that the reference is the same, not updating it therefore.
Coercion doesn't work because it's done before the check for equality.
The solutions we tried were:
Creating a new Setting every time the Setting.Value changes. This works but we were afraid of possible problems of references to old Settings not being updated somewhere in the code that wasn't listening to the PropertyChanged events, for example.
Adding additional Bindings to the Converters multibinding in the xaml of the UserControl, especifically the Setting.Value. There is also a PropertyChangedEvent for this property so when it changes, the Converter reevaluates and the UserControl works as we expected. We didn't have to change the actual code besides the XAML so we finally went with this solution.
i need to create an instance of a class with two property that would be used as converter parameter of a binding.
the class is as below:
public class UnitQuantityBindClass:DependencyObject
{
public static readonly DependencyProperty QuantityProperty = DependencyProperty.Register(
"Quantity", typeof(EQuantities), typeof(UnitQuantityBindClass));
public EQuantities Quantity
{
get
{
return (EQuantities) GetValue(QuantityProperty);
}
set { SetValue(QuantityProperty, value); }
}
public static readonly DependencyProperty UnitProperty = DependencyProperty.Register(
"Unit", typeof(Enum), typeof(UnitQuantityBindClass));
public Enum Unit
{
get { return (Enum)GetValue(UnitProperty); }
set { SetValue(UnitProperty, value); }
}
}
the xaml code is as below also:
<textboxunitconvertor:TextBoxUnitConvertor Name="gasDensityValueControl" InstantaneousConvert="True" Margin="96,163,0,0" IsEnabled="{Binding ElementName=chkGas,Path=IsChecked}" QuantityBind="{Binding _FluidBlackOilClass.SGGas_SC.Quantity , RelativeSource={RelativeSource AncestorType=Window}, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" Width="206" Height="28" HorizontalAlignment="Left" VerticalAlignment="Top">
<textboxunitconvertor:TextBoxUnitConvertor.TextBoxText>
<Binding Path="_FluidBlackOilClass.SGGas_SC.Value" RelativeSource="{RelativeSource AncestorType=Window}" UpdateSourceTrigger="PropertyChanged" Mode="TwoWay" Converter="{StaticResource ValueStorageForUnitConverter}">
<Binding.ConverterParameter>
<classes:UnitQuantityBindClass Quantity="{Binding ElementName=gasDensityValueControl,Converter={StaticResource DummyConverter} ,Path=_Quantity,UpdateSourceTrigger=PropertyChanged, Mode=TwoWay, PresentationTraceSources.TraceLevel=High}" Unit="{Binding ElementName=gasDensityValueControl,Path=_CurrentUnitEnum,UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"></classes:UnitQuantityBindClass>
</Binding.ConverterParameter>
</Binding>
</textboxunitconvertor:TextBoxUnitConvertor.TextBoxText>
<textboxunitconvertor:TextBoxUnitConvertor.CurrentUnitEnumBind>
<Binding Source="{StaticResource CurrentFlowProWorkingClass}" Path="currentFlowProWorkingClass.ProjectUnitSystem" UpdateSourceTrigger="PropertyChanged" Mode="OneTime">
<Binding.ConverterParameter>
<classes:UnitQuantityBindClass Quantity="{Binding ElementName=gasDensityValueControl,Path=_Quantity,UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" Unit="{Binding ElementName=gasDensityValueControl,Path=_CurrentUnitEnum,UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"></classes:UnitQuantityBindClass>
</Binding.ConverterParameter>
</Binding>
</textboxunitconvertor:TextBoxUnitConvertor.CurrentUnitEnumBind>
</textboxunitconvertor:TextBoxUnitConvertor>
but the create class is empty and the Unit and quantity properties does not bind.
why?
Here's the XAML code representing a TextBox used as input for the IdCard
<TextBox.Text>
<Binding Mode="TwoWay"
Path="IdCardNumber"
UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<v:AlphaNumValidationRule ValidationStep="UpdatedValue" />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
The validation :
public class AlphaNumValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
if (string.IsNullOrWhiteSpace((value ?? "").ToString()))
return new ValidationResult(false, Resources.Strings.MessagesResource.RequiredField);
else if (value.ToString().MatchRegex(RegexResource.ALPHANUMERIC))
return new ValidationResult(true, null);
else
return new ValidationResult(false, Resources.Strings.MessagesResource.InvalidFormat);
}
}
The ViewModel
public override bool IsValid
{
get { return !string.IsNullOrWhiteSpace(IdCardNumber); }
}
private string idCardNumber;
public string IdCardNumber
{
get { return idCardNumber; }
set { Set(() => IdCardNumber, ref idCardNumber, value);
RaisePropertyChanged("IsValid");
}
}
What I want to have is to update IsValid everytime the IdCard input is updated , I tried different ValidationStep but none do as I wish.
At first when loading the input for the first time IsValid is false , when typing a correct value it becomes true after deleting input and adding wrong non-supported values IsValid stays the same since it keeps the last correct value.
Any way to solve this ?
There is an attached event Validation.Error that is fired when binding error occurs.
So basically you could attach to this event and set value of Validation.HasErrors property to your viewmodel's IsValid property.
I see a conflict however. You defined your validation logic in the View, but you want to access it in your ViewModel, that's why you are having troubles.
I recommend you to move entire validation logic to your viewmodel by implementing INotifyDataErrorInfo. then you will have all validation rules and validation errors at your disposal in viewmodel.
You can try to change the UpdateSourceTrigger property with LostFocus:
<Binding Mode="TwoWay"
Path="IdCardNumber"
UpdateSourceTrigger="LostFocus">
<Binding.ValidationRules>
<v:AlphaNumValidationRule ValidationStep="UpdatedValue" />
</Binding.ValidationRules>
</Binding>
Edit :
To bind the validation result you can use HasError property :
<TextBox Name="TextBox">
<TextBox.Text>
<Binding Mode="TwoWay"
Path="Text"
UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:AlphaNumValidationRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<TextBlock Text="{Binding (Validation.HasError), ElementName=TextBox}"/>
I have a simple example application, 2 text boxes a validation rule and a button.
I want my button to trigger validation and if it's not valid, not to continue with its execution.
<TextBox>
<TextBox.Text>
<Binding Path="FirstName">
<Binding.ValidationRules>
<local:MyValidationRule ErrorMessage="Enter first name" />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<TextBox>
<TextBox.Text>
<Binding Path="LastName">
<Binding.ValidationRules>
<local:MyValidationRule ErrorMessage="Enter last name" />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Button Command="{Binding OkCommand}" Content="Ok" />
The validation rule:
public class MyValidationRule : ValidationRule
{
public string ErrorMessage { get; set; }
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
string valueToCheck = value as string;
if (string.IsNullOrWhiteSpace(valueToCheck))
{
return new ValidationResult(false, ErrorMessage);
}
else
{
return new ValidationResult(true, null);
}
}
}
My OkCommand is empty method:
OkCommand= new RelayCommand(OkRequested);
private void OkRequested()
{
// Do stuff
}
However, no matter if the text boxes are empty OkCommand gets executed properly. What am I doing wrong here? I want text boxes to be styled if they're not valid (I've excluded styles from example), but that does not happen.
You should add into OkCommand something like this. And same for other textBoxes.
if ( Validation.GetHasError( textBox1 ) )
{
MessageBox.Show( Validation.GetErrors( textBox1 )[0].ErrorContent.ToString() );
return;
}