Loading VB.NET Code Into A Designer
- Does anybody know how to load VB.NET/C# code into a designer? I've been trying to do so for weeks with no success. I know that Visual Studio and SharpDevelop do it, but I can't seem to replicate the functionality in my own app.
Thanks very much for all your help!
Answers
Ok, sorry it took so long... been busy with other things.
Here's a sample design form, designer loader, parser, and ITypeResolutionService. The parser uses SharpDevelop's parser, so you'll need a copy of ICSharpCode.NRefactory.dll. You can get it by downloading the package at http://www.sharpdevelop.net/OpenSource/SD/Download/GetFile.aspx?What=Setup&Release=Serralongue
Here's a breakdown of it all.
1. Form1 has an empty tabpage control and a single menu item that allows you to choose a source VB or C# file.
2. Pick a file and a new design surface is created. The surface using DesginerLoader to load the file.
3. DesignerLoader loads the file you selected, as well as any others associated with it (eg. Form1.vb and Form1.Designer.vb).
4. The two (or more, see the comments) files are merged into a temporary file and parsed via SharpDevParser.
5. The result is displayed in a new tab page.
And that's about it. Here's the code:
Form1.vb
Code SnippetImports
System.IOImports
System.ComponentModelImports
System.ComponentModel.DesignImports
Microsoft.VisualBasicImports
System.ComponentModel.Design.SerializationPublic
Class Form1 Dim serviceContainer As ServiceContainer Protected Overrides Sub OnLoad(ByVal e As System.EventArgs)serviceContainer =
New ServiceContainerserviceContainer.AddService(
GetType(ITypeResolutionService), New TypeResolutionService()) MyBase.OnLoad(e) End Sub Private Sub OpenToolStripMenuItem_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) HandlesOpenToolStripMenuItem.Click
Dim formFile As String
Dim openDialog As OpenFileDialog = New OpenFileDialog() With openDialog.AddExtension =
True.CheckFileExists =
True.CheckPathExists =
True.DefaultExt =
"vb".FileName =
"".Filter =
"VB Files|*.vb|C# Files|*.cs".FilterIndex = 0
.InitialDirectory = Application.StartupPath
If .ShowDialog() = Windows.Forms.DialogResult.OK ThenOpenForm(.FileName)
End If End With End Sub Private Sub OpenForm(ByVal filePath As String) Dim ds As DesignSurface Dim loader As DesignerLoader Dim tabPage As TabPage Dim view As ControlIf File.Exists(filePath) Then
tabPage =
New TabPage(Path.GetFileName(filePath))ds =
New DesignSurface(serviceContainer)
loader = New DesignerLoader(filePath)ds.BeginLoad(loader)
view = ds.View
view.Dock = DockStyle.Fill
tabPage.Controls.Add(view)
tcDesigners.TabPages.Add(tabPage)
tcDesigners.SelectedTab = tabPage
ElseMsgBox(filePath &
" not found.", MsgBoxStyle.Exclamation, "Open Form") End If End SubEnd
ClassForm1.Designer.vb:
Code Snippet<
Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()> _Partial
Class Form1 Inherits System.Windows.Forms.Form 'Form overrides dispose to clean up the component list.<System.Diagnostics.DebuggerNonUserCode()> _
Protected Overrides Sub Dispose(ByVal disposing As Boolean) Try If disposing AndAlso components IsNot Nothing Thencomponents.Dispose()
End If Finally MyBase.Dispose(disposing) End Try End Sub
'Required by the Windows Form Designer Private components As System.ComponentModel.IContainer'NOTE: The following procedure is required by the Windows Form Designer
'It can be modified using the Windows Form Designer. 'Do not modify it using the code editor.<System.Diagnostics.DebuggerStepThrough()> _
Private Sub InitializeComponent() Dim resources As System.ComponentModel.ComponentResourceManager = New System.ComponentModel.ComponentResourceManager(GetType(Form1)) Me.MenuStrip1 = New System.Windows.Forms.MenuStrip Me.FileToolStripMenuItem = New System.Windows.Forms.ToolStripMenuItem Me.OpenToolStripMenuItem = New System.Windows.Forms.ToolStripMenuItem Me.tcDesigners = New System.Windows.Forms.TabControl Me.MenuStrip1.SuspendLayout() Me.SuspendLayout() ' 'MenuStrip1 ' Me.MenuStrip1.Items.AddRange(New System.Windows.Forms.ToolStripItem() {Me.FileToolStripMenuItem}) Me.MenuStrip1.Location = New System.Drawing.Point(0, 0) Me.MenuStrip1.Name = "MenuStrip1" Me.MenuStrip1.RenderMode = System.Windows.Forms.ToolStripRenderMode.Professional Me.MenuStrip1.Size = New System.Drawing.Size(789, 24) Me.MenuStrip1.TabIndex = 0 Me.MenuStrip1.Text = "MenuStrip1" ' 'FileToolStripMenuItem ' Me.FileToolStripMenuItem.DropDownItems.AddRange(New System.Windows.Forms.ToolStripItem() {Me.OpenToolStripMenuItem}) Me.FileToolStripMenuItem.Name = "FileToolStripMenuItem" Me.FileToolStripMenuItem.Size = New System.Drawing.Size(35, 20) Me.FileToolStripMenuItem.Text = "&File" ' 'OpenToolStripMenuItem ' Me.OpenToolStripMenuItem.Image = CType(resources.GetObject("OpenToolStripMenuItem.Image"), System.Drawing.Image) Me.OpenToolStripMenuItem.ImageTransparentColor = System.Drawing.Color.Magenta Me.OpenToolStripMenuItem.Name = "OpenToolStripMenuItem" Me.OpenToolStripMenuItem.ShortcutKeys = CType((System.Windows.Forms.Keys.Control Or System.Windows.Forms.Keys.O), System.Windows.Forms.Keys) Me.OpenToolStripMenuItem.Size = New System.Drawing.Size(152, 22) Me.OpenToolStripMenuItem.Text = "&Open" ' 'tcDesigners ' Me.tcDesigners.Dock = System.Windows.Forms.DockStyle.Fill Me.tcDesigners.Location = New System.Drawing.Point(0, 24) Me.tcDesigners.Name = "tcDesigners" Me.tcDesigners.SelectedIndex = 0 Me.tcDesigners.Size = New System.Drawing.Size(789, 473) Me.tcDesigners.TabIndex = 1 ' 'Form1 ' Me.AutoScaleDimensions = New System.Drawing.SizeF(6.0!, 13.0!) Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font Me.ClientSize = New System.Drawing.Size(789, 497) Me.Controls.Add(Me.tcDesigners) Me.Controls.Add(Me.MenuStrip1) Me.MainMenuStrip = Me.MenuStrip1 Me.Name = "Form1" Me.Text = "Form1" Me.MenuStrip1.ResumeLayout(False) Me.MenuStrip1.PerformLayout() Me.ResumeLayout(False) Me.PerformLayout() End Sub Friend WithEvents MenuStrip1 As System.Windows.Forms.MenuStrip Friend WithEvents FileToolStripMenuItem As System.Windows.Forms.ToolStripMenuItem Friend WithEvents OpenToolStripMenuItem As System.Windows.Forms.ToolStripMenuItem Friend WithEvents tcDesigners As System.Windows.Forms.TabControlEnd
ClassDesignerLoader.vb
Code SnippetImports
ICSharpCode.NRefactoryImports
system.CodeDomImports
System.CodeDom.CompilerImports
System.ComponentModel.DesignImports
System.ComponentModel.Design.SerializationImports
System.IOImports
System.TextPublic
Class DesignerLoader Inherits CodeDomDesignerLoader ' Enum of supported language file extensions. These entries match corresponding SharpDevelop SupportedLanguage ' values. Private Enum SupportedLanguagesCS
VB
End Enum
' Imports / using statements for supported languages. Dim importTokens As String() = {"using", "Imports"}' Statement termination characters for supported languages.
Dim statementTerminators As String() = {";", ""}Dim sourceFile As String
Dim provider As CodeDomProvider Dim errors As String#
Region " Construction / Initialization " ''' <summary> ''' Duh ''' </summary> ''' <param name="fileName">A code file to open.</param> ''' <remarks>If the file has matching associates, they will be loaded as well. See LoadReaders() ''' for details.</remarks> Public Sub New(ByVal fileName As String)Init(fileName)
End Sub
''' <summary> ''' Saves the requested source files and gets a CodeDomProvider appropriate for the file type. ''' </summary> ''' <param name="fileName">Name of the file to load.</param> ''' <remarks></remarks> Private Sub Init(ByVal fileName As String)sourceFile = fileName
provider = GetProvider(fileName)
End Sub#
End Region#
Region " CodeDomDesignerLoader implementation / overrides " ''' <summary> ''' Returns a reference to our CodeDomProvider, which is specific to the type of file passed in ''' at initialization. ''' </summary> ''' <value></value> ''' <returns></returns> ''' <remarks></remarks> Protected Overrides ReadOnly Property CodeDomProvider() As System.CodeDom.Compiler.CodeDomProvider Get Return provider End Get End Property ''' <summary> ''' Parses the specified source file (and associated designer / partial files) with the help of SharpDevelop. ''' </summary> ''' <returns>A CodeCompileUnit representing the class(es) included in the source file.</returns> ''' <remarks></remarks> Protected Overrides Function Parse() As System.CodeDom.CodeCompileUnit Dim parser As SharpDevParser Dim mergeFileName As String = "" Dim reader As TextReader = Nothing Dim ccu As CodeCompileUnit = Nothing Dim readers As List(Of TextReader) = LoadReaders(sourceFile)If readers.Count > 0 Then
mergeFileName = MergeFiles(readers)
If mergeFileName.Length > 0 Thenreader = File.OpenText(mergeFileName)
End If End If
If reader IsNot Nothing Then Tryparser =
New SharpDevParser()ccu = parser.Parse(GetLanguageIndex(provider), reader, GetService(
GetType(ITypeResolutionService))) Catch ex As Exception Finallyreader.Close()
TryFile.Delete(mergeFileName)
Catch ex As Exception End Try End Try End IfCloseReaders(readers)
Return ccu End Function
''' <summary> ''' Returns an instance of a Type Resultion Service. We don't provide one ourselves... just return whatever the ''' LoaderHost gives us. ''' </summary> ''' <value>Whatever the LoaderHost gives us via GetService().</value> ''' <returns>Uhhh, I just said that... Whatever the LoaderHost gives us via GetService().</returns> ''' <remarks></remarks> Protected Overrides ReadOnly Property TypeResolutionService() As System.ComponentModel.Design.ITypeResolutionService Get Return LoaderHost.GetService(GetType(TypeResolutionService)) End Get End Property
''' <summary> ''' Not supported in this implementation. ''' </summary> ''' <param name="unit">CodeCompileUnit to write to file.</param> ''' <remarks></remarks> Protected Overrides Sub Write(ByVal unit As System.CodeDom.CodeCompileUnit) ' End Sub#
End Region#
Region " Nitty gritty " ''' <summary> ''' Attempts to load all partial class definitions given a source code file. ''' </summary> ''' <param name="sourceFile">One of the source files in a set of related files</param> ''' <returns>A list of TextReaders, each loaded with a file assumed to belong to the same ''' class definition. This list will be empty (count = 0) if an error occurs or the specified ''' file is not found.</returns> ''' <remarks>Partial classes are supported but will only be located if all class files reside ''' in the same folder and follow the same naming pattern as the file specified. The naming pattern ''' looked for is {some name}{optional ".*"}.{language extension} ''' ''' Eg. If Form1.Designer.vb is specified, then its code-behind file Form1.vb would also be loaded, ''' as would a file named Form1.partial2.vb, etc. ''' </remarks> Private Function LoadReaders(ByVal sourceFile As String) As List(Of TextReader) Dim basePath As String = Path.GetDirectoryName(sourceFile) Dim fileExtension As String = Path.GetExtension(sourceFile) Dim baseName As String = GetBaseFileName(sourceFile) Dim files() As String = Directory.GetFiles(basePath, baseName & "*" & fileExtension) Dim validFiles As List(Of String) = New List(Of String) Dim readerList As List(Of TextReader) = New List(Of TextReader) Dim regEx As RegularExpressions.Regex = New RegularExpressions.Regex("(" & baseName & ")(\..+)*(" & fileExtension & ")")Try
For Each file As String In files If regEx.IsMatch(file) ThenvalidFiles.Add(file)
End If Next For Each name As String In validFilesreaderList.Add(File.OpenText(name))
Next Catch ex As ExceptionTrace.WriteLine(
"Exception opening source files - " & ex.Message)CloseReaders(readerList)
readerList.Clear()
End Try Return readerList End Function
''' <summary> ''' Gets the first part of a source code file name (ie. everything before the first "."). ''' </summary> ''' <param name="fullPath">Full path to the file to evaluate.</param> ''' <returns>Base file name.</returns> ''' <remarks>Give a file named "Form1.Designer.vb", this function would return "Form1".</remarks> Private Function GetBaseFileName(ByVal fullPath As String) As String Dim baseName As String = Path.GetFileNameWithoutExtension(fullPath) Dim theDot As Integer = baseName.IndexOf(".")If theDot > 0 Then
baseName = baseName.Substring(0, theDot)
End If Return baseName End Function
''' <summary> ''' Merges related code files into a temporary file. ''' </summary> ''' <param name="fileReaders">List of open TextReaders, each loaded with a related code file.</param> ''' <returns>Path to the merged file.</returns> ''' <remarks>The resulting file will have all Import / using statements from all files moved to the top. ''' Parsers like it that way. ''' </remarks> Private Function MergeFiles(ByVal fileReaders As List(Of TextReader)) As String Dim sourceCode As String = "" Dim importList As List(Of String) Dim importToken As String Dim statementTerminator As String Dim mergeFile As String = "" Dim language As Integer = GetLanguageIndex(provider)If language < 0 Then Return ""
TryimportList =
New List(Of String)importToken = importTokens(language)
statementTerminator = statementTerminators(language)
For Each reader As TextReader In fileReaderssourceCode += PullCodeExtractImports(reader, importToken, statementTerminator, importList)
NextimportList.Sort()
For index As Integer = 0 To importList.Count - 1importList(index) = importToken &
" " & importList(index) NextsourceCode =
String.Join(statementTerminator & vbCrLf, importList.ToArray) & vbCrLf & sourceCode If sourceCode.Length > 0 ThenmergeFile = Path.GetTempFileName()
File.AppendAllText(mergeFile, sourceCode)
End If
Catch ex As ExceptionTrace.WriteLine(ex.ToString)
If mergeFile.Length > 0 Then TryFile.Delete(mergeFile)
Catch ex2 As Exception End Try End IfmergeFile =
"" End Try Return mergeFile End Function
''' <summary> ''' Pulls the code from a text file, excluding Import / using statements. ''' </summary> ''' <param name="reader">Reader opened with the source file to read.</param> ''' <param name="importToken">Language-specific token for the Imports / using statement.</param> ''' <param name="statementTerminator">Language-specific code statement terminator. ";" in C#, empty string in VB.</param> ''' <param name="importList">Reference to a list of import statements. Imports found in the current file will be added ''' to this list.</param> ''' <returns>The resulting code, as a string, minus any import statements.</returns> ''' <remarks></remarks> Private Function PullCodeExtractImports(ByVal reader As TextReader, ByVal importToken As String, ByVal statementTerminator As String, ByRef importList As List(Of String)) As String Dim line As String = Nothing Dim resultString As String = "" Dim strippedLine As Stringline = reader.ReadLine
Do Until line Is NothingstrippedLine = line.Trim.TrimEnd(
New Char() {statementTerminator})If strippedLine.ToLower.StartsWith(importToken.ToLower) AndAlso _
importList.IndexOf(strippedLine.Substring(importToken.Length + 1)) < 0 Then
importList.Add(strippedLine.Substring(importToken.Length + 1))
ElseresultString += line & vbCrLf
End Ifline = reader.ReadLine
Loop Return resultString End Function
''' <summary> ''' Returns a SupportedLanguages value given a language-specific CodeDomProvider. ''' </summary> ''' <param name="provider">The CodeDomProvider for the language to query.</param> ''' <returns>One of the SupportedLanguages values, -1 if CodeDomProvider is not supported by this ''' class</returns> ''' <remarks></remarks> Private Function GetLanguageIndex(ByVal provider As CodeDomProvider) As Integer Try Return CInt(System.Enum.Parse(GetType(SupportedLanguages), provider.FileExtension, True)) Catch ex As Exception Return -1 End Try End Function
''' <summary> ''' Closes a bunch of text readers. ''' </summary> ''' <param name="readers">List of open TextReaders to close.</param> ''' <remarks></remarks> Private Sub CloseReaders(ByVal readers As List(Of TextReader)) For Each reader As TextReader In readers Tryreader.Close()
Catch ex As Exception End TryNext
End Sub
''' <summary> ''' Returns a CodeDomProvider appropriate for the file specified, based on its file extension. ''' </summary> ''' <param name="fileName">Code file whose provider is to be loaded.</param> ''' <returns>A CodeDomProvider appropriate for the file specified if successful, ''' Nothing otherwise.</returns> ''' <remarks></remarks> Private Function GetProvider(ByVal fileName As String) As CodeDomProvider Try Dim language As String = CodeDomProvider.GetLanguageFromExtension(Path.GetExtension(fileName)) Return CodeDomProvider.CreateProvider(language) Catch ex As ExceptionTrace.WriteLine(ex.ToString)
Return Nothing End Try End Function#
End RegionEnd
ClassSharpDevParser.vb
Code SnippetImports
system.CodeDomImports
System.CodeDom.CompilerImports
System.ComponentModelImports
System.ComponentModel.DesignImports
System.ComponentModel.Design.SerializationImports
System.Windows.Forms.DesignImports
System.IOImports
ICSharpCode.NRefactoryPublic
Class SharpDevParser Dim _errorOutput As String = "" Dim _errorCount As Integer = 0
''' <summary> ''' Uses the SharpDevelop parser to parse the source file. ''' </summary> ''' <returns>A CodeCompileUnit representing the source file, Nothing if parsing fails.</returns> ''' <remarks>If the parse fails, ErrorOutput will contain the parser's ErrorOutput string and ''' ErrorCount will indicate the number of errors encountered.</remarks> Public Function Parse(ByVal language As SupportedLanguage, ByVal sourceReader As TextReader, ByVal typeResolutionService As ITypeResolutionService) As CodeCompileUnitDim parser As ICSharpCode.NRefactory.IParser = ParserFactory.CreateParser(language, sourceReader)
Dim visitor As Visitors.CodeDomVisitor
ResetErrors()
parser.ParseMethodBodies = True
parser.Parse()
_errorCount = parser.Errors.Count
_errorOutput = parser.Errors.ErrorOutput
If ErrorCount = 0 Thenvisitor =
New Visitors.CodeDomVisitor()visitor.EnvironmentInformationProvider =
New EnvironmentInformationProvider(typeResolutionService)visitor.VisitCompilationUnit(parser.CompilationUnit,
Nothing) Return visitor.codeCompileUnit Else Return Nothing End If End Function
''' <summary> ''' Resets ErrorOutput and ErrorCount to default values. ''' </summary> ''' <remarks></remarks> Private Sub ResetErrors()_errorCount = 0
_errorOutput =
"" End Sub
''' <summary> ''' Returns a string representing errors encountered during a parse. ''' </summary> ''' <value></value> ''' <returns></returns> ''' <remarks></remarks> Public ReadOnly Property ErrorOutput() As String Get Return _errorOutput End Get End Property
''' <summary> ''' Returns the number of errors encountered during a parse. ''' </summary> ''' <value></value> ''' <returns></returns> ''' <remarks></remarks> Public ReadOnly Property ErrorCount() As Integer Get Return _errorCount End Get End PropertyEnd
Class'''
<summary>''' Implementation of IEnvironmentInformationProvider.
'''
</summary>'''
<remarks>IEnvironmentInformationProvider is used by a Visitor to determine whether a Type contains''' a field with the specific name. This is necessary because the parser can't tell, from reading the code,
''' whether a member is a field or a property. An Enum's values, for instance, cannot be differentiated
''' from a class type's properties.
'''
''' Failure to make this determination can result in failure to load the member. In the absence of an
''' IEnvironmentInformationProvider the parser will assume a property/
</remarks> Friend Class EnvironmentInformationProvider Implements IEnvironmentInformationProvider Dim resolutionService As ITypeResolutionService
''' <summary> ''' Constructor. ''' </summary> ''' <param name="typeResolutionService">A Type Resolution Service instance. This is used to resolve the type ''' requested.</param> ''' <remarks></remarks> Public Sub New(ByVal typeResolutionService As ITypeResolutionService)resolutionService = typeResolutionService
End Sub
''' <summary> ''' Determines if a type has a field with a given name. ''' </summary> ''' <param name="fullTypeName">Name of the type to query.</param> ''' <param name="fieldName">Name of the field to look for.</param> ''' <returns>True if the type has a field by the given name, False if an error occurs or the type ''' doesn't contain a corresponding field.</returns> ''' <remarks></remarks> Public Function HasField(ByVal fullTypeName As String, ByVal fieldName As String) As Boolean Implements ICSharpCode.NRefactory.IEnvironmentInformationProvider.HasField Dim theType As Type = Type.GetType(fullTypeName)If theType Is Nothing AndAlso resolutionService IsNot Nothing Then
theType = resolutionService.GetType(fullTypeName)
End If If theType Is Nothing Then Return False Else Return Not theType.GetField(fieldName) Is Nothing End If End FunctionEnd
ClassAnd finally, you need a decent ITypeResolutionService. This is a modified version of SharpDevelop's.
TypeResolutionService.vb
Code SnippetImports
SystemImports
System.ComponentModel.DesignImports
System.ReflectionImports
System.IOImports
Microsoft.Win32'''
<summary>''' Modified port of SharpDev's TypeResolutionService. Resolves types during a designer load.
'''
</summary>'''
<remarks></remarks>Public
Class TypeResolutionService Implements ITypeResolutionService<System.Runtime.InteropServices.DllImport(
"kernel32.dll")> _ Private Shared Function MoveFileEx(ByVal lpExistingFileName As String, ByVal lpNewFileName As String, ByVal dwFlags As Integer) As Boolean End Function
Shared ReadOnly _designerAssemblies As List(Of Assembly) = New List(Of Assembly) Shared ReadOnly assemblyDict As Dictionary(Of String, Assembly) = New Dictionary(Of String, Assembly) '''<summary> ''' List of assemblies used by the form designer. This static list is not an optimal solution, ''' but better than using AppDomain.CurrentDomain.GetAssemblies(). See SD2-630. ''' </summary> Public Shared ReadOnly Property DesignerAssemblies() As List(Of Assembly) Get Return _designerAssemblies End Get End Property Shared Sub New()ClearMixedAssembliesTemporaryFiles()
' Preload commonly referenced assemblies.DesignerAssemblies.Add(
GetType(Object).Assembly) ' MSCorlibDesignerAssemblies.Add(
GetType(Uri).Assembly) ' SystemDesignerAssemblies.Add(
GetType(System.Drawing.Point).Assembly) ' System.DrawingDesignerAssemblies.Add(
GetType(System.Windows.Forms.AutoScaleMode).Assembly) ' System.Windows.FormsDesignerAssemblies.Add(
GetType(System.Windows.Forms.Design.AnchorEditor).Assembly) ' System.Windows.Forms.DesignRegisterVSDesignerWorkaround()
AddHandler AppDomain.CurrentDomain.AssemblyResolve, AddressOf AssemblyResolveEventHandler End Sub
Const MOVEFILE_DELAY_UNTIL_REBOOT As Integer = &H4 Shared Sub MarkFileToDeleteOnReboot(ByVal fileName As String)MoveFileEx(fileName,
Nothing, MOVEFILE_DELAY_UNTIL_REBOOT) End Sub Shared Sub ClearMixedAssembliesTemporaryFiles() Dim files As String() = Directory.GetFiles(Path.GetTempPath(), "*.sd_forms_designer_mixed_assembly.dll") For Each filename As String In files TryFile.Delete(filename)
Catch End Try Next End Sub Dim formSourceFileName As String Public Sub New() End Sub Public Sub New(ByVal formSourceFileName As String) Me.formSourceFileName = formSourceFileName End Sub Shared Function GetHash(ByVal fileName As String) As String Return Path.GetFileName(fileName).ToLowerInvariant() + File.GetLastWriteTimeUtc(fileName).Ticks.ToString() End Function ''' <summary> ''' Loads the file in none-locking mode. Returns null on failure. '''</summary> Public Shared Function LoadAssembly(ByVal fileName As String) As Assembly If Not File.Exists(fileName) Then Return Nothing End If ' FIX for SD2-716, remove when designer gets its own AppDomain For Each asm As Assembly In AppDomain.CurrentDomain.GetAssemblies() Try If String.Equals(asm.Location, fileName, StringComparison.InvariantCultureIgnoreCase) ThenRegisterAssembly(asm)
Return asm End If Catch e As NotSupportedException ' Fixes forum-12823 End Try Next Dim hash As String = GetHash(fileName) SyncLock assemblyDict Dim asm As Assembly If assemblyDict.TryGetValue(hash, asm) Then Return asm End If Tryasm = Assembly.Load(File.ReadAllBytes(fileName))
Catch e As BadImageFormatException If e.Message.Contains("HRESULT: 0x8013141D") Then Dim tempPath As String = Path.GetTempFileName()File.Delete(tempPath)
tempPath +=
".sd_forms_designer_netmodule_assembly.dll" Try 'convert netmodule to assembly Dim p As Process = New Process()p.StartInfo.UseShellExecute =
Falsep.StartInfo.FileName = Path.GetDirectoryName(
GetType(Object).Module.FullyQualifiedName) + Path.DirectorySeparatorChar + "al.exe"p.StartInfo.Arguments =
"""" & fileName & " /out:""" + tempPath + """"p.StartInfo.CreateNoWindow =
Truep.Start()
p.WaitForExit()
If p.ExitCode = 0 And File.Exists(tempPath) Then Dim asm_data As Byte() = File.ReadAllBytes(tempPath)asm = Assembly.Load(asm_data)
asm.LoadModule(Path.GetFileName(fileName), File.ReadAllBytes(fileName))
End If Catch ex As ExceptionMsgBox(ex.ToString, MsgBoxStyle.Critical,
"Error calling linker for netmodule") End Try TryFile.Delete(tempPath)
Catch End Try Else Throw ' don't ignore other load errors End If Catch e As FileLoadException If e.Message.Contains("HRESULT: 0x80131402") Then 'this is C++/CLI Mixed assembly which can only be loaded from disk, not in-memory Dim tempPath As String = Path.GetTempFileName()File.Delete(tempPath)
tempPath +=
".sd_forms_designer_mixed_assembly.dll"File.Copy(fileName, tempPath)
asm = Assembly.LoadFile(tempPath)
MarkFileToDeleteOnReboot(tempPath)
Else Throw ' don't ignore other load errors End If End Try SyncLock DesignerAssemblies If Not DesignerAssemblies.Contains(asm) ThenDesignerAssemblies.Insert(0, asm)
End If End SyncLockassemblyDict(hash) = asm
Return asm End SyncLock End Function Public Shared Sub RegisterAssembly(ByVal asm As Assembly) Dim file As String = asm.Location If file.Length > 0 Then SyncLock assemblyDictassemblyDict(GetHash(file)) = asm
End SyncLock End If SyncLock DesignerAssemblies If Not DesignerAssemblies.Contains(asm) ThenDesignerAssemblies.Insert(0, asm)
End If End SyncLock End Sub Public Function GetAssembly(ByVal name As AssemblyName) As Assembly Implements System.ComponentModel.Design.ITypeResolutionService.GetAssembly Return LoadAssembly(name, False) End Function Public Function GetAssembly(ByVal name As AssemblyName, ByVal throwOnError As Boolean) As Assembly Implements System.ComponentModel.Design.ITypeResolutionService.GetAssembly Return LoadAssembly(name, throwOnError) End Function Shared Function LoadAssembly(ByVal name As AssemblyName, ByVal throwOnError As Boolean) As Assembly Try Dim asm As Assembly = Assembly.Load(name)RegisterAssembly(asm)
Return asm Catch e As System.IO.FileLoadException If throwOnError Then Throw End If Return Nothing End Try End Function Public Function GetPathOfAssembly(ByVal name As AssemblyName) As String Implements System.ComponentModel.Design.ITypeResolutionService.GetPathOfAssembly Dim Assembly As Assembly = GetAssembly(name) If Assembly IsNot Nothing Then Return Assembly.Location End If Return Nothing End Function Public Overloads Function [GetType](ByVal name As String) As Type Implements System.ComponentModel.Design.ITypeResolutionService.GetType Return [GetType](name, False, False) End Function Public Overloads Function [GetType](ByVal name As String, ByVal throwOnError As Boolean) As Type Implements System.ComponentModel.Design.ITypeResolutionService.GetType Return [GetType](name, throwOnError, False) End Function Public Overloads Function [GetType](ByVal name As String, ByVal throwOnError As Boolean, ByVal ignoreCase As Boolean) As Type Implements System.ComponentModel.Design.ITypeResolutionService.GetType If name Is Nothing OrElse name.Length = 0 Then Return Nothing End If If IgnoreType(name) Then Return Nothing End If Try Dim type As Type = System.Type.GetType(name, False, ignoreCase) ' type lookup for typename, assembly, xyz style lookups If type Is Nothing Then Dim idx As Integer = name.IndexOf(",") If idx > 0 Then Dim splitName As String() = name.Split(",") Dim typeName As String = splitName(0) Dim assemblyName As String = splitName(1).Substring(1) Dim assembly As Assembly = Nothing Tryassembly = assembly.Load(assemblyName)
Catch e As Exception End Try If assembly IsNot Nothing Then SyncLock DesignerAssemblies If Not DesignerAssemblies.Contains(assembly) ThenDesignerAssemblies.Add(assembly)
End If End SyncLocktype = assembly.GetType(typeName,
False, ignoreCase) Elsetype = type.GetType(typeName,
False, ignoreCase) End If End If End If If type Is Nothing Then SyncLock DesignerAssemblies For Each asm As Assembly In DesignerAssemblies Dim t As Type = asm.GetType(name, False) If t IsNot Nothing Then Return t End If Next End SyncLock End If If type Is Nothing Thentype = SearchLoadedAssemblies(name, ignoreCase)
End If If throwOnError And type Is Nothing Then Throw New TypeLoadException(name + " not found by TypeResolutionService") End If Return type Catch e As Exception End Try Return Nothing End Function Public Sub ReferenceAssembly(ByVal name As AssemblyName) Implements System.ComponentModel.Design.ITypeResolutionService.ReferenceAssemblyTrace.WriteLine(name)
End Sub#
Region " VSDesigner workarounds " '''<summary> ''' HACK - Ignore any requests for types from the Microsoft.VSDesigner ''' assembly. There are smart tag problems if data adapter ''' designers are used from this assembly. ''' </summary> Function IgnoreType(ByVal name As String) As Boolean Dim idx As Integer = name.IndexOf(",") If idx > 0 Then Dim splitName As String() = name.Split(",") Dim assemblyName As String = splitName(1).Substring(1) If assemblyName = "Microsoft.VSDesigner" Then Return True End If End If Return False End Function Shared vsDesignerIdeDir As String Shared Sub RegisterVSDesignerWorkaround() If vsDesignerIdeDir Is Nothing ThenvsDesignerIdeDir =
"" Dim key As RegistryKey = Registry.LocalMachine.OpenSubKey("SOFTWARE\Microsoft\VisualStudio\8.0\Setup\VS") If key IsNot Nothing ThenvsDesignerIdeDir = key.GetValue(
"VS7CommonDir").ToString If vsDesignerIdeDir.Length > 0 ThenvsDesignerIdeDir = Path.Combine(vsDesignerIdeDir,
"IDE") AddHandler AppDomain.CurrentDomain.AssemblyResolve, AddressOf DomainResolveHandler End If End If End If End Sub Shared Function DomainResolveHandler(ByVal sender As Object, ByVal args As ResolveEventArgs) As Assembly Dim shortName As String = args.Name If shortName.IndexOf(",") >= 0 ThenshortName = shortName.Substring(0, shortName.IndexOf(
",")) End If If shortName.StartsWith("Microsoft.") And File.Exists(Path.Combine(vsDesignerIdeDir, shortName & ".dll")) Then Return Assembly.LoadFrom(Path.Combine(vsDesignerIdeDir, shortName + ".dll")) End If Return Nothing End Function#
End Region Shared Function SearchLoadedAssemblies(ByVal name As String, ByVal ignoreCase As Boolean) As Type Dim lastAssembly As Assembly = Nothing Dim foundType As Type = Nothing For Each asm As Assembly In AppDomain.CurrentDomain.GetAssembliesfoundType = asm.GetType(name,
False, ignoreCase) If foundType IsNot Nothing ThenlastAssembly = asm
Exit For End If Next If lastAssembly IsNot Nothing ThenTypeResolutionService.DesignerAssemblies.Add(lastAssembly)
End If Return foundType End Function Shared Function AssemblyResolveEventHandler(ByVal sender As Object, ByVal args As ResolveEventArgs) As Assembly Dim lastAssembly As Assembly = Nothing For Each asm As Assembly In TypeResolutionService.DesignerAssemblies If asm.FullName = args.Name Then Return asm End If Next For Each asm As Assembly In AppDomain.CurrentDomain.GetAssemblies If asm.FullName = args.Name ThenlastAssembly = asm
End If Next If lastAssembly IsNot Nothing ThenTypeResolutionService.DesignerAssemblies.Add(lastAssembly)
End If Return lastAssembly End FunctionEnd
Class
All Replies
- You mean like switch to code view in a custom designer?
- Kind of. I already have an application with a working designer, toolbox, and property window. I also have a "View Code" button, which loads the code into a textbox, just like it should. However, I want an "Open" menu item that, if a form is opened, can load it into designer view, not just load text into a textbox. If that does/n't make sense, let me know
. Thanks! Yeah I think I know what you mean. You could create a RichTextBox control attached to its own root designer and load a new surface with that control as the root. Kinda like this guy does with his CodeDomDesignerLoader.
I think that's what you mean?
Marc
- Actually, the CodeDomDesignerLoader in that sample is what I'm trying to base my code off of! Could you elaborate on what you were saying about the RichTextBox? Fully explained, what I want is this:
1. User clicks File menu, then Open.
2. User selects VB.NET code file (for example, Form1.vb)
3. Program creates new form designer, and makes the form in the design view have Form1.vb's contents displayed as a form with buttons, listboxes, etc., NOT as text.
Thank you again for helping! Ohhhhhhh you wanna deserialize a form! Ok gotcha. Can't write a sample right this minute but gimme a couple hours.
Marc.
- Welll allllllrighty then. Not many freebies for parsing a source file apparently. This is gonna take a while..
- That's exactly what I've found
. I don't care - if you can find something with a trial version that works, I'll buy the full product! I'm not as worried about the cost of this project as I am about getting it working properly
.
Hope you find something you can work with - you seem really good in this department! Oh by freebies I mean built-in functionality
The VBCodeProvider (and CSharpCodeProvider) handle code generation and compilation but don't seem to do parsing. There are internal classes used by VS that do parsing but they're a little confusing (and, well, internal.)In the end you have to reproduce what the CodeGen.GetCodeCompileUnit() method does in that sample host app. Difference is that you want to read the file and determine what type of code element each statement represents (class declaration, import, namespace, etc.) instead of building them manually. Once that's done you can get a serializer to deserialize the statements.
It's late and my thinking's a little fuzzy... I know I've seen classes in the framework that help do this somewhere. I'll look into it further tomorrow.
Marc.
Oh.
My.
God.
This hurts.
Microsoft, would it have killed ya to provide a parser implementation with your language providers?
I've narrowed it down to 4 approaches:
- Compiling the code via CodeDomProvider.CompileAssemblyFromSource, then iterating through the resulting types to find a class compatible with a root component;
- Implementing something of a lexxer to read through the file and figure out what type of statement each line represents;
- Using SharpDevelop's parser / lexxer implementations, which are available under the GPL but may not be compatible with your implementation; and
- Providing a source file with your application, forcing the users to select only that file, and hard-coding the form instance

