Click here to Skip to main content
Click here to Skip to main content

Solving the .resx Merge Problem

, 17 Jul 2014 CPOL
Rate this:
Please Sign up or sign in to vote.
This article provides a console utility and a method for extending a merge tool to ensure that only real changes (not reordering) appear when merging files from different source control branches.

Comparison Results

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
{
  //
  // 0 cmd line params ==> input from stdin and output is stdout.
  // 1 cmd line param  ==> input source .resx (arg[0]). Output is stdout.
  // 2 cmd line params ==> input source .resx (arg[0]). Output is target .resx (arg[1])
  // The program reads the source and writes a sorted version of it to the output.
  //
  class Program
  {
    static void Main(string[] args)
    {
      XmlReader inputStream = null;

      if (args.Length > 2)
      {
        ShowHelp();
        return;
      }

      if (args.Length == 0) // Input resx is coming from stdin
      {
        try 
        {
          Stream s = Console.OpenStandardInput();
          inputStream = XmlReader.Create(s);
        }
        catch (Exception ex) 
        {
          Console.WriteLine("Error reading from stdin: {0}", ex.Message);
          return;
        }
      }
      else // Input resx is from file specified by first argument 
      {
        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
      {
        // Create a linq XML document from the source.
        XDocument doc = XDocument.Load(inputStream);
        // Create a sorted version of the XML
        XDocument sortedDoc = SortDataByName(doc);
        // Save it to the target
        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;
    }

    //
    // Use Linq to sort the elements.  The comment, schema, resheader, assembly, metadata, data appear in that order, 
    // with resheader, assembly, metadata and data elements sorted by name attribute.
    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
        )
      );
    }

    //
    // Write invocation instructions to stderr.
    //
    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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Tom Clement
Team Leader
United States United States
I've been programming in C, C++, Visual Basic and C# for almost 30 years. I've worked at Sierra Systems, ViewStar, Mosaix, Lucent, Avaya, Avinon, Apptero and now Serena in various roles over my career.

Comments and Discussions

 
QuestionWhere can I download the SortRESX.exe file? PinmemberMember 1100591611-Aug-14 11:28 
AnswerRe: Where can I download the SortRESX.exe file? PinmentorTom Clement22-Oct-14 8:37 
SuggestionIntegrate with kdiff3 (pipe) PinmemberPinx15-Jul-14 21:54 
GeneralRe: Integrate with kdiff3 (pipe) PinmentorTom Clement16-Jul-14 6:47 
GeneralRe: Integrate with kdiff3 (pipe) PinmentorTom Clement17-Jul-14 17:34 
GeneralRe: Integrate with kdiff3 (pipe) PinmemberPinx19-Jul-14 0:28 
GeneralRe: Integrate with kdiff3 (pipe) PinmentorTom Clement20-Jul-14 10:08 
GeneralRe: Integrate with kdiff3 (pipe) PinmemberPinx20-Jul-14 13:24 
SuggestionTeam Foundation Server PinmentorTom Clement11-Jul-14 8:17 
SuggestionCool, I may add this as a new feature to my ResXManager extension if someone votes for it. Pinmembertom-englert11-Jul-14 0:02 
GeneralMy vote of 5 PinmemberScruffyDuck23-Jun-14 5:32 
GeneralRe: My vote of 5 [modified] PinmentorTom Clement23-Jun-14 6:30 
GeneralRe: My vote of 5 PinmemberScruffyDuck23-Jun-14 6:37 
QuestionHow to do with Tortoise SVN PinmemberMember 103857286-Nov-13 23:18 
QuestionThanks PinmemberRavi Dasari26-Jul-13 8:31 
AnswerRe: Thanks PinmentorTom Clement26-Jul-13 9:24 
Generalexcellent work PinmemberM Sheik Uduman Ali7-Nov-09 2:31 
GeneralRe: excellent work PinmemberTom Clement7-Nov-09 7:14 
Answeranother way in Visual Studio Pinmemberbxb10-Jun-09 0:16 
GeneralRe: another way in Visual Studio PinmemberTom Clement10-Jun-09 6:10 
AnswerRe: another way in Visual Studio PinmemberDa_Hero31-Oct-12 2:04 
GeneralTicks all three boxes... PinmemberMaxxx_8-Jun-09 15:46 
GeneralRe: Ticks all three boxes... PinmemberTom Clement8-Jun-09 19:16 
GeneralAbsolutely Brilliant PinmemberAKHEIROL8-Jun-09 9:38 
GeneralRe: Absolutely Brilliant PinmemberTom Clement11-Jun-09 5:59 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.141223.1 | Last Updated 17 Jul 2014
Article Copyright 2009 by Tom Clement
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid