其一:效率。关于这点,其实很简单,只要你能忍受住第一次的疼,那后面的春光无限将任你遨游,当然,开销还是少不了的。其二:与扩展方法的兼容性。dynamic有一个很大的好处,就是语义上的优雅感,而扩展方法也同样带来了语义上的优雅感。可是,鱼与熊掌不可兼得,要想在扩展方法里传递dynamic类型的参数,就只能用传统静态方法调用方式去调用了。其三:受访问性限制的方法和调用显式实现的接口方法。这个不解释,打游击战吧。其四:没有智能感知。这个也就是本文要解决的内容了。也许在将来微软会考虑给它加上智能感知,可是,至少不是现在。唯有自己动手,才能丰衣足食。此外Resharp5也为dynamic加入了部分的智能感知功能。它提供的功能是:dynamic实例使用过一次的属性或方法后,之后就能感知到这个属性或方法,但是问题在于,对于同一个类型的多个实例,这个感知并不通用,而且基类中使用过的属性,在继承类中也感知不到。
void Set(string prop, object data)
T Query<T>(string prop, T defaultValue = default(T))
void Delete(string prop)
上面这组方法的使用也很方便,只是语义优雅感上有所欠缺,就像访问属性全都写成调用方法的方式,方案上可以接受,可是心理上那个憋屈啊。
那么有了dynamic之后我们是不是就可以指哪点哪了呢?答案是肯定的。
本来的++操作需要写成 a.Set(Constants.PropName, a.Query<int>(Constants.PropName, 0) + 1);
现在则只需写成a.PropName++;
孰优孰劣,一望便知。
可是原来的属性名可以用常量来定义,可以轻松的感知和归类所有的属性名,并且接受编译器的校验,可是现在的dynamic则没有了这层保护,当属性一多之后,维护的难度几何级数加大。
问题摆出来了,下面就想办法解决吧。
工欲善其事必先利其器,要为VS2010添加智能感知,首先要安装VS2010 SDK,可以在这里下载。
要自定义智能感知,首先要实现ICompletionSourceProvider接口,注意,这里用到的接口都要添加一堆Microsoft.VisualStudio.XXX.dll的引用,这里不一一列出了,请自行查阅相关资料。
这个接口很简单,只有一个方法
public ICompletionSource TryCreateCompletionSource(ITextBuffer textBuffer) { return new UeqtDynamicCompletionSource(this, textBuffer); }
要让VS知道这个接口实现的存在,需要用到MEF,如下Export进去。
[Export(typeof (ICompletionSourceProvider))]
[ContentType("CSharp")]
[Order(After = "default")]
[Name("UeqtDynamicCompletion")]
internal class UeqtDynamicCompletionSourceProvider : ICompletionSourceProvider
这里Export的类型就是接口的类型,ContentType是需要控制的文件类型,CSharp就是cs文件,text就是所有的非二进制文件等等。Name则是自己随意定义的,用来标识这个实现的。Order则是在MEF中执行的次序,这里设置为
在默认执行之后执行我们自定义的方法,因为这样就能获取到VS已经执行完毕的CompletionSourceSet。
接下来就是实现我们在方法里调用的类internal class UeqtDynamicCompletionSource : ICompletionSource,这个类实现了ICompletionSource接口
主要方法是void ICompletionSource.AugmentCompletionSession(ICompletionSession session, IList<CompletionSet> completionSets)
这里传入的session就是当前文档的所有内容,而completionSets则是VS默认的行为执行完之后的结果集。
因为一般的对象都是继承自object的,所以输入.之后必然会有自动完成的内容,也就是completionSets.Count必定是大于0的,而dynamic则不然,如图
dynamic默认情况下是没有任何自动完成列表的。我们就可以利用这个,来判断是不是dynamic的调用。在AugmentCompletionSession方法中可以首先判断
// 因为dynamic关键字的completionSets必然是Count为0的
if (completionSets.Count != 0) return;
在讲接下来的内容前,先介绍一下,我想使用扩展属性的方式
class Test { public dynamic ExtendedProperties = new ExpandoObject(); }
// 寻找"."之前的字符串是什么
SnapshotPoint currentPoint = (session.TextView.Caret.Position.BufferPosition) - 1;
string allText = session.TextView.TextSnapshot.GetText();
if (allText.Length - currentPoint.Position > UeqtDynamicHelpers.ExtendedProperties.Length)
{
// 检查是不是ExtendedProperties.
if (
allText.Substring(
allText.Substring(0, currentPoint.Position).LastIndexOf('.') - UeqtDynamicHelpers.ExtendedProperties.Length,
UeqtDynamicHelpers.ExtendedProperties.Length) == UeqtDynamicHelpers.ExtendedProperties ||
(allText.Substring(currentPoint.Position, 1) == "." &&
allText.Substring(currentPoint.Position - UeqtDynamicHelpers.ExtendedProperties.Length,
UeqtDynamicHelpers.ExtendedProperties.Length) == UeqtDynamicHelpers.ExtendedProperties))
{
当检测成功时,我们就生成自己的自动完成列表,如何去拿指定文件中定义的列表,先放一放,过会讲,这里先临时创建一些
var mCompList = new List<Completion>();
mCompList.Add(new Completion(“hp”,".hp”,"这是hp,int类型", null, null)
先介绍一下Completion构造函数的5个参数,第一个就是自动完成列表里要出现的内容,第二个则是选择了这个自动完成时要插入的内容,因为在IOleCommandTarget过滤了点,所以这里要多插入个点。
第三个参数是描述,最后两个则是要显示的图标,例如属性的图标或是字段的图标,要用图标的话,可以使用系统默认的,例如
_mSourceProvider.GlyphService.GetGlyph(StandardGlyphGroup.GlyphGroupProperty, StandardGlyphItem.GlyphItemPublic), "72"));
首先需要在UeqtDynamicCompletionSourceProvider类里添加
[Import]
internal IGlyphService GlyphService { get; set; }
这样就能使用系统的图标了。
最后把自动完成列表加入列表集里
var set = new CompletionSet(
"Ueqt",
"Ueqt",
FindTokenSpanAtPosition(session.GetTriggerPoint(_mTextBuffer), session),
mCompList,
null);completionSets.Add(set);
一切似乎已经完成了,可是当我们运行起来后,就会发现,其他任何时候输入东西都会触发AugmentCompletionSession方法的调用,可是对于dynamic,却无论如何不会触发,因为在默认的IOleCommandTarget里被过滤掉了。
那我们只能自己实现IOleCommandTarget了。首先注册一个IVsTextViewCreationListener的实现
[Export(typeof(IVsTextViewCreationListener))] [Name("ueqt completion handler")] [ContentType("CSharp")] [TextViewRole(PredefinedTextViewRoles.Editable)] internal sealed class UeqtVsTextViewCreationListener : IVsTextViewCreationListener
这个接口实现了一个方法
public void VsTextViewCreated(IVsTextView textViewAdapter)
{
IWpfTextView view = AdaptersFactory.GetWpfTextView(textViewAdapter);
if (view == null) return;UeqtCommandFilter filter = new UeqtCommandFilter(view, CompletionBroker);
IOleCommandTarget next;
textViewAdapter.AddCommandFilter(filter, out next);
filter.Next = next;
}
这个方法调用的internal sealed class UeqtCommandFilter : IOleCommandTarget,这个就是我们要新添加的行为
这个接口核心的方法是public int Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut)
VS中所有的操作都会走进来,我们要做的就是在判断出输入了点之后,我们去触发AugmentCompletionSession方法。
public int Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut) { bool handled = false; int hresult = VSConstants.S_OK; // 1. Pre-process if (pguidCmdGroup == VSConstants.VSStd2K) { char typedChar = char.MinValue; //make sure the input is a char before getting it if (pguidCmdGroup == VSConstants.VSStd2K && nCmdID == (uint)VSConstants.VSStd2KCmdID.TYPECHAR) { typedChar = (char)(ushort)Marshal.GetObjectForNativeVariant(pvaIn); } if ((VSConstants.VSStd2KCmdID)nCmdID == VSConstants.VSStd2KCmdID.AUTOCOMPLETE || (VSConstants.VSStd2KCmdID)nCmdID == VSConstants.VSStd2KCmdID.COMPLETEWORD) { handled = StartSession(); } else if (nCmdID == (uint)VSConstants.VSStd2KCmdID.RETURN || nCmdID == (uint)VSConstants.VSStd2KCmdID.TAB || (char.IsWhiteSpace(typedChar) || char.IsPunctuation(typedChar)) && typedChar != '.') { if (nCmdID == (uint)VSConstants.VSStd2KCmdID.RETURN || nCmdID == (uint)VSConstants.VSStd2KCmdID.TAB) { if (_currentSession != null && _currentSession.SelectedCompletionSet != null) { // 自动完成后不输入回车和Tab handled = true; } } else { handled = false; } Complete(); } else if ((VSConstants.VSStd2KCmdID)nCmdID == VSConstants.VSStd2KCmdID.CANCEL) { handled = Cancel(); } } if (!handled) hresult = Next.Exec(pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut); if (ErrorHandler.Succeeded(hresult)) { if (pguidCmdGroup == VSConstants.VSStd2K) { switch ((VSConstants.VSStd2KCmdID)nCmdID) { case VSConstants.VSStd2KCmdID.TYPECHAR: char ch = GetTypeChar(pvaIn); if (ch == '.') { // 这里是关键,否则dynamic不会进ICompletionSource.AugmentCompletionSession _currentSession = null; StartSession(); } //if (ch == ' ') // StartSession(); else if (_currentSession != null && _currentSession.SelectedCompletionSet != null) Filter(); break; case VSConstants.VSStd2KCmdID.BACKSPACE: if (_currentSession != null && _currentSession.SelectedCompletionSet != null) { Filter(); } break; } } } return hresult; }
直接上代码吧,不解释了,关键的地方写了注释了,就在那里会创建对象,触发AugmentCompletionSession。
在Next.Exec(pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut); 上面做的都是判断自动完成是否结束了,结束就Commit或者Cancel。
通过上面这些就可以实现我们要的效果了
注意:这里不会自动对自动完成列表进行Filter,但是,还是会高亮选择和自动滚动之类的。要实现Filter需要实现其他类。
这里介绍一下VS2010的Extension Manager,我们的工程编译出来之后是UeqtDynamicIntellisense.vsix文件和一个dll,双击执行vsix文件之后就会进行extension的安装。
安装完成后在VS2010的Extension Manager中就能看到
第一步算是完成了。接下来如果要维护代码,看到了test.ExtendedProperties.Hp这行内容,如何知道这个HP是啥呢?
默认的显示是
我们希望这里能显示出一些提示信息,那么我们需要实现和注册IQuickInfoSourceProvider接口,以及实现IQuickInfoSource的public void AugmentQuickInfoSession(IQuickInfoSession session, IListquickInfoContent, out ITrackingSpan applicableToSpan)方法
这里的实现步骤和上面的内容很相似,就不赘述了,给出实现后的效果图
这样我们需要的效果就基本到位了。等等,最重要的是不是还没说呢?怎么去自动根据已经写好的文件来读取要添加的自动完成呢?
例如solution里有这样一个文件UeqtExtendedPropertiesConstants.cs
namespace Ueqt
{
class UeqtExtendedPropertiesConstants
{
/// <summary>
/// 这是HP
/// string
/// </summary>
public const string Hp = "hp";}
}
要自动从这个文件里读取到HP的信息,并且能随改随更新
其实也不难,只要找到了方法。
// 找到这个文件
EnvDTE.ProjectItem item = UeqtDynamicHelpers.FindProjectItem("UeqtExtendedPropertiesConstants.cs");
if (item != null)
{
FileCodeModel fileCM = item.FileCodeModel;
CodeNamespace myNameSpace = (CodeNamespace)fileCM.CodeElements.Item("Ueqt");
if (myNameSpace == null)
{
return;
}
CodeClass myClass = (CodeClass)myNameSpace.Children.Item("UeqtExtendedPropertiesConstants");
if (myClass == null)
{
return;
}
foreach (CodeElement ele in myClass.Members)
{
string name = ele.Name;
string docComment = string.Empty;
CodeVariable var = ele as CodeVariable;
if(var!=null)
{
string comment = var.Comment;
docComment = UeqtDynamicHelpers.ShowDocComment(var.DocComment);
}
mCompList.Add(new Completion(name, "." + name, docComment, _mSourceProvider.GlyphService.GetGlyph(StandardGlyphGroup.GlyphGroupProperty, StandardGlyphItem.GlyphItemPublic), "72"));
}
}
就是这个FileCodeModel ,有了它,你就不用自己再去分析一遍文本内容了。
这样智能感知就算基本到位了,后续可以做的工作就是为dynamic添加编译时的自动校验,检查属性是否存在,这也不难,不过要检查类型是否匹配,似乎比较复杂。
先到这里吧。也许下一个版本的VS就会对dynamic提供智能感知了吧?但愿如此,期待吧。