Http://www.microsoft.com/china/msdn/archives/library/dncscol/html/csharp05162002.asp
Eric Gunnerson
Microsoft Corporation
May 17, 2002
Download supergraphfiles.exe from the msdn online Code Center ).
This month, I have just finished my ASP. NET Conference and are sitting in the Palm Springs International Airport Terminal waiting to fly back to Seattle.
This month, my initial plan (to some extent I still "have" the Plan) was for the supergraph application last month.ProgramTo do some work. However, in the past few weeks, I have received several emails asking me when to finish loading and uninstalling the assembly in the appdomain part. So I decided to focus on solving this problem first.
Application Architecture
I specialize inCodePreviously, I wanted to talk about what I tried to do. You may remember that supergraph allows you to select from the function list. I want to place an external program assembly in a specific directory so that supergraph can detect them, load them, and find all the functions they contain.
If supergraph can perform this operation by itself, no separate appdomain is required..Assembly. Load ()It usually runs well, but the Assembly cannot be detached independently (only appdomain can be uninstalled ). This means that if you are writing a server and you want users to update their external programs without starting or stopping the server, you will not be able to implement this task using the default appdomain.
To implement this function, we will load all the external program assembly in a separate appdomain. When a file is added or modified, We Will unmount the appdomain, create a new appdomain, and load the current file to it. In this way, everything will be perfect.
To better understand this, I have created a typical solution, as shown in figure 1.
Figure 1: Typical appdomain Solution
In this chart,Loader Class to createFunctionsNew Appdomain. Create appdomain After,Loader Create an appdomainRemoteloader .
To load the Assembly, goRemoteloaderCall the load function. This function opens the new Assembly, finds all the functions in the Assembly, and packs the functionsFunctionlistObject, and then return the objectLoader. Then, you can useGraphThe function uses thisFunctionlistInFunctionObject.
Create appdomain
The first task is to create an appdomain. To create an appdomain in the correct way, we need to passAppdomainsetup Object. Once you understand how all of this works, the documentation is enough, but if you are trying to understand how it works, it is not helpful. When Google search for this topic returns the last month's column as one of the higher matches, I suspect I may be in a bit of trouble.
The basic problem that must be addressed is how to load the Assembly at runtime. By default, the Global Assembly Cache or the current application directory tree will be viewed during runtime. But we want to load our external programs from a completely different directory.
When you viewAppdomainsetupYou will find thatApplicationbase Set the property to the directory of the assembly to be searched. However, we also need to refer to the original program directory, because that isRemoteloader Class.
Appdomain The creators understand this, so they have provided additional locations for searching for the Assembly. We will useApplicationbase Reference the external program directory, and thenPrivatebinpath Set to point to the main application directory.
The following is fromLoader Class code to achieve this function:
Appdomainsetup setup = new appdomainsetup (); setup. applicationbase = functiondirectory; setup. privatebinpath = appdomain. currentdomain. basedirectory; setup. applicationname = "Graph"; appdomain = appdomain. createdomain ("functions", null, setup); remoteloader = (remoteloader) appdomain. createinstancefromandunwrap ("supergraph.exe", "supergraphinterface. remoteloader ");
Create appdomain Then, useCreateinstancefromandunwrap () Function creation in new application domainRemoteloader Class. Note that the file name of the Assembly where the class is located and the full name of the class must be used.
When this call is executed, we returnRemoteloaderSame instance. In fact, it is a small proxy class that forwards all calls to other AppDomains.Remoteloader Instance. This is the same structure as. Net remoting.
Bind an assembly to the Log Viewer
When you write code to implement this function, you will produce an error. Because the loading subsystem is called "fusion. When running the viewer, you can ask it to record faults. When the application you are running has an assembly loading problem, you can refresh the viewer to obtain detailed information about the current situation.
For example, you will findAssembly. Load () . Dll is not required at the end of the file name, which is very useful. You can learn this from the log because it will tell you that it tried to loadF. dll. dll.
Dynamic Assembly Loading
Therefore, since we have created an application domain, the next step is to figure out how to load the component and extract the function from it. This requires two separate codes. The first code finds the file in the directory, and then loads each file:
Void loaduserassemblies () {availablefunctions = new functionlist (); loadbuiltinfunctions (); directoryinfo d = new directoryinfo (functionassemblydirectory); foreach (fileinfo file in D. getfiles ("*. DLL ") {string filename = file. name. replace (file. extension, ""); functionlist = loader. loadassembly (filename); availablefunctions. merge (functionlist );}}
Graph Functions in the class find all DLL files in the external program directory, delete their extensions, and then tell the loader to load them. The returned function list is merged into the current function list.
The second code is inRemoteloader Class, it loads the Assembly and looks for the function:
Public functionlist loadassembly (string filename) {functionlist = new functionlist (); Assembly = appdomain. currentdomain. load (filename); foreach (type T in assembly. gettypes () {functionlist. addallfromtype (t);} return functionlist ;}
This code is only called for the input file name (actually the Assembly name ). Assembly. Load ()And then load all the useful functionsFunctionlist The instance is returned to the caller.
At this point, the application can start, load the external program assembly, and then you can reference them.
Reload assembly
The next task is to reload these sets as needed. In the end, we hope to automatically implement this task, but for the purpose of testing, I willReload Button to make the Assembly reload. The handler of this button only callsGraph. Reload ()It needs to perform the following operations:
- Uninstall appdomain.
- Create a new appdomain.
- Reload the assembly in the new appdomain.
- Hook the graphic lines to the newly created appdomain.
Step 4 is required becauseGraphlineThe object containsFunctionObject. After the appdomain is uninstalled, the function object cannot be used again.
To solve this problem,Hookupfunctions ()ModifiedGraphlineObject To direct them from the current application domain to the correct function.
The Code is as follows:
Loader. Unload (); loader = new loader (functionassemblydirectory); loaduserassemblies (); hookupfunctions (); reloadcount ++; If (this. reloadcountchanged! = NULL) reloadcountchanged (this, new reloadeventargs (reloadcount ));
You only need to perform the reload operation, and the last two rows will trigger an event. The function is to update the reload counter on the form.
Detect new assembly
The next step is to detect the new or modified assembly displayed in the external program directory. This framework providesFilesystemwatcherClass. Below isGraphCode in the class constructor:
Watcher = new filesystemwatcher (functionassemblydirectory ,"*. DLL "); watcher. enableraisingevents = true; watcher. changed + = new filesystemeventhandler (functionfilechanged); watcher. created + = new filesystemeventhandler (functionfilechanged); watcher. deleted + = new filesystemeventhandler (functionfilechanged );
WhenFilesystemwatcherClass, we tell it in what directory to find and what files to track.EnableraisingeventsAttribute indicates whether to send events when it detects changes. The last three lines hook events to a function in the class. This function only callsReload ()To reload the assembly.
This method is cumbersome. When updating an assembly, you must uninstall the Assembly to load the new version. However, you do not need to uninstall the Assembly when adding or deleting files. In this case, the cost for performing this operation on all changes is not very high, and it makes the code simpler.
After constructing this code, we run the application and try to copy the new assembly to the external program directory. As we expected, we obtained the file change event, and when the reload is complete, new functions will be available.
However, when we try to update an existing Assembly, we encounter a problem. The file has been locked at runtime, Which means we cannot copy the new assembly to the external program directory and we receive an error.
AppdomainClass designers realize this is a problem, so they provide a good solution. WhenShadowcopyfilesSet propertyTrue(StringTrue, Not a Boolean constantTrue. Don't ask me why ......) The runtime will copy the assembly to the cache directory, and then open the assembly. In this way, the original file will not be locked, and we will be able to update the Assembly in use. ASP. NET uses this mechanism.
To enable this functionLoaderClass constructor adds the following lines:
Setup. shadowcopyfiles = "true ";
Then I regenerate the application and get the same error. I checkedShadowcopydirectoriesAttribute document, which clearly statesPrivatebinpathAll specified directories (includingApplicationbaseThe specified directory is shadow copied (if this attribute is not set ). Remember how I said that this document is not very good in this regard?
The document on this attribute is definitely incorrect. I have not verified the exact performance, but I can tell youApplicationbaseBy default, files in the directory are not shadow copied. Specifying a directory can solve this problem:
Setup. shadowcopydirectories = functiondirectory;
It took me at least half an hour to understand this.
Now we can update the existing file and load it correctly. But I just straightened this out and encountered another small problem. When we run the reload function from the form button, reload always occurs in the same thread as the drawing, which means that we cannot try to draw a straight line during the reload process.
Since we have switched to the file change event, it is possible to draw after uninstalling the appdomain and before loading the new appdomain. If this happens, we will get an exception.
This is a traditional multi-threaded programming problem, using C #LockThe statement is easy to process. I addedLockStatement, which ensures that they do not occur at the same time. This solves this problem. Adding an updated version of the Assembly will automatically switch the program to the new version of the function. This is quite good.
There is another strange phenomenon. Originally, the Win32 function used to detect file changes sent a large number of changes. Therefore, an update to the file will send five change events, and the Assembly will be reloaded five times. The solution is to write more intelligent and combine these operations.FilesystemwatcherBut this solution is not provided in this version.
Drag and Drop
Copying a file to a directory is not convenient, so I decided to add the drag-and-drop feature to the application. The first step to implement this task isAllowdropSet property to true, which enables the drag-and-drop function. Next, I hook a routineDragenterEvent. This event is called when the cursor moves on the object to determine whether the current object accepts the drag and drop operation.
Private void form1_dragenter (Object sender, system. Windows. Forms. drageventargs e) {object o = E. Data. getdata (dataformats. filedrop); If (o! = NULL) {e. effect = dragdropeffects. Copy;} string [] formats = E. Data. getformats ();}
In this handler, I check whether there are availableFiledropData (that is, the file is dragged to the window ). If there is, I set the effect to "copy", which will set the cursor accordingly, and if the user releases the mouse button, it will sendDragdropEvent. The last line in this function is used for debugging purposes to view available information in the operation.
The next task isDragdropEvent compilation handler:
Private void form1_dragdrop (Object sender, system. windows. forms. drageventargs e) {string [] filenames = (string []) E. data. getdata (dataformats. filedrop); graph. copyfiles (filenames );}
This routine obtains the data (array of file names) associated with this operation and passes it to the graphic function. Then, the graphic function copies the files to the external program directory and triggers file change events to re-load them.
Status
At this point, you can run the application, drag the new assembly to the program, the program will soon load them and keep running. This is quite good.