DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Adding Compiled .ResX Resources To NuGet Packages

Jon Davis user avatar by
Jon Davis
·
Jul. 10, 12 · Interview
Like (0)
Save
Tweet
Share
6.29K Views

Join the DZone community and get the full member experience.

Join For Free

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 in 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

NuGet

Published at DZone with permission of Jon Davis, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Popular on DZone

  • A Complete Guide to AngularJS Testing
  • Top 5 Node.js REST API Frameworks
  • How to Develop a Portrait Retouching Function
  • Why Every Fintech Company Needs DevOps

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: