7

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

我写了一个小包装器,它包装了几乎所有属性并添加了一个属性,但我不知道如何通过他将验证传递给我的 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
}

我的包装器

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 没有运气

4

2 回答 2

2

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

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

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

UPDATE 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}" />
于 2013-06-29T15:24:01.777 回答
2

由于您的 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。由于只能在编译时在类型级别上定义属性,因此您甚至无法将这些属性动态传递到您的包装器中。您可以编写自定义属性或使用其他验证机制,例如企业库验证块。

希望它可以提供帮助。

于 2013-06-24T03:16:32.330 回答