Let users add functions to your .NET application through macro and plugins

xiaoxiao2021-03-06  39

Jason Clark

This article assumes that you are familiar with .NET and C #

Download this article: plug-ins.exe (135KB)

Overview

Most user applications benefit from the capabilities that can be extended by other developers. Extending a user is very familiar and existing apps that have trained it tends to be simple and effective than from scratch development. Therefore, scalability will make your application more attractive. You can make the application to be scalable by supporting plugins and macros. Use the .NET Framework to easily implement this, even if the core application is not a .NET Framework application. In this article, the author will describe the scalable features of .NET Framework (including advanced binding and reflection) and their usage methods, and introduce plug-in security precautions.

Imagine what the perfect text editor looks like. It starts no more than two seconds to support context coloring and automatic indentation for popular programming languages, support multi-document interface (MDI) and cool and popular tab document arrangements. Ideal that this perfect text editor is perfect in the eyes of bystanders. These features are just the definition of the perfect text editor, others will definitely have different standards. Perhaps the most important feature that the perfect text editor can support rich scalability so developers can use the features they need to expand the application.

Scalable text editor may support creating custom toolbars, menus, macros, and even custom document types. It should allow me to write plugins that can be used to edit the process to add automatic completion, spell check, and other wonderful features such as this. Finally, the perfect text editor should be able to write your own plugin in any language (I personally preferred C #).

It is true that every application I want can expand in this way. If you write a small amount of code in some places, you can customize your favorite applications, then it is fine. Even if I can't do it, I know that other people can do it, I will use their extension from Internet. This is the original intention to carry out this event to make all developers to write the extensible application.

Ideal scalable application

Many applications can be modified using an insertable code. In fact, the entire Microsoft Office application suite can be customized so broadly, so that people can use Office as a platform to write a complete custom application. However, even if there is such a complete customizable ability, I still wrote my first plugin for Microsoft Word (a application I used almost every day).

the reason is simple. All features of Microsoft Office do not fully comply with my standard, including:

• Simpality. I want to operate my plus intoserted applications with very simple software tools that have been familiar with. • Permissions. I want my plugin to access certain objects and function subset of the built-in built-in. This permission should be natural, as part of the programming language I have selected. • Programming language. Sometimes I want to use a specially selected programming language. • ability. In addition to accessing the application's document object model (DOM), I have to have a rich API. • safety. I need to be able to download someone writing and can be downloaded through the Internet. I hope to perform components with potential threats or mistakes without having to consider the security of the system.

Listed short but nausea. In fact, these standards are too strict for ordinary applications before the Microsoft .NET Framework issued, it is impossible to do. But now, I can show you how to use .NET Framework to add all of these scalability features to your hosted and unmanaged applications.

.NET Framework scalability

Scalability is built on late binding, which refers to the ability to discover and execute code at runtime rather than compile (more typical). In the past few years, there are many technological innovations that make significant contributions to advanced bindings, including DLL and COM APIs. .NET Framework increases the simplicity of advanced binding to a new level. To deepen understanding, let's take a very simple code example. Figure 1 shows how simple bindings using reflection in managed objects. If you build the code in Figure 1 within LateBinder.exe, you can pass any assembly (such as a set of assemblies built from the code in Figure 2) to it as a command line parameter. Latebinder.exe reflects the assembly and creates an instance of the class derived from the Form and makes them its own MDI subclass. The reflection in .NET Framework makes the late binding are greatly simplified.

Reflection is one of the basic tools of .NET Framework, which promotes the development of scalable applications. It is one of the four functions that I mentioned here that can be extended.

The public type system uses the .NET Framework for a while, you may start thinking about the public Type System (CTS). However, it is indeed one of the reasons why scalability in the platform is so simple. CTS defines the partial object-oriented features that must be followed by all managed languages, such as derived rules, namespaces, object types, interfaces, and primitive types. These basic provisions of CTS are code settings for the public language runtime (CLR).

Reflection reflecting is the ability to discover information (eg, methods such as the type of assembly or type definition of the method of the assembly). It is possible to reflect it because all managed code is described by the data structure (called metadata) embedded in the assembly.

Fusion .NET Framework uses Fusion to load assemblies into a managed process (AppDomain). Fusion helps implement some advanced features such as strong naming and simplifying the DLL search rules.

Code Access Security Code Access Security (CAS) is a feature of the .NET Framework that simplifies the execution of some trusted code. Briefly, you can use Microsoft .NET Framework's feature to limit the content that the late binding code can be accessed, so you don't have to worry about the user's system.

This is the four functions that change the scalability into a reality .NET Framework. However, since these functions are so cool, an article describes a function that cannot be very thoroughly speaking. Therefore, the best practice is to introduce this topic from a task.

Extensibility

Regardless of your application, as long as it is scalable, you must perform three basic tasks: discovery, loading, and activation extensions. Find the process of finding the plugins and other code bonded when your application is running. Loading is the process of putting code (packaged as an assembly) into a process or appdomain to activate and use the type defined by the assembly. Activation is the process of creating an instance of a late binding object and calling them.

Each of these three phases contains a number of .NET technology and application design considerations. Although technology can be done, .NET Framework does not define a special way to achieve scalability, so you can have many options.

Load: Bind to the code at runtime

Logically, the scalability application must find it before loading the code. However, reflection must load code to discover content related to it, so actually discovery the process may be after loading code. Let's take a look at this meaning.

Reflection can be used to achieve advanced binding. Most reflection classes can be found in the System.Reflection namespace. The three most important classes are system.appdomain, system.type and system.reflection.assembly. Behind I will introduce System.Type. To understand the Appdomain and the Assembly class, we simply look at the hosted process. The CLR runs a managed code in the Win32 process, which is better than the finer in the unmanaged application. For example, a hosted process can contain multiple appdomain, which you can think that the latter is a sub-process or a lightweight application container.

The assembly is a managed version of DLL and EXE, which contains reusable object types (such as class library types) and application code. In addition, any extended or plugin of the application should also exist in the assembly (very similar to DLL). The assembly is also loaded into one or more appdomain within the managed process.

Each managed process has at least one default AppDomain, including some shared resources, such as hosted stacks, hosted thread pools, and execution engines. In addition to these logical components, the managed process can also create any number of appdomain. See Figure 3, which shows a hosted thread containing two appdomain. APPDOMAIN is extremely important in the topic of the insert discovery.

Figure 3 Hosting process

Now let's go back to Appdomain and Assembly types. You can use the Assembly.Load static method to load the assembly to the current AppDomain. The assembly.Load method returns the reference to the Assembly object. This method binds the code to your application at runtime, and the method is to load the setup of the resident code.

Assembly.load By name (without extension) from the AppBaseDir directory load assembly (AppDomain secretly loads the deployed assembly). By default, Assembly.Load looks for a regular executable from the Loading Exe directory. When the early binding assembly is loaded, the assembly binding rules followed by Assembly.Load are the same as the CLR.

AppBasedir of AppDomain can be adjusted by getting an APPDOMAIN object and call AppDomain's AppendPRIVATH and ClearPrivatePath instance methods. If you use the Assembly.Load to load the plug-in set, you may need to operate AppBaseDir for appdomain. This is because the plugins under the sub-directory of the main application directory are very useful. I will soon explain the specific reason.

The Assembly class also implements a method called LoadFrom, which is different from LOAD, which has a full name (including path and extension) with the full name of the assembly file. LoadFrom just simply loads the file you point to, not to find assemblies according to binding and discovery rules. This is useful when loading the assembly used as a plugin. However, it is important to note that LoadFrom has an adverse impact compared to LOAD, and it lacks version binding intelligence, which makes almost all programs except in the plug-in scheme do not pass.

Once the reference to the Assembly object is obtained, you can find the type contained in the program set, and create these types of instances and call methods. I will introduce these operations in the next two sections.

Discover: found code at runtime

When compiling applications, you must explicitly or imply, tell the compiler application to bind and use the code when running. The form of these code is a widely reused class library type (eg Object, FileStream, and ArrayList), and packaging in the Helper program centralized type. The compiler is stored in the assembly list to implement these types of assemblies, while the CLR references the list to load the necessary assemblies. This is a typical hosted code binding process. As mentioned earlier, advanced binding also uses code in the form of assembly; however, the compiler does not directly involve the binding process. Instead, the code must find what assemblies it needs to be loaded at runtime. The discovery process can be achieved by various methods. In fact, .NET Framework does not specify a discovery method, but it does provide you with an important tool for implementing your own technology.

Two discovery mechanisms are worth mentioning. The first is the simplest one (see from the software perspective) is to maintain an XML file, which records the code that the application should bind when running. The second is a relatively advanced method based method, which allows the application to be higher availability. Let's first look at the XML method.

The XML method only needs to parse the XML file and load an assembly using the information in it. The following example shows a possible XML format:

This XML contains the least information required to use reflection to implement the loading process; that is, the name of the assembly you want to activate and one or more types of the program. The code simply finds the name of the XML medium set set, and then loads the assembly to AppDomain using the code as shown in Figure 1.

The XML method is easy to implement, but the application of applications is not available. For example, I don't especially like this text editor: I must edit some .config file to extend the application. However, for server-side applications, this method may be more appropriate than using reflections.

Reflection-based discovery methods can be automatically operated using known locations relative to the application directory. For example, I search for the sample application written in this article in a subdirectory named "plugins" (to download the full source code, go to the link at the top of this article). This special approach is useful because the user only needs to copy the assembly file to the file system, and the application will bind the new code when the application is started.

There are two reasons to achieve this method than the XML method is difficult. First, you must formulate the standards that the loading assembly should follow. Second, the assembly must be reflected to discover if it meets the standard, and whether it should be loaded into Appdomain.

There are three useful criteria to determine a type for code as a plug-in assembly. One of these standards should be adopted when the reflection method is used to discover.

Interface Standard Codes can use reflection search to find all types of implementation known interface types.

Base class standard code Re-use Reflection Search The entire assembly to discover all types derived from one known base class.

Custom attribute criteria Finally, custom properties can be used to mark a type of type as an application.

I will immediately show you how to use the reflection to find some types of advanced binding program sets to implement an interface, derive from a base class, or whether it is attribute. But first we take a look at the design compromise programs that use these binding standards.

Interface and base class standards are more useful than custom attribute methods (or any other ways based on reflection). First, with respect to custom properties, plug-in developers are likely to be familiar with interfaces or base classes. More importantly, you can call the late binding code in an early binding mode using the interface and base class. For example, when you use the interface as a standard that binds to an object type, you can use an instance of the object by a reference for the interface type. This avoids the need to use reflection to call other members of the method and access instance, thereby increasing the performance and code capability of the scalable application. To find a type of related information at runtime, you can use the reflection type System.Type. Each instance of the type derived from Type is referenced to a type in the system. The Type instance can be used to find some facts about this type at runtime, such as its base class or the interface thereof.

To get a set of types implemented by an assembly, simply call the gettypes instance method of the Assembly object. The GetTypes method returns a reference array of Type derived objects, and each type defined in the program set corresponds to one reference.

To determine if a type is to implement a known interface or derive from a base class, you can use the ISASIGNABLEFROM method for the Type object. The following code snippet shows how to test the type indicated by the SOMETYPE variable to implement the IPLUGIN interface:

Boolean importsplugininterface (Type Sometype) {

Return TypeOf (iPlugin) .IsassignableFrom (SomeType);

}

This principle also applies to the test base type.

It is effective for finding which types to be inserted into the application logic, load assemblies to reflect the type of the program. But if there is no type matches your standard, what will it? Simply put, it is not possible to uninstall the assembly already existing in AppDomain. This is the second reason why the reflection based discovery method is slightly difficult than the XML method.

For some client applications, all assemblies reflected in the code (even the assembly that do not meet the plug-in binding standard) are also acceptable. But for the scalable server application, there is not enough space to load any assessments that cannot be unloaded. The solution to this problem is divided into two phases. First, you can uninstall the assembly as long as you uninstall the entire appdomain of the loaded assembly. Second, a temporary Appdomain can be created by programming, and the unique purpose is to load an assembly to reflect in order to find out if the type of the program is needed as a plugin. Once the phase is found, you can uninstall this temporary appdomain and all its assemblies.

This solution sounds seem to be complicated, but it is only a very simple code to be realized. This method is used in this way, and the code to be implemented does not exceed 100 lines. If you want to use this method in your own scalable application, you will find that the source code of PluginManager is very useful (see Download File).

Activate: Create an instance and call method

Once determined which types to be inserted into the application, you will need to create an instance of an object. This can be achieved by reflection; however, you will find slow reflection speed, not practical, and it is difficult to maintain. In addition, you should also choose to use the reflection to create an instance of the plug-in object, and convert the object to a known interface or base class, and then use these objects based on known types throughout the remaining life cycle. Keep in mind that interfaces and base class standards are ideal for this method to find plug-in types.

Due to the CTS, the hosted code is object-oriented and safe. One challenge facing the late binding code is that it is not necessarily known to the type of object when compiling. But because there is a CTS, you can view any object as an Object base class, and then force it to convert to an application and a base class or interface that can be inserted into the code. If the object cannot force forced conversion, an InvalidCastException exception is raised, and the application can capture this exception and processed accordingly. However, before any such operation, you must create an example of the object to be bound to. Unlike early binding objects, you cannot use the New keyword to create an instance because the compiler needs to use the type name used with New, and for the late binding type, this is obviously unknown. The solution is to use a static method Activator.createInstance. This method will create an instance of an object (assuming an example of a reference object's Type derived) and an optional Object reference array (used as constructor parameters). The following code creates an object using CREATEINSTANCE and returns a known interface:

Iplugin createpluginoad (type type) {

Return (iPlugin) Activator.createInstance (TYPE);

}

Once you have an object and force it to convert it into a known type, you can call the object by reference to the variable, just like calling other objects. At this point, the objects in the late binding code are seamlessly integrated with other parts of the application.

Protect scalability

If your application will be widely distributed, and anyone can write a plugin, you must consider security. Fortunately, .NET Framework makes the protection code very easy. Let's take a look at how this is done.

Imagine an extensible text editor. In the ideal case, the user should be able to download the plugin from the Internet and securely insert it into the application, even if the plugin is designed by untrusted third parties. And if it is not trusted, it should be performed in some trusted states. In this state, it does not have the right to access system resources like file systems or registry.

.NET Framework can perform partially trusted code through the CAS. Because the hosted code is compiled in real time, the CLR has the ability to assert that some trusted managed code does not perform the operation of the lack of privileges. If partially trusted code attempts to perform an operation that is not allowed, the CLR will trigger SecurityException exception, and the application can capture the exception.

In some cases, the code defaults to some trust, such as controls distributed in the Internet or embedded within the HTML document. However, you can also use CAS so that users can safely use extended assemblies from third parties. In all cases, the CLR treats a assembly as a security unit, which means that the application can contain multiple assemblies, which will be different from those of each assembly. This is ideal for plugins.

In addition, .NET Framework provides a number of functions, and there are many ways that you can use these methods to create some trusted plugins. The following steps use the simplest and safest way. First, create two subdirectories for the application's plugin. A completely trusted plug-in and the other stored some trusted plugins. The local security policy is then adjusted by programming, and the code group is associated with the subdirectory corresponding to the partially trusted plugin. Finally, grant code internet permissions in the code group, which means that it has a permissions subset, which will be considered safe even if there may be malicious code.

Once this code group is generated, the CLR will automatically associate reduced permissions with assembly loaded from partially trusted subdirectories. In addition to adjusting local security policies (I will introduce you to you immediately), the security of the plugin will work automatically. Adjusting security policies

The .NET Framework security policy engine is very flexible and adjustable. In actual conditions, you can use the .NET Framework Configuration Control Panel Applet installed. Net Framework Configuration Control Panel Applet manually adjusts the security policy (see Figure 4). In addition, the strategy can also be adjusted by programming. To write code to modify the security policy, you must start from the SecurityManager type. This useful type can help you access the three policy levels installed by .NET Framework: Enterprise, Machine, and User. I recommend that you add a custom code group of the plugin to the Machine policy level. To discover the Machine Policy object, you can use the code as shown in Figure 5.

Figure 4 Security configuration

The code group is arranged in a logical hierarchy. Once the Policyvel object is obtained, you can traverse the hierarchy of the code group from the code group returned from the PolicyVel.rootcodeGroup property. By default, the name of the code group is all_code, which represents all managed code. You should create a custom code group as a subgroup of all_code code groups.

The code in Figure 6 creates a custom code group for any code loaded by a particular URL. The code group has Internet privileges, and sets PolicyStateAttribute.exClusive and PolicyStateAttribute.LevelFinal points to indicate that the code that matches this code group only has these privileges. The URL can be HTTP, HTTPS, or File URL. To associate this new code group with the directory in the file system, you can use file URL: file: // d: / programs / extensible-app / partially-trusted.

The CAS function of .NET Framework is very flexible, but it may take a while to use it. By reading this article and the sample application code I have in downloading the file, you should be able to get the information you need to create the plugin architecture. However, I strongly recommend that you use the .NET Framework Configuration Control Panel applet to try to make changes to the system policy, only this can be familiar with the concept. If you change too much, you can restore the policy at any time.

Scalable application design

The EXTENSIBLAPP.EXE sample application included in this article is an example of supporting custom plugins. In fact, the app is just a shell, which only displays an MDI window and allows the user to install the plugin. If you are using the code in this example as a learning tool to write your own extensible application, you should pay special attention to the code in PLUGINMANAGER.CS. This module includes PluginManager reuse class, which handles all non-specially-specific plug-in logic for sample applications.

Figure 7 before the plugin installation

If you build and run an ExtensibleApp.exe example, it will find that it allows you to select a DLL as a plugin to install it into the application. This example contains two plug-in objects: PluginProject1.dll and pluginProject2.dll. They use the application itself to create a toolbar and menu, and add a document type in the application. Figure 7 shows an application before being inserted into a custom code, and Figure 8 shows the application after insertion.

Figure 8 Operation Plugin

The application uses the technology and techniques discussed above in the following article. In addition, it also demonstrates some design methods that should be considered when the application is expandable. Let us look at some of these considerations. version control

Version control is an important aspect of the application lifecycle. It also has an important impact on expandable applications. Scalable applications need to define interfaces, base types, or attribute types used to discover and use plugins. If you include these types, they can be controlled by the application's partial part of the application, which may be Generate binding conflicts. But there is a way to solve this problem.

Solution is to define any types used to discover or bind to plugins, are defined in its own assembly, and only other types generated by the plugin in the program. It should also be avoided in the assembly of any code (at least too much), because the assembly is required to be controlled as little as possible. At the same time, you can also change and version control over other parts of the application as needed. Applications and plugins share fewerly versions of the adhesive assembly.

This brings a problem related to the base type used by the plugin. Unlike the interface, the base class typically contains the same code. This is a tricky problem, and it is also one of the main reasons why we prefer to choose to call advanced binding objects through the interface.

However, you should know that if you need to control the code in the base class and the interface program set, the flexibility provided by .NET Framework can meet the needs you will bind from the old version of the assembly to the new version. However, this requires a change to the binding policy, and the changes must be entered into the app.config file or the overall system of global assembly cache (GAC). It is best to avoid this possibility, so you have to manage the plug-in interface and base class to control them as little as possible (if you have done this).

Robust

Keep in mind that when you write an extensible application, although your code can get strict quality control, the plugin code is likely to not. Therefore, any code to call the late binding object by an interface or base class should encounter these unpredictable problems. Even if the developer does not want to cause damage, some trusted plug-ins can you cause safety anomalies. Similarly, some trusted plugins and complete trusted plugins may have an error, because the plugin author is different from you within your application.

If your extensible application only supports plugins written by yourself or your team, you can do not consider this problem. However, many scalable applications wish to recover normal wheels when the plug-in object occurs.

A powerful manner is that when an object does failure, it will fail in a well-defined way - means that the object will trigger an exception. So if you consistently adhere to the registered unprocessed exception handler, and handle the abnormality generated by the calling plugin code, even if the plugin fails, the user can provide a consistent experience.

If you call the plugin object with an interface, you can use a advanced technology, that is, pack all advanced binding objects in a proxy object that implements the same interface. The proxy object of this conventional purpose will pass all calls to the base plugin, but it will also package the exception handler consistent with the logging failed, while warning the user plug-in and other such situations. For the final plug-in robustness, this is a good idea, but for many applications, it may not require such stability.

Safety Precautions

As long as you have a policy to limit the permissions of the loaded assembly, .NET Framework can maintain the security of some trusted plugins. However, this has a big role in the protection system, but cannot automatically protect the status of the application. Some trusted objects share a hosted pile with objects in the application and need to consider how to limit them to access your internal application objects. Here are some points.

If there is no need, do not specify object types in the application as public objects. If the type is an internal type (default), you can limit the partial trusted code in the application to it. However, it is easy to introduce public types into applications without incident. For example, when you add a type of Form derived to the project, the Visual Studio will generate a public class. This is unnecessary for most applications, so you should delete these PUBLIC keywords, and then add it when you feel necessary. Similarly, if there is no need, members of the public type should not be made public or protected members. Even for non-scalable applications, members should also be private before you feel that it is necessary to improve their accessibility. Thus, if the internal accessibility can meet the requirements, do not increase. Protected members and public members are only used when you intend to publicly members of the program colleges.

You should pay attention to whether the class library type has a bad or incomplete security policy. For example, System.Windows.Forms.mainMenu classes open a method called getForm that returns the form where the menu is located. This is usually the main form of the application. Even if you don't intend to pass reference to the application main form to some trusted plugins, you may not intend to access the plug-in directly to derived from Menu, allowing the plugin to access the application main form. The CLR class developers considered similar security issues. For example, Form.Parent requires its caller with a security permission before returning a reference to the parent form. For example, some of the trusted code running in Internet privileges is unable to access the attribute by default.

As you can see, some trusted plugins may not be able to access general file systems or registry, but you still have to prevent malicious plugins to perform certain things, such as closing your app. Such problems are usually insignificant for client applications. But it is a significant issue for server applications.

In the last section, I will briefly discuss some advanced possibilities of the extended application. These topics are wide, it is difficult to introduce, most applications may not need to use, but understand these possibilities are helpful.

Plugin assembly

In the chapter of the plug-in, you may remember the issue of unmissible assembly. The solution to this problem is whether the test assembly is useful in temporary AppDomain to uninstall them when needed. Then, if you find that there are assemblies you really need, you can load them into your primary Appdomain. However, what if you don't want to save all plugins indefinitely?

For client applications, the following practices are acceptable: loading plug-in sets, using their types until they are no longer needed, and then do not need to be used in the application remaining life cycle. However, the requirements in the server are more strict. Server-side applications must run indefinitely, and do not consume important resources such as process address space, so you need to load and uninstall assemblies containing plug-in types.

To do this, you need to find the assembly in a temporary application domain, and then use the plugin type specifically to another appdomain. This increases a certain complexity to the design of the application, but it is also very good to separate the plug-in and core applications.

IT IS Possible to Instantiate and Use Objects Entirely from Withnin A Separate Appdomain, and The Feature of the .NET Framework That Implements This Is Called Remoting. It is possible to do in an independent appdomain. The function of achieving it in .NET Framework is called remote processing. Remote processing is used to access objects across an process and across network, but can also be used to access objects in different appdomain (even if they are located in the same Windows process). I can't tell the remote processing here, but you can find a detailed introduction in the previously published MSDN Magazine, or find some simple remote processing code in my sample pluginManager type. Hosting plug-in in non-hosting applications

With so many powerful scalable features, you can use the old-style applications written in unmanaged languages, such as Visual Basic 6.0 or C , may feel very disappointed. And now you can't be disappointed. The CLR itself is a COM object, which can host any Win32 process written by the language that can write a COM client.

This means you can write plug-in hosted code (with C # or Visual Basic .NET), then load the running library and adhesive code by the unmanaged code, and then load it with your unmanaged application by loading the code. Interaction plugin. At the same time, COM Interop allows you to seamlessly transfer COM interfaces between managed and non-hosting codes.

In fact, there are three ways to manage managed code and interact with it in the host unmanaged program. You can use COM Intero. CLR allows you to create a COM server using C #, Visual Basic .NET, and other hosting languages. You can bind and use these managed objects in any unmanaged application. It is also possible to use C (MC ) with hosted extension, which can naturally mix managers and non-hosting codes in a single process. If your application is written in C , you will find that managed C includes rich scalability. In addition, it is also possible to directly host CLR. The CLR itself is a host COM object. The .NET Framework SDK has a C header name called Mscoree.h, which contains the definition required to use the run library as a COM object.

Also emphasized that once the managed code is bound to the non-hosting application, the managed code can use the techniques described herein to implement the application's scalability.

summary

.NET Framework provides some very flexible features for code reflection, post-binding, and code security. These features can be used in a variety of ways to implement the expandable application. If a reliable application is also scalable, there may be longer life, or further development, which can prompt more plug-ins for the creation and broader for people. So please study these scalability functions carefully. I think the result will you like.

See related articles: .NET Framework: Building, Packaging, Deploying, and Administering Applications and Types.NET Framework: Building, Packaging, Deploying, and Administering Applications and Types-Part 2 Background information please see: .NET Framework Security by Brian A . LaMacchia, Sebastian Lange, Matthew Lyons, Rudi Martin, and Kevin T. Price (Addison-Wesley, 2002) Jason Clark provides training and consulting for Microsoft and Wintellect (http://www.wintellect.com) and is a former developer On The Windows NT and Windows 2000 Server Team.jason Clark provides training and consulting for Microsoft and Wintelle, and he used to be developers of the Windows NT and Windows 2000 Server team. He combined with people with Programming Server-Side Applications for Microsoft Windows 2000 (Microsoft Press, 2000). You can contact Jason with JCLARK@wintellect.com.

Go to the original English page

转载请注明原文地址:https://www.9cbs.com/read-78676.html

New Post(0)