InitializeComponent for multiple assembly versions (WPF)

InitializeComponent for multiple assembly versions (WPF)

In the code behind of WPF controls the constructor loads the corresponding XAML1 file (code). The code for loading the XAML is automatically generated2 when calling InitializeComponent(), which one can find in virtually every control’s constructor. Unfortunately it might not play well, break, when there are multiple versions loaded of the same assembly containing the control (in the same AppDomain). The solution is simple but, alas, not well documented.

WPF is old, first released in 2006, and open source by now. The issue described here has been know for a long time (< 2009) and reported to Microsoft but was labelled as ‘Won’t fix’. Unfortunately the original report is lost and it is hard to trace whether any reason was provided. The documentation is nonexistent, therefore i wanted to write this post to keep the information alive in the internet hive mind. This post is based on the blogpost by Alex Feinberg (2014) which led me to the actual solution.

The problem usually surfaces when the following exception3 pops-up when the control is being loaded.

Type               : System.Exception
Source             : PresentationFramework
Target             : Application
Message below      :
> The component 'MyNamespace.MyView' does not have a resource identified by the URI '/MyLibrary;component/MyView.xaml'.
----------------------------------------
   at System.Windows.Application.LoadComponent(Object component, Uri resourceLocator)
   at MyNamespace.MyView.InitializeComponent()

Exception indicating the XAML cannot be found

This post is written for .NET framework 4.8.

Problem at hand

C# supports loading multiple versions of the same assembly in a single AppDomain. When the control is loaded its constructor is called, which loads its XAML (by InitializeComponent()), stored as a resource in the assembly. Determining which XAML resource in the assembly should be used is determined by specifying its URI (uniform resource identifier); article. As one can clearly see this in the generated code for InitializeComponent():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute(&quot;PresentationBuildTasks&quot;, &quot;4.0.0.0&quot;)]
public void InitializeComponent() {
    if (_contentLoaded) {
        return;
    }
    _contentLoaded = true;
    System.Uri resourceLocater = new System.Uri(&quot;/MyLibrary;component/MyView.xaml&quot;, System.UriKind.Relative);
     
    #line 1 &quot;..\..\MyView.xaml&quot;
    System.Windows.Application.LoadComponent(this, resourceLocater);
     
    #line default
    #line hidden
}

Generated code as found in /obj/Debug/MyView.g.cs

Notice however it contains the assembly (MyLibrary) and XAML name (MyView.xaml) but not a version , at least it probably does not given you are reading this post. This is where it goes wrong, in a multi version environment this information is not sufficient to uniquely specify the resource. The URI class supports various types of resources (from FTP, file to embedded resource in the assembly) and optional parameters, one of them being a version.

So what is the problem?

WPF internally caches XAML streams and in a multi version environment, if the URI does not specify a version, the wrong cached XAML resource might be used failing a check with a misleading error message: The component '...' does not have a resource identified by the URI '...', even though the resource is present.

