INotifyPropertyChanged 自定义属性

3

我希望进行一些不那么重复和浪费的编码,需要使用 INotifyPropertyChanged 接口和自定义属性。

背景

今天,为了在窗口中使用 MVVM 动态更新值,我们需要执行以下操作:

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;

建议与问题

自定义属性
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;
}
将自定义属性与属性一起使用:
[PropertyChanged]
public string SomeProp { get; set; }

基本上,我不需要为窗口中的每个字段创建完整属性,而只需要拥有简单的属性。

但由于某些原因,它不起作用,而在调试时,编译器甚至不进入自定义属性类。

更新

因此,在研究了这个主题后,属性CallerMemberName是在编译器级别处理的,意味着编译器正在寻找该属性,并将属性/方法/等名称传递给使用该属性的方法。

所以,基本上,如果不编辑编译器代码及其行为,这样的事情是不可能实现的。


你认为PropertyChangedAttribute在哪里被调用?不是在属性的set;中。 - Jeroen van Langen
1
等等,所以因为事件没有在每次进入set;时触发,所以属性也不起作用了? - Yaron Binder
是的,所以你不能走捷径。 - Jeroen van Langen
我认为,属性不会做任何事情。它主要用于描述定义,而不是用作实现。 - Jeroen van Langen
1
永远不要因为不知道某些东西而感到羞耻。我们每天都在学习。 - Jeroen van Langen
显示剩余2条评论
2个回答

0

您可以使用一个实现了INotifyPropertyChanged接口的公共基类和一个源代码生成器来为标记有PropertyChanged的字段生成属性。根据您的需求,您可能会发现现有的工具或者需要自己实现。

最终,一个类可能会像这样(我为自己编写的类支持一些属性,例如属性生成器的setter可见性):

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

生成的代码可能看起来像这样:
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 是这个东西的“通用”实现:

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;
}

注意:据我所知,如果项目中有任何.xaml文件,则源代码生成器将无法工作(请参阅此问题),因此您必须将该逻辑放置在单独的项目中。
正如Rija建议的那样,您也可以与现有框架一起使用这种方法。

1
生成那段代码的意义是什么,如果他必须从一个具有 setvalue 和 getvalue 的类继承... 他最终将编写一个 getter 和一个 setter,而不是一个字段和一个属性。而且无论项目中有什么,它都能正常工作... 相同的努力... 更多的覆盖范围。 - L.Trabacchin
@L.Trabacchin 的意思是要有一个类来处理所有的接口事务,而且在某个时候必须处理属性,无论是在框架、生成器(如此处)或其他任何地方。 - Streamline
当然,但是生成器更难编写,并且有时可能不起作用,正如您所指出的那样。我宁愿完全放弃该属性。只需一个基类(他最终仍然会使用)还有一个GetValue<T>,它使用属性名称作为键将值存储在Dictionary<String,Object>中(也可以像您一样使用ref,但这将成为另一行代码),更简单并且将在任何地方都起作用...还可以使用并发字典使其线程安全...如果我们谈论前端开发,则性能无论如何都不会引人注目... - L.Trabacchin
我的意思是你必须记住属性,因此记住使用getvalue setvalue是相同的。而且,为属性和字段各写一行与为getter和setter各写一行是相同的... - L.Trabacchin
@L.Trabacchin 好的,现在我明白你的意思了。但是无论如何,单个属性不会起到任何作用,因为必须引发“PropertyChanged”事件,所以您必须使用多个属性。 - Streamline

-1
尝试使用MVVMLight Toolkit进行WPF编程:
从NuGet包中安装MVVMLight,
按照以下示例来使用RaisedPropertyChanged方法:
XAML代码:
<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>

主视图模型代码:

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}";
      }
    }

1
谢谢您的回答和时间,但我的目标是通过在属性上方添加属性来仅拥有属性,而不是完整的属性。 - Yaron Binder

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接