如何实现类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触发。
注意事项
- 源生成器项目需设置为.NET Standard 2.0或更高版本,确保兼容性。
- 该实现支持所有基于
INotifyPropertyChanged的UI框架,完全跨平台。 - 无任何冗余依赖,仅保留核心的属性生成和通知逻辑。
内容的提问来源于stack exchange,提问作者SulfurChunk
相关产品推荐
相关产品推荐

