Laden...

MDI-ChildForm aus Plugin.dll laden

Erstellt von florian0 vor 11 Jahren Letzter Beitrag vor 11 Jahren 2.267 Views
F
florian0 Themenstarter:in
5 Beiträge seit 2010
vor 11 Jahren
MDI-ChildForm aus Plugin.dll laden

Hiho,

ich hab mal wieder ein Problem, dass offensichtlich sonst keiner hat.

Ich habe ein Plugin-System gebastelt.
Zielsetzung + Status:

  • Plugins zur Laufzeit laden und entladen (gelöst)
  • Exceptions in Plugins werden abgefangen und verursachen keinen AppCrash (im Kopf gelöst -> UnhandledException-Event)
  • Jedes Plugin bringt ein Mdi-Child ins Programm mit ein (fail)

Zu meiner Verteidigung vorneweg: Ich habe bisher nur wenig mit Assembly, AppDomain, Reflection und Serializing gemacht. Daher verzeiht bitte so manche dumme Frage oder Idee.

So funktioniert mein Plugin-System bisher:
(Jedes Plugin bekommt eine eigene AppDomain, sonst kann ich das Plugin nicht während der Laufzeit entladen)

  1. AppDomain erstellen
  2. Plugin in die AppDomain laden
  3. Name der Klasse, die das Interface implementiert, speichern (AppDomain.SetData)
  4. AppDomain.CreateInstanceFromAndUnwarp (hierfür der Klassen-Name aus Punkt 3)
  5. Zugriff auf das Plugin über die in 4. erstellte Instanz

Wie bekomme ich jetzt die Form da raus?
Ich hatte versucht diese in der zweiten AppDomain zu instanzieren und den MdiParent auf die MainForm zu setzen ... daraus resultiert:

Fehlermeldung:
Der Typ "System.Windows.Forms.MdiClient+ControlCollection" in Assembly "System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" ist nicht als serialisierbar gekennzeichnet.

Ebenso, wenn ich die Form im Plugin selbst instanziere.

(Wenn ich Show() aufrufe, wird die Form problemlos angezeigt, aber eben nicht als MdiChild)

// Create AppDomainSetup-Helper
System.AppDomainSetup appDomainSetup = new System.AppDomainSetup();

// Setup initializer
appDomainSetup.AppDomainInitializer = new AppDomainInitializer(InitializerFunc);

// Set parameters for initializer
appDomainSetup.AppDomainInitializerArguments = new string[] { _fileInfo.FullName };

// Security-Stuff
Evidence adevidence = System.AppDomain.CurrentDomain.Evidence;

// Create AppDomain
_appDomain = System.AppDomain.CreateDomain(_fileInfo.Name, adevidence, appDomainSetup);

// Get ClassName (set by AppDomainInitializer in Func: "InitializerFunc")
string PluginType = (string)_appDomain.GetData("PluginName");
string FormType = (string)_appDomain.GetData("FormName");

// Create instance of Plugin-Interface
//_instance = (IPlugin)_appDomain.CreateInstanceFromAndUnwrap(_fileInfo.FullName, PluginType);
_instance = (IPlugin)_appDomain.CreateInstanceFromAndUnwrap(_fileInfo.FullName, PluginType, false, BindingFlags.Default, null, null, null, null, null);

_instance.Initalize(new PluginParams());

// Versuch 1: 
_form = (frmPlugin)_appDomain.CreateInstanceFromAndUnwrap(_fileInfo.FullName, FormType);
_form.MdiParent = Program.mainForm; // <- Error

// Versuch 2:
_instance.form.MdiParent = Program.mainForm; // <- Error
private static void InitializerFunc(string[] args)
{
    // Get instance of actual appdomain (Your new AppDomain)
    AppDomain mydom = AppDomain.CurrentDomain;

    // Load Assembly from File
    Assembly asm = Assembly.LoadFrom(args[0]);

    // Check all types in the assembly for the implementation of IPlugin
    foreach (Type type in asm.GetTypes())
    {
        Type typeInterface = type.GetInterface(typeof(IPlugin).ToString(), true);

        // Check if communication-interface
        if (typeInterface != null)
        {
            // Save name of class
            mydom.SetData("PluginName", type.FullName);
        }
        else if (type.BaseType.Equals(typeof(frmPlugin)))
        {
            mydom.SetData("FormName", type.FullName);
        }
    }
}

// Gekürzt
public class TestPlugin : MarshalByRefObject, IPlugin
{
    frmTest test;

