• 为dynamic添加智能感知


         dynamic的出现,给了我们另一片天空。可是这片蓝天下究竟有几颗大树呢?没有人知道,因为dynamic还没有带给我们心电感应。

         dynamic大致有以下几点令人诟病的地方:

    其一:效率。关于这点,其实很简单,只要你能忍受住第一次的疼,那后面的春光无限将任你遨游,当然,开销还是少不了的。
    其二:与扩展方法的兼容性。dynamic有一个很大的好处,就是语义上的优雅感,而扩展方法也同样带来了语义上的优雅感。可是,鱼与熊掌不可兼得,要想在扩展方法里传递dynamic类型的参数,就只能用传统静态方法调用方式去调用了。
    其三:受访问性限制的方法和调用显式实现的接口方法。这个不解释,打游击战吧。
    其四:没有智能感知。这个也就是本文要解决的内容了。也许在将来微软会考虑给它加上智能感知,可是,至少不是现在。唯有自己动手,才能丰衣足食。此外Resharp5也为dynamic加入了部分的智能感知功能。
    它提供的功能是: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则不然,如图

          image

          dynamic默认情况下是没有任何自动完成列表的。我们就可以利用这个,来判断是不是dynamic的调用。在AugmentCompletionSession方法中可以首先判断

    // 因为dynamic关键字的completionSets必然是Count为0的
    if (completionSets.Count != 0) return;

          在讲接下来的内容前,先介绍一下,我想使用扩展属性的方式

        class Test
        {
            public dynamic ExtendedProperties = new ExpandoObject();
        }

         如上所示,会把dynamic的属性封装在ExtendedProperties属性中,使用时则是a.ExtendedProperties.XXX

         于是可以检测是不是对ExtendedProperties的调用,只有ExtendedProperties的调用,才显示自动完成列表

                // 寻找"."之前的字符串是什么
                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。

         

          通过上面这些就可以实现我们要的效果了

    image

          注意:这里不会自动对自动完成列表进行Filter,但是,还是会高亮选择和自动滚动之类的。要实现Filter需要实现其他类。

    这里介绍一下VS2010的Extension Manager,我们的工程编译出来之后是UeqtDynamicIntellisense.vsix文件和一个dll,双击执行vsix文件之后就会进行extension的安装。

    安装完成后在VS2010的Extension Manager中就能看到

    image

          第一步算是完成了。接下来如果要维护代码,看到了test.ExtendedProperties.Hp这行内容,如何知道这个HP是啥呢?

          默认的显示是

    image

          我们希望这里能显示出一些提示信息,那么我们需要实现和注册IQuickInfoSourceProvider接口,以及实现IQuickInfoSource的public void AugmentQuickInfoSession(IQuickInfoSession session, IListquickInfoContent, out ITrackingSpan applicableToSpan)方法

         这里的实现步骤和上面的内容很相似,就不赘述了,给出实现后的效果图

         image

          这样我们需要的效果就基本到位了。等等,最重要的是不是还没说呢?怎么去自动根据已经写好的文件来读取要添加的自动完成呢?

          例如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提供智能感知了吧?但愿如此,期待吧。

  • 相关阅读:
    机器学习十讲学习笔记第四讲
    大三寒假学习进度笔记Day22
    机器学习十讲学习笔记第三讲
    大三寒假学习进度笔记Day21
    机器学习十讲学习笔记第二讲
    大三寒假学习进度笔记Day20
    机器学习十讲学习笔记第一讲
    大三寒假学习进度笔记Day19
    大三寒假学习进度笔记Day18
    大三寒假学习进度笔记Day17
  • 原文地址:https://www.cnblogs.com/ueqtxu/p/1800095.html
Copyright © 2020-2023  润新知