使用表达式树分配属性

发布于 2024-11-03 12:37:07 字数 1987 浏览 1 评论 0原文

我正在考虑将属性分配作为表达式树传递给方法的想法。该方法将调用表达式,以便正确分配属性,然后嗅出刚刚分配的属性名称,以便我可以引发 PropertyChanged 事件。我的想法是,我希望能够在 WPF ViewModel 中使用精简的自动属性,并且仍然触发 PropertyChanged 事件。

我对ExpressionTrees一无所知,所以我希望有人能指出我正确的方向:

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

问题是我不知道如何称呼它。这种天真的尝试被编译器拒绝了,原因我确信对于任何能够回答这个问题的人来说都是显而易见的:

        ViewModelBase vm = new ViewModelBase();

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

编辑

谢谢@svick 的完美答案。我移动了一个小东西并将其变成了一个扩展方法。这是带有单元测试的完整代码示例:

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

I'm playing around with the idea of passing a property assignment to a method as an expression tree. The method would Invoke the expression so that the property gets assigned properly, and then sniff out the property name that was just assigned so I can raise the PropertyChanged event. The idea is that I'd like to be able to use slim auto-properties in my WPF ViewModels and still have the PropertyChanged event fired off.

I'm an ignoramus with ExpressionTrees, so I'm hoping someone can point me in the right 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);
    }
}

The problem is I'm not sure how to call this. This naive attempt was rejected by the compiler for reasons that I'm sure will be obvious to anyone who can answer this:

        ViewModelBase vm = new ViewModelBase();

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

EDIT

Thank you @svick for the perfect answer. I moved one little thing around and made it into an extension method. Here's the complete code sample with unit test:

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

酸甜透明夹心 2024-11-10 12:37:07

你不能这样做。首先,lambda 表达式只能转换为委托类型或Expression

如果您将该方法的签名(暂时忽略其实现)更改为 public void RunAndRaise(ExpressionExp),编译器会抱怨“表达式树可能不包含赋值运算符”。

您可以通过使用 lambda 指定属性以及要在另一个参数中将其设置为的值来完成此操作。 此外,我没有找到从表达式访问 vm 值的方法,因此您必须将其放入另一个参数中(您不能使用 this< /code> 为此,因为您需要在表达式中使用正确的继承类型)参见编辑

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

另一种可能性(也是我更喜欢的一种)是专门使用 lambda 从 setter 引发事件像这样:

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

这样,您可以像往常一样使用属性,并且还可以引发计算属性的事件(如果有的话)。

编辑:在阅读Matt Warren 关于实现 IQueryable的系列文章,我意识到我可以访问引用的值,这简化了 RaisePropertyChanged() 的使用(尽管它赢得了对您的 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));
}

You can't do it this way. First, lambda expressions can be converted only to delegate types or Expression<T>.

If you change the signature of the method (for now ignoring its implementation) to public void RunAndRaise(Expression<Action> Exp), the compiler complains that “An expression tree may not contain an assignment operator”.

You could do it by specifying the property using lambda and the value you want to set it to in another parameter. Also, I didn't figure out a way to access the value of vm from the expression, so you have to put that in another parameter (you can't use this for that, because you need the proper inherited type in the expression):see edit

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

Another possibility (and one I like more) is to raise the event from setter specifically using lambda like this:

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

This way, you can use the properties as usual, and you could also raise events for computed properties, if you had them.

EDIT: While reading through Matt Warren's series about implementing IQueryable<T>, I realized I can access the referenced value, which simplifies the usage of RaisePropertyChanged() (although it won't help much with your 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));
}
偷得浮生 2024-11-10 12:37:07

这是一个通用解决方案,它可以为您提供一个从表达式进行赋值的 Action 来指定赋值的左侧,以及要赋值的值。

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

有了这个,问题中的代码就是简单的

ViewModelBase vm = new ViewModelBase();

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

如果你改变定义,就像

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

我们也可以说

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

足够简单,不是吗?

Here is a generaic solution that can give you an Action for assignment from an expression to specify the left hand side of assignment, and a value to assign.

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

with this, the code in the question is simply

ViewModelBase vm = new ViewModelBase();

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

If you change the definition like

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

We can also say

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

Simple enough, isn't it?

佼人 2024-11-10 12:37:07

对于表达式树可能不包含赋值运算符问题的通用解决方案是创建一个本地setterAction并通过Expression调用它:

// raises "An expression tree may not contain an assignment operator"
Expression<Action<string>> setterExpression1 = value => MyProperty = value;

// works
Action<string> setter = value => MyProperty = value;
Expression<Action<string>> setterExpression2 = value => setter(value);

这可能不适合您的具体问题,但我希望这对某人有帮助,因为这个问题是谷歌搜索该错误消息的最佳匹配。

我不确定为什么编译器不允许这样做。

A general solution for the expression tree may not contain an assignment operator issue would be to create a local setter Action and call that one by the Expression:

// raises "An expression tree may not contain an assignment operator"
Expression<Action<string>> setterExpression1 = value => MyProperty = value;

// works
Action<string> setter = value => MyProperty = value;
Expression<Action<string>> setterExpression2 = value => setter(value);

This may not be suited to your exact problem, but I'm hoping this helps someone as this question is kind of the best match for googling that error message.

I'm unsure why exactly the compiler disallows this.

我只土不豪 2024-11-10 12:37:07

您可能是指 .net 4.0 中添加的 Expression.Assign 吗?

May be u mean Expression.Assign which was added in .net 4.0?

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文