Working backwards the original exception in thrown in LoadComponent(...) which actually loads the XAML and is called by InitializeComponent(). The highlighted assembly check will fail.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
public static void LoadComponent(Object component, Uri resourceLocator)
{
    if (component == null)
        throw new ArgumentNullException("component");

    if (resourceLocator == null)
        throw new ArgumentNullException("resourceLocator");

    if (resourceLocator.OriginalString == null)
        throw new ArgumentException(SR.Get(SRID.ArgumentPropertyMustNotBeNull,"resourceLocator", "OriginalString"));

    if (resourceLocator.IsAbsoluteUri == true)
        throw new ArgumentException(SR.Get(SRID.AbsoluteUriNotAllowed));

    // Passed a relative Uri here.
    // needs to resolve it to Pack://Application.
    //..\..\ in the relative Uri will get stripped when creating the new Uri and resolving to the
    //PackAppBaseUri, i.e. only relative Uri within the appbase are created here
    Uri currentUri = new Uri(BaseUriHelper.PackAppBaseUri, resourceLocator);

    //
    // Generate the ParserContext from packUri
    //
    ParserContext pc = new ParserContext();

    pc.BaseUri = currentUri;

    bool bCloseStream = true;  // Whether or not to close the stream after LoadBaml is done.

    Stream stream = null;  // stream could be extracted from the manifest resource or cached in the
                           // LoadBamlSyncInfo depends on how this method is called.

    //
    // We could be here because of an InitializeCompoenent() call from the ctor of this component.
    // Check if this component was originally being created from the same Uri by the BamlConverter
    // or LoadComponent(uri).
    //
    if (IsComponentBeingLoadedFromOuterLoadBaml(currentUri) == true)
    {
        NestedBamlLoadInfo nestedBamlLoadInfo = s_NestedBamlLoadInfo.Peek();

        // If so, use the stream already created for this component on this thread by the
        // BamlConverter and seek to origin. This gives better perf by avoiding a duplicate
        // WebRequest.
        stream = nestedBamlLoadInfo.BamlStream;

        stream.Seek(0, SeekOrigin.Begin);

        pc.SkipJournaledProperties = nestedBamlLoadInfo.SkipJournaledProperties;

        // Reset the OuterUri in the top LoadBamlSyncInfo in the stack for the performance optimization.
        nestedBamlLoadInfo.BamlUri = null;

        // Start a new load context for this component to allow it to initialize normally via
        // its InitializeComponent() so that code in the ctor following it can access the content.
        // This call will not close the stream since it is owned by the load context created by
        // the BamlConverter and will be closed from that context after this function returns.

        bCloseStream = false;
    }
    else
    {
        // if not, this is a first time regular load of the component.
        PackagePart part = GetResourceOrContentPart(resourceLocator);
        ContentType contentType = new ContentType(part.ContentType);
        stream = part.GetSeekableStream();
        bCloseStream = true;

        //
        // The stream must be a BAML stream.
        // check the content type.
        //
        if (!MimeTypeMapper.BamlMime.AreTypeAndSubTypeEqual(contentType))
        {
            throw new Exception(SR.Get(SRID.ContentTypeNotSupported, contentType));
        }
    }

    IStreamInfo bamlStream = stream as IStreamInfo;

    if (bamlStream == null || bamlStream.Assembly != component.GetType().Assembly)
    {
        throw new Exception(SR.Get(SRID.UriNotMatchWithRootType, component.GetType( ), resourceLocator));
    }

    XamlReader.LoadBaml(stream, pc, component, bCloseStream);
}

LoadComponent(...) from PresentationFramework

Source: WPF sourcecode

If it is loaded for the first time, else, the code goes to GetResourceOrContentPart(..) which uses GetResourcePackage(..) to get the Package (containing the XAML resource). It uses the PreloadedPackages cache ( PackageStore), based on URI. The URI will not contain the version and thus will collide if there are multiple versions of the same assembly in use.

If this component has been loaded before the LoadBamlSyncInfo (s_NestedBamlLoadInfo) stack is kept in a local variable so that the Outer LoadBaml and Inner LoadBaml( ) for the same Uri share the related information.

