Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to pass IDataErrorInfo Validation through an wrapper to the XAML

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

like image 336
WiiMaxx Avatar asked Jun 18 '13 08:06

WiiMaxx


2 Answers

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.

like image 132
Bill Zhang Avatar answered Nov 04 '22 09:11

Bill Zhang


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}" />
like image 35
Vyacheslav Volkov Avatar answered Nov 04 '22 09:11

Vyacheslav Volkov