Simple Plugin Architecture Using Reflection With WPF Projects

Introduction

 
In this article we will see simple plugin architecture using reflection. I would recommend to use this plugin architecture for smaller scale applications. For larger level applications we can use the frameworks provided by Microsoft MEF (Managed Extensibility Framework). I will write an  article  about that in the future.
 
We will take an example of building a calculator application.
 
Create project of type Class Library and name it as Plugger. It will be responsible for connecting our main calculator application to sublibraries of calculator.
In this project we will add interface with name IPlugger as below:
  1. using System.Windows.Controls;  
  2.   
  3. namespace Plugger.Contracts  
  4. {  
  5.     public interface IPlugger  
  6.     {  
  7.         /// <summary>  
  8.         /// Name of plugger  
  9.         /// </summary>  
  10.         string PluggerName { getset; }  
  11.   
  12.         /// <summary>  
  13.         /// It will return UserControl which will display on Main application  
  14.         /// </summary>  
  15.         /// <returns></returns>  
  16.         UserControl GetPlugger();  
  17.     }  
  18. }  
Now we will create a plug board where all plugs are connected to the application. For this create a WPF Project and see the below code:
  1. using Plugger.Contracts;  
  2. using System;  
  3. using System.Configuration;  
  4. using System.Diagnostics;  
  5. using System.IO;  
  6. using System.Linq;  
  7. using System.Reflection;  
  8. using System.Windows;  
  9. using System.Windows.Controls;  
  10. using System.Windows.Input;  
  11.   
  12. namespace PlugBoard.Calculator  
  13. {  
  14.     /// <summary>  
  15.     /// Interaction logic for MainWindow.xaml  
  16.     /// </summary>  
  17.     public partial class MainWindow : Window  
  18.     {  
  19.         public MainWindow()  
  20.         {  
  21.             InitializeComponent();  
  22.             LoadView();  
  23.         }  
  24.   
  25.         /// <summary>  
  26.         /// Load all IPlggers available in PlugBoard Folder  
  27.         /// </summary>  
  28.         public void LoadView()  
  29.         {  
  30.             try  
  31.             {  
  32.                 //Configure path of PlugBoard folder to access all calculate libraries   
  33.                 string plugName = ConfigurationSettings.AppSettings["Plugs"].ToString();  
  34.                 TabItem buttonA = new TabItem();  
  35.                 buttonA.Header = "Welcome";  
  36.                 buttonA.Height = 30;  
  37.                 buttonA.Content = "You welcome :)";  
  38.                 tabPlugs.Items.Add(buttonA);  
  39.   
  40.                 var connectors = Directory.GetDirectories(plugName);  
  41.                 foreach (var connect in connectors)  
  42.                 {  
  43.                     string dllPath = GetPluggerDll(connect);  
  44.                     Assembly _Assembly = Assembly.LoadFile(dllPath);  
  45.                     var types = _Assembly.GetTypes()?.ToList();  
  46.                     var type = types?.Find(a => typeof(IPlugger).IsAssignableFrom(a));  
  47.                     var win = (IPlugger)Activator.CreateInstance(type);  
  48.                     TabItem button = new TabItem  
  49.                     {  
  50.                         Header = win.PluggerName,  
  51.                         Height = 30,  
  52.                         Content = win.GetPlugger()  
  53.                     };  
  54.                     tabPlugs.Items.Add(button);  
  55.                 }  
  56.             }  
  57.             catch (Exception ex)  
  58.             {  
  59.                 MessageBox.Show(ex.Message, "Internal Error", MessageBoxButton.OK, MessageBoxImage.Error);  
  60.             }  
  61.   
  62.         }  
  63.   
  64.         private string GetPluggerDll(string connect)  
  65.         {  
  66.             var files = Directory.GetFiles(connect, "*.dll");  
  67.             foreach (var file in files)  
  68.             {  
  69.                 if (FileVersionInfo.GetVersionInfo(file).ProductName.StartsWith("Calci"))  
  70.                     return file;  
  71.             }  
  72.             return string.Empty;  
  73.         }  
  74.   
  75.         private void BtnClose_Click(object sender, RoutedEventArgs e)  
  76.         {  
  77.             Close();  
  78.         }  
  79.   
  80.         private void Window_MouseDown(object sender, MouseButtonEventArgs e)  
  81.         {  
  82.             if (e.ChangedButton == MouseButton.Left)  
  83.                 this.DragMove();  
  84.         }  
  85.     }  
  86. }  
Create PlugBoard folder in solution and in App.Config add key to access the PlugBoard Folder. PlugBoard is the folder were all calculator libraries will be placed.
  1. <?xml version="1.0" encoding="utf-8" ?>  
  2. <configuration>  
  3.     <startup>   
  4.         <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7" />  
  5.     </startup>  
  6.   <appSettings>  
  7.     <add key="Plugs" value="Add Path Of Plugbaord\PlugBoard\"/>  
  8.   </appSettings>  
  9. </configuration>  
The Solution Explorer looks like below now:
 
 
 
This is the first snap of the Main window:
 
 
 
Now it is time to create plugins for the calculator plug board. Create a project of type class library. We will name it as “RegularCalculator”.
 
Delete Class1.cs and Create View folder.
 
Add a WPF UserControl to it with name RegularView.
 
Add Plugger Project as a reference to this project, the project structure looks like below:
 
 
In RegularView.xaml, provide regular calculator operation implementation as below:
  1. <UserControl x:Class="RegularCalculator.View.RegularView"  
  2.              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"  
  3.              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"  
  4.              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"   
  5.              xmlns:d="http://schemas.microsoft.com/expression/blend/2008"   
  6.              xmlns:local="clr-namespace:RegularCalculator.View"  
  7.              BorderBrush="Silver" BorderThickness="2"  
  8.              mc:Ignorable="d"  Height="150" Width="600">  
  9.     <UserControl.Resources>  
  10.         <Style TargetType="Button">  
  11.             <Style.Triggers>  
  12.                 <DataTrigger Binding="{Binding ElementName=txtA, Path=Text}" Value="">  
  13.                     <Setter Property="IsEnabled" Value="False"/>  
  14.                 </DataTrigger>  
  15.                 <DataTrigger Binding="{Binding ElementName=txtB, Path=Text}" Value="">  
  16.                     <Setter Property="IsEnabled" Value="False"/>  
  17.                 </DataTrigger>  
  18.             </Style.Triggers>  
  19.         </Style>  
  20.     </UserControl.Resources>  
  21.     <Grid>  
  22.         <Grid.RowDefinitions>  
  23.             <RowDefinition Height="30"/>  
  24.             <RowDefinition Height="50"/>  
  25.             <RowDefinition Height="50"/>  
  26.         </Grid.RowDefinitions>  
  27.         <Grid Grid.Row="0" Background="Orange">  
  28.             <Label Content="Regular Calci" Foreground="White" FontWeight="Bold"/>  
  29.         </Grid>  
  30.         <Grid Grid.Row="1">  
  31.             <StackPanel Orientation="Horizontal" Margin="5" HorizontalAlignment="Center">  
  32.                 <TextBox Name="txtA" Height="30" Width="100" PreviewTextInput="AllowOnlyNumbers"/>  
  33.                 <TextBox Name="txtB" Height="30" Width="100" Margin="5" PreviewTextInput="AllowOnlyNumbers"/>  
  34.                 <Label Name="lblRes" Height="30" Margin="5" Content="0"/>  
  35.             </StackPanel>  
  36.         </Grid>  
  37.         <Grid Grid.Row="2" Margin="5" HorizontalAlignment="Center">  
  38.             <StackPanel Orientation="Horizontal">  
  39.                 <Button Name="btnAdd" Height="30" Width="100" Content="Add" Background="{x:Null}" Click="BtnAdd_Click"/>  
  40.                 <Button Name="btnSubstract" Height="30" Width="100" Content="Substract" Margin="5" Background="{x:Null}" Click="BtnSubstract_Click"/>  
  41.                 <Button Name="btnMultiply" Height="30" Width="100" Content="Multiply" Margin="5" Background="{x:Null}" Click="BtnMultiply_Click"/>  
  42.                 <Button Name="btnDivide" Height="30" Width="100" Content="Divide" Margin="5" Background="{x:Null}" Click="BtnDivide_Click"/>  
  43.             </StackPanel>  
  44.         </Grid>  
  45.     </Grid>  
  46. </UserControl>  
