Adding Compiled .ResX Resources To NuGet Packages

by Jon Davis 16. November 2011 17:05

At my current workplace, we are using NuGet internally for managing internal ASP.NET MVC 3 project templates. If you open up Visual Studio’s Tools -> Options dialog window and expand Package Manager, you will find a “Package Sources” section where you can define locations for acquiring NuGet packages. We have a second location defined in this section with the path being a directory on our LAN. There are several advantages of taking this approach to managing team resources, not the least of which is the fact that updating our template source code repositories over SVN will not, and should not, update (read: break) the development workflows for people already working on their projects unless those individuals manually invoke an Update-Package command from the Package Manager Console. This scenario is obviously not ideal for many teams but it is quite useful for ours since each project instance has its own lifecycle and is short-lived.

One of the dependencies of our ASP.NET MVC templates is the utilization of .resx files for managing various pieces of content such as the labels on the forms. Have a resource file gives specific members of the organization a very specific and anticipated location to update the content to suit the needs of the project instance. The content items are also programmatically accessible when the Access Modifier is set to public; a .Designer.cs file is generated and injected into the project automatically by Visual Studio, appearing as a generated code-behind file for the .resx file, which exposes the API required to programmatically read content from this file by item name so that the developer does not have to stream the .resx out manually as an embedded resource stream. Microsoft .NET also automatically pulls the content from the suitable resource file when the culture is added to the filename, based on the user’s culture of the current thread; in other words, if a resource file is called FormFields.resx, but the user is French-Canadian (fr-CA), then the content in FormFields.fr-CA.resx is automatically used. Of course, we have to manually synchronize the user’s culture with the thread, that is a separate discussion, but the point is that it can be beneficial, and in our case it is, to utilize .resx resources in a project.

Unfortunately, when installing a NuGet project that contains .resx file content, the generated designer C# file that exposes programmatic access to the items in the resources file does not get added correctly, nor is the metadata on the .resx file that declares the resource’s Access Modifier to be “Public”, which is how Visual Studio knows to inject that generated file. Our template’s C# codebase depends upon the presence of the generated C# object members for the .resx, so in the absence of the generated code-behind file the project importing this template will not compile. Our workaround had been to open up the .resx file, change the Access Modifier back to “Public”, save it, and Rebuild. This worked fine, but it has been a huge annoyance.

So we decided to look into automatically fixing this within the Install.ps1 PowerShell script which NuGet invokes upon installing a package. Visual Studio’s DTE automation object and its Project object are both injected to the Install.ps1 script that PowerShell invokes.

param($installPath, $toolsPath, $package, $project)

We store our resources in a Resources directory in the project, so iterating through the project items to identify our .resx files was straightforward enough.

