I've got my own user control:
[TemplateVisualState(Name = StateValid, GroupName = GroupValidation)]
[TemplateVisualState(Name = StateInvalidFocused, GroupName = GroupValidation)]
[TemplateVisualState(Name = StateInvalidUnfocused, GroupName = GroupValidation)]
public class SearchTextBoxControl : TextBox
{
// properties removed for brevity
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
this.BindingValidationError += (s, e) => UpdateValidationState();
this.UpdateValidationState();
}
public const string GroupValidation = "ValidationStates";
public const string StateValid = "Valid";
public const string StateInvalidFocused = "InvalidFocused";
public const string StateInvalidUnfocused = "InvalidUnfocused";
private void UpdateValidationState()
{
var textBox = this.GetTemplateChild("ContentTextBox");
if (textBox != null)
{
VisualStateManager
.GoToState(textBox as Control,
Validation.GetErrors(this).Any() ?
StateInvalidUnfocused :
StateValid,
true);
}
}
}
and XAML:
<Style TargetType="local:SearchTextBoxControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:SearchTextBoxControl">
<Grid Grid.Column="1"
Grid.ColumnSpan="3"
Grid.Row="1"
Margin="{TemplateBinding Margin}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="32" />
</Grid.ColumnDefinitions>
<TextBox x:Name="ContentTextBox"
Grid.ColumnSpan="2"
IsReadOnly="{TemplateBinding IsReadOnly}"
Text="{TemplateBinding Text}">
</TextBox>
<Button Grid.Column="1"
Style="{StaticResource BrowseButton}"
Command="{TemplateBinding Command}">
<ToolTipService.ToolTip>
<ToolTip Content="{TemplateBinding ToolTip}" />
</ToolTipService.ToolTip>
<Image Source="../../Resources/Images/magnifier.png"
Style="{StaticResource BrowseButtonImage}" />
</Button>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
What should I do to pass validation errors to the TextBox x:Name="ContentTextBox" validation service (I want the same validation error tooltip on my control text box)?
Kind regards!
You can implement IDataErrorInfo Interface. It is available in ComponentModel Lib.
using System.ComponentModel;
public class SearchTextBoxControl : TextBox , IDataErrorInfo
{
#region IDataErrorInfo Members
public string Error
{
get { throw new NotImplementedException(); }
}
public string this[string columnName]
{
get { throw new NotImplementedException(); }
}
#endregion
// Your Code
}
Related
I'm trying to fill my collection with color. I have a border control for displaying the color which is selected using PrepareContainerForItemOverride
method and i have a button control to popup the color in collection. Here is my code.
Picker.cs
public sealed class Picker : ItemsControl
{
private ObservableCollection<SolidColorBrush> _myColors;
public Picker()
{
this.DefaultStyleKey = typeof(Picker);
_myColors = new ObservableCollection<SolidColorBrush>()
{
new SolidColorBrush(Color.FromArgb(255,225,225,25)),
new SolidColorBrush(Color.FromArgb(255,225,25,25)),
new SolidColorBrush(Color.FromArgb(255,225,225,225)),
new SolidColorBrush(Color.FromArgb(255,25,225,25))
};
}
public ObservableCollection<SolidColorBrush> MyColors
{
get
{
return (ObservableCollection<SolidColorBrush>)GetValue(MyColorsProperty);
}
set { SetValue(MyColorsProperty, value); }
}
public static readonly DependencyProperty MyColorsProperty =
DependencyProperty.Register("MyColors",
typeof(ObservableCollection<SolidColorBrush>), typeof(Picker), new
PropertyMetadata(null));
public Popup popup;
protected override void OnApplyTemplate()
{
popup = GetTemplateChild("myPopup") as Popup;
var popupbutton = GetTemplateChild("btn1") as Button;
popupbutton.Click += Popupbutton_Click;
}
private void Popupbutton_Click(object sender, RoutedEventArgs e)
{
popup.IsOpen = popup.IsOpen ? false : true;
}
public bool openPopup
{
get { return (bool)GetValue(openPopupProperty); }
set { SetValue(openPopupProperty, value); }
}
public static readonly DependencyProperty openPopupProperty =
DependencyProperty.Register("openPopup", typeof(bool),
typeof(Picker), new PropertyMetadata(true));
public object Data { get; private set; }
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(String propname)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propname));
}
}
}
Generic.xaml
<Style TargetType="local:Picker" >
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:Picker">
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel>
<Border Height="50" BorderBrush="Blue" BorderThickness="5">
<Border.Background>
<SolidColorBrush Color="{Binding Color}" />
</Border.Background>
</Border>
<Popup Name="myPopup" IsOpen="False" IsLightDismissEnabled="True">
<Grid Width="650" Height="300" Background="Red">
<ItemsControl ItemsSource="{TemplateBinding MyColors}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Border BorderThickness="6" BorderBrush="Black">
<Border.Background>
<SolidColorBrush Color="{Binding Color}" />
</Border.Background>
</Border>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Popup>
</StackPanel>
<Button Grid.Column="1" Name="btn1" Height="55" Content="Click me"/>
</Grid>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Here, I'm not able to popup the color in collection when the button is clicked and i need to display the selected color in the border contol using PrepareContainerForItemOverride method
The problem is you have bound wrong property to ItemsSource. And private field _myColors you have never used. I have edit your code. It works in my side.
Picker.cs
public Picker()
{
this.DefaultStyleKey = typeof(Picker);
MyColors = new ObservableCollection<SolidColorBrush>()
{
new SolidColorBrush(Color.FromArgb(255,225,225,25)),
new SolidColorBrush(Color.FromArgb(255,225,25,25)),
new SolidColorBrush(Color.FromArgb(255,225,225,225)),
new SolidColorBrush(Color.FromArgb(255,25,225,25))
};
}
public ObservableCollection<SolidColorBrush> MyColors
{
get
{
return (ObservableCollection<SolidColorBrush>)GetValue(MyColorsProperty);
}
set { SetValue(MyColorsProperty, value); }
}
public static readonly DependencyProperty MyColorsProperty =
DependencyProperty.Register("MyColors",
typeof(ObservableCollection<SolidColorBrush>), typeof(Picker), new
PropertyMetadata(null));
Generic.xaml
<Style TargetType="local:Picker" >
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:Picker">
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel>
<Border Height="50" BorderBrush="Blue" BorderThickness="5">
<Border.Background>
<SolidColorBrush Color="{Binding Color}" />
</Border.Background>
</Border>
<Popup Name="myPopup" IsOpen="False" IsLightDismissEnabled="True">
<Grid Width="650" Height="300" >
<ItemsControl ItemsSource="{TemplateBinding MyColors}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Border BorderThickness="15" BorderBrush="{Binding}">
</Border>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Popup>
</StackPanel>
<Button Grid.Column="1" Name="btn1" Height="55" Content="Click me"/>
</Grid>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
I have a button with the following content:
<StackPanel Orientation="Horizontal">
<TextBlock Text="Connect"/>
<materialDesign:PackIcon Kind="Arrow"/>
</StackPanel>
I searched and found this: WPF Button content binding but I'm not sure how to apply the solution when I have all of the three: a Stackpanel, the PackIcon (object) and the Textblock.
I have this progressBar which I make it appear under the button:
<ProgressBar x:Name="XZ" Foreground="Black" Grid.Row="4" Grid.Column="1"
Visibility="{Binding Connecting, UpdateSourceTrigger=PropertyChanged, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}"
Value="50"
IsIndeterminate="True" />
I want to make it so when I click the button, instead of showing the ProgressBar where it is right now, to basically remove the Text and the PackIcon and place the ProgressBar in the button.
Actually changing in the controls could be done with Data Triggers; though that seems a bit over the top in this case.
I would just toggle the visibility of two controls:
<Grid>
<StackPanel Orientation="Horizontal" Visibility="{Binding Connecting, Converter={StaticResource BooleanToCollapsedConverter}}"">
<TextBlock Text="Connect"/>
<materialDesign:PackIcon Kind="Arrow"/>
</StackPanel>
<ProgressBar x:Name="XZ" Foreground="Black" Grid.Row="4" Grid.Column="1"
Visibility="{Binding Connecting, Converter={StaticResource BooleanToVisibilityConverter}}"
Value="50"
IsIndeterminate="True" />
</Grid>
That would be the content of your button. BooleanToCollapsedConverter is just the inverse of a VisibiltyToBooleanConverter; there are a number of ways to do it and is left as an exercise.
As an aside; UpdateSourceTrigger doesn't make any sense on a OneWay binding (it doesn't update the source!) and you don't even need that on visibility as that's not an input the user can change.
You could use a data template. Something like:
XAML:
<Window.Resources>
<DataTemplate DataType="{x:Type local:ButtonInfo}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<Label Grid.Row="0" Content="Press me"></Label>
<Label Grid.Row="1" Content="{Binding Label}"></Label>
</Grid>
</DataTemplate>
<DataTemplate DataType="{x:Type local:ProgressInfo}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<ProgressBar Height="30" Value="{Binding Progress}"></ProgressBar>
<Label Grid.Row="1" Content="{Binding Label}"></Label>
</Grid>
</DataTemplate>
</Window.Resources>
<Grid>
<Button Command="{Binding ProcessCommand}" Content="{Binding ButtonInfo}">
</Button>
</Grid>
C#:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainWindowViewModel();
}
}
public class ViewModelBase:INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class MainWindowViewModel:ViewModelBase
{
public MainWindowViewModel()
{
ButtonInfo = new ButtonInfo(){Label = "Button Info"};
ProcessCommand = new DelegateCommand(Process);
}
private ButtonInfo _buttonInfo;
public ButtonInfo ButtonInfo
{
get { return _buttonInfo; }
set
{
_buttonInfo = value;
OnPropertyChanged();
}
}
public DelegateCommand ProcessCommand { get; set; }
private async void Process()
{
ButtonInfo = new ProgressInfo(){Label = "Progress Info"};
await ProcessAsync();
}
private Task ProcessAsync()
{
return Task.Run(() =>
{
for (int i = 0; i < 100; i++)
{
Application.Current.Dispatcher.Invoke(() =>
{
ButtonInfo.Progress = i;
if (i==99)
{
ButtonInfo = new ButtonInfo(){Label = "Button Again"};
}
});
Thread.Sleep(100);
}
});
}
}
public class ButtonInfo:ViewModelBase
{
private string _label;
private int _progress;
private bool _isProcessing;
public string Label
{
get { return _label; }
set
{
_label = value;
OnPropertyChanged();
}
}
public int Progress
{
get { return _progress; }
set
{
_progress = value;
OnPropertyChanged();
}
}
public bool IsProcessing
{
get { return _isProcessing; }
set
{
_isProcessing = value;
OnPropertyChanged();
}
}
}
public class ProgressInfo : ButtonInfo { }
You can create a template for the button to achieve this, then reuse the template everywhere you want a button with loading as following:
<Button Width="120" Height="40" Tag="False" Name="loadingButton" Click="loadingButton_Click">
<Button.Template>
<ControlTemplate>
<Border Name="PART_Border" BorderBrush="Black" BorderThickness="1" CornerRadius="2" Background="Transparent">
<Grid Name="PART_Root">
<TextBlock Name="PART_Text" HorizontalAlignment="Center" VerticalAlignment="Center">Data</TextBlock>
<ProgressBar IsIndeterminate="True" Name="PART_Loading"></ProgressBar>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="Tag" Value="True">
<Setter TargetName="PART_Text" Property="Visibility" Value="Collapsed"></Setter>
<Setter TargetName="PART_Loading" Property="Visibility" Value="Visible"></Setter>
</Trigger>
<Trigger Property="Tag" Value="False" >
<Setter TargetName="PART_Text" Property="Visibility" Value="Visible"></Setter>
<Setter TargetName="PART_Loading" Property="Visibility" Value="Collapsed"></Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Button.Template>
</Button>
And the event for button click would be:
private async void loadingButton_Click(object sender, RoutedEventArgs e)
{
loadingButton.Tag = true.ToString();//display loading
await Task.Run(() => { Thread.Sleep(4000); });//fake data loading
loadingButton.Tag = false.ToString();//hide loading
}
Note that you can also bind the Tag property for a property inside your view model if you where using MVVM pattern.
Have style for a series of buttons btn1, btn2, btn3, etc.
Inside the style for each button is a TextBlock for displaying the "Content" of the button, (since the border inside the style covers any content of the button itself).
Now, I would like for the TextBlock name to be tied to the button name. For example - btn1's text block's name would be btn1Txt. The purpose of this would be the end user can assign each button its own text in a settings menu.
Any hints on how I would go about this? I admit I'm relatively new to WPF and bindings.
EDIT:::: WHAT I"VE GOT SO FAR THAT IS WORKING.
On load, the program checks the settings file for the Text for each button. Each button's content is assigned the proper information. Then inside the style, I bind the TextBlock Text to the content of the parent button.
This may not be the normal way of going about it, but it works
Method
List<string> MainButtons = Properties.Settings.Default.MainButtonNames.Cast<string>().ToList();
for (int i = 0; i < MainButtons.Count(); i++)
{
string actualNum = Convert.ToString((i + 1));
var MainButtonFinder = (Button)this.FindName("MainButton" + actualNum);
Console.WriteLine(MainButtonFinder.Name);
MainButtonFinder.Content = MainButtons[i];
Console.WriteLine(MainButtonFinder.Content);
}
Style
<Style x:Key="MainButtonStyle" TargetType="{x:Type Button}">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="Width" Value="100px"/>
<Setter Property="MinHeight" Value="50"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border CornerRadius="20" Height="45" Width="100" Margin="0" Background="#FF99CCFF">
<TextBlock Text="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=Content}" HorizontalAlignment="Center" VerticalAlignment="Center" FontFamily="LCARS" Foreground="White" Padding="5px" FontSize="18px" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>`
This is the wrong way to go about what you're trying to do. Here's the "right" way to do it. There's a fair amount of boilerplate code here, but you get used to it.
Write a button viewmodel and give your main viewmodel an ObservableCollection of those:
#region ViewModelBase Class
public class ViewModelBase : INotifyPropertyChanged
{
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
#endregion INotifyPropertyChanged
}
#endregion ViewModelBase Class
#region MainViewModel Class
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
ButtonItems.Add(new ButtonItemViewModel("First Command", "First Item", () => MessageBox.Show("First Item Executed")));
ButtonItems.Add(new ButtonItemViewModel("Second Command", "Second Item", () => MessageBox.Show("Second Item Executed")));
}
#region ButtonItems Property
public ObservableCollection<ButtonItemViewModel> ButtonItems { get; }
= new ObservableCollection<ButtonItemViewModel>();
#endregion ButtonItems Property
}
#endregion MainViewModel Class
#region ButtonItemViewModel Class
public class ButtonItemViewModel : ViewModelBase
{
public ButtonItemViewModel(String cmdName, String text, Action cmdAction)
{
CommandName = cmdName;
Text = text;
Command = new DelegateCommand(cmdAction);
}
#region Text Property
private String _text = default(String);
public String Text
{
get { return _text; }
set
{
if (value != _text)
{
_text = value;
OnPropertyChanged();
}
}
}
#endregion Text Property
#region CommandName Property
private String _commandName = default(String);
public String CommandName
{
get { return _commandName; }
private set
{
if (value != _commandName)
{
_commandName = value;
OnPropertyChanged();
}
}
}
#endregion CommandName Property
public ICommand Command { get; private set; }
}
#endregion ButtonItemViewModel Class
public class DelegateCommand : ICommand
{
public DelegateCommand(Action action)
{
_action = action;
}
private Action _action;
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
_action?.Invoke();
}
}
Make that MainViewModel the DataContext of your Window:
public MainWindow()
{
InitializeComponent();
DataContext = new MainViewModel();
}
And here's how you can put it all together in the XAML:
<Grid>
<StackPanel Orientation="Vertical">
<GroupBox Header="Buttons">
<ItemsControl ItemsSource="{Binding ButtonItems}" HorizontalAlignment="Left">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button
Margin="2"
MinWidth="80"
Content="{Binding Text}"
Command="{Binding Command}"
/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</GroupBox>
<GroupBox Header="Edit Buttons">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ListBox
Grid.Column="0"
Margin="2"
x:Name="ButtonEditorListBox"
ItemsSource="{Binding ButtonItems}"
HorizontalAlignment="Left"
>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="2"
Text="{Binding CommandName}"
/>
<TextBlock
Margin="2"
Text="{Binding Text, StringFormat=': "{0}"'}"
/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ListBox>
<ContentControl
Grid.Column="1"
Margin="8,2,2,2"
Content="{Binding SelectedItem, ElementName=ButtonEditorListBox}"
>
<ContentControl.ContentTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical">
<TextBlock
Margin="2"
HorizontalAlignment="Stretch"
FontWeight="Bold"
Text="{Binding CommandName, StringFormat={}{0}: }"
/>
<TextBox
Margin="2"
HorizontalAlignment="Stretch"
Text="{Binding Text, UpdateSourceTrigger=PropertyChanged}"
/>
</StackPanel>
</DataTemplate>
</ContentControl.ContentTemplate>
</ContentControl>
</Grid>
</GroupBox>
</StackPanel>
</Grid>
Back to your question:
Inside the style for each button is a TextBlock for displaying the "Content" of the button, (since the border inside the style covers any content of the button itself).
You're doing styles wrong. Very, very wrong. I can help you fix it if you show me the style.
As I understand what you want is to modify the "text" of the button, it occurs to me that you can do it this way.
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="20"></RowDefinition>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<TextBox Text="{Binding ElementName=ButtonTest, Path=Content, UpdateSourceTrigger=PropertyChanged}" ></TextBox>
<Button Name="ButtonTest" Grid.Row="1" Width="100" Height="40">
<Button.ContentTemplate>
<DataTemplate>
<TextBlock Foreground="Blue" Text="{Binding}"></TextBlock>
</DataTemplate>
</Button.ContentTemplate>
</Button>
</Grid>
I want to display a input-validation for the user when entering empty values in a text box, is there a simple way to use data annotation to do this for WPF ?
I am new to WPF and C# if someone could explain this for me I would really appreciate it.
I want something like this or similar :
Using a ValidationRule on your binding can get the effect you want, you specify them declaratively in your XAML, and you can make them as custom and complex as you need to:
<TextBox x:Name="FilePathTextBox" Width="350" Margin="5,0,0,0">
<TextBox.Text>
<Binding Path="FilePath" UpdateSourceTrigger="PropertyChanged" >
<Binding.ValidationRules>
<this:FilePathValidationRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
will produce this:
Reference my blog post Taking data binding, validation and MVVM to the next level
You have to use ErrorTemplate like below :
<TextBox...>
<Validation.ErrorTemplate>
<ControlTemplate>
<StackPanel>
<!-- Placeholder for the TextBox itself -->
<AdornedElementPlaceholder x:Name="textBox"/>
<TextBlock Text="{Binding [0].ErrorContent}" Foreground="Red"/>
</StackPanel>
</ControlTemplate>
</Validation.ErrorTemplate>
</TextBox>
Data Validation in WPF
Beside all the other correct answers here, additionally to ValidationRule, I'd recommend you to use IDataError-Interface.
With that interface you can match more easily your use of DataAnnotations as you mentioned in the title.
Demo-Model:
public class Pony : IDataErrorInfo, INotifyPropertyChanged {
private Dictionary<string, List<string>> _errors = new Dictionary<string, List<string>>();
[Range(0,4)]
public int Id {
get; set;
}
[Required]
public string Name {
get; set;
}
public Brush Color {
get; set;
}
public string Error {
get {
var builder = new StringBuilder();
foreach (var error in this.Errors) {
if (error.Value.Count > 0) {
foreach (var text in error.Value) {
builder.AppendLine(text);
}
}
}
return builder.Length > 0 ? builder.ToString(0, builder.Length - 2) : builder.ToString();
}
}
public bool HasError => this.Errors.Count > 0;
public virtual string this[string columnName] {
get {
var modelClassProperties = TypeDescriptor.GetProperties(this.GetType());
foreach (PropertyDescriptor prop in modelClassProperties) {
if (prop.Name != columnName) {
continue;
}
this.Errors[columnName] = new List<string>();
foreach (var attribute in prop.Attributes) {
if (!(attribute is ValidationAttribute)) {
continue;
}
var validation = attribute as ValidationAttribute;
if (validation.IsValid(prop.GetValue(this))) {
continue;
}
var dn = prop.Name;
foreach (var pa in prop.Attributes.OfType<DisplayNameAttribute>()) {
dn = pa.DisplayName;
}
this.Errors[columnName].Add(validation.FormatErrorMessage(dn));
this.OnPropertyChanged("Error");
return validation.FormatErrorMessage(dn);
}
}
this.Errors.Remove(columnName);
this.OnPropertyChanged("Error");
return null;
}
}
internal Dictionary<string, List<string>> Errors => this._errors;
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) {
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Demo-XAML
<Grid VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Height="200">
<Grid.Resources>
<Style TargetType="{x:Type TextBox}">
<Setter Property="MinWidth" Value="100"></Setter>
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<StackPanel>
<!-- Placeholder for the TextBox itself -->
<AdornedElementPlaceholder x:Name="textBox"/>
<TextBlock Text="{Binding [0].ErrorContent}" Foreground="Red"/>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type WrapPanel}">
<Setter Property="Margin" Value="0,0,0,10"></Setter>
</Style>
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<GroupBox Header="Pony 1" Grid.Row="1">
<StackPanel>
<WrapPanel>
<TextBlock Text="Id:"/>
<TextBox Text="{Binding Ponys[0].Id, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}"></TextBox>
</WrapPanel>
<WrapPanel Grid.Row="1">
<TextBlock Text="Name:"/>
<TextBox Text="{Binding Ponys[0].Name, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}"></TextBox>
</WrapPanel>
<WrapPanel Grid.Row="2">
<TextBlock Text="Color:"/>
<TextBox Text="{Binding Ponys[0].Color, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}"></TextBox>
</WrapPanel>
</StackPanel>
</GroupBox>
<GroupBox Header="Pony 2">
<StackPanel>
<WrapPanel>
<TextBlock Text="Id:"/>
<TextBox Text="{Binding Ponys[1].Id, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}"></TextBox>
</WrapPanel>
<WrapPanel Grid.Row="1">
<TextBlock Text="Name:"/>
<TextBox Text="{Binding Ponys[1].Name, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}"></TextBox>
</WrapPanel>
<WrapPanel Grid.Row="2">
<TextBlock Text="Color:"/>
<TextBox Text="{Binding Ponys[1].Color, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}"></TextBox>
</WrapPanel>
</StackPanel>
</GroupBox>
</Grid>
Demo-Usage:
public MainWindow() {
InitializeComponent();
this.Ponys = new List<Pony>();
this.Ponys.Add(new Pony() { Color = Brushes.HotPink });
this.Ponys.Add(new Pony() { Id = 9, Name = "Not so fluffy", Color = Brushes.Chocolate });
this.DataContext = this;
}
Conclusion:
This Approach is designed to use in a Base-class.
It fully supports Attribute-Validation and is capable of handling multiple Attributes per Property.
Since ValidationAttribute is not sealed, you can inherit from it and design your own validations.
Example:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class Numeric : ValidationAttribute {
protected override ValidationResult IsValid(object value, ValidationContext validationContext) {
if (value == null) {
return ValidationResult.Success;
}
double result;
var isNumeric = double.TryParse(value.ToString(), out result);
return !isNumeric ? new ValidationResult(this.ErrorMessage) : ValidationResult.Success;
}
}
I developed this, because i hated how i had to use the ValidationRules.
Additionally, the ValidationRules are only Input-Validations. This means, if your data is corrupt, you will never notice
I am currently implementing a ValidationRule to check if some invalid character are in a TextBox. I am happy that setting the class I have implemented that inherits ValidationRule on my TextBox sets it in red when such characters are found, but I would also like to use the Validation.HasError property or the Validation.Errors property to pop a messagebox telling the user that there are errors in the various textboxes in the page.
Is there a way to bind a property in my ViewModel to the Validation.HasError and/or to the Validation.Errors properties in order for me to have access to them in my ViewModel?
Here is my error style for the TextBox:
<Style x:Key="ErrorValidationTextBox" TargetType="{x:Type pres:OneTextBox}">
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<DockPanel LastChildFill="True">
<TextBlock DockPanel.Dock="Right"
Foreground="Red"
FontSize="12pt"
Text="{Binding ElementName=MyAdorner,
Path=AdornedElement.(Validation.Errors)[0].ErrorContent}">
</TextBlock>
<AdornedElementPlaceholder x:Name="MyAdorner"/>
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Here is how I declare my TextBox (OneTextBox encapsulates the regular WPF TextBox) in my XAML:
<pres:OneTextBox Watermark="Name..." Margin="85,12,0,0" Style="{StaticResource ErrorValidationTextBox}"
AcceptsReturn="False" MaxLines="1" Height="22" VerticalAlignment="Top"
HorizontalAlignment="Left" Width="300" >
<pres:OneTextBox.Text>
<Binding Path="InterfaceSpecification.Name" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<interfaceSpecsModule:NoInvalidCharsRule/>
</Binding.ValidationRules>
</Binding>
</pres:OneTextBox.Text>
</pres:OneTextBox>
The Validation.HasError is readonly property, therefore Binding will not work with this property. This can be seen in ILSpy:
public virtual bool HasError
{
get
{
return this._validationError != null;
}
}
As an alternative, you should see a great article which provides a solution in the form of use attached dependency properties, there you will see a detailed explanation of the example.
Below is a full example from this article, I just translated it under C#, the original language is VB.NET:
XAML
<Window x:Class="HasErrorTestValidation.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:HasErrorTestValidation"
WindowStartupLocation="CenterScreen"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:TestData />
</Window.DataContext>
<StackPanel>
<TextBox x:Name="TestTextBox"
local:ProtocolSettingsLayout.MVVMHasError="{Binding Path=HasError}">
<TextBox.Text>
<Binding Path="TestText" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:OnlyNumbersValidationRule ValidatesOnTargetUpdated="True"/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<TextBlock>
<TextBlock.Text>
<Binding Path="HasError" StringFormat="HasError is {0}"/>
</TextBlock.Text>
</TextBlock>
<TextBlock>
<TextBlock.Text>
<Binding Path="(Validation.HasError)" ElementName="TestTextBox" StringFormat="Validation.HasError is {0}"/>
</TextBlock.Text>
</TextBlock>
</StackPanel>
</Window>
Code-behind
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
#region Model
public class TestData : INotifyPropertyChanged
{
private bool _hasError = false;
public bool HasError
{
get
{
return _hasError;
}
set
{
_hasError = value;
NotifyPropertyChanged("HasError");
}
}
private string _testText = "0";
public string TestText
{
get
{
return _testText;
}
set
{
_testText = value;
NotifyPropertyChanged("TestText");
}
}
#region PropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string sProp)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(sProp));
}
}
#endregion
}
#endregion
#region ValidationRule
public class OnlyNumbersValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
var result = new ValidationResult(true, null);
string NumberPattern = #"^[0-9-]+$";
Regex rgx = new Regex(NumberPattern);
if (rgx.IsMatch(value.ToString()) == false)
{
result = new ValidationResult(false, "Must be only numbers");
}
return result;
}
}
#endregion
public class ProtocolSettingsLayout
{
public static readonly DependencyProperty MVVMHasErrorProperty= DependencyProperty.RegisterAttached("MVVMHasError",
typeof(bool),
typeof(ProtocolSettingsLayout),
new FrameworkPropertyMetadata(false,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
null,
CoerceMVVMHasError));
public static bool GetMVVMHasError(DependencyObject d)
{
return (bool)d.GetValue(MVVMHasErrorProperty);
}
public static void SetMVVMHasError(DependencyObject d, bool value)
{
d.SetValue(MVVMHasErrorProperty, value);
}
private static object CoerceMVVMHasError(DependencyObject d,Object baseValue)
{
bool ret = (bool)baseValue;
if (BindingOperations.IsDataBound(d,MVVMHasErrorProperty))
{
if (GetHasErrorDescriptor(d)==null)
{
DependencyPropertyDescriptor desc = DependencyPropertyDescriptor.FromProperty(Validation.HasErrorProperty, d.GetType());
desc.AddValueChanged(d,OnHasErrorChanged);
SetHasErrorDescriptor(d, desc);
ret = System.Windows.Controls.Validation.GetHasError(d);
}
}
else
{
if (GetHasErrorDescriptor(d)!=null)
{
DependencyPropertyDescriptor desc= GetHasErrorDescriptor(d);
desc.RemoveValueChanged(d, OnHasErrorChanged);
SetHasErrorDescriptor(d, null);
}
}
return ret;
}
private static readonly DependencyProperty HasErrorDescriptorProperty = DependencyProperty.RegisterAttached("HasErrorDescriptor",
typeof(DependencyPropertyDescriptor),
typeof(ProtocolSettingsLayout));
private static DependencyPropertyDescriptor GetHasErrorDescriptor(DependencyObject d)
{
var ret = d.GetValue(HasErrorDescriptorProperty);
return ret as DependencyPropertyDescriptor;
}
private static void OnHasErrorChanged(object sender, EventArgs e)
{
DependencyObject d = sender as DependencyObject;
if (d != null)
{
d.SetValue(MVVMHasErrorProperty, d.GetValue(Validation.HasErrorProperty));
}
}
private static void SetHasErrorDescriptor(DependencyObject d, DependencyPropertyDescriptor value)
{
var ret = d.GetValue(HasErrorDescriptorProperty);
d.SetValue(HasErrorDescriptorProperty, value);
}
}
As an alternative to the use of ValidationRule, in MVVM style you can try to implement IDataErrorInfo Interface. For more info see this:
Enforcing Complex Business Data Rules with WPF
all perfect work set NotifyOnValidationError="True" on binding;
(or maybe with binding group also possible)
then use
<Button IsEnabled="{Binding ElementName=tbPeriod, Path=(Validation.HasError)}"
sample with one textBox:
<val:RangeRulecan be changed to ms sample agerangerule etc
<TextBox MaxLength="5" x:Name="tbPeriod" HorizontalAlignment="Left" VerticalAlignment="Top" Width="162" Margin="10,10,0,0" Style="{StaticResource TextBoxInError}">
<TextBox.Text>
<Binding Path="ReportPeriod" UpdateSourceTrigger="PropertyChanged" NotifyOnValidationError="True">
<Binding.ValidationRules>
<val:RangeRule Min="70" Max="5000" />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
In response to Anatoliy's request for an example of a non-working project:
Generic.xaml
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TestAttachedPropertyValidationError">
<Style TargetType="{x:Type local:TextBoxCustomControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:TextBoxCustomControl}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="50"/>
</Grid.ColumnDefinitions>
<Grid.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
</Grid.Resources>
<Label
Grid.Row ="0"
Grid.Column="0"
Content="Enter a numeric value:" />
<TextBox
Grid.Row ="0"
Grid.Column="2"
local:HasErrorUtility.HasError="{Binding NumericPropHasError, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
Text="{Binding NumericProp, Mode=TwoWay, UpdateSourceTrigger=LostFocus, RelativeSource={RelativeSource TemplatedParent}}" />
<Label
Grid.Row ="1"
Grid.Column="0"
Content="Value entered:" />
<Label
Grid.Row ="1"
Grid.Column="2"
Content="{TemplateBinding NumericProp}" />
<Label
Grid.Row ="2"
Grid.Column="0"
Grid.ColumnSpan="3"
Visibility="{TemplateBinding NumericPropHasError, Converter={StaticResource BooleanToVisibilityConverter}}"
Foreground="Red"
Content="Not a numeric value" />
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
TextBoxCustomControl.cs
using System.Windows;
using System.Windows.Controls;
namespace TestAttachedPropertyValidationError
{
public class TextBoxCustomControl : Control
{
static TextBoxCustomControl()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(TextBoxCustomControl), new FrameworkPropertyMetadata(typeof(TextBoxCustomControl)));
}
public static readonly DependencyProperty NumericPropProperty =
DependencyProperty.Register("NumericProp", typeof (int), typeof (TextBoxCustomControl), new PropertyMetadata(default(int)));
public int NumericProp
{
get { return (int) GetValue(NumericPropProperty); }
set { SetValue(NumericPropProperty, value); }
}
public static readonly DependencyProperty NumericPropHasErrorProperty =
DependencyProperty.Register("NumericPropHasError", typeof (bool), typeof (TextBoxCustomControl), new PropertyMetadata(default(bool)));
public bool NumericPropHasError
{
get { return (bool) GetValue(NumericPropHasErrorProperty); }
set { SetValue(NumericPropHasErrorProperty, value); }
}
}
}
HasErrorUtility.cs
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace TestAttachedPropertyValidationError
{
class HasErrorUtility
{
public static readonly DependencyProperty HasErrorProperty = DependencyProperty.RegisterAttached("HasError",
typeof(bool),
typeof(HasErrorUtility),
new FrameworkPropertyMetadata(false,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
null,
CoerceHasError));
public static bool GetHasError(DependencyObject d)
{
return (bool)d.GetValue(HasErrorProperty);
}
public static void SetHasError(DependencyObject d, bool value)
{
d.SetValue(HasErrorProperty, value);
}
private static object CoerceHasError(DependencyObject d, Object baseValue)
{
var ret = (bool)baseValue;
if (BindingOperations.IsDataBound(d, HasErrorProperty))
{
if (GetHasErrorDescriptor(d) == null)
{
var desc = DependencyPropertyDescriptor.FromProperty(Validation.HasErrorProperty, d.GetType());
desc.AddValueChanged(d, OnHasErrorChanged);
SetHasErrorDescriptor(d, desc);
ret = Validation.GetHasError(d);
}
}
else
{
if (GetHasErrorDescriptor(d) != null)
{
var desc = GetHasErrorDescriptor(d);
desc.RemoveValueChanged(d, OnHasErrorChanged);
SetHasErrorDescriptor(d, null);
}
}
return ret;
}
private static readonly DependencyProperty HasErrorDescriptorProperty = DependencyProperty.RegisterAttached("HasErrorDescriptor",
typeof(DependencyPropertyDescriptor),
typeof(HasErrorUtility));
private static DependencyPropertyDescriptor GetHasErrorDescriptor(DependencyObject d)
{
var ret = d.GetValue(HasErrorDescriptorProperty);
return ret as DependencyPropertyDescriptor;
}
private static void SetHasErrorDescriptor(DependencyObject d, DependencyPropertyDescriptor value)
{
d.SetValue(HasErrorDescriptorProperty, value);
}
private static void OnHasErrorChanged(object sender, EventArgs e)
{
var d = sender as DependencyObject;
if (d != null)
{
d.SetValue(HasErrorProperty, d.GetValue(Validation.HasErrorProperty));
}
}
}
}
ViewModel.cs
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace TestAttachedPropertyValidationError
{
public class ViewModel :INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private int _vmNumericProp;
private bool _vmNumericPropHasError;
public int VmNumericProp
{
get { return _vmNumericProp; }
set
{
_vmNumericProp = value;
OnPropertyChanged();
}
}
public bool VmNumericPropHasError
{
get { return _vmNumericPropHasError; }
set
{
_vmNumericPropHasError = value;
OnPropertyChanged();
}
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
MainWindow.xaml
<Window x:Class="TestAttachedPropertyValidationError.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TestAttachedPropertyValidationError"
Title="MainWindow" Height="350" Width="525">
<StackPanel Margin="10">
<StackPanel.Resources>
<local:ViewModel x:Key="VM1"/>
<local:ViewModel x:Key="VM2"/>
</StackPanel.Resources>
<Label Content="Custom Control...}"></Label>
<local:TextBoxCustomControl
Margin="10"
DataContext="{StaticResource VM1}"
NumericProp="{Binding VmNumericProp}"
NumericPropHasError="{Binding VmNumericPropHasError}"/>
<Label Content="Regular XAML...}" Margin="0,20,0,0"/>
<Grid
Margin="10"
DataContext="{StaticResource VM2}"
>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="50"/>
</Grid.ColumnDefinitions>
<Grid.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
</Grid.Resources>
<Label
Grid.Row ="0"
Grid.Column="0"
Content="Enter a numeric value:" />
<TextBox
Grid.Row ="0"
Grid.Column="2"
local:HasErrorUtility.HasError="{Binding VmNumericPropHasError, Mode=TwoWay}"
Text="{Binding VmNumericProp, Mode=TwoWay, UpdateSourceTrigger=LostFocus}" />
<Label
Grid.Row ="1"
Grid.Column="0"
Content="Value entered:" />
<Label
Grid.Row ="1"
Grid.Column="2"
Content="{Binding VmNumericProp}" />
<Label
Grid.Row ="2"
Grid.Column="0"
Grid.ColumnSpan="3"
Visibility="{Binding VmNumericPropHasError, Converter={StaticResource BooleanToVisibilityConverter}}"
Foreground="Red"
Content="Not a numeric value" />
</Grid>
</StackPanel>