To determine why InitializeComponent() does not include the version in the URI one has to look in GenerateInitializeComponent(), which will be called during compilation. There is explicit code to handle and parse the version, see highlighted.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
private void GenerateInitializeComponent(bool isApp)
{
    // public void InitializeComponent()
    // {
    //
    CodeMemberMethod cmmLC = _ccRoot.InitializeComponentFn;

    if (cmmLC == null)
    {
        cmmLC = _ccRoot.EnsureInitializeComponentFn;
        if (!isApp)
        {
            cmmLC.ImplementationTypes.Add(new CodeTypeReference(KnownTypes.Types[(int)KnownElements.IComponentConnector]));
        }
    }

    //     if (_contentLoaded)
    //     {
    //         return;
    //     }
    //
    CodeConditionStatement ccsCL = new CodeConditionStatement();
    ccsCL.Condition = new CodeFieldReferenceExpression(null, CONTENT_LOADED);
    ccsCL.TrueStatements.Add(new CodeMethodReturnStatement());
    if (!isApp)
    {
        cmmLC.Statements.Add(ccsCL);
    }
    else
    {
        cmmLC.Statements.Insert(0, ccsCL);
    }

    //     _contentLoaded = true;
    //
    CodeAssignStatement casCL = new CodeAssignStatement(new CodeFieldReferenceExpression(null, CONTENT_LOADED),
                                                        new CodePrimitiveExpression(true));
    if (!isApp)
    {
        cmmLC.Statements.Add(casCL);
    }
    else
    {
        cmmLC.Statements.Insert(1, casCL);
    }

    // Generate canonicalized string as resource id.
    bool requestExtensionChange = false;
    string resourceID = ResourcesGenerator.GetResourceIdForResourceFile(
            SourceFileInfo.RelativeSourceFilePath + XAML,
            SourceFileInfo.OriginalFileLinkAlias,
            SourceFileInfo.OriginalFileLogicalName,
            TargetPath,
            Directory.GetCurrentDirectory() + Path.DirectorySeparatorChar,
            requestExtensionChange);

    string uriPart = string.Empty;

    // Attempt to parse out the AssemblyVersion if it exists.  This validates that we can either use an empty version string (wildcards exist)
    // or we can utilize the passed in string (valid parse).
    if (!VersionHelper.TryParseAssemblyVersion(AssemblyVersion, allowWildcard: true, version: out _, out bool hasWildcard)
        && !string.IsNullOrWhiteSpace(AssemblyVersion))
    {
        throw new AssemblyVersionParseException(SR.Get(SRID.InvalidAssemblyVersion, AssemblyVersion));
    }

    // In .NET Framework (non-SDK-style projects), the process to use a wildcard AssemblyVersion is to do the following:
    //   - Modify the AssemblyVersionAttribute to a wildcard string (e.g. "1.2.*")
    //   - Set Deterministic to false in the build
    // During MarkupCompilation, the AssemblyVersion property would not be set and WPF would correctly generate a resource URI without a version.
    // In .NET Core/5 (or .NET Framework SDK-style projects), the same process can be used if GenerateAssemblyVersionAttribute is set to false in 
    // the build.  However, this isn't really the idiomatic way to set the version for an assembly.  Instead, developers are more likely to use the 
    // AssemblyVersion build property.  If a developer explicitly sets the AssemblyVersion build property to a wildcard version string, we would use 
    // that as part of the URI here.  This results in an error in Version.Parse during InitializeComponent's call tree.  Instead, do as we would have 
    // when the developer sets a wildcard version string via AssemblyVersionAttribute and use an empty string.
    string version = hasWildcard || String.IsNullOrEmpty(AssemblyVersion)
        ? String.Empty
        : COMPONENT_DELIMITER + VER + AssemblyVersion;

    string token = String.IsNullOrEmpty(AssemblyPublicKeyToken)
        ? String.Empty
        : COMPONENT_DELIMITER + AssemblyPublicKeyToken;

    uriPart = FORWARDSLASH + AssemblyName + version + token + COMPONENT_DELIMITER + COMPONENT + FORWARDSLASH + resourceID;

    //
    //  Uri resourceLocator = new Uri(uriPart, UriKind.Relative);
    //
    string resVarname = RESOURCE_LOCATER;

    CodeFieldReferenceExpression cfreRelUri = new CodeFieldReferenceExpression(new CodeTypeReferenceExpression(typeof(System.UriKind)), "Relative");

    CodeExpression[] uriParams = { new CodePrimitiveExpression(uriPart), cfreRelUri };
    CodeObjectCreateExpression coceResourceLocator = new CodeObjectCreateExpression(typeof(System.Uri), uriParams);
    CodeVariableDeclarationStatement cvdsresLocator = new CodeVariableDeclarationStatement(typeof(System.Uri), resVarname, coceResourceLocator);

    cmmLC.Statements.Add(cvdsresLocator);

    //
    //  System.Windows.Application.LoadComponent(this, resourceLocator);
    //
    CodeMethodReferenceExpression cmreLoadContent = new CodeMethodReferenceExpression(new CodeTypeReferenceExpression(KnownTypes.Types[(int)KnownElements.Application]), LOADCOMPONENT);
    CodeMethodInvokeExpression cmieLoadContent = new CodeMethodInvokeExpression();

    cmieLoadContent.Method = cmreLoadContent;

    CodeVariableReferenceExpression cvreMemStm = new CodeVariableReferenceExpression(resVarname);

    cmieLoadContent.Parameters.Add(new CodeThisReferenceExpression());
    cmieLoadContent.Parameters.Add(cvreMemStm);

    CodeExpressionStatement cesLC = new CodeExpressionStatement(cmieLoadContent);
    AddLinePragma(cesLC, 1);
    cmmLC.Statements.Add(cesLC);

    // private bool _contentLoaded;
    //
    CodeMemberField cmfCL = new CodeMemberField();
    cmfCL.Name = CONTENT_LOADED;
    cmfCL.Attributes = MemberAttributes.Private;
    cmfCL.Type = new CodeTypeReference(typeof(bool));
    _ccRoot.CodeClass.Members.Add(cmfCL);

    if (!isApp)
    {
        // Make sure that ICC.Connect is generated to avoid compilation errors
        EnsureHookupFn();
    }
}

