2007年3月28日星期三

CopySourceAsHtml 插件中文出现乱码的问题

上一篇文章中,我修正了CopySourceAsHtml 插件无法用于中文的Bug。 在VS2005的项目中CopySourceAsHtml 插件工作的很好。可是在VS2005的类库中执行“Copy As Html...”命令却会出现乱码。例如,复制一个类库中的一行C#代码:

47 int i = 0; // 啊


会变成:

47 int i = 0; // 啊'b0?


在“啊”的后面对了“'b0?”这几个字母。 这是为什么呢?

查找问题原因

在执行了“Copy As Html...” 命令后,打开记事本,进行“粘贴”操作。可以看到生成的HTML代码为:


&#21834 是“啊”的Unicode码,'b0?是什么东东呢?看来还得看源代码。在CopySourceAsHtml的源代码的Connnect.cs 的Copy(bool copyNow)函数中,有这样一段代码:

373 try

374 {

375 rtf = Clipboard.GetRtf();

376 if (rtf == null)

377 {

378 MessageBox.Show(Resources.NoRtfDataOnClipboardMessage, Resources.NoRtfDataOnClipboardCaption, MessageBoxButtons.OK, MessageBoxIcon.Error);

379 return;

380 }

381 }

382 catch (ExternalException)

383 {

384 MessageBox.Show(Resources.UnableToAccessClipboardMessage, Resources.UnableToAccessClipboardCaption, MessageBoxButtons.OK, MessageBoxIcon.Error);

385 return;

386 }

387

388 html = copier.CopyAsHtml(rtf);

389

390 Clipboard.SetHtml(html);


