unloading a DLL until it's needed - vb.net

I'm having a hard time wrapping my head around some of the answers I've been reading here about unloading a plugin DLL using AppDomains. Here's my architecture:
In my solution, I have a SharedObjects project containing a ModuleBase class that all plugins (separate projects within the solution) inherit. In the SharedObjects project I also have an interface that all plugins implement (so if I have six plugins, they all implement the same interface and therefore the main program using these plugins doesn't need to know or even care what the name of the plugin's class was when it was compiled; they all implement the same interface and therefore expose the same information). Each plugin project has a project reference to the SharedObjects project. (As a side note, may be important, may not be - that SharedObjects project has a project reference to another solution, CompanyObjects containing a number of commonly-used classes, types, objects, etc.) When it's all said and done, when any given plugin compiles, the output directory contains the following DLLs:
The compiled DLL of the plugin itself
The DLL from the SharedObjects project
The DLL from the CompanyObjects project
Four prerequisite 3rd-party DLLs referenced in the CompanyObjects project
My main program creates a reference to the class where I'm doing all my plugin-related work (that class, PluginHelpers, is stored in the SharedObjects project). The program supplies an OpenFileDialog so that the user can choose a DLL file. Now, as it's running right now, I can move just the plugin DLLs to a separate folder and load them using the Assembly.LoadFrom(PathToDLL) statement. They load without error; I check to make sure they're implementing the interface in the SharedObjects project, gather some basic information, and initialize some background work in the plugin DLL itself so that the interface has something to expose. Problem is, I can't upgrade those DLLs without quitting the main program first because as soon as I use LoadFrom those DLLs are locked.
From this MSDN site I found a solution to the problem of locked DLLs. But I'm getting the same "File or dependency not found" error as the OP using the code that worked for the OP. I even get the error when I open the DLL from the release folder which includes the rest of those DLLs.
The FusionLog is even more confusing: there's no mention of the path I was trying to open; it's trying to look in the directory where I'm debugging the main program from, which is a completely different project on a completely different path than the plugins, and the file it's looking for is the name of the DLL but in the folder where the program is running. At this point I have no idea why it's disregarding the path I gave it and looking for the DLL in a completely different folder.
For reference, here's my Loader class and the code I'm using to (try to) load the DLLs:
Private Class Loader
Inherits MarshalByRefObject
Private _assembly As [Assembly]
Public ReadOnly Property TheAssembly As [Assembly]
Get
Return _assembly
End Get
End Property
Public Overrides Function InitializeLifetimeService() As Object
Return Nothing
End Function
Public Sub LoadAssembly(ByVal path As String)
_assembly = Assembly.Load(AssemblyName.GetAssemblyName(path))
End Sub
Public Function GetAssembly(ByVal path As String) As Assembly
Return Assembly.Load(AssemblyName.GetAssemblyName(path)) 'this doesn't throw an error
End Function
End Class
Public Sub Add2(ByVal PathToDll As String)
Dim ad As AppDomain = AppDomain.CreateDomain("TempPluginDomain")
Dim l As Loader = ad.CreateInstanceAndUnwrap(
GetType(Loader).Assembly.FullName,
GetType(Loader).FullName
)
Dim theDll As Assembly = l.GetAssembly(PathToDll) 'error happens here
'there's another way to do it that makes the exact point of the error clear:
'Dim theDll As Assembly = Nothing
'l.LoadAssembly(PathToDll) 'No problems here. The _assembly variable is successfully set
'theDll = l.TheAssembly 'Here's where the error occurs, as soon as you try to read that _assembly variable.
AppDomain.Unload(ad)
End Sub
Can anyone point me in the right direction so I can load and unload DLLs only as-needed and without any dependency errors?

