📜 ⬆️ ⬇️

Object-Oriented Gin Installer Development

Link to the first part
Link to the second part
Link to the third part

Data input



Any installer should give the user the opportunity to enter some starting parameters, for example, the path to the folder where the program will be installed, the database connection string, etc. Moreover, I would like it to be not just text fields, but fields that enable convenient water data. If this is the program installation path, then in addition to the text field there should be a “Browse ...” button, if it is a connection string to the database, then let there be a button to select or create a data source, etc.

We implement input forms in the form of a command:
')
public class UserInputCommand: Command { public List<UserInputControl> InputControls { get; set; } public string FormCaption { get; set; } public override void Do(ExecutionContext context) { foreach (UserInputControl input in InputControls) { Control control = input.Create(); //       context.InputForm.Controls.Add(control); } } } 

We have added the InputControl property to the execution context (I still don’t know where I will initialize it), this is a container control to which we will add custom controls.
The UserInputControl class also appeared with the Create abstract method:
 public abstract class UserInputControl { public string ResultName { get; set; } public int Height { get; set; } public abstract Control Create(); } 

This is an abstract class from which we will inherit all the specific user controls, such as for example UserInputTextBox — a simple text input field:
 public class UserInputTextBox : UserInputControl { public string Caption { get; set; } public string InitialValue { get; set; } public override Control Create() { TextBox textbox = new TextBox(); return control; } } 

This is just a simplified code. In fact, in addition to the input text field, there will still be a Label displaying the Caption header, and these two controls will be placed in the Panel, which itself will return as a result of the Create method. Following this pattern, inheriting from UserInputControl, we will create all other user controls.
Plus, the input form should wait for the end of user input, which I implemented by running the main control flow of the installer in a separate thread with continued execution by clicking on the Next button.
Serialization
I chose the XML serialization format because it is well supported by the NET platform. I will use the System.Xml.Serialization.XmlSerializer class, because of its ease of use, serializing an object in it requires writing just three lines of code, with additional flexibility (if necessary) achieved using attributes from the System.Xml.Serial namespace .
Here is the code:
 //  XmlSerializer ser = new XmlSerializer(typeof(T)); FileStream stream = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.Write); ser.Serialize(stream, obj); // XmlSerializer ser = new XmlSerializer(typeof(T)); stream = new FileStream(filePath, FileMode.Open, FileAccess.Read); result = (T)ser.Deserialize(stream); 

As you can see, the following are used as input arguments: the path to the file where the serialized object will be written; the object itself; and the type of object being serialized. This code is great for serialization not inherited from other classes. But if we are going to serialize, for example, the class CreateFile, the inheritance hierarchy of which is CreateFile <- TransactionalCommand <- Command, then at T = Command, the serializer will tell us to use the XmlInclude attribute indicating the serializer, all possible nested and parent classes used in the object being serialized. And it would not be so problematic if these attributes would not need to be applied to the base class Command, specifying for it one XmlInclude attribute for all its possible heirs. And since I assume that users will increase the collection of commands as plug-ins, without having access to the source code of the Command class, this means that the serializer will not be able to serialize all these user-added classes. Fortunately, there is a way out. All Included types can be specified not only by attributes, but also by direct arguments of the XmlSerializer constructor, to which the second argument can be supplied with an array of included-types Type [].
 Type[] types; XmlSerializer ser = new XmlSerializer(typeof(T), types); 

This means that when creating an instance of a serializer, we need to have information about all the types used. That is some meta information about the installer.
Plugins and Metadata
I will enter for this class GinMetaData
 public class GinMetaData { public List<ExternalCommand> Commands { get; private set; } public Type[] IncludedTypes { get; private set; } public static GinMetaData GetInstance(); public void Plugin(string folderPath) public ExternalCommand GetCommandByName(string name) } 

I suppose that in the application there will always be only one instance of this class, which means we will implement it as a singleton. In this case, the IncludedTypes property will contain all types that can be used by the serializer, with types from both the installer’s main library and all its plug-ins. To connect to the metadata of new plug-ins, the Plugin method is used, which accepts as input the path to the folder with plug-ins. If you call it several for different folders, then all NET-libraries will be connected to the application as plug-ins (unless of course they contain new commands and helper classes).
Links to all commands available to the installer are loaded into the GinMetaData.Commands list. To do this, I created the LoadCommandsFrom (Assembly) method in the GinMetaData class:
 private void LoadCommandsFrom(Assembly assembly) { foreach (Type type in assembly.GetTypes()) { if (ExternalCommand.ContainsCommand(type)) { ExternalCommand cmd = new ExternalCommand(type); Commands.Add(cmd); _includedTypes.Add(cmd.CommandType); } } } 

It remains to consider the ExternalCommand class used in this code. Here is its interface:
 public class ExternalCommand { public ExternalCommand(Type type) public Type CommandType { get; private set; } public Command Instance { get; private set; } public PropertyInfo[] Properties { get; private set; } public ConstructorInfo Constructor { get; private set; } public CommandMetadata Metadata { get; private set; } public static bool ContainsCommand(Type type) public object GetProperty(string propertyName) public void SetProperty(string propertyName, object value) public ExternalCommand Clone() } 

Each instance of ExternalCommand is essentially a wrapper around an instance of each of the classes — descendants of the Command class. The ExternalCommand class has one constructor with a Type argument - a type loaded from the .NET assembly. Since in the connected assemblies, in general, there can be not only commands, but also any auxiliary classes, before trying to create an instance of Command from the type loaded from the assembly, you need to check whether the loadable type is a valid command. The static method ContainsCommand (Type) just checks the loadable type for compliance with all formal requirements - the type must inherit from Command or from any of its successors, the type must not be abstract, the type must have a default constructor (without it, serializers do not work), except In addition, the type should not be marked with the GinIgnoreType attribute (I introduced the attribute specifically to allow plugin developers to mark those types that should not be exported from the plugin to the installer).
The CommandType property stores the Type of the reflection of the loaded command, the Instance property stores an instance of the loaded command created using the Constructor constructor. The Properties property provides an array of command properties. Since the command instance is stored in ExternalCommand as a link to Command - the parent class of all commands, the interface of which has only the Do () method and nothing else, and therefore the properties (properties) of this instance are not available directly, so all the properties I exported directly as array propertyInfo. But this property is also needed mainly for listing properties. In order to set and read each specific property, the ExternalCommand class has two methods: GetProperty (string key), and SetProperty (string key, object value).
The Metadata property is any metadata about the loaded command. I meant that this metadata will be used to display commands in the interface of the visual designer of packages. It is not the topic of this article, but it is necessary to imply its existence. In the command metadata, you can store command parameters such as its name, its description, the way it is displayed in the package designer, etc. At the moment, the command metadata contains only two parameters: the name of the command, its description and the name of the group of commands:
 public class CommandMetadata { public string Name { get; set; } public string Desription { get; set; } public string Group { get; set; } } 

A group of commands, this is a text string - the name of the grouping command node in the interface of the package designer. It is necessary to structure a large number of commands in the list into groups, such as, for example: file operations, IIS management, SQL commands, structuring commands. And other groups.
Metadata is attached to the team using attributes. As long as I have only one metadata attribute of the GinNameAttribute command, here is its description:
 [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] public class GinNameAttribute : Attribute { public string Name { get; set; } public string Description { get; set; } public string Group { get; set; } } 

It applies to the command class, like this:
 [GinName( Name = "-", Description = "  if-then-else", Group = " ")] public class ExecuteIf : TransactionalCommand, IContainerCommand { // …… } 

When each command is loaded into an ExternalCommand instance, its command also reads its attributes, including the GinNameAttribute attribute, which is then converted to an instance of the CommandMetadata class.
The source code of the installer, as well as three typical scenarios for its use (package creation, execution and rollback) I put in the repository on google-code . You can use it for your own purposes. I think that this was the last post on the design of the installer.

Source: https://habr.com/ru/post/135310/


All Articles