-1

Looking to load up XAML/events/everything connected to a user control for a DLL that is loaded at runtime into another WPF project based on a config file.

So you have 2 DLLs, only one will be loaded based on JSON configuration, however I want all my buttons and everything to work correctly. This is the code I have ATM

User control for Grey DLL

<UserControl x:Class="WPFGreyButtonTest.InstrumentUserControl"
             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:WPFGreyButtonTest"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid>
        <Label x:Name="colourName" Content="GREY" HorizontalAlignment="Left" Height="93" Margin="284,88,0,0" VerticalAlignment="Top" Width="243" FontWeight="Bold" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" FontSize="50" Foreground="#FF8B8B8B"/>
    </Grid>
</UserControl>

No actual code

User control for Purple DLL

<UserControl x:Class="WPFPurpleButtonTest.InstrumentUserControl"
             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:WPFPurpleButtonTest"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid>
        <Label x:Name="colourName" Content="PURPLE" HorizontalAlignment="Left" Height="93" Margin="284,88,0,0" VerticalAlignment="Top" Width="243" FontWeight="Bold" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" FontSize="50" Foreground="#FFDC00FF"/>
        <Button x:Name="button" Content="Button" HorizontalAlignment="Left" Margin="365,253,0,0" VerticalAlignment="Top" Width="75"/>

    </Grid>
</UserControl>

Code for that (both DLLs have the same class name i.e. InstrumentUserControl)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WPFPurpleButtonTest
{
    /// <summary>
    /// Interaction logic for InstrumentUserControl.xaml
    /// </summary>
    public partial class InstrumentUserControl : UserControl
    {
        public static readonly DependencyProperty InnerButtonProperty = DependencyProperty.Register("InnerButton", typeof(Button), typeof(InstrumentUserControl));

        public Button InnerButton
        {
            get { return (Button)GetValue(InnerButtonProperty); }
            set { SetValue(InnerButtonProperty, value); }
        }

        public InstrumentUserControl()
        {
            InitializeComponent();
            InnerButton = button;
        }
    }
}

Main WPF application code

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WPFSandBox
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        UserControl userControl = null;
        InstrumentEnum instrumentType = InstrumentEnum.Invalid;
        string dllToLoad = null;

        // When we first initialize our WPF app, in the constructor we can
        // allow a config file (such as a json) to be read and load up
        // an appropriate user control view
        public MainWindow()
        {
            InitializeComponent();
            ReadJson();
            LoadRunTimeDLL();

            //var ucs = new List<InstrumentUserControl>();
            var ucs = new List<UserControl>();

            ucs.Add(userControl);
            //ucs.Add(new UserControl1());
            //ucs.Add(new UserControl1());
            //ucs.Add(new UserControl1());

            ic.ItemsSource = ucs;
        }

        private void ReadJson()
        {
            using (StreamReader r = new StreamReader("../../Config/Config.json"))
            {
                string json = r.ReadToEnd();
                var jsonData = JsonConvert.DeserializeObject<Dictionary<string, string>>(json);

                foreach (var entry in jsonData)
                {
                    if (entry.Key == "InstrumentType")
                    {
                        Enum.TryParse(entry.Value, out instrumentType);
                    }
                    else if (entry.Key == "DllToLoad")
                    {
                        dllToLoad = entry.Value;
                    }
                }
            }
        }

        private void LoadRunTimeDLL()
        {
            string assemblyName = string.Format("{0}\\{1}.dll", 
                new FileInfo(Assembly.GetExecutingAssembly().Location).DirectoryName, dllToLoad);

            if (assemblyName != null)
            {
                Assembly asm = Assembly.LoadFile(assemblyName);
                Type[] tlist = asm.GetTypes();
                foreach (Type t in tlist)
                {
                    if (t.Name == "InstrumentUserControl")
                    {
                        userControl = Activator.CreateInstance(t) as UserControl;
                        break;
                    }
                }

                if (userControl != null)
                {
                    //contentControl.Content = userControl;
                }
            }
        }
    }
}

And it's MainWindow xaml

<Window x:Class="WPFSandBox.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WPFSandBox"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <!--<Grid>
        <ContentControl Grid.Row="1" x:Name="contentControl" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch"/>
    </Grid>-->
    <ItemsControl x:Name="ic" />
</Window>

Any help would be appreciated on how I can make sure whichever DLL gets loaded, the button works correctly.

I'm trying to load WPF DLLs at runtime. Works fine, but the buttons and having the events tied to the main WPF app is what I don't know how to do

