Handling PropertyChanged in a type-safe way
Asked Answered
E

5

9

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?

Eckard answered 8/9, 2010 at 13:30 Comment(0)
F
4

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.

Feld answered 8/9, 2010 at 13:36 Comment(1)
(Though frankly, if you really want proper type safety, maybe you should consider not using PropertyChangedEventArgs at all and instead declaring one of your own that contains the PropertyInfo object instead of a string.)Feld
K
10

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):
            ...

        ....               
    }
}
Krystinakrystle answered 23/9, 2016 at 9:20 Comment(1)
In my opinion, this answer is better and, most importantly, easier to solve the problem.Reagent
F
4

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.

Feld answered 8/9, 2010 at 13:36 Comment(1)
(Though frankly, if you really want proper type safety, maybe you should consider not using PropertyChangedEventArgs at all and instead declaring one of your own that contains the PropertyInfo object instead of a string.)Feld
H
2

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.

Hickory answered 12/5, 2015 at 20:33 Comment(0)
S
1

Josh Smith's MVVM Foundation includes a PropertyObserver class that does what you want.

Shirley answered 8/9, 2010 at 13:41 Comment(2)
His code still uses strings to pass the property name, he simply adds a verification to check that that is a valid property name: if (TypeDescriptor.GetProperties(this)[propertyName] == null)Violoncellist
+ 1 for pointing to a great set of articles - great code and well put together explanationsVioloncellist
L
1

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();
}
Lardaceous answered 20/12, 2010 at 10:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.