How To Load WPF Application Resources "via Code" (not "via XAML") from a separate assembly

3.3k views Asked by At

I have a WPF application (call it a Launcher) and wish to specify additional application resources (such as additional Views, Components and Content) via Code, not via XAML. Further, these resources are defined by a second assembly (and not the same assembly which defines App.xaml)

Currently we have this defined in App.xaml:

<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="/GUI;component/Common/ViewResources.xaml" />
            <ResourceDictionary Source="/GUI;component/Common/ResourceDictionary.xaml" />
            <ResourceDictionary Source="/Components;component/Common/ResourceDictionary.xaml" />
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Application.Resources>

This works, but it's not what I am attempting to do. For these resources to resolve I need to specify an assembly reference in the URL or include/duplicate the resources in the EXE (the assembly containing App.xaml.)

Is there a way to set/initialize -application resources- in GUI side in code? Any other idea?

You must assume the Launcher will have already entered "App::Run", you must load the necessary UI resources after this event occurs, and you must remain in-process.


There are 3 obvious solutions which are not acceptable:

  1. Add a Dictionary in GUI and Merge it in all Views (e.g. extend an existing UI)

  2. Since the Launcher is already an Application we can't do something like this in GUI.Start (e.g. run a second App in-process):

    public void Start() { App app = new App(); app.Run(); }

  3. Both (Launcher and GUI) need to stay in the same process, so a solution like this is not acceptable (e.g. launch a second App out-of-process):

    Process myProc; myProc = Process.Start("GUI.exe");

2

There are 2 answers

3
Fábio Ferreira On

A colleague just came up with a solution, and its simple actually:

Providing a property GUI.Resources, used to set all Launcher App Resources:

public ResourceDictionary Resources
{
    get
    {
        ResourceDictionary resourceGUIView = new ResourceDictionary();
        resourceGUIView.Source = new Uri("/GUI;component/Common/ViewResources.xaml", UriKind.RelativeOrAbsolute);
        ResourceDictionary resourceGUI = new ResourceDictionary();
        resourceGUI.Source = new Uri("/GUI;component/Common/ResourceDictionary.xaml", UriKind.RelativeOrAbsolute);
        ResourceDictionary resourceComponent = new ResourceDictionary();
        resourceComponent.Source = new Uri("/Component;component/Common/ResourceDictionary.xaml", UriKind.RelativeOrAbsolute);

        ResourceDictionary generalResource = new ResourceDictionary();

        generalResource.MergedDictionaries.Add(resourceGUIView);
        generalResource.MergedDictionaries.Add(resourceGUI);
        generalResource.MergedDictionaries.Add(resourceComponent);

        return generalResource;
    }
}

Then call it in App.xaml.cs of the Launcher:

protected override void OnStartup(StartupEventArgs e)
{
    this.Resources = this.context.GUI.Resources;

    ...

    base.OnStartup(e);
}
0
Shaun Wilson On

A few other approaches to consider:

  • Modify the Pack URI to specify an assembly reference, this is appropriate in both 'via XAML' and 'via Code' cases.

  • Create an app-config with an <appSettings/> entry that specifies the target UI entry-point as a "Type Full Name" such as "MyNamespace.InitializerClassName,MyAssemblyName", and then pass this as a parameter to a call Activator.CreateInstance(). The constructor of this type would perform necessary initialization (or throw.)

  • Create a purpose-built interface, and use reflection to locate an "initializer type" for the other assembly. Then call a well-known method on that interface to perform UI init.

  • Create a purpose-built attribute, and use reflection to locate an "initializer method" for the other assembly. Then call the method to perform UI init.

  • Create a secondary AppDomain and call App.Run() on a NEW THREAD within the context of the secondary AppDomain. You may have to leverage MarshalByRefObject to perform the remote invocation for thread creation, exception handling and to extend the host environment before calling App.Run(). This approach will allow the launcher and target UI to coexist in the same physical process space, allow the launcher to extend the run-time environment, and allow the target UI to perform initialization in-code.

None of these approaches should necessarily require a compile-time reference to the UI assembly, making the launcher portable from one UI to the next (except in the obvious case where an Assembly name would be specified in a Pack URI or the App.Config)

It looks like the OPs own solution tightly couples to the target assembly at compile time (there is no configuration/discovery/reflection process) and the deletion of the target UI assembly would break the build/compilation.