so I have written the following DP and ValidationRule:
public class ComparisonValue : DependencyObject
{
public Object ComparisonObject
{
get { return (Object)GetValue(ComparisonObjectProp); }
set {
SetValue(ComparisonObjectProp, value);
}
}
public static readonly DependencyProperty ComparisonObjectProp =
DependencyProperty.Register("ComparisonObject", typeof(object), typeof(ComparisonValue), new UIPropertyMetadata(null));
}
public class ObjectComparisonValidator : ValidationRule
{
private ComparisonValue _ObjectToCompare;
public ComparisonValue ObjectToCompare
{
get
{
return _ObjectToCompare;
}
set
{
_ObjectToCompare = value;
}
}
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
if (value != null)
{
if (!value.Equals(ObjectToCompare.ComparisonObject))
{
return new ValidationResult(false, "Values are not equal");
}
else
{
return new ValidationResult(true, null);
}
}
else
{
if (value != ObjectToCompare.ComparisonObject)
{
return new ValidationResult(false, "Values are not equal");
}
else
{
return new ValidationResult(true, null);
}
}
}
}
Then in my XAML I have the following markup:
<UserControl.Resources>
<l:EnumToStringConverter x:Key="CustomEnumConverter"/>
<l:BooleanToBrushConverter x:Key="BooleanToBrushConverter"/>
<l:ObjectComparisonValidator x:Key="ObjectComparisonValidator"/>
<l:ComparisonValue x:Key="ComparisonValue"/>
</UserControl.Resources>
....
<TextBox Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2" Height="25" Text="{Binding Path=NetworkKey.Value, UpdateSourceTrigger=PropertyChanged}">
<TextBox.Background>
<Binding Path="NetworkKey.Changed" Converter="{StaticResource BooleanToBrushConverter}">
<Binding.ConverterParameter>
<x:Array Type="Brush">
<SolidColorBrush Color="Yellow"/>
<SolidColorBrush Color="White"/>
</x:Array>
</Binding.ConverterParameter>
</Binding>
</TextBox.Background>
</TextBox>
....
<TextBox Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2" Height="25">
<TextBox.Text>
<Binding Path="DuplicateNetworkKey.Value" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<l:ObjectComparisonValidator>
<l:ObjectComparisonValidator.ObjectToCompare>
<l:ComparisonValue ComparisonObject="{Binding Path=NetworkKey.Value}"/>
</l:ObjectComparisonValidator.ObjectToCompare>
</l:ObjectComparisonValidator>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
<TextBox.Background>
<Binding Path="DuplicateNetworkKey.Changed" Converter="{StaticResource BooleanToBrushConverter}">
<Binding.ConverterParameter>
<x:Array Type="Brush">
<SolidColorBrush Color="Yellow"/>
<SolidColorBrush Color="White"/>
</x:Array>
</Binding.ConverterParameter>
</Binding>
</TextBox.Background>
</TextBox>
Now the problem I am having is that the Validate method fo the Validation rule gets invoked, but when the binding for NetworkKey gets triggered, the Setter in the ComparisonValue for the object never gets invoked, so any time the validation rule runs, the ComparisonObject property of ObjectComparisonValidator.ObjectToCompare is null, and thus validation fails. Whats wrong with the binding I have for ComparisonObject?
just for a bit of clarification, the type of NetworkKey and DuplicateKey (props in the VM) are INPC classes. Heres the code for them as well:
public class ValueField<T> : AChangeReportingViewModel, INotifyPropertyChanged
{
private T _OriginalVal;
public T OriginalVal
{
get
{
return _OriginalVal;
}
set
{
_OriginalVal = value;
Value = value;
Changed = false;
PropertyChanged(this, new PropertyChangedEventArgs("OriginalVal"));
}
}
private T _Value;
public T Value
{
get
{
return _Value;
}
set
{
_Value = value;
if (_Value == null)
{
if (_OriginalVal != null) Changed = true;
}
else
{
Changed = !_Value.Equals(_OriginalVal);
}
PropertyChanged(this, new PropertyChangedEventArgs("Value"));
}
}
private Boolean _Changed;
public Boolean Changed
{
get
{
return _Changed;
}
set
{
if (_Changed != value)
{
if (value) ChangeMade();
else ChangeReversed();
}
_Changed = value;
PropertyChanged(this, new PropertyChangedEventArgs("Changed"));
}
}
public event PropertyChangedEventHandler PropertyChanged = delegate { };
}
<Binding Path="DuplicateNetworkKey.Value" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<l:ObjectComparisonValidator>
<l:ObjectComparisonValidator.ObjectToCompare>
<l:ComparisonValue ComparisonObject="{Binding Path=NetworkKey.Value}"/>
</l:ObjectComparisonValidator.ObjectToCompare>
</l:ObjectComparisonValidator>
</Binding.ValidationRules>
</Binding>
The inner binding object is not part of the visual tree, and so does not inherit the data context of the parent. To bind outside of the visual tree, use an x:Reference binding:
<l:ComparisonValue ComparisonObject="{Binding Source={x:Reference Root}
Path=DataContext.NetworkKey.Value}"/>
It's similar to an ElementName binding, but you can't do those outside of the visual tree. Note that "Root" in this example is the name of the root UI element.
Related
I want to validate a textbox if something is wrong. The idea is if something is wrong than the next TextBox should have a warning image.
<TextBox Text="{Binding Age, UpdateSourceTrigger=PropertyChanged}">
<Validation.ErrorTemplate>
<ControlTemplate>
<StackPanel>
<!-- Placeholder for the TextBox itself -->
<AdornedElementPlaceholder x:Name="textBox"/>
<image source="some-Image.png" width="20" Height="20" />
</StackPanel>
</ControlTemplate>
</Validation.ErrorTemplate>
</TextBox>
But the thing is the image is not showing, it only shows the border of the icon.
Am I using AdornedElementPlaceholder correctly?
Tested solution that works and displays the image when error occurs:
<TextBox BorderThickness="0.8">
<Validation.ErrorTemplate>
<ControlTemplate>
<StackPanel>
<AdornedElementPlaceholder/>
<Image Source="Image.jpg" Width="20" Height="20"/>
</StackPanel>
</ControlTemplate>
</Validation.ErrorTemplate>
<TextBox.Text>
<Binding Path="ValidationTest" UpdateSourceTrigger="PropertyChanged" Mode="TwoWay" ValidatesOnDataErrors="True">
<Binding.ValidationRules>
<validation:IntegerValidationRule ValidationStep="CommittedValue" Min="1" Max="99999999"/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
And here is the validation rule that I have used for this example:
public class IntegerValidationRule : ValidationRule
{
private int _min = int.MinValue;
private int _max = int.MaxValue;
private string _fieldName = "Field";
private string _customMessage = String.Empty;
public int Min
{
get { return _min; }
set { _min = value; }
}
public int Max
{
get { return _max; }
set { _max = value; }
}
public string FieldName
{
get { return _fieldName; }
set { _fieldName = value; }
}
public string CustomMessage
{
get { return _customMessage; }
set { _customMessage = value; }
}
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
int num = 0;
var val = (value as BindingExpression).DataItem;
if (!int.TryParse(value.ToString(), out num))
return new ValidationResult(false, $"{FieldName} must contain an integer value.");
if (num < Min || num > Max)
{
if (!String.IsNullOrEmpty(CustomMessage))
return new ValidationResult(false, CustomMessage);
return new ValidationResult(false, $"{FieldName} must be between {Min} and {Max}.");
}
return new ValidationResult(true, null);
}
}
Which is just modified example from MSDN Docs
Some notes:
You didn't provide the validation rule so I am assuming it works as expected and produces valid validation result.
The Binding of your TextBox.Text property doesn't include the Validation rule.
In trying to solve an issue Im having in another project - Ive created the following example to replicate the issue.
The idea is that when the user enters new values, via the slider or textbox, those values should then be "ConvertedBack" via the convertor, and the source updated. I don't seem to be seeing this though, I believe due to the fact that InternalRep's properties are being written to, but not informing the bindexpression for the InternalRepProperty.
What is the best way to go about solving this problem?
One method I tried was to handle the sliders ValueChanged event, but this caused the convertor to ... ConvertBack then Convert then ConvertBack then Convert, not sure why.
When the user changes a value, I need the convertor to only ConvertBack to update the source, and nothing else, .. is this possible?
TextSplitter XAML
<ContentControl x:Class="WpfApplication23.TextSplitter"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:local="clr-namespace:WpfApplication23"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<UniformGrid Columns="3" Rows="2">
<TextBox Text="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:TextSplitter}},
Path=InternalRep.First, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" />
<TextBox Text="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:TextSplitter}},
Path=InternalRep.Second, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" />
<TextBox Text="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:TextSplitter}},
Path=InternalRep.Third, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" />
<Slider Value="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:TextSplitter}},
Path=InternalRep.First, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" Maximum="255" />
<Slider Value="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:TextSplitter}},
Path=InternalRep.Second, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" Maximum="255" />
<Slider Value="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:TextSplitter}},
Path=InternalRep.Third, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" Maximum="255" ValueChanged="OnSliderChnaged" />
</UniformGrid>
</ContentControl>
TextSplitter C#
public class InternalRep
{
public int First { get; set; }
public int Second { get; set; }
public int Third { get; set; }
};
public class LettersToInternalRepMultiConvertor : IMultiValueConverter
{
public object Convert(object[] values, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
InternalRep ir = new InternalRep()
{
First = (int)(char)values[0],
Second = (int)(char)values[1],
Third = (int)(char)values[2],
};
return ir;
}
public object[] ConvertBack(object value, Type[] targetTypes,
object parameter, System.Globalization.CultureInfo culture)
{
InternalRep ir = (InternalRep)value;
if (ir != null)
{
return new object[]
{
(char)ir.First,
(char)ir.Second,
(char)ir.Third
};
}
else
{
throw new Exception();
}
}
}
public partial class TextSplitter : ContentControl
{
public static readonly DependencyProperty FirstProperty = DependencyProperty.Register(
"First", typeof(char), typeof(TextSplitter));
public static readonly DependencyProperty SecondProperty = DependencyProperty.Register(
"Second", typeof(char), typeof(TextSplitter));
public static readonly DependencyProperty ThirdProperty = DependencyProperty.Register(
"Third", typeof(char), typeof(TextSplitter));
public static readonly DependencyProperty InternalRepProperty = DependencyProperty.Register(
"InternalRep", typeof(InternalRep), typeof(TextSplitter));
BindingExpressionBase beb = null;
public TextSplitter()
{
InitializeComponent();
MultiBinding mb = new MultiBinding();
mb.Mode = BindingMode.TwoWay;
mb.Bindings.Add(SetBinding("First"));
mb.Bindings.Add(SetBinding("Second"));
mb.Bindings.Add(SetBinding("Third"));
mb.Converter = new LettersToInternalRepMultiConvertor();
beb = SetBinding(InternalRepProperty, mb);
}
Binding SetBinding(string _property)
{
Binding b = new Binding(_property);
b.Mode = BindingMode.TwoWay;
b.Source = this;
return b;
}
public char First
{
get { return (char)GetValue(FirstProperty); }
set { SetValue(FirstProperty, value); }
}
public char Second
{
get { return (char)GetValue(SecondProperty); }
set { SetValue(SecondProperty, value); }
}
public char Third
{
get { return (char)GetValue(ThirdProperty); }
set { SetValue(ThirdProperty, value); }
}
}
MainWindow XAML
<Window x:Class="WpfApplication23.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication23"
Title="MainWindow" Height="640" Width="800" WindowStartupLocation="CenterScreen">
<StackPanel>
<local:TextSplitter First="{Binding A, Mode=TwoWay}"
Second="{Binding B, Mode=TwoWay}"
Third="{Binding C, Mode=TwoWay}"/>
</StackPanel>
</Window>
MainWindow Code
namespace WpfApplication23
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
char m_a = 'a';
public char A
{
get { return m_a; }
set { m_a = value; }
}
char m_B = 'b';
public char B
{
get { return m_B; }
set{ m_B = value; }
}
char m_c = 'c';
public char C
{
get { return m_c; }
set { m_c = value; }
}
}
}
You need to implement INotifyPropertyChanged in your ViewModel.
Here, the ViewModel is the Window , so you have to do:
public partial class MainWindow : Window, INotifyPropertyChanged
{
public MainWindow()
{
this.DataContext = this;
InitializeComponent();
}
char m_a = 'a';
public char A
{
get { return m_a; }
set
{
if (value != m_a)
{
m_c = value;
RaisePropertyChanged("A");
}
}
}
char m_B = 'b';
public char B
{
get { return m_B; }
set
{
if (value != m_B)
{
m_c = value;
RaisePropertyChanged("B");
}
}
}
char m_c = 'c';
public char C
{
get { return m_c; }
set
{
if (value != m_c)
{
m_c = value;
RaisePropertyChanged("C");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string _Prop)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(_Prop));
}
}
DelegateCommand _RecomputeCommand;
public DelegateCommand RecomputeCommand
{
get { return _RecomputeCommand ?? (_RecomputeCommand = new DelegateCommand(Recompute)); }
}
public void Recompute()
{
//Do something with A, B and C.
}
}
EDIT: you should simply bind the 3 sliders to A, B, C (you'll need the above implementation) and then act upon a RecomputeCommand like so:
<StackPanel>
<Slider Value="{Binding A}" Maximum="255">
<i:Interaction.Triggers>
<i:EventTrigger EventName="ValueChanged">
<i:InvokeCommandAction Command="{Binding RecomputeCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Slider>
<Slider Value="{Binding B}" Maximum="255">
<i:Interaction.Triggers>
<i:EventTrigger EventName="ValueChanged">
<i:InvokeCommandAction Command="{Binding RecomputeCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Slider>
<Slider Value="{Binding C}" Maximum="255">
<i:Interaction.Triggers>
<i:EventTrigger EventName="ValueChanged">
<i:InvokeCommandAction Command="{Binding RecomputeCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Slider>
</StackPanel>
Of course this can be in a ContentControl as needed.
DataGrid ItemsSource="{Binding Legs}" Grid.Row="1"
DataContext="{Binding}"
Tag="{Binding}"
CanUserAddRows="False"
CanUserDeleteRows="False"
HorizontalAlignment="Stretch"
IsSynchronizedWithCurrentItem="True"
x:Name="statusGrid"
AutoGenerateColumns="False" HorizontalScrollBarVisibility="Hidden"
RowStyle="{StaticResource GridRowStyle}"
>
And then
<DataGrid.Columns>
<DataGridTemplateColumn Width="Auto" Header="Status">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Image Height="16" MaxHeight="14" Width="14" MaxWidth="14" HorizontalAlignment="Center">
<Image.Source>
<MultiBinding>
<Binding Path="SwapswireBuyerStatusText"/>
<Binding Path="SwapswireSellerStatusText"/>
<MultiBinding.Converter>
<Control:SwapswireStatusImagePathConvertor AllGoodPath="/Resources/Done.png"
InProgressPath="/Resources/Go.png" WarningPath="/Resources/Warning.png">
**<Control:SwapswireStatusImagePathConvertor.DealProp>
<Control:DealObject Deal="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGrid}}, Path=Tag}"/>
</Control:SwapswireStatusImagePathConvertor.DealProp>**
</Control:SwapswireStatusImagePathConvertor>
</MultiBinding.Converter>
</MultiBinding>
</Image.Source>
</Image>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
Following is my Depenency property on Convertor
public class DealObject : DependencyObject
{
public static DependencyProperty TradedDecimalsProperty =
DependencyProperty.Register("TradedDecimals", typeof(Int32), typeof(DealObject), new UIPropertyMetadata(0));
public static DependencyProperty DealProperty =
DependencyProperty.Register("Deal", typeof(CMBSTrade), typeof(DealObject), new UIPropertyMetadata(new CMBSTrade()));
public CMBSTrade Deal
{
get { return (CMBSTrade)GetValue(DealProperty); }
set { SetValue(DealProperty, value); }
}
public Int32 TradedDecimals
{
get { return (Int32)GetValue(TradedDecimalsProperty); }
set { SetValue(TradedDecimalsProperty, value); }
}
}
[ValueConversion(typeof(object), typeof(BitmapImage))]
public class SwapswireStatusImagePathConvertor : IMultiValueConverter
{
public String AllGoodPath { get; set; }
public String InProgressPath { get; set; }
public String WarningPath { get; set; }
private DealObject _DealProp = null;
public DealObject DealProp
{
get { return _DealProp; }
set { _DealProp = value; }
}
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
String path = WarningPath;
try
{
if (null != DealProp && null != DealProp.Deal && null != values && values.Length == 2)
{
String str1 = System.Convert.ToString(values[0]);
String str2 = System.Convert.ToString(values[1]);
if (DealProp.Deal.Swapswire)
{
switch (MBSConfirmationHelper.GetSwapswireStatus(str1, str2, MBSConfirmationHelper.GetParticipantType(DealProp.Deal)))
{
case DealExecutionStatus.InProgress:
path = InProgressPath; break;
case DealExecutionStatus.ActionRequired:
case DealExecutionStatus.Error:
path = WarningPath;
break;
case DealExecutionStatus.Executed:
path = AllGoodPath;
break;
case DealExecutionStatus.NotApplicable:
path = String.Empty;
break;
}
}
else path = String.Empty;
}
}
catch (Exception)
{
}
return String.IsNullOrEmpty(path)? null : new BitmapImage(new Uri(path, UriKind.Relative));
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException("Not Implemented");
}
}
in the above XAML code... I am trying to access datagrid's Tag property, the binding is working for the object is always comming null. How can I achieve this?
The problem is evident from your code...
<Control:SwapswireStatusImagePathConvertor.DealProp>
<Control:DealObject
Deal="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type DataGrid}},
Path=Tag}"/>
</Control:SwapswireStatusImagePathConvertor.DealProp>**
Your converter or any property within the converter can never be part of the visual tree and hence the binding wont work on it even if your deal object or the converter itself is a DependencyObject!
Please revise your binding ...
Why does your converter need to have a property that needs binding? You already have MultiBinding, so involve this binding as part of that ....
<MultiBinding>
<Binding Path="SwapswireBuyerStatusText"/>
<Binding Path="SwapswireSellerStatusText"/>
**<Binding RelativeSource="{RelativeSource FindAncestor,
AncestorType={x:Type DataGrid}}"
Path="Tag"/>**
<MultiBinding.Converter>
<Control:SwapswireStatusImagePathConvertor
AllGoodPath="/Resources/Done.png"
InProgressPath="/Resources/Go.png"
WarningPath="/Resources/Warning.png" />
</MultiBinding.Converter>
</MultiBinding>
Now your converter receives all the 3 values that it needs ...
String str1 = System.Convert.ToString(values[0]);
String str2 = System.Convert.ToString(values[1]);
** this.DealProp = new DealObject();
this.DealProp.Deal = values[2] as CMBSTrade; **
if (DealProp.Deal.Swapswire)
{
....
}
Let me know if this helps...
I am creating an application where I want to use validation rules, but on some screens there is not enough space to display the resulting error alongside the field which is in error, so I want to put it in a status bar at the bottom of the field.
This sample is from several bits I have pieced together from the web which gives a form which validates using different rules and displays the errors in different ways, but I cant see how to get the error message into the StatusBarItem using XAML. I feel sure there is a simple way to do it. Can anyone help me please?
The sample was written in VS2010 using Framework 4.0.
MainWindow.xaml
<Window x:Class="SampleValidation.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="clr-namespace:SWL.Libraries.SysText"
Title="Sample ValidationRule WPF" Height="350" Width="525"
Loaded="Window_Loaded" WindowStartupLocation="CenterScreen">
<Window.Resources>
<ControlTemplate x:Key="validationTemplate">
<!--
<TextBlock Foreground="Red" FontSize="20">***</TextBlock>
-->
<DockPanel LastChildFill="True">
<TextBlock DockPanel.Dock="Right" Foreground="Red" Margin="5" FontSize="8pt" Text="***" />
<AdornedElementPlaceholder />
</DockPanel>
</ControlTemplate>
<Style x:Key="textBoxInError1" TargetType="{x:Type TextBox}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={x:Static RelativeSource.Self},
Path=(Validation.Errors)[0].ErrorContent}" />
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="textBoxInError2" TargetType="{x:Type TextBox}">
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<DockPanel LastChildFill="True">
<TextBlock DockPanel.Dock="Right" Foreground="Red" Margin="5" FontSize="8pt"
Text="{Binding ElementName=MyAdorner, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}" />
<Border BorderBrush="Red" BorderThickness="2">
<AdornedElementPlaceholder Name="MyAdorner" />
</Border>
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.Errors)[0].ErrorContent}" />
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<TextBlock Height="24" HorizontalAlignment="Left" Margin="36,73,0,0" Name="textBlock1"
Text="Node Address:" VerticalAlignment="Top" Width="87" />
<TextBlock Height="24" HorizontalAlignment="Left" Margin="36,112,0,0" Name="textBlock2"
Text="Node Name:" VerticalAlignment="Top" Width="78" />
<TextBox Height="24" HorizontalAlignment="Left" Margin="129,70,0,0" Name="textBox1"
VerticalAlignment="Top" Width="119" TextChanged="textBox1_TextChanged"
TabIndex="0" Style="{StaticResource textBoxInError2}"
Validation.ErrorTemplate="{StaticResource validationTemplate}">
<Binding Path="NodeAddress" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<c:NumberRangeRule Min="1" Max="100" />
</Binding.ValidationRules>
</Binding>
</TextBox>
<TextBox Height="24" HorizontalAlignment="Left" Margin="129,109,0,0" Name="textBox2"
VerticalAlignment="Top" Width="119" TextChanged="textBox2_TextChanged"
TabIndex="1" Style="{StaticResource textBoxInError2}">
<Binding Path="NodeName" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<c:NameFormatRule MinLength="6" MaxLength="9" />
</Binding.ValidationRules>
</Binding>
</TextBox>
<StatusBar Height="23" HorizontalAlignment="Stretch" Margin="0,0,0,0"
Name="myStatusBar" VerticalAlignment="Bottom">
<StatusBarItem x:Name="errorStatusBarItem" Content="No errors" />
</StatusBar>
<Button Content="Close" Height="29" HorizontalAlignment="Left" Margin="108,227,0,0"
Name="btnCLOSE" VerticalAlignment="Top" Width="85" Click="btnCLOSE_Click" TabIndex="3" />
<Button Content="Apply" Height="29" HorizontalAlignment="Left" Margin="297,227,0,0"
Name="btnAPPLY" VerticalAlignment="Top" Width="85" Click="btnAPPLY_Click" TabIndex="2" />
</Grid>
</Window>
MainWindow.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace SampleValidation {
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window {
public int NodeAddress { get; set; }
public string NodeName { get; set; }
public bool IsAllLoaded { get; set; }
public MainWindow() {
NodeAddress = 1;
NodeName = "freddy";
IsAllLoaded = false;
InitializeComponent();
btnAPPLY.Visibility = System.Windows.Visibility.Hidden;
DataContext = this;
}
private void btnAPPLY_Click(object sender, RoutedEventArgs e) {
// if there are no format errors reported by the validation rules
Validator.ErrorText = "";
if (Validator.IsValid(this))
// Save the data
btnAPPLY.Visibility = System.Windows.Visibility.Hidden; // hide the button indicating nothing new to save
else
MessageBox.Show("Cant Save Changes - Error in form\r\n" + Validator.ErrorText, "Save not allowed", MessageBoxButton.OK, MessageBoxImage.Error);
}
private void btnCLOSE_Click(object sender, RoutedEventArgs e) {
if (btnAPPLY.Visibility != System.Windows.Visibility.Hidden) {
MessageBoxResult myAnswer = MessageBox.Show("Save Changes?", "Confirmation", MessageBoxButton.YesNoCancel);
if (myAnswer == MessageBoxResult.Cancel)
return;
if (myAnswer == MessageBoxResult.Yes)
btnAPPLY_Click(sender, e);
}
this.Close();
}
private void Window_Loaded(object sender, RoutedEventArgs e) {
IsAllLoaded = true;
}
private void ShowModified() {
if (IsAllLoaded)
btnAPPLY.Visibility = System.Windows.Visibility.Visible;
} // ShowModified
private void textBox2_TextChanged(object sender, TextChangedEventArgs e) {
ShowModified();
}
private void textBox1_TextChanged(object sender, TextChangedEventArgs e) {
ShowModified();
}
} // class MainWindow
public static class Validator {
public static string ErrorText { get; set; }
static Validator() {
ErrorText = "";
}
public static bool IsValid(DependencyObject parent) {
// Validate all the bindings on the parent
bool valid = true;
LocalValueEnumerator localValues = parent.GetLocalValueEnumerator();
while (localValues.MoveNext()) {
LocalValueEntry entry = localValues.Current;
if (BindingOperations.IsDataBound(parent, entry.Property)) {
Binding binding = BindingOperations.GetBinding(parent, entry.Property);
if (binding.ValidationRules.Count > 0) {
BindingExpression expression = BindingOperations.GetBindingExpression(parent, entry.Property);
expression.UpdateSource();
if (expression.HasError) {
ErrorText = expression.ValidationError.ErrorContent.ToString();
valid = false;
}
}
}
}
// Validate all the bindings on the children
System.Collections.IEnumerable children = LogicalTreeHelper.GetChildren(parent);
foreach (object obj in children) {
if (obj is DependencyObject) {
DependencyObject child = (DependencyObject)obj;
if (!IsValid(child)) {
valid = false;
}
}
}
return valid;
}
} // class Validator
ValidationRules.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Globalization;
using System.Windows.Controls;
namespace SWL.Libraries.SysText {
public class NumberRangeRule : ValidationRule {
private int _min;
private int _max;
public NumberRangeRule() { }
public int Min {
get { return _min; }
set { _min = value; }
}
public int Max {
get { return _max; }
set { _max = value; }
}
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
int val = 0;
try {
if (((string)value).Length > 0)
val = Int32.Parse((String)value);
} catch (Exception e) {
return new ValidationResult(false, "Illegal Characters or " + e.Message);
}
if ((val < Min) || (val > Max)) {
return new ValidationResult(false, "Please Enter Number in Range: " + Min + " - " + Max + ".");
} else {
return new ValidationResult(true, null);
}
}
}
public class NameFormatRule : ValidationRule {
private int _minLength;
private int _maxLength;
public NameFormatRule() { }
public int MinLength {
get { return _minLength; }
set { _minLength = value; }
}
public int MaxLength
get { return _maxLength; }
set { _maxLength = value; }
}
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
try {
if (((string)value).Length > 0) {
if (((string)value).Length < MinLength || ((string)value).Length > MaxLength)
return new ValidationResult(false, String.Format ("Enter a string of {0} to {1} characters in length", MinLength, MaxLength));
return new ValidationResult(true, null);
}
return new ValidationResult(true, null);
} catch (Exception e) {
return new ValidationResult(false, "Illegal Characters or " + e.Message);
}
}
}
}
UPDATE
I recently had to handle multiple error messages that were displayed when the mouse hovered an error icon. I made my viewmodel implement IDataErrorInfo. I then created a class-level Dictionary, keyed by a unique identifier for each control (I just used an enum), in my viewmodel to hold one error per control. Then I created a public property of type string and called it ErrorText. The getter of ErrorText iterates the errors dictionary and builds out all the errors into one string.
Here's an example, it may need tweaked. I know this is extremely simplified but should get you going in a direction. For complex validation, you can still validation using ValidationRule objects you create and then just check the IsValid property of the ValidationResult object returned.
// NOTE: The enum member name matches the name of the property bound to each textbox
public enum ControlIDs
{
TextBox1Value = 0,
TextBox2Value
}
public class MyViewModel : IDataErrorInfo
{
private readonly Dictionary<ControlIDs, string> errors;
public string ErrorText
{
get
{
if (errors.ContainsKey(ControlIDs.TextBox1) && errors.ContainsKey(ControlIDs.TextBox2))
{
return "Errors: " + errors[ControlsIDs.TextBox1] + ", " + errors[ControlsIDs.TextBox2];
}
if (errors.ContainsKey(ControlIDs.TextBox1))
{
return "Error: " + errors[ControlsIDs.TextBox1];
}
if (errors.ContainsKey(ControlIDs.TextBox2))
{
return "Error: " + errors[ControlsIDs.TextBox2];
}
}
}
public MyViewModel()
{
errors = new Dictionary<ControlIDs, string>();
}
private void UpdateErrorCollection(ControlIDs fieldKey, string error)
{
if (errors.ContainsKey(fieldKey))
{
errors[fieldKey] = error;
}
else
{
errors.Add(fieldKey, error);
}
OnPropertyChanged("ErrorText");
}
#region IDataErrorInfo
public string Error
{
get { throw new NotImplementedException(); }
}
public string this[string columnName]
{
string error = string.Empty;
if (columnName == ControlIDs.TextBox1Value.ToString())
{
if (string.IsNullOrWhiteSpace(TextBox1Value))
{
error = "TextBox1 must contain a value";
UpdateErrorCollection(ControlIDs.TextBox1Value, error);
}
else
{
errors.Remove(ControlIDs.TextBox1Value);
}
}
else if (columnName == ControlIDs.TextBox2Value))
{
if (string.IsNullOrWhiteSpace(TextBox2Value))
{
error = "TextBox2 must contain a value";
UpdateErrorCollection(ControlIDs.TextBox2Value, error);
}
else
{
errors.Remove(ControlIDs.TextBox2Value);
}
}
// returning null indicates success
return !string.IsNullOrWhiteSpace(error) ? error : null;
}
#endregion
}
Now just bind your StatusBarItem TextBlock to the ErrorText property.
I have information that was gathered from a service about TFS builds put into ViewModels.
Here are the models:
public class Collection : ViewModel
{
private string _name = string.Empty;
public string Name
{
get { return _name; }
set
{
_name = value;
OnPropertyChanged(() => Name);
}
}
private ObservableCollection<Project> _projects = new ObservableCollection<TFSProject>();
public ObservableCollection<Project> Projects
{
get { return _projects; }
set
{
_projects = value;
OnPropertyChanged(() => Projects);
}
}
}
public class Project : ViewModel
{
private string _name = string.Empty;
public string Name
{
get { return _name; }
set
{
_name = value;
OnPropertyChanged(() => Name);
}
}
private ObservableCollection<string> _buildDefinitions = new ObservableCollection<string>();
public ObservableCollection<string> BuildDefinitions
{
get { return _buildDefinitions; }
set
{
_buildDefinitions = value;
OnPropertyChanged(() => BuildDefinitions);
}
}
}
I am binding my combobox's itemssource to a ObservableCollection<Collection>. The problem is that the collection, project, and build definition names are stored in a class that defines them as separate string properties:
public class BuildMonitor : INotifyPropertyChanged
{
[Description("In TFS, each team project exists within a TFS Collection. This is the name of the collection applicable to the build that this monitor is for. Default='Vision2010'")]
public string Collection
{
get { return collection_; }
set
{
collection_ = value;
OnPropertyChanged(() => Collection);
}
}
private string collection_ = "Vision2010";
[Description("BuildDefintions reside within a TeamProject. This is the name of the TeamProject where the build definition this monitor is for resides. Default='Double-Take2010'")]
public string TeamProject { get { return teamProject_; } set { teamProject_ = value; OnPropertyChanged(() => TeamProject); } }
private string teamProject_ = "Double-Take2010";
[Description("Builds are defined in TFS as the execution of a particular BuildDefinition. This is the name of the build defintion (thus; the build) this monitor is for.")]
public string BuildDefinition { get { return buildDefinition_; } set { buildDefinition_ = value; OnPropertyChanged(() => BuildDefinition); } }
private string buildDefinition_;
[Description("Used only if this monitor should watch for builds specified by a particular user. Enter the domain name of the user, or leave blank to monitor builds by any user.")]
public string RequestedByFilter { get { return requestedByFilter_; } set { requestedByFilter_ = value; OnPropertyChanged(() => RequestedByFilter); } }
private string requestedByFilter_;
[Description("The command to execute when the build monitor is triggered.")]
public string Command { get { return command_; } set { command_ = value; OnPropertyChanged(() => Command); } }
private string command_;
[Description("The arguments to pass to the command. Arguments will resolve known build monitor macros.")]
public string Arguments { get { return arguments_; } set { arguments_ = value; OnPropertyChanged(() => Arguments); } }
private string arguments_;
[Description("If TRUE, the monitor will fire only once, at which point it will be marked as 'invalid' and never fire again.")]
public bool RunOnce { get { return runOnce_; } set { runOnce_ = value; OnPropertyChanged(() => RunOnce); } }
private bool runOnce_ = false;
[Description("The maximum age (in hours) a build can be (since finished), for the monitor to consider it for processing. Default='0'")]
public int MaxAgeInHours { get { return maxAgeInHours_; } set { maxAgeInHours_ = value; OnPropertyChanged(() => MaxAgeInHours); } }
private int maxAgeInHours_ = 0;
[Description("Which status trigger the monitor should 'fire' on. When the build status matches this trigger, the monitor command will be executed. Default='Succeeded'")]
public BuildStatus EventTrigger { get { return eventTrigger_; } set { eventTrigger_ = value; OnPropertyChanged(() => EventTrigger); } }
private BuildStatus eventTrigger_ = BuildStatus.Succeeded;
[Browsable(false), Description("Used internally to reliably compare two BuildMonitors against each other.")]
public Guid ID { get { return id_; } set { id_ = value; } }
private Guid id_ = Guid.NewGuid();
[Browsable(false), Description("Used internally to determine if the monitor is still valid/should be processed.")]
public bool IsEnabled { get { return isEnabled_; } set { isEnabled_ = value; } }
private bool isEnabled_ = true;
[Browsable(false), XmlIgnore, Description("Used internally to track when the monitor is 'busy' (currently running the 'Command' selected.")]
public int CurrentProcessID { get { return currentProcessID_; } set { currentProcessID_ = value; } }
private int currentProcessID_ = 0;
[Browsable(false), XmlIgnore, Description("Used internally to track the build that the monitor is currently processing.")]
private string currentBuildUri_;
public string CurrentBuildUri { get { return currentBuildUri_; } set { currentBuildUri_ = value; } }
[field: NonSerialized, Browsable(false)]
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged<T>(Expression<Func<T>> propertyExpression)
{
MemberExpression memberExpression = (MemberExpression)propertyExpression.Body;
string propertyName = memberExpression.Member.Name;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
In my xaml I have attempted to represent the selection of this data by setting the itemssource of the collection combobbox to a relative source binding which is the ObservableCollection<Collection>. I get the items in the list ok but since the itemssource is a List<BuildMonitors>, I can't seem to get the selected item to map over the name property of the selected item to the actual binding of the data item (string Collection in the BuildMonitor instance).
<tk:DataGrid ItemsSource="{Binding Monitors}"
AutoGenerateColumns="False">
<tk:DataGrid.Columns>
<tk:DataGridTemplateColumn Header="Collection">
<tk:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox x:Name="Collection"
ItemsSource="{Binding Path=AllCollections, RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type apollo:BuildMonitorNew}}}"
DisplayMemberPath="Name"
SelectedItem="{Binding .}"
SelectedValuePath="Collection"
SelectedValue="{Binding Name}"/>
</DataTemplate>
</tk:DataGridTemplateColumn.CellEditingTemplate>
<tk:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Collection,Mode=TwoWay}"/>
</DataTemplate>
</tk:DataGridTemplateColumn.CellTemplate>
</tk:DataGridTemplateColumn>
<tk:DataGridTemplateColumn Header="Project">
<tk:DataGridTemplateColumn.CellEditingTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding ElementName=Collection,Path=SelectedItem.Projects}">
<ComboBox x:Name="Projects"
ItemsSource="{Binding}"
DisplayMemberPath="Name"/>
</HierarchicalDataTemplate>
</tk:DataGridTemplateColumn.CellEditingTemplate>
</tk:DataGridTemplateColumn>
<tk:DataGridTextColumn Binding="{Binding Command}"
Header="Command"/>
<tk:DataGridTextColumn Binding="{Binding Arguments}"
Header="Arguments"
Width="*"/>
</tk:DataGrid.Columns>
My first thought is that although my viewmodel may be a better representation of the data (hierarchical), the structure of the data to select vs the data to actual store is too different.
I would love to be wrong here and find a snazzy way to convert the data that is actually selected(Collection,Project, and then BuildDefinition) to the path of the data that is stored (BuildMonitor).
Any ideas?
I found that the multivalue converter allowed me to transform the hierarchical structure into parts for the combobox item source where they are representing a list.
Converter:
public class BuildMonitorItemSource : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (values.Count() == 2)
{
string collectionToGet = parameter.ToString();
ObservableCollection<TFSCollection> allcollections = values[1] as ObservableCollection<TFSCollection>;
BuildLauncher.BuildMonitor currentBM = (values[0] as BuildLauncher.BuildMonitor);
if (collectionToGet.Equals("Projects"))
{
return allcollections.FirstOrDefault(x => x.Name.Equals(currentBM.Collection, StringComparison.OrdinalIgnoreCase)).Projects;
}
else if (collectionToGet.Equals("BuildDefinitions"))
{
TFSCollection currentCollection = allcollections.FirstOrDefault(x => x.Name.Equals(currentBM.Collection));
TFSProject currentProject = currentCollection.Projects.FirstOrDefault(x => x.Name.Equals(currentBM.TeamProject));
return currentProject.BuildDefinitions;
}
}
return values;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
This is the modified xml:
<tk:DataGrid ItemsSource="{Binding Monitors}"
AutoGenerateColumns="False"
IsEnabled="{Binding RelativeSource={RelativeSource AncestorType=apollo:BuildMonitorNew}, Path=TFSAuthenticated}"
Name="MontiorsGrid">
<tk:DataGrid.Columns>
<tk:DataGridCheckBoxColumn Binding="{Binding IsEnabled}">
<tk:DataGridCheckBoxColumn.Header>
<Ellipse Grid.Column="0"
HorizontalAlignment="Left"
Height="10" Width="10"
Stroke="Black"
StrokeThickness="1"
Fill="Green"/>
</tk:DataGridCheckBoxColumn.Header>
</tk:DataGridCheckBoxColumn>
<tk:DataGridTemplateColumn Header="Collection">
<tk:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox x:Name="CollectionCombo"
ItemsSource="{Binding Path=AllCollections, RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type apollo:BuildMonitorNew}}}"
DisplayMemberPath="Name"
SelectedValue="{Binding Collection}"
SelectedValuePath="Name">
</ComboBox>
</DataTemplate>
</tk:DataGridTemplateColumn.CellEditingTemplate>
<tk:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Collection,Mode=TwoWay}"/>
</DataTemplate>
</tk:DataGridTemplateColumn.CellTemplate>
</tk:DataGridTemplateColumn>
<tk:DataGridTemplateColumn Header="Project">
<tk:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox x:Name="Projects"
DisplayMemberPath="Name"
SelectedValue="{Binding TeamProject}"
SelectedValuePath="Name">
<ComboBox.ItemsSource>
<MultiBinding Converter="{StaticResource GetItemSource}" ConverterParameter="Projects" >
<Binding Path="." />
<Binding Path="AllCollections" RelativeSource="{RelativeSource Mode=FindAncestor,AncestorType={x:Type apollo:BuildMonitorNew}}"/>
</MultiBinding>
</ComboBox.ItemsSource>
</ComboBox>
</DataTemplate>
</tk:DataGridTemplateColumn.CellEditingTemplate>
<tk:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding TeamProject,Mode=TwoWay}"/>
</DataTemplate>
</tk:DataGridTemplateColumn.CellTemplate>
</tk:DataGridTemplateColumn>
<tk:DataGridTemplateColumn Header="Build Definition">
<tk:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<ComboBox SelectedItem="{Binding BuildDefinition}">
<ComboBox.ItemsSource>
<MultiBinding Converter="{StaticResource GetItemSource}" ConverterParameter="BuildDefinitions">
<Binding Path="." />
<Binding Path="AllCollections" RelativeSource="{RelativeSource Mode=FindAncestor,AncestorType={x:Type apollo:BuildMonitorNew}}"/>
</MultiBinding>
</ComboBox.ItemsSource>
</ComboBox>
</DataTemplate>
</tk:DataGridTemplateColumn.CellEditingTemplate>
<tk:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding BuildDefinition,Mode=TwoWay}"/>
</DataTemplate>
</tk:DataGridTemplateColumn.CellTemplate>
</tk:DataGridTemplateColumn>
<tk:DataGridTextColumn Binding="{Binding Command}"
Header="Command"/>
<tk:DataGridTextColumn Binding="{Binding Arguments}"
Header="Arguments"
Width="*"/>
</tk:DataGrid.Columns>
</tk:DataGrid>