using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
using System.IO;
using System.Threading;
using System.Globalization;
using TradeIdeas.MiscSupport;
/* 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;
}
// 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 T Deserialize(string xml)
{
XmlSerializer s1 = new XmlSerializer(typeof(T));
using (TextReader sr = new StringReader(xml))
{
return (T)s1.Deserialize(sr);
}
}
public static string Serialize(object theObject)
{
using (var stream = new StringWriter())
{
using (var writer = XmlWriter.Create(stream, new XmlWriterSettings() { NamespaceHandling = NamespaceHandling.OmitDuplicates, Indent = true }))
{
XmlSerializer serializer = new XmlSerializer(theObject.GetType());
serializer.Serialize(writer, theObject);
return stream.ToString();
}
}
}
public static string Serialize2(object dataToSerialize)
{
if (dataToSerialize == null) return null;
using (StringWriter stringwriter = new System.IO.StringWriter())
{
var serializer = new XmlSerializer(dataToSerialize.GetType());
serializer.Serialize(stringwriter, dataToSerialize);
return stringwriter.ToString();
}
}
public static string SerializeWithoutDefaultNamespaces(object theObject)
{
using (var stream = new StringWriter())
{
using (var writer = XmlWriter.Create(stream, new XmlWriterSettings() { NamespaceHandling = NamespaceHandling.OmitDuplicates, OmitXmlDeclaration = true, Indent = true }))
{
XmlSerializer serializer = new XmlSerializer(theObject.GetType());
serializer.Serialize(writer, theObject, new XmlSerializerNamespaces(new[] { XmlQualifiedName.Empty }));
return stream.ToString();
}
}
}
}
}