Assegna proprietà con un albero di espressione

c# expression-trees

Domanda

Sto giocando con l'idea di passare un incarico di proprietà a un metodo come un albero di espressioni. Il metodo richiama l'espressione in modo che la proprietà venga assegnata correttamente, quindi annusa il nome della proprietà appena assegnato in modo da poter generare l'evento PropertyChanged. L'idea è che mi piacerebbe essere in grado di utilizzare proprietà automatiche sottili nei miei WPF ViewModels e far sparire l'evento PropertyChanged.

Sono un ignorante con ExpressionTrees, quindi spero che qualcuno possa indicarmi la giusta direzione:

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

Il problema è che non sono sicuro di come chiamare questo. Questo ingenuo tentativo è stato rifiutato dal compilatore per motivi che sono sicuro sarà ovvio per chiunque possa rispondere a questo:

        ViewModelBase vm = new ViewModelBase();

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

MODIFICARE

Grazie @svick per la risposta perfetta. Ho spostato una piccola cosa e l'ho trasformata in un metodo di estensione. Ecco il codice completo con test unitario:

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

Risposta accettata

Non puoi farlo in questo modo. Innanzitutto, le espressioni lambda possono essere convertite solo in tipi delegati o Expression<T> .

Se si modifica la firma del metodo (per ora ignorando la sua implementazione) su public void RunAndRaise(Expression<Action> Exp) , il compilatore si lamenta che "Un albero delle espressioni non può contenere un operatore di assegnazione".

Puoi farlo specificando la proprietà usando lambda e il valore in cui vuoi impostarlo in un altro parametro. Inoltre, non ho capire un modo per accedere al valore di vm dall'espressione, quindi bisogna mettere che in un altro parametro (non è possibile utilizzare this per questo, perché è necessario il tipo ereditato corretta nell'espressione) : vedi modifica

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

Un'altra possibilità (e una che mi piace di più) è di sollevare l'evento dal setter usando specificamente lambda come questo:

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

In questo modo, puoi utilizzare le proprietà come al solito e potresti anche generare eventi per le proprietà calcolate, se le avessi.

EDIT: Durante la lettura della serie di Matt Warren sull'implementazione di IQueryable<T> , mi sono reso conto che posso accedere al valore di riferimento, che semplifica l'utilizzo di RaisePropertyChanged() (sebbene non sia di grande aiuto con il tuo 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));
}

Risposta popolare

Ecco una soluzione generica che può darti Action per l'assegnazione da un'espressione per specificare il lato sinistro dell'assegnamento e un valore da assegnare.

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

con questo, il codice nella domanda è semplicemente

ViewModelBase vm = new ViewModelBase();

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

Se cambi la definizione come

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

Possiamo anche dire

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

Abbastanza semplice, no?



Autorizzato sotto: CC-BY-SA with attribution
Non affiliato con Stack Overflow
È legale questo KB? Sì, impara il perché
Autorizzato sotto: CC-BY-SA with attribution
Non affiliato con Stack Overflow
È legale questo KB? Sì, impara il perché