简体   繁体   中英

Should I create a lot of PropertyChangedEventHandler or test the PropertyChangedEventArgs?

So far my model implements INotifyPropertyChanged , and every property raises this event. Almost all ViewModels listen to these changes via the PropertyChangedEventHandler .

The problem is that this handler gets called for every change in the model, even if the property change is not important for the View.

One option is to check which property raised the event. However, I don not like the idea to test the PropertyName string. It requires the hard coding of the property name which I already avoided in the Model with calls like PropertyChanged.Notify(()=> PropertyName)

The second option I see is to implement for all my properties single event:

public event PropertyChangedEventHandler LayerChanged;
public event PropertyChangedEventHandler FieldChanged;
public event PropertyChangedEventHandler LinkDictionaryChanged;

....

What is the best practice? I would prefer the second option.

EDIT: I try to be more specific

My Model classes work like that:

  public bool IsFeatureLayer
        {
            get { return _isFeatureLayer; }
            set { PropertyChanged.ChangeAndNotify(ref _isFeatureLayer, value, () => IsFeatureLayer);}
        }

Or

  PropertyChanged.Notify(() => LinkDictionary);

So the question is not how to make the notification call safer, because I already use extension methods to do this without the string name of the property.

The question is how to find out who invoked the event without using a string.

 void _MCDAExtensionPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if(e.PropertyName.Equals("LinkDictionary"){
              //event handling
           }
        }

This is totally unsafe because the name of the Property in my model can change and I have to fix it on different places.

If you're targeting .NET 4.5, implementing INotifyPropertyChanged is a lot easier and safer with the new CallerMemberName attribute.

In short, the CallerMemberName attribute allows you to get the name of the calling member as a method parameter. This way, you can have something like this:

private string name;
public string Name
{
    get { return name; }
    set { SetProperty(ref name, value); }
}

private void SetProperty<T>(ref T field, T value, [CallerMemberName] string callerMemberName = "")
{
    // callerMemberName = "Name" (the property that called it).

    // Set the field value and raise PropertyChanged event.
}

You can see an example of how to use it here .

As for which option to choose - I believe that the difference in terms of execution time is negligible, as opposed to the coding overhead and code clutter (both in the code itself and with intellisense) you get for having an extra event for each property. I would definitely go with the first option.

EDIT:

Unfortunately, when handling the PropertyChanged event, you can only test against the PropertyName string, and there is no way to get that string in a way that remains consistent even when the property name changes. For dependency properties you have MyDependencyProperty.Name , but that's not applicable for regular properties.

Eventually, your options are either to use a different event for each property, or define a constant in the class defining the property that holds the property name, hoping that you'll remember to modify it when/if you change the property name. Assuming you don't have many classes that implement INotifyPropertyChanged where you attach a handler yourself, having an event for each property in those specific classes isn't that bad.

If I understand your question correctly, you could use something like this:

public static class PropertyChangedExtensions
{
    public static void RegisterPropertyHandler<T, TProperty>(this T obj, Expression<Func<T, TProperty>> propertyExpression, PropertyChangedEventHandler handlerDelegate)
        where T : class, INotifyPropertyChanged
    {
        if (obj == null) throw new ArgumentNullException("obj");

        var propertyName = GetPropertyName(propertyExpression);

        obj.PropertyChanged += (sender, args) =>
            {
                if (args.PropertyName == propertyName && handlerDelegate != null)
                    handlerDelegate(sender, args);
            };
    }

    public static void Notify<T>(this PropertyChangedEventHandler eventHandler, object sender, Expression<Func<T>> propertyExpression)
    {
        var handler = eventHandler;
        if (handler != null) handler(sender, new PropertyChangedEventArgs(GetPropertyName(propertyExpression)));
    }

    private static string GetPropertyName(LambdaExpression propertyExpression)
    {
        var memberExpression = propertyExpression.Body as MemberExpression;
        if (memberExpression == null)
        {
            var unaryExpression = propertyExpression.Body as UnaryExpression;
            if (unaryExpression == null) 
                throw new ArgumentException("Expression must be a UnaryExpression.", "propertyExpression");

            memberExpression = unaryExpression.Operand as MemberExpression;
        }

        if (memberExpression == null) 
            throw new ArgumentException("Expression must be a MemberExpression.", "propertyExpression");

        var propertyInfo = memberExpression.Member as PropertyInfo;
        if (propertyInfo == null) 
            throw new ArgumentException("Expression must be a Property.", "propertyExpression");

        return propertyInfo.Name;
    }
}

The RegisterPropertyHandler method allows you to register a handler for a specific property without using "magic strings". You use it like this:

public class PersonViewModel : INotifyPropertyChanged
{
    public PersonViewModel()
    {
        Address = new AddressViewModel();
        Address.RegisterPropertyHandler(a => a.ZipCode, ZipCodeChanged);
    }

    private AddressViewModel _address;

    public AddressViewModel Address
    {
        get { return _address; }
        set
        {
            _address = value;
            PropertyChanged.Notify(this, () => Address);
        }
    }

    private static void ZipCodeChanged(object sender, PropertyChangedEventArgs args)
    {
        // This will only be called when the 'ZipCode' property of 'Address' changes.
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

public class AddressViewModel : INotifyPropertyChanged
{
    private string _zipCode;

    public string ZipCode
    {
        get
        {
            return _zipCode;
        }
        set
        {
            _zipCode = value;
            PropertyChanged.Notify(this, () => ZipCode);
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

I see you already have a Notify extension method, so you would only need to add the RegisterPropertyHandler . At least this is a start :)

Just to your project extension method like this:

    public static string GetPropertyName<TObj,TRet>(this TObj obj, Expression<Func<TObj,TRet>> expression)
    {
        MemberExpression body = GetMemberExpression(expression);
        return body.Member.Name;
    }

This way you will have compile checking of Properties names, string with property name in expense of little performance. With this you can call:

PropertyChanged.Notify(this.GetPropetyName(t=>t.PropertyName))

It's not ideal but without strings it's hard to accomplish that.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM