SharpDevelop source analysis (three, plugin system)

xiaoxiao2021-03-06  55

Third, plugin system

The backup book said the SHARPDevelop entry main function structure, ServiceManager.Service first calls AddintReSinglet in the initializeserviceubsystem method, and AddINtree is initialized here. This goen go to AddIntree to focus on the plugin system of SharpDevelop. In the case of the description, in order to facilitate the "plug-in" and plug-in specific "function module", the two words will not distinguish, and you can distinguish between the specific meaning from the context (in fact, SharpDevelop "plugin "It means .addin configuration file, each" plugin "may contain multiple" function modules ").

1, the configuration of the plug-in is since the plugin system, then let's take a look at the organization of the SharpDevelop plugin system. Many times, the same thing will come out from different angles to conclude that SharpDevelop's plugin system is also the case. Before watching the code of SharpDevelop, according to my understanding of the plugin, I think the so-called "plugin" is representing a functional module, the configuration of the plugin describes the plugin and specifies how this plugin is hung in the system. SharpDevelop's idea of ​​plug-in tree, that is, every plugin has a path of extension points in the system. So according to the understanding of the plugin, write plugins to do,: a in

After this, I have written a plug-in addintreeView that looks at the plug-in tree and intends to hang it in SharpDevelop. According to the definition of SharpDevelop to the plugin, after I have implemented the specific plugin's AddInTreeViewCommand, write a configuration file addintreeView.Addin as follows:

<

Addin

Name

= "AddIntreeView"

Author

= "SimonLiu"

Copyright

= "GPL"

URL

= "Http://www.icsharpcode.net"

Description

= "Display AddIntree"

Version

= "1.0.0"

>

<

Runtime

>

<

Import

askSEMBLY

= "../../ bin / addintreeview.dll"

/>

Runtime

>

<

Extension

Path

= "/ SharpDevelop / Workbench / MainMenu / Tools"

>

<

Menuitem

id

= "AddIntreeView"

label

= "View addINtree"

Class

= "Addins.addintreeView.AddintreeViewCommand"

/>

Extension

>

Addin

>

In the configuration file, the Runtime section specifies the specific path of the library file addins.dll where the plug-in function module is located. Specify the extended point path / SharPdevelop / Workbench / MainMenu / Tools in the Extension section (I plan to hang it to the main menu " Under the Tools menu, then specify its Codon for MenuItem and specific ID, tags, and command class names in Extension. In this way, SharpDevelop is very good, my plugin appears under the Tools menu. After that, I wrote a SHARPDevelop's resource manager (ResourceEditor) plugin class resourceEditor.dll and hang it under the Tool menu. Similarly, I also wrote a resourceEdIitor.Addin file to correspond. The system is working very normal. If we write such a configuration file for each plugin, the plug-in library file (.dll), plugin configuration file (.addin) is one or one. But this brings a small problem, in such a plug-in-based system, each menu, toolbar button, form, panel are a plugin, then we need to write to each plugin File, this will have a lot of configuration files (it seems a bit too much, not well managed). SharpDevelop also thought of this problem, so it allows us to merge the configuration of multiple plugins in a plug-in configuration file. Therefore, I merged my two plug-in library files into an Addins.dll, and again wrote my plugin profile myaddins.addin as follows:

<

Addin

Name

= "MyAddins"

Author

= "SimonLiu"

Copyright

= "GPL"

URL

= "Http://www.icsharpcode.net"

Description

= "Display AddIntree"

Version

= "1.0.0"

>

<

Runtime

>

<

Import

askSEMBLY

= "../../ bin / addins.dll"

/>

Runtime

>

<

Extension

Path

= "/ SharpDevelop / Workbench / MainMenu / Tools"

>

<

Menuitem

id

= "ResourceEditor"

label

= "Resource Editor"

Class

= "Addins.ResourceEditor.command.ResourceEditorCommand"

/>

<

Menuitem

id

= "AddIntreeView"

label

= "View addINtree"

Class

= "Addins.addintreeView.AddintreeViewCommand"

/>

Extension

>

Addin

>

In this way, I use a plug-in functional module to configure it. Similarly, I can also merge dozens of functional modules into a plug-in configuration file. SharpDevelop called this plugin configuration file as "Addin", and packages the specific functional module to Codon and uses the Command class to package the specific function. SharpDevelop itself The core configuration of SharpDevelopCore.Addin contains all basic menus, toolbars, and PAD plug-in configuration. Let's take a look at it, now we have two trees. First, the plug-in tree itself is a tree structure. This tree is constructed according to the extension path of all Codon of all Codon of the system, indicating the location of each Codon in the plug-in tree, and you can write this small. Small AddintreeView to see the actual structure in SharpDevelop. Second, the plug-in configuration file itself has a tree structure. The root node of this tree structure is the various plug-in configuration files of the system, which is constructed according to the EXTENSION node in this configuration file, which describes each Codon with the extension node. We can take a look at the structure of this tree through Addinscout under SharpDevelop's Tools menu. In order to experiment, I streamline SharpDevelop's plug-in and constitute a simple small plug-in system. Below is a screenshot of two trees of this streamlined system. Everyone can understand the relationship between plug-in trees and plug-in configuration files through these two pairs (just two angles of the same problem, one is Codon's ExtensionPath, one is the content of the configuration file). Summarize the profile format of the SharpDevelop plugin. The first is the node, you need to specify the name of Addin, the properties of the author. Second, in the node under Addin Node, use to specify the library file in which Codon in this plugin configuration is used. If distributed in multiple library files, you can refer to one by one. Then, write the configuration of the specific function module. The configuration of each function module is started with an extension point , and after specifying the path (PATH) attribute, configured in this node to the specific Codon under this extension. Each Codon has different properties depending on the specific implementation. Everyone can study the core profile of SharpDevelop, SharpDevelopcore.Addin, believe it is easy to understand. 2, the core of the plug-in system, TDIN, and ADDINTREE, said in the main function of SharpDevelop, ServiceManager.Service first calls AddintreSingleton's Addintree instance in the initializeservicesubsystem method, and AddINTree initializes this time. Now let's take a look at what addintreesingleton.addree has done something, it defines in the /src/main/core/addins/addintreesingleton.cs file.

public

Static

IaddinTree Addintree

{Get {if (addintree == null) {createaddintree ();} Return AddInTree;}}

AddintReesingleton is a Singleton in the plug-in tree (specific "design mode"), and addintreSingleton.AddIndree is an attribute that returns an IaddIntree interface. Here I noticed that AddINtreSinglet was inherited from DefaultAddIntree. Since it is a single mode, all the methods contain all the static methods, there is no instantiation, and the external is to access the plug-in tree through the AddINtree property, why should I inherit from DefaultAddintree? It seems that there is nothing necessary. This may be a small problem that is missing during the reconstruction process. Let's take a look at the content of the IaddIntree interface. It defines several contents: A. Attribute ConditionFactory ConditionFactory returns a factory class of constructor, where the conditions are the conditions in the plug-in configuration, and we will later in detail later. B, attribute CodonFactory CodonFactory Returns a factory class constructed for Codon. C, attribute addincollection addins Returns the root node AddIn (plugin) collection of the plug-in tree. D, Method IaddinTreenode gettreenode (String Path) Returns the corresponding tree node E, method void insertdin (address), adds a plug-in to the tree according to the extension path in Addin, method Void RemoveAddin (Addin Added a plug-in G, method assembly loadassembly (string assemblyfile) Reads the assembly specified by the Import in the plugin, and constructs the corresponding CodonFactory and CodonFactory classes.

AddintReSinglet calls the createAdDINTree method when calling AddINTree to perform initialization. The CreateAddintree method is implemented in this way:

Addintree

=

New

DefaultAddintree ();

The initialization plug-in tree is the instance of DefaultAddintree, which I feel the traces of reconstruction. First, DEFAULTADDINTREE looks from the name is the default plugin tree (since it is the default, then in other words can be other plug-in trees). But SharpDevelop did not provide an interface to the outside using custom plug-in trees (unless we modify the code here), that is, this name is not as implied as it itself. Second, according to Singleton's usual ways and previous mentioned inherited questions from DefaultAddintree, I guess the content of DEFAULTADDINTREE was originally implemented in AddintReesingleton, and later perhaps the code of code for code, put it out. , Forming a defaultaddintree class. As for the problem of inheriting the DEFAULTADDINTREE, maybe it is a base class of ADDINTREE here. This is the external question, and it has not been confirmed that you can't put it in your heart (interested in finding the old version of the old version of SharpDevelop). There are two lines of viewing code, one is the code of the constructor of the defaultadDree, which constructs Codon and Condtion factory classes in this constructor. The other is the code behind CreateAdd Andree, search the plugin file, and constructs AddIn according to the plugin file. Everyone can choose to take a branch line or choose to look at the main line first (but you will miss a lot of content). 2.1 Branch (DEFAULTADDINTREE constructor) We interrupt the CreateAdDintree code, and jump to the defaultaddintree constructor to see. DEFAULTADDINTREE defines in the /src/main/core/addins/defaultaddintree.cs file. In the constructor of the defaultaddintree, it is noted that it has a modifier INTERNAL, that is, this class only allows the class in this program to instantiate DEFAULTADDINTREE (really embarrassing). The code in the constructor is only one sentence:

Assembly.GetexecutingSemBly ());

Although there is only one line of code, but here is very delicate, it is the key to the overall situation, and I have to write it. First, obtain the ASSEMBLY of the entry program through the global assembly object object, in turn to the loadCodonsandConditions method. In this method, all data types in the incoming Assembly are enumerated. If it is not abstract, and is the subclass of AbstractCodon, and has the corresponding CodonNameAttribute property information, then establishes a corresponding CodonBuilder according to the name of this class and it is added to CodonFactory (the same operation is also made to Condition, we focus on the same operation. Look at the Codon section, Condition is basically the same as CODON). The CodonFactory class and the CodonBuilder class constitute a flexible foundation of the SharpDevelop plugin system, and you can see it carefully. We present examples with examples, with the AddIndreeViewCommand I wrote before. MenuItemcodon is searched in the assembly in the entrance, which is a subclass of AbstractCodon, a codon of the MenuItem (Menu Item) Command. Condition, execute CodonFactory.Addcodonbuilder (

New

CodonBuilder (Type.Fullname, Assembly);

First, CodonBuilder constructed according to the class name Menuitemcodon and Assembly. CodonBuilder defines in the /src/main/core/addins/codons/codonbuilder.cs file. The name MenuItem is obtained according to the MenuItemCodon's CodonNameAttribute property in the constructor of the CodonBuilder. CodonNameAttribute describes the name of the Codon, which is also the tag corresponding to the .addin configuration file, which will post it. In addition to the ClassName (class name) and CODONNAME properties including the Codonbuilder, there is only one method BuildCodon.

public

ICodon BuildCodon (Addin Add)

{ICodon codon; try {// create instance (ignore case) codon = (ICodon) assembly.CreateInstance (ClassName, true); // set default values ​​codon.AddIn = addIn;} catch (Exception) {codon = null;} Return Codon;

Obviously, BuildCodon creates a specific CODON instance based on the Assembly and type ClassName incorporated in the constructor, and associates with the specific addin. After that, CodonFactory calls the AddCodonBuilder method to add this CodonBuilder to its builder collection. Let's look up, see how CodonFactory uses this CodonBuilder. In file /src/main/core/addins/codons/codonfactory.cs, CodonFactory has only two ways. The AddConbuilder method adds the CodonBuilder to a HashTable indexed with CodonName. Another way is very important: public

ICodon Createcodon (Addin Add, XMLNode Codonnode)

{CodonBuilder builder = codonHashtable [codonNode.Name] as CodonBuilder; if (! Builder = null) {return builder.BuildCodon (addIn);} throw new CodonNotFoundException (String.Format ( "no codon builder found for <{0}>" , Codonnode.name);

Here, addin is the description of this configuration file (that is, plugin), and what is the CodonNode of this XMLNode type? Remember the labels of , , under the tab? I have said that these are Codon's description, now we are looking at it. As an example of the aging of the agdree application:

<

Extension

Path

= "/ SharpDevelop / Workbench / MainMenu / Tools"

>

<

Menuitem

id

= "AddIntreeView"

label

= "View addINtree"

Class

= "Addins.addintreeView.AddintreeViewCommand"

/>

Extension

>

SHARPDevelop After reading the tab of the plug-in configuration file, SharPDevelop After the CREATECTORY CREATECODON method is passed in turn of CodonFactory. Here, its childnodes [0] is the node here, that is, the CodonNode parameter. The name of this XML node is Menuitem, so Createcodon's first line

CodonBuilder Builder

=

Codonhashtable [CodonNode.Name]

AS

Codonbuilder;

Find the corresponding CodonBuilder based on the name of the node (Menuitem). Remember the CodonBuilder in front to get Menuitemcodon's CodonName based on CodonnameAttribute? It is this Menuitem. CodonFactory found Codonbuilder's CodonBuilder (this is built in the DEFAULTADDINTREE constructor "to create and join CodonFactory, remember?), Then use this CodonBuilder to establish the corresponding Codon and return it to the caller . In this way, through CodonNameAttribute, SharpDevelop, the node of the AddIn Profile, Codonbulder, MenuItemcodon three parts, form a route for constructing Codon. Let's go back to organize the idea, SharpDevelop works below: a, build each Codon, use CodonNameAttribute to specify it in the configuration node in the configuration node, call the loadCodonsandConditions method in the configuration of the defaultaddintree, search all Codon, according to CodonnameAttribute is established in CodonBuilder to join CodonFactory. C, read the configuration file, travers all the nodes under the tag, and establish the corresponding Codon using CodonFactory according to the NAME of the node. Among them, the NAME of the XML node under the CodonNameAttribute, CodonBuilder's CodonName, and label is consistent. The same is true for the treatment of Condition. Sorry, I am not very convenient to go online, I don't quite map in blog (all excuses for provincial excuses ^), otherwise it may understand the converging relationship here.

Ok, see how the flexibility of the plugin in SharpDevelop is embodied. First, the Codon node name under the ExtensIn configuration is not linked in the code and the specific CODON class, but linked with Codon with CodonNameAttribute. The advantage of this is that SharpDevelop's Codon and XML tags have unlimited extension capabilities. Suppose we have to define a Codon class SplashFormCodon role to specify a form of a form as a system startup. It is very simple to do: First, use CodonNameAttribute to specify CodonName to SPLASH in SplashFormCodon and define the properties you need in SplashFormCodon. Then, write this in the address of the AddIn configuration file:

<

Extension

Path

= "/ Sharpdevelop /"

>

<

Splash

id

= "Mysplashform"

Class

= "Mysplashformclass"

/>

Extension

>

Is not it simple? In addition, the treatment of Condition is also the same, that is, we can also use similar methods to flexibly join your definition conditions.

Here I have a small question: I don't know if I understand the design pattern. I feel that the implementation of the CodonBuilder class does not seem to be as implied by its class name, but it seems Should be Proxy mode, so I think it is easier to understand if it is called CodonProxy? What do you think? In addition, although a little bit is slightly, I think the configuration will make us more easier to associate more than the code: <

Extension

Path

= "/ Sharpdevelop /"

>

<

Codon

Name

= "Splash"

id

= "Mysplashform"

Class

= "Mysplashformclass"

/>

Extension

>

2.2 The main line (AddIte "ingleton. Createaddintree), I am a bit tired. But let's continue the code of CreateAdDintree in AddintDreamingleton. After establishing the instance of defaultadDintree, AddINtreSinglet is searched for files for .addin in the plugin directory. I still remember that ADDINTREESINGLETON. SETDIRECTORIES was called in SharpDevelop's Main function. It is a search for this incoming directory. It seems that SharpDevelop as a plugin for all the suffixes in the plug-in directory as .addin.

FileUtilityService FileUtilityService

=

FileUtilityService) ServiceManager.Services.getService

Typeof

FileUtilityService)))

First learn how to get what you need from ServiceManager, in which a service is available in this way in SharpDevelop. Call GetService Incorporation of the type of service class to get as a parameter, returns an iService interface, and then converts into needed services.

After finding an Addin file, call INSERTDINS to add the configuration in this addin file to the directory tree.

Static

StringCollection Insertdins (StringCollection AddInfiles)

{StringCollection retryList = new StringCollection (); foreach (string addInFile in addInFiles) {AddIn addIn = new AddIn (); try {addIn.Initialize (addInFile); addInTree.InsertAddIn (addIn);} catch (CodonNotFoundException) {retryList.Add (addinfile) {RetryList.add (address);} catch (exception e) {throw new addinitializeException (addinfile, e);}}

INSERTADDINS establishes a corresponding Addin (plugin), calling the addintree's InsertDin method to hang it into the plugin tree. There is a small process here, since it is a class corresponding to the Codon's label in the ASSEMBLY search and the plug-in configuration, and the Assembly in the Codon class is imported through the Import tag. Therefore, when the Codon class corresponding to a Codon tag in the configuration, the file where the CODON class is located is imported in other addin files. At this time, I will find CodonBuilder in the front branch line to find CodonBuilder, so you have to find CodonBuilder correctly after the AddIN processing in the Codon class. This is a problem of depends on relationships. SharpDevelop is a relatively simple, when you call the InsertDins method, when CodonNotFoundException occurs, you will join a RetryList list to return. After CreateAddIntree processing all the addin files, recirculate the ADDIN in the Retrylist list. If you can't succeed in the retrorelist in a certain loop, you will be prompted to fail. Let's look back to see the processing of addIN.

2.2.1 addin.initialize (initialization of addin) After the instance of addIN, call the Initialize method for initialization. Addin is a package for a .addin file, defined in the /src/main/core/addins/addin.cs file. It contains the description of the root element of the .addin file, including the name, author, and copyright. In the node: one is the node, which contains specifies asSembly to import; another is the node, specifying the Codon's extension point. In the Addin.Initialize method, use the XMLDocument object to read the corresponding addin file. First read the basic properties such as Name, Author, and Copyright, which will then traverse all ChildNodes (child nodes).

If the child node is the Runtime node, call the AddRuntiMelibralies method.

Foreach

(

Object

o

in

El.childNodes)

.? {XmlElement curEl = (XmlElement) o; string assemblyName = curEl.Attributes [ "assembly"] InnerText; string pathName = Path.IsPathRooted (assemblyName) assemblyName: fileUtilityService.GetDirectoryNameWithSeparator (path) assemblyName; Assembly asm = AddInTreeSingleton.AddInTree .Loadassembly (pathname); Runtimelibraries [assemblyname] = asm;}

By adding all Codon and Condition subclasses in Assembly into the corresponding Factory class via the addintAassembly method (call the loadCodonsandConditions method, we have seen this file and the corresponding Assembly in the defaultaddintree) In the Runtimelibralies list. If the child node is an Extension node, call the AddExtensions method.

Extension e

=

New

Extension (El.attributes "

"

Path

"

] .Innertext); AddCodonStoextension (E, EL,

New

ConditionCollection (); extensions.add (e);

The XML description of this extension is created in the ExtensIn's extensions list and adds the Codon included to the created Extension object through the AddCodonStoExtension method. The extension object is an inline embedded class, one of which is the list of CodonCollection. AddCodonStoExtension is to save the Codon that appears in the configuration to save it in this list.

Let's take a look at the AddCodonStoExtension method. In the code, I have slightly analyzed the processing of the Condition and some irrelevant parts, we focus on the processing of the plugin. The first is a Foreach (Object O IN EL.CHILDES) traversed all child nodes under , for each child node, as follows:

Xmlelement Curel

=

XMLELEMENT O;

Switch

(curel.name)

{(Processing conditions) default: ICodon codon = AddInTreeSingleton.AddInTree.CodonFactory.CreateCodon (this, curEl); AutoInitializeAttributes (codon, curEl); (codon.InsertBefore processing codon.InsertAfter and, mainly in the processing list codon The order of the order, this is more important for the process of MenuItemCodon) E.CODONCOLLECTION.ADD (CODON); if (curel.childnodes.count> 0) {EXTENSION NEWEXTENSION = New Extension (E.PATH '/' CODON.ID); AddCodonstoextension (NewExtension, Curel, Condition); extensions.add (newextension);} Break;

We have seen a long-awaited call

AddinTreeSingleton.addintree.codonfactory.createcodon

THIS

CUREL);

After the pavement in the above branch 2.1 code, SharpDevelop uses the created CodonFactory, call the CreateCodon method to construct the actual Codon object according to the nodes under , everything is in the case. E.CODONCOLLECTION.ADD (CODON); add constructed Codon objects to the CodonCollection list of the Extension object. Thereafter, SharpDevelop has been processed in this allowed unlimited nested structure such as a menu. If the node has nested child nodes, construct a new Extension object, recursive call addCodonStoExtension Add to this Extension object. Note that this new constructed Extension object is not saved in Codon, but is saved directly in the extension point list of Addin. This is to facilitate finding. After all, it is not used in the specific CODON. We can learn the specific location in the plugin tree through the Path property of the Extension object. 2.2.2 addINTree.Insertdin (Add AddIN Add to AddINTree) After completing the constructor of Addin, you need to add AddInTree objects to AddINTree.

Addins.Add (addin);

Foreach

(Addin.extension Extension

in

Addin.extensions

{AddExtensions (extension);

In DefaultAddIntree, two lessons are saved. One is a tree formed according to the structure of the plugin file, each plugin file as a root node, and the down is an Extension, Codon node. Addins.Add (add); adds the plugin to this tree structure. Another tree is constructed as a path based on the Path Codon of Extension, and each tree node is an AddINTreenode class that contains the Codon object on this path. Codon nested in this node is accessed through its child node. You can specify a path to get a path for a certain node on a plug-in tree in DefaultAddIndreE. The AddExtensions method is very simple, traversed all Codon in Extension, creates all nodes on this path as the path, creates all nodes on this path, and connects Codon to this addINtreenode. Since Codon's ID is a globally unique, each ADDINTREENODE has a unique Codon.

3, the last km (Condon and Command) In the discussion of the plug-in tree, we associate the ADDIN-Extension-Codon configuration with the classes they correspond to the classes. However, we have not involved how Codon and how it contains how it is associated. Because of this association is to call in external plugins tree (remember telling SharpDevelop program entry Main function, method InitializeServicesSubsystem ServiceManager mention of it? AddServices ((IService []) AddInTreeSingleton.AddInTree.GetTreeNode (servicesPath) .BuildChildItems (this). ToARRAY (iService)))))))))))))))) This is called to call it here, so separately here. To achieve this association is the BuildChildItems and buildchildITEM methods of Addintreenode and the Codon's builditem method. BuildChildItem methods and methods BuildChildItems only one word, BuildChildItem is to find the node containing at Codon belongs AddInTreeNode child nodes according Codon specified ID and call BuildItem method Codon; and BuildChildItems is first traversed all belong AddInTreeNode The child node, call the Codon's builditem method of each child node, then call the Codon's builditem method of the ADDINTREENODE (that is, a tree is traversed). Focus on Codon's builditem method. In AbstractCodon, this method is an abstract method, and there is not clear that this method is made in the code annotation of SharpDevelop. But we can find a Codon instance to see. For example, Classcodon's buildItem: PublicItem: Public

Override

Object

BuildItem

Object

Owner, ArrayList Subitems, ConditionCollection Conditions

{System.diagnostics.debug.assert (Class! = Null && class.Length> 0); returniful.createObject (class);}

Call AddIn's CreateObject, incoming Class (class name) of Codon as a parameter, establish an instance of this class. For example this configuration

<

Extension

Path

= "/ Workspace / AutoStart"

>

<

Class

id

= "InitializeWorkBenchCommand"

Class

= "Icsharpcode.sharpDevelop.commands.initializeworkbenchcommand"

/>

Extension

>

The Class attribute in Codon is ICsharpcode.sharpDevelop.commands.initializeWorkBenchCommand. That is, Codon's class refers to the name of the Command class that implements the specific function module. When reading the section in the AddIN configuration, AddINTree saves Assembly to Runtimelibraries, so the CreateObject method can find and create an instance of the class by them. Everyone can look at the implementation of MenuiteMcodon again, and it is also a corresponding SDMENUCOMMAND. Thus, the plugin structure of SharpDevelop itself can be separated from the specific object, and the actual object is established in the builditem of each Codon. Therefore, we can find that there is no well-decoupling effect in SharpDevelop, the entire basic plugin system section does not have any GUI operations. 4, the problem is good, this paper analyzes the analysis of the plug-in tree to this end. I will mention a small question to see the official thinking: In the process of constructing the plug-in tree, if the Codon's node path does not exist (that is to say its dependency does not exist), then SharpDevelop will prompt the failure and terminate the program. run. However, it is actually because of the reasons or permissions deployment, some Codon failures do not affect the use of the entire system, such as the trial version only provides partial plug-in to the customer, and does not want the system to terminate. Then there is a problem that Codon relies on failure and allows you to continue to run. In addition, I hope that the plugins are not transferred in the system when the system is started, but when the running period actually calls the system, it is a caching mechanism so that the thermal deployment of the system plugin can be implemented. How to modify the plugin system of SharpDevelop to implement these two features?

Next book, you should analyze the services in SharpDevelop at a request of a netizen.

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

New Post(0)