Since I'm guessing option 4 is out, here's the rundown on the others.
- Using this option, you have to make sure the source file has the appropriate imports statements. Something we take for granted, like the statement MsgBox("I need a parser!"), relies on having Microsoft.VisualBasic or Microsoft.CSharp (depending on the language) listed as imports. There are others as well. These are implied when you compile via the IDE but not when using the language provider's compiler.
Another issue is the fact that designable components are often paired with the ol' classname.designer.extension file which has the designer generated partial class.
You can code around these issues to address most cases but you'll no doubt run into some exceptions. I'm working on a sample using this method.
- This would allow you to build a CodeStatementCollection.. uhh... collection, and instantiate the class using CodeDomSerializer. It's obviously more complicated but I think this is the "proper" way to do it. I might work on this one at some point.
- Self explanatory. NRefactory seems to be parsing workhorse (ICSharpCode.NRefactory.dll to be precise.)
Actually there is another one - this guy wrote a C# parser back in '02. You could play with that and see about upgrading it to .Net 2.0, as well as including VB support. I'm thinking I'll be doing this in the near future.
Anyway, that's what I've discovered. I'll post a sample of option 1 in the next day or so (getting late again and my brain hurts.)
Marc
- Oh this is interesting - http://www.panopticoncentral.net/ has a VB parser if your source files are in VB. The source project is at http://www.panopticoncentral.net/Files/VBParser%20Source%20(8.0.0.60327).zip
- I found a parser that might work... http://www.programmar.com . It already has pre-made VB and C# grammars (I think you'd need to add support for partial classes) and an API. The API looks messed up
, but maybe you could figure it out
.
Thanks for all the great work you're doing! I didn't even know some of this stuff existed in .NET before
. Ok cool.
I've pretty much completed my li'l loader but it really isn't the right way to go. I can load forms and components but there's alot of potential for situations that wouldn't be properly handled. Also, controls on the form are locked when they're loaded this way.There's probably a way around that but I'll drop it since I don't think this is a good solution.
The right way is a proper parser (*cough*Microsoft*cough*).
Marc.
- Too bad that didn't work...
. How do you think we could get a good "proper" parser? I'd be happy to have a swing at SharpDevelop's parser/lexxer if it wouldn't be too complicated (what are the limitations/incompatibilities?). Are you only planning to load VB code, or do you want C# compatibility as well? I guess it's best to include both if for no other reason than future expansion. I'll whip up a sample using SharpDevelop's version.
The "limitation" of using SharpDevelop's is that it's subject to the GNU Lesser General Public License. That's a pretty forgiving license so it'll probably fit in with what you're doing, but look it over anyway.
Marc.- Yeah, in the end I'll want C# support too. I'll anxiously await your sample
!
Okay, I'll take a look over the licensing there. Thanks for the heads up! Ok, sorry it took so long... been busy with other things.
Here's a sample design form, designer loader, parser, and ITypeResolutionService. The parser uses SharpDevelop's parser, so you'll need a copy of ICSharpCode.NRefactory.dll. You can get it by downloading the package at http://www.sharpdevelop.net/OpenSource/SD/Download/GetFile.aspx?What=Setup&Release=Serralongue
Here's a breakdown of it all.
1. Form1 has an empty tabpage control and a single menu item that allows you to choose a source VB or C# file.
2. Pick a file and a new design surface is created. The surface using DesginerLoader to load the file.
3. DesignerLoader loads the file you selected, as well as any others associated with it (eg. Form1.vb and Form1.Designer.vb).
4. The two (or more, see the comments) files are merged into a temporary file and parsed via SharpDevParser.
5. The result is displayed in a new tab page.
And that's about it. Here's the code:
Form1.vb
Code SnippetImports
System.IOImports
System.ComponentModelImports
System.ComponentModel.DesignImports
Microsoft.VisualBasicImports
System.ComponentModel.Design.SerializationPublic
Class Form1 Dim serviceContainer As ServiceContainer Protected Overrides Sub OnLoad(ByVal e As System.EventArgs)serviceContainer =
New ServiceContainerserviceContainer.AddService(
GetType(ITypeResolutionService), New TypeResolutionService()) MyBase.OnLoad(e) End Sub Private Sub OpenToolStripMenuItem_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) HandlesOpenToolStripMenuItem.Click
Dim formFile As String
Dim openDialog As OpenFileDialog = New OpenFileDialog() With openDialog.AddExtension =
True.CheckFileExists =
True.CheckPathExists =
True.DefaultExt =
"vb".FileName =
"".Filter =
"VB Files|*.vb|C# Files|*.cs".FilterIndex = 0
.InitialDirectory = Application.StartupPath
If .ShowDialog() = Windows.Forms.DialogResult.OK ThenOpenForm(.FileName)
End If End With End Sub Private Sub OpenForm(ByVal filePath As String) Dim ds As DesignSurface Dim loader As DesignerLoader Dim tabPage As TabPage Dim view As ControlIf File.Exists(filePath) Then
tabPage =
New TabPage(Path.GetFileName(filePath))ds =
New DesignSurface(serviceContainer)
loader = New DesignerLoader(filePath)ds.BeginLoad(loader)
view = ds.View
view.Dock = DockStyle.Fill
tabPage.Controls.Add(view)
tcDesigners.TabPages.Add(tabPage)
tcDesigners.SelectedTab = tabPage
ElseMsgBox(filePath &
" not found.", MsgBoxStyle.Exclamation, "Open Form") End If End SubEnd
ClassForm1.Designer.vb:
Code Snippet<
Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()> _Partial
Class Form1 Inherits System.Windows.Forms.Form 'Form overrides dispose to clean up the component list.<System.Diagnostics.DebuggerNonUserCode()> _
Protected Overrides Sub Dispose(ByVal disposing As Boolean) Try If disposing AndAlso components IsNot Nothing Thencomponents.Dispose()
End If Finally MyBase.Dispose(disposing) End Try End Sub
'Required by the Windows Form Designer Private components As System.ComponentModel.IContainer'NOTE: The following procedure is required by the Windows Form Designer
'It can be modified using the Windows Form Designer. 'Do not modify it using the code editor.<System.Diagnostics.DebuggerStepThrough()> _
Private Sub InitializeComponent() Dim resources As System.ComponentModel.ComponentResourceManager = New System.ComponentModel.ComponentResourceManager(GetType(Form1)) Me.MenuStrip1 = New System.Windows.Forms.MenuStrip Me.FileToolStripMenuItem = New System.Windows.Forms.ToolStripMenuItem Me.OpenToolStripMenuItem = New System.Windows.Forms.ToolStripMenuItem Me.tcDesigners = New System.Windows.Forms.TabControl Me.MenuStrip1.SuspendLayout() Me.SuspendLayout() ' 'MenuStrip1 ' Me.MenuStrip1.Items.AddRange(New System.Windows.Forms.ToolStripItem() {Me.FileToolStripMenuItem}) Me.MenuStrip1.Location = New System.Drawing.Point(0, 0) Me.MenuStrip1.Name = "MenuStrip1" Me.MenuStrip1.RenderMode = System.Windows.Forms.ToolStripRenderMode.Professional Me.MenuStrip1.Size = New System.Drawing.Size(789, 24) Me.MenuStrip1.TabIndex = 0 Me.MenuStrip1.Text = "MenuStrip1" ' 'FileToolStripMenuItem ' Me.FileToolStripMenuItem.DropDownItems.AddRange(New System.Windows.Forms.ToolStripItem() {Me.OpenToolStripMenuItem}) Me.FileToolStripMenuItem.Name = "FileToolStripMenuItem" Me.FileToolStripMenuItem.Size = New System.Drawing.Size(35, 20) Me.FileToolStripMenuItem.Text = "&File" ' 'OpenToolStripMenuItem ' Me.OpenToolStripMenuItem.Image = CType(resources.GetObject("OpenToolStripMenuItem.Image"), System.Drawing.Image) Me.OpenToolStripMenuItem.ImageTransparentColor = System.Drawing.Color.Magenta Me.OpenToolStripMenuItem.Name = "OpenToolStripMenuItem" Me.OpenToolStripMenuItem.ShortcutKeys = CType((System.Windows.Forms.Keys.Control Or System.Windows.Forms.Keys.O), System.Windows.Forms.Keys) Me.OpenToolStripMenuItem.Size = New System.Drawing.Size(152, 22) Me.OpenToolStripMenuItem.Text = "&Open" ' 'tcDesigners ' Me.tcDesigners.Dock = System.Windows.Forms.DockStyle.Fill Me.tcDesigners.Location = New System.Drawing.Point(0, 24) Me.tcDesigners.Name = "tcDesigners" Me.tcDesigners.SelectedIndex = 0 Me.tcDesigners.Size = New System.Drawing.Size(789, 473) Me.tcDesigners.TabIndex = 1 ' 'Form1 ' Me.AutoScaleDimensions = New System.Drawing.SizeF(6.0!, 13.0!) Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font Me.ClientSize = New System.Drawing.Size(789, 497) Me.Controls.Add(Me.tcDesigners) Me.Controls.Add(Me.MenuStrip1) Me.MainMenuStrip = Me.MenuStrip1 Me.Name = "Form1" Me.Text = "Form1" Me.MenuStrip1.ResumeLayout(False) Me.MenuStrip1.PerformLayout() Me.ResumeLayout(False) Me.PerformLayout() End Sub Friend WithEvents MenuStrip1 As System.Windows.Forms.MenuStrip Friend WithEvents FileToolStripMenuItem As System.Windows.Forms.ToolStripMenuItem Friend WithEvents OpenToolStripMenuItem As System.Windows.Forms.ToolStripMenuItem Friend WithEvents tcDesigners As System.Windows.Forms.TabControlEnd
ClassDesignerLoader.vb
Code SnippetImports
ICSharpCode.NRefactoryImports
system.CodeDomImports
System.CodeDom.CompilerImports
System.ComponentModel.DesignImports
System.ComponentModel.Design.SerializationImports
System.IOImports
System.TextPublic
Class DesignerLoader Inherits CodeDomDesignerLoader ' Enum of supported language file extensions. These entries match corresponding SharpDevelop SupportedLanguage ' values. Private Enum SupportedLanguagesCS
VB
End Enum
' Imports / using statements for supported languages. Dim importTokens As String() = {"using", "Imports"}' Statement termination characters for supported languages.
Dim statementTerminators As String() = {";", ""}Dim sourceFile As String
Dim provider As CodeDomProvider Dim errors As String#
Region " Construction / Initialization " ''' <summary> ''' Duh ''' </summary> ''' <param name="fileName">A code file to open.</param> ''' <remarks>If the file has matching associates, they will be loaded as well. See LoadReaders() ''' for details.</remarks> Public Sub New(ByVal fileName As String)Init(fileName)
End Sub
''' <summary> ''' Saves the requested source files and gets a CodeDomProvider appropriate for the file type. ''' </summary> ''' <param name="fileName">Name of the file to load.</param> ''' <remarks></remarks> Private Sub Init(ByVal fileName As String)sourceFile = fileName
provider = GetProvider(fileName)
End Sub#
End Region#
Region " CodeDomDesignerLoader implementation / overrides " ''' <summary> ''' Returns a reference to our CodeDomProvider, which is specific to the type of file passed in ''' at initialization. ''' </summary> ''' <value></value> ''' <returns></returns> ''' <remarks></remarks> Protected Overrides ReadOnly Property CodeDomProvider() As System.CodeDom.Compiler.CodeDomProvider Get Return provider End Get End Property ''' <summary> ''' Parses the specified source file (and associated designer / partial files) with the help of SharpDevelop. ''' </summary> ''' <returns>A CodeCompileUnit representing the class(es) included in the source file.</returns> ''' <remarks></remarks> Protected Overrides Function Parse() As System.CodeDom.CodeCompileUnit Dim parser As SharpDevParser Dim mergeFileName As String = "" Dim reader As TextReader = Nothing Dim ccu As CodeCompileUnit = Nothing Dim readers As List(Of TextReader) = LoadReaders(sourceFile)If readers.Count > 0 Then
mergeFileName = MergeFiles(readers)
If mergeFileName.Length > 0 Thenreader = File.OpenText(mergeFileName)
End If End If
If reader IsNot Nothing Then Tryparser =
New SharpDevParser()ccu = parser.Parse(GetLanguageIndex(provider), reader, GetService(
GetType(ITypeResolutionService))) Catch ex As Exception Finallyreader.Close()
TryFile.Delete(mergeFileName)
Catch ex As Exception End Try End Try End IfCloseReaders(readers)
Return ccu End Function
''' <summary> ''' Returns an instance of a Type Resultion Service. We don't provide one ourselves... just return whatever the ''' LoaderHost gives us. ''' </summary> ''' <value>Whatever the LoaderHost gives us via GetService().</value> ''' <returns>Uhhh, I just said that... Whatever the LoaderHost gives us via GetService().</returns> ''' <remarks></remarks> Protected Overrides ReadOnly Property TypeResolutionService() As System.ComponentModel.Design.ITypeResolutionService Get Return LoaderHost.GetService(GetType(TypeResolutionService)) End Get End Property
''' <summary> ''' Not supported in this implementation. ''' </summary> ''' <param name="unit">CodeCompileUnit to write to file.</param> ''' <remarks></remarks> Protected Overrides Sub Write(ByVal unit As System.CodeDom.CodeCompileUnit) ' End Sub#
End Region#
Region " Nitty gritty " ''' <summary> ''' Attempts to load all partial class definitions given a source code file. ''' </summary> ''' <param name="sourceFile">One of the source files in a set of related files</param> ''' <returns>A list of TextReaders, each loaded with a file assumed to belong to the same ''' class definition. This list will be empty (count = 0) if an error occurs or the specified ''' file is not found.</returns> ''' <remarks>Partial classes are supported but will only be located if all class files reside ''' in the same folder and follow the same naming pattern as the file specified. The naming pattern ''' looked for is {some name}{optional ".*"}.{language extension} ''' ''' Eg. If Form1.Designer.vb is specified, then its code-behind file Form1.vb would also be loaded, ''' as would a file named Form1.partial2.vb, etc. ''' </remarks> Private Function LoadReaders(ByVal sourceFile As String) As List(Of TextReader) Dim basePath As String = Path.GetDirectoryName(sourceFile) Dim fileExtension As String = Path.GetExtension(sourceFile) Dim baseName As String = GetBaseFileName(sourceFile) Dim files() As String = Directory.GetFiles(basePath, baseName & "*" & fileExtension) Dim validFiles As List(Of String) = New List(Of String) Dim readerList As List(Of TextReader) = New List(Of TextReader) Dim regEx As RegularExpressions.Regex = New RegularExpressions.Regex("(" & baseName & ")(\..+)*(" & fileExtension & ")")Try
For Each file As String In files If regEx.IsMatch(file) ThenvalidFiles.Add(file)
End If Next For Each name As String In validFilesreaderList.Add(File.OpenText(name))
Next Catch ex As ExceptionTrace.WriteLine(
"Exception opening source files - " & ex.Message)CloseReaders(readerList)
readerList.Clear()
End Try Return readerList End Function
''' <summary> ''' Gets the first part of a source code file name (ie. everything before the first "."). ''' </summary> ''' <param name="fullPath">Full path to the file to evaluate.</param> ''' <returns>Base file name.</returns> ''' <remarks>Give a file named "Form1.Designer.vb", this function would return "Form1".</remarks> Private Function GetBaseFileName(ByVal fullPath As String) As String Dim baseName As String = Path.GetFileNameWithoutExtension(fullPath) Dim theDot As Integer = baseName.IndexOf(".")If theDot > 0 Then
baseName = baseName.Substring(0, theDot)
End If Return baseName End Function
''' <summary> ''' Merges related code files into a temporary file. ''' </summary> ''' <param name="fileReaders">List of open TextReaders, each loaded with a related code file.</param> ''' <returns>Path to the merged file.</returns> ''' <remarks>The resulting file will have all Import / using statements from all files moved to the top. ''' Parsers like it that way. ''' </remarks> Private Function MergeFiles(ByVal fileReaders As List(Of TextReader)) As String Dim sourceCode As String = "" Dim importList As List(Of String) Dim importToken As String Dim statementTerminator As String Dim mergeFile As String = "" Dim language As Integer = GetLanguageIndex(provider)If language < 0 Then Return ""
TryimportList =
New List(Of String)importToken = importTokens(language)
statementTerminator = statementTerminators(language)
For Each reader As TextReader In fileReaderssourceCode += PullCodeExtractImports(reader, importToken, statementTerminator, importList)
NextimportList.Sort()
For index As Integer = 0 To importList.Count - 1importList(index) = importToken &
" " & importList(index) NextsourceCode =
String.Join(statementTerminator & vbCrLf, importList.ToArray) & vbCrLf & sourceCode If sourceCode.Length > 0 ThenmergeFile = Path.GetTempFileName()
File.AppendAllText(mergeFile, sourceCode)
End If
Catch ex As ExceptionTrace.WriteLine(ex.ToString)
If mergeFile.Length > 0 Then TryFile.Delete(mergeFile)
Catch ex2 As Exception End Try End IfmergeFile =
"" End Try Return mergeFile End Function
''' <summary> ''' Pulls the code from a text file, excluding Import / using statements. ''' </summary> ''' <param name="reader">Reader opened with the source file to read.</param> ''' <param name="importToken">Language-specific token for the Imports / using statement.</param> ''' <param name="statementTerminator">Language-specific code statement terminator. ";" in C#, empty string in VB.</param> ''' <param name="importList">Reference to a list of import statements. Imports found in the current file will be added ''' to this list.</param> ''' <returns>The resulting code, as a string, minus any import statements.</returns> ''' <remarks></remarks> Private Function PullCodeExtractImports(ByVal reader As TextReader, ByVal importToken As String, ByVal statementTerminator As String, ByRef importList As List(Of String)) As String Dim line As String = Nothing Dim resultString As String = "" Dim strippedLine As Stringline = reader.ReadLine
Do Until line Is NothingstrippedLine = line.Trim.TrimEnd(
New Char() {statementTerminator})If strippedLine.ToLower.StartsWith(importToken.ToLower) AndAlso _
importList.IndexOf(strippedLine.Substring(importToken.Length + 1)) < 0 Then
importList.Add(strippedLine.Substring(importToken.Length + 1))
ElseresultString += line & vbCrLf
End Ifline = reader.ReadLine
Loop Return resultString End Function
''' <summary> ''' Returns a SupportedLanguages value given a language-specific CodeDomProvider. ''' </summary> ''' <param name="provider">The CodeDomProvider for the language to query.</param> ''' <returns>One of the SupportedLanguages values, -1 if CodeDomProvider is not supported by this ''' class</returns> ''' <remarks></remarks> Private Function GetLanguageIndex(ByVal provider As CodeDomProvider) As Integer Try Return CInt(System.Enum.Parse(GetType(SupportedLanguages), provider.FileExtension, True)) Catch ex As Exception Return -1 End Try End Function
''' <summary> ''' Closes a bunch of text readers. ''' </summary> ''' <param name="readers">List of open TextReaders to close.</param> ''' <remarks></remarks> Private Sub CloseReaders(ByVal readers As List(Of TextReader)) For Each reader As TextReader In readers Tryreader.Close()
Catch ex As Exception End TryNext
End Sub
''' <summary> ''' Returns a CodeDomProvider appropriate for the file specified, based on its file extension. ''' </summary> ''' <param name="fileName">Code file whose provider is to be loaded.</param> ''' <returns>A CodeDomProvider appropriate for the file specified if successful, ''' Nothing otherwise.</returns> ''' <remarks></remarks> Private Function GetProvider(ByVal fileName As String) As CodeDomProvider Try Dim language As String = CodeDomProvider.GetLanguageFromExtension(Path.GetExtension(fileName)) Return CodeDomProvider.CreateProvider(language) Catch ex As ExceptionTrace.WriteLine(ex.ToString)
Return Nothing End Try End Function#
End RegionEnd
ClassSharpDevParser.vb
Code SnippetImports
system.CodeDomImports
System.CodeDom.CompilerImports
System.ComponentModelImports
System.ComponentModel.DesignImports
System.ComponentModel.Design.SerializationImports
System.Windows.Forms.DesignImports
System.IOImports
ICSharpCode.NRefactoryPublic
Class SharpDevParser Dim _errorOutput As String = "" Dim _errorCount As Integer = 0
''' <summary> ''' Uses the SharpDevelop parser to parse the source file. ''' </summary> ''' <returns>A CodeCompileUnit representing the source file, Nothing if parsing fails.</returns> ''' <remarks>If the parse fails, ErrorOutput will contain the parser's ErrorOutput string and ''' ErrorCount will indicate the number of errors encountered.</remarks> Public Function Parse(ByVal language As SupportedLanguage, ByVal sourceReader As TextReader, ByVal typeResolutionService As ITypeResolutionService) As CodeCompileUnitDim parser As ICSharpCode.NRefactory.IParser = ParserFactory.CreateParser(language, sourceReader)
Dim visitor As Visitors.CodeDomVisitor
ResetErrors()
parser.ParseMethodBodies = True
parser.Parse()
_errorCount = parser.Errors.Count
_errorOutput = parser.Errors.ErrorOutput
If ErrorCount = 0 Thenvisitor =
New Visitors.CodeDomVisitor()visitor.EnvironmentInformationProvider =
New EnvironmentInformationProvider(typeResolutionService)visitor.VisitCompilationUnit(parser.CompilationUnit,
Nothing) Return visitor.codeCompileUnit Else Return Nothing End If End Function
''' <summary> ''' Resets ErrorOutput and ErrorCount to default values. ''' </summary> ''' <remarks></remarks> Private Sub ResetErrors()_errorCount = 0
_errorOutput =
"" End Sub
''' <summary> ''' Returns a string representing errors encountered during a parse. ''' </summary> ''' <value></value> ''' <returns></returns> ''' <remarks></remarks> Public ReadOnly Property ErrorOutput() As String Get Return _errorOutput End Get End Property
''' <summary> ''' Returns the number of errors encountered during a parse. ''' </summary> ''' <value></value> ''' <returns></returns> ''' <remarks></remarks> Public ReadOnly Property ErrorCount() As Integer Get Return _errorCount End Get End PropertyEnd
Class'''
<summary>''' Implementation of IEnvironmentInformationProvider.
'''
</summary>'''
<remarks>IEnvironmentInformationProvider is used by a Visitor to determine whether a Type contains''' a field with the specific name. This is necessary because the parser can't tell, from reading the code,
''' whether a member is a field or a property. An Enum's values, for instance, cannot be differentiated
''' from a class type's properties.
'''
''' Failure to make this determination can result in failure to load the member. In the absence of an
''' IEnvironmentInformationProvider the parser will assume a property/
</remarks> Friend Class EnvironmentInformationProvider Implements IEnvironmentInformationProvider Dim resolutionService As ITypeResolutionService
''' <summary> ''' Constructor. ''' </summary> ''' <param name="typeResolutionService">A Type Resolution Service instance. This is used to resolve the type ''' requested.</param> ''' <remarks></remarks> Public Sub New(ByVal typeResolutionService As ITypeResolutionService)resolutionService = typeResolutionService
End Sub
''' <summary> ''' Determines if a type has a field with a given name. ''' </summary> ''' <param name="fullTypeName">Name of the type to query.</param> ''' <param name="fieldName">Name of the field to look for.</param> ''' <returns>True if the type has a field by the given name, False if an error occurs or the type ''' doesn't contain a corresponding field.</returns> ''' <remarks></remarks> Public Function HasField(ByVal fullTypeName As String, ByVal fieldName As String) As Boolean Implements ICSharpCode.NRefactory.IEnvironmentInformationProvider.HasField Dim theType As Type = Type.GetType(fullTypeName)If theType Is Nothing AndAlso resolutionService IsNot Nothing Then
theType = resolutionService.GetType(fullTypeName)
End If If theType Is Nothing Then Return False Else Return Not theType.GetField(fieldName) Is Nothing End If End FunctionEnd
ClassAnd finally, you need a decent ITypeResolutionService. This is a modified version of SharpDevelop's.
TypeResolutionService.vb
Code SnippetImports
SystemImports
System.ComponentModel.DesignImports
System.ReflectionImports
System.IOImports
Microsoft.Win32'''
<summary>''' Modified port of SharpDev's TypeResolutionService. Resolves types during a designer load.
'''
</summary>'''
<remarks></remarks>Public
Class TypeResolutionService Implements ITypeResolutionService<System.Runtime.InteropServices.DllImport(
"kernel32.dll")> _ Private Shared Function MoveFileEx(ByVal lpExistingFileName As String, ByVal lpNewFileName As String, ByVal dwFlags As Integer) As Boolean End Function
Shared ReadOnly _designerAssemblies As List(Of Assembly) = New List(Of Assembly) Shared ReadOnly assemblyDict As Dictionary(Of String, Assembly) = New Dictionary(Of String, Assembly) '''<summary> ''' List of assemblies used by the form designer. This static list is not an optimal solution, ''' but better than using AppDomain.CurrentDomain.GetAssemblies(). See SD2-630. ''' </summary> Public Shared ReadOnly Property DesignerAssemblies() As List(Of Assembly) Get Return _designerAssemblies End Get End Property Shared Sub New()ClearMixedAssembliesTemporaryFiles()
' Preload commonly referenced assemblies.DesignerAssemblies.Add(
GetType(Object).Assembly) ' MSCorlibDesignerAssemblies.Add(
GetType(Uri).Assembly) ' SystemDesignerAssemblies.Add(
GetType(System.Drawing.Point).Assembly) ' System.DrawingDesignerAssemblies.Add(
GetType(System.Windows.Forms.AutoScaleMode).Assembly) ' System.Windows.FormsDesignerAssemblies.Add(
GetType(System.Windows.Forms.Design.AnchorEditor).Assembly) ' System.Windows.Forms.DesignRegisterVSDesignerWorkaround()
AddHandler AppDomain.CurrentDomain.AssemblyResolve, AddressOf AssemblyResolveEventHandler End Sub
Const MOVEFILE_DELAY_UNTIL_REBOOT As Integer = &H4 Shared Sub MarkFileToDeleteOnReboot(ByVal fileName As String)MoveFileEx(fileName,
Nothing, MOVEFILE_DELAY_UNTIL_REBOOT) End Sub Shared Sub ClearMixedAssembliesTemporaryFiles() Dim files As String() = Directory.GetFiles(Path.GetTempPath(), "*.sd_forms_designer_mixed_assembly.dll") For Each filename As String In files TryFile.Delete(filename)
Catch End Try Next End Sub Dim formSourceFileName As String Public Sub New() End Sub Public Sub New(ByVal formSourceFileName As String) Me.formSourceFileName = formSourceFileName End Sub Shared Function GetHash(ByVal fileName As String) As String Return Path.GetFileName(fileName).ToLowerInvariant() + File.GetLastWriteTimeUtc(fileName).Ticks.ToString() End Function ''' <summary> ''' Loads the file in none-locking mode. Returns null on failure. '''</summary> Public Shared Function LoadAssembly(ByVal fileName As String) As Assembly If Not File.Exists(fileName) Then Return Nothing End If ' FIX for SD2-716, remove when designer gets its own AppDomain For Each asm As Assembly In AppDomain.CurrentDomain.GetAssemblies() Try If String.Equals(asm.Location, fileName, StringComparison.InvariantCultureIgnoreCase) ThenRegisterAssembly(asm)
Return asm End If Catch e As NotSupportedException ' Fixes forum-12823 End Try Next Dim hash As String = GetHash(fileName) SyncLock assemblyDict Dim asm As Assembly If assemblyDict.TryGetValue(hash, asm) Then Return asm End If Tryasm = Assembly.Load(File.ReadAllBytes(fileName))
Catch e As BadImageFormatException If e.Message.Contains("HRESULT: 0x8013141D") Then Dim tempPath As String = Path.GetTempFileName()File.Delete(tempPath)
tempPath +=
".sd_forms_designer_netmodule_assembly.dll" Try 'convert netmodule to assembly Dim p As Process = New Process()p.StartInfo.UseShellExecute =
Falsep.StartInfo.FileName = Path.GetDirectoryName(
GetType(Object).Module.FullyQualifiedName) + Path.DirectorySeparatorChar + "al.exe"p.StartInfo.Arguments =
"""" & fileName & " /out:""" + tempPath + """"p.StartInfo.CreateNoWindow =
Truep.Start()
p.WaitForExit()
If p.ExitCode = 0 And File.Exists(tempPath) Then Dim asm_data As Byte() = File.ReadAllBytes(tempPath)asm = Assembly.Load(asm_data)
asm.LoadModule(Path.GetFileName(fileName), File.ReadAllBytes(fileName))
End If Catch ex As ExceptionMsgBox(ex.ToString, MsgBoxStyle.Critical,
"Error calling linker for netmodule") End Try TryFile.Delete(tempPath)
Catch End Try Else Throw ' don't ignore other load errors End If Catch e As FileLoadException If e.Message.Contains("HRESULT: 0x80131402") Then 'this is C++/CLI Mixed assembly which can only be loaded from disk, not in-memory Dim tempPath As String = Path.GetTempFileName()File.Delete(tempPath)
tempPath +=
".sd_forms_designer_mixed_assembly.dll"File.Copy(fileName, tempPath)
asm = Assembly.LoadFile(tempPath)
MarkFileToDeleteOnReboot(tempPath)
Else Throw ' don't ignore other load errors End If End Try SyncLock DesignerAssemblies If Not DesignerAssemblies.Contains(asm) ThenDesignerAssemblies.Insert(0, asm)
End If End SyncLockassemblyDict(hash) = asm
Return asm End SyncLock End Function Public Shared Sub RegisterAssembly(ByVal asm As Assembly) Dim file As String = asm.Location If file.Length > 0 Then SyncLock assemblyDictassemblyDict(GetHash(file)) = asm
End SyncLock End If SyncLock DesignerAssemblies If Not DesignerAssemblies.Contains(asm) ThenDesignerAssemblies.Insert(0, asm)
End If End SyncLock End Sub Public Function GetAssembly(ByVal name As AssemblyName) As Assembly Implements System.ComponentModel.Design.ITypeResolutionService.GetAssembly Return LoadAssembly(name, False) End Function Public Function GetAssembly(ByVal name As AssemblyName, ByVal throwOnError As Boolean) As Assembly Implements System.ComponentModel.Design.ITypeResolutionService.GetAssembly Return LoadAssembly(name, throwOnError) End Function Shared Function LoadAssembly(ByVal name As AssemblyName, ByVal throwOnError As Boolean) As Assembly Try Dim asm As Assembly = Assembly.Load(name)RegisterAssembly(asm)
Return asm Catch e As System.IO.FileLoadException If throwOnError Then Throw End If Return Nothing End Try End Function Public Function GetPathOfAssembly(ByVal name As AssemblyName) As String Implements System.ComponentModel.Design.ITypeResolutionService.GetPathOfAssembly Dim Assembly As Assembly = GetAssembly(name) If Assembly IsNot Nothing Then Return Assembly.Location End If Return Nothing End Function Public Overloads Function [GetType](ByVal name As String) As Type Implements System.ComponentModel.Design.ITypeResolutionService.GetType Return [GetType](name, False, False) End Function Public Overloads Function [GetType](ByVal name As String, ByVal throwOnError As Boolean) As Type Implements System.ComponentModel.Design.ITypeResolutionService.GetType Return [GetType](name, throwOnError, False) End Function Public Overloads Function [GetType](ByVal name As String, ByVal throwOnError As Boolean, ByVal ignoreCase As Boolean) As Type Implements System.ComponentModel.Design.ITypeResolutionService.GetType If name Is Nothing OrElse name.Length = 0 Then Return Nothing End If If IgnoreType(name) Then Return Nothing End If Try Dim type As Type = System.Type.GetType(name, False, ignoreCase) ' type lookup for typename, assembly, xyz style lookups If type Is Nothing Then Dim idx As Integer = name.IndexOf(",") If idx > 0 Then Dim splitName As String() = name.Split(",") Dim typeName As String = splitName(0) Dim assemblyName As String = splitName(1).Substring(1) Dim assembly As Assembly = Nothing Tryassembly = assembly.Load(assemblyName)
Catch e As Exception End Try If assembly IsNot Nothing Then SyncLock DesignerAssemblies If Not DesignerAssemblies.Contains(assembly) ThenDesignerAssemblies.Add(assembly)
End If End SyncLocktype = assembly.GetType(typeName,
False, ignoreCase) Elsetype = type.GetType(typeName,
False, ignoreCase) End If End If End If If type Is Nothing Then SyncLock DesignerAssemblies For Each asm As Assembly In DesignerAssemblies Dim t As Type = asm.GetType(name, False) If t IsNot Nothing Then Return t End If Next End SyncLock End If If type Is Nothing Thentype = SearchLoadedAssemblies(name, ignoreCase)
End If If throwOnError And type Is Nothing Then Throw New TypeLoadException(name + " not found by TypeResolutionService") End If Return type Catch e As Exception End Try Return Nothing End Function Public Sub ReferenceAssembly(ByVal name As AssemblyName) Implements System.ComponentModel.Design.ITypeResolutionService.ReferenceAssemblyTrace.WriteLine(name)
End Sub#
Region " VSDesigner workarounds " '''<summary> ''' HACK - Ignore any requests for types from the Microsoft.VSDesigner ''' assembly. There are smart tag problems if data adapter ''' designers are used from this assembly. ''' </summary> Function IgnoreType(ByVal name As String) As Boolean Dim idx As Integer = name.IndexOf(",") If idx > 0 Then Dim splitName As String() = name.Split(",") Dim assemblyName As String = splitName(1).Substring(1) If assemblyName = "Microsoft.VSDesigner" Then Return True End If End If Return False End Function Shared vsDesignerIdeDir As String Shared Sub RegisterVSDesignerWorkaround() If vsDesignerIdeDir Is Nothing ThenvsDesignerIdeDir =
"" Dim key As RegistryKey = Registry.LocalMachine.OpenSubKey("SOFTWARE\Microsoft\VisualStudio\8.0\Setup\VS") If key IsNot Nothing ThenvsDesignerIdeDir = key.GetValue(
"VS7CommonDir").ToString If vsDesignerIdeDir.Length > 0 ThenvsDesignerIdeDir = Path.Combine(vsDesignerIdeDir,
"IDE") AddHandler AppDomain.CurrentDomain.AssemblyResolve, AddressOf DomainResolveHandler End If End If End If End Sub Shared Function DomainResolveHandler(ByVal sender As Object, ByVal args As ResolveEventArgs) As Assembly Dim shortName As String = args.Name If shortName.IndexOf(",") >= 0 ThenshortName = shortName.Substring(0, shortName.IndexOf(
",")) End If If shortName.StartsWith("Microsoft.") And File.Exists(Path.Combine(vsDesignerIdeDir, shortName & ".dll")) Then Return Assembly.LoadFrom(Path.Combine(vsDesignerIdeDir, shortName + ".dll")) End If Return Nothing End Function#
End Region Shared Function SearchLoadedAssemblies(ByVal name As String, ByVal ignoreCase As Boolean) As Type Dim lastAssembly As Assembly = Nothing Dim foundType As Type = Nothing For Each asm As Assembly In AppDomain.CurrentDomain.GetAssembliesfoundType = asm.GetType(name,
False, ignoreCase) If foundType IsNot Nothing ThenlastAssembly = asm
Exit For End If Next If lastAssembly IsNot Nothing ThenTypeResolutionService.DesignerAssemblies.Add(lastAssembly)
End If Return foundType End Function Shared Function AssemblyResolveEventHandler(ByVal sender As Object, ByVal args As ResolveEventArgs) As Assembly Dim lastAssembly As Assembly = Nothing For Each asm As Assembly In TypeResolutionService.DesignerAssemblies If asm.FullName = args.Name Then Return asm End If Next For Each asm As Assembly In AppDomain.CurrentDomain.GetAssemblies If asm.FullName = args.Name ThenlastAssembly = asm
End If Next If lastAssembly IsNot Nothing ThenTypeResolutionService.DesignerAssemblies.Add(lastAssembly)
End If Return lastAssembly End FunctionEnd
ClassMarc, you rock.
I've been looking for an example of this stuff for over a week. Everybody shows you how to use the CodeDom to generate the code but nobody worries about modifying the code generated.
I'm going to work on converting your sample to C#, let me know if you want me to repost it or send it to you or something appropriate.
Russ
Russ,
Everybody shows you how to use the CodeDom to generate the code but nobody worries about modifying the code generated.
Yeah, I noticed
Looking into this taught me why that is though. Even if Microsoft provided parsers with its CodeDomProviders, they wouldn't be bulletproof because you're opening a file that wasn't created or managed in a known way (as opposed to creating and modifying a form in VS, where they can control the circumstances.) Still I wish they'd supply a best-guess implementation and save us this aggravation.I personally don't need the parser but feel free to post a C# translation. I'm sure many would appreciate it.
Regards,
Marc.
- Incredible! Your sample accomplished what I haven't been able to do for weeks!
I do have a couple more quick questions, though. First, under the LGPL with ICSharpCode.NRefactory.dll, am I allowed to use the library if I just add it as a reference like for using your sample then distributing it as such in a commercial application? What kind of license info would I have to provide? Also, is there any way to edit the DesignerLoader class so that it can generate code from the designer, like in the MSDN Magazine sample's CodeDomHostLoader? Finally, is it possible to associate an opened form with a PropertyGrid? I can get this to work with new forms, just not opened ones! 
Thank you so much for your terrific help - you rock! First, under the LGPL with ICSharpCode.NRefactory.dll, am I allowed to use the library if I just add it as a reference like for using your sample then distributing it as such in a commercial application?
I'm not a lawyer or anything, but my understanding is that you can distribute the DLL with your app. as long as you haven't made any code changes to it. If you do, then you have to publish the source for the modified DLL.
Here are relevant quotes:
From the #Develop licensing FAQ:
What’s in for me (the developer) now that you changed the license?
We imagine two main scenarios: you writing arbitrarily-licensed addins for #develop, as well as you using our assemblies to build your applications (especially the core, the text editor and #report). After all, linking to our binary assemblies is now restriction-free. All we ask (but not require) is that you do not rename the assemblies you use (we see this as free PR for us).
What if I need to modify the assemblies to suit my needs?
The only difference between GPL and LGPL is the linking exception. So if you need to modify the source code, the restriction of shipping your source code modifications alongside the compiled binary still kicks in. If you do not want to ship your source code modification, get in touch with us.
From Christoph Wille (one of the brains behind #D) in a forum post:
any modifications to our codebase must be made public. Linking to assemblies that are based on such a modified codebase is fine. Because our codebase is LGPL (linking to assemblies fine, modifications of the source code must be released under the same license).
From GNU.Org:
5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License.
However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables.
Also, is there any way to edit the DesignerLoader class so that it can generate code from the designer, like in the MSDN Magazine sample's CodeDomHostLoader?
Sure
Once you have it in the designer, it's in object form and is available for code generation. You'll need to use some smarts if you want to split the object into multiple source files the way VS does (one designer file, one "user" file.) I'll see about putting something together.Finally, is it possible to associate an opened form with a PropertyGrid?
I'm sure there is but I'm drawing a blank on it at the moment. Gotta head out for a bit but I'll get back to you on that a little later.
Marc.
I'm not going to modify the parser so much as to modify your sample code and make all of it C#.
Annonymous 5400 was asking about linking to a Property Grid. I am in the process of getting that up and running and can post it in a while. The project I am coding also includes generating and storing the C# code that is derived from the CodeCompileUnit so that too is part of my next steps. It sounds like Anonymous and I are on a parallel tack.
Marc, when I am done are you interested in co-publishing anything on this? The only ones who really know this stuff and tell you so are the CSharp folks and there is so much there as an example that it's overwhelming. It wasn't until I saw you override the Parse method of the loader that I realized what was going on. Until then I knew how to get a ccu but didn't know what to do with it.
Russ
Hey Russ,
I was just about to post the propertygrid thing
I'll do the VB side, you go ahead and post your C# version. Anonnymous, if you're able to get it working with an existing form you shouldn't have a problem with the new one. It's all in the design surface.As far as co-publishing, I'm game for pretty much anything. Drop me a line at the email address in my profile.
I should make it clear, though, that there are pitfalls to this approach. I just now ran into a problem - #D's parser doesn't like the VB event handler construct when you're handling an event in your own class (for instance, MyForm_Load(...) Handles Me.Load) The visitor will still work but I don't know yet what it does behind the scenes. I'll go search the #D forums to see if it's a known limitation.
I also ran into a problem with a referencing a resource property - the statement "Me.PictureBox1.Image = Global.WindowTester.My.Resources.Resources.Test21" is accepted by SharpDev but VS complains that Test21 isn't a valid property. Again, seems to be a compatibility issue between #D and VS.
Anyway, Anonnymous, here's a very basic design surface that'll refresh the property grid when you select something in your opened form. Create the following design surface, then modify the OpenForm sub in the sample I posted. Replace the line ds = New DesignSurface(serviceContainer) with ds = New BasicDesignSurface(serviceContainer)
BasicDesignSurface:
Code SnippetImports
System.ComponentModel.DesignPublic
Class BasicDesignSurface Inherits DesignSurface Dim selectionSvc As ISelectionService Public Sub New() Me.New(Nothing) End Sub Public Sub New(ByVal parentProvider As IServiceProvider) MyBase.New(parentProvider)HookSelectionChangeEvents()
End Sub Private Sub HookSelectionChangeEvents() If SelectionService IsNot Nothing Then AddHandler SelectionService.SelectionChanged, AddressOf selectionService_SelectionChanged End If End Sub Private Sub UnhookSelectionChangeEvents() If SelectionService IsNot Nothing Then RemoveHandler SelectionService.SelectionChanged, AddressOf selectionService_SelectionChanged End If End Sub Public ReadOnly Property SelectionService() As ISelectionService Get If IsNothing(selectionSvc) ThenselectionSvc = GetService(
GetType(ISelectionService)) End If Return selectionSvc End Get End Property ''' <summary> ''' When the selection changes this sets the PropertyGrid's selected component ''' </summary> Private Sub selectionService_SelectionChanged(ByVal sender As Object, ByVal e As EventArgs) Dim comps As ArrayList = New ArrayList Dim propertyGrid As PropertyGrid If SelectionService IsNot Nothing ThenpropertyGrid =
TryCast(GetService(GetType(PropertyGrid)), PropertyGrid) If propertyGrid IsNot Nothing ThenpropertyGrid.SelectedObjects = SelectionService.GetSelectedComponents
End If End If End Sub Protected Overrides Sub Dispose(ByVal disposing As Boolean)UnhookSelectionChangeEvents()
MyBase.Dispose(disposing) End SubEnd
ClassRegards,
Marc.
Ok the event handler thing is documented here. Here's how to fix the sample code.
1. Replace the If / Else in the loop in function DesignerLoader.PullCodeExtractImports() with the following:
Code SnippetIf
strippedLine.ToLower.StartsWith(importToken.ToLower) AndAlso _importList.IndexOf(strippedLine.Substring(importToken.Length + 1)) < 0
ThenimportList.Add(strippedLine.Substring(importToken.Length + 1))
Else ' Compatibility work-around: #Develop's parser doesn't like VB's "Handles Me.xxx" event handler ' statement. The fix is to replace "Handles Me." with "Handles MyBase." Fortunately the latter ' statement is also valid in VSresultString += ReplaceHandlesClause(line) & vbCrLf
End If2. Add new function DesignerLoader.ReplacesHandlesClause():
Code Snippet''' <summary>
''' Replaces a VB Handles clause ("MyForm_Load(...) Handles Me.Load") with one that is compatible with ''' SharpDev's parser ("Handles MyBase.Load"). ''' </summary> ''' <param name="line">Source code line to work on.</param> ''' <returns>Source line with a modified Handles clause.</returns> ''' <remarks>A Handles clause can include a list of events being handled, so we have to find the ''' start of the clause and work through the remainder of the string to find the "Me" references.</remarks> Private Function ReplaceHandlesClause(ByVal line As String) As String Dim handlesClause As String = "Handles" Dim meStatement As String = "Me" Dim myBaseStatement As String = "MyBase" Dim handlesClauseIndex As Integer Dim meIndex As Integer Dim handlesSubstring As String
handlesClauseIndex = line.IndexOf(handlesClause, 0, StringComparison.InvariantCultureIgnoreCase) If handlesClauseIndex >= 0 Then
' The simplest way to do this would be to use handlesSubstring.Replace() to replace all ' occurences of Me with MyBase, but we can't rely on the code being mixed case. Ohhh, it probably ' is, but ya never know.meIndex = 0
handlesSubstring = line.Substring(handlesClauseIndex)
meIndex = handlesSubstring.IndexOf(meStatement, meIndex, StringComparison.InvariantCultureIgnoreCase)
Do While meIndex >= 0
handlesSubstring = handlesSubstring.Substring(0, meIndex) & myBaseStatement & handlesSubstring.Substring(meIndex + meStatement.Length)
meIndex = handlesSubstring.IndexOf(meStatement, meIndex, StringComparison.InvariantCultureIgnoreCase)
Loop
Return line.Substring(0, handlesClauseIndex) & handlesSubstring Else Return line End If End FunctionThat should do it. I'll look into the other problem.
Marc.
I'm not sure how it translates into VB but I was able to get the Property grid running at a higher level so I didn't have to create a BasicDesignSurface. If you call Host.GetServices for ISelectionService you get an object that you can then dynamically wire to the SelectionChanged event.
Either way you do it, supposing you can do what I describe in VB, you have to make sure that the event handler has access to the PropertyGrid instance you want to update.
Oh, yeah that's right. I assumed Anonymous had a custom design surface but you can do it from anywhere as long as you can get to the services container and have access to the propertygrid.
I've gotten busy lately so I haven't had time write a sample on saving the loaded file back to disk, sorry. Basically you just need to do the following:
- Call the DesignerLoader's Flush() method;
- If any changes have been made, DesignerLoader will call the protected Write() method (which I left empty in the sample) with a code compile unit representing the form currently loaded;
- Use that code compile unit in a call to CodeDomProvider.GenerateCodeFromCompileUnit() to write it out to a file. You can either use the same provider you used to open the file if you're saving to the same language; otherwise create one of the proper type.
Note that Flush() won't call Write() unless a change has been made. Specifically, it'll look at the protected Modified property, which is set by DesignerLoader as it monitors PropertyChanged, ComponentAdded, ComponentRemoved, and other events in the designer. If you want to force the flush, either override Flush() and set Modified = True before calling the base method, or provide a public method (ForceFlush(), ForceSave(), whatever) that sets the property and then calls Flush().
I gotta say, though, that without some form of document or project management, loading a standalone code file into a designer will be tricky Anonymous. If you try loading a file with some non-VS component / control, or a reference to a non- .Net assembly, you won't be able to resolve it unless you have that same reference in your app. I don't know what type of environment you're planning to do this in so maybe that won't be an issue for you.
At any rate, good luck with it

Marc.
Not sure if this should start a new thread or not but the parser we have been talking about doesn't handle the switch statement, at least not the version I have.
- Let's keep going in this thread... it's easy to find! Anyway, what switch statement are you talking about? Give us more info and maybe we can help out...
I've been busy, and now have a working code generating version of DesignerLoader. Here it is in VB:DesignerLoader.vbImports ICSharpCode.NRefactory
Imports System.CodeDom
Imports System.CodeDom.Compiler
Imports System.ComponentModel.Design
Imports System.ComponentModel.Design.Serialization
Imports System.IO
Imports System.Text
Imports Microsoft.CSharp
Public Class DesignerLoader
Inherits CodeDomDesignerLoader
Private codeCompileUnit As CodeCompileUnit = Nothing
' Enum of supported language file extensions. These entries match corresponding SharpDevelop SupportedLanguage
' values.
Private Enum SupportedLanguages
CS
VB
End Enum
' Imports / using statements for supported languages.
Dim importTokens As String() = {"using", "Imports"}
' Statement termination characters for supported languages.
Dim statementTerminators As String() = {";", ""}
Dim sourceFile As String
Dim provider As CodeDomProvider
Dim errors As String
#Region " Construction / Initialization "
''' <summary>
''' Duh
''' </summary>
''' <param name="fileName">A code file to open.</param>
''' <remarks>If the file has matching associates, they will be loaded as well. See LoadReaders()
''' for details.</remarks>
Public Sub New(ByVal fileName As String)
Init(fileName)
End Sub
''' <summary>
''' Saves the requested source files and gets a CodeDomProvider appropriate for the file type.
''' </summary>
''' <param name="fileName">Name of the file to load.</param>
''' <remarks></remarks>
Private Sub Init(ByVal fileName As String)
sourceFile = fileName
provider = GetProvider(fileName)
End Sub
#End Region
#Region " CodeDomDesignerLoader implementation / overrides "
''' <summary>
''' Returns a reference to our CodeDomProvider, which is specific to the type of file passed in
''' at initialization.
''' </summary>
''' <value></value>
''' <returns></returns>
''' <remarks></remarks>
Protected Overrides ReadOnly Property CodeDomProvider() As System.CodeDom.Compiler.CodeDomProvider
Get
Return provider
End Get
End Property
''' <summary>
''' Parses the specified source file (and associated designer / partial files) with the help of SharpDevelop.
''' </summary>
''' <returns>A CodeCompileUnit representing the class(es) included in the source file.</returns>
''' <remarks></remarks>
Protected Overrides Function Parse() As System.CodeDom.CodeCompileUnit
Dim parser As SharpDevParser
Dim mergeFileName As String = ""
Dim reader As TextReader = Nothing
Dim ccu As CodeCompileUnit = Nothing
Dim readers As List(Of TextReader) = LoadReaders(sourceFile)
If readers.Count > 0 Then
mergeFileName = MergeFiles(readers)
If mergeFileName.Length > 0 Then
reader = File.OpenText(mergeFileName)
End If
End If
If reader IsNot Nothing Then
Try
parser = New SharpDevParser()
ccu = parser.Parse(GetLanguageIndex(provider), reader, GetService(GetType(ITypeResolutionService)))
Catch ex As Exception
Finally
reader.Close()
Try
File.Delete(mergeFileName)
Catch ex As Exception
End Try
End Try
End If
CloseReaders(readers)
Return ccu
End Function
''' <summary>
''' Returns an instance of a Type Resultion Service. We don't provide one ourselves... just return whatever the
''' LoaderHost gives us.
''' </summary>
''' <value>Whatever the LoaderHost gives us via GetService().</value>
''' <returns>Uhhh, I just said that... Whatever the LoaderHost gives us via GetService().</returns>
''' <remarks></remarks>
Protected Overrides ReadOnly Property TypeResolutionService() As System.ComponentModel.Design.ITypeResolutionService
Get
Return LoaderHost.GetService(GetType(TypeResolutionService2))
End Get
End Property
''' <summary>
''' Supporting function for the <see cref="GetCode">GetCode</see> function
''' </summary>
''' <param name="unit">CodeCompileUnit to write to file.</param>
''' <remarks></remarks>
Protected Overrides Sub Write(ByVal unit As System.CodeDom.CodeCompileUnit)
codeCompileUnit = unit
End Sub
Public Function GetCode(ByVal context As String) As String
Me.Modified = True
Flush()
Dim o As New CodeGeneratorOptions()
o.BlankLinesBetweenMembers = True
o.BracingStyle = "C"
o.ElseOnClosing = False
o.IndentString = " "
If context = "C#" Then
Dim swCS As New StringWriter()
Dim cs As New CSharpCodeProvider()
cs.GenerateCodeFromCompileUnit(codeCompileUnit, swCS, o)
Dim code As String = swCS.ToString()
swCS.Close()
Return code
ElseIf context = "VB" Then
Dim swVB As New StringWriter()
Dim vb As New VBCodeProvider()
vb.GenerateCodeFromCompileUnit(codeCompileUnit, swVB, o)
Dim code As String = swVB.ToString()
swVB.Close()
Return code
End If
Return [String].Empty
End Function
#End Region
#Region " Nitty gritty "
''' <summary>
''' Attempts to load all partial class definitions given a source code file.
''' </summary>
''' <param name="sourceFile">One of the source files in a set of related files</param>
''' <returns>A list of TextReaders, each loaded with a file assumed to belong to the same
''' class definition. This list will be empty (count = 0) if an error occurs or the specified
''' file is not found.</returns>
''' <remarks>Partial classes are supported but will only be located if all class files reside
''' in the same folder and follow the same naming pattern as the file specified. The naming pattern
''' looked for is {some name}{optional ".*"}.{language extension}
'''
''' Eg. If Form1.Designer.vb is specified, then its code-behind file Form1.vb would also be loaded,
''' as would a file named Form1.partial2.vb, etc.
''' </remarks>
Private Function LoadReaders(ByVal sourceFile As String) As List(Of TextReader)
Dim basePath As String = Path.GetDirectoryName(sourceFile)
Dim fileExtension As String = Path.GetExtension(sourceFile)
Dim baseName As String = GetBaseFileName(sourceFile)
Dim files() As String = Directory.GetFiles(basePath, baseName & "*" & fileExtension)
Dim validFiles As List(Of String) = New List(Of String)
Dim readerList As List(Of TextReader) = New List(Of TextReader)
Dim regEx As RegularExpressions.Regex = New RegularExpressions.Regex("(" & baseName & ")(\..+)*(" & fileExtension & ")")
Try
For Each file As String In files
If regEx.IsMatch(file) Then
validFiles.Add(file)
End If
Next
For Each name As String In validFiles
readerList.Add(File.OpenText(name))
Next
Catch ex As Exception
Trace.WriteLine("Exception opening source files - " & ex.Message)
CloseReaders(readerList)
readerList.Clear()
End Try
Return readerList
End Function
''' <summary>
''' Gets the first part of a source code file name (ie. everything before the first ".").
''' </summary>
''' <param name="fullPath">Full path to the file to evaluate.</param>
''' <returns>Base file name.</returns>
''' <remarks>Give a file named "Form1.Designer.vb", this function would return "Form1".</remarks>
Private Function GetBaseFileName(ByVal fullPath As String) As String
Dim baseName As String = Path.GetFileNameWithoutExtension(fullPath)
Dim theDot As Integer = baseName.IndexOf(".")
If theDot > 0 Then
baseName = baseName.Substring(0, theDot)
End If
Return baseName
End Function
''' <summary>
''' Merges related code files into a temporary file.
''' </summary>
''' <param name="fileReaders">List of open TextReaders, each loaded with a related code file.</param>
''' <returns>Path to the merged file.</returns>
''' <remarks>The resulting file will have all Import / using statements from all files moved to the top.
''' Parsers like it that way.
''' </remarks>
Private Function MergeFiles(ByVal fileReaders As List(Of TextReader)) As String
Dim sourceCode As String = ""
Dim importList As List(Of String)
Dim importToken As String
Dim statementTerminator As String
Dim mergeFile As String = ""
Dim language As Integer = GetLanguageIndex(provider)
If language < 0 Then Return ""
Try
importList = New List(Of String)
importToken = importTokens(language)
statementTerminator = statementTerminators(language)
For Each reader As TextReader In fileReaders
sourceCode += PullCodeExtractImports(reader, importToken, statementTerminator, importList)
Next
importList.Sort()
For index As Integer = 0 To importList.Count - 1
importList(index) = importToken & " " & importList(index)
Next
sourceCode = String.Join(statementTerminator & vbCrLf, importList.ToArray) & vbCrLf & sourceCode
If sourceCode.Length > 0 Then
mergeFile = Path.GetTempFileName()
File.AppendAllText(mergeFile, sourceCode)
End If
Catch ex As Exception
Trace.WriteLine(ex.ToString)
If mergeFile.Length > 0 Then
Try
File.Delete(mergeFile)
Catch ex2 As Exception
End Try
End If
mergeFile = ""
End Try
Return mergeFile
End Function
''' <summary>
''' Replaces a VB Handles clause ("MyForm_Load(...) Handles Me.Load") with one that is compatible with
''' SharpDev's parser ("Handles MyBase.Load").
''' </summary>
''' <param name="line">Source code line to work on.</param>
''' <returns>Source line with a modified Handles clause.</returns>
''' <remarks>A Handles clause can include a list of events being handled, so we have to find the
''' start of the clause and work through the remainder of the string to find the "Me" references.</remarks>
Private Function ReplaceHandlesClause(ByVal line As String) As String
Dim handlesClause As String = "Handles"
Dim meStatement As String = "Me"
Dim myBaseStatement As String = "MyBase"
Dim handlesClauseIndex As Integer
Dim meIndex As Integer
Dim handlesSubstring As String
handlesClauseIndex = line.IndexOf(handlesClause, 0, StringComparison.InvariantCultureIgnoreCase)
If handlesClauseIndex >= 0 Then
' The simplest way to do this would be to use handlesSubstring.Replace() to replace all
' occurences of Me with MyBase, but we can't rely on the code being mixed case. Ohhh, it probably
' is, but ya never know.
meIndex = 0
handlesSubstring = line.Substring(handlesClauseIndex)
meIndex = handlesSubstring.IndexOf(meStatement, meIndex, StringComparison.InvariantCultureIgnoreCase)
Do While meIndex >= 0
handlesSubstring = handlesSubstring.Substring(0, meIndex) & myBaseStatement & handlesSubstring.Substring(meIndex + meStatement.Length)
meIndex = handlesSubstring.IndexOf(meStatement, meIndex, StringComparison.InvariantCultureIgnoreCase)
Loop
Return line.Substring(0, handlesClauseIndex) & handlesSubstring
Else
Return line
End If
End Function
''' <summary>
''' Pulls the code from a text file, excluding Import / using statements.
''' </summary>
''' <param name="reader">Reader opened with the source file to read.</param>
''' <param name="importToken">Language-specific token for the Imports / using statement.</param>
''' <param name="statementTerminator">Language-specific code statement terminator. ";" in C#, empty string in VB.</param>
''' <param name="importList">Reference to a list of import statements. Imports found in the current file will be added
''' to this list.</param>
''' <returns>The resulting code, as a string, minus any import statements.</returns>
''' <remarks></remarks>
Private Function PullCodeExtractImports(ByVal reader As TextReader, ByVal importToken As String, ByVal statementTerminator As String, ByRef importList As List(Of String)) As String
Dim line As String = Nothing
Dim resultString As String = ""
Dim strippedLine As String
line = reader.ReadLine
Do Until line Is Nothing
strippedLine = line.Trim.TrimEnd(New Char() {statementTerminator})
If strippedLine.ToLower.StartsWith(importToken.ToLower) AndAlso _
importList.IndexOf(strippedLine.Substring(importToken.Length + 1)) < 0 Then
importList.Add(strippedLine.Substring(importToken.Length + 1))
Else
' Compatibility work-around: #Develop's parser doesn't like VB's "Handles Me.xxx" event handler
' statement. The fix is to replace "Handles Me." with "Handles MyBase." Fortunately the latter
' statement is also valid in VS
resultString += ReplaceHandlesClause(line) & vbCrLf
End If
line = reader.ReadLine
Loop
Return resultString
End Function
''' <summary>
''' Returns a SupportedLanguages value given a language-specific CodeDomProvider.
''' </summary>
''' <param name="provider">The CodeDomProvider for the language to query.</param>
''' <returns>One of the SupportedLanguages values, -1 if CodeDomProvider is not supported by this
''' class</returns>
''' <remarks></remarks>
Private Function GetLanguageIndex(ByVal provider As CodeDomProvider) As Integer
Try
Return CInt(System.Enum.Parse(GetType(SupportedLanguages), provider.FileExtension, True))
Catch ex As Exception
Return -1
End Try
End Function
''' <summary>
''' Closes a bunch of text readers.
''' </summary>
''' <param name="readers">List of open TextReaders to close.</param>
''' <remarks></remarks>
Private Sub CloseReaders(ByVal readers As List(Of TextReader))
For Each reader As TextReader In readers
Try
reader.Close()
Catch ex As Exception
End Try
Next
End Sub
''' <summary>
''' Returns a CodeDomProvider appropriate for the file specified, based on its file extension.
''' </summary>
''' <param name="fileName">Code file whose provider is to be loaded.</param>
''' <returns>A CodeDomProvider appropriate for the file specified if successful,
''' Nothing otherwise.</returns>
''' <remarks></remarks>
Private Function GetProvider(ByVal fileName As String) As CodeDomProvider
Try
Dim language As String = CodeDomProvider.GetLanguageFromExtension(Path.GetExtension(fileName))
Return CodeDomProvider.CreateProvider(language)
Catch ex As Exception
Trace.WriteLine(ex.ToString)
Return Nothing
End Try
End Function
#End Region
End Class
Also (this is addressing Malacki's post), isn't there a way to dynamically add references to your program at runtime? Would this solve the problem? If it's possible to do this, when the user opens a form, the program could use a Try/Catch block around the view = ds.view line (which seems to be the make-or-break statement for the designer), and show an error message allowing the user to specify the appropriate references. In the end, I will be using a project management system, but I think it would be a really great feature to load a standalone file into a designer. - Anyone??