slickchick2
  • 137
  • 9
  • What problem are you having? (I didn't downvote but I'm guessing someone else did based on the same question) – jhilgeman Jul 29 '19 at 14:22
  • yeah (sad times :( ). I'm trying to load WPF DLLs at runtime. Works fine, but the buttons and having the events tied to the main WPF app is what I don't know how to do – slickchick2 Jul 29 '19 at 14:26
  • On an unrelated note, you might consider having all your instrument controls extend from some base class so that your main class can have some common ways of interacting with whatever is loaded and when you're searching the loaded DLL for the desired type, you can search for types that extend from that base class (just to be more specific rather than searching for anything with that name). – jhilgeman Jul 29 '19 at 14:27
  • Well a base class would address that if there are common events. That way all the buttons raise the base class events and your main app can simply subscribe to those common events. Otherwise, you'll have to use some reflection to find the events and subscribe to them. – jhilgeman Jul 29 '19 at 14:30
  • jhilgeman could you provide an example to what you just said + maybe help with the initial question? – slickchick2 Jul 29 '19 at 14:31
  • @jhilgeman if possible of course – slickchick2 Jul 29 '19 at 15:02

1 Answers1

0

Okay, so let's say you have this setup currently:

Solution: MainApp
- Project: MainApp (WPF App) 
- Project: PurpleButton (Class Library) 
- Project: GreyButton (Class Library) 

What I'm talking about is adding another project, like this:

Solution: MainApp
- Project: MainApp (WPF App) 
- Project: MainAppPlugins (Class Library) <-----
- Project: WPFPurpleButtonTest (Class Library) 
- Project: WPFGreyButtonTest (Class Library) 

In the MainAppPlugins library, you add a new "PluginButton" class based on UserControl (but don't add an actual user control with a designer or anything). It might look something like this:

using System;
using System.Windows.Controls;

namespace MainAppPlugins
{
    public abstract class PluginButton : UserControl
    {
        public event EventHandler OnProcessedSomeData;
    }
}

Then you update all the other projects (MainApp, WPFPurpleButtonTest, and WPFGreyButtonTest ) and add a project reference to MainAppPlugins (so that every project knows what "MainAppPlugins.PluginButton" is).

In Purple and Grey, you then change your user controls to extend from MainAppPlugins.PluginButton instead of extending from UserControl. So you do this in two places. First in your code-behind (the .cs file):

namespace WPFGreyButtonTest
{
    public partial class InstrumentUserControl : PluginButton // <-- 
    {
      ...your existing code...
    }
}

And then in your XAML you add an XML namespace reference to the MainAppPlugins assembly and then change the base type to reflect the PluginButton:

<mainappplugins:PluginButton x:Class="WPFGreyButtonTest.InstrumentUserControl"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:mainappplugins="clr-namespace:MainAppPlugins;assembly=MainAppPlugins"
         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:WPFGreyButtonTest"
         mc:Ignorable="d" 
         d:DesignHeight="450" d:DesignWidth="800">
...
</mainappplugins:PluginButton>

At this point, both buttons now extend/inherit from the PluginButton class, so they now both have a common event called OnProcessedSomeData, and both of them can raise that event when appropriate.

You can keep adding common code / events to both buttons simply by modifying the PluginButton class in your MainAppPlugins project and then rebuilding.

Now, when we go back to MainApp and we're looking for types, instead of looking at the name, you can check the type's BaseType:

// if (t.Name == "InstrumentUserControl")
if(t.BaseType == typeof(MainAppPlugins.PluginButton))
{
   ...
}

...and you can also use the more specific common PluginButton type when creating the control:

 // userControl = Activator.CreateInstance(t) as UserControl;
 userControl = Activator.CreateInstance(t) as MainAppPlugins.PluginButton;

...which means the MainApp code can now reference all the common events/code, like our custom OnProcessedSomeData event:

userControl.OnProcessedSomeData += ...your event handler here...

So both grey and purple buttons might have 90% different code, but the other 10% is defined by events, properties, etc... - whatever code and events and properties are found within PluginButton. Since MainApp knows about PluginButton, it can see and interact with either of those two buttons by referencing that common code.

So let's take it a step further and say you wanted both buttons to return data via a GetData() method. In your MainAppPlugins.PluginButton class, you would define a method with a virtual flag:

public abstract class PluginButton : UserControl
{
    ...

    public virtual string GetData()
    {
        return "Default value";
    }
}

And in each button's code-behind you'd override that method to return the button-specific results:

namespace WPFGreyButtonTest
{
    public partial class InstrumentUserControl : PluginButton 
    {
        ...your existing code...

        public override string GetData()
        {
            return "Data from the Grey button";
        }
    }
}

and

namespace WPFPurpleButtonTest
{
    public partial class InstrumentUserControl : PluginButton 
    {
        ...your existing code...

        public override string GetData()
        {
            return "Data from the Purple button";
        }
    }
}

..and in MainApp, you could call userControl.GetData() and it would call the correct, button-specific GetData() call.

My final recommendation for any application that implements dynamically-loaded DLLs/plugins is to think about your application's safety in case a malicious user ever built their own DLL with malicious code in it and then called it WPFPurpleButtonTest.dll and then was able to overwrite your file with theirs.

If there's ANY remote possibility of this situation occurring, I'd strongly recommend you implement some kind of restriction on which DLLs to load unless they are digitally signed by an internal, trusted CA. This recommendation is a much bigger topic, so you'll probably have to do your own research on how to digitally sign a DLL and then check that signature before you load the DLL. That way, your app won't load DLLs that haven't been explicitly trusted by you.

jhilgeman
  • 1,543
  • 10
  • 27
  • Glad to help. Good luck with your project! – jhilgeman Jul 30 '19 at 02:12
  • https://stackoverflow.com/questions/18532574/how-to-set-the-data-context-of-a-grid-in-a-wpf-page-to-an-other-wpf-window if you have any idea about this one i'd appreciate it too! – slickchick2 Jul 30 '19 at 12:48