很明显,CopySourceAsHtml的工作原理是先取得VS2005存放在剪贴板中的RTF字符串(第375行),然后将RTF字符串解析、替换成HTML标记的代码(第388行)。
那么,造成乱码的原因就可能有两个:(1) VS2005存放在剪贴板中的RTF文本有问题(毕竟只有在类库中使用CopySourceAsHtml插件会出现乱码。(2) CopySourceAsHtml插件解析RTF的代码有问题。

VS2005存放在剪贴板中的RTF文本

在VS2005中选中一个汉字,例如:"啊",按Ctrl+C进行复制,然后使用通过一段小程序即可取得VS2005 放到剪贴板中的RFT文本:

130 private void button1_Click(object sender, EventArgs e)

131 {

132 textBox1.Text = System.Windows.Forms.Clipboard.GetText(TextDataFormat.Rtf);

133 }


发现RTF文本中啊被表示成了 “... \fs18 \cf11 \uc2\u21834 \'b0\'a1}”。其中21834是“啊”的Unicode编码,b0a1是“啊”的GB2312编码。不知道为什么VS2005会同时将这两种编码全都放到RTF文本中。不过如果把这段RTF文本放到一个.RTF 文件中,会发现写字板会将\u21834 过滤掉,只留下\'b0\'a1。而CopySourceAsHtml并未进行这些过滤,结果出现了“啊'b0?”这样的结果。

修正

俺对RTF格式一点都不了解,下载了一个RTF Spec也看得头大。幸好有前辈高人已经将这个问题解决了,请见Huan-Lin's Blog的文章http://huanlin.dyndns.org/cs/blogs/huan-lins_blog/archive/2006/10/20/CopySourceAsHtml-Fix.aspx。Hua-Lin修正了多处Unicode方面的Bug,真的很佩服他。

2007年3月24日星期六

修正 CopySourceAsHTML 插件无法用于中文 VS2005 的Bug

为了写Blog,我下载了一个叫 CopySourceAsHTML 的 VS2005 插件,通过它为VS2005增加的“Copy As HTML...”命令复制出来的代码会被HTML标记包裹,可以直接粘贴到Blog的HTML标记中(本篇文章中的代码就是用它作成的^_^)。

令人遗憾的是,当我兴冲冲地下载了CopySourceAsHTML 的安装文件 “CopySourceAsHtml.msi” 并且完成安装后,启动 VS2005,看到了这样一个 Exception:




进入VS2005后,发现菜单上也没有“Copy As Html...”选项。好在CopySourceAsHtml是一个开源软件,通过分析其源代码,笔者找到了修正此Bug的方法。

分析源代码

可以看到 Exception 的信息是“值不在预期范围内”,位置是 Connect.AddControls() 中对 CommandBars.CommandBarControls.get_Item() 的调用。安装了CopySourceAsHtml软件后,在“C:\Program Files\J.T. Leigh & Associates Inc\CopySourceAsHtml\” 可以找到一个叫 “Source.zip” 的压缩包,即是源代码了。将其解压缩,双击“Source\CopySourceAsHtml.sln” 打开工程,再打开 “Connect.cs”,来到第195行,可以看到如下代码:

187 private void AddControls()

188 {

189

190 CommandBars commandBars;

191 CommandBarPopup editPopup;

192 int copyIndex;

193

194 commandBars = (CommandBars)this.application.CommandBars;

195 editPopup = (CommandBarPopup)commandBars["MenuBar"].Controls["Edit"];

196

197 copyIndex = FindCopyOnCommandBar("Edit") + 1;




猜测不是在commandBars中找不到"MenuBar"就是在commandBars["MenuBar"].Controls中找不到"Edit"。

在第194行后面插入如下代码:

194 commandBars = (CommandBars)this.application.CommandBars;

195 // test

196 System.IO.StringWriter msg = new StringWriter();

197 foreach (CommandBar eachCommandBar in commandBars)

198 {

199 msg.Write(eachCommandBar.Name + " ");

200 }

201 throw new Exception(msg.ToString());

202 editPopup = (CommandBarPopup)commandBars["MenuBar"].Controls["Edit"];




重新编译工程,然后将新生成的CopySourceAsHtml.dll(在C:\Program Files\J.T. Leigh & Associates Inc\CopySourceAsHtml\Source\Source\Bin\) 复制粘贴到“My Documents\Visual Studio 2005\Addins\”覆盖同名文件(如果提示文件正在使用中,则需要关闭VS2005。也可以将原来的CopySourceAsHtml.dll重命名为CopySourceAsHtml.dll.old再粘贴新的CopySourceAsHtml.dll),然后打开一个新的VS2005,就可以看到commandBars中的所有内容了,发现有“MenuBar”。将测试代码变为:

194 commandBars = (CommandBars)this.application.CommandBars;

195 // test

196 System.IO.StringWriter msg = new StringWriter();

197 foreach (CommandBarControl eachCommandBarControl in commandBars["MenuBar"].Controls)

198 {

199 msg.Write(eachCommandBarControl.Caption + " ");

200 }

201 throw new Exception(msg.ToString());

202 editPopup = (CommandBarPopup)commandBars["MenuBar"].Controls["Edit"];



再次替换“My Documents\Visual Studio 2005\Addins\CopySourceAsHtml.dll”,重启VS2005,可以看到commandBars["MenuBar"].Controls中每个CommandBarControl的Caption都是中文的,其中与Edit相对应的应该是“编辑”,将editPopup = (CommandBarPopup)commandBars["MenuBar"].Controls["Edit"]; 替换为editPopup = (CommandBarPopup)commandBars["MenuBar"].Controls["编辑"]; 再次替换CopySourceAsHtml.dll并重启VS2005,Exception不再显现了。 现在源代码变为:

187 private void AddControls()

188 {

189

190 CommandBars commandBars;

191 CommandBarPopup editPopup;

192 int copyIndex;

193

194 commandBars = (CommandBars)this.application.CommandBars;

195 //editPopup = (CommandBarPopup)commandBars["MenuBar"].Controls["Edit"];

196 editPopup = (CommandBarPopup)commandBars["MenuBar"].Controls["编辑"];

197

198 copyIndex = FindCopyOnCommandBar("Edit") + 1;



不过“Copy As HTML...”命令出现的位置似乎不对,而且我们记得在第一次看到的Exception中提示在Connect.cs的第63行也有错误。通过与上面相似的方法,可以发现菜单“编辑|复制”不是英文的“Copy”而是“复制(C)”。修正的方法是在VS2005中的解决方案资源管理器中右击CopySourceAsHTML项目,在上下文菜单中点击“属性”,点击“资源”选项卡,将名称为“CopyMenuItemText”的值由原来的“Copy”更改为“复制(C)”。再次替换“My Documents\Visual Studio 2005\Addins\CopySourceAsHtml.dll”,重启VS2005,CopySourceAsHTML已经可以正常工作了。

下载修正后的CopySourceAsHtml.dll

如果你觉得自己改代码太麻烦,可以直接下载我的修正过的CopySourceAsHtml.dll文件。请按如下步骤操作:
  1. 下载CopySourceAsHtml的安装文件CopySourceAsHtml.msi,并安装。
  2. 下载我修正过的CopySourceAsHtml.dll文件,地址为:http://download.csdn.net/source/162560
  3. 将第二步下载的CopySourceAsHtml.dll文件复制到“My Documents\Visual Studio 2005\Addins\”覆盖同名文件。
  4. 重新启动VS2005。

一些疑问

CopySourceAsHtml这个插件之所以会抛出异常,是因为application.CommandBars.Controls的ObjectIndex由英文版VS2005的“Edit”变成了中文版VS2005的“编辑”。我对插件编程一窍不通,不知到为什么会发生这种事情,但想来不外乎三种情况:
  • CopySourceAsHtml这个插件使用了错误的访问application.CommandBars.Controls的方法。例如,也许存在一种英文版VS2005和中文版VS2005通用的访问application.CommandBars.Controls的方法,使得不论是哪个语言版本的VS2005都能正常使用这个插件。或者:
  • 中文版VS2005使用了错误的汉化方法。例如不应该直接将“Edit”改为“编辑”,而应该在资源文件中进行更改。或者:
  • VS2005的application.CommandBars.Controls设计有缺陷,没有考虑国际化方面的问题。造成无法编写出适用于所有语言版本的VS2005的插件。

请网友高人不吝赐教。

2007年3月23日星期五

应用泛型和匿名函数打造 DictionaryBuilder

编写数据库相关的程序,Dictinary是一个比较常用的工具。例如,在你的系统数据库中有一个“部门”表:

部门ID部门名称部门人数
1信息系统部17
2总务部23
3销售部55
4制造部880
5技术部250

与之对应的 Domain Object 为 Department:

public class Department

{

  private long _id;

  public long id // 部门ID

  {

    get { return _id; }

    set { _id = value; }

  }

  private string _name;

  public string name // 部门名称

  {

    get { return _name; }

    set { _name = value; }

  }

  private int _deployeeCount;

  public int deployeeCount // 部门人数

  {

    get { return _deployeeCount; }

    set { _deployeeCount = value; }

  }

}


假设我们已经从数据库中取得了部门表中的所有数据,并由这些数据创建了5个Department的实体(每条数据一个Department的实体),这些数据存放在一个List对象中,伪代码为:
List<department> departments = 从数据库中取得部门表中的所有数据并创建一个List;
创建一个以部门ID为Key的Dictonary的代码为:

Dictionary<long, Department> id_Department_Dic = new Dictionary<long, Department>();

foreach (Department eachDepartment in departments)

{

  if (!id_Department_Dic.ContainsKey(eachDepartment.id))

  {

    id_Department_Dic.Add(eachDepartment.id, eachDepartment);

  }

}


创建一个以部门名称为Key的Dictionary的代码为:

Dictionary<long, Department> name_Department_Dic = new Dictionary<long, Department>();

foreach (Department eachDepartment in departments)

{

  if (!name_Department_Dic.ContainsKey(eachDepartment.name))

  {

    name_Department_Dic.Add(eachDepartment.name, eachDepartment);

  }

}


看出这两段代码的不同之处了么?没错,仅仅是将两处“id”换成了“name”而已。你是不是已经嗅到了腐朽的气味了呢?让我们来把重复的代码提炼出来吧。

重构

我们希望把创建Dictionary的代码提炼到一个通用的函数中,把Key和Value作为参数传递到这个函数中。第一种方法是使用反射机制,代码类似于这样:

public Dictionary<long, Department> buildDictionary(string keyName, List<Department> src)

{

  foreach (Department eachDepartment in src)

  {

    long id = 通过反射机制从eachDepartment 中取得名为keyName的属性的值;

    if (!id_Department_Dic.ContainsKey(id))

    {

      id_Department_Dic.Add(id, eachDepartment);

    }

  }

}


使用反射的一大缺点是速度很慢,慢到了令人无法忍受的地步。还有一个缺点是作为Key的属性的名称是通过一个string类型的参数传递到函数中的,如果属性名拼写错了,只有在运行时才能发现。所以我们放弃使用这一方法。

第二种方法是使用委托机制。什么是委托呢?如果你熟悉C++,可以将委托当作函数指针来使用。或者,你可以把委托理解为一个类(Class),只不过这个类中定义的不是普通的属性和方法,而是定义了函数的参数个数、参数类型和返回值的类型。

我们可以在类库中新建一个叫DictionaryBuilder的类,它接收从Client传来的List,并委托Client告诉它每个Department的Key是什么。DictionaryBuilder类和Client代码见下图:


使用匿名函数

其实我们不必非得在Client代码中写一个getKey()函数,因为.Net 2.0提供了一个叫“匿名函数”的功能。我们在Client代码中可以直接这样写:

class Program

{

  static void Main(string[] args)

  {

    IList<Department> departments = new List<Department>();

    //Dictionary<long, Department> dic

    //  = new DictionaryBuilder(departments).toDictionary(getKey);

    Dictionary<long, Department> dic

      = new DictionaryBuilder(departments).toDictionary(

      delegate(Department arg)

      {

        return arg.id;

      });

  }

  //private static long getKey(Department arg)

  //{

  //  return arg.id;

  //}

}


匿名函数的原理和妙处留给大家慢慢体会,我就不多说了(其实是我也不知道)。

加上泛型

当然,我们费了半天的劲写的DictionaryBuilder如果只能接收List<Department>就太不划算了,我们的DictionaryBuilder要能够接受任何类型!这就要使用 .Net 2.0 新增的泛型功能。让我们来先看看代码吧:

public class DictionaryBuilder<TKey, TValue>

{

  private ICollection<TValue> _values;

  public delegate TKey GetKey(TValue obj);

  public DictionaryBuilder(ICollection<TValue> values)

  {

    _values = values;

  }

  public Dictionary<TKey, TValue> toDictionary(GetKey getKey)

  {

    Dictionary<TKey, TValue> result =

      new Dictionary<TKey, TValue>();

    foreach (TValue eachValue in _values)

    {

      TKey key = getKey(eachValue);

      if (!result.ContainsKey(key))

      {

        result.Add(key, eachValue);

      }

    }

    return result;

  }

}


觉得有些难懂么?如果你把这段代码与上图中的代码仔细对比一下,就会发现,只不过是把所有的 long 换成了TKey,把所有的Department换成了TValue而已!呵呵,简单吧?单从使用泛型的角度讲,只要把泛型理解成“类型的占位符”就行了。我们的Client代码也需要做一点小小的改变(涂了阴影的部分):

class Program

{

  static void Main(string[] args)

  {

    IList<Department> departments = new List<Department>();

    //Dictionary<long, Department> dic

    //  = new DictionaryBuilder(departments).toDictionary(getKey);

    Dictionary<long, Department> dic

      = new DictionaryBuilder<long, Department>(departments).toDictionary(

      delegate(Department arg)

      {

        return arg.id;

      });

  }

  //private static long getKey(Department arg)

  //{

  //  return arg.id;

  //}

}



好了,到目前为止主要工作均已完成,我的DictionaryBuilder要比上文提到的复杂一点点,可以在这里下载: http://docs.google.com/Doc?id=dgwxq3xg_2fmtd98
(完)