Defining Shared Constructors
Visual Basic .NET implements constructors as Sub New. By prefixing a constructor with the Shared modifier, you can implement a shared constructor.
Shared constructors are used to initialize shared members to their default values implicitly and can be implemented to perform additional initialization of shared members. Shared constructors are run implicitly after your program loads and are guaranteed to run only once before any instances of a type containing a shared constructor are created and before any shared members are accessed. Shared constructors are also run before derived types are used.
You can't overload shared members. Shared constructors cannot call other constructors, cannot take parameters, and cannot be used to overload instance constructors. The declaration of a shared constructor is always in the following format:
Shared Sub New() End Sub
Implicitly, this is a public member. The shared constructor in the preceding fragment is distinct from an instance constructor:
Sub New() End Sub
The implicit nature of shared constructors makes them excellent devices for initializing shared members and simulating Singleton objects by using class methods and a shared constructor.
Shared Constructors and Singletons
A Singleton object is an object that is only created once. There are certain resources of which you generally only need one when a program is running and of which only one is accessible to the running program. A good candidate for a Singleton object is the registry.
Programs generally update the registry to persist state information about an application, such as paths for data files, login information, last Web site visited, or file opened. The registry is a good place to store this information. And, a good strategy for implementing a mechanism to store state information is to wrap the Registry class that ships with .NET in a class that exposes named state properties. In this way, the state information is accessible anywhere in your program, including an Options form, and is easy to access and modify. The alternative is to create, initialize, and release the object each time you want to modify the state information.
A further benefit of encapsulating the registry in a class is that you change how and where state information is saved without having to modify the code that uses it. Define an interface that allows users to get and set state information, and you can change the underlying implementation at any time. This is useful during development. Sometimes it's easier to save state information to an INI file while you're developing the code and switch to the registry before releasing your product. Using a wrapper class insulates the rest of your code from this strategy.
Because there is only one registry, the registry is a good example of something that can be treated as a Singleton object. Making the registry even easier to use can be accomplished via a shared constructor in a class that encapsulates a registry object and has shared setters and getters (property methods) for reading and writing registry information. The next section contains an example.
Shared Constructor Example
Shared constructors can be used to emulate Singleton objects. Singleton objects are generally implemented via a shared method that ensures that only one instance of the object is created. This is accomplished by making the constructor private. A private constructor means that consumers can't create instances of the class. However, if there is a public shared method of the same class, it can call the private constructor and subsequently create instances. Using a Static counter or a shared field, the public shared method can ensure that only one instance of the Singleton exists.
Singletons are used when it's beneficial or important that only one instance of an object exists. The technique is also used for convenience. What better way to simplify code than to inhibit the unnecessary creation of objects?
Using a shared constructor and a Private constructor to prevent consumers from creating unnecessary objects, Listing 11.6 demonstrates two kinds of persistency in the limited context of the text editor; one half of the code demonstrates using the Registry and the other half demonstrates using a HybridDictionary object. (The text editor itself is intentionally only partially complete. The example solution is on this book's Web site (see www.samspublishing.com) in DualModeStateExample.sln).
The Registry class will be used when the DualModeStateExample.exe is distributed, and a HybridDictionary will be used during development. (Refer to the code listing for the implementation of the persisted HybridDictionary.)
The Options class uses the Registry to save state information if we aren't configured for DEBUG mode, and uses a persisted HybridDictionary class if we are still testing.
Listing 11.6 Both implementations of persisted application options
1: ' Options.vb - Implements dual mode state persistency using 2: ' a shared constructor and properties 3: ' Copyright 2001. All Rights Reserved. 4: ' Written by Paul Kimmel. email@example.com 5: 6: Imports Microsoft.Win32, System.Collections.Specialized, System.IO 7: 8: #If DEBUG = False Then 9: 10: Public Class Options 11: 12: Private Sub New() 13: End Sub 14: 15: Private Enum Keys 16: LastFileName 17: End Enum 18: 19: Public Shared Property LastFileName() As String 20: Get 21: Return ReadString(Keys.LastFileName.ToString) 22: End Get 23: Set(ByVal Value As String) 24: WriteString(Keys.LastFileName.ToString, Value) 25: End Set 26: End Property 27: 28: Private Shared Function Subkey() As String 29: Return "Software\ " & "VB.NET UNLEASHED" 30: End Function 31: 32: Private Shared Function OpenKey() As RegistryKey 33: 34: Return Registry.LocalMachine.OpenSubKey(Subkey(), True) 35: 36: End Function 37: 38: Private Shared Function ReadString(ByVal Key As String, _ 39: Optional ByVal Value As String = "") As String 40: Try 41: Return OpenKey().GetValue(Key, Value) 42: Catch 43: Return Value 44: End Try 45: End Function 46: 47: Private Shared Sub WriteString(ByVal Key As String, _ 48: ByVal Value As String) 49: Try 50: OpenKey().SetValue(Key, Value) 51: Catch 52: Registry.LocalMachine.CreateSubKey(Subkey()) 53: OpenKey().SetValue(Key, Value) 54: End Try 55: End Sub 56: 57: 58: End Class 59: 60: #Else 61: 62: Public Class Options 63: 64: Private Sub New() 65: 66: End Sub 67: 68: Private Enum Keys 69: LastFileName 70: End Enum 71: 72: Private Shared Loading As Boolean 73: Private Shared FDictionary As HybridDictionary 74: 75: Shared Sub New() 76: FDictionary = New HybridDictionary() 77: LoadDictionary() 78: End Sub 79: 80: Public Shared Property LastFileName() As String 81: Get 82: Return ReadString(Keys.LastFileName.ToString) 83: End Get 84: Set(ByVal Value As String) 85: WriteString(Keys.LastFileName.ToString, Value) 86: End Set 87: End Property 88: 89: 90: Private Shared Function ReadString(ByVal Key As String, _ 91: Optional ByVal Value As String = "") 92: 93: If (FDictionary.Item(Key) Is Nothing) Then Return Value 94: Return FDictionary.Item(Key) 95: 96: End Function 97: 98: Private Shared Sub WriteString(ByVal Key As String, _ 99: ByVal Value As String) 100: FDictionary.Remove(Key) 101: FDictionary.Add(Key, Value) 102: SaveDictionary() 103: End Sub 104: 105: Private Shared Function GetOptionsFile() As String 106: Return Application.StartupPath & "\ options.ini" 107: End Function 108: 109: Private Shared Sub SaveDictionary() 110: If (Loading) Then Exit Sub 111: Dim Writer As StreamWriter = _ 112: File.CreateText(GetOptionsFile()) 113: Try 114: Writer.WriteLine(Keys.LastFileName.ToString & "=" & _ 115: FDictionary.Item(Keys.LastFileName.ToString)) 116: Finally 117: Writer.Close() 118: End Try 119: End Sub 120: 121: Private Shared Sub LoadDictionary() 122: If (Not File.Exists(GetOptionsFile())) Then Exit Sub 123: 124: Dim Reader As StreamReader = _ 125: File.OpenText(GetOptionsFile()) 126: 127: Try 128: Loading = True 129: Dim Line As String = Reader.ReadLine() 130: LastFileName = Mid(Line, InStr(Line, "=") + 1) 131: 132: Finally 133: Reader.Close() 134: Loading = False 135: End Try 136: 137: End Sub 138: 139: End Class 140: 141: #End If
Listing 11.6 implements two versions of the Options class. Both versions have the exact same interface, so consumers aren't affected adversely when the Configuration information changes and we switch from persisting application settings to an OPTIONS.INI file versus the registry. Using an OPTIONS.INI file is a safer alternative and is more convenient when doing initial development.
Each version of the Options class uses a Private constructor, ensuring that consumers don't create instances. The classes are defined in such a manner as not to require instances, that is, all members are shared. (Unless mentioned, further discussion of the Options class applies to both examples.) Options uses an enumeration to make reading and writing keys easy to follow. Although the class only persists the LastFileName value, implementing additional state information simply requires adding enumerated values to the Keys enum and implementing the shared properties. LastFileName is a suitable model for any shared state information. Each shared property delegates reading and writing to the ReadString and WriteString methods. Because everything except the property is private, this class will be easy for consumers to use even though it required a little extra effort to write.
The biggest difference between the two versions of Options is that the registry version has to create and open registry keys. It's assumed that the registry information exists unless an exception occurs (starting at line 51); in the event of an exception, the key is created and the write is attempted again.
The HybridDictionary (defined in the System.Collections.Specialized namespace) example stores the keys in name /value pairs and uses the File, StreamReader, and StreamWriter classes defined in System.IO to manage file persistence. Only the HybridDictionary version requires a shared constructor (lines 75 to 78), which is responsible for creating an instance of the dictionary. Keep in mind that you never have to call the Shared constructor, and the compiler will generate code guaranteeing that the Shared constructor is called before any other code uses the class.
Listing 11.7 demonstrates a consumer that uses the Options class. The consumer is indifferent to which Options class is used. Switching back and forth between implementations of the Options class based on configuration settings has no impact or revisionary requirements on the consumer.
Listing 11.7 Form1 represents a consumer of the Options class
1: Public Class Form1 2: Inherits System.Windows.Forms.Form 3: 4: [ Windows Foprm Designer generated code ] 5: 6: Private Sub MenuItem5_Click(ByVal sender As System.Object, _ 7: ByVal e As System.EventArgs) Handles MenuItem5.Click 8: Application.Exit() 9: End Sub 10: 11: Private Sub MenuItem2_Click(ByVal sender As System.Object, _ 12: ByVal e As System.EventArgs) Handles MenuItem2.Click 13: ShowOpenFile() 14: End Sub 15: 16: Private Sub AddRecentFile(ByVal FileName As String) 17: Options.LastFileName = FileName 18: AddSubMenu(FileName) 19: End Sub 20: 21: Private Sub OnOpen(ByVal sender As Object, ByVal e As System.EventArgs) 22: DoOpenFile(CType(sender, MenuItem).Text) 23: End Sub 24: 25: Private Sub AddSubMenu(ByVal RecentFile As String) 26: MenuItemRecent.MenuItems.Add(RecentFile, AddressOf OnOpen) 27: End Sub 28: 29: Private Sub DoOpenFile(ByVal FileName As String) 30: RichTextBox1.LoadFile(FileName, _ 31: RichTextBoxStreamType.PlainText) 32: End Sub 33: 34: Private Sub OpenFile(ByVal FileName As String) 35: DoOpenFile(FileName) 36: AddRecentFile(OpenFileDialog1.FileName) 37: End Sub 38: 39: Private Sub ShowOpenFile() 40: OpenFileDialog1.Filter = "Plain Text (*.txt)*.txt" 41: OpenFileDialog1.InitialDirectory = "C:\ " 42: 43: If (OpenFileDialog1.ShowDialog() <> DialogResult.OK) Then _ 44: Exit Sub 45: 46: OpenFile(OpenFileDialog1.FileName) 47: End Sub 48: 49: Protected Overrides Sub OnLoad(ByVal e As System.EventArgs) 50: If (Options.LastFileName = "") Then Exit Sub 51: AddSubMenu(Options.LastFileName) 52: 53: End Sub 54: End Class
Listing 11.7 defines a form (with the generated code hidden) that implements a simple text editor. The application allows the user to open a file. The file is loaded into a RichTextBox control. The Recent menu is dynamically updated to contain submenus referring to previously opened files, and Options.LastFileName is updated. On subsequent runs, LastFileName is added to the Recent menu when the program is started (refer to the OnLoad method on lines 49 to 53).
The very short functions demonstrate optimum reuse. AddSubMenu is reused in AddRecentFile and OnLoad, and DoOpenFile (which loads the file into the RichTextBox) is reused in the OpenFile method and OnOpen event handler.
Listing 11.7 demonstrates several techniques that you might find practical. Line 26 demonstrates how to add submenus dynamically and associate event handlers with those menus . Line 22 demonstrates an example of using a dynamic menu caption as an argument to a method and dynamic type conversion. Lines 30 and 31 demonstrate how to load the contents of a file into a RichTextBox control; you could create the text editor by using additional properties defined in the RichTextBox control, like SaveFile. And, there are several examples of the refactoring "Extract Method," reducing the code listing considerably. An example of refactoring was extracting DoOpenFile from OpenFile on lines 2937.