    public frmPlugin form
    {
        get { return test; }
    }

    public void Initalize(PluginParams param)
    {
        test = new frmTest();
    }

}

"Darth Maim" hatte im vorherigen Thread schon geantwortet. Die Vorschläge habe ich auf den aktuellen Code angewendet (Bis auf FileInfo, ich weis nichtmehr warum aber es hatte einen Grund warum ich das genommen habe ...). Auf seine Bitte hin, kommt hier noch der Stacktrace:

   bei System.Windows.Forms.Control.get_Controls()
   bei System.Windows.Forms.Control.set_ParentInternal(Control value)
   bei System.Windows.Forms.Form.set_ParentInternal(Control value)
   bei System.Windows.Forms.Form.set_MdiParentInternal(Form value)
   bei System.Windows.Forms.Form.set_MdiParent(Form value)
   bei System.Windows.Forms.Form.set_MdiParent(Form value)
   bei PluginTest.PluginSystem.Plugin.Load() in D:\Development\Projects\PluginTest\PluginSystem\Plugin.cs:Zeile 138.
   bei PluginTest.PluginSystem.Plugin._Constructor(String assemblyPath, Boolean autoEnable) in D:\Development\Projects\PluginTest\PluginSystem\Plugin.cs:Zeile 40.
   bei PluginTest.PluginSystem.Plugin..ctor(String assemblyPath, Boolean autoEnable) in D:\Development\Projects\PluginTest\PluginSystem\Plugin.cs:Zeile 32.
   bei PluginTest.Forms.frmPluginManager.btn_UpdatePluginList_Click(Object sender, EventArgs e) in D:\Development\Projects\PluginTest\Forms\frmPluginManager.cs:Zeile 22.
   bei System.Windows.Forms.Control.OnClick(EventArgs e)
   bei System.Windows.Forms.Button.OnClick(EventArgs e)
   bei System.Windows.Forms.Button.OnMouseUp(MouseEventArgs mevent)
   bei System.Windows.Forms.Control.WmMouseUp(Message& m, MouseButtons button, Int32 clicks)
   bei System.Windows.Forms.Control.WndProc(Message& m)
   bei System.Windows.Forms.ButtonBase.WndProc(Message& m)
   bei System.Windows.Forms.Button.WndProc(Message& m)
   bei System.Windows.Forms.Control.ControlNativeWindow.OnMessage(Message& m)
   bei System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m)
   bei System.Windows.Forms.NativeWindow.DebuggableCallback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)
   bei System.Windows.Forms.UnsafeNativeMethods.DispatchMessageW(MSG& msg)
   bei System.Windows.Forms.Application.ComponentManager.System.Windows.Forms.UnsafeNativeMethods.IMsoComponentManager.FPushMessageLoop(Int32 dwComponentID, Int32 reason, Int32 pvLoopData)
   bei System.Windows.Forms.Application.ThreadContext.RunMessageLoopInner(Int32 reason, ApplicationContext context)
   bei System.Windows.Forms.Application.ThreadContext.RunMessageLoop(Int32 reason, ApplicationContext context)
   bei System.Windows.Forms.Application.Run(Form mainForm)
   bei PluginTest.Program.Main() in D:\Development\Projects\PluginTest\Program.cs:Zeile 21.
   bei System.AppDomain._nExecuteAssembly(Assembly assembly, String[] args)
   bei System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
   bei Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
   bei System.Threading.ThreadHelper.ThreadStart_Context(Object state)
   bei System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   bei System.Threading.ThreadHelper.ThreadStart()

Ich hab keine Ahnung was ich machen soll, wo ich ansetzen soll oder was überhaupt das Problem verursacht. Den MdiClient kann ich ja schlecht als Serializable kennzeichen ...

Gruß
florian0

2.187 Beiträge seit 2005
vor 11 Jahren

Hallo florian0,

100%ig kenne ich mich nicht mit AppDomains aus, aber du müsstest 2 Probleme haben:

  1. So bald du Unwrapp auf das Objekt aus der anderen AppDomain anwendest wird dieses Objekt in die aktuellen AppDomain geladen. Das bedeutet, dass dein ganzer Schöner plan mit entladen und so weiter hinfällig wird, da die Assembly des PlugIns dann in der "Haupt"-AppDomain ebenfalls geladen ist!

  2. Um MdiParent des PlugIns zu setzen muss die Form die du dort übergibst in die PlugIn-AppDomain übertragen werden. Das ist eventuell nicht möglich und löst Exceptions aus, die man fälschlicher weise mit Typen aus den PlugIn-AppDomains assoziiert stadt mit dem MdiParent aus der Haupt-AppDomain.

