2011-12-20 16 views
12

Sto implementando il modello di osservatore per la nostra applicazione, attualmente in uso con RX Framework.Miglioramento della gestione degli eventi PropertyChanged e PropertyChanging

Al momento ho un esempio che assomiglia a questo:

Observable.FromEventPattern<PropertyChangedEventArgs>(Instance.Address, "PropertyChanged") 
    .Where(e => e.EventArgs.PropertyName == "City") 
    .ObserveOn(Scheduler.ThreadPool) 
    .Subscribe(search => OnNewSearch(search.EventArgs)); 

(Ho un uno simile per "PropertyChanging")

Le EventArgs non mi danno molto. Quello che mi piacerebbe è un'estensione di EventArgs che mi dia la possibilità di vedere i valori precedenti e nuovi, così come la possibilità di contrassegnare l'evento nel listener 'cambiante', in modo tale che la modifica non possa effettivamente persistere. Come si può fare? Grazie.

+0

Vedere [questo] (https: //github.com/dotnet/corefx/issues/19627) – Shimmy

risposta

22

Penso che dipenda dal modo in cui si implementano le interfacce INotifyPropertyChanging e INotifyPropertyChanged.

Le classi PropertyChangingEventArgs e PropertyChangedEventArgs purtroppo non forniscono un valore precedente e successivo della proprietà o la possibilità di annullare la modifica, ma è possibile derivare le proprie classi di argomenti evento che forniscono tale funzionalità.

Innanzitutto, definire le seguenti classi di argomenti dell'argomento. Si noti che questi derivano dalla classe PropertyChangingEventArgs e dalla classe PropertyChangedEventArgs. Questo ci permette di passare questi oggetti come argomenti ai delegati PropertyChangingEventHandler e PropertyChangedEventHandler.

class PropertyChangingCancelEventArgs : PropertyChangingEventArgs 
{ 
    public bool Cancel { get; set; } 

    public PropertyChangingCancelEventArgs(string propertyName) 
     : base(propertyName) 
    { 
    } 
} 

class PropertyChangingCancelEventArgs<T> : PropertyChangingCancelEventArgs 
{ 
    public T OriginalValue { get; private set; } 

    public T NewValue { get; private set; } 

    public PropertyChangingCancelEventArgs(string propertyName, T originalValue, T newValue) 
     : base(propertyName) 
    { 
     this.OriginalValue = originalValue; 
     this.NewValue = newValue; 
    } 
} 

class PropertyChangedEventArgs<T> : PropertyChangedEventArgs 
{ 
    public T PreviousValue { get; private set; } 

    public T CurrentValue { get; private set; } 

    public PropertyChangedEventArgs(string propertyName, T previousValue, T currentValue) 
     : base(propertyName) 
    { 
     this.PreviousValue = previousValue; 
     this.CurrentValue = currentValue; 
    } 
} 

Avanti, si avrebbe bisogno di utilizzare queste classi nell'implementazione delle interfacce INotifyPropertyChanging e INotifyPropertyChanged. Un esempio di implementazione è la seguente:

class Example : INotifyPropertyChanging, INotifyPropertyChanged 
{ 
    public event PropertyChangingEventHandler PropertyChanging; 

    public event PropertyChangedEventHandler PropertyChanged; 

    protected bool OnPropertyChanging<T>(string propertyName, T originalValue, T newValue) 
    { 
     var handler = this.PropertyChanging; 
     if (handler != null) 
     { 
      var args = new PropertyChangingCancelEventArgs<T>(propertyName, originalValue, newValue); 
      handler(this, args); 
      return !args.Cancel; 
     } 
     return true; 
    } 

    protected void OnPropertyChanged<T>(string propertyName, T previousValue, T currentValue) 
    { 
     var handler = this.PropertyChanged; 
     if (handler != null) 
      handler(this, new PropertyChangedEventArgs<T>(propertyName, previousValue, currentValue)); 
    } 

    int _ExampleValue; 

    public int ExampleValue 
    { 
     get { return _ExampleValue; } 
     set 
     { 
      if (_ExampleValue != value) 
      { 
       if (this.OnPropertyChanging("ExampleValue", _ExampleValue, value)) 
       { 
        var previousValue = _ExampleValue; 
        _ExampleValue = value; 
        this.OnPropertyChanged("ExampleValue", previousValue, value); 
       } 
      } 
     } 
    } 
} 

nota, i gestori di eventi per la PropertyChanging e PropertyChanged eventi sarà ancora bisogno di prendere la classe PropertyChangingEventArgs originale e di classe PropertyChangedEventArgs come parametri, piuttosto che una versione più specifica. Tuttavia, sarà possibile eseguire il cast degli oggetti args degli eventi con tipi più specifici per accedere alle nuove proprietà.

Di seguito è un esempio di gestori di eventi per questi eventi:

class Program 
{ 
    static void Main(string[] args) 
    { 
     var exampleObject = new Example(); 

     exampleObject.PropertyChanging += new PropertyChangingEventHandler(exampleObject_PropertyChanging); 
     exampleObject.PropertyChanged += new PropertyChangedEventHandler(exampleObject_PropertyChanged); 

     exampleObject.ExampleValue = 123; 
     exampleObject.ExampleValue = 100; 
    } 

    static void exampleObject_PropertyChanging(object sender, PropertyChangingEventArgs e) 
    { 
     if (e.PropertyName == "ExampleValue") 
     { 
      int originalValue = ((PropertyChangingCancelEventArgs<int>)e).OriginalValue; 
      int newValue = ((PropertyChangingCancelEventArgs<int>)e).NewValue; 

      // do not allow the property to be changed if the new value is less than the original value 
      if(newValue < originalValue) 
       ((PropertyChangingCancelEventArgs)e).Cancel = true; 
     } 

    } 

    static void exampleObject_PropertyChanged(object sender, PropertyChangedEventArgs e) 
    { 
     if (e.PropertyName == "ExampleValue") 
     { 
      int previousValue = ((PropertyChangedEventArgs<int>)e).PreviousValue; 
      int currentValue = ((PropertyChangedEventArgs<int>)e).CurrentValue; 
     } 
    } 
} 
+0

Questo è estremamente utile, grazie mille! – user981225

+0

Sarebbe possibile implementare questo usando un DynamicProxy, per evitare tutto quel codice nei metodi setter? – user981225

+0

@ user981225 - Non ho familiarità con DynamicProxy (come in [Castle] (http://www.castleproject.org/dynamicproxy/index.html)?), Ma suppongo che intendi adottare un approccio di AOP o di policy injection . Sono d'accordo con la tua linea di pensiero e cercherò sicuramente un meccanismo (AOP, iniezione di politiche o altro) per consolidare quel codice di settaggio della proprietà in modo da non avere molti codici ridondanti. Tuttavia, non ho alcun meccanismo particolare da consigliare. –

2

La risposta accettata è davvero male, si può fare semplicemente con Buffer().

Observable.FromEventPattern<PropertyChangedEventArgs>(Instance.Address, "PropertyChanged") 
    .Where(e => e.EventArgs.PropertyName == "City") 
    .Buffer(2,1) //Take 2 events at a time, every 1 event 
    .ObserveOn(Scheduler.ThreadPool) 
    .Subscribe(search => ...); //search[0] is old value, search[1] is new value 
+2

Mmm ... un paio di problemi: 1) Gli eventi built-in PropertyChanged/PropertyChanging non forniscono i valori nuovi o originali della proprietà, che sarebbero necessari per far funzionare questo approccio "Buffer". 2) Questo approccio richiede che l'evento venga attivato più volte, in modo da poter confrontare il valore della proprietà mentre cambia tra due eventi. L'OP potrebbe voler essere in grado di osservare i valori precedenti e attuali della proprietà solo per un singolo evento. 3) L'OP vuole essere in grado di annullare la modifica al valore della proprietà. L'evento PropertyChanging integrato non fornisce un meccanismo di annullamento. –

