Avoid calling RaisePropertyChanged in every setter

两盒软妹~` 提交于 2019-12-22 08:35:05

问题


I want to get rid of the space consuming and repetitive RaisePropertyChanged-Properties on my model classes. I want my model class...

public class ProductWorkItem : NotificationObject
{
    private string name;
    public string Name
    {
        get { return name; }
        set { 
            if (value == name) return; 
            name = value; RaisePropertyChanged(() => Name); 
        }
    }
    private string description;
    public string Description
    {
        get { return description; }
        set { 
            if (value == description) return; 
            description = value; RaisePropertyChanged(() => Description); 
        }
    }
    private string brand;
    public string Brand
    {
        get { return brand; }
        set { 
            if (value == brand) return; 
            brand = value; RaisePropertyChanged(() => Brand); 
        }
    }
}

...to look as simple as this again: (but notify the view when a property changes)

public class ProductWorkItem
{
    public string Name{ get; set; }
    public string Description{ get; set; }
    public string Brand{ get; set; }
}

Could this be achieved with some sort of proxy class?

I want to avoid writing a proxy for every single model class.


回答1:


I know of no simple and maintainable approach to this in "vanilla" C#, but you can achieve this with aspects. I have used PostSharp for this, which has a disadvantage of being a paid 3rd party product, but has a free version, where you can do this as well. PostSharp leverages the advantages of attributes like target specifying, inheritance etc. and extends them to aspects.

You can then define a LocationInterceptionAspect, which overrides OnSetValue method to call your RaisePropertyChanged delegate. Then you can use autogenerated properties decorated with your aspect attribute.

Paid version of PostSharp allows you to do this on class level, so you would only need one attribute (or none, if you decorate your base class and define the attribute as inheritable). This is described on the PostSharp site as a use case of InstanceLevelAspect




回答2:


I came along the NotifyPropertyWeaver extension and haved used it on a regular basis since then. It's a Visual Studio extension, which implements the always same INPC stuff for you, before the code gets compiled. You don't notice anything of that.

You need to install the extension, and your model then needs to look like this:

public class ProductWorkItem : INotifyPropertyChanged
{
    public string Name{ get; set; }
    public string Description{ get; set; }
    public string Brand{ get; set; }

    public event PropertyChangedEventHandler PropertyChanged;
}

The extension than adds all the rest for you. What I like about that approach, is that your class still "officially" implements the INPC interface and you can use it in non-WPF contexts as well (as INPC is not at all just a WPF thing), but still don't have to litter you classes with all that stuff. It raises notifications for readonly properties that depend on a property.

Of course, it's a bit fake, as it just automizes the writing and doesn't change anything about the underlying concept at all. But maybe it's a compromise...

Here is more information: Link




回答3:


We can avoid repetitive code of writing RaisePropertyChanged on each property setter in WPF.

Use free version of Postsharp.

By using following code we can bind only Virtual property to view.

namespace Test
{
[Serializable]
[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Module | AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = true)]
public sealed class RaisePropertyChangedAttribute : MethodInterceptionAspect
{
    private string propertyName;

    /// <summary>
    /// Compiles the time validate.
    /// </summary>
    /// <param name="method">The method.</param>
    public override bool CompileTimeValidate(MethodBase method)
    {
        return IsPropertySetter(method) && !method.IsAbstract && IsVirtualProperty(method);
    }

    /// <summary>
    /// Method invoked at build time to initialize the instance fields of the current aspect. This method is invoked
    /// before any other build-time method.
    /// </summary>
    /// <param name="method">Method to which the current aspect is applied</param>
    /// <param name="aspectInfo">Reserved for future usage.</param>
    public override void CompileTimeInitialize(MethodBase method, AspectInfo aspectInfo)
    {
        base.CompileTimeInitialize(method, aspectInfo);
        propertyName = GetPropertyName(method);
    }

    /// <summary>
    /// Determines whether [is virtual property] [the specified method].
    /// </summary>
    /// <param name="method">The method.</param>
    /// <returns>
    ///   <c>true</c> if [is virtual property] [the specified method]; otherwise, <c>false</c>.
    /// </returns>
    private static bool IsVirtualProperty(MethodBase method)
    {
        if (method.IsVirtual)
        {
            return true;
        }

        var getMethodName = method.Name.Replace("set_", "get_");
        var getMethod = method.DeclaringType.GetMethod(getMethodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

        return getMethod != null && getMethod.IsVirtual;
    }

    private static string GetPropertyName(MethodBase method)
    {
        return method.Name.Replace("set_", string.Empty);
    }

    /// <summary>
    /// Determines whether [is property setter] [the specified method].
    /// </summary>
    /// <param name="method">The method.</param>
    /// <returns>
    /// <c>true</c> if [is property setter] [the specified method]; otherwise, <c>false</c>.
    /// </returns>
    private static bool IsPropertySetter(MethodBase method)
    {
        return method.Name.StartsWith("set_", StringComparison.OrdinalIgnoreCase);
    }

    /// <summary>
    /// Method invoked <i>instead</i> of the method to which the aspect has been applied.
    /// </summary>
    /// <param name="args">Advice arguments.</param>
    public override void OnInvoke(MethodInterceptionArgs args)
    {
        var arg = args as MethodInterceptionArgsImpl;

        if ((arg != null) && (arg.TypedBinding == null))
        {
            return;
        }

        // Note ViewModelBase is base class for ViewModel
        var target = args.Instance as ViewModelBase;

        args.Proceed();

        if (target != null)
        {
            target.OnPropertyChanged(propertyName);                    
        }
    }
}
}



回答4:


This is already an old stuff, just nobody mentioned:

https://kindofmagic.codeplex.com/

You can turn on the automatic notification for every properties in your ViewModel with 1 attribute on the class.




回答5:


I found this class in the System.Dynamic namespace... It lets you intercept the actual DataBinding calls made by the DependencyObject on your binding target, to the Property on your binding source.

http://i.msdn.microsoft.com/en-us/library/system.windows.data.binding.DataBindingMostBasic(v=vs.110).png?appId=Dev11IDEF1&l=EN-US&k=k(System.Windows.Data.Binding)%3bk(VS.XamlEditor)%3bk(TargetFrameworkMoniker-.NETFramework

So what one could do now is to implement a class (lets call it DynamicNpcProxy) that implements INotifyPropertyChanged, is derived from DynamicObject and overrides both the TryGetMember and TrySetMember methods.

public class DynamicNpcProxy : DynamicObject, INotifyPropertyChanged
{
    public DynamicNpcProxy(object proxiedObject)
    {
        ProxiedObject = proxiedObject;
    }

    //...

    public object ProxiedObject { get; set; }

    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
        SetMember(binder.Name, value);
        return true;
    }

    protected virtual void SetMember(string propertyName, object value)
    {
        GetPropertyInfo(propertyName).SetValue(ProxiedObject, value, null);
        if (PropertyChanged != null) 
            PropertyChanged(ProxiedObject, new PropertyChangedEventArgs(propertyName));
    }

    protected PropertyInfo GetPropertyInfo(string propertyName)
    {
        return ProxiedObject.GetType().GetProperty(propertyName);
    }

    // override bool TryGetMember(...)
}

To get it to work, wrap the proxy around your current binding source, replace them and let DynamicObject do the rest.

In ViewModel.cs:

IList<ProductWorkItem> items;
//... assign items
var proxies = items.Select(p => new DynamicNpcProxy(p)).ToList();
ICollectionView Products = CollectionViewSource.GetDefaultView(proxies);

In View.xaml:

<TextBox Text="{Binding Products.CurrentItem.Name}" /> 
<TextBox Text="{Binding Products.CurrentItem.Description}" /> 

What you end up with is this:

Also check out this article over at the code project which provides even more information...




回答6:


Coming from the other side (and if you don't have a fancy Extension) you can "autospecify" changed properties with the extension methods outlined in my answer here: WCF service proxy not setting "FieldSpecified" property

Specifically, you could use the 'non-reflection method' to configure specific "OnPropertyChanged" handling per class to do other things than just set a specified field.

public static class PropertySpecifiedExtensions2
{
    /// <summary>
    /// Bind the <see cref="INotifyPropertyChanged.PropertyChanged"/> handler to automatically call each class's <see cref="IAutoNotifyPropertyChanged.Autonotify"/> method on the property name.
    /// </summary>
    /// <param name="entity">the entity to bind the autospecify event to</param>
    public static void Autonotify(this IAutoNotifyPropertyChanged entity)
    {
        entity.PropertyChanged += (me, e) => ((IAutoNotifyPropertyChanged)me).WhenPropertyChanges(e.PropertyName);
    }

    /// <summary>
    /// Create a new entity and <see cref="Autonotify"/> it's properties when changed
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    public static T Create<T>() where T : IAutoNotifyPropertyChanged, new()
    {
        var ret = new T();
        ret.Autonotify();
        return ret;
    }
}

/// <summary>
/// Used by <see cref="PropertySpecifiedExtensions.Autonotify"/> to standardize implementation behavior
/// </summary>
public interface IAutoNotifyPropertyChanged : INotifyPropertyChanged
{
    void WhenPropertyChanges(string propertyName);
}

And then each class themselves defines the behavior:

public partial class MyRandomClass: IAutoNotifyPropertyChanged
{

    /// <summary>
    /// Create a new empty instance and <see cref="PropertySpecifiedExtensions.Autospecify"/> its properties when changed
    /// </summary>
    /// <returns></returns>
    public static MyRandomClass Create()
    {
        return PropertySpecifiedExtensions2.Create<MyRandomClass>();
    }

    public void WhenPropertyChanges(string propertyName)
    {
        switch (propertyName)
        {
            case "field1": this.field1Specified = true; return;
            // etc
        }

        // etc
        if(propertyName.StartsWith(...)) { /* do other stuff */ }
    }
}

The downside to this is, of course, magic strings for property names making refactoring difficult, which you could get around with Expression parsing?



来源:https://stackoverflow.com/questions/13663719/avoid-calling-raisepropertychanged-in-every-setter

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!