问题
There have been plenty of articles about how to use reflection and LINQ to raise PropertyChanged events in a type-safe way, without using strings.
But is there any way to consume PropertyChanged events in a type-safe manner? Currently, I'm doing this
void model_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case "Property1":
...
case "Property2":
...
....
}
}
Is there any way to avoid hard-coding strings in a switch statement to handle the different properties? Some similar LINQ- or reflection-based approach?
回答1:
Let’s declare a method that can turn a lambda expression into a Reflection PropertyInfo
object (taken from my answer here):
public static PropertyInfo GetProperty<T>(Expression<Func<T>> expr)
{
var member = expr.Body as MemberExpression;
if (member == null)
throw new InvalidOperationException("Expression is not a member access expression.");
var property = member.Member as PropertyInfo;
if (property == null)
throw new InvalidOperationException("Member in expression is not a property.");
return property;
}
And then let’s use it to get the names of the properties:
void model_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == GetProperty(() => Property1).Name)
{
// ...
}
else if (e.PropertyName == GetProperty(() => Property2).Name)
{
// ...
}
}
Unfortunately you can’t use a switch
statement because the property names are no longer compile-time constants.
回答2:
With C# 6.0 you can use nameof. You can also reference a class' property without creating an instance of that class.
void model_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case nameof(ClassName.Property1):
...
case nameof(ClassName.Property2):
...
....
}
}
回答3:
Josh Smith's MVVM Foundation includes a PropertyObserver class that does what you want.
回答4:
I avoid the switch by combining the command pattern and some Expression logic. You encapsulate the case-action in a command. I'll illustrate this using a Model View Controller structure. real world code - WinForms,but it is the same idea
the example loads a tree in a view, when the Tree property is set in the model.
a custom ICommand
void Execute();
string PropertyName { get; }
Concrete Command
public TreeChangedCommand(TreeModel model, ISelectTreeView selectTreeView,Expression<Func<object>> propertyExpression )
{
_model = model;
_selectTreeView = selectTreeView;
var body = propertyExpression.Body as MemberExpression;
_propertyName = body.Member.Name;
}
constructor controller
//handle notify changed event from model
_model.PropertyChanged += _model_PropertyChanged;
//init commands
commands = new List<ICommand>();
commands.Add(new TreeChangedCommand(_model,_mainView,()=>_model.Tree));
propertyChanged handler
void _model_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
//find the corresponding command and execute it. (instead of the switch)
commands.FirstOrDefault(cmd=>cmd.PropertyName.Equals(e.PropertyName)).Execute();
}
回答5:
A recent solution I have come up with is to encapsulate the event dispatch logic into a dedicated class.
The class has a public method called Handle
which has the same signature as the PropertyChangedEventHandler
delegate meaning it can be subscribed to the PropertyChanged
event of any class that implements the INotifyPropertyChanged
interface.
The class accepts delegates like the often used DelegateCommand
used by most WPF implementations meaning it can be used without having to create subclasses.
The class looks like this:
public class PropertyChangedHandler
{
private readonly Action<string> handler;
private readonly Predicate<string> condition;
private readonly IEnumerable<string> properties;
public PropertyChangedHandler(Action<string> handler,
Predicate<string> condition, IEnumerable<string> properties)
{
this.handler = handler;
this.condition = condition;
this.properties = properties;
}
public void Handle(object sender, PropertyChangedEventArgs e)
{
string property = e.PropertyName ?? string.Empty;
if (this.Observes(property) && this.ShouldHandle(property))
{
handler(property);
}
}
private bool ShouldHandle(string property)
{
return condition == null ? true : condition(property);
}
private bool Observes(string property)
{
return string.IsNullOrEmpty(property) ? true :
!properties.Any() ? true : properties.Contains(property);
}
}
You can then register a property changed event handler like this:
var eventHandler = new PropertyChangedHandler(
handler: p => { /* event handler logic... */ },
condition: p => { /* determine if handler is invoked... */ },
properties: new string[] { "Foo", "Bar" }
);
aViewModel.PropertyChanged += eventHandler.Handle;
The PropertyChangedHandler
takes care of checking the PropertyName
of the PropertyChangedEventArgs
and ensures that handler
is invoked by the right property changes.
Notice that the PropertyChangedHandler also accepts a predicate so that the handler delegate can be conditionally dispatched. The class also allows you to specify multiple properties so that a single handler can be bound to multiple properties in one go.
This can easily be extended using some extensions methods for more convenient handler registration which allows you to create the event handler and subscribe to the PropertyChanged
event in a single method call and specify the properties using expressions instead of strings to achieve something that looks like this:
aViewModel.OnPropertyChanged(
handler: p => handlerMethod(),
condition: p => handlerCondition,
properties: aViewModel.GetProperties(
p => p.Foo,
p => p.Bar,
p => p.Baz
)
);
This is basically saying that when either the Foo
, Bar
or Baz
properties change handlerMethod
will be invoked if handlerCondition
is true.
Overloads of the OnPropertychanged
method are provided to cover different event registration requirements.
If, for example, you want to register a handler that is called for any property changed event and is always executed you can simply do the following:
aViewModel.OnPropertyChanged(p => handlerMethod());
If, for example, you want to register a handler that is always executed but only for a single specific property change you can do the following:
aViewModel.OnPropertyChanged(
handler: p => handlerMethod(),
properties: aViewModel.GetProperties(p => p.Foo)
);
I have found this approach very useful when writing WPF MVVM applications. Imagine you have a scenario where you want to invalidate a command when any of three properties change. Using the normal method you would have to do something like this:
void PropertyChangedHandler(object sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case "Foo":
case "Bar":
case "Baz":
FooBarBazCommand.Invalidate();
break;
....
}
}
If you change the name of any of the viewModel properties you will need to remember to update the event handler to select the correct properties.
Using the PropertyChangedHandler
class specified above you can achieve the same result with the following:
aViewModel.OnPropertyChanged(
handler: p => FooBarBazCommand.Invalidate(),
properties: aViewModel.GetProperties(
p => p.Foo,
p => p.Bar,
p => p.Baz
)
);
This now has compile-time safety so If any of the viewModel properties are renamed the program will fail to compile.
来源:https://stackoverflow.com/questions/3668217/handling-propertychanged-in-a-type-safe-way