Asignar propiedad con un ExpressionTree

c# expression-trees

Pregunta

Estoy jugando con la idea de pasar una asignación de propiedad a un método como un árbol de expresión. El método Invocaría la expresión para que la propiedad se asigne correctamente, y luego rastrear el nombre de la propiedad que se acaba de asignar para que pueda levantar el evento PropertyChanged. La idea es que me gustaría poder usar propiedades automáticas delgadas en mis modelos de visualización de WPF y aún tener el evento PropertyChanged activado.

Soy un ignorante con ExpressionTrees, así que espero que alguien pueda orientarme en la dirección correcta:

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

El problema es que no estoy seguro de cómo llamar a esto. Este ingenuo intento fue rechazado por el compilador por razones que estoy seguro que serán obvias para cualquiera que pueda responder esto:

        ViewModelBase vm = new ViewModelBase();

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

EDITAR

Gracias @svick por la respuesta perfecta. Moví una cosita y la convertí en un método de extensión. Aquí está el ejemplo de código completo con prueba de unidad:

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

Respuesta aceptada

No puedes hacerlo de esta manera. Primero, las expresiones lambda solo se pueden convertir a tipos delegados o Expression<T> .

Si cambia la firma del método (por ahora ignorando su implementación) a public void RunAndRaise(Expression<Action> Exp) , el compilador se queja de que "Un árbol de expresiones puede no contener un operador de asignación".

Puede hacerlo especificando la propiedad utilizando lambda y el valor que desea establecer en otro parámetro. Además, no encontré una forma de acceder al valor de vm desde la expresión, por lo que tiene que poner eso en otro parámetro (no puede usar this para eso, porque necesita el tipo heredado adecuado en la expresión) : ver editar

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

Otra posibilidad (y una que me gusta más) es plantear el evento desde el setter específicamente utilizando lambda de esta manera:

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 esta manera, puede usar las propiedades como de costumbre y también podría generar eventos para las propiedades computadas, si las tuviera.

EDITAR: Mientras leía la serie de Matt Warren sobre la implementación de IQueryable<T> , me di cuenta de que puedo acceder al valor de referencia, lo que simplifica el uso de RaisePropertyChanged() (aunque no ayudará mucho con su 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));
}

Respuesta popular

Aquí hay una solución genérica que puede darle una Action para la asignación de una expresión para especificar el lado izquierdo de la asignación y un valor para asignar.

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 esto, el código en la pregunta es simplemente

ViewModelBase vm = new ViewModelBase();

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

Si cambia la definición como

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

También podemos decir

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

Bastante simple, ¿no es así?



Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow
¿Es esto KB legal? Sí, aprende por qué
Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow
¿Es esto KB legal? Sí, aprende por qué