Avoid calling RaisePropertyChanged in every setter
Asked Answered
I

6

6

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.

Infatuated answered 1/12, 2012 at 21:3 Comment(1)
The NotificationObject is part of the Prism 4.0 frameworkInfatuated
I
0

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

Infatuated answered 2/12, 2012 at 2:42 Comment(1)
This works, if properties on your model are only changed via the proxy, in this case the GUI. Changes invoked "behind the scene" don't raise notification. Depends on the actual circumstances whether this is a solution or not.Magritte
E
6

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

Exception answered 1/12, 2012 at 23:6 Comment(0)
M
3

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

Magritte answered 2/12, 2012 at 10:56 Comment(1)
you dont actually need the VSIX installed to use it. it just helps you configure the MSBuild task. once configured it will work on other machines with no VSIXSuppression
M
2

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);                    
        }
    }
}
}
Minta answered 27/7, 2013 at 20:19 Comment(0)
I
0

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

Infatuated answered 2/12, 2012 at 2:42 Comment(1)
This works, if properties on your model are only changed via the proxy, in this case the GUI. Changes invoked "behind the scene" don't raise notification. Depends on the actual circumstances whether this is a solution or not.Magritte
D
0

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?

Damalis answered 15/1, 2014 at 18:40 Comment(0)
H
0

This is already an old stuff, just nobody mentioned:

https://marketplace.visualstudio.com/items?itemName=AlexeyLavnikov.KindOfMagic

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

Harmonyharmotome answered 31/1, 2017 at 14:0 Comment(1)
@JRLawhorne Thank you, I have replaced the link.Harmonyharmotome

© 2022 - 2024 — McMap. All rights reserved.