
Introduction
I've been using Windows Forms for the last 8 years or so and, while it has its quirks, it's by and large been pretty straightforward to use. However, the one case where
Windows Forms causes me real pain occurs when I need to merge a .resx file from two branches of a source control system. It turns out that the Windows Forms implementation
in Visual Studio reorders (apparently randomly) the elements in the .resx file when changes are made to a form or custom control. While this doesn't affect the behavior
of the form or control (the sequence is irrelevant), it wreaks havoc on any merge/diff tools you are using because every resequencing is treated as a change.
This project is written with .NET 3.5 and has not been tested with any other configurations that support LINQ. If you used this successfully
with .NET 3.0 or .NET 2.0 with SP1, please add a message outlining your experience so that others can benefit from it.
The Problem
When merging a Windows Forms form or control between two branches of a source control system, the merge tool typically displays many false conflicts
because insignificant changes in element sequences are treated as significant.
The Solution
This article describes a simple console application that can be used as a pre-comparison conversion to sort the elements in a .resx file by name attribute.
While you could use this filter standalone to modify the .resx files before comparison, many merge/diff utilities can be configured to run the conversion prior
to comparison and merge.
Because both files are sorted in a deterministic way, the merge/diff utility can accurately determine what elements are new and changed without introducing
false conflicts because of circumstantial differences in element location.
Implementation
I originally thought it would be simplest to implement an XSLT transform to sort the various elements in the .resx XML, but discovered, to my delight,
that I could use LINQ to achieve the same result trivially. The entire code for the project follows:
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Linq;
namespace SortRESX
{
class Program
{
static void Main(string[] args)
{
if (args.Length != 2)
{
ShowHelp();
return;
}
try
{
XDocument doc = XDocument.Load(args[0]); XDocument sortedDoc = SortDataByName(doc); sortedDoc.Save(args[1]); }
catch (Exception ex)
{
Console.Error.WriteLine("Error loading resx file {0}" +
"or error saving it to {1}: {2}", args[0], args[1], ex.Message);
}
return;
}
private static XDocument SortDataByName(XDocument resx)
{
return new XDocument(
new XElement(resx.Root.Name,
from comment in resx.Root.Nodes() where comment.NodeType ==
XmlNodeType.Comment select comment,
from schema in resx.Root.Elements() where schema.Name.LocalName ==
"schema" select schema,
from resheader in resx.Root.Elements("resheader")
orderby (string) resheader.Attribute("name") select resheader,
from assembly in resx.Root.Elements("assembly")
orderby (string) assembly.Attribute("name") select assembly,
from metadata in resx.Root.Elements("metadata")
orderby (string)metadata.Attribute("name") select metadata,
from data in resx.Root.Elements("data")
orderby (string)data.Attribute("name") select data
)
);
}
private static void ShowHelp()
{
string sExeName = System.Diagnostics.Process.GetCurrentProcess().ProcessName;
Console.Error.WriteLine("Command line format\n{0} <input resx file>
<output resx file>", sExeName);
}
}
}
There's not much to it. The input file is used to initialize a LINQ XDocument XML document. The SortDataByName() method
is called to return a sorted version of the document, and the converted file is saved to the target path. The real work is done
by the SortDataByName() method, which contains a single LINQ statement.
The LINQ statement constructs a new document with a single root element with the same name as the root element in the source document. The contents
of that element are defined by the six LINQ queries corresponding to the groups of nodes desired in the target:
- The standard comment node.
- The .resx schema element
- The resheader elements, sorted by name attribute
- The assembly elements, sorted by name attribute
- The metadata elements, sorted by name attribute
- The data elements, sorted by name attribute
And that's it. The SortRESX.exe program is now ready for integration with a merge/diff utility.
Integration with Merge/Diff Tools
Many commercial merge/diff utilities provide a mechanism that permits you to pre-process files with specified extensions using an external program before they
are compared or used for merging. By associating SortRESX.exe with .resx file types, the comparison is done against sorted files,
which enables only significant changes to appear. To associate SortRESX.exe with .resx file types for such a tool,
you can copy SortRESX.exe into the installation directory for the utility, then use the instructions provided in the utility to associate
it with the ".resx" file type.
Points of Interest
The same .resx sorting code used in this project could probably be used as an add-in to Visual Studio that would perform the sort every
time the .resx file is saved. This alternative approach has the advantage that it enables all diff/merge tools to behave non-pathologically
if all .resx files had been thus sorted. The disadvantage of this approach is that if the merge involved files created before the add-in was adopted,
the merge problem described above would still manifest itself.
History
- June 3, 2009: Initial version.
- June 15, 2009: Changed introductory paragraph.
I've been programming in C, C++, Visual Basic and C# for over 25 years. I've worked at Sierra Systems, ViewStar, Mosaix, Lucent, Avaya, Avinon, Apptero and now Serena in various roles over my career.
There was a time, before all that, when I was a foosball player, then a litigation attorney. My wife gave me a Tornado foosball table for my birthday, so I'm starting to feel the power again!