In RegularView.xaml.cs, for RegularView class inherit the interface IPlugger and provide implementation as below:
  1. using Plugger.Contracts;  
  2. using System.Text.RegularExpressions;  
  3. using System.Windows;  
  4. using System.Windows.Controls;  
  5. using System.Windows.Input;  
  6.   
  7. namespace RegularCalculator.View  
  8. {  
  9.     /// <summary>  
  10.     /// Interaction logic for RegularView.xaml  
  11.     /// </summary>  
  12.     public partial class RegularView : UserControl, IPlugger  
  13.     {  
  14.         public RegularView()  
  15.         {  
  16.             InitializeComponent();  
  17.         }  
  18.          
  19.         /// <summary>  
  20.         /// This is name will display in main plug board  
  21.         /// </summary>  
  22.         public string PluggerName { getset; } = "Regular";  
  23.   
  24.         /// <summary>  
  25.         /// This will get called when user clicked on Regular option from plug board  
  26.         /// </summary>  
  27.         /// <returns></returns>  
  28.         public UserControl GetPlugger() => this;  
  29.   
  30.         private void BtnAdd_Click(object sender, RoutedEventArgs e)  
  31.         {  
  32.             lblRes.Content = int.Parse(txtA.Text) + int.Parse(txtB.Text);  
  33.         }  
  34.   
  35.         private void BtnSubstract_Click(object sender, RoutedEventArgs e)  
  36.         {  
  37.             lblRes.Content = int.Parse(txtA.Text) - int.Parse(txtB.Text);  
  38.         }  
  39.   
  40.         private void BtnMultiply_Click(object sender, RoutedEventArgs e)  
  41.         {  
  42.             lblRes.Content = int.Parse(txtA.Text) * int.Parse(txtB.Text);  
  43.         }  
  44.   
  45.         private void BtnDivide_Click(object sender, RoutedEventArgs e)  
  46.         {  
  47.             lblRes.Content = int.Parse(txtA.Text) / int.Parse(txtB.Text);  
  48.         }  
  49.   
  50.         private void AllowOnlyNumbers(object sender, TextCompositionEventArgs e)  
  51.         {  
  52.             Regex regex = new Regex("[^0-9]+");  
  53.             e.Handled = regex.IsMatch(e.Text);  
  54.         }  
  55.   
  56.     }  
  57. }  
We are implementing IPlugger here, because when we load Main Calculator application, the main application looks for class which is being inherited by IPugger and gets the plugger name by property PluggerName and loads User control on main app using method GetPlugger().
 
And in go to project Properties -> Application -> Assembly Information update the Product Name as Calci. While loading this assembly in main app, in this dll we need to check for IPlugger interface. So even if we have some other assemblies in a project which is not related to our project, we can skip it for loading on UI. Below is the snap of Assembly Information:
 
 
 
Now again go to project Properties -> Build and change the Outpath to PlugBoard folder. As we discussed earlier, PlugBoard is the folder where we are placing all our assemby dlls and in the main application App.Config we are configuring the path of PlugBoard.
 
 
 
Build the entire solution and set “PlugBoard.Calculator” project as startup project. Run the application, below is the output snap of window. Here you can see “Regular” is added:
 
 
 
We will follow the same process for creating projects with names “TableCalculator” and “TrigonometryCalculator” and below is the output snap of window. Now you can see two  more options plugged to the main calculator application with the names “Table Calci” and “Trigonometry”.
 
 
 
 
 
For your reference, Project Solution is uploaded and you can download it. Before running the project remember to configure PlugBoard folder path in App.Config.
 
This is how we can just keep on adding plugs we need and building the project. After delivering it to the customer, if the customer requires more option we can keep on extending the calculator functionalities without touching the previous code. So basically it will obey the Open/Close Principle of solid principles; i.e. open for extension, close for modification.
 

Summary

 
In this article we have seen, how we can develop plug and play solutions to meet our requirements which need extensibility.