Click here to Skip to main content
15,880,469 members
Articles / Desktop Programming / Windows Forms

Solving the .resx Merge Problem

Rate me:
Please Sign up or sign in to vote.
4.88/5 (31 votes)
17 Jul 2014CPOL4 min read 116.3K   3.3K   42   34
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:

C#
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)


Written By
Product Manager
United States United States
I've been programming in C, C++, Visual Basic and C# for over 35 years. I've worked at Sierra Systems, ViewStar, Mosaix, Lucent, Avaya, Avinon, Apptero, Serena and now Guidewire Software in various roles over my career.

Comments and Discussions

 
SuggestionTeam Foundation Server Pin
Tom Clement11-Jul-14 7:17
professionalTom Clement11-Jul-14 7:17 
SuggestionCool, I may add this as a new feature to my ResXManager extension if someone votes for it. Pin
tom-englert10-Jul-14 23:02
tom-englert10-Jul-14 23:02 
GeneralRe: Cool, I may add this as a new feature to my ResXManager extension if someone votes for it. Pin
modiX20-Jun-16 22:37
modiX20-Jun-16 22:37 
GeneralRe: Cool, I may add this as a new feature to my ResXManager extension if someone votes for it. Pin
tom-englert21-Jun-16 19:49
tom-englert21-Jun-16 19:49 
GeneralRe: Cool, I may add this as a new feature to my ResXManager extension if someone votes for it. Pin
ZhaoRuFei18-Dec-17 15:47
ZhaoRuFei18-Dec-17 15:47 
GeneralMy vote of 5 Pin
ScruffyDuck23-Jun-14 4:32
ScruffyDuck23-Jun-14 4:32 
GeneralRe: My vote of 5 Pin
Tom Clement23-Jun-14 5:30
professionalTom Clement23-Jun-14 5:30 
GeneralRe: My vote of 5 Pin
ScruffyDuck23-Jun-14 5:37
ScruffyDuck23-Jun-14 5:37 
QuestionHow to do with Tortoise SVN Pin
Member 103857286-Nov-13 22:18
Member 103857286-Nov-13 22:18 
QuestionThanks Pin
Ravi Dasari26-Jul-13 7:31
Ravi Dasari26-Jul-13 7:31 
AnswerRe: Thanks Pin
Tom Clement26-Jul-13 8:24
professionalTom Clement26-Jul-13 8:24 
Generalexcellent work Pin
M Sheik Uduman Ali7-Nov-09 1:31
M Sheik Uduman Ali7-Nov-09 1:31 
GeneralRe: excellent work Pin
Tom Clement7-Nov-09 6:14
professionalTom Clement7-Nov-09 6:14 
Answeranother way in Visual Studio Pin
bxb9-Jun-09 23:16
bxb9-Jun-09 23:16 
GeneralRe: another way in Visual Studio Pin
Tom Clement10-Jun-09 5:10
professionalTom Clement10-Jun-09 5:10 
AnswerRe: another way in Visual Studio Pin
Da_Hero31-Oct-12 1:04
professionalDa_Hero31-Oct-12 1:04 
GeneralTicks all three boxes... Pin
_Maxxx_8-Jun-09 14:46
professional_Maxxx_8-Jun-09 14:46 
GeneralRe: Ticks all three boxes... Pin
Tom Clement8-Jun-09 18:16
professionalTom Clement8-Jun-09 18:16 
GeneralAbsolutely Brilliant Pin
AKHEIROL8-Jun-09 8:38
AKHEIROL8-Jun-09 8:38 
GeneralRe: Absolutely Brilliant Pin
Tom Clement11-Jun-09 4:59
professionalTom Clement11-Jun-09 4:59 
Generalcomparison tool Pin
sheitman806-Jun-09 13:02
sheitman806-Jun-09 13:02 
GeneralRe: comparison tool [modified] Pin
Tom Clement6-Jun-09 14:31
professionalTom Clement6-Jun-09 14:31 
GeneralSimple and yet genius Pin
Marcelo de Aguiar5-Jun-09 9:13
Marcelo de Aguiar5-Jun-09 9:13 
GeneralRe: Simple and yet genius Pin
Tom Clement5-Jun-09 9:27
professionalTom Clement5-Jun-09 9:27 

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

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