Attribuer une propriété avec un ExpressionTree

c# expression-trees

Question

Je joue avec l'idée de passer une assignation de propriété à une méthode sous la forme d'un arbre d'expression. La méthode invoquait l'expression pour que la propriété soit affectée correctement, puis détectait le nom de la propriété qui venait d'être affectée afin que je puisse déclencher l'événement PropertyChanged. L'idée est que j'aimerais pouvoir utiliser des propriétés automatiques minces dans mes WPF ViewModels tout en maintenant le déclenchement de l'événement PropertyChanged.

Je suis un ignorant avec ExpressionTrees, j'espère donc que quelqu'un pourra me diriger dans la bonne direction:

public class ViewModelBase {
    public event Action<string> PropertyChanged = delegate { };

    public int Value { get; set; }

    public void RunAndRaise(MemberAssignment Exp) {
        Expression.Invoke(Exp.Expression);
        PropertyChanged(Exp.Member.Name);
    }
}

Le problème est que je ne sais pas comment appeler cela. Cette tentative naïve a été rejetée par le compilateur pour des raisons qui, j'en suis sûr, seront évidentes pour quiconque peut répondre à cette question:

        ViewModelBase vm = new ViewModelBase();

        vm.RunAndRaise(() => vm.Value = 1);

MODIFIER

Merci @svick pour la réponse parfaite. J'ai déplacé une petite chose et en ai fait une méthode d'extension. Voici l'exemple de code complet avec le test unitaire:

[TestClass]
public class UnitTest1 {
    [TestMethod]
    public void TestMethod1() {
        MyViewModel vm = new MyViewModel();
        bool ValuePropertyRaised = false;
        vm.PropertyChanged += (s, e) => ValuePropertyRaised = e.PropertyName == "Value";

        vm.SetValue(v => v.Value, 1);

        Assert.AreEqual(1, vm.Value);
        Assert.IsTrue(ValuePropertyRaised);
    }
}


public class ViewModelBase : INotifyPropertyChanged {
    public event PropertyChangedEventHandler PropertyChanged = delegate { };

    public void OnPropertyChanged(string propertyName) {
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

public class MyViewModel : ViewModelBase {
    public int Value { get; set; }
}

public static class ViewModelBaseExtension {
    public static void SetValue<TViewModel, TProperty>(this TViewModel vm, Expression<Func<TViewModel, TProperty>> exp, TProperty value) where TViewModel : ViewModelBase {
        var propertyInfo = (PropertyInfo)((MemberExpression)exp.Body).Member;
        propertyInfo.SetValue(vm, value, null);
        vm.OnPropertyChanged(propertyInfo.Name);
    }
}

Réponse acceptée

Vous ne pouvez pas le faire de cette façon. Premièrement, les expressions lambda peuvent être converties uniquement en types délégués ou Expression<T> .

Si vous modifiez la signature de la méthode (pour l'instant en ignorant son implémentation) en public void RunAndRaise(Expression<Action> Exp) , le compilateur se plaint de ce que «Une arborescence d'expression ne peut pas contenir d'opérateur d'affectation».

Vous pouvez le faire en spécifiant la propriété en utilisant lambda et la valeur que vous souhaitez définir dans un autre paramètre. De plus, je ne l' ai pas trouver un moyen d'accéder à la valeur de vm de l'expression, donc vous devez le mettre dans un autre paramètre (vous ne pouvez pas utiliser this pour cela, parce que vous avez besoin du bon type hérité dans l'expression) : voir éditer

public static void SetAndRaise<TViewModel, TProperty>(
    TViewModel vm, Expression<Func<TViewModel, TProperty>> exp, TProperty value)
    where TViewModel : ViewModelBase
{
    var propertyInfo = (PropertyInfo)((MemberExpression)exp.Body).Member;
    propertyInfo.SetValue(vm, value, null);
    vm.PropertyChanged(propertyInfo.Name);
}

Une autre possibilité (et celle que j’aime plus) est de relancer l’événement en utilisant spécifiquement lambda comme ceci:

private int m_value;
public int Value
{
    get { return m_value; }
    set
    {
        m_value = value;
        RaisePropertyChanged(this, vm => vm.Value);
    }
}

static void RaisePropertyChanged<TViewModel, TProperty>(
    TViewModel vm, Expression<Func<TViewModel, TProperty>> exp)
    where TViewModel : ViewModelBase
{
    var propertyInfo = (PropertyInfo)((MemberExpression)exp.Body).Member;
    vm.PropertyChanged(propertyInfo.Name);
}

De cette façon, vous pouvez utiliser les propriétés comme d'habitude et vous pouvez également déclencher des événements pour les propriétés calculées, si vous les aviez.

EDIT: En parcourant la série de Matt Warren sur l’implémentation d’ IQueryable<T> , j’ai réalisé que je pouvais accéder à la valeur référencée, ce qui simplifie l’utilisation de RaisePropertyChanged() (bien que cela n’aide pas beaucoup avec votre SetAndRaise() ):

private int m_value;
public int Value
{
    get { return m_value; }
    set
    {
        m_value = value;
        RaisePropertyChanged(() => Value);
    }
}

static void RaisePropertyChanged<TProperty>(Expression<Func<TProperty>> exp)
{
    var body = (MemberExpression)exp.Body;
    var propertyInfo = (PropertyInfo)body.Member;
    var vm = (ViewModelBase)((ConstantExpression)body.Expression).Value;
    vm.PropertyChanged(vm, new PropertyChangedEventArgs(propertyInfo.Name));
}

Réponse populaire

Voici une solution générique qui peut vous donner une Action à assigner à partir d'une expression pour spécifier le côté gauche de l'affectation, ainsi qu'une valeur à affecter.

public Expression<Action> Assignment<T>(Expression<Func<T>> lvalue, T rvalue)
{
    var body = lvalue.Body;
    var c = Expression.Constant(rvalue, typeof(T));
    var a = Expression.Assign(body, c);
    return Expression.Lambda<Action>(a);
}

avec cela, le code dans la question est simplement

ViewModelBase vm = new ViewModelBase();

vm.RunAndRaise(Assignment(() => vm.Value, 1));

Si vous changez la définition comme

public void RunAndRaise(Expression<Action> Exp) {
    Exp.Compile()();
    PropertyChanged(Exp.Member.Name);
}

On peut aussi dire

//Set the current thread name to "1234"
Assignment(() => Thread.CurrentThread.Name, "1234")).Compile()();

Assez simple, n'est-ce pas?



Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi
Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi