本鱼拟成立工作室承接项目开发/软件定制/云设施开发运维/办公设备技术支持等,如您有相关需求,欢迎来询 | ::博客文章推荐::

VS2010中WinForm中,“属性的代码生成失败”的问题,熟悉WinForm组件开发的同学进来讨论下

: 开发工具 木魚 8569℃ 0评论

UPDATED: 提供了可替换的解决方案,请参照本文末尾。

 

这是一个困扰我很久的问题,一直没有解决,搜索英文版的网站也没有能解决。姑且放在首页,用VS2010做过组件开发的同学进来讨论下。
简介:当一个在界面上提供扩展属性的组件(实现IExtenderProvider接口)使用项目引用或处在同一个项目里的时候,项目重编译后,组件上自定义的类别转换器将无法访问对应的类型,出现类型转换失败。

下面给出一个示例项目,求证是否是通用的问题还是我的VS设置的问题(源代码可以点此下载:http://files.cnblogs.com/nicch/vs2010_component_version_test.rar)。

现在假定要创建一个小组件,继承自系统的Label,功能是提供一个名为TipMessage的扩展属性。基本功能就是当这个Label放在对话框上的时候,可以为每个控件都扩展出一个名为“TipMessage”的属性,当鼠标在对应的控件上移过的时候,这个Label都会显示这个设置好的提示信息。为了稍微复杂一点,这里假定还可以设置文本的颜色,类似于下面的效果:

未命名-1

我们把这个组件和最终的窗体放在同一个项目中。

1.先定义一个TipMessage的类,储存提示信息

这个类没有什么可说的,直接上代码。

[TypeConverter(typeof(TipMessageConvertor))]public class TipMessage
{
///<summary>
/// 创建 <see cref="TipMessage" /> 的新实例
///</summary>
public TipMessage()
{
Color = SystemColors.ControlText;
}

///<summary>
/// 创建 <see cref="TipMessage" /> 的新实例
///</summary>
public TipMessage(string message, Color color)
{
Message = message;
Color = color;
}
///<summary>
/// 获得或设置要显示的信息
///</summary>
public string Message { get; set; }

///<summary>
/// 获得或设置显示的信息
///</summary>
public Color Color { get; set; }
}

这里自定义了一个类型转换器,现在遇到的问题主要就是在类型转换器中遇到的。这个类型,当时用VS的默认代码序列化时,会在设计器文件 (*.designer.cs) 生成类似这样的代码:

TipMessage message = new TipMessage();
message.Message = "";
message.Color = SystemColors.ControlText;

现在觉得这种方式有点累赘,想要将属性直接在构造函数中设置,像这样(有时候这是必须的):

TipMessage message = new TipMessage("", SystemColors.ControlText);

要生成这样的设计器代码,必须自己重写类别转换器。除此以外,因为 TipMessage 是我们自定义的一个类,VS并没有为它实现默认的设计器,如果不自己定义,那么实际上这个属性在属性窗格中是不可编辑的。

2.重写一个类别设计器

现在实现 TipMessageConvertor,继承自 ExpandableObjectConverter ,重写与转换相关的四个核心方法。

public class TipMessageConvertor : ExpandableObjectConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if (sourceType == typeof(string)) return true;

return base.CanConvertFrom(context, sourceType);
}

public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
{
if (value == null) return null;
if (value is string) return string.IsNullOrEmpty(value.ToString()) ? null : new TipMessage() { Message = value.ToString() };

return base.ConvertFrom(context, culture, value);
}

public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
if (destinationType == typeof(string) || destinationType == typeof(InstanceDescriptor)) return true;

return base.CanConvertTo(context, destinationType);
}

public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
{
if (value == null) return null;
var obj = (TipMessage)value; //注意这行,后面会出错

if (destinationType == typeof(string)) return obj.Message;
else if (destinationType == typeof(InstanceDescriptor))
{
var constructor = typeof(TipMessage).GetConstructor(new Type[]{
typeof(string),
typeof(Color)
});

return new InstanceDescriptor(constructor, new object[]{
obj.Message,obj.Color
}, true);
}

return base.ConvertTo(context, culture, value, destinationType);
}
}

这个类别设计器支持在 stringTipMessage 之间相互转换,除此之外还能将 TipMessage 转换为 InstanceDescriptorInstanceDescriptor 用来提供对象创建的实例化信息,VS根据它来生成目标对象的构造函数。

注意代码中的那行注释,问题就在那行中发生。

3. 生成 TipMessageLabel

因为本文不是为了讲述如何生成一个扩展属性的控件,因此对代码不做太详细的描述……有兴趣的同学请搜索相关的介绍文章。

[ProvideProperty("TipMessage", typeof(Control))]public class TipMessageLable : Label, IExtenderProvider
{
public TipMessageLable()
{
_message = new Dictionary<object, TipMessage>();
this.ForeColor = SystemColors.ControlText;
}

public bool CanExtend(object extendee)
{
return extendee is Control && extendee != this;
}

#region 扩展属性

Dictionary<object, TipMessage> _message;

///<summary>
/// 获得与指定控件相关联的提示信息
///</summary>
///<param name="control">关联的控件</param>
///<returns>相关的信息.如果没有,则返回null</returns>
public TipMessage GetTipMessage(Control control)
{
var msg = (TipMessage)null;

if (!_message.TryGetValue(control, out msg)) return null;
return msg;
}

public void SetTipMessage(Control control, TipMessage value)
{
if (value == null)
{
control.MouseEnter -= new EventHandler(ctl_MouseEnter);
control.MouseLeave -= new EventHandler(ctl_MouseLeave);
if (_message.ContainsKey(control)) _message.Remove(control);
}
else
{
control.MouseEnter += new EventHandler(ctl_MouseEnter);
control.MouseLeave += new EventHandler(ctl_MouseLeave);

if (_message.ContainsKey(control)) _message[control] = value;
else _message.Add(control, value);
}
}

void ctl_MouseLeave(object sender, EventArgs e)
{
base.Text = _defaultText;
base.ForeColor = _defaultColor;
}

void ctl_MouseEnter(object sender, EventArgs e)
{
var message = this.GetTipMessage(sender as Control);
if (message == null) return;

base.Text = message.Message;
base.ForeColor = message.Color;
}


#endregion

string _defaultText;
Color _defaultColor;
///<summary>
/// Gets or sets the text associated with this control.
///</summary>
///<returns>
/// The text associated with this control.
///</returns>
///<filterpriority>1</filterpriority>
public override string Text
{
get
{
return base.Text;
}
set
{
_defaultText = value;
base.Text = value;
}
}

public override System.Drawing.Color ForeColor
{
get
{
return base.ForeColor;
}
set
{
_defaultColor = value;
base.ForeColor = value;
}
}
}

 

4. 代码测试

至此,已经实现了基本的效果。在窗体的设计界面上,添加一个这样的 TipMessageLable 的控件,即能看到每个控件多会出来这样的一个属性:

Snap11

设置好每个按钮的扩展信息后,保存,编译。运行最终的程序,既可以看到最终的结果,与我们预期的相符(见这篇文章的开头)。

现在我们对代码稍微修改下,并重新编译。此时,问题就出现了。

重新打开上面窗口的设计器,并修改一些属性,点击保存——VS就会咣当一下蹦出这样的一个对话框:

Snap6

属性“TipMessage”的代码生成失败。错误是: “[A]ComponentTest.TipMessage 无法强制转换为 [B]ComponentTest.TipMessage。类型 A 源自“ComponentTest, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null”(在上下文“LoadNeither”中的“C:UsersWFAppDataLocalMicrosoftVisualStudio10.0ProjectAssemblies8hxo9xod01ComponentTest.exe”位置处)。类型 B 源自“ComponentTest, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null”(在上下文“LoadNeither”中的“C:UsersWFAppDataLocalMicrosoftVisualStudio10.0ProjectAssembliesdmc6p70601ComponentTest.exe”位置处)。”

似乎VS同时加载了两个版本的程序集,一个是之前编译的,一个是后来编译的。

使用ProcessExplorer查看VS加载的DLL,也证实了这种猜测(实际上可以看出来这里加载了4个版本的……):

Snap7

这里代码生成失败将会导致对相关属性的修改全部丢失。

每次打开这个项目,VS的设计器里面修改相关的属性,编译后,再去修改这些属性,便会遇到这个问题。

重置VS的设置似乎没有用。不知道有没有同学能验证下?也期待能有同学给出解决方案。

测试项目源码在此:http://files.cnblogs.com/nicch/vs2010_component_version_test.rar

 

UPDATED @ 2011年1月9日

——————————–

shuidao 同学在这里分析了这个异常发生的原因。窗体设计器在设计的时候,加载了原始编译的DLL,但是经过项目编译之后,设计器会使用新版本的程序集来生成代码,因此会导致新版本的程序集中的类型和旧版本程序集中的类型冲突,无法相互转换。

值得一提的是,在VS2008中并没有这样的问题,回想一下在VS2008中,如果重新编译了,窗体设计器会刷新一次,可以猜测为就是为了重新加载最新版本的程序集;但是在VS2010中却看不到这个刷新,也可以猜测正是这里没有刷新导致了这个问题。但是,就算在编译的时候关闭窗体设计器,在编译后重新打开,这样的异常还是会发生,证明VS2010根本就没有打算加载新版本的程序集来进行设计,应该属于VS2010的一个BUG。

在 Microsoft Connect 上VS Team曾经确认过这是Windows窗体设计器的一个BUG(并且已经标记为已修复),但是昨晚我特地卸载中文版的VS安装上英文版VS并打上SP1Beta补丁后,发现在这个问题依然存在(顺便说一下,SP1Beta补丁安装需要占用系统磁盘2.5G空间),期望落空。

至于替代的解决方法,shuidao 同学也提供了两条。昨晚我也在尝试使用反射来解决问题,但是这个方法如果对应的属性内容是很麻烦的,又比如还内嵌了其它自定义类型,则会比较麻烦,最好将除了必须在构造函数中指定的对象属性定义为DesignerSerializationVisibility(DesignerSerializationVisibility.Content),让VS来自动生成代码。另一个方法就是将TipMessage这样的类放在其它项目中,然后本项目只引用DLL,TypeConverter也和TipMessage类分开,这样重新编译的时候就不会导致这样的问题。但是这样做也就局限性,一个是在标记类别转换的时候,必须手动指定程序集的完整名称和类型(VS的做法);还有一个就是,TipMessage所在项目不是是解决方案中的项目,就算是也不可在解决方案中更改,否则一样会有这个问题发生(想到了可能有更完整的解决方案,但除了更复杂以外似乎没有很方便的做法)。

可见比较简单的还是使用反射。后面尝试下是否能写个通用的方法来自动绑定旧的类型到新的类型中。

最后感谢所有参与讨论的同学 Smile

喜欢 (1)
发表我的评论
取消评论
表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址