Ich denke dass man mit C# hier nicht so einfach zum Ziel kommt.
Auf CodeProject gibt es einen Window-Tabifier der das ganze über die Windows-API löst. Das hat seine eigenen Nachteil, löst aber das Laden und Entladen der Assemblies, in dem jedes PlugIn einen echten eingenen Windows-Prozess hat.
Ein anderer Ansatz der mir einfallen würde ist, dass man zum Übertragen der PlugIn's keine eigenen Form-Klassen verwendet (so wie der Windows-Forms-Designer) sie anlegt, sondern eine 0815 System.Windows.Forms.Form instanziiert und füllt (diese kann dann ja direkt in der Haupt-AppDomain erstellt werden). Wie man diese Form füllt ist aber eine gradwanderung zwischen Komplexität und Funktionalität und man müsste sich das gut durchdenken.

Gruß
Juy Juka

F
florian0 Themenstarter:in
5 Beiträge seit 2010
vor 11 Jahren

Hi,

ersteres Problem hätte ich auch erwartet. Jedoch kann ich die AppDomain problemlos entladen und die Plugin.dll ist danach freigegeben und kann erneut geladen werden.
Zweiteres habe ich nun durchschaut. Aus CreateInstanceFromAndUnwarp kommt keine "echte" Instanz sondern nur ein System.Runtime.Remoting.Proxies.__TransparentProxy, über den die Kommunikation läuft. Alles was durch den TransparentProxy soll, muss serialisiert werden.

Inzwischen hab ich ein kleines Workaround, mit dem ich relativ zufrieden bin. Ich denke mal Tabifier funktioniert genauso:


// GetWindowLong, SetWindowLong und SetParent sind aus der user32.dll importiert

// Form des Plugins anzeigen
_instance.Form.Show();

// MDI-Child in der MainForm erstellen
Forms.MDIchild myChild = new cpg.Swiftness.Forms.MDIchild(Program.mainForm);
myChild.Show();

// Border der Plugin-Form entfernen
int intStyle = GetWindowLong(_instance.Form.Handle, GWL_STYLE);
intStyle &= ~(WS_CAPTION | WS_THICKFRAME | WS_MINIMIZE | WS_MAXIMIZE | WS_SYSMENU);
SetWindowLong(_instance.Form.Handle, GWL_STYLE, intStyle);

// Plugin-Form per SetParent in das MDIChild anzeigen
SetParent(_instance.Form.Handle, myChild.Handle);

Zwar nicht die Lösung, die ich mir erhofft hatte. Aber damit kann ich leben.

Die Form in der Haupt-AppDomain zu erstellen und im Plugin zu füllen hatte ich auch überlegt. Aber mangels Serialisierungskenntnissen ist das im Moment nicht so einfach.
Ich hatte auch überlegt einfach eine WPF-Anwendung zu verwenden und den XAML-Teil als String durch den Proxy zu schieben ...

Ich kann mir nicht vorstellen, dass in einer Hochsprache wie C# bzw. dem .NET Framework, dass ja sonst auch keine Wünsche offen lässt, soetwas nicht möglich sein sollte.

Weitere Ideen sind willkommen. Sollte ich einen Durchbruch erlangen (Zweifel) werd ich hier davon berichten.

Gruß
florian0

49.485 Beiträge seit 2005
vor 11 Jahren

Hallo florian0,

Ich kann mir nicht vorstellen, dass in einer Hochsprache wie C# bzw. dem .NET Framework, dass ja sonst auch keine Wünsche offen lässt, soetwas nicht möglich sein sollte.

mir ist nicht ganz klar, was du mit "soetwas" meinst. Aber was die Serialisierung angeht kommt man da bei AppDomains meines Wissens nicht herum, denn der Gag von AppDomains ist ja gerade, diese gegeneinander und gegen die Hauptanwendung so abzukapseln, dass kein direkter Speicherzugriff möglich ist. Also können Zugriffe an Objekten einer anderen AppDomain auch nicht direkt vorgenommen werden, sondern müssen (vom transparenten Proxy) über einen Kommunikationskanal geschickt werden, so dass das Objekt selbst und alle Parameter serialisiert werden müssen.

herbivore