I think I finally got it. It ended up being a few things - I needed the shared DLLs all in one place, and as Hans mentioned above, I needed my appdomains squared away. My solution architecture looks like this: a folder with all my plugin projects; a "Shared Objects" assembly with one class file for the base plugin architecture, and a second class containing my "plugin wrapper" class and supporting classes; and a console app that ties everything together. Each plugin project has a project reference to the shared objects project, as does the console app. Nothing references the plugins directly.
So in my Shared Objects project, I have the code for my PluginBase class and my IPlugin interface:
Public Interface IPlugin
ReadOnly Property Result As Integer
Sub Calculate(ByVal param1 As Integer, ByVal param2 As Integer)
End Interface
Public MustInherit Class PluginBase
Inherits MarshalByRefObject
'None of this is necessary for the example to work, but I know I'll need to use an inherited base class later on so I threw it into the example now.
Protected ReadOnly Property PluginName As String
Get
Return CustomAttributes("AssemblyPluginNameAttribute")
End Get
End Property
Protected ReadOnly Property PluginGUID As String
Get
Return CustomAttributes("AssemblyPluginGUIDAttribute")
End Get
End Property
Protected IsInitialized As Boolean = False
Protected CustomAttributes As Dictionary(Of String, String)
Protected Sub Initialize()
CustomAttributes = New Dictionary(Of String, String)
Dim attribs = Me.GetType.Assembly.GetCustomAttributesData
For Each attrib In attribs
Dim name As String = attrib.Constructor.DeclaringType.Name
Dim value As String
If attrib.ConstructorArguments.Count = 0 Then
value = ""
Else
value = attrib.ConstructorArguments(0).ToString.Replace("""", "")
End If
CustomAttributes.Add(name, value)
Next
IsInitialized = True
End Sub
End Class
<AttributeUsage(AttributeTargets.Assembly)>
Public Class AssemblyPluginNameAttribute
Inherits System.Attribute
Private _name As String
Public Sub New(ByVal value As String)
_name = value
End Sub
Public Overridable ReadOnly Property PluginName As String
Get
Return _name
End Get
End Property
End Class
<AttributeUsage(AttributeTargets.Assembly)>
Public Class AssemblyPluginGUIDAttribute
Inherits System.Attribute
Private _g As String
Public Sub New(ByVal value As String)
_g = value
End Sub
Public Overridable ReadOnly Property PluginGUID As String
Get
Return _g
End Get
End Property
End Class
And I have my PluginWrapper class with its supporting classes:
Imports System.IO
Imports System.Reflection
''' <summary>
''' The wrapper for plugin-related activities.
''' </summary>
''' <remarks>Each wrapper contains: the plugin; code to load and unload it from memory; and the publicly-exposed name and GUID of the plugin.</remarks>
Public Class PluginWrapper
Private _pluginAppDomain As AppDomain = Nothing
Private _isActive As Boolean = False
Private _plugin As IPlugin = Nothing
Private _pluginInfo As PluginInfo = Nothing
Private _pluginPath As String = ""
Public ReadOnly Property IsActive As Boolean
Get
Return _isActive
End Get
End Property
Public ReadOnly Property PluginInterface As IPlugin
Get
Return _plugin
End Get
End Property
Public ReadOnly Property PluginGUID As String
Get
Return _pluginInfo.PluginGUID
End Get
End Property
Public ReadOnly Property PluginName As String
Get
Return _pluginInfo.PluginName
End Get
End Property
Public Sub New(ByVal PathToPlugin As String)
_pluginPath = PathToPlugin
End Sub
Public Sub Load()
Dim l As New PluginLoader(_pluginPath)
_pluginInfo = l.LoadPlugin()
Dim setup As AppDomainSetup = New AppDomainSetup With {.ApplicationBase = System.IO.Directory.GetParent(_pluginPath).FullName}
_pluginAppDomain = AppDomain.CreateDomain(_pluginInfo.PluginName, Nothing, setup)
_plugin = _pluginAppDomain.CreateInstanceAndUnwrap(_pluginInfo.AssemblyName, _pluginInfo.TypeName)
_isActive = True
End Sub
Public Sub Unload()
If _isActive Then
AppDomain.Unload(_pluginAppDomain)
_plugin = Nothing
_pluginAppDomain = Nothing
_isActive = False
End If
End Sub
End Class
<Serializable()>
Public NotInheritable Class PluginInfo
Private _assemblyname As String
Public ReadOnly Property AssemblyName
Get
Return _assemblyname
End Get
End Property
Private _typename As String
Public ReadOnly Property TypeName
Get
Return _typename
End Get
End Property
Private _pluginname As String
Public ReadOnly Property PluginName As String
Get
Return _pluginname
End Get
End Property
Private _pluginguid As String
Public ReadOnly Property PluginGUID As String
Get
Return _pluginguid
End Get
End Property
Public Sub New(ByVal AssemblyName As String, ByVal TypeName As String, ByVal PluginName As String, ByVal PluginGUID As String)
_assemblyname = AssemblyName
_typename = TypeName
_pluginname = PluginName
_pluginguid = PluginGUID
End Sub
End Class
Public NotInheritable Class PluginLoader
Inherits MarshalByRefObject
Private _pluginBaseType As Type = Nothing
Private _pathToPlugin As String = ""
Public Sub New()
End Sub
Public Sub New(ByVal PathToPlugin As String)
_pathToPlugin = PathToPlugin
Dim ioAssemblyFile As String = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(_pathToPlugin), GetType(PluginBase).Assembly.GetName.Name) & ".dll")
Dim ioAssembly As Assembly = Assembly.LoadFrom(ioAssemblyFile)
_pluginBaseType = ioAssembly.GetType(GetType(PluginBase).FullName)
End Sub
Public Function LoadPlugin() As PluginInfo
Dim domain As AppDomain = Nothing
Try
domain = AppDomain.CreateDomain("Discovery")
Dim loader As PluginLoader = domain.CreateInstanceAndUnwrap(GetType(PluginLoader).Assembly.FullName, GetType(PluginLoader).FullName)
Return loader.Load(_pathToPlugin)
Finally
If Not IsNothing(domain) Then
AppDomain.Unload(domain)
End If
End Try
End Function
Private Function Load(ByVal PathToPlugin As String) As PluginInfo
Dim r As PluginInfo = Nothing
Try
Dim objAssembly As Assembly = Assembly.LoadFrom(PathToPlugin)
For Each objType As Type In objAssembly.GetTypes
If Not ((objType.Attributes And TypeAttributes.Abstract) = TypeAttributes.Abstract) Then
If Not objType.GetInterface("SharedObjects.IPlugin") Is Nothing Then
Dim attribs = objAssembly.GetCustomAttributes(False)
Dim pluginGuid As String = ""
Dim pluginName As String = ""
For Each attrib In attribs
Dim name As String = attrib.GetType.ToString
If name = "SharedObjects.AssemblyPluginGUIDAttribute" Then
pluginGuid = CType(attrib, AssemblyPluginGUIDAttribute).PluginGUID.ToString
ElseIf name = "SharedObjects.AssemblyPluginNameAttribute" Then
pluginName = CType(attrib, AssemblyPluginNameAttribute).PluginName.ToString
End If
If (Not pluginGuid = "") And (Not pluginName = "") Then
Exit For
End If
Next
r = New PluginInfo(objAssembly.FullName, objType.FullName, pluginName, pluginGuid)
End If
End If
Next
Catch f As FileNotFoundException
Throw f
Catch ex As Exception
'ignore non-valid dlls
End Try
Return r
End Function
End Class
Finally, each plugin project looks a little like this:
Imports SharedObjects
<Assembly: AssemblyPluginName("Addition Plugin")>
<Assembly: AssemblyPluginGUID("{4EC46939-BD74-4665-A46A-C99133D8B2D2}")>
Public Class Plugin_Addition
Inherits SharedObjects.PluginBase
Implements SharedObjects.IPlugin
Private _result As Integer
#Region "Implemented"
Public Sub Calculate(ByVal param1 As Integer, ByVal param2 As Integer) Implements SharedObjects.IPlugin.Calculate
If Not IsInitialized Then
MyBase.Initialize()
End If
_result = param1 + param2
End Sub
Public ReadOnly Property Result As Integer Implements SharedObjects.IPlugin.Result
Get
Return _result
End Get
End Property
#End Region
End Class
To set it all up, the main program creates a new instance of the PluginWrapper class, supplies a path to a DLL, and loads it:
Dim additionPlugin As New PluginWrapper("C:\path\to\Plugins\Plugin_Addition.dll")
additionPlugin.Load()
Once you're done doing whatever you need to do with the program...
additionPlugin.PluginInterface.Calculate(3, 2)
...and retrieving the results...
Console.WriteLine("3 + 2 = {0}", additionPlugin.PluginInterface.Result)
...just unload the plugin:
additionPlugin.Unload()
If you need to reload it while the wrapper is still in memory, just call the Load() method again - all the information it needs to create a new AppDomain and reload the assembly is in there. And, in answer to my initial question, once the Unload() method has been called, the assembly is freed and can be replaced/upgraded as necessary, which was the whole point of doing this in the first place.
Part of where I was getting tripped up earlier was that I wasn't including the SharedObjects.dll file in the same folder as the plugins. What I found is that any referenced assembly needs to be present. So in my post-build events for both my plugins and the Shared Objects project, I have this: xcopy /y $(ProjectDir)$(OutDir)$(TargetFileName) c:\path\to\Plugins. Every time I build the solution, all the DLLs are placed in the folder where they need to be.
Sorry if this is a little long, but this is a little complicated. Maybe there's a shorter way to do it...but at the moment, this gets me everything I need.

Related

Interface with subProject

Hy, Can I create an interface with a sub-project?
Public Interface IDraft
Property LienVue() As String
End Interface
Public Class C2018
Implements IDraft
Private m_LienVue As String
Sub New(Optional ByVal LienVue As String = "")
m_LienVue = LienVue
End Sub
Property LienVue() As String Implements IDraft.LienVue
Get
Return getlienvue()
End Get
Set(value As String)
m_LienVue = value
End Set
End Property
Private Function getlienvue() As String
Return "xxxxxxxx"
End Function
End Class
The c2018 class is in a sub-project of my main project. The goal is to add a reference to a different API from the main project

How can I process events of a used class?

I am still in the process of re-thinking from VB6 to .Net, so please forgive if this is trivial.
In a class I have properties which can change, and when they do they should raise a Changed event.
Public Class CPT
Public Event Changed()
Private gsText As String
Public Property Text() As String
Get
Return gsText
End Get
Set(ByVal sValue As String)
If sValue <> gsText Then
gsText = sValue
RaiseEvent Changed()
End If
End Set
End Property
End Class
Another class features an Add method, in which it adds new items of above class into a collection.
Public Class UFB
Private goTexts As New Dictionary(Of String, CPT)
Public Sub Add(sKey As String, sText As String)
Dim oPT As New CPT
oPT.Text = sText
goTexts.Add(sKey, oPT)
End Sub
End Class
Obviously, UFB objects can not receive Changed events, because oPT is not declared on module level and thus can not feature WithEvents.
What is the .Net way to enable UFB to listen to CPT's Changed events (which appear in many other CPT properties).

Can I shorten the access of my Lazy class without disadvantage

So this is how I would design my Lazy class (From this SO):
Public NotInheritable Class MySingleton
Private Shared ReadOnly _instance As New Lazy(Of MySingleton)(Function() New _
MySingleton(), System.Threading.LazyThreadSafetyMode.ExecutionAndPublication)
Private Sub New()
End Sub
Public Shared ReadOnly Property Instance() As MySingleton
Get
Return _instance.Value
End Get
End Property
Private _MyString As String
Public Property MyString As String
Get
Return _MyString
End Get
Set(value As String)
_MyString = value
End Set
End Property
End Class
To access the _MyString value, I do the following:
Dim MyString = MySingleton.Instance.MyString
In fact, I always have to type the ".Instance."
Does it have any disadvantage if I design the Property the following way:
Public Property MyString As String
Get
Return instance._MyString
End Get
Set(value As String)
instance._MyString = value
End Set
End Property
So I can access it without always writing the ".Instance."
Dim MyString = MySingleton.MyString
Yes you can (of course with Public Shared Property), but you are losing some of the benefits from singleton over static classes.
Lets say you have another class MyWorker
Public Class MyWorker
Public Sub Work(instance as MySingleton)
Dim value as String = instance.MyString
' Do something ...
End Sub
End Class
I would not do this. This may not look like a big issue, but on the long run you have a tight coupling in your code base and a hard time mocking your class for unit testing, one of the reasons for using singeltons over static classes in the first place.
I often use this approach, when accessing Singelton values mutiple times:
Dim instance as MySingelton = MySingelton.Value
If instance.MyString = "something" Then
instance.MyString = "something else"
End If
much cleaner approach.

Showing progress of ZipFiles Class

I was wondering, how can I get the percentage of this being done, so I can display it on a progress bar?
ZipFile.CreateFromDirectory("C:\temp\folder", "C:\temp\folder.zip")
and also
ZipFile.ExtractToDirectory("C:\temp\folder.zip", "C:\temp\folder")
This doesnt have any events or callbacks that you can use to report progress. Simply means you cant with the .Net version. If you used the 7-Zip library you can do this easily.
I came across this question while checking for related questions for the identical question, asked for C# code. It is true that the .NET static ZipFile class does not offer progress reporting. However, it is not hard to do using the ZipArchive implementation, available since earlier versions of .NET.
The key is to use a Stream wrapper that will report bytes read and written, and insert that in the data pipeline while creating or extracting the archive.
I wrote a version in C# for an answer to the other question, and since I didn't find any VB.NET examples, figured it would be helpful to include a VB.NET version on this question.
(Arguably, I could include both examples in a single answer and propose closing one of the questions as a duplicate of the other. But since it's doubtful the close vote would result in an actual closure, the connection between the two questions would not be as obvious as it should be. I think for best visibility to future users trying to find the solution appropriate for their needs, leaving this as two different questions is better.)
The foundation of the solution is the Stream wrapper class:
StreamWithProgress.vb
Imports System.IO
Public Class StreamWithProgress
Inherits Stream
' NOTE For illustration purposes. For production code, one would want To
' override *all* of the virtual methods, delegating to the base _stream object,
' to ensure performance optimizations in the base _stream object aren't
' bypassed.
Private ReadOnly _stream As Stream
Private ReadOnly _readProgress As IProgress(Of Integer)
Private ReadOnly _writeProgress As IProgress(Of Integer)
Public Sub New(Stream As Stream, readProgress As IProgress(Of Integer), writeProgress As IProgress(Of Integer))
_stream = Stream
_readProgress = readProgress
_writeProgress = writeProgress
End Sub
Public Overrides ReadOnly Property CanRead As Boolean
Get
Return _stream.CanRead
End Get
End Property
Public Overrides ReadOnly Property CanSeek As Boolean
Get
Return _stream.CanSeek
End Get
End Property
Public Overrides ReadOnly Property CanWrite As Boolean
Get
Return _stream.CanWrite
End Get
End Property
Public Overrides ReadOnly Property Length As Long
Get
Return _stream.Length
End Get
End Property
Public Overrides Property Position As Long
Get
Return _stream.Position
End Get
Set(value As Long)
_stream.Position = value
End Set
End Property
Public Overrides Sub Flush()
_stream.Flush()
End Sub
Public Overrides Sub SetLength(value As Long)
_stream.SetLength(value)
End Sub
Public Overrides Function Seek(offset As Long, origin As SeekOrigin) As Long
Return _stream.Seek(offset, origin)
End Function
Public Overrides Sub Write(buffer() As Byte, offset As Integer, count As Integer)
_stream.Write(buffer, offset, count)
_writeProgress?.Report(count)
End Sub
Public Overrides Function Read(buffer() As Byte, offset As Integer, count As Integer) As Integer
Dim bytesRead As Integer = _stream.Read(buffer, offset, count)
_readProgress?.Report(bytesRead)
Return bytesRead
End Function
End Class
The wrapper class can be used to implement progress-aware versions of the ZipFile static methods:
ZipFileWithProgress.vb
Imports System.IO
Imports System.IO.Compression
NotInheritable Class ZipFileWithProgress
Private Sub New()
End Sub
Public Shared Sub CreateFromDirectory(
sourceDirectoryName As String,
destinationArchiveFileName As String,
progress As IProgress(Of Double))
sourceDirectoryName = Path.GetFullPath(sourceDirectoryName)
Dim sourceFiles As FileInfo() = New DirectoryInfo(sourceDirectoryName).GetFiles("*", SearchOption.AllDirectories)
Dim totalBytes As Double = sourceFiles.Sum(Function(f) f.Length)
Dim currentBytes As Long = 0
Using archive As ZipArchive = ZipFile.Open(destinationArchiveFileName, ZipArchiveMode.Create)
For Each fileInfo As FileInfo In sourceFiles
' NOTE: naive method To Get Sub-path from file name, relative to
' input directory. Production code should be more robust than this.
' Either use Path class Or similar to parse directory separators And
' reconstruct output file name, Or change this entire method to be
' recursive so that it can follow the sub-directories And include them
' in the entry name as they are processed.
Dim entryName As String = fileInfo.FullName.Substring(sourceDirectoryName.Length + 1)
Dim entry As ZipArchiveEntry = archive.CreateEntry(entryName)
entry.LastWriteTime = fileInfo.LastWriteTime
Using inputStream As Stream = File.OpenRead(fileInfo.FullName)
Using outputStream As Stream = entry.Open()
Dim progressStream As Stream = New StreamWithProgress(inputStream,
New BasicProgress(Of Integer)(
Sub(i)
currentBytes += i
progress.Report(currentBytes / totalBytes)
End Sub), Nothing)
progressStream.CopyTo(outputStream)
End Using
End Using
Next
End Using
End Sub
Public Shared Sub ExtractToDirectory(
sourceArchiveFileName As String,
destinationDirectoryName As String,
progress As IProgress(Of Double))
Using archive As ZipArchive = ZipFile.OpenRead(sourceArchiveFileName)
Dim totalBytes As Double = archive.Entries.Sum(Function(e) e.Length)
Dim currentBytes As Long = 0
For Each entry As ZipArchiveEntry In archive.Entries
Dim fileName As String = Path.Combine(destinationDirectoryName, entry.FullName)
Directory.CreateDirectory(Path.GetDirectoryName(fileName))
Using inputStream As Stream = entry.Open()
Using outputStream As Stream = File.OpenWrite(fileName)
Dim progressStream As Stream = New StreamWithProgress(outputStream, Nothing,
New BasicProgress(Of Integer)(
Sub(i)
currentBytes += i
progress.Report(currentBytes / totalBytes)
End Sub))
inputStream.CopyTo(progressStream)
End Using
End Using
File.SetLastWriteTime(fileName, entry.LastWriteTime.LocalDateTime)
Next
End Using
End Sub
End Class
The .NET built-in implementation of IProgress(Of T) is intended for use in contexts where there is a UI thread where progress reporting events should be raised. As such, when used in a console program, like which I used to test this code, it will default to using the thread pool to raise the events, allowing for the possibility of out-of-order reports. To address this, the above uses a simpler implementation of IProgress(Of T), one that simply invokes the handler directly and synchronously.
BasicProgress.vb
Class BasicProgress(Of T)
Implements IProgress(Of T)
Private ReadOnly _handler As Action(Of T)
Public Sub New(handler As Action(Of T))
_handler = handler
End Sub
Private Sub Report(value As T) Implements IProgress(Of T).Report
_handler(value)
End Sub
End Class
And naturally, it's useful to have an example with which to test and demonstrate the code.
Module1.vb
Imports System.IO
Module Module1
Sub Main(args As String())
Dim sourceDirectory As String = args(0),
archive As String = args(1),
archiveDirectory As String = Path.GetDirectoryName(Path.GetFullPath(archive)),
unpackDirectoryName As String = Guid.NewGuid().ToString()
File.Delete(archive)
ZipFileWithProgress.CreateFromDirectory(sourceDirectory, archive,
New BasicProgress(Of Double)(
Sub(p)
Console.WriteLine($"{p:P2} archiving complete")
End Sub))
ZipFileWithProgress.ExtractToDirectory(archive, unpackDirectoryName,
New BasicProgress(Of Double)(
Sub(p)
Console.WriteLine($"{p:P0} extracting complete")
End Sub))
End Sub
End Module
Additional notes regarding this implementation can be found in my answer to the related question.

Can't create a Collection of Inherited Classes

Maybe I just don't know what to search for, but I'm going a little bonkers here trying to figure out how to create a collection of inherited classes. The base class, I will never use.
Basically, I have 3 components:
A base class call ImageFormat
Child classes of ImageForm
Code in Sub Main() to loop create a
collection and loop through it.
So it does it, #3. The problem is that it always gets the last item added to the collection and uses it's values only.
Here's my Base Class:
Public MustInherit Class ImageFormat
Protected Shared _extentions As String()
Protected Shared _targettype As String
Protected Shared _name As String
Public ReadOnly Property Extentions As String()
Get
Return _extentions
End Get
End Property
Public ReadOnly Property TargetType As String
Get
Return _targettype
End Get
End Property
Public ReadOnly Property Name As String
Get
Return _name
End Get
End Property
End Class
And here are the child classes:
Class WindowsEnhancedMetafile
Inherits ImageFormat
Sub New()
_extentions = {"EMF"}
_targettype = "jpg"
_name = "Windows Enhanced Metafile"
End Sub
End Class
Class WindowsBitmap
Inherits ImageFormat
Sub New()
_extentions = {"BMP", "DIB", "RLE", "BMZ"}
_targettype = "jpg"
_name = "Windows Bitmap"
End Sub
End Class
Class WindowsMetafile
Inherits ImageFormat
Sub New()
_extentions = {"WMF"}
_targettype = "jpg"
_name = "Windows Metafile"
End Sub
End Class
(don't know if these child classes need to different, like just instantied from ImageFormat type or Singleton patterns - would appreciate anything thoughts you have on this)
Then, my routine is:
Sub Main()
Dim imgFormats As New List(Of ImageFormat)
imgFormats.Add(New WindowsBitmap)
imgFormats.Add(New WindowsMetafile)
imgFormats.Add(New WindowsEnhancedMetafile)
Dim name As String = String.Empty
For Each imgFormat In imgFormats
name = imgFormat.Name
Console.WriteLine(name)
Next
Console.ReadLine()
End Sub
This returns Windows Enhanced Metafile three times at the Console. What am I doing wrong here?
The three properties:
Protected Shared _extentions As String()
Protected Shared _targettype As String
Protected Shared _name As String
Are marked as Shared - they belong to the class not the object.
Each time you assign a new value to _name it overrides the old value, thus why you get the same name printed each time.
It should be:
Protected _extentions As String()
Protected _targettype As String
Protected _name As String
Well, your _name et al are Shared, which means they are class-level variables. When you're adding WindowsEnhancedMetafile, it happens to overwrite these fields with WMF-specific information. If you changed your code to:
imgFormats.Add(New WindowsMetafile)
imgFormats.Add(New WindowsEnhancedMetafile)
imgFormats.Add(New WindowsBitmap)
you would've had "Windows Bitmap" printed three times.
All you have to do is to change your field declarations to
Protected _extentions As String()
Protected _targettype As String
Protected _name As String