$resitems = @{}
foreach ($item in $project.ProjectItems) 
{
    if ($item.Name -eq "Resources") {
        foreach ($resitem in $resources.ProjectItems) {
            if ($resitem.Name.ToLower().EndsWith(".resx")) {
               $resitems.Add("Resources\" + $resitem.Name, $resitem.FileNames(0))
            }
        }
    }
}

Unfortunately, I found no way within EnvDTE automation to modify the properties the .resx file that pertain to the generated file! At best, the ProjectItem object exposes a Properties member that lists out various bits of metadata, but I found nothing here that can be changed to cause the .resx file to use a generated file.

The best I could find and tried to play with was a property called “IsDesignTimeBuildInput” that I thought I could apply to the .Designer.cs file, but attempting to set this value to true proved unfruitful:

# where $cb2 is the .Designer.cs file
$cb2.Properties.Item("IsDesignTimeBuildInput").Value = $TRUE

.. results in ..
Exception setting "Value": "Invalid number of parameters. (Exception from HRESULT: 0x8002000E (DISP_E_BADPARAMCOUNT))"

I did manage to get a code-behind file added to the .resx file, however.

switch ($item.ProjectItems) { default {
	if ($_.Name.ToLower() -eq $resitem.Name.ToLower().Replace(".resx", ".designer.cs")) {
		$hasCodeBehind = $TRUE
		$codebehinditem = $_
	}
}}
if ($hasCodeBehind -eq $TRUE) {
	$fn = $resitem.FileNames(0)
	$cbfn = $codebehinditem.FileNames(0)
	$codebehinditem.Remove()
	$cb2 = $resitem.ProjectItems.AddFromFile($cbfn)
}

At this point, it would prove obviously beneficial to use a comparison tool such as Beyond Compare (which I used) to compare the contents of the .resx file, the .Designer.cs file, and the .csproj file (the Visual Studio project file) between my half-restored NuGet injection and a properly working project instance. Doing this, I found that there are absolutely no changes made to the .resx file to toggle its code-behind / generator behavior, and of course the .Designer.cs is just the output of the generator so it has no flags, either. All of this metadata is therefore made to the project (.csproj) file.

And since there do not seem to be any EnvDTE interfaces to support these project file changes, it seems that the change must be made in the project XML directly. This can cause all kinds of unpredictable problems, the least of which is an ugly dialog box for the user, “Project has changed, reload?” Nonetheless, this is what’s working for us, and it’s better than a broken build that requires us to manually open the .resx file.

The full solution:

param($installPath, $toolsPath, $package, $project)

#script to fix code-behind for resx
set-alias Write-Host -Name whecho
whecho "Restoring resource code-behinds (this may cause the project to be reloaded with a dialog) ..."
$resitems = @{}
foreach ($item in $project.ProjectItems) 
{
    if ($item.Name -eq "Resources") {
        $resources = $item
        foreach ($resitem in $resources.ProjectItems) {
            $codebehinditem = $NULL
            if ($resitem.Name.ToLower().EndsWith(".resx")) {
                $hasCodeBehind = $FALSE
                switch ($item.ProjectItems) { default {
                    if ($_.Name.ToLower() -eq $resitem.Name.ToLower().Replace(".resx", ".designer.cs")) {
                        $hasCodeBehind = $TRUE
                        $codebehinditem = $_
                    }
                }}
                if ($hasCodeBehind -eq $TRUE) {
                    $fn = $resitem.FileNames(0)
                    $cbfn = $codebehinditem.FileNames(0)
                    $codebehinditem.Remove()
                    $cb2 = $resitem.ProjectItems.AddFromFile($cbfn)
                }
                $resitems.Add("Resources\" + $resitem.Name, $resitem.FileNames(0))
                whecho $resitem.Name
            }
        }
    }
}
$project.Save($project.FullName)
$projxml = [xml](get-content $project.FullName)
$ns = New-Object System.Xml.XmlNamespaceManager $projxml.NameTable
$defns = "http://schemas.microsoft.com/developer/msbuild/2003"
$ns.AddNamespace("csproj", $defns)
foreach ($item in $resitems.GetEnumerator()) {
	$xpath = "//csproj:Project/csproj:ItemGroup/csproj:Compile[@Include=`"" + $item.Name.Replace(".resx", ".Designer.cs") + "`"]"
	$resxDesignerNode = $projxml.SelectSingleNode($xpath, $ns)
	
	if ($resxDesignerNode -ne $NULL) {
	
		$autogen = $projxml.CreateElement('AutoGen', $defns)
		$autogen.InnerText = 'True'
		$resxDesignerNode.AppendChild($autogen)
		
		$designtime = $projxml.CreateElement('DesignTime', $defns)
		$designtime.InnerText = 'True'
		$resxDesignerNode.AppendChild($designtime)
	}
	
	$xpath = "//csproj:Project/csproj:ItemGroup/csproj:EmbeddedResource[@Include=`"" + $item.Name + "`"]"
	$resxNode = $projxml.SelectSingleNode($xpath, $ns)

	$generator = $projxml.CreateElement('Generator', $defns)
	$generator.InnerText = 'PublicResXFileCodeGenerator'
	$resxNode.AppendChild($generator)
	
	if ($resxDesignerNode -ne $NULL) {
		$lastGenOutput = $projxml.CreateElement('LastGenOutput', $defns)
		$lastGenOutput.InnerText = $item.Name.Replace("Resources\", "").Replace(".resx", ".Designer.cs")
		$resxNode.AppendChild($lastGenOutput)
	}

}
$projxml.Save($project.FullName)

UPDATE: Just an update on this, we have abandoned this approach to editing the XML. The project XML can be manipulated in-memory using the MSBuild automation object. Hints of what to do are found here:

http://nuget.codeplex.com/discussions/254095

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Tags:

Comments

Add comment


(Will show your Gravatar icon)  

  Country flag

biuquote
  • Comment
  • Preview
Loading




 

Powered by BlogEngine.NET 1.4.5.0
Theme by Mads Kristensen

About the author

Jon Davis (aka "stimpy77") has been a programmer, developer, and consultant for web and Windows software solutions professionally since 1997, with experience ranging from OS and hardware support to DHTML programming to IIS/ASP web apps to Java network programming to Visual Basic applications to C# desktop apps.
 
Software in all forms is also his sole hobby, whether playing PC games or tinkering with programming them. "I was playing Defender on the Commodore 64," he reminisces, "when I decided at the age of 12 or so that I want to be a computer programmer when I grow up."

Jon was previously employed as a senior .NET developer at a very well-known Internet services company whom you're more likely than not to have directly done business with. However, this blog and all of jondavis.net have no affiliation with, and are not representative of, his former employer in any way.

Contact Me 


Tag cloud

Calendar

<<  July 2014  >>
MoTuWeThFrSaSu
30123456
78910111213
14151617181920
21222324252627
28293031123
45678910

View posts in large calendar