0

Per tutti coloro che non vogliono il meglio di entrambi RX e di essere in grado di annullare ecco un ibrido di entrambe queste idee

La roba ViewModel classe base

public abstract class INPCBase : INotifyPropertyChanged, INotifyPropertyChanging 
{ 
    public event PropertyChangingEventHandler PropertyChanging; 

    public event PropertyChangedEventHandler PropertyChanged; 

    protected bool OnPropertyChanging<T>(string propertyName, T originalValue, T newValue) 
    { 
     var handler = this.PropertyChanging; 
     if (handler != null) 
     { 
      var args = new PropertyChangingCancelEventArgs<T>(propertyName, originalValue, newValue); 
      handler(this, args); 
      return !args.Cancel; 
     } 
     return true; 
    } 

    protected void OnPropertyChanged<T>(string propertyName, T previousValue, T currentValue) 
    { 
     var handler = this.PropertyChanged; 
     if (handler != null) 
      handler(this, new PropertyChangedEventArgs<T>(propertyName, previousValue, currentValue)); 
    } 
} 


public class PropertyChangingCancelEventArgs : PropertyChangingEventArgs 
{ 
    public bool Cancel { get; set; } 

    public PropertyChangingCancelEventArgs(string propertyName) 
     : base(propertyName) 
    { 
    } 
} 

