When i first load the window, the button is visible and there isn't an error in the validation (no red line round textbox).
When typing values into the textbox, the validation rules work as they should.
I would like, if possible, to have the button hidden at the start, and for the validation rules to start when the I start typing text into the box.
Here is the code that i have so far. The xaml:
<TextBox x:Name="txtName" HorizontalAlignment="Left" Height="23" Margin="156,119,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="120" Validation.ErrorTemplate="{StaticResource ValidationErrorTemplate}">
<TextBox.Text>
<Binding Path="Name" UpdateSourceTrigger="PropertyChanged" NotifyOnSourceUpdated="True">
<Binding.ValidationRules>
<local:ValidationTest/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Button x:Name="btn1" Content="Button" HorizontalAlignment="Left" Margin="85,221,0,0" VerticalAlignment="Top" Width="75" Click="Button_Click">
<Button.Style>
<Style TargetType="Button">
<Setter Property="Visibility" Value="Hidden"/>
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Path=(Validation.HasError), ElementName=txtName}" Value="False"/>
</MultiDataTrigger.Conditions>
<Setter Property="Visibility" Value="Visible"></Setter>
</MultiDataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
My validation logic:
class ValidationTest : ValidationRule
{
private int result;
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
if (value == null || string.IsNullOrEmpty(value.ToString()))
{
return new ValidationResult(false, "Value cannot be empty.");
}
if (value.ToString().Length > 4)
{
return new ValidationResult(false, "Name cannot be more than 20 characters long.");
}
return ValidationResult.ValidResult;
}
}
The error template i am using:
<Window.Resources>
<Style TargetType="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>
<ControlTemplate x:Key="ValidationErrorTemplate">
<DockPanel>
<Border BorderBrush="Red" BorderThickness="1.5">
<AdornedElementPlaceholder x:Name="ErrorAdorner"></AdornedElementPlaceholder>
</Border>
</DockPanel>
</ControlTemplate>
</Window.Resources>
I have tried to update the binding when the window loads by using txtName.GetBindingExpression(TextBox.TextProperty).UpdateSource();, but that shows the validation error (red line around textbox). However, the button is hidden, so is there any way of hiding the validation error until the user types text into the textbox?
Am i approaching this in the right way?
I know I am a little late to the party but I came across this question while I was looking to do the same thing. The only thing that I didn't like about using a flag to control when the Validation is performed is that you needed to set the DoValidation flag at some point in your code and I wanted it to be a little more "Automated".
I found quite a few examples online but they all seemed to use the Boolean flag method. I found this MSDN Article and used it as a base then adjusted the code.
I came up with a solution that seems to work really well. Basically in a nutshell what I did was instead of having one variable to keep track of when the validation should be performed I created another Dictionary to keep track of:
When the Validation should be performed.
Store the state of the Validation (Valid, Invalid).
I only wanted the validation to be performed after the first update, so the first order of business is to decide if the Validation should be performed. The first run of the Validation stores the parameter in the Dictionary, then next time around if the parameter is present it performs the validation and stores a true/false (Invalid/Valid) result. This is also a handy way of telling both if the Model has been Validated and if it is valid, so I also added a parameter/flag to simply return if there are any results and the state of the validation. This is especially useful for binding the command enable/disable.
Here is how I accomplished this:
Base PropertyValidation Model:
public abstract class PropertyValidation : INotifyPropertyChanged, IDataErrorInfo
{
#region Fields
private readonly Dictionary<string, object> _values = new Dictionary<string, object>();
/// <summary>
/// This holds the list of validation results and controls when the validation should be
/// performed and if the validation is valid.
/// </summary>
private Dictionary<string, bool> _validationResults { get; set; } = new Dictionary<string, bool>();
#endregion
#region Protected
/// <summary>
/// Sets the value of a property.
/// </summary>
/// <typeparam name="T">The type of the property value.</typeparam>
/// <param name="propertySelector">Expression tree contains the property definition.</param>
/// <param name="value">The property value.</param>
protected void SetValue<T>(Expression<Func<T>> propertySelector, T value)
{
string propertyName = GetPropertyName(propertySelector);
SetValue<T>(propertyName, value);
}
/// <summary>
/// Sets the value of a property.
/// </summary>
/// <typeparam name="T">The type of the property value.</typeparam>
/// <param name="propertyName">The name of the property.</param>
/// <param name="value">The property value.</param>
protected void SetValue<T>(string propertyName, T value)
{
if (string.IsNullOrEmpty(propertyName))
{
throw new ArgumentException("Invalid property name", propertyName);
}
_values[propertyName] = value;
OnPropertyChanged(propertyName);
}
/// <summary>
/// Gets the value of a property.
/// </summary>
/// <typeparam name="T">The type of the property value.</typeparam>
/// <param name="propertySelector">Expression tree contains the property definition.</param>
/// <returns>The value of the property or default value if not exist.</returns>
protected T GetValue<T>(Expression<Func<T>> propertySelector)
{
string propertyName = GetPropertyName(propertySelector);
return GetValue<T>(propertyName);
}
/// <summary>
/// Gets the value of a property.
/// </summary>
/// <typeparam name="T">The type of the property value.</typeparam>
/// <param name="propertyName">The name of the property.</param>
/// <returns>The value of the property or default value if not exist.</returns>
protected T GetValue<T>(string propertyName)
{
if (string.IsNullOrEmpty(propertyName))
{
throw new ArgumentException("Invalid property name", propertyName);
}
object value;
if (!_values.TryGetValue(propertyName, out value))
{
value = default(T);
_values.Add(propertyName, value);
}
return (T)value;
}
/// <summary>
/// Validates current instance properties using Data Annotations.
/// </summary>
/// <param name="propertyName">This instance property to validate.</param>
/// <returns>Relevant error string on validation failure or <see cref="System.String.Empty"/> on validation success.</returns>
protected virtual string OnValidate(string propertyName)
{
string error = string.Empty;
if (string.IsNullOrEmpty(propertyName))
{
throw new ArgumentException("Invalid property name", propertyName);
}
//Check if the Field has been added, this keeps track of when the validation
//is performed.
if (_validationResults.Any(x => x.Key == propertyName))
{
var value = GetValue(propertyName);
var results = new List<System.ComponentModel.DataAnnotations.ValidationResult>(1);
var result = Validator.TryValidateProperty(
value,
new ValidationContext(this, null, null)
{
MemberName = propertyName
},
results);
if (!result)
{
var validationResult = results.First();
error = validationResult.ErrorMessage;
//Store a true result in the validation to set the error.
_validationResults[propertyName] = true;
}
else
{
//If the Validation has been run and not invalid make sure the
//paramter in the list is cleared, otherwise validation would
//always return invalid once it is invalidated.
_validationResults[propertyName] = false;
}
}
else
{
//This is the first run of the Validation, simply store the paramter
//in the validation list and wait until next time to validate.
_validationResults.Add(propertyName, true);
}
//Notify that things have changed
OnPropertyChanged("IsValid");
//Return the actual result
return error;
}
#endregion
#region Public
/// <summary>
/// This returns if the Validation is Valid or not
/// </summary>
/// <returns>True if the Validation has been perfomed and if there are not
/// true values. Will return false until the validation has been done once.</returns>
public bool IsValid {
get { return (!_validationResults.Any(x => x.Value) && (_validationResults.Count > 0)); }
}
/// <summary>
/// Clears/Reset the Validation
/// </summary>
public void ClearValidation()
{
_validationResults.Clear();
}
#endregion
#region Change Notification
/// <summary>
/// Raised when a property on this object has a new value.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Raises this object's PropertyChanged event.
/// </summary>
/// <param name="propertyName">The property that has a new value.</param>
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.VerifyPropertyName(propertyName);
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
{
var e = new PropertyChangedEventArgs(propertyName);
handler(this, e);
}
}
protected void OnPropertyChanged<T>(Expression<Func<T>> propertySelector)
{
var propertyChanged = PropertyChanged;
if (propertyChanged != null)
{
string propertyName = GetPropertyName(propertySelector);
propertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion // IOnPropertyChanged Members
#region Data Validation
string IDataErrorInfo.Error {
get {
throw new NotSupportedException("IDataErrorInfo.Error is not supported, use IDataErrorInfo.this[propertyName] instead.");
}
}
string IDataErrorInfo.this[string propertyName] {
get {
return OnValidate(propertyName);
}
}
#endregion
#region Privates
private string GetPropertyName(LambdaExpression expression)
{
var memberExpression = expression.Body as MemberExpression;
if (memberExpression == null)
{
throw new InvalidOperationException();
}
return memberExpression.Member.Name;
}
private object GetValue(string propertyName)
{
object value;
if (!_values.TryGetValue(propertyName, out value))
{
var propertyDescriptor = TypeDescriptor.GetProperties(GetType()).Find(propertyName, false);
if (propertyDescriptor == null)
{
throw new ArgumentException("Invalid property name", propertyName);
}
value = propertyDescriptor.GetValue(this);
_values.Add(propertyName, value);
}
return value;
}
#endregion
#region Debugging
/// <summary>
/// Warns the developer if this object does not have
/// a public property with the specified name. This
/// method does not exist in a Release build.
/// </summary>
[Conditional("DEBUG")]
[DebuggerStepThrough]
public void VerifyPropertyName(string propertyName)
{
// Verify that the property name matches a real,
// public, instance property on this object.
if (TypeDescriptor.GetProperties(this)[propertyName] == null)
{
string msg = "Invalid property name: " + propertyName;
if (this.ThrowOnInvalidPropertyName)
throw new Exception(msg);
else
Debug.Fail(msg);
}
}
/// <summary>
/// Returns whether an exception is thrown, or if a Debug.Fail() is used
/// when an invalid property name is passed to the VerifyPropertyName method.
/// The default value is false, but subclasses used by unit tests might
/// override this property's getter to return true.
/// </summary>
protected virtual bool ThrowOnInvalidPropertyName { get; private set; }
#endregion // Debugging Aides
}
Model:
public class MyModel : PropertyValidation
{
[Required(ErrorMessage = "Name must be specified")]
[MaxLength(50, ErrorMessage = "Name too long, Name cannot contain more than 50 characters")]
public string Name {
get { return GetValue(() => Name); }
set { SetValue(() => Name, value); }
}
[Required(ErrorMessage = "Description must be specified")]
[MaxLength(150, ErrorMessage = "Description too long, Description cannot contain more than 150 characters")]
public string Description {
get { return GetValue(() => Description); }
set { SetValue(() => Description, value); }
}
}
Error Template:
<ControlTemplate x:Key="ValidationErrorTemplate">
<DockPanel LastChildFill="true">
<Border Background="Red" DockPanel.Dock="right"
Margin="-20,0,0,0" Width="10" Height="10" CornerRadius="10"
ToolTip="{Binding ElementName=customAdorner, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}">
<TextBlock Text="!" VerticalAlignment="center" HorizontalAlignment="center" FontWeight="Bold" Foreground="white"/>
</Border>
<AdornedElementPlaceholder Name="customAdorner" VerticalAlignment="Center" >
<Border BorderBrush="red" BorderThickness="1" >
<Border.Effect>
<BlurEffect Radius="5" />
</Border.Effect>
</Border>
</AdornedElementPlaceholder>
</DockPanel>
</ControlTemplate>
Data Template/Form:
<DataTemplate x:Key="MyModelDetailsTemplate" DataType="{x:Type data:MyModel}" >
<StackPanel Grid.IsSharedSizeScope="True">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto" SharedSizeGroup="Labels" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Grid.Column="0">Name</Label>
<TextBox x:Name="Name"
Grid.Column="1"
MinWidth="150"
Validation.ErrorTemplate="{StaticResource ValidationErrorTemplate}"
Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True, NotifyOnValidationError=True}" />
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto" SharedSizeGroup="Labels" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Grid.Column="0">Description</Label>
<TextBox Grid.Column="1"
Validation.ErrorTemplate="{StaticResource ValidationErrorTemplate}"
Text="{Binding Description, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True, NotifyOnValidationError=True}" AcceptsReturn="True" VerticalAlignment="Stretch" />
</Grid>
</StackPanel>
</DataTemplate>
RelayCommand (For Completeness)
public class RelayCommand : ICommand
{
private Action<object> execute;
private Predicate<object> canExecute;
private event EventHandler CanExecuteChangedInternal;
public RelayCommand(Action<object> execute) : this(execute, DefaultCanExecute)
{
}
public RelayCommand(Action<object> execute, Predicate<object> canExecute)
{
if (execute == null)
{
throw new ArgumentNullException("execute");
}
if (canExecute == null)
{
throw new ArgumentNullException("canExecute");
}
this.execute = execute;
this.canExecute = canExecute;
}
public event EventHandler CanExecuteChanged {
add {
CommandManager.RequerySuggested += value;
this.CanExecuteChangedInternal += value;
}
remove {
CommandManager.RequerySuggested -= value;
this.CanExecuteChangedInternal -= value;
}
}
public bool CanExecute(object parameter)
{
return this.canExecute != null && this.canExecute(parameter);
}
public void Execute(object parameter)
{
this.execute(parameter);
}
public void OnCanExecuteChanged()
{
EventHandler handler = this.CanExecuteChangedInternal;
if (handler != null)
{
//DispatcherHelper.BeginInvokeOnUIThread(() => handler.Invoke(this, EventArgs.Empty));
handler.Invoke(this, EventArgs.Empty);
}
}
public void Destroy()
{
this.canExecute = _ => false;
this.execute = _ => { return; };
}
private static bool DefaultCanExecute(object parameter)
{
return true;
}
}
ViewModel:
** Note the PropertyValidation here is not required, A separate INotifyPropertyChanged base could be used instead, I only used it for the OnPropertyChanged Notification and to keep things simple **
public class PageHomeVM : PropertyValidation
{
private ICommand saveCommand;
public ICommand SaveCommand {
get {
return saveCommand;
}
set {
saveCommand = value;
OnPropertyChanged();
}
}
public MyModel MyModel { get; set; } = new MyModel();
public PageHomeVM()
{
SaveCommand = new RelayCommand(SaveRecord, p => MyModel.IsValid);
MyModel.ClearValidation();
}
public void SaveRecord(object p)
{
//Perform the save....
}
}
View:
<pages:BasePage.DataContext>
<ViewModels:PageHomeVM/>
</pages:BasePage.DataContext>
<StackPanel>
<Label Content="MyModel Details"/>
<ContentPresenter ContentTemplate="{StaticResource MyModelDetailsTemplate}" Content="{Binding MyModel}" />
<Button x:Name="btnSave"
Command="{Binding SaveCommand}"
Width="75"
HorizontalAlignment="Right">Save</Button>
</StackPanel>
I hope this helps...
save a flag for once you have any value
class ValidationTest : ValidationRule
{
private int result;
private bool hadValue = false;
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
if (hadValue && (value == null || string.IsNullOrEmpty(value.ToString())))
{
return new ValidationResult(false, "Value cannot be empty.");
}
if (value.ToString().Length > 4)
{
hadValue = true;
return new ValidationResult(false, "Name cannot be more than 20 characters long.");
}
hadValue = true;
return ValidationResult.ValidResult;
}
}
Related
I'm learning how to use MVVM and how bind data inside a WPF App. I've created a custom CheckedListBox in XAML file this way:
<ListBox x:Name="materialsListBox" ItemsSource="{Binding CustomCheckBox}">
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding Path=IsChecked, UpdateSourceTrigger=PropertyChanged, Mode=OneWayToSource}" Content="{Binding Item}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
and also I want a single Image to dynamically show up for each CheckBox I check. I understand that I need to use Binding and UpdateSourceTrigger Property but I'm not sure how to realize this.
What should I add here so that my app does what I want?
<Image HorizontalAlignment="Left" Height="100" Margin="432,146,0,0" VerticalAlignment="Top" Width="100"/>
Here's a part of my C# code for the ViewModel:
public class MainViewModel : ViewModelBase
{
private ObservableCollection<CheckedListItem<string>> _customCheckBox;
public ObservableCollection<CheckedListItem<string>> CustomCheckBox
{
set
{
_customCheckBox = value;
OnPropertyChanged();
}
get { return _customCheckBox; }
}
public class CheckedListItem<T> : ViewModelBase
{
private bool _isChecked;
private T _item;
public CheckedListItem()
{
}
public CheckedListItem(T item, bool isChecked = false)
{
item = _item;
isChecked = _isChecked;
}
public T Item
{
set
{
_item = value;
OnPropertyChanged();
}
get { return _item; }
}
public bool IsChecked
{
set
{
_isChecked = value;
OnPropertyChanged();
}
get { return _isChecked; }
}
}
...
Thank you for any recommendation.
One eazy way to do ProperyChanged events is to use the base set for ViewModelBase this.Set because it will raise the changed event for you.
to do this I split up the view model and view in to 2, one for the main view and one for a view combining the check box and image. You can do it with one like you have but it was just easier for me.
View Model for the CheckBox and image
public class CheckBoxViewModel : ViewModelBase
{
private bool isChecked;
private string imageSource;
private string imageName;
public CheckBoxViewModel(string imageSource, string imageName)
{
this.ImageSource = imageSource;
this.ImageName = imageName;
}
public ICommand Checked => new RelayCommand<string>(this.OnChecked);
private void OnChecked(object imageName)
{
}
public string ImageSource
{
get { return this.imageSource; }
set { this.Set(() => this.ImageSource, ref this.imageSource, value); }
}
public string ImageName
{
get { return this.imageName; }
set { this.Set(() => this.ImageName, ref this.imageName, value); }
}
public bool IsChecked
{
get { return this.isChecked; }
set { this.Set(() => this.IsChecked, ref this.isChecked, value); }
}
}
Main Window View Model
public class MainViewModel : ViewModelBase
{
private ObservableCollection<CheckBoxViewModel> items = new ObservableCollection<CheckBoxViewModel>();
public ObservableCollection<CheckBoxViewModel> Items => this.items;
public MainViewModel()
{
var view = new CheckBoxViewModel("Image.Jpg", "Image 1");
this.Items.Add(view);
var view2 = new CheckBoxViewModel("Image2.Jpg", "Image 2");
this.Items.Add(view2);
}
}
Checkbox and image view
<UserControl.Resources>
<local:MainViewModel x:Key="MainViewModel" />
<local:MainViewModel x:Key="ViewModel" />
<local:BoolToVisibility x:Key="BoolToVisibility" />
</UserControl.Resources>
<Grid >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20*"/>
<ColumnDefinition Width="201*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<CheckBox Command="{Binding Checked}" HorizontalAlignment="Center" VerticalAlignment="Center" IsChecked="{Binding Path=IsChecked, UpdateSourceTrigger=PropertyChanged, Mode=OneWayToSource}" Content="{Binding ImageName}" />
<Image Grid.Column="1" Source="{Binding ImageSource}" HorizontalAlignment="Center" VerticalAlignment="Center" Visibility="{Binding IsChecked, Converter={StaticResource BoolToVisibility}}" />
</Grid>
Main View
<Window.Resources>
<local:MainViewModel x:Key="MainViewModel" />
<DataTemplate DataType="{x:Type local:CheckBoxViewModel}">
<local:view/>
</DataTemplate>
</Window.Resources>
<Grid>
<ListView DataContext="{StaticResource MainViewModel}" ItemsSource="{Binding Items}"/>
</Grid>
This way the main view model adds CheckBoxViewModels to its items and then the main view automatically adds the child view to the list view.
Whats notable is how the images visibility is flipped. I used a value converter that you add to the Images visibility Binding. It will convert a true false value to a type of Visibility.
public class BoolToVisibility : IValueConverter
{
/// <summary>
/// Converts a value.
/// </summary>
/// <param name="value">The value produced by the binding source.</param>
/// <param name="targetType">The type of the binding target property.</param>
/// <param name="parameter">The converter parameter to use.</param>
/// <param name="culture">The culture to use in the converter.</param>
/// <returns>A converted value. If the method returns null, the valid null value is used.</returns>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value != null)
{
if ((bool)value)
{
return Visibility.Visible;
}
else
{
return Visibility.Collapsed;
}
}
return Visibility.Collapsed;
}
/// <summary>
/// Converts a value.
/// </summary>
/// <param name="value">The value that is produced by the binding target.</param>
/// <param name="targetType">The type to convert to.</param>
/// <param name="parameter">The converter parameter to use.</param>
/// <param name="culture">The culture to use in the converter.</param>
/// <returns>A converted value. If the method returns null, the valid null value is used.</returns>
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return null;
}
}
I am trying to create an application using WPF. I am trying to fully build it using the MVVM model. However, I am puzzled on how to correctly display the error message. I thought it would be trivial step but seems to be the most complex.
I created the following view using xaml
<StackPanel Style="{StaticResource Col}">
<DockPanel>
<Grid DockPanel.Dock="Top">
<Grid.ColumnDefinitions >
<ColumnDefinition Width="*" ></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Style="{StaticResource Col}">
<Label Content="Name" Style="{StaticResource FormLabel}" />
<Border Style="{StaticResource FormInputBorder}">
<TextBox x:Name="Name" Style="{StaticResource FormControl}" Text="{Binding Name, ValidatesOnDataErrors=True, NotifyOnValidationError=True, UpdateSourceTrigger=PropertyChanged}" />
</Border>
</StackPanel>
<StackPanel Grid.Column="1" Style="{StaticResource Col}">
<Label Content="Phone Number" Style="{StaticResource FormLabel}" />
<Border Style="{StaticResource FormInputBorder}">
<TextBox x:Name="Phone" Style="{StaticResource FormControl}" Text="{Binding Phone, ValidatesOnDataErrors=True, NotifyOnValidationError=True, UpdateSourceTrigger=PropertyChanged}" />
</Border>
</StackPanel>
</Grid>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Style="{StaticResource PrimaryButton}" Command="{Binding Create}">Create</Button>
<Button>Reset</Button>
</StackPanel>
</DockPanel>
</StackPanel>
Then I created the following ViewModel
public class VendorViewModel : ViewModel
{
protected readonly IUnitOfWork UnitOfWork;
private string _Name { get; set; }
private string _Phone { get; set; }
public VendorViewModel()
: this(new UnitOfWork())
{
}
public VendorViewModel(IUnitOfWork unitOfWork)
{
UnitOfWork = unitOfWork;
}
[Required(ErrorMessage = "The name is required")]
[MinLength(5, ErrorMessage = "Name must be more than or equal to 5 letters")]
[MaxLength(50, ErrorMessage = "Name must be less than or equal to 50 letters")]
public string Name
{
get { return _Name; }
set
{
_Name = value;
NotifyPropertyChanged();
}
}
public string Phone
{
get { return _Phone; }
set
{
_Phone = value;
NotifyPropertyChanged();
}
}
/// <summary>
/// Gets the collection of customer loaded from the data store.
/// </summary>
public ICollection<Vendor> Vendors { get; private set; }
protected void AddVendor()
{
var vendor = new Vendor(Name, Phone);
UnitOfWork.Vendors.Add(vendor);
}
public ICommand Create
{
get
{
return new ActionCommand(p => AddVendor(),
p => IsValidRequest());
}
}
public bool IsValidRequest()
{
// There got to be a better way to check if everything passed or now...
return IsValid("Name") && IsValid("Phone");
}
}
Here is how my ViewModel base class look like
public abstract class ViewModel : ObservableObject, IDataErrorInfo
{
/// <summary>
/// Gets the validation error for a property whose name matches the specified <see cref="columnName"/>.
/// </summary>
/// <param name="columnName">The name of the property to validate.</param>
/// <returns>Returns a validation error if there is one, otherwise returns null.</returns>
public string this[string columnName]
{
get { return OnValidate(columnName); }
}
/// <summary>
/// Validates a property whose name matches the specified <see cref="propertyName"/>.
/// </summary>
/// <param name="propertyName">The name of the property to validate.</param>
/// <returns>Returns a validation error, if any, otherwise returns null.</returns>
protected virtual string OnValidate(string propertyName)
{
var context = new ValidationContext(this)
{
MemberName = propertyName
};
var results = new Collection<ValidationResult>();
bool isValid = Validator.TryValidateObject(this, context, results, true);
if (!isValid)
{
ValidationResult result = results.SingleOrDefault(p => p.MemberNames.Any(memberName => memberName == propertyName));
if (result != null)
return result.ErrorMessage;
}
return null;
}
protected virtual bool IsValid(string propertyName)
{
return OnValidate(propertyName) == null;
}
/// <summary>
/// Not supported.
/// </summary>
[Obsolete]
public string Error
{
get
{
throw new NotSupportedException();
}
}
}
Here is my ObservableObject class
public class ObservableObject : INotifyPropertyChanged
{
/// <summary>
/// Raised when the value of a property has changed.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Raises <see cref="PropertyChanged"/> for the property whose name matches <see cref="propertyName"/>.
/// </summary>
/// <param name="propertyName">Optional. The name of the property whose value has changed.</param>
protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
My goal is to show a red border around the incorrect field then display the error message right underneath it to tell the use what went wrong.
How do I show the error correctly? Also, how to I not show any error when the view is first loaded?
Base on this blog I need to edit the Validation.ErrorTemplate
So I tried adding the following code to the App.xaml file
<!-- Style the error validation by showing the text message under the field -->
<Style TargetType="TextBox">
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<StackPanel>
<Border BorderThickness="1" BorderBrush="DarkRed">
<StackPanel>
<AdornedElementPlaceholder x:Name="errorControl" />
</StackPanel>
</Border>
<TextBlock Text="{Binding AdornedElement.ToolTip, ElementName=errorControl}" Foreground="Red" />
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="BorderBrush" Value="Red" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}" />
</Trigger>
</Style.Triggers>
</Style>
But that isn't showing the error message, also when the view is first loaded I get an error. Finally, even when the form become valid, the action button stay disabled.
UPDATED
After moving the Property="Validation.ErrorTemplate" into the FormControl group it worked. However, the error message seems to be going over the buttons instead of pushing the buttons down. Also, the text does not seems to be wrapping vertically allowing the border to strach over the other control as you can see in the following screen shows.
I'll try to answer all your questions:
How do I show the error correctly?
The ErrorTemplate is not applied because the FormControl style on your TextBox has precedence over the style containing the Validation.ErrorTemplate. Moving the Validation.ErrorTemplate code into the FormControl style will fix this issue.
Also, how to I not show any error when the view is first loaded?
What is the use of a Required validation if it's not applied immediately? The MinLength and MaxLength validations will only be executed when you start typing in the field.
However, the error message seems to be going over the buttons instead of pushing the buttons down.
As Will pointed out, this is because the error messages are shown on the AdornerLayer, which cannot interfere with the layer your controls are on. You have the following options:
Use the AdornerLayer but leave some room between controls
Use a ToolTip to show the error messages
Use an extra TextBlock in the template of the TextBox that shows the error messages.
These options are described here
I am trying to display record in a DataGrid and for each row on the grid, I want to show a button to delete that record.
I added the button successfully to the dataGrid. However, when the view is loaded, the "delete" command is called for every record. In another word, I the confirm dialog appears 10 time because I have 10 records to display.
Questions
How to I prevent the command from being executed each time and only allow it to run on button click?
Also, how can I move the last 2 column all the way to the far right so they are vertically aligned?
In my ViewModel, I added the following command
public ICommand DeleteVendor
{
get
{
MessageBoxResult confirmation = MessageBox.Show("Are you sure?", "Delete Confirmation", MessageBoxButton.YesNo);
bool processDelete = (confirmation == MessageBoxResult.Yes);
return new ActionCommand(p => HandleDeleteVendor(), p => processDelete);
}
}
private void HandleDeleteVendor()
{
if (SelectedVendor == null)
{
throw new Exception("No vendor was selected");
}
UnitOfWork.Vendors.Remove(SelectedVendor);
UnitOfWork.Save();
}
Then in my view I added the following XAML code
<DataGrid ItemsSource="{Binding Vendors}"
SelectedItem="{Binding SelectedVendor}"
AutoGenerateColumns="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Name"
Binding="{Binding Name}" />
<DataGridTextColumn Header="Account Number"
Binding="{Binding AccountCode}" />
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button VerticalAlignment="Center"
Command="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}},
Path=DataContext.ShowVendor}">
<StackPanel Orientation="Horizontal">
<fa:FontAwesome Icon="Eye"
FontSize="18" />
<Label Content="Details"
Padding="7 0 0 0" />
</StackPanel>
</Button>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button VerticalAlignment="Center"
Command="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}},
Path=DataContext.DeleteVendor}">
<StackPanel Orientation="Horizontal">
<fa:FontAwesome Icon="Trash"
FontSize="18" />
<Label Content="Delete"
Padding="7 0 0 0" />
</StackPanel>
</Button>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
If needed here is my ICommand implementation
public sealed class ActionCommand : ICommand
{
private readonly Action<Object> Action;
private readonly Predicate<Object> Allowed;
/// <summary>
/// Initializes a new instance of the <see cref="ActionCommand"/> class.
/// </summary>
/// <param name="action">The <see cref="Action"/> delegate to wrap.</param>
public ActionCommand(Action<Object> action)
: this(action, null)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ActionCommand"/> class.
/// </summary>
/// <param name="action">The <see cref="Action"/> delegate to wrap.</param>
/// <param name="predicate">The <see cref="Predicate{Object}"/> that determines whether the action delegate may be invoked.</param>
public ActionCommand(Action<Object> action, Predicate<Object> allowed)
{
if (action == null)
{
throw new ArgumentNullException("action", "You must specify an Action<T>.");
}
Action = action;
Allowed = allowed;
}
/// <summary>
/// Defines the method that determines whether the command can execute in its current state.
/// </summary>
/// <returns>
/// true if this command can be executed; otherwise, false.
/// </returns>
/// <param name="parameter">Data used by the command. If the command does not require data to be passed, this object can be set to null.</param>
public bool CanExecute(object parameter)
{
if (Allowed == null)
{
return true;
}
return Allowed(parameter);
}
/// <summary>
/// Defines the method to be called when the command is invoked.
/// </summary>
/// <param name="parameter">Data used by the command. If the command does not require data to be passed, this object can be set to null.</param>
public void Execute(object parameter)
{
Action(parameter);
}
/// <summary>
/// Executes the action delegate without any parameters.
/// </summary>
public void Execute()
{
Execute(null);
}
/// <summary>
/// Occurs when changes occur that affect whether the command should execute.
/// </summary>
public event EventHandler CanExecuteChanged
{
add
{
if (Allowed != null)
{
CommandManager.RequerySuggested += value;
}
}
remove
{
if (Allowed != null)
{
CommandManager.RequerySuggested -= value;
}
}
}
/// <summary>
/// Raises the <see cref="CanExecuteChanged" /> event.
/// </summary>
public void RaiseCanExecuteChanged()
{
CommandManager.InvalidateRequerySuggested();
}
}
confirmation should be a part of delete method, not a part of command definition:
public ICommand DeleteVendor
{
get
{
return new ActionCommand(p => HandleDeleteVendor(), p => SelectedVendor != null);
}
}
private void HandleDeleteVendor()
{
MessageBoxResult confirmation = MessageBox.Show("Are you sure?", "Delete Confirmation", MessageBoxButton.YesNo);
if (confirmation != MessageBoxResult.Yes);
return;
if (SelectedVendor == null)
{
// IMO, it is better to handle this situation gracefully and show messageBox with a warning
throw new Exception("No vendor was selected");
}
UnitOfWork.Vendors.Remove(SelectedVendor);
UnitOfWork.Save();
}
Hello I am trying to set the value of a property of my ListBoxItem, just not sure how to use Binding in this case if someone could help me, I appreciate it since!
Below XAML
<ControlTemplate TargetType="controls:ModernVerticalMenu">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{TemplateBinding ListWidth}"/>
<ColumnDefinition Width="{TemplateBinding ListWidth}"/>
<ColumnDefinition Width="auto"/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Border Background="{TemplateBinding BackColor}" Height="{TemplateBinding Height}" BorderThickness="1" BorderBrush="{DynamicResource bordaSuperior}">
<!-- link list -->
<ListBox x:Name="LinkList" ItemsSource="{Binding Links, RelativeSource={RelativeSource TemplatedParent}}"
ScrollViewer.HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}" >
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Height="50" Background="Transparent" Width="500">
<Border Name="border" Padding="10">
<Path x:Name="icon" Data="{Binding IconData}" Stretch="Fill" Fill="{DynamicResource Accent}" Width="20" Height="20" HorizontalAlignment="Left" VerticalAlignment="Center" />
</Border>
<TextBlock x:Name="texto" ToolTip="{Binding Tooltip}" Text="{Binding DisplayName}" Margin="45,2,2,2" FontSize="{DynamicResource MediumFontSize}" TextTrimming="CharacterEllipsis" VerticalAlignment="Center" HorizontalAlignment="Left" />
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IconData}" Value="{x:Null}">
<Setter Property="Margin" TargetName="texto">
<Setter.Value>
<Thickness Bottom="2" Top="2" Left="10" Right="2"/>
</Setter.Value>
</Setter>
</DataTrigger>
<Trigger Property="IsMouseOver" Value="true">
<Trigger.Setters>
<Setter Property="Fill" TargetName="icon">
<Setter.Value>
<SolidColorBrush Color="#f2f2f2" />
</Setter.Value>
</Setter>
</Trigger.Setters>
</Trigger>
</DataTemplate.Triggers>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="ListBoxItem.IsMouseOver" SourceName="LinkList" Value="true">
<Trigger.Setters>
<Setter Property="SelectedLinkGroup" Value="{Binding Source=LinkList,Path=Children}"/>
</Trigger.Setters>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
In the code below I am trying to set the value SelectedLinkGroup From property to the value of Children referring to the ListBoxItem LinkList.
<Setter Property="SelectedLinkGroup" Value="{Binding Source=LinkList,Path=Children}"/>
using FirstFloor.ModernUI.Presentation;
using System;
using System.Data;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
namespace FirstFloor.ModernUI.Windows.Controls
{
/// <summary>
/// Represents a control that contains multiple pages that share the same space on screen.
/// </summary>
public class ModernVerticalMenu
: Control
{
/// <summary>
/// Identifies the ContentLoader dependency property.
/// </summary>
public static readonly DependencyProperty ContentLoaderProperty = DependencyProperty.Register("ContentLoader", typeof(IContentLoader), typeof(ModernVerticalMenu), new PropertyMetadata(new DefaultContentLoader()));
/// <summary>
/// Identifies the ListWidth dependency property.
/// </summary>
public static readonly DependencyProperty ListWidthProperty = DependencyProperty.Register("ListWidth", typeof(GridLength), typeof(ModernVerticalMenu), new PropertyMetadata(new GridLength(170)));
/// <summary>
/// Identifies the Links dependency property.
/// </summary>
public static readonly DependencyProperty LinksProperty = DependencyProperty.Register("Links", typeof(LinkCollection), typeof(ModernVerticalMenu), new PropertyMetadata(OnLinksChanged));
/// <summary>
/// Identifies the SelectedSource dependency property.
/// </summary>
public static readonly DependencyProperty SelectedSourceProperty = DependencyProperty.Register("SelectedSource", typeof(Uri), typeof(ModernVerticalMenu), new PropertyMetadata(OnSelectedSourceChanged));
/// <summary>
/// Defines the SelectedLinkGroup dependency property.
/// </summary>
public static readonly DependencyProperty SelectedLinkGroupProperty = DependencyProperty.Register("SelectedLinkGroup", typeof(LinkCollection), typeof(ModernVerticalMenu), new PropertyMetadata(OnSelectedLinkGroupChanged));
/// <summary>
/// Defines the SelectedLink dependency property.
/// </summary>
public static readonly DependencyProperty SelectedLinkProperty = DependencyProperty.Register("SelectedLink", typeof(Link), typeof(ModernVerticalMenu), new PropertyMetadata(OnSelectedLinkChanged));
/// <summary>
/// Defines the SelectedLink dependency property.
/// </summary>
public static readonly DependencyProperty BackColorProperty = DependencyProperty.Register("BackColor", typeof(SolidColorBrush), typeof(ModernVerticalMenu), new PropertyMetadata(null));
/// <summary>
/// Occurs when the selected source has changed.
/// </summary>
public event EventHandler<SourceEventArgs> SelectedSourceChanged;
private ListBox linkList;
/// <summary>
/// Initializes a new instance of the <see cref="ModernVerticalMenu"/> control.
/// </summary>
public ModernVerticalMenu()
{
this.DefaultStyleKey = typeof(ModernVerticalMenu);
// this.BackColor = new SolidColorBrush(Color.FromRgb(0,0,255));
// create a default links collection
SetCurrentValue(LinksProperty, new LinkCollection());
}
private static void OnSelectedLinkGroupChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
// retrieve the selected link from the group
var group = (LinkCollection)e.NewValue; // cria uma nova instancia do grupo
Link selectedLink = null; //cria um link selecionado
if (group != null)
{ //se o grupo copiado existe
selectedLink = group.FirstOrDefault();
}
// update the selected link
((ModernVerticalMenu)o).SetCurrentValue(SelectedLinkProperty, selectedLink);
}
private static void OnSelectedLinkChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
// update selected source
var newValue = (Link)e.NewValue;
Uri selectedSource = null;
if (newValue != null)
{
selectedSource = newValue.Source;
}
((ModernVerticalMenu)o).SetCurrentValue(SelectedSourceProperty, selectedSource);
}
private static void OnLinksChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
((ModernVerticalMenu)o).UpdateSelection();
}
private static void OnSelectedSourceChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
((ModernVerticalMenu)o).OnSelectedSourceChanged((Uri)e.OldValue, (Uri)e.NewValue);
}
private void OnSelectedSourceChanged(Uri oldValue, Uri newValue)
{
UpdateSelection();
// raise SelectedSourceChanged event
var handler = this.SelectedSourceChanged;
if (handler != null) {
handler(this, new SourceEventArgs(newValue));
}
}
private void UpdateSelection()
{
if (this.linkList == null || this.Links == null) {
return;
}
// sync list selection with current source
this.linkList.SelectedItem = this.Links.FirstOrDefault(l => l.Source == this.SelectedSource);
// SetValue(SelectedLinkGroupProperty, this.Links.FirstOrDefault(l => l.Children == this.SelectedLinkGroup));
if (this.Links.FirstOrDefault(l => l.Children == this.SelectedLinkGroup) != null) { }
}
/// <summary>
/// When overridden in a derived class, is invoked whenever application code or internal processes call System.Windows.FrameworkElement.ApplyTemplate().
/// </summary>
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
if (this.linkList != null) {
this.linkList.SelectionChanged -= OnLinkListSelectionChanged;
}
this.linkList = GetTemplateChild("LinkList") as ListBox;
if (this.linkList != null) {
this.linkList.SelectionChanged += OnLinkListSelectionChanged;
}
UpdateSelection();
}
private void OnLinkListSelectionChanged(object sender, SelectionChangedEventArgs e)
{
//
var link = this.linkList.SelectedItem as Link;
if (link != null && link.Source != this.SelectedSource)
{
SetCurrentValue(SelectedSourceProperty, link.Source);
SetCurrentValue(SelectedLinkGroupProperty, link.Children);
}
}
/// <summary>
/// Gets or sets the content loader.
/// </summary>
public IContentLoader ContentLoader
{
get { return (IContentLoader)GetValue(ContentLoaderProperty); }
set { SetValue(ContentLoaderProperty, value); }
}
/// <summary>
/// Gets or sets the collection of links that define the available content in this tab.
/// </summary>
public LinkCollection Links
{
get { return (LinkCollection)GetValue(LinksProperty); }
set { SetValue(LinksProperty, value); }
}
/// <summary>
/// Gets or sets the collection of links that define the available content in this tab.
/// </summary>
public LinkCollection SelectedLinkGroup
{
get { return (LinkCollection)GetValue(SelectedLinkGroupProperty); }
set { SetValue(SelectedLinkGroupProperty, value); }
}
/// <summary>
/// Gets or sets the collection of links that define the available content in this tab.
/// </summary>
public Link SelectedLink
{
get { return (Link)GetValue(SelectedLinkProperty); }
set { SetValue(SelectedLinkProperty, value); }
}
/// <summary>
/// Gets or sets the width of the list when Layout is set to List.
/// </summary>
/// <value>
/// The width of the list.
/// </value>
public GridLength ListWidth
{
get { return (GridLength)GetValue(ListWidthProperty); }
set { SetValue(ListWidthProperty, value); }
}
/// <summary>
/// Gets or sets the source URI of the selected link.
/// </summary>
/// <value>The source URI of the selected link.</value>
public Uri SelectedSource
{
get { return (Uri)GetValue(SelectedSourceProperty); }
set { SetValue(SelectedSourceProperty, value); }
}
/// <summary>
/// Gets or sets the source URI of the selected link.
/// </summary>
/// <value>The source URI of the selected link.</value>
public SolidColorBrush BackColor
{
get { return (SolidColorBrush)GetValue(BackColorProperty); }
set { SetValue(BackColorProperty, value); }
}
}
}
From my understanding of the code, you are trying to set the SelectedLinkGroup property to the items in your LinkList when your mouse is over a ListBoxItem.
As your LinkList's ItemsSource is bound to the Links property on your control you should be able to bind directly to this property to obtain the same result. The following code does just that.
Value="{Binding Links, RelativeSource={RelativeSource TemplatedParent}}"
Alternatively you could bind to the LinkList Items directly.
Value="{Binding ElementName=LinkList, Path=ItemsSource}"
Here you specify the name of the element to bind to and the property on that element.
I have a little puzzle I'm trying to solve and am not sure how to go about it...
WPF application based on MVVM approach...
I have a SubstituionDataSet class that inherits from DataSet and defines an additional collection:
namespace Lib
{
public class SubstitutionDataSet : DataSet
{
public SubstitutionDataSet()
{
TableNames = new ObservableCollection<SubstitutionDataTable>();
Tables.CollectionChanging += DataTablesCollectionChanging;
}
public ObservableCollection<SubstitutionDataTable> TableNames { get; set; }
private void DataTablesCollectionChanging(object sender, CollectionChangeEventArgs e)
{
var actionTable = (DataTable) e.Element;
if (e.Action == CollectionChangeAction.Add)
{
actionTable.Columns.CollectionChanged += DataColumnCollectionChanged;
TableNames.Add(new SubstitutionDataTable { Name = actionTable.TableName });
}
else if (e.Action == CollectionChangeAction.Remove)
{
actionTable.Columns.CollectionChanged -= DataColumnCollectionChanged;
TableNames.Remove(TableNames.First(tn => tn.Name == actionTable.TableName));
}
}
private void DataColumnCollectionChanged(object sender, CollectionChangeEventArgs e)
{
var actionColumn = (DataColumn) e.Element;
var hostTable = (DataTable) actionColumn.Table;
var hostSubsitutionTable = TableNames.First(tn => tn.Name == hostTable.TableName);
if (e.Action == CollectionChangeAction.Add)
{
hostSubsitutionTable.ColumnNames.Add(actionColumn.ColumnName);
}
else if (e.Action == CollectionChangeAction.Remove)
{
hostSubsitutionTable.ColumnNames.Remove(hostSubsitutionTable.ColumnNames.First(cn => cn == actionColumn.ColumnName));
}
}
}
}
With the SubstitutionDataTable defined as below:
namespace Lib
{
public sealed class SubstitutionDataTable: INotifyPropertyChanged
{
private string _name;
/// <summary>
/// The <see cref="Name" /> property's name.
/// </summary>
private const string NamePropertyName = "Name";
public SubstitutionDataTable()
{
ColumnNames = new ObservableCollection<string>();
}
/// <summary>
/// Gets the Name property.
/// Changes to that property's value raise the PropertyChanged event.
/// </summary>
public string Name
{
get
{
return _name;
}
set
{
if (_name == value)
{
return;
}
_name = value;
RaisePropertyChanged(NamePropertyName);
}
}
public ObservableCollection<string> ColumnNames { get; set; }
#region Implementation of INotifyPropertyChanged
/// <summary>
/// A property has changed - update bindings
/// </summary>
[field: NonSerialized]
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
}
...Now this is the crux of the puzzle...
The above classes are used to define a new DataTable within a DataSet and add columns and rows and run-time. I have another Class that allows configuration of an obfuscation process, part of the configuration allows selection of a DataTable and DataColumn from the SubstituionDataSet defined above.
namespace Lib
{
public class ObfuscationParams : INotifyPropertyChanged
{
private string _dataColumn;
private string _dataTable;
private char _maskCharacter;
private int _numberCharacters;
/// <summary>
/// The <see cref="MaskCharacter" /> property's name.
/// </summary>
private const string MaskCharacterPropertyName = "MaskCharacter";
/// <summary>
/// The <see cref="DataColumn" /> property's name.
/// </summary>
private const string DataColumnPropertyName = "DataColumn";
/// <summary>
/// The <see cref="DataTable" /> property's name.
/// </summary>
private const string DataTablePropertyName = "DataTable";
# region Mask Obfuscation Properties
/// <summary>
/// Defines whether whitespace is to be trimmed or not for a Mask obfuscation.
/// </summary>
public bool IsWhiteSpaceTrimmed { get; set; }
/// <summary>
/// Defines the mask character to be used for a Mask obfuscation.
/// </summary>
public char MaskCharacter
{
get { return _maskCharacter; }
set
{
if (_maskCharacter == value)
return;
_maskCharacter = value;
RaisePropertyChanged(MaskCharacterPropertyName);
}
}
/// <summary>
/// Defines the number of masking characters to apply.
/// </summary>
public int NumberCharacters
{
get { return _numberCharacters; }
set { _numberCharacters = value < 1 ? 1 : (value > 16 ? 16 : value); }
}
/// <summary>
/// Defines the mask position for a Mask obfuscation.
/// </summary>
public MaskPosition MaskPosition { get; set; }
#endregion
# region Substitute Obfuscation Properties
/// <summary>
/// Defines which datacolumn is to be used for a Substitution obfuscation.
/// </summary>
public string DataColumn
{
get { return _dataColumn; }
set
{
if (_dataColumn == value)
return;
_dataColumn = value;
RaisePropertyChanged(DataColumnPropertyName);
}
}
/// <summary>
/// Defines which datatable is to be used for a substitition obfuscation.
/// </summary>
public string DataTable
{
get { return _dataTable; }
set
{
if (_dataTable == value)
return;
_dataTable = value;
RaisePropertyChanged(DataTablePropertyName);
_dataTable = value;
}
}
#endregion
#region Implementation of INotifyPropertyChanged
/// <summary>
/// A property has changed - update bindings
/// </summary>
[field: NonSerialized]
public virtual event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
}
I have the configuration working and can configure a number of obfuscations and then serialize the configuration to disk.
When I deserialize I find the bindings on the GUI don't show the correct DataTable and DataColumn selections, the DataTable just shows the fully qualified object name.
I am currently just trying to get the DataTable binding working - I know I need to rework the DataColumn binding.
The GUI (usercontrol) is defined as below:
<UserControl xmlns:igEditors="http://infragistics.com/Editors" x:Class="SubstitutionOptions"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="421" d:DesignWidth="395">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="23" />
<RowDefinition Height="23" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150" />
<ColumnDefinition Width="20" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0"
Grid.Column="0"
Text="Dataset" />
<igEditors:XamComboEditor Grid.Row="0"
Grid.Column="2"
Name="tablesComboBox"
NullText="select a dataset..."
ItemsSource="{Binding DataContext.Project.SubstitutionDataSet.TableNames, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"
DisplayMemberPath="Name"
SelectedItem="{Binding DataContext.SelectedFieldSubstitutionDataTable, Mode=TwoWay, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"/>
<TextBlock Grid.Row="1"
Grid.Column="0"
Text="Column" />
<igEditors:XamComboEditor Grid.Row="1"
Grid.Column="2"
NullText="select a column..."
ItemsSource="{Binding DataContext.SelectedFieldSubstitutionDataTable.ColumnNames, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"
SelectedItem="{Binding DataColumn, Mode=TwoWay}"/>
</Grid>
</UserControl>
I hope I have explained the problem sufficiently. Has anyone got any ideas on how I can either get it working using the current design or redesign the approach to achieve what I need?
OK, think I've cracked it now, not that anyone seems interested :-)
I'll post the answer for posterity though...
I changed the bindings for the Comboboxes like so...
<TextBlock Grid.Row="0"
Grid.Column="0"
Text="Dataset" />
<igEditors:XamComboEditor Grid.Row="0"
Grid.Column="2"
NullText="select a dataset..."
ItemsSource="{Binding DataContext.VDOProject.SubstitutionDataSet.TableNames, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"
DisplayMemberPath="Name"
Text="{Binding DataTable, Mode=TwoWay}"
SelectedItem="{Binding DataContext.SelectedFieldSubstitutionDataTable, Mode=OneWayToSource, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"/>
<TextBlock Grid.Row="1"
Grid.Column="0"
Text="Column" />
<igEditors:XamComboEditor Grid.Row="1"
Grid.Column="2"
NullText="select a column..."
ItemsSource="{Binding DataContext.SelectedFieldSubstitutionDataTable.ColumnNames, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"
Text="{Binding DataColumn, Mode=TwoWay}" />