You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现类Community Toolkit MVVM的ObservableProperty属性(无需重复构建)

实现轻量版ObservableProperty功能(跨平台、无冗余)

如果你想复刻Community Toolkit MVVM中ObservableProperty的核心功能——基于字段自动生成带PropertyChanged触发逻辑的属性,同时避免引入工具包冗余内容、支持跨平台UI框架(WPF/MAUI/UWP等),可以通过自定义特性+编译源生成器的方式实现,完全无需手动编写重复的属性代码。


1. 定义基础ObservableObject基类

这个类仅实现INotifyPropertyChanged接口,是所有ViewModel的轻量基类,无任何冗余:

using System.ComponentModel;
using System.Runtime.CompilerServices;

public abstract class ObservableObject : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}
  • [CallerMemberName]特性会自动传入调用方法的属性名,无需手动填写,减少出错概率。

2. 自定义ObservableProperty特性

用这个特性标记需要自动生成属性的字段:

[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class ObservablePropertyAttribute : Attribute
{
    // 可扩展添加自定义参数(比如指定属性名),基础版无需额外逻辑
}

3. 编写编译源生成器(核心逻辑)

源生成器会在编译时扫描代码,自动为带[ObservableProperty]的字段生成对应的属性代码。需要创建一个单独的类库项目,引用Microsoft.CodeAnalysis.CSharp包:

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Text;

[Generator]
public class ObservablePropertyGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
        // 注册语法接收器,收集所有私有字段
        context.RegisterForSyntaxNotifications(() => new ObservablePropertyReceiver());
    }

    public void Execute(GeneratorExecutionContext context)
    {
        if (context.SyntaxReceiver is not ObservablePropertyReceiver receiver) return;

        // 获取自定义特性的符号引用
        var attributeSymbol = context.Compilation.GetTypeByMetadataName("YourNamespace.ObservablePropertyAttribute");
        if (attributeSymbol == null) return;

        foreach (var field in receiver.Fields)
        {
            var fieldSymbol = context.Compilation.GetSemanticModel(field.SyntaxTree).GetDeclaredSymbol(field) as IFieldSymbol;
            if (fieldSymbol == null) continue;

            // 检查字段是否标记了[ObservableProperty]
            var hasTargetAttribute = fieldSymbol.GetAttributes()
                .Any(attr => attr.AttributeClass?.Equals(attributeSymbol, SymbolEqualityComparer.Default) ?? false);
            if (!hasTargetAttribute) continue;

            // 生成属性名:去掉下划线(如果有),首字母大写
            var propertyName = GetPropertyName(fieldSymbol.Name);
            // 生成属性代码
            var sourceCode = GeneratePropertySource(fieldSymbol, propertyName);
            // 将生成的代码添加到编译流程
            context.AddSource($"{fieldSymbol.ContainingType.Name}_{propertyName}.g.cs", sourceCode);
        }
    }

    private string GetPropertyName(string fieldName)
    {
        if (fieldName.StartsWith("_")) fieldName = fieldName.Substring(1);
        return char.ToUpper(fieldName[0]) + fieldName.Substring(1);
    }

    private string GeneratePropertySource(IFieldSymbol fieldSymbol, string propertyName)
    {
        var fieldType = fieldSymbol.Type.ToDisplayString();
        var className = fieldSymbol.ContainingType.Name;
        var namespaceName = fieldSymbol.ContainingType.ContainingNamespace.ToDisplayString();

        var sb = new StringBuilder();
        sb.AppendLine($"namespace {namespaceName};");
        sb.AppendLine();
        sb.AppendLine($"public partial class {className}");
        sb.AppendLine("{");
        sb.AppendLine($"    public {fieldType} {propertyName}");
        sb.AppendLine("    {");
        sb.AppendLine($"        get => {fieldSymbol.Name};");
        sb.AppendLine($"        set");
        sb.AppendLine("        {");
        sb.AppendLine($"            if (!EqualityComparer<{fieldType}>.Default.Equals({fieldSymbol.Name}, value))");
        sb.AppendLine("            {");
        sb.AppendLine($"                {fieldSymbol.Name} = value;");
        sb.AppendLine($"                OnPropertyChanged(nameof({propertyName}));");
        sb.AppendLine("            }");
        sb.AppendLine("        }");
        sb.AppendLine("    }");
        sb.AppendLine("}");

        return sb.ToString();
    }
}

// 语法接收器:收集所有私有字段节点
public class ObservablePropertyReceiver : ISyntaxReceiver
{
    public List<FieldDeclarationSyntax> Fields { get; } = new();

    public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
    {
        if (syntaxNode is FieldDeclarationSyntax fieldDecl && 
            fieldDecl.Modifiers.Any(SyntaxKind.PrivateKeyword))
        {
            Fields.Add(fieldDecl);
        }
    }
}
  • 生成的属性会自动做相等性检查,避免无意义的PropertyChanged触发。
  • 生成的代码以partial类形式存在,需要你的ViewModel也是partial类才能合并。

4. 使用示例

在ViewModel中标记字段即可,编译时自动生成属性:

// 必须是partial类,才能和生成的代码合并
public partial class UserViewModel : ObservableObject
{
    [ObservableProperty]
    private string _userName;

    [ObservableProperty]
    private int _age;
}

编译后会自动生成对应的UserName和Age属性,包含完整的set逻辑和PropertyChanged触发。


注意事项

  1. 源生成器项目需设置为.NET Standard 2.0或更高版本,确保兼容性。
  2. 该实现支持所有基于INotifyPropertyChanged的UI框架,完全跨平台。
  3. 无任何冗余依赖,仅保留核心的属性生成和通知逻辑。

内容的提问来源于stack exchange,提问作者SulfurChunk

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 18:57:50