Extensions in VB-Modulen in Intellisense "verstecken"

    • Allgemein

      Extensions in VB-Modulen in Intellisense "verstecken"

      Kennen Sie das?
      Sie haben eine tolle Bibliothek mit Extension-Methoden geschrieben aber Intellisense ist immer voll mit Einträgen, auch wenn Sie sie nicht brauchen?

      Gibt es denn keine bessere Lösung?

      Doch!
      Naja, teilweise. C# und VB behandeln Extensions ja unterschiedlich:

      C#-Quellcode

      1. public static class Extensions
      2. {
      3. public static void Foo(this int Target) { }
      4. }

      VB.NET-Quellcode

      1. Public Module Extensions
      2. <System.Runtime.CompilerServices.Extension()>
      3. Public Sub Foo(Target As Integer)
      4. End Sub
      5. End Module

      Das kompiliert zwar in beiden Fällen zu Klassen, die statische Methoden beinhalten, aber die sind leicht unterschiedlich:

      CIL-Quellcode

      1. // VB
      2. .class public auto ansi sealed Extensions extends Object
      3. {
      4. .custom instance void StandardModuleAttribute::.ctor() = (01 00 00 00)
      5. .custom instance void ExtensionAttribute::.ctor() = (01 00 00 00)
      6. .method public static void Foo (int32 Target) cil managed
      7. {
      8. .custom instance void ExtensionAttribute::.ctor() = (01 00 00 00)
      9. //...
      10. }
      11. }
      12. // C#
      13. .class public auto ansi abstract sealed beforefieldinit Extensions extends Object
      14. {
      15. .custom instance void ExtensionAttribute::.ctor() = (01 00 00 00)
      16. .method public hidebysig static void Foo (int32 Target) cil managed
      17. {
      18. .custom instance void ExtensionAttribute::.ctor() = (01 00 00 00)
      19. //...
      20. }
      21. }

      Es gibt folgende Unterschiede:
      Die C#-Klasse ist abstrakt und "BeforeFieldInit".
      Die VB-Klasse hat zusätzlich das StandardModule-Attribut.
      Die C#-Methode ist "HideBySig".

      Das StandardModule-Attribut sorgt in VB dafür, dass alle Member des Moduls sichtbar sind, wenn das Modul sichtbar ist. Aber: Extension-Methoden, die in C# geschrieben wurden, können auch in VB verwendet werden, obwohl das Attribut fehlt.

      In einer unerwarteten Eingebung heute Nachmittag kam mir der Gedanke, dass man ja Mono.Cecil verwenden könnte, die VB-Klassen nachträglich so umzubauen, dass sie wie die C#-Klassen aussehen.
      Gesagt, getan. Das folgende ist eine Konsolenanwendung. Das Befehlszeilenargument ist ein Pfad zu einer Assembly, in der alle Module umgebastelt werden.

      VB.NET-Quellcode

      1. Public Class Program
      2. Public Const InvalidArgCount = 2
      3. Public Const NonExistingAssembly = 3
      4. Public Const AssemblyNotLoaded = 4
      5. Public Const AssemblyNotSaved = 5
      6. Public Shared Function Main(Args As String()) As Integer
      7. 'Assembly öffnen
      8. 'Für alle Typen mit StandardModuleAttribute
      9. ' Attribute hinzufügen: abstract, beforefieldinit
      10. ' CustomAttribute entfernen: StandardModuleAttribute
      11. ' Für alle Methoden im Typ mit ExtensionAttribute
      12. ' Attribute hinzufügen: hidebysig
      13. 'Assembly speichern
      14. If Args.Length < 1 Then
      15. Console.WriteLine("Zu wenig Argumente")
      16. Return InvalidArgCount
      17. End If
      18. Dim AssemblyFilePath = Args(0)
      19. If Not System.IO.File.Exists(AssemblyFilePath) Then
      20. Console.WriteLine("Datei existiert nicht")
      21. Return NonExistingAssembly
      22. End If
      23. Dim Asm As Mono.Cecil.AssemblyDefinition
      24. Try
      25. Asm = Mono.Cecil.AssemblyDefinition.ReadAssembly(AssemblyFilePath)
      26. Catch ex As Exception
      27. Console.WriteLine("Fehler beim Laden der Assembly: " & ex.GetType.Name)
      28. Console.WriteLine(ex.Message)
      29. Return AssemblyNotLoaded
      30. End Try
      31. ProcessTypes(Asm.Modules.SelectMany(Function(i) i.Types))
      32. Try
      33. Asm.Write(AssemblyFilePath)
      34. Catch ex As Exception
      35. Console.WriteLine("Fehler beim Speichern der Assembly: " & ex.GetType.Name)
      36. Console.WriteLine(ex.Message)
      37. Return AssemblyNotSaved
      38. End Try
      39. Console.WriteLine("Fertig")
      40. Return 0
      41. End Function
      42. Private Shared Sub ProcessTypes(Types As IEnumerable(Of Mono.Cecil.TypeDefinition))
      43. For Each Type In Types
      44. Dim ModuleAttributeMatch = Type.CustomAttributes.SingleOrDefault(Function(i) i.AttributeType.FullName = "Microsoft.VisualBasic.CompilerServices.StandardModuleAttribute")
      45. If ModuleAttributeMatch IsNot Nothing Then
      46. Console.WriteLine("Modul: " & Type.FullName)
      47. Type.Attributes = Type.Attributes Or Mono.Cecil.TypeAttributes.Abstract Or Mono.Cecil.TypeAttributes.BeforeFieldInit
      48. For Each Method In Type.Methods
      49. If Not Method.IsConstructor AndAlso Not Method.IsGetter AndAlso Not Method.IsSetter Then
      50. Dim ExtensionAttributeMatch = Method.CustomAttributes.SingleOrDefault(Function(i) i.AttributeType.FullName = "System.Runtime.CompilerServices.ExtensionAttribute")
      51. If ExtensionAttributeMatch IsNot Nothing Then
      52. Console.WriteLine(" Extension: " & Method.Name)
      53. Method.Attributes = Method.Attributes Or Mono.Cecil.MethodAttributes.HideBySig
      54. End If
      55. End If
      56. Next
      57. Type.CustomAttributes.Remove(ModuleAttributeMatch)
      58. End If
      59. ProcessTypes(Type.NestedTypes)
      60. Next
      61. End Sub
      62. End Class

      Es ist nicht unbedingt nötig, die Attributes (nicht zu verwechseln mit CustomAttributes, die Namenskonventionen sind hier ungünstig gewählt) der Klassen und Methoden anzupassen. Es reicht, das StandardModule-Attribut zu entfernen, um die Methoden in Intellisense auszublenden. Welche Nebeneffekte das aber sonst noch haben könnte, hab ich nicht getestet.

      Der aufmerksame Leser wird anmerken, dass das nur mit bereits kompilierten Assemblies funktioniert. Das stimmt, deshalb "teilweise" oben. VisualStudio hält sich natürlich an seine Regeln was den Code im aktuellen Projekt betrifft. Aber für Bibliotheken, auf die verwiesen wird, funktioniert das ziemlich gut:

      Man kann auch ein PostBuild-Event für die Projektmappe hinzufügen: "$(SolutionDir)PostBuildRemoveModules\bin\Release\PostBuildRemoveModules.exe" "$(TargetPath)"
      Funktioniert auch in den Express-Versionen: Die .vbproj-Datei (bzw. .csproj) öffnen und ganz unten ein bisschen was einfügen:

      XML-Quellcode

      1. <!-- ... -->
      2. </ItemGroup>
      3. <ItemGroup>
      4. <Service Include="{94E38DFF-614B-4CBD-B67C-F211BB35CE8B}" />
      5. </ItemGroup>
      6. <ItemGroup />
      7. <Import Project="$(MSBuildToolsPath)\Microsoft.VisualBasic.targets" />
      8. <!-- Das hier einfügen: -->
      9. <PropertyGroup>
      10. <PostBuildEvent>"$(SolutionDir)PostBuildRemoveModules\bin\Release\PostBuildRemoveModules.exe" "$(TargetPath)"</PostBuildEvent>
      11. </PropertyGroup>
      12. </Project>


      Eine andere Methode, Extensions in Intellisense auszublenden, hat ErfinderDesRades schon mal gezeigt:
      EditorBrowsableState.Never für Extension-Methods
      Das funktioniert aber nur einwandfrei, solange sich das Modul mit den Extensions im aktuellen Projekt befindet. In einem anderen Projekt werden die Extensions dann garnicht mehr angezeigt.
      Man kann da möglicherweise was mit dem Präprozessor machen und immer umschalten:

      VB.NET-Quellcode

      1. ''' <summary>
      2. ''' Beinhaltet Extension-Methoden für blablabla...
      3. ''' </summary>
      4. #If InvisibleModules Then
      5. <System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)>
      6. Public Module Extensions
      7. #Else
      8. Public Module Extensions
      9. #End If
      10. '...
      11. End Module

      Also wenn man gerade an der Bibliothek arbeitet InvisibleModules auf True stellen und beim Release auf False:

      Das ist leider wartungstechnisch etwas aufwändiger.
      "Luckily luh... luckily it wasn't poi-"
      -- Brady in Wonderland, 23. Februar 2015, 1:56
      Desktop Pinner | ApplicationSettings | OnUtils