At the moment i'm facing an ridiculous problem which i'm not able to fix
I wrote a little wrapper which wraps almost any Property and added one Property but i don't know how to pass the Validation through him to my XAML
Here is my code
XAML
<TextBox Height="23" HorizontalAlignment="Left" Margin="42,74,0,0" Name="textBox2" VerticalAlignment="Top" Width="120"
DataContext="{Binding TB2}"/>
<!-- this Style is be added to the parent of TextBox -->
<Style TargetType="{x:Type TextBox}">
<Setter Property="Text" Value="{Binding Value,UpdateSourceTrigger=PropertyChanged,ValidatesOnDataErrors=True}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsDirty}" Value="true">
<Setter Property="BorderBrush" Value="Orange"/>
</DataTrigger>
</Style.Triggers>
</Style>
ViewModel
public class vm : IDataErrorInfo, INotifyPropertyChanged
{
[Required]
[Range(4, 6)]
public string TB1 { get; set; }
[Required]
[Range(4, 6)]
public myWrapper TB2
{
get { return tb2; }
set{
tb2 = value;
OnPropertyChanged("TB2");
}
}
private myWrapper tb2;
public vm()
{
TB1 = "";
tb2 = new myWrapper("T");
}
#region IDataErrorInfo
private Dictionary<string, string> ErrorList = new Dictionary<string, string>();
public string Error { get { return getErrors(); } }
public string this[string propertyName] { get { return OnValidate(propertyName); } }
private string getErrors()
{
string Error = "";
foreach (KeyValuePair<string, string> error in ErrorList)
{
Error += error.Value;
Error += Environment.NewLine;
}
return Error;
}
protected virtual string OnValidate(string propertyName)
{
if (string.IsNullOrEmpty(propertyName))
throw new ArgumentException("Invalid property name", propertyName);
string error = string.Empty;
var value = this.GetType().GetProperty(propertyName).GetValue(this, null);
var results = new List<ValidationResult>(2);
var context = new ValidationContext(this, null, null) { MemberName = propertyName };
var result = Validator.TryValidateProperty(value, context, results);
if (!result)
{
var validationResult = results.First();
error = validationResult.ErrorMessage;
}
if (error.Length > 0)
{
if (!ErrorList.ContainsKey(propertyName))
ErrorList.Add(propertyName, error);
}
else
if (ErrorList.ContainsKey(propertyName))
ErrorList.Remove(propertyName);
return error;
}
#endregion //IDataErrorInfo
#region INotifyPropertyChanged
// Declare the event
public event PropertyChangedEventHandler PropertyChanged;
// Create the OnPropertyChanged method to raise the event
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
#endregion
}
myWrapper
public class myWrapper : INotifyPropertyChanged
{
private object currentValue;
private object currentOriginal;
public object Value
{
get { return currentValue; }
set
{
currentValue = value;
OnPropertyChanged("Value");
OnPropertyChanged("IsDirty");
}
}
public bool IsDirty
{
get { return !currentValue.Equals(currentOriginal); }
}
#region cTor
public myWrapper(object original)
{
currentValue = original;
currentOriginal = original.Copy(); // creates an deep Clone
}
#endregion
#region INotifyPropertyChanged
// Declare the event
public event PropertyChangedEventHandler PropertyChanged;
// Create the OnPropertyChanged method to raise the event
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
#endregion
}
i also tested IDataErrorInfo in myWrapper with no luck
Since your TextBox actually binds to the wrapper, you have to add IDataErrorInfo to the wrapper class. Now the question is how to connect validation logic between actual ViewModel and your wrapper.
As johndsamuels said you can pass a delegate into wrapper like this:
#region cTor
private string _propertyName;
private Func<string, string> _validationFunc;
public myWrapper(string propertyName, object original, Func<string, string> validationFunc)
{
_propertyName = propertyName;
_validationFunc = validationFunc;
currentValue = original;
currentOriginal = original.Copy(); // creates an deep Clone
}
#endregion
You need to pass the property name also since the actual ViewModel might validate several properties in the same method. In your actual ViewModel, you pass OnValidate method as delegate then it will be fine.
Now you will go into a dilemma about validation. You are using Data Annotations. RangeAttribute, for example , only can validate int, double or string. Since attribute can be defined only on type level at compile time, you even cannot dynamically pass these attributes into your wrapper. You can either write your custom attribute or use other validation mechanism such as Enterprise Library validation block.
Hope it can help.
I think you don't need to use wrapper to save a state. It would be better if you use some provider to save the state of model. For example, I wrote the provider that can save state of all public properties in dictionary, and then can restore it.
public interface IEntityStateProvider
{
void Save(object entity);
void Restore(object entity);
}
public class EntityStateProvider : IEntityStateProvider
{
#region Nested type: EditObjectSavedState
private class SavedState
{
#region Constructors
public SavedState(PropertyInfo propertyInfo, object value)
{
PropertyInfo = propertyInfo;
Value = value;
}
#endregion
#region Properties
public readonly PropertyInfo PropertyInfo;
public readonly object Value;
#endregion
}
#endregion
#region Fields
private static readonly Dictionary<Type, IList<PropertyInfo>> TypesToProperties =
new Dictionary<Type, IList<PropertyInfo>>();
private readonly Dictionary<object, List<SavedState>> _savedStates = new Dictionary<object, List<SavedState>>();
#endregion
#region Implementation of IEntityStateProvider
public void Save(object entity)
{
var savedStates = new List<SavedState>();
IList<PropertyInfo> propertyInfos = GetProperties(entity);
foreach (PropertyInfo propertyInfo in propertyInfos)
{
object oldState = propertyInfo.GetValue(entity, null);
savedStates.Add(new SavedState(propertyInfo, oldState));
}
_savedStates[entity] = savedStates;
}
public void Restore(object entity)
{
List<SavedState> savedStates;
if (!_savedStates.TryGetValue(entity, out savedStates))
throw new ArgumentException("Before call the Restore method you should call the Save method.");
foreach (SavedState savedState in savedStates)
{
savedState.PropertyInfo.SetValue(entity, savedState.Value, null);
}
_savedStates.Remove(entity);
}
#endregion
#region Methods
private static IList<PropertyInfo> GetProperties(object entity)
{
Type type = entity.GetType();
IList<PropertyInfo> list;
if (!TypesToProperties.TryGetValue(type, out list))
{
list = type.GetProperties()
.Where(info => info.CanRead && info.CanWrite)
.ToArray();
TypesToProperties[type] = list;
}
return list;
}
#endregion
}
Now all you need to do it's save state of your view-model before editing, and then if you need you can restore previous state of view-model.
public class vm : IDataErrorInfo, INotifyPropertyChanged
{
private readonly IEntityStateProvider _stateProvider;
public vm(IEntityStateProvider stateProvider)
{
_stateProvider = stateProvider;
_stateProvider.Save(this);
}
............
}
This is a simple example of code and you can change this code as you need.
UPDATE 0 You can extend the interface and add the HasChanges method:
public interface IEntityStateProvider
{
void Save(object entity);
void Restore(object entity);
bool HasChanges(object entity, string property);
}
Here the implementation:
public bool HasChanges(object entity, string property)
{
List<SavedState> list;
if (!_savedStates.TryGetValue(entity, out list))
throw new ArgumentException("Before call the HasChanges method you should call the Save method.");
SavedState savedState = list.FirstOrDefault(state => state.PropertyInfo.Name == property);
if (savedState == null)
return false;
object newValue = savedState.PropertyInfo.GetValue(entity);
return !Equals(newValue, savedState.Value);
}
In your view model you should implement the IDataErrorInfo as explicit, and create new indexer property that will be responsible for checking the changes.
public class vm : INotifyPropertyChanged, IDataErrorInfo
{
private readonly IEntityStateProvider _stateProvider;
private string _property;
public vm(IEntityStateProvider stateProvider)
{
_stateProvider = stateProvider;
Property = "";
_stateProvider.Save(this);
}
public string Property
{
get { return _property; }
set
{
if (value == _property) return;
_property = value;
OnPropertyChanged("Property");
OnPropertyChanged("Item[]");
}
}
public bool this[string propertyName]
{
get { return _stateProvider.HasChanges(this, propertyName); }
}
#region Implementation of IDataErrorInfo
string IDataErrorInfo.this[string columnName]
{
get
{
//Your logic here
return null;
}
}
string IDataErrorInfo.Error
{
get
{
//Your logic here
return null;
}
}
#endregion
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
And then you can write binding like this and it will work.
<TextBox Height="23" HorizontalAlignment="Left" Margin="42,74,0,0" Name="textBox2" VerticalAlignment="Top"
Width="120">
<TextBox.Resources>
<!-- this Style is be added to the parent of TextBox -->
<Style TargetType="{x:Type TextBox}">
<Setter Property="Text"
Value="{Binding Path=Property, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay, ValidatesOnDataErrors=True}" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=[Property], UpdateSourceTrigger=PropertyChanged}" Value="true">
<Setter Property="BorderBrush" Value="Orange" />
<Setter Property="BorderThickness" Value="2" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBox.Resources>
</TextBox>
This is just a rough example showing the essence of solutions that you can do without wrappers for properties.
UPDATE 1 To avoid the creation of new styles, you can add attached property like this:
public static class ExtendedProperties
{
public static readonly DependencyProperty IsDirtyProperty =
DependencyProperty.RegisterAttached("IsDirty", typeof(bool), typeof(ExtendedProperties), new PropertyMetadata(default(bool)));
public static void SetIsDirty(UIElement element, bool value)
{
element.SetValue(IsDirtyProperty, value);
}
public static bool GetIsDirty(UIElement element)
{
return (bool)element.GetValue(IsDirtyProperty);
}
}
And then write this XAML:
<Window.Resources>
<!-- this Style is be added to the parent of TextBox -->
<Style TargetType="{x:Type TextBox}">
<Style.Triggers>
<Trigger Property="internal:ExtendedProperties.IsDirty" Value="True">
<Setter Property="BorderBrush" Value="Orange" />
<Setter Property="BorderThickness" Value="2" />
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<TextBox Height="23" HorizontalAlignment="Left" Margin="42,74,0,0" Name="textBox2" VerticalAlignment="Top"
Width="120"
Text="{Binding Path=Property, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay, ValidatesOnDataErrors=True}"
internal:ExtendedProperties.IsDirty="{Binding Path=[Property], UpdateSourceTrigger=PropertyChanged}" />
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With