I have a need to extend a class that is implemented in a base (UI-framework) library as a singleton class. Without going into too much detail, the class was designed to serve as a base class for application-specific implementation but implemented as a singleton in order to take advantage of that pattern, but to also provide a pseudo-global means of accessing settings and properties needed throughout the application. It is critical that the same instance of the class is used throughout - thus the singleton implementation.
While I know that everything you read about implementing singleton indicates that the class should be sealed (NotInheritable in VB), this class is intended to be extended, so we seem to have an inherent conflict. In the "base" class, properties that are used by other classes in the UI-framework library as well as by application code are defined. But in order to provide a single, consistent way to access the information, we are able to extend the class by deriving a new class from it in our application code addign whatever additional properties and methods as are necessary.
The best example of what we are trying to accomplish can be found in the .NET Framework with the System.Net.WebRequest base class and derived FileWebRequest, FtpWebRequest and HttpWebRequest classes. The WebRequest.Create() method accepts the request's URI and instantiates the proper class based in the value. This requires that the derived classes be "registered" with the application in the config file by indicating what URI prefix is associated with each class (e.g. ftp -> FtpWebRequest).
Perhaps I need to be a little more concrete...
Essentially, we are using a UI-framework that supports automation-based application development (a derivative of the MVC pattern). If we use the infamous and oft-referred MS-Office paradigm, all Office applications share a common set of properties and methods exposed through the Application object. But, each has it's own spin. For instance, if we were developing Excel and Word, we would have a base Application class in our common Office library (Office.Application) that would have Caption, CommandBars and DisplayRecentFiles properties as well as Help and Quit methods, etc. Each application would then implement their own Application class (Excel.Application and Word.Application) derived from Office.Application. The Excel class would have an ActiveSheet property whereas the Word class would have an ActiveDocument property.
This is essentially the model we are trying to follow.
One last note, it has occurred to me that we could possibly accomplish this by abandoning the singleton pattern and implementing each property and method statically - which would more resemble the current (2003 and before) versions of the Office apps - but, we are using this framework to support ASP.NET apps as well as Windows. This is an important distinction because static variables behave in a bit of a different way with ASP.NET apps then might be intuitive. Because of the ASP.NET process model, client request share the same process and thereby share the same static variables. Anyone who has tried to store a singleton (static) object in Session and noticed how one client's changes magically affect other clients has experienced this problem. And, since we need a separate instance of our object PER USER (i.e. per session), it would be a logistical nightmare to have to work-around this problem on a per property and method basis whereas addressing this in the singleton method allows us to encapsulate the fix in the base class and remove this from the shoulders of our application developers.
FYI - there's a great work-around for the ASP.NET singleton problem. See http://dotnet.org.za/eduard/archive/2004/07/07/2609.aspx
Hopefully that explains our situation thoroughly. I have scoured the web for info on this and have found nothing, so I am hoping that the experienced users of this forum will chime in and help us figure out how to make this happen.
Thanks for your input...
The reason this won;t work is that we need the same instance of the object to be used by code in ALL of the class libraries that need it. So, running with the example, when running our Word application, code in both the Word and Office class libraries need to use the same instance of the Application class. Same with our Excel app. However, even though both Word and Excel know that they want to create Application.GetInstance<Word.Application>() or Application.GetInstance<Excel.Application>, respectively, the classes in our common Office class library don't know how to instantiate the object this way since they won't know what application is being executed. All that code knows is Application.GetInstance<Office.Application>. The end result is separate instances for each class library and not what we are trying to accomplish.
We need a way for code in the base library to instantiate the most sub-class applicable to that application. In our example, either Word.Application or Excel.application depending on which app is running. This way, every reference to the Application object in the app will refer to the same instance throughout.
Make more sense?
Same reason - all references need to be the same instance. That is why I indicated the singleton pattern originally. And, as I mentioned, the base class library doesn't know anything about the derived type.
So, for instance, if we have code in our base Office class library that makes use of the Application.CommandBars property, such as:
Application.CommandBars.ListChanged += new EventHander(CommandBars_ListChanged);
And, in our actual application library, Word.dll, we have the following code:
Application.CommandBars.Add(new CommandBar("myBar"));
unless both references to "Application" are returning the same instance, the second statement will not trigger the desired event in the base class library.
To go with the generic approach suggested, we would have the following statements:
in Office.dll:
Application<Office.Application>.CommandBars.ListChanged += new EventHander(CommandBars_ListChanged);
and,
in Word.dll:
Application<Word.Application>.CommandBars.Add(new CommandBar("myBar"));
Clearly these are two separate instances whether we are tracking them in a dictionary or not.
What we need is a way for the following to be true from both the base class library (Office.dll) and the application class library (Word.dll):
(Office.Application.Current == Word.Application.Current)
so that any calling code will return the same instance no matter how many layers deep the object model is or how many assemblies are involved.
Does that make things clearer?
Nah, you're still not getting it. Forget the COM model because that didn't use inheritance with a common base class library.
Yes, you can instantiate the "client application" - for lack of a better name - using Word.Application or Excel.Application. But the common, Office class library doesn't know anything about either of those applications even existing, let alone which is running. And, for that matter, both could be running - and multiple sessions of each could be running. It doesn't matter. When ANY code in ANY class in ANY library within the process of the current application calls for the current Application object, we want it to return the same object.
Maybe this will help. Here's some quick example code of our three Application classes:
In Office.dll (our common base class library):
public class Application
{
private CommandBarCollection _commandBars;
private static Application _currentInstance;
public CommandBarCollection CommandBars
{
get
{
if (_commandBars == null)
_commandBars = new CommandBarCollection(this);
return _commandBars;
}
}
public static Application Current
{
get
{
if (_currentInstance == null)
?????????
return _currentInstance;
}
}
}
public class MainMenu : Office.Menu
{
public MainMenu(Office.Application application) : base(application)
{
application.CommandBars.ListChanged += new CommandBarChangedEventHandler(CommandBars_ListChanged);
}
private CommandBars_ListChanged(object sender, CommandBarChangedEventArgs e)
{
if (e.ListChangedType == ItemAdded)
Items["Toolbars"].Add(new MenuItem(e.CommandBar.Name));
}
}
In Word.dll (a "client application" referencing the common base class library):
public class Application : Office.Application
{
private DocumentCollection _documents;
public DocumentCollection Documents
{
get
{
if (_documents == null)
_documents = new DocumentCollection(this);
return _documents;
}
}
protected void Initialize()
{
CommandBars.Add(new FormattingToolbar());
}
}
In Excel.dll (another "client application" referencing the common base class library):
public class Application : Office.Application
{
private WorkbookCollection _workbooks;
public WorkbookCollection Workbooks
{
get
{
if (_workbooks == null)
{
_workbooks = new WorkbookCollection(this);
_workbooks.SelectionChanged += new SelectionChangedEventHandler(Workbooks_SelectionChanged);
}
return _workbooks;
}
}
private Workbooks_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
Application.Title = e.SelectedItem.Name;
}
}
This is a pretty simplistic example but does demonstrate the needed functionality with the exception of how to instantiate the Application class. As you can see, the object used by the MainMenu class in Office.dll needs to be the same as what's used in Word.dll (and Excel.dll) if the event is to respond to changes made to the CommandBars collection by code in that library - such as adding the FormattingToolbar in Word.dll.
Does this shed better light on the situation?
Set the current instance when the application instantiates.
public class Application
{
private CommandBarCollection _commandBars;
private static Application _currentInstance;
protected Application()
{
if (_currentInstance != null)
throw new AppAlreadyExistsException();
_currentInstance = this;
}
public CommandBarCollection CommandBars
{
get
{
if (_commandBars == null)
_commandBars = new CommandBarCollection(this);
return _commandBars;
}
}
public static Application Current
{
get { return _currentInstance; }
}
}
Yea that'd work except that it relies on "who gets there first". If the first call to Application.Current is made as:
app = Office.Application.Current;
then the instance will be of the Office.Application type; whereas, if the first call is:
app = Word.Application.Current;
then the instance will be of the Word.Application type.
I like the solution, but there is no assurance that it is the desired type that is set.
In the Word application, we need it to always be Word.Application. If we use this approach and the call from within the Office base class library (first statement above) executes first and the _currentInstance is set to type Office.Application then not only will we not be able to take advantage of the additional properties and methods added by the derived Word.Application class, but we will generate a run-time exception when we try to access one of them.
Know what I mean?
So then, how will other classes in the base class library make use of the base Application class (BaseUIApplication)?
As I've said, that is one of the requirements.
public class BaseUIClass
{
public void Initialize()
{
BaseUIApplication.Current.CommandBars.Add(new CommandBar("myBar"));
}
}
This code will ONLY work if code that initializes the SpecificUIApplication executes first. In the scenerio you described this will be the case but since many clients applications of many types will make use of the base library there is no guarantee that this will be the case. By making the BaseUIApplication abstract, all we do is ensure that a run-time exception is thrown when an attempt is made to access the BaseUIApplication.Current property before initialized by the client application.
I am looking for a better, more reliable and stable approach. Also keep in mind that we will be using this to drive web applications as well as windows form apps. Perhaps relying on Application and CommandBars as the example steered things in an unintended direction. The whole point is to have a singleton class that is used in one class library extended and used in another.
I will have to setup and test some different scenerios and see if anything comes from it, I guess. Not as easy an answer as I'd hoped to get.
Copyright (c) Marimer LLC