Click here to Skip to main content
15,936,677 members
Please Sign up or sign in to vote.
3.00/5 (2 votes)
Hi,

I'd like to load a Usercontrol by reflection with the function "Show" in my interface like this :
void Show();


The code for the interface and to load is OK.

Any idea ? Thank you for your great support


Additional question :
I'd like to launch a function in the plugin. IView is implemented the function "void LoadSettings()" for example.
With the list, the selected object is of type "
IUIViewProviderBase
. How to "reach" the object IView ?

What I have tried:

For the moment, the function show is returning the UserControl object to display and the userControl is display in the main application.
UserControl Show();


It's working well but the client doesn't want to main application to see the UserControl object


The interface is like this :

namespace MyApp
{
    public interface IPlugins
    {
        /// <summary>
        /// Function called to receive the UserControl to display
        /// </summary>
        /// <returns>USerControl object to display</returns>
        UserControl Show();

    }
}


The code to load the plugin is :
Assembly dll = Assembly.LoadFrom(dllPath); 

            //Check all classes implemented in the DLL
            foreach (Type type in dll.GetTypes())
            {
                Type[] interfaces = type.GetInterfaces();
                //Check in the class implement an interface IPlugins
                if (interfaces.Contains(typeof(IPlugins)))
                {
                    //Create an instance of the class
                    (IPlugins) iPlugin = (IPlugins)Activator.CreateInstance(type);
                    ...
                }
            }


In the XAML, I'm displaying the UserControl with this :
<Grid Grid.Column="1" Name="UC_Grid" ScrollViewer.HorizontalScrollBarVisibility="Auto" ScrollViewer.VerticalScrollBarVisibility="Auto">

        </Grid>


and in the code :
UC_Grid.Children.Add(myIPlugin.Control); // myIPlugin.Control is the UserControl in the plugin
Posted
Updated 16-Jul-18 17:40pm
v3
Comments
[no name] 2-Jul-18 21:51pm    
Check the "visual tree" in Debug.

Need to confirm the Visibility, Height, etc.

(Assuming you actually added the UC).

As recommended by johannesnestler, I like to use MEF[^] for apps like these.

This is going to be a long answer but hang in there, it will be worth it...

I've made a couple of examples for you so that you have an idea of how you could do it using MEF:
1. Console Solution - This demonstrates how MEF can work;
2. WPF app with modules - This expands on the Console example with autoloading modules

Firstly, here is a simple console app example that shows how MEF works:
C#
internal static class Program
{
    private static void Main()
    {
        Mef.Initialize();
        var process = new Process();
        process.Service?.Print();
        Console.ReadKey();
    }
}

class Process
{
    public Process()
    {
        Mef.ComposeParts(this);
    }

    [Import]
    public DummyService Service { get; set; }

}

[Export]
class DummyService
{
    public void Print()
    {
        Console.WriteLine("Running Service...");
    }
}

MEF does all the heavy lifting for you. [Export] has a matching Import and does it's magic when you ask it to: Mef.ComposeParts(this);. Here is the (shorterned) class that I use to wrap MEF:
C#
public static class Mef
{
    private static object lockObj = new object();
    private static CompositionContainer container;
    private static AggregateCatalog catalog;

    public static void Initialize()
    {
        catalog = new AggregateCatalog();
        container = new CompositionContainer(catalog);
    }

    public static void ComposeParts(params object[] attributedParts)
    {
        lock (lockObj)
            container.ComposeParts(attributedParts);
    }
}

The Initialize() method stitches all the Imports & Exports together.

Now, for the WPF plugins and loading/displaying UserControls, a little bit more work is required. The solution is made up of 3 parts:
1. Common plugin contact Dll
2. Main WPF app
3. The modules containing the UserControls

The Common plugin is made up of:
1. The MEF Helper Class
2. A UIProvider - metadata that describes the plugin used by the main app
3. An IView interface - used to bind the UserControl to the UIProvider
4. An ExportView attribute - For naming unique IViews that are exported

The revised MEF class that finds plugin modules and loads them in realtime:
C#
public static class Mef
{
    private static object lockObj = new object();

    private static CompositionContainer container;

    private static AggregateCatalog catalog;

    private static void Initialize()
    {
        catalog = new AggregateCatalog();
        catalog.Catalogs.Add(new DirectoryCatalog(path: ".", searchPattern: "*.exe"));
        catalog.Catalogs.Add(new DirectoryCatalog(path: ".", searchPattern: "*.dll"));
        container = new CompositionContainer(catalog);
    }

    public static void Initialize(bool? isRecomposable = true)
    {
        Initialize();
        if (isRecomposable == true)
            StartWatch();
    }

    public static void Initialize<T>(T attributedPart, bool isRecomposable)
    {
        Initialize(isRecomposable);
        if (isRecomposable)
            ComposeParts(attributedPart);
        else
            lock (lockObj)
                container.SatisfyImportsOnce(attributedPart);
    }

