属性持有对象?

看flash CS3帮助文档的时候,偶然看到这个概念——属性持有对象。

通过几个例子,我们可以对这个概念有个初步的了解。

首先就我们常用的Bitmap类对象和BitmapData对象来说,Bitmap类对象属性bitmapData持有BitmapData对象,我们可以新建一个fla文件,然后在第一帧上写如下代码:

var bmpdata:BitmapData = new BitmapData(100,100,true,0xFF669900);
var bmp:Bitmap = new Bitmap(bmpdata);
addChild(bmp);
trace(bmp.bitmapData); //输出 [object BitmapData]

看代码的最后一句,bmp.bitmapData属性持有对应的BitmapData对象,输出的时候也就是[object BitmapData]。

其实这个现象在AS3中很常见,只是我们没注意到而已,还有我们常见的Loader类对象的contentLoaderInfo属性持有LoaderInfo对象。

<think>我们之前已经实现了嵌套对象属性动态显示,现在需要支持编辑并同步更新源对象。关键在于如何将用户在UI上的修改传递回嵌套对象属性。以下是实现步骤: 1. **修改PropertyItem类实现INotifyPropertyChanged**:使每个属性项能够通知值的变化。 2. **扩展属性加载逻辑**:在创建PropertyItem时,对于嵌套对象,我们不仅要加载其子属性,还要在值改变时更新源对象。 3. **绑定模式设置**:在XAML中,将编辑控件的绑定模式设置为TwoWay,以便双向更新。 4. **处理嵌套属性的更新**:当嵌套属性中的某个值改变时,需要逐层向上更新,直到根对象。 详细实现: ### 1. 修改PropertyItem类 我们需要让PropertyItem支持属性更改通知,同时要能够更新源对象(即嵌套对象的父对象)的对应属性。 ```csharp public class PropertyItem : INotifyPropertyChanged { private object _value; // 注意:这里我们保存两个额外的引用: // - 当前属性所属的对象(即父对象,如果是根对象则为null) private object _parentObject; // - 当前属性对应的PropertyInfo,用于设置值 private PropertyInfo _propertyInfo; public string Name { get; set; } public object Value { get => _value; set { if (_value == value) return; _value = value; OnPropertyChanged(); // 如果当前属性是嵌套对象,则重新加载其嵌套属性(因为对象引用可能改变) if (IsComplexType) { NestedProperties.Clear(); LoadNestedProperties(value, NestedProperties); } // 更新父对象属性 if (_parentObject != null && _propertyInfo != null) { try { // 设置父对象属性值 _propertyInfo.SetValue(_parentObject, value); } catch { /* 处理异常,例如类型转换失败 */ } } } } public Type Type { get; set; } public ObservableCollection<PropertyItem> NestedProperties { get; } = new ObservableCollection<PropertyItem>(); public bool IsComplexType => Value != null && !Value.GetType().IsPrimitive && Value.GetType() != typeof(string); // 构造函数,需要传入父对象属性信息(对于根属性,parentObject为null,propertyInfo为null) public PropertyItem(object parentObject, PropertyInfo propertyInfo, object value) { _parentObject = parentObject; _propertyInfo = propertyInfo; Name = propertyInfo?.Name ?? "Unknown"; _value = value; Type = propertyInfo?.PropertyType ?? value?.GetType() ?? typeof(object); } // 用于根对象直接创建属性项(没有父对象属性信息) public PropertyItem(string name, object value) { Name = name; _value = value; Type = value?.GetType() ?? typeof(object); } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } ``` ### 2. 修改ViewModel的加载方法 在加载属性时,我们需要传入父对象属性信息,以便在属性值改变时能够更新父对象。 ```csharp public class DynamicViewModel : INotifyPropertyChanged { // ... 其他代码 ... private void LoadProperties(object target) { if (target == null) return; Properties.Clear(); foreach (var prop in target.GetType().GetProperties()) { var propValue = prop.GetValue(target); // 创建属性项,传入父对象(即target)和属性信息 var item = new PropertyItem(target, prop, propValue); // 递归加载嵌套属性 if (item.IsComplexType) { LoadNestedProperties(propValue, item.NestedProperties); } Properties.Add(item); } } private void LoadNestedProperties(object obj, ObservableCollection<PropertyItem> collection, HashSet<object> visited = null) { if (obj == null) return; visited ??= new HashSet<object>(); if (visited.Contains(obj)) return; visited.Add(obj); foreach (var prop in obj.GetType().GetProperties()) { var propValue = prop.GetValue(obj); // 创建嵌套属性项,传入父对象(即当前obj)和属性信息 var item = new PropertyItem(obj, prop, propValue); if (item.IsComplexType) { // 递归加载 LoadNestedProperties(propValue, item.NestedProperties, new HashSet<object>(visited)); } collection.Add(item); } } } ``` ### 3. 修改XAML绑定 在XAML中,我们需要确保编辑控件的绑定是双向的(TwoWay)。由于我们已经在PropertyItem中实现了INotifyPropertyChanged,所以当用户在TextBox中编辑文本时,会触发PropertyItem.Value的setter,从而更新源对象。 ```xml <TreeView ItemsSource="{Binding Properties}"> <TreeView.Resources> <HierarchicalDataTemplate DataType="{x:Type local:PropertyItem}" ItemsSource="{Binding NestedProperties}"> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding Name}" FontWeight="Bold" Width="150"/> <ContentControl Content="{Binding}"> <ContentControl.Resources> <!-- 对于字符串,使用TextBox并双向绑定 --> <DataTemplate DataType="{x:Type sys:String}"> <TextBox Text="{Binding Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Width="200"/> </DataTemplate> <!-- 对于整数 --> <DataTemplate DataType="{x:Type sys:Int32}"> <TextBox Text="{Binding Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Width="200"/> </DataTemplate> <!-- 其他基本类型类似 --> <!-- 如果是嵌套对象,则显示提示 --> <DataTemplate> <TextBlock Text="[查看嵌套属性]" FontStyle="Italic" Foreground="Gray"/> </DataTemplate> </ContentControl.Resources> </ContentControl> </StackPanel> </HierarchicalDataTemplate> </TreeView.Resources> </TreeView> ``` ### 4. 处理类型转换 上面的实现对于字符串类型可以直接使用,但对于其他类型(如整数),在TextBox中输入字符串需要转换为整数。如果转换失败,则无法更新源对象。我们可以通过以下方式增强: - 使用转换器(ValueConverter)处理转换 - 在PropertyItem.Value的setter中捕获异常并回滚 这里提供一个简单的转换器示例: ```csharp public class StringToObjectConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { return value?.ToString(); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { // 尝试将字符串转换为目标类型 if (value is string str) { try { // 这里使用TypeConverter,支持基本类型 var converter = TypeDescriptor.GetConverter(targetType); if (converter != null && converter.CanConvertFrom(typeof(string))) { return converter.ConvertFromString(str); } } catch { // 转换失败,返回原来的值(或者可以返回DependencyProperty.UnsetValue以保持原值) } } return value; // 转换失败,返回原值(这样界面不会更新,但用户输入的错误值会保留,可能需要错误提示) } } ``` 然后在XAML中使用转换器: ```xml <Window.Resources> <local:StringToObjectConverter x:Key="StringConverter"/> </Window.Resources> <!-- 在TextBox的绑定中使用转换器 --> <TextBox Text="{Binding Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, Converter={StaticResource StringConverter}}" Width="200"/> ``` 但是注意:我们在绑定中使用了`UpdateSourceTrigger=PropertyChanged`,这样每次按键都会尝试更新。如果类型转换失败,用户会看到输入的值,但实际源对象不会被更新。为了更好的用户体验,可以配合验证规则(ValidationRule)或INotifyDataErrorInfo接口实现错误提示。 ### 5. 支持嵌套对象整个替换 如果嵌套对象属性本身被替换(例如,将一个Address对象替换为另一个Address对象),则我们需要更新整个嵌套属性的子树。在PropertyItem.Value的setter中,我们已经处理了这种情况:当设置一个新对象时,会清空NestedProperties并重新加载。 但是注意:如果替换的是嵌套对象,那么原来的嵌套对象的所有子属性项都将被移除,新的对象属性将被加载。 ### 注意事项 - **性能**:对于大型对象,递归加载可能消耗资源,考虑延迟加载(当节点展开时才加载子属性)。 - **循环引用**:在加载嵌套属性时,我们使用了visited集合来防止循环引用,但在编辑时如果用户创建了循环引用(例如,将父对象赋值给子对象属性),则会导致无限递归。需要在设计时避免,或者在setter中加入引用检查。 - **只读属性**:如果某个属性没有setter,则不应该允许编辑。在PropertyItem中可以添加IsReadOnly属性,根据PropertyInfo.CanWrite设置。 ### 示例效果 当用户编辑嵌套属性(如Person.Address.Street)时: 1. 修改TextBox中的文本(Street的值) 2. 触发PropertyItem.Value的setter(对于Address对象中的Street属性) 3. 在setter中,通过_parentObject(即Address对象实例)和_propertyInfo(Street的PropertyInfo)设置新值 4. 这样,Person对象的Address属性的Street就被更新了。 因为我们的Person对象(根对象)的Address属性是一个对象,而Address对象中的Street被更新,所以整个对象树就被更新了。 通过这种方式,我们实现了嵌套属性的编辑并同步更新源对象。 §§相关问题§§ 1. 如何优化大型嵌套对象的编辑性能? 2. WPF中如何实现嵌套属性的数据验证? 3. 如何防止嵌套属性编辑时出现循环引用? 4. 如何支持集合类型的嵌套属性编辑? </think>在 WPF 中实现嵌套属性编辑并同步更新源对象,需要结合双向绑定、属性变更通知和递归更新机制。以下是完整实现方案: ### 解决方案步骤 #### 1. 实现支持双向绑定的嵌套属性模型 ```csharp public class NestedProperty : INotifyPropertyChanged { private object _value; private readonly object _parent; private readonly PropertyInfo _propertyInfo; public string Name { get; } public object Value { get => _value; set { if (Equals(_value, value)) return; _value = value; OnPropertyChanged(); // 更新源对象属性值 _propertyInfo?.SetValue(_parent, value); // 如果是嵌套对象则重新加载子属性 if (IsComplexType) LoadNestedProperties(); } } public bool IsComplexType => Value != null && !Value.GetType().IsPrimitive && Value.GetType() != typeof(string); public ObservableCollection<NestedProperty> Children { get; } = new(); public NestedProperty(string name, object value, object parent = null, PropertyInfo propertyInfo = null) { Name = name; _value = value; _parent = parent; _propertyInfo = propertyInfo; if (IsComplexType) LoadNestedProperties(); } private void LoadNestedProperties() { Children.Clear(); if (Value == null) return; foreach (var prop in Value.GetType().GetProperties()) { var childValue = prop.GetValue(Value); Children.Add(new NestedProperty( name: prop.Name, value: childValue, parent: Value, // 传递当前对象作为父对象 propertyInfo: prop // 传递属性信息用于更新 )); } } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string name = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); } } ``` #### 2. 创建支持嵌套编辑的 ViewModel ```csharp public class NestedEditorViewModel : INotifyPropertyChanged { private object _sourceObject; public ObservableCollection<NestedProperty> Properties { get; } = new(); public object SourceObject { get => _sourceObject; set { _sourceObject = value; LoadProperties(); } } private void LoadProperties() { Properties.Clear(); if (_sourceObject == null) return; foreach (var prop in _sourceObject.GetType().GetProperties()) { Properties.Add(new NestedProperty( name: prop.Name, value: prop.GetValue(_sourceObject), parent: _sourceObject, propertyInfo: prop )); } } // 实现 INotifyPropertyChanged... } ``` #### 3. 使用 HierarchicalDataTemplate 实现嵌套 UI ```xaml <TreeView ItemsSource="{Binding Properties}"> <TreeView.Resources> <!-- 嵌套对象模板 --> <HierarchicalDataTemplate DataType="{x:Type local:NestedProperty}" ItemsSource="{Binding Children}"> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding Name}" Width="150" Margin="0,0,10,0"/> <!-- 基本类型编辑控件 --> <ContentControl Content="{Binding}"> <ContentControl.Resources> <!-- 字符串编辑 --> <DataTemplate DataType="{x:Type sys:String}"> <TextBox Text="{Binding Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Width="200" Validation.ErrorTemplate="{StaticResource ValidationTemplate}"/> </DataTemplate> <!-- 数值编辑 --> <DataTemplate DataType="{x:Type sys:Int32}"> <TextBox Text="{Binding Value, Mode=TwoWay, UpdateSourceTrigger=LostFocus}" Width="200"> <TextBox.Text> <Binding Path="Value" UpdateSourceTrigger="PropertyChanged"> <Binding.ValidationRules> <local:NumberValidationRule ValidationStep="UpdatedValue"/> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox> </DataTemplate> <!-- 嵌套对象提示 --> <DataTemplate> <TextBlock Text="[编辑嵌套属性]" FontStyle="Italic" Foreground="Blue"/> </DataTemplate> </ContentControl.Resources> </ContentControl> </StackPanel> </HierarchicalDataTemplate> </TreeView.Resources> </TreeView> ``` #### 4. 添加数据验证支持 ```csharp // 数值验证规则 public class NumberValidationRule : ValidationRule { public override ValidationResult Validate(object value, CultureInfo culture) { if (value == null || !int.TryParse(value.ToString(), out _)) return new ValidationResult(false, "请输入有效数字"); return ValidationResult.ValidResult; } } // XAML 验证模板 <ControlTemplate x:Key="ValidationTemplate"> <StackPanel> <AdornedElementPlaceholder/> <TextBlock Text="{Binding [0].ErrorContent}" Foreground="Red"/> </StackPanel> </ControlTemplate> ``` #### 5. 使用示例 ```csharp // 数据模型 public class Address { public string Street { get; set; } = "人民路"; public int Number { get; set; } = 100; } public class Person { public string Name { get; set; } = "张三"; public int Age { get; set; } = 30; public Address Address { get; set; } = new Address(); } // 初始化 public MainWindow() { InitializeComponent(); var vm = new NestedEditorViewModel(); vm.SourceObject = new Person(); // 设置源对象 DataContext = vm; } ``` ### 关键实现机制 1. **双向数据绑定**: - 使用 `Mode=TwoWay` 绑定确保 UI 更改同步到数据模型[^1] - 通过 `UpdateSourceTrigger` 控制更新时机(`PropertyChanged`/`LostFocus`) 2. **递归属性更新**: - 每个 `NestedProperty` 持有对象属性信息 - 值变更时通过反射更新源对象:`_propertyInfo.SetValue(_parent, value)` 3. **动态类型感知**: - 根据属性类型自动选择编辑控件(文本框/数值框等) - 复杂类型自动展开嵌套属性编辑器 4. **即时响应更新**: - 实现 `INotifyPropertyChanged` 接口通知层级变更 - 当嵌套对象被替换时自动刷新子属性集合 5. **数据验证集成**: - 使用 `ValidationRule` 实现输入验证 - 自定义错误模板显示验证消息[^3] > **性能提示**:对大型对象建议: > 1. 添加 `[DebuggerDisplay]` 属性优化反射性能 > 2. 实现延迟加载(展开节点时才加载子属性) > 3. 使用 `WeakReference` 防止内存泄漏 此方案支持任意深度的嵌套属性编辑,并确保所有修改自动同步回源对象,符合 WPF 的 MVVM 最佳实践[^2]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值