【问题标题】:How to pass IDataErrorInfo Validation through an wrapper to the XAML如何通过包装器将 IDataErrorInfo 验证传递给 XAML
【发布时间】:2013-06-18 08:09:45
【问题描述】:

目前我正面临一个我无法解决的荒谬问题

我写了一个小包装器,它包装了几乎所有属性并添加了一个属性,但我不知道如何通过他将验证传递给我的 XAML

这是我的代码

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>

视图模型

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
 }

我还在 myWrapper 中测试了 IDataErrorInfo,但没有成功

【问题讨论】:

  • myWrapper 中的 IDataErrorInfo 应该可以工作
  • @blindmeis 我已经测试过了,但它没有,因为Value 没有任何元数据,所以它总是有效的,我需要以某种方式从我的vm 获取这些信息
  • 我的意思是它应该从 wpf 端工作 - 所以如果你在 mywrapper 中实现 idataerrorinfo 而没有属性东西进行测试 - 你应该在 wpf ui 中看到验证。 afaik wpf - 绑定到 mywrapper 时 - 转到 mywrapper 对象以检查 idataerrorinfo 的验证。所以你必须以任何方式在那里实现它
  • 如上所述,IDataErrorInfo 必须在 myWrapper 中实现。但是,您可以使用委托将逻辑转移回 vm。
  • @johndsamuels 那么您将如何使用委托实现 IDataErrorInfo?

标签: c# validation xaml mvvm


【解决方案1】:

由于您的 TextBox 实际上绑定到包装器,因此您必须将 IDataErrorInfo 添加到包装器类。现在的问题是如何在实际 ViewModel 和包装器之间连接验证逻辑。

正如 johndsamuels 所说,您可以像这样将委托传递给包装器:

#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

您还需要传递属性名称,因为实际的 ViewModel 可能会在同一方法中验证多个属性。在您的实际 ViewModel 中,您将 OnValidate 方法作为委托传递就可以了。

现在您将陷入验证的两难境地。您正在使用数据注释。例如 RangeAttribute 只能验证 int、double 或 string。由于只能在编译时在类型级别上定义属性,因此您甚至无法将这些属性动态传递到您的包装器中。您可以编写自定义属性或使用其他验证机制,例如企业库验证块。

希望对你有帮助。

【讨论】:

    【解决方案2】:

    我认为您不需要使用包装器来保存状态。如果你使用一些提供者来保存模型的状态会更好。比如我写的provider可以将所有公共属性的状态保存在字典中,然后可以恢复。

    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
    }
    

    现在您需要做的就是在编辑之前保存视图模型的状态,然后如果需要,您可以恢复视图模型的先前状态。

    public class vm : IDataErrorInfo, INotifyPropertyChanged
    {
        private readonly IEntityStateProvider _stateProvider;
    
        public vm(IEntityStateProvider stateProvider)
        {
            _stateProvider = stateProvider;
            _stateProvider.Save(this);
        }
        ............
    }
    

    这是一个简单的代码示例,您可以根据需要更改此代码。

    更新 0 您可以扩展接口并添加 HasChanges 方法:

    public interface IEntityStateProvider
    {
        void Save(object entity);
    
        void Restore(object entity);
    
        bool HasChanges(object entity, string property);
    }
    

    这里是实现:

    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);
    }
    

    在您的视图模型中,您应该显式实现 IDataErrorInfo,并创建负责检查更改的新索引器属性。

    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));
        }
    }
    

    然后你可以像这样编写绑定,它会起作用。

    <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>
    

    这只是一个粗略的示例,展示了您可以在没有属性包装器的情况下执行的解决方案的本质。

    更新 1 为避免创建新样式,您可以像这样添加附加属性:

    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);
        }
    }
    

    然后编写这个 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}" />
    

    【讨论】:

    • sry 但我看不出这对我有什么帮助,因为我可以使用 IEditableObject 来实现相同的效果,但仍然不知道哪个属性已更改并将绑定的 TextBox 边框设置为橙色
    • 感谢您的时间,但这仍然不是我要问的,所以我会再解释一下让我们建议您将 10 个属性绑定到您的示例的 10 个文本框我会重复 XAML 样式foreach 文本框和我只需要将样式添加到父级(Grid、StackPanel、...)
    • 好的,我用 2 个文本框对其进行测试,但如果我更改一个文本(不管哪个),两个边框都会更改,但我只希望编辑为橙色
    • HasChanges 在仅更改一个属性后被调用为所有属性
    • 是的,它确实对所有人进行了检查,我认为此检查不会花费很长时间。您可以使用 IValueConverter 更改检查的实施。
    猜你喜欢
    • 2018-08-29
    • 1970-01-01
    • 1970-01-01
    • 2020-07-12
    • 2021-05-16
    • 1970-01-01
    • 2019-08-28
    • 1970-01-01
    • 2016-12-29
    相关资源
    最近更新 更多