Introduction
I've been using Windows Forms for the last 13 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 4.5, .NET 4.0, .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)
{
XmlReader inputStream = null;
if (args.Length > 2)
{
ShowHelp();
return;
}
if (args.Length == 0) {
try
{
Stream s = Console.OpenStandardInput();
inputStream = XmlReader.Create(s);
}
catch (Exception ex)
{
Console.WriteLine("Error reading from stdin: {0}", ex.Message);
return;
}
}
else {
string arg0 = args[0].ToLower();
if( arg0.StartsWith(@"/h") || arg0.StartsWith(@"/?"))
{
ShowHelp();
return;
}
try
{
inputStream = XmlReader.Create(args[0]);
}
catch(Exception ex)
{
Console.WriteLine("Error opening file '{0}': {1}", args[0], ex.Message);
}
}
try
{
XDocument doc = XDocument.Load(inputStream);
XDocument sortedDoc = SortDataByName(doc);
if (args.Length == 2)
sortedDoc.Save(args[1]);
else
{
Console.OutputEncoding = Encoding.UTF8;
sortedDoc.Save(Console.Out);
}
}
catch (Exception ex)
{
Console.Error.WriteLine(ex);
}
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()
{
Console.Error.WriteLine(
"0 arguments ==> Input from stdin. Output to stdout.\n" +
"1 argument ==> Input from specified .resx file. Output to stdout.\n" +
"2 arguments ==> Input from first specified .resx file." +
"Output to second specified .resx file.");
}
}
}
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 or stream. 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.
Update
I've updated the code and the article so that the utility works either with filename command line arguments, or stdin and stdout streams, so it can be directly used with comparison tools that use this mechanism for preprocessing the files prior to comparison. Any of the following methods work:
- SortRESX filename1.resx filename2.resx
- SortRESX < filename1.resx filename2.resx
- SortRESX < filename1.resx > filename2.resx
- Type filename1.resx | SortResx filename2.resx
- Type filename1.resx | SortResx > filename2.resx
If there are zero command line arguments, the utility reads from stdin and writes to stdout. If there is one, it reads from the specified file and writes to stdout. If there are two, they are treated as the input and output filenames.
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 or streams, 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.
- July 17, 2014: Added support for stdin and stdout.