public class PropertyChangingCancelEventArgs<T> : PropertyChangingCancelEventArgs 
{ 
    public T OriginalValue { get; private set; } 

    public T NewValue { get; private set; } 

    public PropertyChangingCancelEventArgs(string propertyName, T originalValue, T newValue) 
     : base(propertyName) 
    { 
     this.OriginalValue = originalValue; 
     this.NewValue = newValue; 
    } 
} 

public class PropertyChangedEventArgs<T> : PropertyChangedEventArgs 
{ 
    public T PreviousValue { get; private set; } 

    public T CurrentValue { get; private set; } 

    public PropertyChangedEventArgs(string propertyName, T previousValue, T currentValue) 
     : base(propertyName) 
    { 
     this.PreviousValue = previousValue; 
     this.CurrentValue = currentValue; 
    } 
} 

Poi ho queste estensioni di coppia.

uno per ottenere il nome della proprietà da albero di espressione

public static class ExpressionExtensions 
{ 

    public static string GetPropertyName<TProperty>(this Expression<Func<TProperty>> expression) 
    { 
     var memberExpression = expression.Body as MemberExpression; 
     if (memberExpression == null) 
     { 
      var unaryExpression = expression.Body as UnaryExpression; 
      if (unaryExpression != null) 
      { 
       if (unaryExpression.NodeType == ExpressionType.ArrayLength) 
        return "Length"; 
       memberExpression = unaryExpression.Operand as MemberExpression; 

       if (memberExpression == null) 
       { 
        var methodCallExpression = unaryExpression.Operand as MethodCallExpression; 
        if (methodCallExpression == null) 
         throw new NotImplementedException(); 

        var arg = (ConstantExpression)methodCallExpression.Arguments[2]; 
        return ((MethodInfo)arg.Value).Name; 
       } 
      } 
      else 
       throw new NotImplementedException(); 

     } 

     var propertyName = memberExpression.Member.Name; 
     return propertyName; 

    } 

    public static string GetPropertyName<T, TProperty>(this Expression<Func<T, TProperty>> expression) 
    { 
     var memberExpression = expression.Body as MemberExpression; 

     if (memberExpression == null) 
     { 
      var unaryExpression = expression.Body as UnaryExpression; 

      if (unaryExpression != null) 
      { 
       if (unaryExpression.NodeType == ExpressionType.ArrayLength) 
        return "Length"; 
       memberExpression = unaryExpression.Operand as MemberExpression; 

       if (memberExpression == null) 
       { 
        var methodCallExpression = unaryExpression.Operand as MethodCallExpression; 
        if (methodCallExpression == null) 
         throw new NotImplementedException(); 

        var arg = (ConstantExpression)methodCallExpression.Arguments[2]; 
        return ((MethodInfo)arg.Value).Name; 
       } 
      } 
      else 
       throw new NotImplementedException(); 
     } 
     var propertyName = memberExpression.Member.Name; 
     return propertyName; 

    } 

    public static String PropertyToString<R>(this Expression<Func<R>> action) 
    { 
     MemberExpression ex = (MemberExpression)action.Body; 
     return ex.Member.Name; 
    } 

