Avatar ·

Реализация INotifyPropertyChanged: есть ли способ лучше?

Всем привет! Я начинающий C# разработчик и пишу простое приложение на WinForms. У меня есть класс модели, который должен уведомлять UI об изменениях свойств. Я реализовал INotifyPropertyChanged, но код получается очень громоздким и повторяющимся. Вот как я сейчас делаю: ```csharp public class Person : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private string _name; private int _age; private string _address; public string Name { get { return _name; } set { if (_name != value) { _name = value; OnPropertyChanged("Name"); } } } public int Age { get { return _age; } set { if (_age != value) { _age = value; OnPropertyChanged("Age"); } } } public string Address { get { return _address; } set { if (_address != value) { _address = value; OnPropertyChanged("Address"); } } } private void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } ``` Проблема в том, что для каждого свойства я вынужден писать одно и то же: поле, свойство с проверкой на изменение и вызовом OnPropertyChanged. Если у меня 20 свойств — это 20 одинаковых блоков кода. Это утомительно и легко допустить ошибку (например, опечататься в имени свойства в строке). Я слышал, что в более новых версиях C# есть какие-то фишки, которые упрощают это. Может быть, есть какой-то атрибут, кодогенерация или другой паттерн? Или я обречен писать эту "шаблонную" логику руками? Подскажите, пожалуйста, как сделать код чище и менее повторяющимся. Спасибо! <<>>

Использование атрибута CallerMemberName и вспомогательного метода

Да, есть гораздо более элегантный способ! Начиная с C# 5.0 (.NET 4.5), появился атрибут CallerMemberName, который автоматически подставляет имя вызывающего члена (свойства или метода). Это избавляет от необходимости передавать строку с именем свойства вручную и исключает опечатки.

Вот как можно переписать ваш класс:

public class Person : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private string _name;
    private int _age;
    private string _address;

    public string Name
    {
        get => _name;
        set => SetProperty(ref _name, value);
    }

    public int Age
    {
        get => _age;
        set => SetProperty(ref _age, value);
    }

    public string Address
    {
        get => _address;
        set => SetProperty(ref _address, value);
    }

    // Универсальный метод для всех свойств
    private bool SetProperty<T>(ref T backingStore, T value, [CallerMemberName] string propertyName = "")
    {
        if (EqualityComparer<T>.Default.Equals(backingStore, value))
            return false;

        backingStore = value;
        OnPropertyChanged(propertyName);
        return true;
    }

    private void OnPropertyChanged([CallerMemberName] string propertyName = "")
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

Что изменилось и почему это лучше?

  • Убраны строковые литералы: Атрибут [CallerMemberName] автоматически подставляет имя свойства, из которого вызван метод. Теперь невозможно ошибиться в написании "Name" или "Age".
  • Один метод для всех свойств: SetProperty — это обобщенный метод, который принимает ссылку на поле (ref T backingStore), новое значение и автоматически получает имя свойства. Он также возвращает bool, который показывает, было ли значение изменено — это удобно для дополнительной логики.
  • Лаконичность: Сеттер каждого свойства теперь состоит из одной строки: set => SetProperty(ref _name, value);. Никаких повторяющихся проверок и вызовов.
  • Производительность: Метод EqualityComparer<T>.Default.Equals работает оптимально для любого типа и избегает боксинга для значимых типов (как int).

Дополнительные улучшения

Если вы хотите еще больше сократить код, можно использовать кодогенерацию. В современных версиях .NET (например, .NET 6+ и .NET 8) есть source generators, которые могут автоматически создавать реализации INotifyPropertyChanged. Например, популярный пакет PropertyChanged.SourceGenerator из NuGet позволяет писать так:

// После добавления пакета и использования partial class
public partial class Person : INotifyPropertyChanged
{
    [NotifyPropertyChanged]
    private string _name = "";

    [NotifyPropertyChanged]
    private int _age;

    [NotifyPropertyChanged]
    private string _address = "";
}

Генератор сам создаст свойства Name, Age, Address с полной реализацией INotifyPropertyChanged. Это максимально сокращает ручной код.

Но если вы не хотите подключать сторонние библиотеки, описанный выше подход с CallerMemberName и SetProperty является золотым стандартом для WinForms и WPF приложений уже много лет. Он прост, надежен и не требует дополнительных зависимостей.

<<>>

Альтернатива: Использование класса ObservableObject из MVVM Toolkit

Если вы работаете с .NET Core или .NET 5+ и используете паттерн MVVM, рекомендую обратить внимание на официальный пакет CommunityToolkit.Mvvm (ранее Microsoft.Toolkit.Mvvm). Он предоставляет готовый базовый класс ObservableObject, который уже содержит всю логику INotifyPropertyChanged.

Пример использования:

using CommunityToolkit.Mvvm.ComponentModel;

public partial class Person : ObservableObject
{
    [ObservableProperty]
    private string _name = "";

    [ObservableProperty]
    private int _age;

    [ObservableProperty]
    private string _address = "";
}

Атрибут [ObservableProperty] (доступен в C# 11 и выше с partial классами) автоматически генерирует публичное свойство Name, Age, Address с полной поддержкой уведомлений. Вам не нужно писать ни полей, ни свойств, ни методов — всё делает кодогенератор.

Плюсы: Минимум кода, встроенная поддержка команд, сообщений и других MVVM-функций. Минусы: Зависимость от внешнего пакета и требование .NET 6+ с C# 11. Для старых проектов (.NET Framework) этот подход не подходит.