2013-06-18 19 views
7

W tej chwili jestem stoi to śmieszny problem, który nie jestem w stanie naprawićJak przekazać IDataErrorInfo Walidacja poprzez owinięcie do XAML

pisałem trochę owijkę, która otacza niemal dowolny obiekt i dodano jedną właściwość ale nie wiem jak przekazać zatwierdzaniu przez niego do mojego XAML

Oto mój kod

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 
} 

ja również testowane IDataErrorInfo w myWrapper bez powodzenia

+0

IDataErrorInfo w myWrapper powinien pracować – blindmeis

+0

@ blindmeis już go przetestowałem i tak nie jest, ponieważ 'Value' nie ma żadnych metadanych, więc zawsze będzie ważne, muszę uzyskać te informacje w jakiś sposób z mojego' vm' – WiiMaxx

+0

mam na myśli, że powinien działać od strony wpf - jeśli więc zaimplementujesz idataerrorinfo bez atrybutów w mywrapper do testowania - powinieneś zobaczyć walidację w wpf ui. afaik wpf - podczas wiązania z mywrapper - przechodzi do obiektu mywrapper, aby sprawdzić idataerrorinfo dla valdation. więc musisz go zaimplementować tam w dowolny sposób – blindmeis

Odpowiedz

2

Ponieważ TextBox rzeczywiście wiąże się z owinięcia, trzeba dodać IDataErrorInfo do klasa opakowania. Teraz pytanie brzmi: jak połączyć logikę walidacji między rzeczywistym ViewModel a twoim opakowaniem.

Jak johndsamuels że można przekazać delegata do owijki tak:

#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 

Trzeba przekazać nazwę właściwości również, ponieważ rzeczywista ViewModel może zatwierdzić kilka właściwości w tej samej metody. W twoim prawdziwym ViewModelu, przekazujesz metodę OnValidate jako delegata, wtedy wszystko będzie dobrze.

Teraz przejdziesz do dylematu dotyczącego sprawdzania poprawności. Używasz adnotacji danych. Na przykład funkcja RangeAttribute może sprawdzać poprawność int, double lub string. Ponieważ atrybut można zdefiniować tylko na poziomie typu podczas kompilacji, nie można dynamicznie przekazywać tych atrybutów do opakowania. Możesz napisać niestandardowy atrybut lub użyć innego mechanizmu sprawdzania poprawności, takiego jak blok sprawdzania poprawności w bibliotece korporacyjnej.

Mam nadzieję, że to pomoże.

2

Myślę, że nie trzeba używać opakowania, aby zapisać stan. Byłoby lepiej, gdybyś użył jakiegoś dostawcy, by zapisać stan modelu. Na przykład napisałem dostawcę, który może zapisać stan wszystkich właściwości publicznych w słowniku, a następnie może go przywrócić.

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 
} 

Teraz wszystko co musisz zrobić, to jest zapisać stan swojej widzenia model przed edycji, a następnie, jeśli trzeba można przywrócić stan poprzedni widzenia model.

public class vm : IDataErrorInfo, INotifyPropertyChanged 
{ 
    private readonly IEntityStateProvider _stateProvider; 

    public vm(IEntityStateProvider stateProvider) 
    { 
     _stateProvider = stateProvider; 
     _stateProvider.Save(this); 
    } 
    ............ 
} 

To jest prosty przykład kodu i możesz go zmienić w razie potrzeby.

UPDATE 0 można rozszerzyć interfejs i dodać HasChanges metody:

public interface IEntityStateProvider 
{ 
    void Save(object entity); 

    void Restore(object entity); 

    bool HasChanges(object entity, string property); 
} 

Tutaj realizacja:

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

w widoku modelu należy wdrożyć IDataErrorInfo jako jawne i utworzyć nową właściwość indeksu, która będzie odpowiedzialna za sprawdzanie zmian.

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

A następnie można napisać powiązanie w ten sposób i będzie działać.

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

To jest tylko przybliżony przykład pokazujący istotę rozwiązań, które można wykonać bez opakowania dla właściwości.

UPDATE 1 Aby uniknąć tworzenia nowych stylów, można dodać załączony właściwość tak:

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

A potem napisać 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}" /> 
+0

sry, ale nie widzę, jak to mi pomoże, ponieważ mógłbym użyć [IEditableObject] (http://msdn.microsoft.com/de-de/library/system.componentmodel.ieditableobject.aspx), aby osiągnąć to samo ale wciąż nie wie, która właściwość się zmieniła i ustawić powiązaną ramkę TextBox na pomarańczową – WiiMaxx

+0

Zaktualizowałem odpowiedź. –

+0

thx za twój czas man, ale to wciąż nie jest to, o co pytam, więc wyjaśnię to nieco więcej, sugerujemy, że masz 10 właściwości bindowanych do 10 pól tekstowych z twoją próbką, powtarzałbym pole tekstowe foreach XAML Syle i tylko ja trzeba dodać styl do rodzica (Grid, StackPanel, ...) – WiiMaxx