3

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.

Yaron Binder
  • 107
  • 1
  • 1
  • 9
  • Where do you think the PropertyChangedAttribute is called? Not in the `set;` of the property. – Jeroen van Langen Dec 01 '21 at 08:45
  • 1
    Wait, so because the event didn't fire inside the set for every enter to the `set;`, the attribute don't work either? – Yaron Binder Dec 01 '21 at 11:30
  • yep, so, you can't take a shortcut. – Jeroen van Langen Dec 01 '21 at 12:15
  • Got it. I also tried doing it like so: `public string SomeProp { get; [PropertyChnged] set; }` but it don't do anything, and for me it make sense that it should work, right? – Yaron Binder Dec 01 '21 at 12:51
  • I think, an attribute will do nothing. It is mostly used to describe about definitions, not used as implementations. – Jeroen van Langen Dec 01 '21 at 21:42
  • I read about attributes more, apparently, if you don't actively use reflection of the attribute class like doing `var propName = typeof(MyClass).GetProperties().GetCustomAttributes();` and looping over it or something, the attribute will remain "untouched". So my understanding of the subject and the using of it was wrong from the beginnings, and now I feel ashamed that i waste you all time, but hopefully in the future this question help someone like me :) – Yaron Binder Dec 02 '21 at 05:23
  • 1
    Never be ashamed about not knowing something. We all learn daily. – Jeroen van Langen Dec 02 '21 at 08:02

2 Answers2

0

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.

Streamline
  • 952
  • 1
  • 15
  • 23
  • 1
    What's the point of generating that code if he has to inherit from a class that have a setvalue could have also a get value... He will end up writing a getter and a setter instead of a field and an attribute. And it will work no matter what's in the project... Same effort... More coverage – L.Trabacchin Dec 01 '21 at 15:16
  • @L.Trabacchin The point is to have one class that handles all interface stuff and at one point the attribute has to be handled anyway, either in a framework, a generator like here or anything else. – Streamline Dec 02 '21 at 12:02
  • Sure, but a generator is harder to write, and might not work sometimes as you pointed out. I would rather discard the attribute at all. Just a base class (that he ends up using anyway) with also a GetValue that stores the value in a Dictionary using the prop name as key (can also work with the ref like you did but it will become one more line of code) is simpler and will work everywhere ... could also be thread safe using a concurrent dictionary... and if we are talking front end development the performance won't be noticeable anyway... – L.Trabacchin Dec 02 '21 at 18:08
  • what i mean is you have to remember the attribute, so having to remember to use getvalue setvalue is the same. And writing one line for the attribute and one for the field is the same as writing one line for the getter and one for the setter ... – L.Trabacchin Dec 02 '21 at 18:09
  • @L.Trabacchin Ok, now i get what you meant. But anyway, a single attribute won't do anything as he has to have the `PropertyChanged` event raised so you _have_ to use more than single attribute. – Streamline Dec 02 '21 at 18:27
-1

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}";
      }
    }
Yaron Binder
  • 107
  • 1
  • 1
  • 9
  • 1
    Thank you for your answer and time, but my goal is to have only property instead of full property by adding the attribute above the property. – Yaron Binder Dec 01 '21 at 09:29