INotifyPropertyChanged custom attribute

Viewed 257

I like to do some less repetitive and wasteful coding of full properties that needs the INotifyPropertyChanged interface and do custom attribute.

Background

Today, in order to use MVVM with dynamic updating values in the window, we need to do the following:

private string _SomeProp;
public string SomeProp
{
    get => _SomeProp;
    set
    {
        _SomeProp = value;
        OnPropertyChanged();
    }
}

public void OnPropertyChanged([CallerMemberName] string name = null) 
    => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));

public event PropertyChangedEventHandler PropertyChanged;

Suggestion and the Problem

The custom attribute
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace MyProject.Models;

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public class PropertyChangedAttribute : Attribute, INotifyPropertyChanged
{
    public PropertyChangedAttribute([CallerMemberName] string propertyName = null) 
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    
    public event PropertyChangedEventHandler PropertyChanged;
}
The using of that custom attribute with a property:
[PropertyChanged]
public string SomeProp { get; set; }

So basically I don't have to create full property for each field in the window, but only having simple property.

But for some reason, it won't work, and when debugging, the compiler don't even enter the custom attribute class.

Update

So after research on the subject, the attribute CallerMemberName is processed in the compiler level, meaning, the compiler looking for that attribute and it itself pass the property/methods/etc... name to the method that use that attribute.

So basically, such thing isn't imposable to do without editing the compiler code and it's behavior.

2 Answers

Try to use MVVMLight Toolkit for WPF :

Install MVVMLight from nuget package,

Follow is the exemple to use RaisedPropertyChanged Method :

the xaml code :

<Window x:Class="TestMVVMLight.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:TestMVVMLight"
        mc:Ignorable="d"
        DataContext="{Binding Main, Source={StaticResource Locator}}"
        Title="MainWindow" Height="450" Width="800">

    <Grid>
        <TextBox HorizontalAlignment="Left" Height="35" Margin="27,23,0,0" TextWrapping="Wrap" Text="{Binding FirstName}" VerticalAlignment="Top" Width="119"/>
        <TextBox HorizontalAlignment="Left" Height="35" Margin="171,23,0,0" TextWrapping="Wrap" Text="{Binding LastName}" VerticalAlignment="Top" Width="120"/>
        <TextBox HorizontalAlignment="Left" Height="35" Margin="329,23,0,0" TextWrapping="Wrap" Text="{Binding FullName, Mode=OneWay}" VerticalAlignment="Top" Width="291"/>
    </Grid>

</Window>

MainViewModel code:

using GalaSoft.MvvmLight;

namespace TestMVVMLight.ViewModel
{
    public class MainViewModel : ViewModelBase
    {
        /// <summary>
        /// Initializes a new instance of the MainViewModel class.
        /// </summary>
        public MainViewModel() { }

        private string _firstname = "";
        private string _lastname = "";
        
        public string FirstName 
        { 
            get =>  this._firstname;
            set 
            {
                this._firstname = value;
                RaisePropertyChanged(() => FullName);
            }
        }

        public string LastName
        {
            get => this._lastname;
            set
            {
                this._lastname = value;
                RaisePropertyChanged(() => FullName);
            }
        }

        public string FullName => $"{FirstName} {LastName}";
      }
    }

You can use a common base class that implements INotifyPropertyChanged and a source generator that generates the property for a field marked with PropertyChanged. Based on your needs you may find an existing tool or you have to implement it yourself.

A class could like this in the end (the one i wrote for myself supports some properties for the property to generate e.g. the setter visibility):

public partial class MyCLass : NotifyPropertyChanges
{
    [NotificationProperty(SetterVisibility = Visibility.Private)]
    private int count;
}

The generated code could then look like this:

partial class MyCLass
{
    [System.Runtime.CompilerServices.CompilerGenerated]
    public int Count
    {
        [System.Runtime.CompilerServices.CompilerGenerated]
        get => this.count;
        [System.Runtime.CompilerServices.CompilerGenerated]
        private set => SetValue(ref this.count, value, "Count");
    }
}

SetValue is the "common" implementation this stuff:

protected bool SetValue<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
{
    if (EqualityComparer<T>.Default.Equals(field, value))
        return false;

    OnPropertyChanging(new PropertyChangingEventArgs(propertyName));
    field = value;
    OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
    return true;
}

Note: The source generator won't work if you have any .xaml files in the project AFAIK (see this question), so you have to place that logic in a seperate project.

You can also use this approach, as Rija suggested, together with an existing framework.

Related