4

So this is how you invoke Main() in non WPF applications:

var entry = assembly.EntryPoint;

if (assembly.EntryPoint.GetParameters().Length == 0)
    entry.Invoke(null, new object[0]);
else
    entry.Invoke(null, new object[] { args });

but somehow it doesn't work at all for WPF applications, I've tried (MSDN's way):

Assembly asm = Assembly.LoadFrom(file);

Type myType = asm.GetType("WpfApplication1.App");

// Get the method to call.
MethodInfo myMethod = myType.GetMethod("Main");

// Create an instance.
object obj = Activator.CreateInstance(myType);

// Execute the method.
myMethod.Invoke(obj, null);

still no success, Reflector shows Main() as

[DebuggerNonUserCode, STAThread]
public static void Main()
{
    App app = new App();
    app.InitializeComponent();
    app.Run();
}

no matter what I do, I get "System.Reflection.TargetInvocationException" exception.

Any help?

PS. More debugging revealed it can't load „mainwindow.xaml” resource originally located in the assembly that I want to load

{System.IO.IOException: Nie można zlokalizować zasobu „mainwindow.xaml”.
   w MS.Internal.AppModel.ResourcePart.GetStreamCore(FileMode mode, FileAccess access)
   w System.IO.Packaging.PackagePart.GetStream(FileMode mode, FileAccess access)
   w System.IO.Packaging.PackagePart.GetStream()
   w System.Windows.Application.LoadComponent(Uri resourceLocator, Boolean bSkipJournaledProperties)
   w System.Windows.Application.DoStartup()
   w System.Windows.Application.<.ctor>b__0(Object unused)
   w System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Boolean isSingleParameter)
   w System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Boolean isSingleParameter, Delegate catchHandler)
   w System.Windows.Threading.Dispatcher.WrappedInvoke(Delegate callback, Object args, Boolean isSingleParameter, Delegate catchHandler)
   w System.Windows.Threading.DispatcherOperation.InvokeImpl()
   w System.Windows.Threading.DispatcherOperation.InvokeInSecurityContext(Object state)
   w System.Threading.ExecutionContext.runTryCode(Object userData)
   w System.Runtime.CompilerServices.RuntimeHelpers.ExecuteCodeWithGuaranteedCleanup(TryCode code, CleanupCode backoutCode, Object userData)
   w System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
   w System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   w System.Windows.Threading.DispatcherOperation.Invoke()
   w System.Windows.Threading.Dispatcher.ProcessQueue()
   w System.Windows.Threading.Dispatcher.WndProcHook(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   w MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   w MS.Win32.HwndSubclass.DispatcherCallbackOperation(Object o)
   w System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Boolean isSingleParameter)
   w System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Boolean isSingleParameter, Delegate catchHandler)
   w System.Windows.Threading.Dispatcher.WrappedInvoke(Delegate callback, Object args, Boolean isSingleParameter, Delegate catchHandler)
   w System.Windows.Threading.Dispatcher.InvokeImpl(DispatcherPriority priority, TimeSpan timeout, Delegate method, Object args, Boolean isSingleParameter)
   w System.Windows.Threading.Dispatcher.Invoke(DispatcherPriority priority, Delegate method, Object arg)
   w MS.Win32.HwndSubclass.SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam)
   w MS.Win32.UnsafeNativeMethods.DispatchMessage(MSG& msg)
   w System.Windows.Threading.Dispatcher.PushFrameImpl(DispatcherFrame frame)
   w System.Windows.Threading.Dispatcher.PushFrame(DispatcherFrame frame)
   w System.Windows.Threading.Dispatcher.Run()
   w System.Windows.Application.RunDispatcher(Object ignore)
   w System.Windows.Application.RunInternal(Window window)
   w System.Windows.Application.Run(Window window)
   w System.Windows.Application.Run()
   w WpfApplication1.App.Main()}

so I suspect, the problem is that CLR tries to find .xml within loader application and not within THE ACTUAL wpf application.

Bartosz Wójcik
  • 1,079
  • 2
  • 13
  • 31
  • WCF applications must be started via the Service Manager. You cannot start them on your own. – SASS_Shooter Sep 14 '12 at 18:27
  • I don't think the OP wants to start WCF apps - he's trying to start WPF apps. – Surfbutler Sep 14 '12 at 18:37
  • Yes, WPF applications, I've browsed a ton of websites, MSDN and can't get to any working solution, in my desperation I've tried this WCF way of loading (via ServiceHost) but still it doesn't work. – Bartosz Wójcik Sep 14 '12 at 18:57
  • What type of application are you trying to load it from? In trying to reproduce it, I keep getting this inner exception "Cannot create more than one System.Windows.Application instance in the same AppDomain." – Jason Haley Oct 01 '12 at 00:49

