Simplify Common Tasks by Customizing the My Namespace
This article is based on a prerelease version of Visual Basic 2005. All information contained herein is subject to change.
This article discusses:
|This article uses the following technologies:
Visual Basic 2005
Code download available at:My.exe(177 KB)
Configuring Existing My Namespace Members
Extending the My Namespace
Extending the My.Application Event Model
My Namespace Customizations
Designing Class Libraries for My
Packaging and Deploying Extensions
The My namespace in Visual Basic® 2005 is designed to help you easily write compelling applications. Its task-based APIs, intuitive hierarchy, and application framework allow you to harness the power of the Microsoft® .NET Framework often with only a single line of code to complete a difficult task. Underlying the My namespace's APIs is a fully extensible architecture you can leverage to customize the behavior of My and to add new services to its hierarchy to adapt to specific application needs.
The My namespace exposes properties and methods that "just work." By solving common programming problems, its solution-driven hierarchy and preconfigured services alleviate the need to navigate the entire .NET Framework. Unlike many of the types and members of the .NET Framework, the My namespace is optimized to provide simple solutions to common problems. However, you needn't abandon the simplicity that the My namespace provides because your requirements are different. Using the My namespace's customization model, you can modify the behavior of individual My namespace members to meet specific application requirements.
Configuring Existing My Namespace Members
One of the biggest problems developers face when using general-purpose infrastructures is configuration. For this reason, the My namespace's members contain a default configuration that is optimized to accommodate the most common scenario.
For example, the System.Security namespace contains a powerful, highly extensible infrastructure for performing role-based security. However, performing a common task, such as configuring the required classes to run as the current Windows® user, can be a bit complicated, as you can see:
System.Threading.Thread.CurrentPrincipal = _ New System.Security.Principal.WindowsPrincipal( _ System.Security.Principal.WindowsIdentity.GetCurrent)
My.User encapsulates the Windows user by default so this initialization code is unnecessary. Like many members in the My namespace, however, the default configuration of My.User is not hardwired and can be overridden as needed:
My.User.CurrentPrincipal = _ New WindowsPrincipal(WindowsIdentity.GetAnonymous())
Setting the CurrentPrincipal property on My.User effectively changes the user that the application will run as. My.User, in turn, returns information about the user specified.
My.User is a logical encapsulation of the current logged on user. By default, My.User is initialized to the current Windows user. However, running as the Windows user is not always appropriate: it is sometimes necessary to rely on an external infrastructure such as a SQL database to retrieve user information. Using the System.Security.Principal infrastructure, My.User can be reconfigured to use IPrincipal and IIdentity implementations designed to interact with alternative authentication and authorization models:
My.User.CurrentPrincipal = CustomPrincipal
Once My.User.CurrentPrincipal has been set, subsequent calls to My.User will query the custom implementation for the user name, user's roles, and so on. Although designing custom IPrincipal and IIdentity implementations is beyond the scope of this article, you can find more information at Authentication and Authorization.
Many applications require detailed tracing information to be maintained for health monitoring and audit purposes. My.Application.Log provides straightforward methods for writing categorized trace information to a log file:
Private Sub MyApplication_UnhandledException( _ ByVal sender As Object, ByVal e As UnhandledExceptionEventArgs) _ Handles Me.UnhandledException My.Application.Log.WriteException(e, TraceEventType.Critical, _ "The application has crashed due to an unhandled exception") End Sub
Because My.Application.Log is built atop the System.Diagnostics.TraceSource infrastructure, you can alter its default configuration to route and filter trace messages as needed using a .config file. For example, it's often necessary to write critical error messages to the Application Event Log so system administrators can identify application failures. Adding the code in Figure 1 to the App.config file ensures that critical messages will be routed to the Application Event Log while messages of all severities will continue to be routed to a file.
Figure 1 Routing Trace Messages
<system.diagnostics> <sources> <!-- The DefaultSource is used by My.Application.Log to write messages. --> <source name="DefaultSource" switchName="DefaultSwitch"> <!-- The listeners section defines where messages will be written. Here I'm writing to a file and the event log --> <listeners> <add name="FileLog"/> <add name="EventLog"/> </listeners> </source> </sources> <!-- The switches determine when the TraceSource is enabled/disabled. Here I'm stating that My.Application.Log is enabled for all messages with a severity greater than or equal to Information. --> <switches> <add name="DefaultSwitch" value="Information" /> </switches> <!-- Listeners are responsible for writing the log messages. --> <sharedListeners> <add name="FileLog" type= "Microsoft.VisualBasic.Logging.FileLogTraceListener" initializeData="FileLogWriter"/> <add name="EventLog" type= "System.Diagnostics.EventLogTraceListener" initializeData="My Customization Example"> <!-- By providing a filter I can ensure that only a subset of messages will be written by the listener. Here I'm specifying that only Critical messages are written to the Event Log.--> <filter type="System.Diagnostics.EventTypeFilter" initializeData="Critical"/> </add> </sharedListeners> </system.diagnostics>
Since My.Application.Log is driven by a .config file, it has the added benefit of being modifiable after the application has been deployed because the application does not need to be recompiled.
My.Computer.Network provides straightforward methods for determining network availability as well as uploading and downloading files, as shown here:
If My.Computer.Network.IsAvailable Then My.Computer.Network.DownloadFile( _ "http://myserver.com/log.txt", _ "c:\archives\serverlog.txt") End If
It is built using the System.Net classes and inherits their runtime configuration characteristics. For example, administrators may add the following section to a machine or application configuration file to use the default system proxy for all network requests:
<system.net> <defaultProxy> <proxy usesystemdefault="true"/> </defaultProxy> </system.net>
My.Computer.Network, in turn, will automatically detect these settings and use them for network requests. For more information on System.Net configuration, see <system.net> Element.
My.Settings encapsulates a powerful infrastructure for manipulating application settings, such as Web service URLs and user preferences. It enables you to add and remove settings at design time and to retrieve those settings at run time without writing error-prone, often tedious code to locate, parse, and update configuration (.config) files. (For more information on My.Settings, see Using My.Settings in Visual Basic 2005). Because many applications require settings to be persisted in alternative locations (such as the Windows registry or .ini files) you can implement custom logic that tells how and where My.Settings should read and write settings.
The My.Settings configuration model employs the Provider Design Pattern, which is used extensively in ASP.NET 2.0. (See Provider Model Design Pattern and Specification, Part 1 for more information on the Provider Pattern). The Settings Provider performs the logic for reading and writing settings using a specific medium, such as a Web service or the Windows registry. A Settings Provider can be specified for individual settings or all settings in My.Settings.
Within Visual Studio® 2005, you can specify Settings Providers using the property grid for the setting. When specifying the provider, the following format is used: Namespace.Type. Assembly. As an example, Figure 2 shows how to customize My.Settings to use the RegistrySettingsProvider that is included in the source code for this article.
Figure 2 Using the RegistrySettingsProvider
Extending the My Namespace
The My namespace exposes solutions for many common programming tasks that surface in application development, but if you're working in a specific domain you can encounter problems that the My namespace does not solve. For example, My.Computer contains support for reading and writing to serial ports but it does not support USB ports; My.Application includes tracing functionality but it doesn't expose crash reporting services; Data intensive applications may benefit from a top-level "My.Data" node for performing common data tasks. Using the extensibility model included in the My namespace, you can add new members to the My namespace to meet growing application needs. Deploying My namespace extensions to other developers working in the same domain allows them to use the extensions as easily as the other members of the My namespace.
You might be asking why you should extend at all. To date, developers have relied on external class libraries to solve specific problems, so why write the code if someone else has already done it? Often times developers will use third-party and community-owned class libraries, which involves a certain learning curve. What problem does the library solve? How is it intended to be used? And how do I actually use it? These are the sorts of questions you grapple with. The existences of varying design standards only complicates the learning process. The My namespace's extension model seeks to add a level of consistency and simplicity to the process of sharing solutions among developers. Following its straightforward design guidelines, you can expose solutions in a manner that ensures consumers of the solution will understand its intended usage pattern and that developers of all experience levels can be productive with the solution.
Unlike other .NET Framework namespaces, which reside in external assemblies (DLLs), the My namespace actually resides below the root namespace of the Visual Basic project in an in-memory file. Figure 3 shows some contents of the My namespace.
Figure 3 My Namespace Overview
Namespace My Module MyProject Public ReadOnly Property Application() As MyApplication Public ReadOnly Property Computer() As MyComputer Public ReadOnly Property User() As User ... End Module Partial Class MyApplication Inherits ApplicationServices.WindowsFormsApplicationBase End Class Partial Class My Computer Inherits Devices.Computer End Class End Namespace
Namespace Devices Public Class Computer Public ReadOnly Property Ports() As Ports ... End Class End Namespace Namespace ApplicationServices Public Class WindowsFormsApplicationBase Public ReadOnly Property Info() As AssemblyInfo ... End Class End Namespace
The Application, Computer, and User property definitions are manifested as My.Application, My.Computer, and My.User, respectively. Interestingly, My.Application and My.Computer actually return instances of the MyApplication and MyComputer classes, which are also defined within the My namespace. These classes, and the fact that they're declared with the Partial keyword, are essential to the extension of My.Application and My.Computer.
Because My is just a namespace, adding top-level properties to it is relatively straightforward. Start by adding a module to the project and enclosing the module within the My namespace. Next, annotate the module with the HideModuleName attribute. The HideModuleName attribute tells IntelliSense to hide the module name when users navigate the My namespace.
Finally, add the properties as appropriate to the module and for each property added to the My namespace, add a backing field of type ThreadSafeObjectProvider(Of T). This field is used to create thread-safe instances inside the property, such that each thread that is accessing the extended property receives its own instance of the returned type. The following example adds a SampleExtension extension to the My namespace:
Namespace My <HideModuleName()> _ Module MyProject Private m_extension As _ New ThreadSafeObjectProvider(Of SampleExtension) Public ReadOnly Property SampleExtension() As SampleExtension Get Return m_extension.GetInstance() End Get End Property End Module End Namespace
Because the types returned from My.Application and My.Computer are defined as Partial in the project, extending My.Application and My.Computer is straightforward. The process for extending My.Computer is as follows. First, add a Friend class named MyComputer and mark the class with the Partial keyword. Enclose the MyComputer class in the My namespace and then add properties and methods to the MyComputer class. As properties and methods are added they are immediately available under My.Computer:
Imports System.Net.NetworkInformation Namespace My Partial Class MyComputer Public ReadOnly Property IPAddresses() As IPAddressCollection ... End Property End Class End Namespace
Extending My.Application follows the same process. The only difference is that the Partial class name is MyApplication.
Extending the My.Application Event Model
My.Application includes a number of events in Windows Forms projects that encapsulate changes to the overall application state, such as application startup and shutdown, changes to network connectivity, and application crashes. Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase, the base class from which My.Application derives, includes entry points for extensibility that you can use to perform work before and after these events fire. Here is how these methods are invoked:
- OnInitialize. The first entry point to be called. Any preprocessing of command-line arguments can be done here. If OnInitialize returns False, the event sequence will not continue and the application will exit.
- OnCreateSplashScreen. Called after the initialization work is complete. This method is where the splash screen for the application is set.
- OnStartup. Called just before the Startup event is fired. Returning false from this method cancels the event sequence and exits the application.
- OnRun. Called after the startup event has been handled and before the main form is shown.
- OnCreateMainForm. The last of the startup events to be fired. It's where the MainForm property on My.Application is set.
- OnShutdown. Called just before the Shutdown event is fired.
Figure 4 demonstrates how to extend the My.Application event model by adding logic to ensure that the splash screen is shown for the time specified as a command-line argument.
Figure 4 Extending the MyApplication Event Model
Class MyApplication Private m_splashTimeout As Integer = 2000 Private m_timerThread As Thread ''' <summary> ''' Searches the command-line arguments for a splash screen ''' timeout. ''' </summary> Protected Overrides Function OnInitialize( _ ByVal commandLineArgs As ReadOnlyCollection(Of String)) _ As Boolean For Each argument As String In commandLineArgs If (argument.Contains("/splashtimeout")) Then m_splashTimeout = Integer.Parse( _ argument.Split(":")(1)) * 1000 End If m_timerThread = New Thread(AddressOf RunTimer) m_timerThread.IsBackground = True m_timerThread.Start() Next Return MyBase.OnInitialize(commandLineArgs) End Function ''' <summary> ''' Before showing the main form, make sure that the splash ''' screen has shown for the requested length of time. ''' </summary> ''' <remarks></remarks> Protected Overrides Sub OnRun() If SplashScreen IsNot Nothing AndAlso _ SplashScreen.Visible = True AndAlso _ m_timerThread IsNot Nothing AndAlso _ m_timerThread.IsAlive Then m_timerThread.Join(m_splashTimeout) End If MyBase.OnRun() End Sub ''' <summary> ''' This method executes for roughly m_splashTimeout. ''' </summary> Private Sub RunTimer() Dim runningTime As Integer = 0 While runningTime < m_splashTimeout Thread.Sleep(100) runningTime += 100 End While End Sub End Class
In addition to adding top-level members to the My namespace, members can also be removed and redefined as needed. For example, if a specific application domain requires an application object other than the default Microsoft.VisualBasic.ApplicationServices.ApplicationBase, My.Application can be redefined to return the more appropriate application object. Similarly, members such as My.WebServices can be removed from the My namespace to accommodate scenarios in which Web services are not used.
The MyType construct tells the Visual Basic compiler which top-level members should appear for a given project type. For example, My.Forms is not very useful in a Windows Service project so it is omitted from the My namespace. Extenders of the My namespace can use MyType to reshape the My namespace in order to provide a highly customized experience for their consumers.
MyType is effectively a predefined compiler definition that is used to conditionally compile members of the My namespace. As an example, consider the following code, which the compiler uses to construct the My namespace in Windows Forms projects:
#If _MyType = "WindowsForms" Then 'My.Forms will be included in the My Namespace #Const _MYFORMS = True 'My.Application will include Windows Forms specific 'events and members #Const _MYAPPLICATION = "WindowsForms" 'My.Computer will contain members for Windows client applications #Const _MYCOMPUTER = "Windows" 'My.User will return the Windows user #Const _MYUSER = "Windows" #ElseIf _MyType = "Console" Then '... #End If
When a custom MyType value is provided to the Visual Basic compiler, the My namespace is effectively empty and individual members must be added back as needed. Adding new members to the top-level of the My namespace follows the process outlined in the section on adding members to the top-level My namespace; predefined, top-level members, such as My.Forms, can be added back to the My namespace using compiler constants.
Each predefined, top-level member of the My namespace has a compiler constant associated it. For example, a Boolean compilation constant called _MYFORMS is used to specify whether or not "Forms" should be included in the My namespace. Each predefined, top-level member of the My namespace and its associated compilation constant is described in Figure 5.
Figure 5 My Namespace Compilation Constants
|Member Name||Compilation Constant||Available Values||Returns|
|My.Forms||_MYFORMS||True||My.Forms is included|
|False||My.Forms is excluded|
|My.WebServices||_MYWEBSERVICES||True||My.WebServices is included|
|False||My.WebServices is excluded|
Because predefined, top-level members of the My namespace are configured using compilation constants, their values must be provided to the Visual Basic compiler via command-line arguments. Within Visual Studio, compiler constants are specified on the Compile page of the Project Designer.
To demonstrate the process for defining a custom MyType, I'll configure My.User such that it returns a custom type. The custom type, ExtendedUser, contains utility methods called Login and Logout for performing authentication. Start by creating a class library and then add the following class to it:
Imports System.Security Public Class ExtendedUser Inherits Microsoft.VisualBasic.ApplicationServices.User Public Function Login(ByVal username As String, _ ByVal password As SecureString) As Boolean 'TODO: Perform login logic End Function Public Sub Logout() 'TODO: Perform logout logic End Sub End Class
Next, create a Windows Forms Project and add a reference to the Class Library in the Windows Forms Project. Add the following custom definition for My.User:
#If _MyType = "Custom" Then Namespace My <HideModuleName()> _ Module MyExtensionModule Private m_userInstanceProvider As New _ ThreadSafeObjectProvider(Of ExtendedUser) Public ReadOnly Property User() As ExtendedUser Get Return m_userInstanceProvider.GetInstance() End Get End Property End Module End Namespace #End If
Set _MyType="Custom" and include the predefined, top-level members of the My namespace using command-line arguments to the compiler. To include all predefined members except My.User (as just defined), here are the command-line arguments:
MyType="Custom", _MYAPPLICATIONTYPE="WindowsForms", _MyType _MYCOMPUTERTYPE="Windows", _MYFORMS=True, MYWEBSERVICES=True
Once you have completed these steps, My.User will return ExtendedUser, wherever it used in the Windows Forms Project.
My Namespace Customizations
When you're considering strategies for designing, deploying, and maintaining your My namespace extensions, it is easiest to divide the problem into two pieces: the My namespace extension and the class library. The My namespace extension is the code that is going to be used to expose your class library in My, as was demonstrated earlier. The class library is the assembly (DLL) that contains the types that will be exposed in the extensions.
The following guidelines will help minimize the maintenance costs for My namespace extension components.
Only include the extension logic The logic included in the My namespace extension itself should only include the plumbing that will be used to expose functionality in the My namespace. Because this component resides in consumers' projects as source code, updating the extension component is very expensive and should be avoided when possible.
Minimize project assumptions Assuming a set of references, project-level Imports, or specific compiler settings (for example, Option Strict is off) is generally unwise. Instead, minimize dependencies, fully qualify all type references using the Global keyword, and ensure the extension compiles with Option Strict to minimize errors in the extension.
Isolate the extension code Placing all of the My namespace extension code in a single file or a separate folder in the project will help users isolate the My namespace extension.
Designing Class Libraries for My
Like most object models, some design patterns work well in the My namespace and others do not. When designing an extension to the My namespace, consider the following principles:
Stateless APIs Methods in the My namespace should provide an end-to-end solution to a specific problem. The parameter values provide the only input required to complete the task. Methods that rely on previous state tend to be confusing in the context of My. My.Computer.FileSystem includes good examples of stateless APIs that work well in the context of My.
Global instances The only state that is maintained within the My namespace is global to the project. For example, My.Application.Info encapsulates state that is shared through the app.
Simple parameter types In general, the easiest methods to call do not require complex types as parameters but instead take Strings, primitive types, and so on.
Factory methods Some types are necessarily difficult to instantiate. Providing factory methods as extensions to the My namespace enables you to more easily discover and consume types that fall into this category. An example of a factory method that works well is My.Computer.FileSystem.OpenTextFileReader. There are several Stream types, but this method helps demystify which Stream to use and how to create it.
These guidelines do not preclude general design principles for class libraries. Rather, they are recommendations that are optimized for developers using Visual Basic and the My namespace. A discussion of the .NET Framework Class Design Guidelines is available at Designing .NET Class Libraries.
Packaging and Deploying Extensions
The process for packaging and deploying My namespace extensions is similar to deploying a standard class library or third-party controls. The only other step is to provide a straightforward mechanism for adding the extension to each project. Visual Studio 2005 includes an improved template format that is well-suited to packaging and distributing My namespace extensions.
A Visual Studio Template (.vstemplate) file encapsulates content that can either be used as a baseline for new projects (a project template) or added to an existing project (an item template). Packaging My namespace extensions as template files integrates the extension seamlessly into Visual Studio, allowing consumers to add them to their projects using the Add New Item and New Project dialogs. A benefit of this approach is that Visual Studio templates can be installed per-user or per-machine. A detailed example of a My namespace extension packaged as a Visual Studio Item template is included in the code download.
The My namespace includes a full-featured set of members and services designed to make the most common programming scenarios the easiest to implement. Although extensive, the core set of APIs included in the My namespace is merely the starting point for future innovation. Its extensibility model enables you to create, share, and continually evolve the My namespace as application requirements change and new solutions are discovered.
Joe Binder is a program manager on the Visual Basic team at Microsoft where he works on the My namespace, the Visual Basic runtime, and the Visual Basic Application Framework. He can be reached at email@example.com.