    public static void ComposeParts(params object[] attributedParts)
    {
        lock (lockObj)
            container.ComposeParts(attributedParts);
    }

    private static void StartWatch()
    {
        var watcher = new FileSystemWatcher() { Path = ".", NotifyFilter = NotifyFilters.LastWrite };
        watcher.Changed += (s, e) =>
        {
            string lName = e.Name.ToLower();
            if (lName.EndsWith(".dll") || lName.EndsWith(".exe"))
                Refresh();
        };
        watcher.EnableRaisingEvents = true;
    }

    public static void Refresh()
    {
        DispatcherHelper.CheckBeginInvokeOnUI(() =>
        {
            foreach (DirectoryCatalog dCatalog in catalog.Catalogs)
                dCatalog.Refresh();
        });
    }
}

// Helper class for dispatcher operations on the UI thread...
// (Abridged version)
// full version: https://github.com/lbugnion/mvvmlight/blob/master/GalaSoft.MvvmLight/GalaSoft.MvvmLight.Platform%20(NET45)/Threading/DispatcherHelper.cs
public static class DispatcherHelper
{
    public static Dispatcher UIDispatcher
    {
        get;
        private set;
    }

    public static void CheckBeginInvokeOnUI(Action action)
    {
        if (action == null)
            return;

        CheckDispatcher();

        if (UIDispatcher.CheckAccess())
            action();
        else
            UIDispatcher.BeginInvoke(action);
    }

    private static void CheckDispatcher()
    {
        if (UIDispatcher == null)
        {
            var error = new StringBuilder("The DispatcherHelper is not initialized.");
            error.AppendLine();
            error.Append("Call DispatcherHelper.Initialize() in the static App constructor.");
            throw new InvalidOperationException(error.ToString());
        }
    }

    public static void Initialize()
    {
        if (UIDispatcher != null
            && UIDispatcher.Thread.IsAlive)
            return;

        UIDispatcher = Dispatcher.CurrentDispatcher;
    }
}

The DispatcherHelper is used to avoid any cross thredding issues.

Now the base UIProvider. This allows for multiple types like UserControl and Page types and describes each plugin to the hosting app:
C#
public interface IUIProviderBase
{
    string Key { get; }
    string Title { get; }
}

public abstract class UIProviderBase : IUIProviderBase
{
    public string Key { get; set; }
    public string Title { get; set; }
}

And base UIViewProviderBase implementation that has a method for providing the UserControl, the marker interface IView, and a custom Export attribute for the UserControl:
C#
public interface IUIViewProviderBase : IUIProviderBase
{
    ExportFactory<IView> Entry { get; set; }
}

public abstract class UIViewProviderBase : UIProviderBase, IUIViewProviderBase
{
    public abstract ExportFactory<IView> Entry { get; set; }
}

public interface IView
{
}

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class ExportViewAttribute : ExportAttribute
{
    public ExportViewAttribute(string contractName)
        : base(contractName, typeof(IView))
    {
    }
}

Now we can create standalone DLL/EXE plugin modules. The plugin module needs to reference the Common plugin contact Dll above. The DLL should either be a Wpf UserControl or Wpf Custom Control project type.

Here is one Plugin example. Make as many as you want to test with...

First the plugin UIProvider to describe the plugin with the factory method for the view (UserControl):
C#
[Export(typeof(IUIViewProviderBase))]
public class UIProvider : UIViewProviderBase
{
    public UIProvider()
    {
        Key = "PluginA";
        Title = "My Plugin A";
    }

    [Import("MyViewA")]
    public override ExportFactory<IView> Entry { get; set; }
}

Now the plugin View (UserControl) itself:
C#
[ExportView("MyViewA")]
public partial class View : UserControl, IView
{
    public View()
    {
        InitializeComponent();
    }
}

And here is a mock XAML for testing purposes:
XML
<UserControl x:Class="PluginA.View"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:PluginA"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Viewbox>
        <TextBlock Text="ViewA"/>
    </Viewbox>
</UserControl>

Lastly, the WPF Host app. I like to use the MVVM design pattern, but code behind can work just as well. Make sure that you also add a reference to the Common plugin contact Dll above.

First the ViewModel to host the plugins for the main view/window:
C#
public class MainViewModel : IPartImportsSatisfiedNotification
{
    public MainViewModel()
    {
        Mef.Initialize(this, isRecomposable: true);
    }

    [ImportMany(typeof(IUIViewProviderBase), AllowRecomposition = true)]
    public ObservableCollection<IUIViewProviderBase> Plugins { get; set; }

    public void OnImportsSatisfied()
    {
        // uncomment if you need to do something when plugin(s) is/are loaded
        //Debugger.Break();
    }
}