2 Answers2

4

I've found a way to do that. You basically have two options.

  1. Load the exe in the separate AppDomain.
  2. Use reflection trickery to change default ResourceAssembly.

Option first, while cleaner, has disadvantage of being slower (WPF needs to load into the new AppDomain too):

//Assembly: WpfLoader.dll
[STAThread]
class Program
{
    public class Loader : MarshalByRefObject
    {
        public void Load()
        {
            var dll = File.ReadAllBytes("WpfTest.exe");
            var assembly = Assembly.Load(dll);
            Application.ResourceAssembly = assembly;
            assembly.EntryPoint.Invoke(null, new object[0]);
        }


    }


    static void Main(string[] args)
    {
        var domain = AppDomain.CreateDomain("test");
        domain.Load("WpfLoader");

        var loader = (Loader)domain.CreateInstanceAndUnwrap("WpfLoader", "WpfLoader.Program+Loader");
        loader.Load();
    }
}

The second solution uses reflection to change ResourceAssembly of current application. You cannot do that with public API's, as the Application.ResourceAssembly is read only once it's set. You have to use reflection to access private members of both Application and ResourceUriHelper classes.

var dll = File.ReadAllBytes("WpfTest.exe");
var assembly = Assembly.Load(dll);

var app = typeof(Application);

var field = app.GetField("_resourceAssembly", BindingFlags.NonPublic | BindingFlags.Static);
field.SetValue(null, assembly);

//fix urihelper
var helper = typeof(BaseUriHelper);
var property = helper.GetProperty("ResourceAssembly", BindingFlags.NonPublic | BindingFlags.Static);
property.SetValue(null, assembly, null);

//load
assembly.EntryPoint.Invoke(null, new object[0]);

There is still a caveat with both solutions: You cannot use more than one Wpf application which uses relative resources in one AppDomain, so should you wish to load more than one app, you need to create multiple AppDomains.

For those examples to work, you need to do two things:

  • It needs to be called from single appartment state threads, so remember to use [STAThread]
  • You need to add PresentationCore.dll, PresentationFramework.dll and WindowsBase.dll references
ghord
  • 13,260
  • 6
  • 44
  • 69
  • Good stuff. Btw AppDomain.ExecuteAssembly() sets ResourceAssembly to the loaded assembly for you. https://learn.microsoft.com/en-us/dotnet/api/system.windows.application.resourceassembly?view=netcore-3.1 – Assaf Levy Sep 09 '20 at 07:46
0

The option 2 from @ghord is very good, but it needs a way to load the *.dll referenced in the WPF.

So you also have to modify the App.xaml.cs of the WPF in the following way

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
        base.OnStartup(e);
    }
    private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
    {
        string dir = @"C:\your\path\to\WPF\bin\Debug\";
        string fileExtension = "*.dll";
        string needed = args.Name.Split(',')[0];
        if (needed.EndsWith(".resources"))
        {
            return null;
        }
        foreach (String file in Directory.GetFiles(dir, fileExtension, SearchOption.TopDirectoryOnly))
        {
            string name = System.IO.Path.GetFileNameWithoutExtension(file);
            if (args.Name.StartsWith(name))
            {
                byte[] bytes = File.ReadAllBytes(file);
                Assembly assembly = Assembly.Load(bytes);
                return assembly;
            }
        }
        Debug.WriteLine(args.Name);

        return null;
    }
}

The string dir can be set as the path to the Release folder of your WPF app. Notice that it is important to skip the .resources but for the rest we can assume that all the needed *.dll files are found in the dir folder.

Btw, all this goes very well with an encrypted signature for your assemblies: read for example my article to see how to encrypt a signature for your exe and dll files.

quick win tip

Let's say that your launcher is named launcher.exe and the original WPF is mywfp.exe and you have defined a WPF window TestView under a project folder view. Now you can face an exception like the following:

System.Exception:
"The component 'mywfp.TestView' does not have a resource identified by the URI '/mywfp;component/view/testview.xaml'."

The quick trick here is to:

  • rename mywfp.exe as mywfp_original.exe (or as you like)
  • rename the launcher launcher.exe as the original wpf, i.e. mywfp.exe !

In case you want to add a splash screen, include the image with a Resource Build Action in the loader exe: show it ASAP in the STA thread of the loader and close it before invoking the entry point of the in-memory loaded app.

    SplashScreen splashScreen = new SplashScreen("splashscreen.jpg");
    splashScreen.Show(false);

...

    splashScreen.Close(TimeSpan.FromSeconds(1));
    assembly.EntryPoint.Invoke(null, new object[0]);