目录

WPF-ViewModel属性机制

官方定义

属性是一种成员,它提供灵活的机制来读取、写入或计算私有字段的值。属性可作为公共数据成员使用,但它们是称为"访问器"(accessors)的特殊方法。这使得可以轻松访问数据,还有助于提供方法的安全性和灵活性。

官方把属性归类为方法,不是字段。

一、C#属性(Property)

在C#中,属性用于 对字段进行封装访问。

基本结构:

private string _name;

public string Name
{
    get { return _name;}
    set { _name = value;}
}

结构说明:

部分 作用
_name 内部字段(Field)
Name 对外公开属性(Property)
get 读取属性值
set 设置属性值

访问示例:

Name = "Alice"; // 调用set
var n = Name; // 调用 get

二、value的本质

1.value是什么?

value 是 C# 在属性 set 访问器中自动提供的隐式参数。

它表示:

外部赋给该属性的新值

示例:

set
{
    _name = value;
}

当执行:

Name = "Alice";

实际发生:

调用 set("Alice")

因此:

value = "Alice"

2.value的作用域

value 只存在于 set 代码块中。

合法:

set
{
    Console.WriteLine(value);
}

非法:

get
{
	Console.WriteLine(value);
}

原因:

value 是 set 方法参数

三、属性的底层原理

属性在编译后会变成 两个方法:

public string Name
{
    get { return _name;}
    set { _name = value; }
}

概念上等价于:

private string _name;

public string GetName()
{
    return _name;
}

public void SetName(string value)
{
    _name = value;
}

调用:

Name = "Alice";

等价于:

SetName("Alice");

四、为什么需要字段(Field)

public string Name
{
    get { return Name;}
    set { Name = value;}
}

问题:无限递归调用

执行流程:

Name = "Alice"
set
Name = value
再次调用 set
无限循环


访问 Name 时:
obj.Name 
  → 调用 get 访问器
  → get 中 return Name
  → 再次调用 get 访问器
  → 再次 return Name
  → 再次调用 get 访问器
  → ... 无限循环 → StackOverflowException
  
设置 Name 时:
obj.Name = "张三"
  → 调用 set 访问器
  → set 中 Name = value
  → 再次调用 set 访问器
  → 再次 Name = value
  → 再次调用 set 访问器
  → ... 无限循环 → StackOverflowException

最终会引发异常:StackOverflowException

正确写法:

private string _name;

public string Name
{
    get { return _name; }
    set { _name = value; }
}

结构:

Property
Field

而不是:

Property → Property

五、自动属性(Auto Property)

如果不需要额外逻辑,可以使用自动属性:

public string Name { get; set; }

编译器会自动生成隐藏字段:

<Name>K_BackingField

但在WPF ViewModel 中不常用直接使用,原因是:

需要 PropertyChanged 通知 UI 更新

六、WPF数据绑定原理

在 WPF,UI 和 ViewModel 通过 Binding 连接。

<TextBox Text="{Binding Name}"/>

执行流程:

UI输入
Binding
ViewModel.Name = 新值
set(value)
更新字段
通知 UI

如果没有通知:UI不会刷新

七、INotifyPropertyChanged

WPF 使用接口: INotifyPropertyChanged – 用于通知 UI 数据变化。

接口定义:

public interface INOtifyPropertyChanged
{
    event PropertyChangedEventHandler PropertyChanged;
}

当前属性变化时从触发:

PropertyChanged 事件

八、ViewModel 标准写法

public class MainViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    
    private string _name;
    
    public string Name
    {
        get => _name;
        set => 
        {
            if(_name != value)
            {
                _name = value;
                OnPropertyChanged(nameof(Name));
            }
        }
    }
    
    protected void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

流程:

set(value)
字段更新
PropertyChanged
Binding监听
UI刷新

九、为什么要写 If (_field != value)

原因:

避免 重复刷新 UI

如果没有判断:

set
{
    _name = value;
    OnPropertyChanged(nameof(Name));
}

即使值没有变化:

仍然触发 UI 更新

判断后:

if (_name != value)

只在值改变时通知。

优点:

  • 减少 UI 刷新
  • 提高性能
  • 避免循环绑定

十、CallerMemberName(现代写法)

为了避免写字符串"Name", 可以使用:

CallerMemberName

改写:

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

调用:

OnpropertyChanged();

编译器会自动填充:Name

优点:

  • 避免硬编码字符串
  • 避免重构错误

十一、MVVM写法(SetProperty)

实际项目中通常封装一个通用方法:

protected bool SetProperty<T>(ref T field, T value, 
                             [CallerMemberName] string propertyName = null)
{
    if (EqualityComparer<T>.Default.Equals(field, value))
    {
        return false;
    }
    
    field = value;
    OnPropertyChanged(propertyName);
    return true;
}

使用:

public class MainViewModel : ViewModelBase
{
    private string _name;
    
    public string Name
    {
        get => _name;
        set => SetProperty(ref _name, value);
    }
}

十二、完整ViewModel模板

public class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    
    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
    
    protected bool SetProperty<T>(ref T field, T value,
                                 [CallerMemberName] string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, value))
        {
            return false;
        }
        
        field = value;
        OnPropertyChanged(propertyName);
        return true;
    }
}

使用:

public class MainViewModel : ViewModelBase
{
    private string _name;
    
    public string Name
    {
        get => _name;
        set => SetProperty(ref _name, value);
    }
}

执行流程总结:

UI输入
Binding
ViewModel.Property = value
set(value)
SetProperty()
字段更新
PropertyChanged
WPF Binding监听
UI刷新