Назначить свойство ExpressionTree
Я играю с идеей передачи присвоения свойства методу в качестве дерева выражений. Этот метод будет вызывать выражение так, чтобы свойство было назначено правильно, а затем вынюхило имя свойства, которое было только что назначено, поэтому я могу поднять событие PropertyChanged. Идея состоит в том, что я хотел бы иметь возможность использовать тонкие авто-свойства в моих WPF ViewModels и все еще иметь событие 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 за отличный ответ. Я переместил одну маленькую вещь и превратил ее в метод расширения. Здесь полный образец кода с 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);
}
}
Ответы
Ответ 1
Вы не можете этого сделать. Во-первых, лямбда-выражения могут быть преобразованы только в типы делегирования или Expression<T>
.
Если вы измените подпись метода (теперь игнорируя его реализацию) до public void RunAndRaise(Expression<Action> Exp)
, компилятор жалуется, что "дерево выражений может не содержать оператор присваивания".
Вы можете сделать это, указав свойство, используя lambda, и значение, которое вы хотите установить в другом параметре. Кроме того, я не нашел способа получить доступ к значению vm
из выражения, поэтому вам придется поместить его в другой параметр (вы не можете использовать this
для этого, потому что вам нужно правильный унаследованный тип в выражении): см. 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);
}
Другая возможность (и одна из них мне больше нравится) состоит в том, чтобы поднять событие из сеттера, в частности, используя лямбда:
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);
}
Таким образом, вы можете использовать свойства, как обычно, и вы также можете создавать события для вычисленных свойств, если они у вас есть.
EDIT:. Просматривая серию Matt Warren о внедрении IQueryable<T>
, я понял, что могу получить доступ к ссылочным значение, что упрощает использование 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));
}
Ответ 2
Вот общее решение, которое может дать вам 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()();
Достаточно просто, не так ли?
Ответ 3
Общее решение для проблемы expression tree may not contain an assignment operator
- это создать локальный сеттер Action
и вызвать его с помощью 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);
Возможно, это не подходит для вашей конкретной проблемы, но я надеюсь, что это поможет кому-то, поскольку этот вопрос является наилучшим вариантом для поиска этого сообщения об ошибках.
Я не уверен, почему именно компилятор запрещает это.
Ответ 4
Возможно, будет означать Expression.Assign, который был добавлен в .net 4.0?