WinForms中依赖其他属性的自定义属性初始化异常问题
这个问题我之前做WinForms自定义控件时也踩过坑,设计器按字母顺序生成初始化代码的逻辑确实会给依赖其他属性的自定义属性带来麻烦。咱们先把问题捋清楚:你的MyExampleProperty在设置时依赖Height,但设计器生成代码时,MyExampleProperty(M开头)会比Size(S开头)先被赋值,这时候Height还是控件的默认值,导致你的判断逻辑把值重置成了1,保存窗体后就出现了值异常的情况。
下面给几个靠谱的解决办法,按推荐程度排序:
1. 实现ISupportInitialize接口(最推荐)
WinForms设计器本身提供了ISupportInitialize接口来处理这种多属性依赖的初始化场景。这个接口包含BeginInit和EndInit两个方法:设计器会先调用BeginInit标记初始化开始,然后批量设置所有属性,最后调用EndInit标记初始化完成。我们可以利用这个时机,缓存设计模式下的属性值,等所有属性(包括Size)都设置完成后再应用。
修改后的代码如下:
public class WhateverButton : Button, ISupportInitialize { private int m_myExampleProperty; private int? _pendingPropertyValue; // 缓存设计模式下待设置的属性值 private bool _isInitialized; public WhateverButton() { m_myExampleProperty = 1; } public int MyExampleProperty { get => m_myExampleProperty; set { // 如果是设计模式且未完成初始化,先缓存值 if (DesignMode && !_isInitialized) { _pendingPropertyValue = value; return; } // 正常逻辑:根据当前Height判断赋值 m_myExampleProperty = value > (this.Height / 2F) ? 1 : value; } } protected override void OnSizeChanged(EventArgs e) { base.OnSizeChanged(e); // Size变化时重新校验属性值 m_myExampleProperty = m_myExampleProperty > this.Height / 2F ? 1 : m_myExampleProperty; // 如果有缓存的待设置值,此时应用 if (_pendingPropertyValue.HasValue) { MyExampleProperty = _pendingPropertyValue.Value; _pendingPropertyValue = null; } } public void BeginInit() { _isInitialized = false; } public void EndInit() { _isInitialized = true; // 初始化完成后,应用缓存的属性值 if (_pendingPropertyValue.HasValue) { MyExampleProperty = _pendingPropertyValue.Value; _pendingPropertyValue = null; } } }
这样不管设计器生成代码的顺序如何,都会等所有属性设置完成后再处理MyExampleProperty的赋值,彻底避免了依赖属性未初始化的问题。
2. 用ShouldSerialize方法控制序列化行为
如果你不想实现接口,也可以通过添加ShouldSerializeMyExampleProperty和ResetMyExampleProperty方法,告诉设计器什么时候需要序列化这个属性。比如只有当属性值基于当前Size的计算结果不等于默认值时,才让设计器序列化它:
public class WhateverButton : Button { private int m_myExampleProperty; public WhateverButton() { m_myExampleProperty = 1; } public int MyExampleProperty { get => m_myExampleProperty; set { m_myExampleProperty = value > (this.Height / 2F) ? 1 : value; } } protected override void OnSizeChanged(EventArgs e) { base.OnSizeChanged(e); m_myExampleProperty = m_myExampleProperty > this.Height / 2F ? 1 : m_myExampleProperty; } // 告诉设计器:只有当属性值不是基于当前Height的默认结果时才序列化 public bool ShouldSerializeMyExampleProperty() { var expectedValue = m_myExampleProperty > Height / 2F ? 1 : m_myExampleProperty; return m_myExampleProperty != expectedValue; } // 重置属性的默认方法 public void ResetMyExampleProperty() { m_myExampleProperty = 1; } }
这个方法能避免设计器序列化错误的值,但本质上还是依赖代码顺序,可靠性不如第一种方案。
3. 修改属性名称(临时 workaround)
你提到的把属性名称改成排序在Size之后的(比如SizeDependentExampleProperty),确实能解决问题,但这只是一种临时的规避手段。如果后续新增其他依赖属性,或者设计器的排序逻辑有变化,可能又会出现类似问题,不推荐作为长期解决方案。
内容的提问来源于stack exchange,提问作者rick