    public static void CheckIsNotNull<R>(this Expression<Func<R>> action, string message) 
    { 
     MemberExpression ex = (MemberExpression)action.Body; 
     string memberName = ex.Member.Name; 
     if (action.Compile()() == null) 
     { 
      throw new ArgumentNullException(memberName, message); 
     } 
    } 

} 

E poi la parte Rx

public static class ObservableExtensions 
{ 

    public static IObservable<ItemPropertyChangingEvent<TItem, TProperty>> ObserveSpecificPropertyChanging<TItem, TProperty>(
     this TItem target, Expression<Func<TItem, TProperty>> propertyName) where TItem : INotifyPropertyChanging 
    { 
     var property = propertyName.GetPropertyName(); 

     return ObserveSpecificPropertyChanging(target, property) 
       .Select(i => new ItemPropertyChangingEvent<TItem, TProperty>() 
       { 
        OriginalEventArgs = (PropertyChangingCancelEventArgs<TProperty>)i.OriginalEventArgs, 
        Property = i.Property, 
        Sender = i.Sender 
       }); 
    } 

    public static IObservable<ItemPropertyChangingEvent<TItem>> ObserveSpecificPropertyChanging<TItem>(
     this TItem target, string propertyName = null) where TItem : INotifyPropertyChanging 
    { 

     return Observable.Create<ItemPropertyChangingEvent<TItem>>(obs => 
     { 
      Dictionary<string, PropertyInfo> properties = new Dictionary<string, PropertyInfo>(); 
      PropertyChangingEventHandler handler = null; 

      handler = (s, a) => 
      { 
       if (propertyName == null || propertyName == a.PropertyName) 
       { 
        PropertyInfo prop; 
        if (!properties.TryGetValue(a.PropertyName, out prop)) 
        { 
         prop = target.GetType().GetProperty(a.PropertyName); 
         properties.Add(a.PropertyName, prop); 
        } 
        var change = new ItemPropertyChangingEvent<TItem>() 
        { 
         Sender = target, 
         Property = prop, 
         OriginalEventArgs = a, 
        }; 

        obs.OnNext(change); 
       } 
      }; 

      target.PropertyChanging += handler; 

      return() => 
      { 
       target.PropertyChanging -= handler; 
      }; 
     }); 
    } 



    public class ItemPropertyChangingEvent<TSender> 
    { 
     public TSender Sender { get; set; } 
     public PropertyInfo Property { get; set; } 
     public PropertyChangingEventArgs OriginalEventArgs { get; set; } 

     public override string ToString() 
     { 
      return string.Format("Sender: {0}, Property: {1}", Sender, Property); 
     } 
    } 


    public class ItemPropertyChangingEvent<TSender, TProperty> 
    { 
     public TSender Sender { get; set; } 
     public PropertyInfo Property { get; set; } 
     public PropertyChangingCancelEventArgs<TProperty> OriginalEventArgs { get; set; } 
    } 

} 

Poi esempio di utilizzo sarà così

public class MainWindowViewModel : INPCBase 
{ 
    private string field1; 
    private string field2; 


    public MainWindowViewModel() 
    { 
     field1 = "Hello"; 
     field2 = "World"; 

     this.ObserveSpecificPropertyChanging(x => x.Field2) 
      .Subscribe(x => 
      { 
       if (x.OriginalEventArgs.NewValue == "DOG") 
       { 
        x.OriginalEventArgs.Cancel = true; 
       } 
      }); 

    } 

    public string Field1 
    { 
     get 
     { 
      return field1; 
     } 
     set 
     { 
      if (field1 != value) 
      { 
       if (this.OnPropertyChanging("Field1", field1, value)) 
       { 
        var previousValue = field1; 
        field1 = value; 
        this.OnPropertyChanged("Field1", previousValue, value); 
       } 
      } 
     } 
    } 


    public string Field2 
    { 
     get 
     { 
      return field2; 
     } 
     set 
     { 
      if (field2 != value) 
      { 
       if (this.OnPropertyChanging("Field2", field2, value)) 
       { 
        var previousValue = field2; 
        field2 = value; 
        this.OnPropertyChanged("Field2", previousValue, value); 
       } 
      } 
     } 
    } 
} 

funziona a meraviglia

Problemi correlati