We need a UIExportViewConverter class to load the plugin View (UserControl)
C#
class UIExportViewConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return ((ExportFactory<IView>) value).CreateExport().Value;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Now we can bind the MainViewModel to the MainWindow and display our plugins:
XML
<Window
    x:Class="PluginHost.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    mc:Ignorable="d"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:PluginHost"
    xmlns:converter="clr-namespace:PluginHost.Converters"

    Title="MEF PLUGIN HOST EXAMPLE" Height="450" Width="800">

    <Window.DataContext>
        <local:MainViewModel/>
    </Window.DataContext>

    <Window.Resources>
        <converter:UIExportViewConverter x:Key="UIExportViewConverter"/>
        <DataTemplate x:Key="ItemTemplate">
            <ContentControl Height="100" 
                            Content="{Binding Entry,
                Converter={StaticResource UIExportViewConverter}}"/>
        </DataTemplate>
        <DataTemplate x:Key="DetailsTemplate">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="TITLE: " FontWeight="Bold"
                           Margin="0 0 10 0"/>
                <TextBlock Text="{Binding Title}"/>
            </StackPanel>
        </DataTemplate>
    </Window.Resources>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <ItemsControl ItemsSource="{Binding Plugins}"
                      ItemTemplate="{StaticResource ItemTemplate}"/>

        <ListBox Grid.Row="1"
                 Margin="10"
                 Height="100"
                 ItemsSource="{Binding Plugins}"
                 ItemTemplate="{StaticResource DetailsTemplate}"/>

        <TextBlock Grid.Row="2"
                   Margin="10"
                   Text="{Binding Plugins.Count, FallbackValue=Total: 0, StringFormat=Total: {0}}"/>
    </Grid>
</Window>

The plugin host app will show the loaded plugins, the plugin metadata from the UIProvider and a total count of plugins loaded.

When you run the app, the plugins are not loaded. Copy PluginA.DLL into the app folder and the plugin will automatically load and display. The same thing will happen for every other plugin that you drop into the app folder.

Hope this helps... Enjoy! :)

*UPDATE:* This may also be of interest to you: MEF and AppDomain - Remove Assemblies On The Fly[<a href="https://www.codeproject.com/Articles/633140/MEF-and-AppDomain-Remove-Assemblies-On-The-Fly" target=_blank" title="New Window">^]
 
Share this answer
 
v4
Comments
canard29 5-Jul-18 17:04pm    
Thank you for your answer !! I'm working on it now
canard29 6-Jul-18 11:29am    
I want first to thank @Graeme_Grant for his great suppport in this article

I copied the code, added the reference and everything is working well !
I have an additional question. I want to have access to the selected listBox item (IView) to call a function inside.

What I have tried:

In MainView.xaml

<ListBox Grid.Row="1" Margin="10" Height="100"
Name="PluginList"
ItemsSource="{Binding Plugins}"
ItemTemplate="{StaticResource DetailsTemplate}"
SelectionChanged="ListBox_SelectionChanged"/>


in MainWindow.xaml.cs

private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
IUIViewProviderBase selectedPlugin = PluginList.SelectedItem as IUIViewProviderBase;
ExportFactory<iview> Iviews = selectedPlugin.Entry;
IView item = Iviews.CreateExport().Value as IView;

}


The problem is in the last line, CreateExport is creating a new instance of Iview...I don't reach the existing instance.
Graeme_Grant 6-Jul-18 12:25pm    
This is a different issue to the original problem and requires a new question to be asked.

What you are trying to do breaks all rules where plugins are involved. The host should not know the inner workings of the plugin as they are supposed to be loosely coupled.

But quickly, you have two choices for sharing data:
1. Use a messaging service like the one found in MVVMLight;
2. Continue to use MEF to share data like we have above.
Have a look at MEF - it may is exactly what you need...
Managed Extensibility Framework (MEF) | Microsoft Docs[^]
 
Share this answer
 
Comments
Graeme_Grant 5-Jul-18 1:17am    
Beat me to it ... an example would have helped him ... I added one below.
The sample is working well and it's integrated in my app. I learnt a lot with your very good sample. I'm trying to solve 2 issues, not very related to MEF :
- The userControl is displayed in the top left corner. I'd like to display it in the center of the ContentControl
- If the usercontrol is larger than the app size, scroll bars are not displayed.

I tried to fix these issues with XAML but I didn't find yet a solution but if yo have some tips...
 
Share this answer
 
Comments
Nelek 17-Jul-18 1:30am    
Please stop posting "solutions" to chat with the people. If you want to tell them something, use the "have a question or comment" button below each solution or the "reply" button below each already existent comment. This way they will get a notification that you answered them and the probability to be read is much bigger.
Nelek 17-Jul-18 1:32am    
Additionally, if you are now having problems with another thing. I would recommend you to do a fast search to check if already answered in CP and if not, start a new thread. To avoid mixing topics keeps threads clean and it is easier to find things when searching for something concrete.

This content, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



CodeProject, 20 Bay Street, 11th Floor Toronto, Ontario, Canada M5J 2N8 +1 (416) 849-8900