一个微不足道的问题,但我在这里画了一个空白,似乎无法在网上找到答案。
基本上,我正在尝试创建一个方法,该方法将属于INotifyPropertyChanged
类的 2 个属性作为参数(要在反射中使用的实际属性,而不是属性值),并像绑定一样使它们保持"同步"。
例
我有一个名为 Student
的类,其中包含一个名为 int SemesterScore
的属性。我有另一个名为 Semester
的类,其属性名为 int Score
。这两个类都实现了IPropertyNotifyChanged
.
现在,让我们暂时假设我们不能扩展任何类(就像在我的现实生活中一样),并且我可能在不同的类中
多次使用它。基本上,我希望能够在我的一个类中调用一个方法,该方法将两个属性"链接"在一起......也就是说,如果其中一个发生更改,它将自动更新另一个。
在非工作代码中,这是基本概念:
public class Student : INotifyPropertyChanged
{
private int _semesterScore;
public int SemeseterScore
{
get { return _semesterScore; }
set { [ set property stuff with property changed] }
}
}
public class Semester: INotifyPropertyChanged
{
private int _score;
public int Score
{
get { return _score; }
set { [ set property stuff with property changed] }
}
}
public class Entry
{
public static void Main(string[] args)
{
Student student = new Student();
Semester semester = new Semester();
AttachProperties(student.SemesterScore, semester.Score); // This obviously won't work, but this is where I pass the properties in
semester.Score = 7;
Console.WriteLine(student.SemesterScore); // Output will be 7
}
public static void AttachProperties([sometype] prop1, [sometype] prop2)
{
// Sudo code
prop1.classInstance.PropertyChanged += (pe)
{
if (pe.Property == prop1.Name)
prop2.Value = prop1.Value;
}
prop2.classInstance.PropertyChanged += (pe)
{
if (pe.Property == prop2.Name)
prop1.Value = prop2.Value;
}
}
}
有什么办法可以做到这一点吗?我知道一些解决方法(又名传递 INotifyPropertyChanged 类和属性名称,然后进行一些反思以使其工作),但是传递属性实例(并使用它做一些事情)的问题在我的编码生涯中出现了几次。
一种方法是使用Observable
,就像上面建议的@itay-podhacer一样。
但是,如果您想仅使用反射来实现,INotifyPropertyChanged
这里是您可以做到的。
首先,让我们了解SemesterScore
,Student
都实现INotifyPropertyChanged
:
public class Student : INotifyPropertyChanged
{
private int semesterScore;
public int SemesterScore
{
get { return semesterScore; }
set
{
semesterScore = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class Semester : INotifyPropertyChanged
{
private int score;
public int Score
{
get { return score; }
set
{
score = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
现在,让我们在 AttachProperties
帮助程序方法中将属性绑定在一起。为此,我们将使 AttachProperties 方法采用Expression<Func<T,object>
参数,以便避免传递魔术字符串,并可以使用反射来检索属性名称。
顺便说一下,要在生产中运行它,您可能希望记住该反射代码以提高性能。
private static void AttachProperties<T1,T2>(Expression<Func<T1, object>> property1, T1 instance1, Expression<Func<T2, object>> property2, T2 instance2)
where T1 : INotifyPropertyChanged
where T2 : INotifyPropertyChanged
{
var p1 = property1.GetPropertyInfo();
var p2 = property2.GetPropertyInfo();
//A NULL or empty PropertyName in PropertyChangeEventArgs means that all properties changed
//See: https://msdn.microsoft.com/en-us/library/system.componentmodel.inotifypropertychanged.propertychanged(v=vs.110).aspx#Anchor_1
((INotifyPropertyChanged)instance1).PropertyChanged += (_, e) =>
{
if (e.PropertyName == p1.Name || string.IsNullOrEmpty(e.PropertyName))
{
SyncProperties(p1, p2, instance1, instance2);
}
};
((INotifyPropertyChanged)instance2).PropertyChanged += (_, e) =>
{
if (e.PropertyName == p2.Name || string.IsNullOrEmpty(e.PropertyName))
{
SyncProperties(p2, p1, instance2, instance1);
}
};
}
private static void SyncProperties(PropertyInfo sourceProperty, PropertyInfo targetProperty, object sourceInstance, object targetInstance)
{
var sourceValue = sourceProperty.GetValue(sourceInstance);
var targetValue = targetProperty.GetValue(targetInstance);
if (!sourceValue.Equals(targetValue))
{
targetProperty.SetValue(targetInstance, sourceValue);
}
}
最后,下面是从参数中检索PropertyInfo
的反射代码:
public static class ReflectionExtension
{
public static PropertyInfo GetPropertyInfo<T>(this Expression<Func<T, object>> expression)
{
var memberExpression = GetMemberExpression(expression);
return (PropertyInfo)memberExpression.Member;
}
private static MemberExpression GetMemberExpression<TModel, T>(Expression<Func<TModel, T>> expression)
{
MemberExpression memberExpression = null;
if (expression.Body.NodeType == ExpressionType.Convert)
{
var body = (UnaryExpression)expression.Body;
memberExpression = body.Operand as MemberExpression;
}
else if (expression.Body.NodeType == ExpressionType.MemberAccess)
{
memberExpression = expression.Body as MemberExpression;
}
if (memberExpression == null)
{
throw new ArgumentException("Not a member access", "expression");
}
return memberExpression;
}
}
完成所有这些操作后,您现在可以保持两个属性同步:
public class PropertySyncTests
{
public void Should_sync_properties()
{
var semester = new Semester();
var student = new Student();
AttachProperties(x => x.Score, semester, x => x.SemesterScore, student);
semester.Score = 7;
student.SemesterScore.ShouldBe(7);
}
}
我知道一些解决方法(又名传递 INotifyPropertyChanged 类和属性名称,然后进行一些反思以使其工作),但是传递属性实例(并使用它做一些事情)的问题在我的编码生涯中出现了几次。
归根结底,这是做到这一点的方法。但是,我认为您可能不知道的一个关键技巧是表达式树。可以创建一个将Expression<Func<T>>
作为参数的函数,然后深入研究表达式树以发现INotifyPropertyChanged
实例和参数中给定的属性。用法可能如下所示:
AttachProperties(() => student.SemesterScore, () => semester.Score);
在上面的示例中要AttachProperties
的参数将具有以下结构Expression
。
<LambdaExpression> () => student.SemesterScore
Body <MemberExpression> student.SemesterScore
Member <PropertyInfo> SemesterScore
Expression <MemberExpression> student
Member <FieldInfo> [closure class.]student
Expression <ConstantExpression> [closure]
Value [closure instance]
请注意,您正在使用 lambda 表达式中的 student
创建闭包,因此要获取 student
的值,您需要使用反射来获取[closure class].student
字段的值。获取 SemesterScore
属性只需正确强制转换表达式并从传入的 lambda 表达式中获取 .Body.Member
属性即可。
本学期PropertyChanged
活动:
Student student = new Student();
Semester semester = new Semester();
semester.PropertyChanged += Semester_PropertyChanged;
然后将新分数分配给学生
private void Semester_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
student.SemesterScore = semester.Score;
}
这将触发学生的PropertyChanged
事件,并在分数发生变化时更新他的SemesterScore
。
看看反应式 UI 扩展,它将使您能够"观察"属性的更改,然后在发生此类更改时进行更新(或执行任何您想要的操作)。
它将允许您执行以下操作:
student
.WhenAnyValue(item => item.SemeseterScore)
.Subscribe(item =>
{
semester.Score = item.SemeseterScore
});
semester
.WhenAnyValue(item => item.Score)
.Subscribe(item =>
{
item.SemeseterScore = semester.Score
});
您可能需要在类中添加一个 Ignore
标志,并在 Subscribe
代码中打开和关闭它,这样就不会在两个类之间创建无限的更新循环。
所以我将@StriplingWarrior和@Pedro的答案结合在一起,得到了我的最终结果:
public static void AttachProperties<T1, T2>(Expression<Func<T1>> property1, Expression<Func<T2>> property2)
{
var instance1 = Expression.Lambda<Func<object>>(((MemberExpression)property1.Body).Expression).Compile()();
var iNotify1 = instance1 as INotifyPropertyChanged;
var prop1 = GetPropertyInfo(property1);
var instance2 = Expression.Lambda<Func<object>>(((MemberExpression)property2.Body).Expression).Compile()();
var iNotify2 = instance2 as INotifyPropertyChanged;
var prop2 = GetPropertyInfo(property2);
AttachProperty(prop1, iNotify1, prop2, iNotify2);
AttachProperty(prop2, iNotify2, prop1, iNotify1);
}
static void AttachProperty(
PropertyInfo property1,
INotifyPropertyChanged class1Instance,
PropertyInfo property2,
INotifyPropertyChanged class2Instance)
{
class2Instance.PropertyChanged += (_, propArgs) =>
{
if (propArgs.PropertyName == property2.Name || string.IsNullOrEmpty(propArgs.PropertyName))
{
var prop = property2.GetValue(class2Instance);
property1.SetValue(class1Instance, prop);
}
};
}
static PropertyInfo GetPropertyInfo<T1>(Expression<Func<T1>> property)
{
MemberExpression expression = null;
if (property.Body.NodeType == ExpressionType.Convert)
{
var body = (UnaryExpression)property.Body;
expression = body.Operand as MemberExpression;
}
else if (property.Body.NodeType == ExpressionType.MemberAccess)
{
expression = property.Body as MemberExpression;
}
if (expression == null)
{
throw new ArgumentException("Not a member access", nameof(property));
}
return expression.Member as PropertyInfo;
}
这在我给出的示例和我的现实生活中的项目中都正确工作。谢谢!