using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Drawing; using TradeIdeas.MiscSupport; using TradeIdeas.XML; using System.Xml; using System.Collections; using System.Threading; using System.Reflection; namespace TradeIdeas.TIProData { /// /// A DataNode is a base class for presenting streaming data. It's a little like pub-sub, /// but DataNodes are easy to stack. If you want to "subscribe" in this system, you need /// to Find() a DataNode. While there are no restrictions on how that DataNode is /// implemented, it's a safe bet that the DataNode you found will call Find() on other /// DataNodes, forming a tree. (More precisely, a DAG. One DataNode can find two others, /// each of which share a fourth DataNode.) /// /// There are four basic operations related to DataNodes. /// 1) You can Find() a data node. Find() might create the DataNode from scratch. If /// it's at all possible, Find() will reuse an existing data node. This will typically /// return a DataNode object and a DataNode.Link object. Often Find() is a static /// method. Other times you create factory objects which will have a Find() method. /// 2) You can call DataNode.Link.Release() when you are done with the DataNode. This /// will stop the DataNode from making any callbacks to you. This will also decrement /// a reference counter. That will tell the DataNode to release its resources after /// the reference count goes to 0. Note that DataNode objects are shared, but you /// create a new Link object each time you call Find(). /// 3) You can ask the DataNode for its data. That's different for different DataNode /// classes. For the most part you can ask for this at any time. There might be /// restrictions, like only call this in a certain thread. And most DataNode classes /// have a way to say "invalid", "nothing yet, try again", or something like that. /// Often you want to call this in the following callback, but that's not required. /// 4) The DataNode can give you a callback when its data changes. This is a standard /// Action callback, and is the same for all DataNode classes. If the caller needs /// more information, create a delegate. /// /// Sharing data nodes is essential. We make it very cheap and easy to Find() the data /// you need. If you need data, you shouldn't try to think about what other parts of your /// program already have that data. Instead you should just ask for it from the source. /// We can only do that because sharing DataNodes is so cheap. Think about all the extra /// objects that would exist if every request created a new DataNode. Remember that /// DataNodes are usually built on top of other data nodes. So when you ask for the price /// if MSFT you might be indirectly creating a lot of objects. /// /// This is based heavily on /// cpp_alert_server/source/generate_alerts/misc_framework/DataNodes.C. That's how /// we manage data in the program that generates alerts. This idea has been evolving /// for a long time, and started with GUI programs. Here are a few important differences /// between this version and the C++ version: /// 1) In C++ we had to create our own variant type. We needed that for the key in /// the data structure that holds all known DataNode objects. In C# we're going /// to use an object for the key. C# gives us more. Notice the StaticList class /// which is helpful but not required for creating keys. /// 2) The C++ version almost exclusively worked in one thread. There were special /// routines to help move data into that thread. We are following the normal C# /// conventions and allowing more access from more threads. In particular, the old /// GUI versions would typically create DataNode objects from the GUI thread, while /// most of the work was done in a seperate DataNode thread. We assume that this /// will look similar to the older GUI versions. You need to access the same objects /// from the GUI thread and a data thread. We use locks to protect the shared data, /// like DataNode._all. Individual DataNode classes can add additonal restrictions /// on their users. /// 3) The old GUI versions had special support for sending data to the GUI. Among other /// things, this was a place where we could slow down the events. If MSFT has 5 /// trades in a row, most DataNode objects will see all 5 events. But the code that /// moves events to the GUI thread might only send one update. The result will be /// the same as if the user looked away for just a second: you might not see all /// of the updates, but the current state is correct. I'm planning to add something /// similar here. We definately need some support routines to make it easier to /// cross that barier. I'm not sure if the performance issues are as important as /// they once were, but I'm still planning to add some of that. /// 4) The C++ code had a DataNodeManager class. That mostly managed the thread. We /// don't have anything quite like that because in this version other parts of the /// program manage the threads. Some items have moved to static items in the /// DataNode class, like DataNode._all. StockDataManager, a typical source for our /// data, handles a lot of the threading stuff that DataNodeManager handled in C++. /// 5) The C++ code had a special listener class for the callbacks. That's a place /// where C# makes things much easier than C++. We just use Action for callbacks. /// 6) The C++ code, and other legacy systems, all had the idea of broadcast messages. /// A lot of that was just getting a message into a thread. As I've said, this C# /// implementation doesn't typically manage threads. The other issue in C++ and /// Delphi was finding the target of a message. For example, we'd create a new /// DataNode in the GUI thread. The constructor would mark all data as invalid. /// Then the constructor would send itself a message, telling itself to wake up in /// a different thread and do the rest of it's initialization. In C# that's trivial /// with a delegate. The delegate will automatically capture a pointer to the /// DataNode. If we tried to use a pointer in Delphi or C++ there was danger that /// the item would be deleted before the message arrived, and when we tried to /// delever the message we'd be accessing an invalid pointer. Again, C# gives us /// autoamtic garbage collection, so that problem goes away. /// public class DataNode { // Notes on locks: // 1) It is legal to lock _listeners while already holding a lock on _all. // 2) Except for rule #1, you cannot request a lock if you are holding a different lock. /// /// A description of all data nodes. /// This is only for debugging. And this only makes sense when there are only a /// small number of these. /// /// /// Something userfriendly. Probably includes \r\n between items. /// Details are likely to change from one version to the next. /// public static string DebugDumpAll() { StringBuilder sb = new StringBuilder(); lock (_all) { sb.Append(_all.Count).Append(" DataNode objects:"); foreach (var kvp in _all) sb.Append("\r\n").Append(kvp.Key); } return sb.ToString(); } private static readonly Dictionary _all = new Dictionary(); public interface Link { void Release(); } private class LinkImpl : Link { private readonly DataNode _dataNode; private readonly Action _listener; void Link.Release() { _dataNode.RemoveLink(this); } public LinkImpl(DataNode dataNode, Action listener) { _dataNode = dataNode; _listener = listener; } public void NotifyListener() { if (null != _listener) try { _listener(); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } } public bool HasListener { get { return null != _listener; } } } public interface IReplaceWith { /// /// This is aimed at Factory objects. Some objects are incomplete but read only. /// ReplaceWith will not modify the original object, but it will return a new /// object. The new object is just like the original, except that the replacements /// have been made. /// /// Note that we aren't looking for strings. We are looking for PlaceHolder objects. /// Each placehold has a name, which is a string. /// /// /// object ReplaceWith(Dictionary replacements); /// /// True if we can't find any PlaceHolder objects. /// /// If you try to Find() a factory and it has placeholders, that will throw an /// exception. This lets us check first so we can present the user with a reasonable /// error message. /// /// Note, Find() could still fail with an exception. You should always prepair for /// that. This function only helps in some cases. /// /// Remember that we're letting the user take more control of the process of building /// these objects. Normally this would be an assertion failed. But now we need to /// present nice error messages and make sure the rest of the program doesn't stop. /// /// bool ReplacementsComplete(); } [XmlSerializerInit("*")] public class PlaceHolder : IReplaceWith, XmlSerializer.Self { public override int GetHashCode() { return Key.GetHashCode(); } public override bool Equals(object obj) { if (ReferenceEquals(this, obj)) return true; PlaceHolder otherAsPlaceHolder = obj as PlaceHolder; if (null == otherAsPlaceHolder) return false; return Key == otherAsPlaceHolder.Key; } public readonly string Key; public PlaceHolder(string key) { Key = key; } object IReplaceWith.ReplaceWith(Dictionary replacements) { object replacement; if (replacements.TryGetValue(Key, out replacement)) return replacement; else return this; } bool IReplaceWith.ReplacementsComplete() { return false; } public override string ToString() { StringBuilder result = new StringBuilder(); result.Append("PlaceHolder(").Append(Key).Append(')'); return result.ToString(); } private const string PLACE_HOLDER = "PLACE_HOLDER"; public void Encode(XmlElement body) { body.SetProperty(XmlSerializer.TYPE, PLACE_HOLDER); body.SetProperty(XmlSerializer.VALUE, Key); } private static void XmlSerializerInit() { XmlSerializer.RegisterDecoder(PLACE_HOLDER, Decode); } private static object Decode(XmlElement top) { String value = top.Property(XmlSerializer.VALUE, null); if (null == value) throw new ArgumentException("Unable to find VALUE for PlaceHolder."); return new PlaceHolder(value); } public static readonly PlaceHolder SYMBOL = new PlaceHolder("Symbol"); } /// /// This is like a normal PlaceHolder, but the type of the object is DataNode.Factory. /// Some functions always want to take a Factory as their input. If you try to replace /// this place holder with something other than a Factory, it will throw an exception. /// public class FactoryPlaceHolder : Factory, XmlSerializer.Self { public readonly string Key; public object MetaData { get { // There's no great answer here. This is used by GuiDataNode.GetFacory() to // make some optimizations. GetFactory() wants the know the exact type of // object this factory will turn into, and it wants to know now. But this // is a placeholder and we can't know until later. This seems like a SAFE // answer. Worst case we'll have an unnecessary GuiDataNode, but the result // will be correct. return typeof(FactoryPlaceHolder); } } public FactoryPlaceHolder(string key) { Key = key; } object IReplaceWith.ReplaceWith(Dictionary replacements) { object replacement; if (replacements.TryGetValue(Key, out replacement)) return (DataNode.Factory)replacement; else return this; } bool IReplaceWith.ReplacementsComplete() { return false; } public override string ToString() { StringBuilder result = new StringBuilder(); result.Append("PlaceHolder(").Append(Key).Append(')'); return result.ToString(); } public void Encode(XmlElement body) { body.SetProperty(XmlSerializer.TYPE, "FACTORY_PLACE_HOLDER"); body.SetProperty(XmlSerializer.VALUE, Key); } public Link Find(out DataNode dataNode, Action callback = null) { throw new NotImplementedException(); } } /// /// Try to apply IReplaceWith.ReplaceWith(). /// /// /// Start from this object. /// Presumably the result will be this object or something similar to it. /// /// A list of replacements to make. This will be pass on to IReplaceWith.ReplaceWith() /// /// Sometimes we know there's nothing for us to do. In that case we quickly return the original target, unmodified, /// and we DON'T TOUCH the possiblyChanged argument. Other times it looks like there's work so we call /// IReplaceWith.ReplaceWith() and we set possiblyChanged to true. You should initialize possiblyChanged to false before /// calling this. Note: After calling ReplaceWithHelper() several times in a row possiblyChanged will be true if at least /// one of the calls set it to true, which is what you want. /// /// If you want to know for certain if something changed, try Equals(). We skip that step because Equals() can be /// expensive. /// /// /// Possibly a new object that looks just like the original, except for the requested replacements. /// If no replacements apply, this might return the exact same object, or it might return a new object /// that Equals() to original. /// private static object ReplaceWithHelper(object target, Dictionary replacements, ref bool possiblyChanged) { IReplaceWith t = target as IReplaceWith; if (null == t) return target; else { possiblyChanged = true; return t.ReplaceWith(replacements); } } /// /// This is basically a delegate, with a couple of nice properties. /// 1) Most of the data has been seperated from the function call. This allows us /// to inspect and modify the Factory object. /// 2) Delegates have terrible hash codes. This forces the creator to give another /// object represent the delegate. We use that second object to help us build a /// better hash code. /// public interface Factory : IReplaceWith { /// /// Find an instance of the DataNode described by this factory. This might create /// a new object, or it might reuse an existing object. This might throw an exception, /// in particular if you haven't called ReplaceWith() on all of the placeholders. /// /// Use this to access the data you requested. /// /// This will be called each time the data changes. null means don't do anything /// when the data changes. We have some optimizations for the null case. /// /// Use this link to release the DataNode when you are done with it. Link Find(out DataNode dataNode, Action callback = null); /// /// This can return anything, including null. /// /// In general this has no specific meaning. However, certain types of DataNode /// classes give it meaning. /// /// Mostly we use this to determine the type of a Factory. It is inconveneint to /// create a new class for each type of Factory. (I've tried that!!) Instead we /// add some type of tag, like a class or a unique object, and we store that tag /// in the Factory. This could have additional information, but it almost /// certainly needs to start by identifying the type of the Factory. What if you /// read a value of 5? What does that mean? This is a delayed DataNode that /// waits 5 seconds before passing on any notifications? This is a filter data /// node that will tell you if the price is above a certain value, in this case /// $5? /// /// See the GuiDataNode class for an actual example where this is used. /// object MetaData { get; } } /// /// 1) Most of the data has been seperated from the function call. This allows us /// to inspect and modify the Factory object. /// 2) Delegates have terrible hash codes. This forces the creator to give another /// object represent the delegate. We use that second object to help us build a /// better hash code. /// protected internal class FactoryWithHash : Factory, XmlSerializer.Self { Link Factory.Find(out DataNode dataNode, Action callback) { lock (_all) { if (!_all.TryGetValue(this, out dataNode)) { dataNode = _worker(_arguments); dataNode._key = this; _all[this] = dataNode; } // Don't release _all until we do this. We haven't updated the reference // count yet. If we released _all it's possible that someone could drop // this item's reference count return dataNode.CreateLink(callback); } } public delegate DataNode Worker(object arguments); private readonly Worker _worker; private readonly int _hashCode; private readonly int _hashContribution; private readonly object _arguments; private readonly string _name; public object Arguments { get { return _arguments; } } /// /// This is required by the Factory interface. /// public object MetaData { get; private set; } /// /// Constructor. /// /// The delegate which will actually create the new object. /// /// This input will contribute to the hash code of the resulting object. GetHashCode() /// for delegates is known to suck. Try to pick something that is unique to this worker. /// /// For example, we expect that most DataNode classes will create exactly one type of /// factory. The arguents might vary, but the DataNode will probably have exactly one /// unique Worker. So use that class to get the hashContribution. /// /// Note that we use worker to compute FactoryWithHash.Equals() but we use /// hashContribution to compute FactoryWithHash.GetHashCode(). It is up to the caller /// to make sure these two inputs are consistent. /// /// /// This will be passed into the worker as its input. This can be anything, including /// null. If you call ReplaceWith on this object, it will try to call ReplaceWith on /// its arguments. /// /// /// This is only used for debugging. If name is null, we'll try to get a /// name from worker, but the result is surprisingly hard to read. /// /// /// This is used to populate the MetaData field. This can be null. /// private FactoryWithHash(Worker worker, int hashContribution, object arguments, string name, object metaData) { _worker = worker; _hashContribution = hashContribution; _arguments = arguments; if (null == arguments) _hashCode = hashContribution ^ typeof(FactoryWithHash).GetHashCode(); else _hashCode = hashContribution ^ typeof(FactoryWithHash).GetHashCode() ^ arguments.GetHashCode(); _name = name ?? worker.ToString(); MetaData = metaData; } private static string GetXmlName(Type type) { return type.Name; } /// /// Same as above. The only difference is that we let you supply a type for the hash /// contribution. (Presumably this will be the class of the method that's calling /// this constructor.) We get the hash code of that type. Also, we get the name from /// that type. And the MetaData is that type. /// /// /// /// public FactoryWithHash(Worker worker, Type hashContribution, object arguments) : this(worker, hashContribution.GetHashCode(), arguments, GetXmlName(hashContribution), hashContribution) { } public override bool Equals(object obj) { if (object.ReferenceEquals(this, obj)) // Same instance. return true; FactoryWithHash other = obj as FactoryWithHash; if (null == other) // obj was null or obj was the wrong type. return false; return _worker.Equals(other._worker) && _arguments.Equals(other._arguments); } public override int GetHashCode() { return _hashCode; } public override string ToString() { StringBuilder result = new StringBuilder(); result.Append("Factory(").Append(_name).Append(", ").Append(_arguments).Append(')'); return result.ToString(); } object IReplaceWith.ReplaceWith(Dictionary replacements) { bool possiblyChanged = false; object replacementArgs = ReplaceWithHelper(_arguments, replacements, ref possiblyChanged); if (possiblyChanged) return new FactoryWithHash(_worker, _hashContribution, replacementArgs, _name, MetaData); else return this; } bool IReplaceWith.ReplacementsComplete() { IReplaceWith r = _arguments as IReplaceWith; if (r == null) return true; else return r.ReplacementsComplete(); } private const string ARGS = "ARGS"; public static XmlElement GetArgs(XmlElement factory) { XmlElement result = factory.Node(ARGS); if (null == result) throw new ArgumentException("Could not find ARGS in DataNode.Factory."); return result; } /// /// Use this if you want to use a custom function to translate this type from XML back /// to a FactoryWithHash object. /// /// Consider using RegisterStandardDecoder() or [XmlSerializerInit]. Those will /// automatically do a lot of the work for you, and in most cases that's all you need. /// /// Note that FactoryWithHash has it's own encoder which is always used. Your /// decoder should be compatible with that. /// /// /// public static void RegisterDecoder(Type factoryType, Func decoder) { XmlSerializer.RegisterDecoder(GetXmlName(factoryType), decoder); } /// /// This will create and register a decoder for a certain type of object. /// /// The assumption is that the factory contains a StaticList of ojects. These will /// be fed back into a static public method called GetFactory(). If the class has more /// than one function with that name, we should be able to figure out which one to call /// based on the type and number of objects in the StaticList. /// /// Normally you'd tag the class with [XmlSerializerInit] rather than calling this /// function directly. However, you might want to do additional work in the the /// initialization phase. Then you can say [XmlSerializerInit(MyInitFunction)]. /// MyInitFunction can handle the other work, then call RegisterStandardDecoder(). /// /// /// This is typically the type of the DataNode. This should be the same type as used /// in the hashContribution argument to the FactoryWithHash constructor. /// public static void RegisterStandardDecoder(Type factoryType) { RegisterDecoder(factoryType, xml => StandardDecoder(factoryType, xml)); } private static DataNode.Factory StandardDecoder(Type type, XmlElement source) { object args = XmlSerializer.Decode(GetArgs(source)); try { // Look for a GetFactory() method with one argument matching the argument // we just read. Notice the order in which we check. It is possible that // someone created a Factory for a ConstantDataNode and the argument was // a StaticList. In that case we want to call GetFactory() directly on // that list, not on the members of that list. object[] argsAsArray = new object[] { args }; object result = type.InvokeMember("GetFactory", BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public | BindingFlags.OptionalParamBinding, null, null, argsAsArray); return (DataNode.Factory)result; } catch { } { StaticList argsAsList = args as StaticList; if (null == argsAsList) throw new ArgumentException("Unable to reconstruct Factory for type " + type + " from object of type " + ((null == args) ? "null" : args.GetType().ToString())); // ARGS is of type StaticList, so look for a GetFactory() method whose signature // matches the elements of that list. object[] argsAsArray = argsAsList.GetItems(); object result = type.InvokeMember("GetFactory", BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public | BindingFlags.OptionalParamBinding, null, null, argsAsArray); return (DataNode.Factory)result; } } public void Encode(XmlElement body) { body.SetProperty(XmlSerializer.TYPE, _name); XmlElement args = body.NewNode(ARGS); XmlSerializer.Encode(_arguments, args); } } /// /// A list that's appropriate for a key in a hash table. The list can never change. The /// hash code is computed once when the list is created and then cached. The list /// recursively inspects each of its items to compute the hash code and to implement /// Equals(). We can't enforce it, but this only works if the individual items never /// change. /// [XmlSerializerInit("*")] public class StaticList : IReplaceWith, IEnumerable, XmlSerializer.Self { private readonly int _hashCode; private readonly object[] _items; /// /// Don't modify this array! /// /// public object[] GetItems() { return _items; } /// /// Mostly aimed at debugging. /// /// The contents of the list in a human readable form. public override string ToString() { StringBuilder result = new StringBuilder("StaticList["); bool first = true; foreach(object item in _items) { if (first) first = false; else result.Append(", "); if (null == item) result.Append("null"); else if (item is TransformationDataNode.Transformation) // Perhaps this should apply to any Delegate type? result.Append(((TransformationDataNode.Transformation)item).Method); else result.Append(item); } result.Append(']'); return result.ToString(); } public StaticList(System.Collections.IList source) { _items = new object[source.Count]; source.CopyTo(_items, 0); // If you need to create a hash code based on a list of items, some of which might be // null, consider using the Hasher class instead of copying this code. It does pretty // much the same thing as this, but it's easier to reuse. uint hashCode = (uint)typeof(StaticList).GetHashCode(); foreach (object item in _items) { hashCode = (hashCode << 3) | (hashCode >> 29); if (null != item) hashCode |= (uint)item.GetHashCode(); } _hashCode = (int)hashCode; } public static StaticList Create(params object[] items) { // This is a little silly. We want an array of objects, and we're starting // with an array of objects. But we make a copy anyway. The problem is that // we want a new array that no one else can touch. return new StaticList(items); } public override int GetHashCode() { return _hashCode; } /// /// A StaticList can only be equal to another StaticList. /// Recursively compare the items in this list to the other list. /// Uses a few optimizations. /// /// May be null /// public override bool Equals(object obj) { if (object.ReferenceEquals(this, obj)) // Same instance. return true; StaticList other = obj as StaticList; if (null == other) // Different type return false; return Equals(other); } /// /// Recursively compare the items in this list to the other list. /// /// May not be null. /// private bool Equals(StaticList other) { if (_hashCode != other._hashCode) return false; if (_items.Length != other._items.Length) return false; for (int i = 0; i < _items.Length; i++) if (!object.Equals(_items[i], other._items[i])) return false; return true; } object IReplaceWith.ReplaceWith(Dictionary replacements) { bool possiblyChanged = false; List newItems = new List(_items.Length); foreach (object item in _items) newItems.Add(ReplaceWithHelper(item, replacements, ref possiblyChanged)); if (possiblyChanged) return new StaticList(newItems); else return this; } // for IReplaceWith public bool ReplacementsComplete() { foreach (object item in _items) if (item is IReplaceWith) if (!((IReplaceWith)item).ReplacementsComplete()) return false; return true; } public StaticList ReplaceWith(Dictionary replacements) { return (StaticList)((IReplaceWith)this).ReplaceWith(replacements); } public object this[int i] { get { return _items[i]; } } public int Length { get { return _items.Length; } } public static void Extract(object source, out T0 v0, out T1 v1, out T2 v2, out T3 v3, out T4 v4) { StaticList list = (StaticList)source; System.Diagnostics.Debug.Assert(list.Length == 5); v0 = (T0)list[0]; v1 = (T1)list[1]; v2 = (T2)list[2]; v3 = (T3)list[3]; v4 = (T4)list[4]; } public IEnumerator GetEnumerator() { return ((IEnumerable)_items).GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)_items).GetEnumerator(); } private const string STATIC_LIST = "static_list"; public void Encode(XmlElement body) { body.SetProperty(XmlSerializer.TYPE, STATIC_LIST); int i = 0; foreach (object item in _items) { XmlElement child = body.NewNode("ITEM_" + i); i++; XmlSerializer.Encode(item, child); } } private static IList DecodeChildren(XmlElement body) { List children = new List(); foreach (XmlElement child in body.Enum()) children.Add(XmlSerializer.Decode(child)); return children; } public StaticList(XmlElement body) : this(DecodeChildren(body)) { } private static void XmlSerializerInit() { XmlSerializer.RegisterDecoder(STATIC_LIST, args => new StaticList(args)); } } /// /// The request used to Find() this DataNode. Use this copy when we clean up. Before /// this object destroyes itsef it needs to remove itself from _all using this key. /// private Factory _key; // The people we may have to notify. private readonly HashSet _listeners = new HashSet(); // Links to us which have no callbacks. These were seperated from _listeners // strictly as an optimization. // // For simplicity let's say you have to lock _listeners any time you want to // access _listeners or _otherOwners. I'm thinking about a case when someone // is trying to add one time of link and someone in a different thread is // trying to remove the other type of link. If someone asked "are we done // with this object?" how could we know for sure. private int _otherOwners; // One more listener. private void AddLink(LinkImpl link) { //System.Diagnostics.Debug.WriteLine("AddLink(" + _key + ")"); lock (_listeners) { if (link.HasListener) _listeners.Add(link); else _otherOwners++; } } // This severs the connection to the given link. It should only be called // by a Link. If this is the last link, it will cause the DataNode to // release its resources. private void RemoveLink(LinkImpl link) { //System.Diagnostics.Debug.WriteLine("RemoveLink(" + _key + ")"); bool needToRelease = false; lock (_all) { lock (_listeners) { bool okay; if (link.HasListener) okay = _listeners.Remove(link); else { _otherOwners--; okay = _otherOwners >= 0; } System.Diagnostics.Debug.Assert(okay); needToRelease = ReadyForRelease(); } if (needToRelease) _all.Remove(_key); } // Outside of the lock. if (needToRelease) Release(); } /// /// This gets called after the last listener disconnects. This will do /// various cleanup including calling CleanUp(). Originally CleanUp() and /// Release() were the same call. But I was worried that people would /// forget to call base.Release(); /// private void Release() { //System.Diagnostics.Debug.WriteLine("Release(" + _key + ")"); CleanUp(); lock (_automaticLinks) { foreach (Link link in _automaticLinks) link.Release(); _automaticLinks.Clear(); } } /// /// This gets called after the last listener disconnects. This means /// to release any resources we've been using. Override this if your /// class needs to release any special resources, but be sure to call /// this original function. /// protected virtual void CleanUp() { } // This is true if there are no listeners or other reasons to live. private bool ReadyForRelease() { lock (_listeners) { return (_otherOwners <= 0) && (_listeners.Count == 0); } } protected void NotifyListeners() { List listeners; lock (_listeners) { listeners = new List(_listeners); } foreach (LinkImpl link in listeners) // NotifyListener() is responsible for catching exceptions. link.NotifyListener(); } /// /// Be careful with your constructors. You are almost certainly holding a /// lock on _all while your constructor is running. /// /// Never construct a DataNode object directly. You want to do it as part /// of the Find() / Link mechanism. /// protected DataNode() { } /// /// This creates a new listener. It returns a DataNode.Link as the recipt. /// The recipt can be used to terminate the connection to this data node. The /// listener is called whenever the data in this data node changes, except /// when the link is terminated. /// /// Can be null. /// A new Link object. private Link CreateLink(Action listener) { LinkImpl result = new LinkImpl(this, listener); AddLink(result); return result; } // Automatic links create simipler code. They are not strictly required, but // they help. All items in this list will be released as soon as the last // link to this data node is released private readonly List _automaticLinks = new List(); protected void AddAutoLink(Link link) { lock (_automaticLinks) { _automaticLinks.Add(link); } } private object _value; /// /// This is a convenient way to share data with the caller. A class can add /// other ways. Having a standard interface helps code reuse. For example a /// cache DataNode could be a wrapper around any other DataNode that uses /// this function. /// /// If you used a GenericDataNode in a previous version of the software, /// just use an ordinary DataNode and GetValue() in this version. /// /// Value can be null. Assume that's common. Most DataNode objects get /// their data from a server. Before the server gives us a result, the Value /// will be null. null can mean other things, too, so you should always be /// ready for that. /// /// The default implementation stores the value in a variable and notifies /// listeners any time that variable changes. However, a DataNode object /// could override this and keep the interface. Perhaps you want to defer /// some of the computation until the first time someone asks for the value. /// /// It seems like get has to be thread safe. That's unfortunate because it /// makes delayed computation hard or impossible. But how else could a /// GuiDataNode initialize itself? /// public object Value { get { return _value; } protected set { //if ((null == value) || (value.ToString() == "")) // System.Diagnostics.Debug.WriteLine("Here"); // Same as (!_value.Equals(value)), but the following works with // nulls, too. if (!object.Equals(_value, value)) { //System.Diagnostics.Debug.WriteLine(DateTime.Now + ": Value " + _value + " => " + value + " for " + _key); _value = value; NotifyListeners(); } //else // System.Diagnostics.Debug.WriteLine(DateTime.Now + ": Value " + _value + " unchanged for " + _key); } } /// /// Calling List.Equals() will only tell you if the two lists are the same object. /// It does not look at the contents. /// /// This function walks through the lists and returns true if the lists are the same /// size and each item in the first list is the same as the corresponding item in the /// second list. /// /// We use object.Equal(object, object) to compare the items in the list. So this is /// not a deep compare. In particular if an item in a list is another list, we don't /// call this function recursively. /// /// If both of the inputs are null, this returns true. If exactly one of the inputs /// is null, this returns false. /// /// /// The type of the items in the first list. We always use it as an object, so the /// type doesn't matter. I would have used a ? for the item type if I was in Java. /// /// /// The type of the items in the second list. We always use it as an object, so the /// type doesn't matter. I would have used a ? for the item type if I was in Java. /// /// The first list. May be null. /// The second list. May be null. /// /// True if the items in the first list are Equal() to the items in the second list /// or if both lists are null. /// public static bool ListContentsEqual(IList a, IList b) { if (object.ReferenceEquals(a, b)) return true; if ((null == a) || (null == b)) return false; if (a.Count != b.Count) return false; for (int i = a.Count - 1; i >= 0; i--) if (!object.Equals(a[i], b[i])) return false; return true; } /// /// This is like saying "Value = newList". The difference is that we use /// ListContentsEqual() to make sure the lists are really different before sending /// out a notification. This is just an optimization. /// /// /// The type of the items in the list. Note that both the current value and the /// new value both must be of type IList(T). /// /// The new value for this DataNode. May be null. protected void SetListValue(IList newList) { if (!ListContentsEqual((IList)Value, newList)) Value = newList; } } /// /// If you wish to register your class with the XmlSerializer, you need to do that /// in a special way. Add [XmlSerializerInit] above your class definition. Then /// create a method starting "private static void XmlSerializerInit()". In that /// method make your calls to XmlSerializer.RegisterEncoder(), /// XmlSerializer.RegisterDecoder(), and XmlSerializer.AddKnownObject(). That /// will ensure that there are no threading issues and that everything gets registered /// before someone needs it. /// [AttributeUsage(AttributeTargets.Class)] public class XmlSerializerInitAttribute : System.Attribute { /// /// The work to do when the XmlSerializer is initializing. /// The default is null. This means to call /// FactoryWithHash.RegisterStandardDecoder() on the class. /// Alternatively you can give the name of a static private method in the class. /// In that case we will call that method on this class. That method should call /// RegisterStandardDecoder() if this is a DataNode.Factory, but can also do /// additional work. /// If this is set to "*" that's the same as setting it to "XmlSerializerInit". /// That's just a convenience to save some typing and to suggest a standard name /// that's easy for the programmer to recognize. /// public readonly string MethodName; /// /// /// /// /// The method in this class to call when the XmlSerializer is initializing. /// TODO add more /// public XmlSerializerInitAttribute(string methodName = null) { if ("*" == methodName) // A convenience. You could have written out the entire string. methodName = "XmlSerializerInit"; MethodName = methodName; } } /// /// Translate between objects and XML. /// /// Not all object types are supported. This is primarily aimed at DataNodeFactory objects. /// /// This must be cross platform. We might write from GWT and read in C#, or vice versa. /// public static class XmlSerializer { private static bool _frozen; /// /// Some objects know how to encode themselves. /// public interface Self { /// /// Convert yourself to XML. /// /// /// Typically the parent creates and names the XML element while the object being encoded /// fills in the details. /// void Encode(XmlElement body); } static public void Encode(object toEncode, XmlElement body) { System.Diagnostics.Debug.Assert(_frozen); if (null == toEncode) { body.SetProperty(TYPE, NULL_TYPE); return; } string name; if (_knownObjectsByValue.TryGetValue(toEncode, out name)) { body.SetProperty(TYPE, OBJECT_TYPE); body.SetProperty(VALUE, name); return; } if (toEncode is Self) { (toEncode as Self).Encode(body); return; } Action encoder; if (_allEncoders.TryGetValue(toEncode.GetType(), out encoder)) { encoder(toEncode, body); return; } throw new ArgumentException("Can't encode items of type " + toEncode.GetType() + ", object=" + toEncode); } private static void EncodeInt(object toEncode, XmlElement body) { // Note: Casting from an object to an Int64 is different from casting from a number // to an Int64. // int i = 5; // object o = i; // int64 thisWorks = (Int64)i; // int64 thisThrowsAnException = (Int64)o; body.SetProperty(VALUE, (Int64)toEncode); body.SetProperty(TYPE, INT_TYPE); } private static void EncodeInt32(object toEncode, XmlElement body) { // Int64 is our preferred type for data. But Int32 is used for some configuration settings, // like the number of digits to show after the decimal point. body.SetProperty(VALUE, (Int32)toEncode); body.SetProperty(TYPE, INT32_TYPE); } private static void EncodeDouble(object toEncode, XmlElement body) { body.SetProperty(VALUE, (double)toEncode); body.SetProperty(TYPE, DOUBLE_TYPE); } private static void EncodeColor(object toEncode, XmlElement body) { Color asColor = (Color)toEncode; body.SetProperty(VALUE, asColor); body.SetProperty(TYPE, COLOR_TYPE); body.SetProperty("debug", asColor.Name); } private static void EncodeString(object toEncode, XmlElement body) { String asString = (String)toEncode; body.SetProperty(VALUE, asString); body.SetProperty(TYPE, STRING_TYPE); } private static readonly Dictionary _knownObjectsByValue = new Dictionary(); private static readonly Dictionary _knownObjectsByName = new Dictionary(); /// /// This will iterate over each member of the enumeration, and call AddKnownObject() on /// each one. /// /// The enumeration to examine. /// /// This is added to the front of each item to get the item's XML name. /// The default is to use the type. The default will look like valid, /// fully qualified, C# identifiers, e.g. /// TradeIdeas.TIProGUI.DataNodeRectangleViewer.SecondValueMeaning.Width /// public static void AddEnum(Type type, string prefix=null) { System.Diagnostics.Debug.Assert(type.IsEnum); if (null == prefix) // This is consistant with C# source code. A completely namespace, list of nested // classes, the name of the enum, and the name of the item, all seperated by periods. prefix = type.FullName.Replace('+', '.') + '.'; foreach (Object value in type.GetEnumValues()) AddKnownObject(value, prefix + value.ToString()); } /// /// This is an object that we know how to serialize. /// This is something that has to be consistent between different versions of the program. /// Any time have to serialize the given object, we use the given name in XML. /// /// The object that we want to describe. /// /// A description of the object. /// This exists in its own namespace; it only has to be unique with respect to other /// calls to AddKnownObject(). /// public static void AddKnownObject(object o, string name) { System.Diagnostics.Debug.Assert(!_frozen); System.Diagnostics.Debug.Assert(!(_knownObjectsByName.ContainsKey(name) || _knownObjectsByValue.ContainsKey(o))); _knownObjectsByValue[o] = name; _knownObjectsByName[name] = o; } /// /// This has the same result calling the other version of AddKnownObject. /// C# will not automatically convert a method name to an object in a function call. /// Before we added this, each caller would manually cast the method name to /// TransformationDataNode.Transformation, then C# would automatically convert /// that to Object. /// /// /// public static void AddKnownObject(TransformationDataNode.Transformation t, string name) { AddKnownObject((object)t, name); } static private readonly Dictionary> _allEncoders = new Dictionary> { { typeof(Int64), EncodeInt }, { typeof(Int32), EncodeInt32 }, { typeof(double), EncodeDouble }, { typeof(Color), EncodeColor }, { typeof(string), EncodeString } }; static public void RegisterEncoder(Type type, Action encoder) { System.Diagnostics.Debug.Assert(!_frozen); if (typeof(Self).IsAssignableFrom(type)) // This would be ambiguous. If you have a rule for class X and class X implements // self, which rule would you use? I can't think of any good reason for adding // such a rule, so I just make it illegal. throw new ArgumentException("Always uses the Self interface and ignores this."); _allEncoders.Add(type, encoder); } /// /// We use this property to say what type of object we are encoding / decoding. /// This property is required. If it is missing or invalid Decode() will throw an exception. /// public const string TYPE = "TYPE"; /// /// This property is commonly used to store the data for a specific type. Each type /// can make its own decision, but this is convenient for a a lot of types. /// /// When this property is used convention dictates that it is required. /// If it is missing or invalid Decode() should throw an exception. /// public const string VALUE = "VALUE"; private const string INT_TYPE = "int"; private const string INT32_TYPE = "int32"; private const string DOUBLE_TYPE = "double"; private const string COLOR_TYPE = "color"; private const string STRING_TYPE = "string"; private const string NULL_TYPE = "null"; private const string OBJECT_TYPE = "object"; private static readonly Type[] EMPTY_LIST = new Type[0]; static XmlSerializer() { System.Diagnostics.Debug.WriteLine("Initializing XmlSerializer"); System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch(); stopwatch.Start(); string definedIn = typeof(XmlSerializerInitAttribute).Assembly.GetName().Name; foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) // Note that we have to call GetName().Name. Just GetName() will not work. The following // if statement never run when I tried to compare the results of GetName(). if ((!assembly.GlobalAssemblyCache) && ((assembly.GetName().Name == definedIn) || assembly.GetReferencedAssemblies().Any(a => a.Name == definedIn))) foreach (Type type in assembly.GetTypes()) { object[] attributes = type.GetCustomAttributes(typeof(XmlSerializerInitAttribute), false); if (attributes.Length > 0) { System.Diagnostics.Debug.Assert(attributes.Length == 1); XmlSerializerInitAttribute attribute = (XmlSerializerInitAttribute)attributes[0]; if (null == attribute.MethodName) // The common case. Make this easy for the person writing the class. DataNode.FactoryWithHash.RegisterStandardDecoder(type); else { MethodInfo methodInfo = type.GetMethod(attribute.MethodName, BindingFlags.NonPublic | BindingFlags.Static, null, Type.EmptyTypes, null); if ((null == methodInfo) || !methodInfo.IsStatic) throw new Exception("Could not find method private static void " + type.FullName + "." + attribute.MethodName + "()"); methodInfo.Invoke(type, EMPTY_LIST); } } } stopwatch.Stop(); System.Diagnostics.Debug.WriteLine("XmlSerializer finished after " + stopwatch.Elapsed.TotalSeconds); _frozen = true; } static public object Decode(XmlElement top) { // The name of the object is now owned by the parent. That makes it easy for the parent // object to have multiple children, some of which are optional. You can look up children // by name or index! System.Diagnostics.Debug.Assert(_frozen); string type = top.Property(TYPE, "not specified"); Func parser; _allDecoders.TryGetValue(type, out parser); if (null == parser) throw new ArgumentException("Unknown object type: " + type); return parser(top); } static private readonly Dictionary> _allDecoders = new Dictionary> { { INT_TYPE, DecodeInt }, { INT32_TYPE, DecodeInt32 }, { DOUBLE_TYPE, DecodeDouble }, { NULL_TYPE, delegate { return null; } }, { COLOR_TYPE, DecodeColor }, { STRING_TYPE, DecodeString }, { OBJECT_TYPE, DecodeObject } }; static public void RegisterDecoder(string name, Func decoder) { System.Diagnostics.Debug.Assert(!_frozen); _allDecoders.Add(name, decoder); } private static object DecodeInt(XmlElement top) { Int64? value = top.PropertyInt(VALUE); if (!value.HasValue) throw new ArgumentException("Unable to find VALUE for int."); return value.Value; } private static object DecodeInt32(XmlElement top) { Int32? value = top.PropertyInt32(VALUE); if (!value.HasValue) throw new ArgumentException("Unable to find VALUE for int."); return value.Value; } private static object DecodeDouble(XmlElement top) { double? value = top.PropertyDouble(VALUE); if (!value.HasValue) throw new ArgumentException("Unable to find VALUE for double."); return value.Value; } private static object DecodeColor(XmlElement top) { Color? value = top.PropertyColor(VALUE); if (!value.HasValue) throw new ArgumentException("Unable to find VALUE for color."); return value.Value; } private static object DecodeString(XmlElement top) { string value = top.Property(VALUE, null); if (null == value) // In a lot of places our XML reader tries to use a default. // We explicitly are not allowing any defaults at this time. // It doesn't seem to add much. throw new ArgumentException("Unable to find VALUE for string"); return value; } private static object DecodeObject(XmlElement top) { string value = top.Property(VALUE, null); if (null == value) // The standard says to throw an exception if we can't find the VALUE. throw new ArgumentException("Unable to find VALUE for object"); object result; if (!_knownObjectsByName.TryGetValue(value, out result)) // The standard says to throw an exception if we can't parse the VALUE. // We can have some optional fields, espeically if the format changes over // time, but if there's an obvious problems we should NOT return a default. throw new ArgumentException("Unable to find object of type " + VALUE); else return result; } public static string DebugGetKnownTypes() { StringBuilder result = new StringBuilder(); foreach (var kvp in _allEncoders) { result.Append(kvp.Key.Name); result.Append("\r\n"); } return result.ToString(); } public static string DebugGetKnownTypeStrings() { StringBuilder result = new StringBuilder(); foreach (var kvp in _allDecoders) { result.Append(kvp.Key); result.Append("\r\n"); } return result.ToString(); } public static string DebugGetKnownObjects() { StringBuilder result = new StringBuilder(); foreach (var kvp in _knownObjectsByName) { result.Append(kvp.Key); result.Append(" <==> "); result.Append(kvp.Value); result.Append("\r\n"); } return result.ToString(); } } [XmlSerializerInit("*")] public class TransformationDataNode : DataNode { public static object StringToInt(object input) { string asString = input as String; // Note: If the value is not a string, but we're expecting a string, ignore it. // Treat it like a null value. That probably would mean a programming error. if (null == asString) return null; Int64 asInt; if (ServerFormats.TryParse(asString, out asInt)) return asInt; else // Not a valid integer. return null; } public static object StringToDouble(object input) { string asString = input as String; if (null == asString) return null; double asDouble; if (ServerFormats.TryParse(asString, out asDouble)) return asDouble; else // Not a valid double. return null; } public static object StringToTime(object input) { string asString = input as String; if (null == asString) return null; else return ServerFormats.DecodeServerTime(asString); } private static void XmlSerializerInit() { XmlSerializer.AddKnownObject(StringToInt, "StringToInt"); XmlSerializer.AddKnownObject(StringToDouble, "StringToDouble"); XmlSerializer.AddKnownObject(StringToTime, "StringToTime"); FactoryWithHash.RegisterStandardDecoder(typeof(TransformationDataNode)); } public delegate object Transformation(object input); private DataNode _child; private readonly Transformation _transformation; private TransformationDataNode(object args) { StaticList argList = (StaticList)args; System.Diagnostics.Debug.Assert(argList.Length == 2); _transformation = (Transformation)argList[0]; AddAutoLink(((Factory)argList[1]).Find(out _child, Update)); Update(); } /// /// For simplicity I'm copying, processing, and caching the data as soon as I get it. /// Sometimes it makes more sense to wait until someone asks for the data. /// private void Update() { try { Value = _transformation(_child.Value); } catch { Value = null; } } public static Factory GetFactory(Transformation transformation, Factory input) { return new FactoryWithHash(args => new TransformationDataNode(args), typeof(TransformationDataNode), StaticList.Create(transformation, input)); } } /// /// A very simple data node. You tell it a value at creation time, and that is always the /// Value read from this data node. This is useful when an object expects data nodes as /// inputs, but in this case we want to give a constant value to that object. This /// DataNode never sends an update message because its value never changes. /// [XmlSerializerInit] public class ConstantDataNode : DataNode { private ConstantDataNode(object args) { Value = args; } /// /// /// /// The value for the DataNode to return. This can be a PlaceHolder. This can be null. /// If the value is not null, we will call GetHashCode() and Equals() on it. /// /// public static Factory GetFactory(object value) { return new FactoryWithHash(args => new ConstantDataNode(args), typeof(ConstantDataNode), value); } /// /// This is the inverse of GetFactory(). If you created an object by calling /// GetFactory(x), and you call TryDeconstruct() on the factory, you should get /// x back as the result. If the input was any other object, this will fail. /// /// The object we are trying to deconstruct. /// /// The value returned by the DataNode. Which WAS the input to GetFactory(). /// null is returned on failure. /// Note: null can also be returned on success, if the orginal input was null. /// /// /// True on success, false on failure. /// public static bool TryDeconstruct(object factory, out object constantValue) { FactoryWithHash f = factory as FactoryWithHash; if (null == f) { constantValue = null; return false; } if (!typeof(ConstantDataNode).Equals(f.MetaData)) { constantValue = null; return false; } constantValue = f.Arguments; return true; } /// /// This is similar to the two parameter version of TryDeconstruct(). /// After finding the original value we try to convert that to the type T. /// If we can't find the original value or the original value was not of /// the right type, we return the default value for T. /// /// If you are expecting a value type, you can specify that type directly, /// or you can specifcy the nullable version of that type. Either will /// work the same on success. But the nullable type will return null /// on error. /// /// The type we want the result to be. /// The object to deconstruct. /// /// The original input to GetFactory(), or default(T) on failure. /// public static T TryDeconstruct (object factory) { object contents; bool success = TryDeconstruct(factory, out contents); if (!success) return default(T); if (!(contents is T)) return default(T); return (T)contents; } } /// /// This takes a single streaming input and returns one of four constants. /// /// This was aimed at picking a color. Often you want to display something in green if it's /// positive, red if it's negative, black or white if it's 0. /// [XmlSerializerInit] public class CompareToZeroDataNode : DataNode, IFormatProvider { private readonly object _greater; private readonly object _equal; private readonly object _less; private readonly object _ifNull; private DataNode _value; private CompareToZeroDataNode(object args) { StaticList list = (StaticList)args; Factory factory = (Factory)list[0]; _greater = list[1]; _equal = list[2]; _less = list[3]; _ifNull = list[4]; AddAutoLink(factory.Find(out _value, Update)); Update(); } private void Update() { object value = _value.Value; IConvertible asIConvertible = value as IConvertible; if (null == asIConvertible) Value = _ifNull; else try { double asDouble = asIConvertible.ToDouble(this); if (asDouble > 0) Value = _greater; else if (asDouble == 0) Value = _equal; else Value = _less; } catch { Value = _ifNull; } } /// /// /// /// /// A factory for the value we want to monitor. The result should be IConvertible to double. /// /// The value to return if the input is positive. /// The value to return if the input is 0. /// The value to return if the input is negative. /// The value to return in case of error. /// public static Factory GetFactory(Factory value, object greater, object equal, object less, object ifNull = null) { return new FactoryWithHash(arg => new CompareToZeroDataNode(arg), typeof(CompareToZeroDataNode), StaticList.Create(value, greater, equal, less, ifNull)); } public static bool TryDeconstruct(object factory, out Factory value, out object greater, out object equal, out object less, out object ifNull) { value = null; greater = null; equal = null; less = null; ifNull = null; try { FactoryWithHash f = factory as FactoryWithHash; if (null == f) return false; if (!typeof(CompareToZeroDataNode).Equals(f.MetaData)) return false; StaticList.Extract(f.Arguments, out value, out greater, out equal, out less, out ifNull); return true; } catch { // It's tempting not to catch this exception. This is something that looks like one // of our factories, but was malformed. This should be an assertion failure, maybe. return false; } } public static bool TryDeconstruct(object factory) { Factory f; object o; return TryDeconstruct(factory, out f, out o, out o, out o, out o); } object IFormatProvider.GetFormat(Type formatType) { // We don't want to do anything exotic. We want Int16 and UInt32, and other simple // types to conert to double. return null; } } /// /// Concatinate N lists to make one list. /// /// Each input DataNode should return an IEnumerable. (The non-generic type. Of course, /// each specialized IEnumerable inherits from this.) If the result is null, we treat /// that as an empty list. The input data nodes should not return any other data types. /// Currently we naïvely treat any other data type as an empty list. But that could /// change and we could fail an assertion or something like that. The input lists should /// be read-only and thread safe. /// /// This DataNode always returns a read-only IList(object). It never returns null. /// /// We call object.Equals(object, object) on the items in the list. That call should /// be thread-safe and reasonably cheap. Other than that restriction the elements of /// the list can be anything, including null. /// /// Note the current optimization strategy: Each time an input notifies us of a change /// we build a new list that is a concatination of the inputs. Then we compare the /// new result to the current result, one element at a time, to see if there was really /// a change or not. By default DataNode.Value calls object.Equal(object, object) on /// the top level objects only. We know that would always say there was a change because /// we are creating a new list and List.Equals() only compares object identity. It doesn't /// walk through the list. /// public class ConcatinateListsDataNode : DataNode { private List _dataNodes; private ConcatinateListsDataNode(object args) { StaticList list = (StaticList)args; List dataNodes = new List(list.Length); foreach (object item in list) { Factory factory = (Factory)item; DataNode dataNode; AddAutoLink(factory.Find(out dataNode, Update)); dataNodes.Add(dataNode); } Thread.MemoryBarrier(); _dataNodes = dataNodes; Update(); } private void Update() { if (null == _dataNodes) // We have received a response before the constructor finished. return; // What if the inputs are updated in different threads? We might have two // calls to Update at the same time. The second one might finish before the // first, and the first would overwrite the newest result with an older // result. lock (_dataNodes) { List newValue = new List(); foreach (DataNode dataNode in _dataNodes) { IEnumerable list = dataNode.Value as IEnumerable; if (null != list) { foreach (object item in list) newValue.Add(item); } } SetListValue(newValue); } } public static Factory GetFactory(params Factory[] sublists) { return new FactoryWithHash(arg => new ConcatinateListsDataNode(arg), typeof(ConcatinateListsDataNode), StaticList.Create(sublists)); } } /// /// Most data nodes are respononsible for subscribing to whatever data sources /// they need. PushDataNode is different. By default the Value is always null /// and there are no callbacks. However, someone holding onto this data node /// can send any data they want. They can set the new value and a notification /// will be sent to all listeners. /// /// This was originally designed to work with the GUI. You can wrap a data /// node around a text box or something like that. Any time the user types, /// a callback will read the Text property, process it, and use the result /// to update this data node's Value property. /// public class PushDataNode : DataNode { private readonly object _args; private PushDataNode(object args) { _args = args; } /// /// This is the traditional form of GetFactory(). For this class there are two other /// forms of GetFactory() which might work better. This function is used by the other /// two. /// /// See GetFactory() with no arguments and Find(), both in this class. /// /// /// Any unique value. Two calls to GetFactory with the same key will return the same /// data node. Two calls with different keys will return different data nodes. /// /// This can be null or any object which respects GetHashCode() and Equals(). /// /// The factory. public static Factory GetFactory(object key) { return new FactoryWithHash(arg => new PushDataNode(arg), typeof(PushDataNode), key); } /// /// Typically a form will use Find() to get the first copy of this object. It will /// hold on to this object so it can broadcast messages. It will call this method /// on the object to create factories. Those factories will allow someone to listen /// to the broadcast messages. /// /// A factory which will point back to this exact object. public Factory GetFactory() { return GetFactory(_args); } /// /// Typically a form will use this method to get the first copy of this object. /// It will hold on to thos object so it can broadcast messages. It will call /// GetFactory() on this object to create factories. Those factories will allow /// someone to listen to the broadcast messages. /// /// This also takes care of converting from DataNode to PushDataNode. You could /// always do that yourself, this is just a convenience. Normally no one needs /// to make that conversion. Normally you only need access to the Value property. /// But that's for listeners. This call is good for broadcasters. /// /// /// Any unique value. Two calls to Find() with the same key will return the same /// data node. Two calls with different keys will return different data nodes. /// /// This can be null or any object which respects GetHashCode() and Equals(). /// /// This will receive a copy of the data node. /// A standard DataNode.Link public static Link Find(object key, out PushDataNode dataNode) { DataNode temp; Link result = GetFactory(key).Find(out temp); dataNode = (PushDataNode)temp; return result; } /// /// This changes the Value property and notifies all listeners. /// /// The value for the Value property. public void SetValue(object newValue) { Value = newValue; } /// /// If you only set the value to null or to values of type IList(T), this might /// be an optimization. /// /// See DataNode.SetListValue() for more details. This is the same function, but /// changed from protected to public. /// /// /// All values must be null or of type IList(T). T cannot change for the lifetime /// of this object. /// /// The new value that we want to broadcast. public new void SetListValue(IList newList) { base.SetListValue(newList); } } public static class DataNodeExtensionMethods { public static DataNode.Factory SetSymbol(this DataNode.Factory original, string symbol) { return (DataNode.Factory)original.ReplaceWith(new Dictionary() { {DataNode.PlaceHolder.SYMBOL.Key, symbol} }); } public static DataNode.Factory ReplaceWithF(this DataNode.Factory original, Dictionary replacements) { return (DataNode.Factory)original.ReplaceWith(replacements); } public static DataNode.Factory StringToDouble(this DataNode.Factory original) { return TransformationDataNode.GetFactory(TransformationDataNode.StringToDouble, original); } public static DataNode.Factory StringToInt(this DataNode.Factory original) { return TransformationDataNode.GetFactory(TransformationDataNode.StringToInt, original); } }; }