13 votes

Meilleure gestion des événements PropertyChanged et PropertyChanging

Je suis en train d'implémenter le modèle d'observateur pour notre application - je joue actuellement avec le RX Framework.

J'ai actuellement un exemple qui ressemble à ceci :

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

(J'en ai un similaire pour "PropertyChanging")

Les EventArgs ne me donnent pas grand-chose. Ce que j'aimerais, c'est une extension des EventArgs qui me donnerait la possibilité de voir les valeurs précédentes et nouvelles, ainsi que la possibilité de marquer l'événement dans l'écouteur "changing", de sorte que le changement ne persiste pas réellement. Comment cela est-il possible ? Je vous remercie.

24voto

Dr. Wily's Apprentice Points 5805

Je pense que cela dépend de la manière dont vous implémentez les interfaces INotifyPropertyChanging et INotifyPropertyChanged.

Les classes PropertyChangingEventArgs et PropertyChangedEventArgs ne fournissent malheureusement pas de valeur avant et après la propriété ni la possibilité d'annuler le changement, mais vous pouvez dériver vos propres classes d'args d'événements qui offrent cette fonctionnalité.

Commencez par définir les classes d'arguments d'événements suivantes. Notez qu'elles dérivent des classes PropertyChangingEventArgs et PropertyChangedEventArgs. Cela nous permet de transmettre ces objets en tant qu'arguments aux délégués PropertyChangingEventHandler et 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;
    }
}

Ensuite, vous devrez utiliser ces classes dans votre implémentation des interfaces INotifyPropertyChanging et INotifyPropertyChanged. Voici un exemple d'implémentation :

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

Notez que vos gestionnaires d'événements pour les événements PropertyChanging et PropertyChanged devront toujours prendre en paramètre les classes PropertyChangingEventArgs et PropertyChangedEventArgs d'origine, plutôt qu'une version plus spécifique. Toutefois, vous pourrez convertir les objets args de l'événement en types plus spécifiques afin d'accéder aux nouvelles propriétés.

Vous trouverez ci-dessous un exemple de gestionnaires d'événements pour ces événements :

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

2voto

Nicolas Dorier Points 4038

La réponse acceptée est vraiment mauvaise, vous pouvez le faire simplement avec 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

0voto

sacha Points 485

Pour ceux qui veulent le meilleur des deux RX et la possibilité d'annuler, voici un hybride de ces deux idées.

La classe de base ViewModel

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

Ensuite, j'ai ces deux extensions.

Un pour obtenir le nom de la propriété à partir de l'arbre d'expression

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

}

Et puis la partie 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; }
    }

}

L'exemple d'utilisation sera alors le suivant

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

Fonctionne très bien

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X