GenerateInitializeComponent(...) from MarkupCompiler.cs

Source: WPF sourcecode

It is well documented, i’m still using .NET Framework style projects and as per code documentation: “During MarkupCompilation, the AssemblyVersion property would not be set and WPF would correctly generate a resource URI without a version.”, this is the issue. Idiomatic the AssemblyVersion property is not set (AssemblyInfo.cs is used instead which is not sufficient in this case) so WPF generates the URI without version included leading to the problems discussed earlier.

Unfortunately this is nowhere documented besides the source code (fortunately it is open source).

Solution

Specify the AssemblyVersion property in your .csproj file. When set InitializeComponent() will generated code that specifies the version in the URI as well, resolving the problem.

If you prefer to only specify it in AssemblyInfo.cs though, one can use a msbuild Target to add the AssemblyVersion property to the project before being compiled. Reducing maintenance. For example:

<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <Target Name="BeforeBuild">
        <ReadLinesFromFile File="$(MSBuildProjectDirectory)\Properties\AssemblyInfo.cs">
            <Output TaskParameter="Lines" ItemName="ItemsFromFile" />
        </ReadLinesFromFile>

        <PropertyGroup>
            <Pattern>\[assembly: AssemblyVersion\(.(\d+)\.(\d+)\.(\d+)\.(\d+)</Pattern>
            <In>@(ItemsFromFile)</In>
            <Out>$([System.Text.RegularExpressions.Regex]::Match($(In), $(Pattern)))</Out>
        </PropertyGroup>

        <CreateProperty Value="$(Out.Remove(0, 28))">
            <Output TaskParameter="Value" PropertyName="AssemblyVersion" />
        </CreateProperty>
    </Target>
</Project

Source: eberthold

Alternative one could manually patch the *.g.cs files and add the version number, although i would strongly recommend the former solution.

One could also more to the SDK-style projects for which it is idiomatic to work with the AssemblyVersion property.

As last alternative one could ditch the default InitializeComponent() and create your own. (and if you want to be fancy use a extension method and source generator, C# 9 or above is required)


  1. technically BAML (Binary Application Markup Language) will be loaded instead of XAML. It is a pre-tokenized binary version of the XAML code for improved performance. ↩︎

  2. the generated code can be found in MyView.g.cs located in the obj folder. ↩︎

  3. unfortunately the error can be triggered by a lot of other scenarios as well which i will not all discuss here. See for example StackOverflow. A common one is not turning off Copy Local↩︎

Noticed an error in this post? Corrections are appreciated.

© Nelis Oostens