using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Text; using System.Xml; using System.IO; using System.Threading; using System.Globalization; using TradeIdeas.MiscSupport; using System.Xml.Serialization; /* These are some support tools to make it easier to use XML the way we are used * to using XML. There are two basic rules. 1) The top level node has to exist. * This represents the file, not an XML tag. (The top level tag should be the * only node in this node.) If the document didn't parse right the top level node * will be null. Also, if the input was null (meaning that there was a * communication error) the parser will return a null, not an exception. * 2) Nothing else has to exist. The server typically will not create nodes or * attributes unless they contain something. The following line means that the * type of list2 is "exclude": * * The following all mean the same thing, the type of LIST2 is the default. * * * * * It is up to the client and server to agree on what the default should be for * a value. Typically it's the empty string, but it can be anything. * The main focus of these routines is to deal with nulls and other missing * data so the main program doesn't have to. It deals with missing data by * returning a default value. */ namespace TradeIdeas.XML { /// /// These are some support tools to make it easier to use XML the way we are used to using XML. /// In particular, all errors just cause us to return null or a default value. /// public static class XmlHelper { /// /// This converts an array of bytes into an XML document. /// /// The bytes to parse. /// The document we created, or null on error. public static XmlDocument Get(byte[] raw) { if (raw == null) // For simplicity, one error is as good as the next. return null; XmlDocument result = new XmlDocument(); using (MemoryStream stream = new MemoryStream(raw)) { try { result.Load(stream); } catch { // The server certainly tries to send only valid XML. result = null; } } return result; } /// /// This is returned by Enum() sometimes when there is no other data. /// This is just cached so we don't have to create a new empty list /// every time we need one. /// private static readonly IEnumerable EmptyList = new List().AsReadOnly(); /// /// This allows the user to enumerate over the tags in this node. /// This skips comments and possibly other items to get to the tags. /// This not very efficient if the caller is planning to break before /// reading all the items. /// /// /// all of the XmlElements, i.e. tags, contained directly in this node, which might be the empty list. public static IEnumerable Enum(this XmlNode node) { if (node == null) // I think this is okay but I can't find anything in writing. Presumably foreach // is a read only operation and it's safe to do read only operations in multiple // threads without a lock. Also I think that it's okay to iterate over an empty // list of any type. I don't think the type check is done (for non-generic // collections) until we see an actual item in the foreach. return EmptyList; else { List list = new List(); foreach (XmlNode child in node) { XmlElement element = child as XmlElement; if (null != element) list.Add(element); } return list; } } /// /// Find a child by number. As always, the various error conditions all /// return null. This will skip over items which are not XmlElements, /// like and . /// Note: If you plan to look at a lot of items, Enum() will be much more effecient. /// This takes O(N) time per call, so O(N*N) to look at every item this way. /// /// The parent node to look in. This can be null. /// Which child to find. 0 is the first child. /// The nth XmlElement in the parent. public static XmlElement Node(this XmlNode node, int index) { if (node == null) return null; if (index < 0) return null; foreach (XmlNode child in node) { XmlElement element = child as XmlElement; if (null == element) // This is a comment or something. Skip it. Don't count it! continue; if (index == 0) // Found the nth XmlElement! return element; // Found an XmlElement. We made progress. Count it. index--; } // Not enough XmlElements. return null; } /// /// Look up a child by name /// /// The parent node. /// The name of the child to find. /// The first child with the given name. null on error. public static XmlElement Node(this XmlNode node, string index) { if (node == null) return null; // The delphi version would return null if there were two or more items with this name. // We never really used that, except as a primative form of error checking. return node[index]; } /// /// Looks for a property in a given node. /// /// The node to search for the property. /// The name of the property. /// The value to return if the property is not found. /// The value we found. Any errors will cause us to return def. public static string Property(this XmlNode node, string name, string def = "") { if (node == null) return def; XmlAttribute attribute = node.Attributes[name]; if (attribute == null) return def; return attribute.Value; } /// /// Look up a property and convert it to an integer. /// /// Look in here for the property. /// The name of the property. /// Return this value if the property is not found or is invalid. /// The value of the property, or def public static int Property(this XmlNode node, string name, int def) { string asString = Property(node, name); int result; if (ServerFormats.TryParse(asString, out result)) return result; else return def; } /// /// Look up a property and convert it to an int64. /// /// Look in here for the property. /// The name of the property. /// Return this value if the property is not found or is invalid. /// The value of the property, or def public static Int64 Property(this XmlNode node, string name, Int64 def) { return node.PropertyInt(name) ?? def; } /// /// Look up a property and convert it to an int64. /// /// Look in here for the property. /// The name of the property. /// The value of the property, or null if there's a problem. public static Int64? PropertyInt(this XmlNode node, string name) { string asString = Property(node, name); Int64 result; if (Int64.TryParse(asString, out result)) return result; else return null; } /// /// Look up a property and convert it to an int32. /// /// Look in here for the property. /// The name of the property. /// The value of the property, or null if there's a problem. public static Int32? PropertyInt32(this XmlNode node, string name) { string asString = Property(node, name); Int32 result; if (Int32.TryParse(asString, out result)) return result; else return null; } /// /// Look up a property and convert it to a boolean. /// /// "0" and "1" convert to false and true, accordingly. Anything that C# normally considers /// a boolean will get converted in the normal way. Anything else is a failure and will /// return a default value. /// /// This is my own custom set of rules. PHP will, by default, convert false to "" and back. /// However, it seems like "" is better interpreted as a missing value. /// /// Look in here for the property. /// The name of the property. /// Return this value if the property is not found or is invalid. /// The value of the property, or def public static bool Property(this XmlNode node, string name, bool def) { return PropertyBool(node, name) ?? def; } /// /// Looks for a property and tries to convert it to a boolean. /// /// See the previous function for more details. /// /// Look in here for the property. /// The name of the property. /// The value, or null if we couldn't find or parse the value. public static bool? PropertyBool(this XmlNode node, string name) { string asString = Property(node, name); bool result; Int64 intResult; if (asString == "0") // This is a very common way to marshal a false value. return false; if (asString == "1") // This is a very common way to marshal a true value. return true; if (Boolean.TryParse(asString, out result)) // This is the C# way of marshaling a boolean. I think "True" works but I can't find // an exact set of rules. "0" and "1" both fail. return result; else if (Int64.TryParse(asString, out intResult)) return intResult != 0; else return null; } public static Color Property(this XmlNode node, string name, Color def) { return PropertyColor(node, name)??def; } public static Color InvalidColor { // Because Property() returns a Color, not a Color?, there is no obvious // way to see if the color exists or not. You can set the default to this // then you can see if you got this back. This is not perfect, but it // seems reasonable. How many shades of transparent do you need? get { return Color.FromArgb(0x00badbad); } } public static Color? PropertyColor(this XmlNode node, string name) { string asString = Property(node, name); int asInt; if (ServerFormats.TryParse(asString, out asInt)) return SmartColor(asInt); else return null; } public static double Property(this XmlNode node, string name, double def) { return node.PropertyDouble(name)??def; } public static double? PropertyDouble(this XmlNode node, string name) { string asString = Property(node, name); double result; if (ServerFormats.TryParse(asString, out result)) return result; else return null; } public static E PropertyEnum(this XmlNode node, string name, E def) /* where E : System.Enum * You cannot use System.Enum in this type of constraint. * http://connect.microsoft.com/VisualStudio/feedback/details/93336/constraint-cannot-be-special-class-system-enum-why-not * Without this constraint, I can't call this Property(), or almost anything would match. */ { string asString = Property(node, name); try { // TryParse requires dot net 4.0. return (E)System.Enum.Parse(typeof(E), asString); } catch { } return def; } public static string Text(this XmlNode node, string def = "") { if (null == node) return def; return node.InnerText; } public static string SafeName(this XmlNode node, string def = "") { if (null == node) return def; return node.LocalName; } public static decimal Property(this XmlNode node, string name, decimal def) { return node.PropertyDecimal(name) ?? def; } public static decimal? PropertyDecimal(this XmlNode node, string name) { string asString = Property(node, name); decimal result; if (ServerFormats.TryParse(asString, out result)) return result; else return null; } // This is consistent with the way we look up string resources. We try to find something // specific to the current culture. Failing that we look for something more and more // generic, eventually falling back on the default. // // For example, if name is "TEXT", we might start by looking for "en-US_TEXT", then for // "en_TEXT", and finally "TEXT" before giving up. // // This was originally aimed at config files distributed with the client, which ran the // GUI. But eventually some server messages were added which use this same format. // // Is that right? It works with TEXT and de_TEXT. I haven't tried en-US_TEXT. I don't // know if that's a valid name for an XML attribute. Maybe I need to translate that to // en_US_TEXT. TODO: research and/or test. public static string PropertyForCulture(this XmlNode node, string name, string def = "") { //CultureInfo cultureInfo = CultureInfo.CurrentCulture; CultureInfo cultureInfo = Thread.CurrentThread.CurrentUICulture; while (true) { if (cultureInfo == CultureInfo.InvariantCulture) return node.Property(name, def); else { string cultureName = cultureInfo.Name; string result = node.Property(cultureName + "_" + name, null); if (null != result) return result; cultureInfo = cultureInfo.Parent; } } } // This next section allows you to have a list of xml files and search through all of them. // // First: // Second: // list.Node(0).Node("ONE").Property("VALUE") --> "used" // list.Node(0).Node("TWO").Property("VALUE") --> "used" // list.Node(0).Node("THREE").Property("VALUE") --> "used" // list.Node(0).Node("FOUR").Property("VALUE") --> "" // // The list can be empty, but it is never null. XmlNodes are typically excluded from the // list if they are null, for performance reasons. However, a list with a null in it will // not fail. The list returned by Node() should be treated as read-only. That allows for // some optimizations. I can't imagine that someone would need to change the list, so // that seems like an easy choice. // // Select() reduces a list of XmlNodes to a single node. The *last* non-null node in the // list always takes priority. This allows you to ensure some consistency in your // choices. // // First: // Second: // Third: // XmlNode colorScheme = list.Node("API").Node("GUI").Node("COLORS").Select(); // colorScheme.Node("FG").Property("black") --> "green" // colorScheme.Node("BG").Property("white") --> "white" // In this case we choose all of our colors from the second XML file. Without the // call to Select() we'd be using green on green! // // Property() and PropertyForCulture() both call Select() to find a node before looking // inside the node for a property. // // First: // Second: // If the language is English, list.Node(0).Node("OK_BUTTON").PropertyForCulture("TEXT", "***") will return // "***". If you really want to override the translation in a file, you have to copy the entire node, // like we did for the FLIP_BUTTON. I suspect that case is rare. If you add a translation, you probably // want to do it for all clients. // // The syntax is set up so the list routines can almost be a drop in replacement for the // single node routines. There is (presumably) a global variable for the config file. The main program // will change to load a list of config files. Anyone naively using // configFile.Node().Node().Node().Property() doesn't even have to know about the change. A lot of code // will save an intermediate result. This is done in part for performance and in part to make the // code shorter. In this case you'd have to change the type of the variable storing the result, but // this should be easy to do. The compiler will help you find these cases automatically. // // This is mainly aimed at the config files. There is too much duplication in there. It seems pretty // obvous looking at them which items go in a common file and which ones are specific to one application. // Pretty much all the translations go into the common file, and everything else stays in NorthAmerica.xml // or Xetra.xml. But that decision is always put off until runtime, so other configurations are possible. // Presumably we will continue to use single node functions, not the list functions, to look at the // messages from the server, and the saved layouts. // Choose the last non-null item. That's consistent with the way the server typically works. More generic // stuff first. Most specific stuff last. Last one overrides the first one. public static XmlNode Select(this List nodes) { for (int i = nodes.Count - 1; i >= 0; i--) if (null != nodes[i]) return nodes[i]; return null; } // Like Property() and PropertyForCulture(), this calls Select() then enumerates the children of the // resulting node. This does not try to combine all of the nodes in the list. public static IEnumerable Enum(this List nodes) { return nodes.Select().Enum(); } public static string PropertyForCulture(this List nodes, string name, string def = "") { return nodes.Select().PropertyForCulture(name, def); } public static List Node(this List nodes, int index) { List result = new List(); foreach (var node in nodes) { XmlNode possible = node.Node(index); if (null != possible) result.Add(possible); } return result; } public static List Node(this List nodes, string index) { List result = new List(); foreach (var node in nodes) { XmlNode possible = node.Node(index); if (null != possible) result.Add(possible); } return result; } public static string Property(this List nodes, string name, string def = "") { return nodes.Select().Property(name, def); } public static int Property(this List nodes, string name, int def) { return nodes.Select().Property(name, def); } public static bool Property(this List nodes, string name, bool def) { return nodes.Select().Property(name, def); } public static Color Property(this List nodes, string name, Color def) { // I doubt if this will be used as the single node version of this was aimed // at layouts, not the config file. return nodes.Select().Property(name, def); } public static double Property(this List nodes, string name, double def) { return nodes.Select().Property(name, def); } public static E PropertyEnum(this List nodes, string name, E def) { return nodes.Select().PropertyEnum(name, def); } public static string Text(this List nodes, string def = "") { return nodes.Select().Text(def); } public static string SafeName(this List nodes, string def = "") { return nodes.Select().SafeName(def); } // This next section is different because these methods will actually change the XML node. public static XmlElement NewNode(this XmlNode node, string name = "NODE") { // This will add a new child at the end of the list of children. if (null == node) // For consistency with the rest of the methods, this will return null if the // input is null. There's no point in us reporting a null pointer exception here. // If the user did not expect a null pointer, he'll get the same results a little // further down the line. return null; XmlElement result = node.OwnerDocument.CreateElement(name); node.AppendChild(result); return result; } public static XmlNode SetProperty(this XmlNode node, string name, string value) { // Set the specified attribute of the specified element to the specified value. // I'm not sure why this is so hard! // This will fail if node is null. It seems like you are actually trying to // change something, and a null pointer implies a programmer error. Unlike // NewNode(), I don't expect that error to get caught anywhere else, so we // catch it here. XmlAttribute attribute = node.OwnerDocument.CreateAttribute(name); attribute.Value = value; node.Attributes.SetNamedItem(attribute); // return the original node for the sake of chaining: // FindNode().SetAttribute("X","5").SetAttribute("Y","10"); return node; } public static XmlNode SetProperty(this XmlNode node, string name, Color value) { // For simplicity we always convert the color to a number. This includes the most important // part of a color, but not everything. If you save a color as a number, and then restore // it, == might not return true. In particular, a named color will forget its name! That // could be fixed with a lot more code. For some reason C# doesn't have a standard way to // export and import a color (or I couldn't find it!) and I didn't think it was worth it to // do it myself. return SetProperty(node, name, value.ToArgb()); } public static XmlNode SetProperty(this XmlNode node, string name, Int64 value) { return SetProperty(node, name, ServerFormats.ToString((double)value)); } public static XmlNode SetProperty(this XmlNode node, string name, int value) { return SetProperty(node, name, ServerFormats.ToString((double)value)); } public static XmlNode SetProperty(this XmlNode node, string name, decimal value) { return SetProperty(node, name, ServerFormats.ToString((double)value)); } public static XmlNode SetProperty(this XmlNode node, string name, double value) { // This is to be consistent with the way we read from XML. Since we use the same routines // to read a config file as a file from the server, we always expect the config file to // use the standard us/english notation for numbers. So we also have to save our config // files the same way. return SetProperty(node, name, ServerFormats.ToString(value)); } public static XmlNode SetProperty(this XmlNode node, string name, double? value) { // This is to be consistent with the way we read from XML. Since we use the same routines // to read a config file as a file from the server, we always expect the config file to // use the standard us/english notation for numbers. So we also have to save our config // files the same way. return SetProperty(node, name, ServerFormats.ToString(value)); } public static XmlNode SetProperty(this XmlNode node, string name, object value) { // This just seems so common, I had to add the code here. return SetProperty(node, name, value.ToString()); } private static readonly Dictionary _namedColorsByValue = new Dictionary(); static XmlHelper() { foreach (KnownColor knownColor in System.Enum.GetValues(typeof(KnownColor))) { Color color = Color.FromKnownColor(knownColor); if (color.IsNamedColor && !color.IsSystemColor) // Get rid of system colors. We don't use them. They would probably add // more confusion than anything else. And, most imporantly, they would // probably cause a lot of duplicate entries. I would hate to display // "ControlText" when most people would call the color "Black." // // It's tempting to say that the system colors have a lower priority. // So we'd say "Black" and not "ControlText". But the system colors are // not unique, so they'd still fight with each other. _namedColorsByValue[color.ToArgb()] = color; } } /// /// Note: Color.FromArgb(Color.Blue.ToArgb()).Equals(Color.Blue) returns false! /// /// If you display them on the screen, both colors will look identical. But one has a name and /// the other doesn't. /// /// We don't save the names of colors, just the values, in XML. That was probably the right /// answer at the time. In any case, it would be almost impossible to change now. /// /// XmlHelper.SmartColor(Color.Blue.ToArgb()).Equals(Color.Blue) returns true! This function /// also works on colors that don't have names. In that case the result is the same as /// Color.FromArgb(); /// /// When you read a color from XML we always use SmartColor(). We just assume that's the /// better choice. That's consistent with the output of ColorDialog and /// ColorConverter.ConvertFromInvariantString(). If you get a color from somewhere else /// you can use this function make your results consistent. /// /// /// public static Color SmartColor(int argb) { Color result; if (_namedColorsByValue.TryGetValue(argb, out result)) return result; else return Color.FromArgb(argb); } public static XmlNode LoadFromString(string xmlString) { try { var doc = new XmlDocument(); doc.LoadXml(xmlString); return doc.DocumentElement; } catch (Exception ex) { string debugView = ex.ToString(); return null; } } public static XmlNode SerializeToXmlNode(T data) { var serializer = new XmlSerializer(typeof(T)); using (var sww = new StringWriter()) { using (XmlWriter writer = XmlWriter.Create(sww)) { serializer.Serialize(writer, data); return LoadFromString(sww.ToString()); } } } } }