大体こんな感じで安定してきたので書き残し。

public abstract class BindableBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
    {
        if (Equals(storage, value)) return false;
        
        storage = value;
        RaisePropertyChanged(propertyName);
        return true;
    }

    protected virtual void RaisePropertyChanged([CallerMemberName] string propertyName = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

以前まではExpression Tree使ってリファクタリングが効くようにしていたRaisePropertyChangedのオーバーロードがあったりもしたけど、nameof演算子が使えるようになってからは全く使わなくなったので省略。

実際の使い方は以下の通り。

public class WindowViewModel : BindableBase
{
    private string _labelText;
    public string LabelText
    {
        get => _labelText;
        set => SetProperty(ref _labelText, value);
    }
}