C# learners have probably come across a topic called Reflection. It is one of the most difficult topics for beginners. The degree of learning difficulty can be estimated as 5-6 out of 10. (Subjective opinion) In this article, we will answer the questions of what reflection is useful for and how we can use reflection in practice.
What is reflection?
Reflection is the process by which a program defines runtime types and uses them for a specific purpose.
The Serialization you use - Deserialization, Intellisense, Attribute usage, later binding, etc. is all based on reflection.
Let me tell you a very simple fact: Most of you probably use Visual Studio, Visual Studio Code, Sublime text, Chrome, etc. You have downloaded extensions to some programs. Have you ever wondered how we can manipulate these programs without having access to their source code? How does a new menu appear in visual studio? How can something that is not already in the program appear as a new function in the program just by adding an extension? After all, how to add new functionality to the program without opening the source code? See, the answer to these questions is hidden in reflection.
When we write a program in C#, we usually define the types (class, delegate, interface, etc.) before compilation. We know for sure that for example, it is necessary to create an object of this class and call such or such method. Or when adding any or several DLLs to the program, we do Add Reference and add the DLLs to the program and use their types. If any problem occurs, the compiler informs us about it (compile time detection).
However, there are several cases where all processes appear after compilation, depending on the architecture extension style chosen when writing the program. That is, the definition of types, the creation of their object, and even the calling of the necessary methods are determined at runtime.
In one of the companies I worked for, software writing was based on this structure. So we had a big program, and every time we needed new functionality, we did not open and edit the code with 10,000 lines. We just wrote small DLLs, put them in the necessary folder, and after the program was restarted, it could read those DLLs and execute the codes in them. This is itself an architectural approach rule. (Your company may have a different way of writing scalable software)
How to use Reflection in practice?
Please download the source code from here.
As you can see, we have 3 projects in the solution:
A project called ReflectionAppUI is our parent application. We will try to expand this program without opening its source code in the future.
Our DLL file called BankofBaku will be dynamically added to our main program. That is, without using the standard DLL addition procedure (add reference). We will define a directory according to the convention and put the DLLs we wrote in that directory and the main program will read and execute those DLLs from that directory.
Our 3rd project is called ProviderProtocol. This project serves as a bridge for the other two projects. Therefore, both ReflectionAppUI and our BankOfBaku project must reference to ProviderProtocol.
As a convention, we create a libs folder at the same level as the bin folder in the ReflectionAPPUI project. We will drop the DLL files into that folder.
First, let's write our ProviderProtocol, which is our agreement rule. That DLL consists of a very simple protocol. In this protocol, we show the name of the provider and what it will do when clicked.
namespace ProviderProtocol {
public interface IProvider {
string Name {
get;
set;
}
void OnButtonClicked();
}
}
Now let's create a provider that implements this protocol. We will create a provider called BankofBaku in this project. You can add other providers to this solution, as long as those providers implement the Iprovider interface.
public class BankOfBakuProvider: IProvider {
public string Name {
get;
set;
} = "Bank Of Baku";
public void OnButtonClicked() {
//implementation simplified for learning..
MessageBox.Show("This is Bank of Baku provider");
}
}
Finally, let's come to our main project, namely ReflectionAppUI. This is our GUI. The way the project works is very simple. We put the DLL providers we wrote in the libs folder. Those DLLs must implement the IProvider interface. Then, by clicking the "Reload Providers" button in our program, we see that the providers are rendered on the screen. The program loads all the DLLs in the libs folder and checks whether they implement the IProvider interface, and if it implements that interface, it creates an object of the class at runtime and adds a button for each provider to the interface.
public partial class MainForm: Form {
ProviderVisualizer _providerVisualizer;
public MainForm() {
InitializeComponent();
_providerVisualizer = new ProviderVisualizer(grbx_providers);
}
private void MainForm_Load(object sender, EventArgs e) {
//get path to libs folder
string libsPath = ApplicationPath.PathTo("libs");
_providerVisualizer.LoadFrom(libsPath);
}
private void btn_relaod_Click(object sender, EventArgs e) {
_providerVisualizer.ClearProviders();
_providerVisualizer.LoadFrom(ApplicationPath.PathTo("libs"));
}
}
The main functionality of the program is hidden in the ProviderVisualizer class. The implementation of that class is as follows
namespace ReflectionAppUI.Core {
public class ProviderVisualizer {
private readonly Control _control;
private int _locationX;
private int _locationY;
public ProviderVisualizer(Control control) {
_control = control;
InitializeDefaultParams();
}
public void ClearProviders() {
_control.Controls.Clear();
InitializeDefaultParams();
}
private void InitializeDefaultParams() {
_locationX = 20;
_locationY = 34;
}
public void AddProvider(IProvider provider) {
Button button = new Button {
Text = provider.Name,
Size = new Size(150, 100),
Location = new Point(_locationX, _locationY)
};
button.Click += (sndr, args) => {
provider.OnButtonClicked();
};
_locationX += 150;
_control.Controls.Add(button);
}
public void LoadFrom(string path) {
//get path to libs folder
string libsPath = path;
//get only dll files
string[] providers = Directory.GetFiles(libsPath, "*.dll");
//for simplicity excaped LINQ query...
//for every provider ....
foreach(string provider in providers) {
//load it into application RAM..
Assembly assembly = Assembly.LoadFile(provider);
//get all types in assembly
Type[] assemblyTypes = assembly.GetTypes();
foreach(Type assemblyType in assemblyTypes) {
Type type = assemblyType.GetInterface("IProvider", true);
//if current type implemented IProvider interface then..
if (type != null) {
//create instance of class at runtime
IProvider prvdr = (IProvider) Activator.CreateInstance(assemblyType);
this.AddProvider(prvdr);
}
}
}
}
}
}
Now, if we put the DLL files we have already written in the libs folder, we will see that a new button is automatically added, if we take the DLL from that folder and click on the "Reload Providers" folder, then we will see that the button disappears automatically.
Please download the source code from here.