using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows.Forms; using TradeIdeas.TIProData; using TradeIdeas.XML; using System.Drawing; using System.Collections; using System.Drawing.Drawing2D; using System.Xml; using System.Reflection; namespace TradeIdeas.TIProGUI { /// /// The assumption is that a lot of the GUIs will be mostly fixed when we display them for /// the user. But the user will be able to change the symbol at run time. /// /// This works closely with DataNode.IReplaceWith. A call to /// IReplaceWithRuntime.ReplaceWith() will almost certainly result in several calls to /// DataNode.IReplaceWith.ReplaceWith(). /// /// DataNode.IReplaceWith is mostly aimed at DataNode.Factory objects. DataNode.Factory /// objects are all read only. Calling ReplaceWith on a DataNode.Factory object will /// generate a new object with the requested changes, and will leave the original in tact. /// /// IReplaceWithRuntime is mostly aimed at Control objects. You don't want to rebuild /// your GUI each time someone changes the symbol. Only the data. So calling /// IReplaceWithRuntime.ReplaceWith() will change the current object, telling it to /// request and display a different type of data. /// public interface IReplaceWithRuntime { /// /// This says what data to look for. This is often implemented by one or more calls /// to ReplaceWith() on DataNode.Factory objects. Then we throw out the old /// DataNode objects and use the new Factory objects to Find() new DataNode objects. /// /// The value should be a mapping from the name of a PlaceHolder to the value that /// should replace that PlaceHolder. /// /// This can be null. Null is the typical initial state, and null is a way to tell /// this object to stop what it's doing. Null might be treated the same as an /// empty dictionary. That seems reasonable when the object only cares about one /// PlaceHolder, typically the symbol. /// /// The object that you set this to should never change. This value might be sent /// to a lot of objects, and some might hold on to it. /// Dictionary Replacements { set; } } /// /// A place for extension methods. /// public static class IReplaceWithRuntimeHelper { /// /// This is a simple way to set the symbol for a control. Normally we give you the /// option to set any number of values at once. The symbol is a special case, so we /// make it easy. /// /// The recipient of this request. /// /// The new value for the symbol. /// Null is a legal value. That's a common way to turn off all data in a control. /// public static void SetSymbol(this IReplaceWithRuntime obj, string symbol) { if (null == symbol) obj.Replacements = null; else obj.Replacements = new Dictionary { { DataNode.PlaceHolder.SYMBOL.Key, symbol } }; } /// /// This is used in a lot of the container classes to pass the replacement message from the /// owner to each of its children. If the control argument implements IReplaceWithRuntime /// then we do the cast and set the replacements. If not, we do nothing. /// /// The recipient of the requests. /// /// The new values. /// Null is legal here. That's a common way to turn off all data in a control. /// public static void TryReplaceWith(this Control control, Dictionary replacements) { IReplaceWithRuntime irwr = control as IReplaceWithRuntime; if (null != irwr) irwr.Replacements = replacements; } /// /// A combination of SetSymbol() and TryReplaceWith(). This tries to cast the control /// to a IReplaceWithRuntime. If that fails we do nothing. If that succeeds, we call /// SetSymbol() on it. /// /// /// public static void TrySetSymbol(this Control control, string symbol) { IReplaceWithRuntime irwr = control as IReplaceWithRuntime; if (null != irwr) irwr.SetSymbol(symbol); } } /// /// This helps you build hash codes for a composite object from the hash codes of its /// components. /// public struct Hasher { public Hasher(params object[] fields) { _accumulator = 0; Add(fields); } private int _accumulator; private static int Rotate(int value) { uint v = (uint)value; return (int)((v << 7) | (v >> (32 - 7))); } public void AddOne(object o) { _accumulator = Rotate(_accumulator); if (null != o) _accumulator ^= o.GetHashCode(); } public void Add(params object[] fields) { foreach (object o in fields) AddOne(o); } public override int GetHashCode() { return _accumulator; } } /// /// Similar to DataNode.Factory, but simpler. We never reuse controls, so we don't have /// to worry about Equals() or GetHashCode. /// /// Note that factories are optional for controls. In some places an input will clearly /// be a factory, not a control. But, unlike DataNode objects, most controls have public /// constructors. /// /// It would be very tempting to say that DataNodeControlFactory inherits from /// DataNode.IReplaceWith. /// /// IReplaceWith is a minor pain, but it might still be worth it. Presumably the /// DataNode.Factory objects used to create a DataNodeControl will still have a /// symbol PlaceHolder when we create the DataNodeControlFactory, and when that /// creates the Control. ISetSymbol will take care of that. /// /// It seems like IReplaceWith could be valuable while building controls. For example, /// think about the GWT SmartLayoutPanel objects. They are built in advance with /// names for each cell. Later you fill in the panels by name. That's not a perfect /// example because that doesn't work with factories. In particular, sometime you change /// templates, but you keep the controls inside. You translpant the controls to the /// new parent without rebuilding them. /// public abstract class DataNodeControlFactory { public abstract Control Create(); protected DataNodeControlFactory() { } protected DataNodeControlFactory(XmlElement top) { } public override bool Equals(object obj) { if (null == obj) // Required by https://msdn.microsoft.com/en-us/library/bsc2ak47(v=vs.110).aspx // "Implementations of Equals must not throw exceptions; they should always return a value. // For example, if obj is null, the Equals method should return false instead of throwing // an ArgumentNullException." return false; if (ReferenceEquals(this, obj)) return true; if (!GetType().Equals(obj.GetType())) // Note that we don't compare objects to type DataNodeControlFactory. That works for // some types, but this is an abstract type. The assumption is that superclasses will // start their Equals() methods by calling this. If this returns true, then they will // cast their argumnt and check additional fields. return false; // Add more fields here. TODO return true; } public override int GetHashCode() { return GetHashCodeBase().GetHashCode(); } /// /// Get the part of the hash code which is based only on the fields available to this class. /// Superclasses will probably override GetHashCode() and that will call MakeHashCode() /// which in turn will call this. /// /// private Hasher GetHashCodeBase() { // TODO add more fields. return new Hasher(GetType()); } protected int MakeHashCode(params object[] fields) { Hasher hasher = GetHashCodeBase(); hasher.Add(fields); return hasher.GetHashCode(); } } /// /// A very simple way to display strings. DataNodes provide the value to /// display, along with the foreground and background colors. /// /// Currently this is based on a Label. That's an implementation detail that /// is likely to change. In particular, we like the ability to display /// different things depending on how large the field is. We might display /// "100,000,002" if we have a lot of space, but "100.0M" if we have less space. /// public class DataNodeLabel : Label, IReplaceWithRuntime { private readonly List _links = new List(); private DataNode _valueDataNode; private DataNode _foreColorDataNode; private DataNode _backColorDataNode; private DataNode.Factory _valueFactory; private DataNode.Factory _foreColorFactory; private DataNode.Factory _backColorFactory; /// /// Create the new object. /// /// Initially the symbol will be null, so we won't try to instantiate anything. /// We will set the symbol on each formula before trying to create any DataNode /// objects. /// /// /// This provides the value to display. /// If the value is null we display "". /// Otherwise we call ToString() on the value. /// /// We will automatically wrap this factory in a GuiDataNode to ensure there are /// no threading issues. /// /// /// The foreground color of the label. /// /// If the foreground factory and/or the background factory are null, we will /// create reasonable factories. The colors are chosen so they will work together /// and make the value easy to read. /// /// If the DataNode returns null, we'll try to pick a color. This algorithm /// is much simpler than leaving the factory null. In particular, this algorithm /// doesn't know or care about the other color. If both the foreground and /// background return null at the same time, then our choices will work. /// /// We will automatically wrap this factory in a GuiDataNode to ensure there are /// no threading issues. /// /// /// The background color of the label. /// /// If the foreground factory and/or the background factory are null, we will /// create reasonable factories. The colors are chosen so they will work together /// and make the value easy to read. /// /// If the DataNode returns null, we'll try to pick a color. This algorithm /// is much simpler than leaving the factory null. In particular, this algorithm /// doesn't know or care about the other color. If both the foreground and /// background return null at the same time, then our choices will look good. /// /// We will automatically wrap this factory in a GuiDataNode to ensure there are /// no threading issues. /// public DataNodeLabel(DataNode.Factory value, DataNode.Factory foreColor = null, DataNode.Factory backColor = null) { _valueFactory = GuiDataNode.GetFactory(value); if (null == foreColor) if (null == backColor) { // Nothing is specified. Always use the default colors. _foreColorFactory = ConstantDataNode.GetFactory(DEFAULT_FOREGROUND); _backColorFactory = ConstantDataNode.GetFactory(DEFAULT_BACKGROUND); } else { // The user didn't specify a foreground color, so pick one that should look good // with the background color. Notice that the two colors sharing the GuiDataNode and the // data its holding. _backColorFactory = GuiDataNode.GetFactory(backColor); _foreColorFactory = TransformationDataNode.GetFactory(GradientInfoDataNode.AltColor, _backColorFactory); } else if (null == backColor) { // The user didn't specifiy a background color, so pick one based on the background. _foreColorFactory = GuiDataNode.GetFactory(foreColor); _backColorFactory = TransformationDataNode.GetFactory(GradientInfoDataNode.AltColor, _foreColorFactory); } else { // The user specified everything. _foreColorFactory = GuiDataNode.GetFactory(foreColor); _backColorFactory = GuiDataNode.GetFactory(backColor); } AutoSize = true; // To be more like the form builder. this.Disposed += delegate { Replacements = null; }; } public class Factory : DataNodeControlFactory { // For DataNodeControlFactory public override Control Create() { Control result = new DataNodeLabel(Value, ForeColor, BackColor); if (null != FinalSetup) FinalSetup(result); return result; } /// /// See DataNodeLabel.DataNodeLabel() for more details. /// public DataNode.Factory Value; /// /// May be null. See DataNodeLabel.DataNodeLabel() for more details. /// public DataNode.Factory ForeColor; /// /// May be null. See DataNodeLabel.DataNodeLabel() for more details. /// public DataNode.Factory BackColor; /// /// May be null. This is a good place to do things that aren't specific to this type /// of Control. Like setting the width. /// public Action FinalSetup; public override int GetHashCode() { return MakeHashCode(Value, ForeColor, BackColor, FinalSetup); } public override bool Equals(object obj) { if (!base.Equals(obj)) return false; // We should have already bailed out by now if obj is not the right type. Factory other = (Factory)obj; return Equals(Value, other.Value) && Equals(ForeColor, other.ForeColor) && Equals(BackColor, other.BackColor) && Equals(FinalSetup, other.FinalSetup); } } public Dictionary Replacements { set { foreach (DataNode.Link link in _links) link.Release(); _links.Clear(); if (null == value) { _valueDataNode = null; _foreColorDataNode = null; _backColorDataNode = null; } else { _links.Add(_valueFactory.ReplaceWithF(value).Find(out _valueDataNode, ValueCallback)); _links.Add(_foreColorFactory.ReplaceWithF(value).Find(out _foreColorDataNode, ForeColorCallback)); _links.Add(_backColorFactory.ReplaceWithF(value).Find(out _backColorDataNode, BackColorCallback)); ValueCallback(); ForeColorCallback(); BackColorCallback(); } } } private void ValueCallback() { object value = _valueDataNode.Value; if (null == value) Text = ""; else Text = value.ToString(); } private static readonly Color DEFAULT_FOREGROUND = SystemColors.ControlText; private static readonly Color DEFAULT_BACKGROUND = SystemColors.Control; private void ForeColorCallback() { object value = _foreColorDataNode.Value; if (value is Color) ForeColor = (Color)value; else ForeColor = DEFAULT_FOREGROUND; } private void BackColorCallback() { object value = _backColorDataNode.Value; if (value is Color) BackColor = (Color)value; else BackColor = DEFAULT_BACKGROUND; } } public class DataNodeTableLayoutPanel : TableLayoutPanel, IReplaceWithRuntime { private Dictionary _replacements; public Dictionary Replacements { set { _replacements = value; foreach (Control control in Controls) control.TryReplaceWith(value); } } public DataNodeTableLayoutPanel() { ControlAdded += DoControlAdded; } private void DoControlAdded(object sender, System.Windows.Forms.ControlEventArgs e) { e.Control.TryReplaceWith(_replacements); } } public class DataNodeList { private List _links = new List(); private List _dataNodes = new List(); public void Release() { _dataNodes = null; foreach (DataNode.Link link in _links) link.Release(); _links.Clear(); } public object this[int index] { get { if ((index >= _dataNodes.Count) || (index < 0)) return null; DataNode dataNode = _dataNodes[index]; if (null == dataNode) return null; return dataNode.Value; } } public string GetString(int index, string defaultValue = "") { object value = this[index]; if (null == value) return defaultValue; else return value.ToString(); } public double? GetDouble(int index) { object asObject = this[index]; if (asObject is double) return (double)asObject; else if (asObject is Int64) return (Int64)asObject; else return null; } public event Action OnChange; private void SomethingHappened() { OnChange?.Invoke(); } /// /// Null for no message. Mostly aimed at a developer. /// public string ErrorMessage { get; private set; } public DataNodeList(params DataNode.Factory[] factories) { try { _links.Capacity = factories.Length; _dataNodes.Capacity = factories.Length; foreach (DataNode.Factory factory in factories) if (null == factory) _dataNodes.Add(null); else { DataNode dataNode; _links.Add(factory.Find(out dataNode, SomethingHappened)); _dataNodes.Add(dataNode); } } catch (Exception ex) { foreach (DataNode.Link link in _links) link.Release(); _links.Clear(); throw ex; } } static private DataNode.Factory[] MakeArray(DataNode.StaticList input) { DataNode.Factory[] result = new DataNode.Factory[input.Length]; for (int i = 0; i < result.Length; i++) result[i] = (DataNode.Factory)input[i]; return result; } public static DataNodeList Create(DataNode.StaticList factories) { if (!factories.ReplacementsComplete()) { DataNodeList result = new DataNodeList(); result.ErrorMessage = "Missing replacements."; return result; } else try { return new DataNodeList(MakeArray(factories)); } catch (Exception ex) { DataNodeList result = new DataNodeList(); result.ErrorMessage = ex.Message; return result; } } } /// /// This describes a "view". If you have a table, you probably want one of these per /// column. If you're only using data nodes to populate your table, you probably want /// one for every column. /// /// This doesn't require a table. If you use another widget you can still use this /// to paint. So we can reuse our various paint routines. /// /// One class that inherits from this will have one style of drawing. E.g. /// DataNodeTextViewer will display text with various colors for the foreground and /// background. A different class will draw logarithmic triangles. Different /// classes can request different amounts of data and different types of data. /// /// Each object of this type can have different data associated with it. For example /// we might use the triangles to display the % volume in one column, and a user's /// custom formula in a different column. /// /// These objects are typically read only. These do not need factories. You can /// uses these along side DataNode.Factory and DataNodeControlFactory objects as /// as you conver to and from XML. But these also sit side by side with DataNode /// and DataNodeControl objects at run time. In some sense DataNodeViewer objects /// are their own factories. /// /// Note: These objects have some state that can change. But that's completely /// internal. For example it was inconvenient to copy the client width from each /// function to the next. So we create a temporary variable in the DataNodeViewer /// object to handle that. But that's an implemention detail. /// public abstract class DataNodeViewer { /// /// Request the data for one instance of this view. /// For a table you'll probably call this once on each cell. The viewer will /// come from the column. The replacements are specific to the row. /// /// /// The same input you'd use in DataNode.IReplaceWith.ReplaceWith(). /// Very often this will be the stock symbol and nothing else. /// /// /// A DataNodeList suitable for use with Paint(). Don't mix and match. /// When you call Paint() on an object, the DataNodeList must have come from /// the same object. /// public abstract DataNodeList GetData(Dictionary replacements); protected abstract void Paint(DataNodeList data, Graphics graphics, Rectangle bounds, Color ForeColor, Color BackColor, bool alwaysUseTheseColors); public void Paint(DataNodeList data, Graphics graphics, Rectangle bounds, Color ForeColor, Color BackColor, bool alwaysUseTheseColors, Font font) { Font = font; try { ClientWidth = bounds.Width; Paint(data, graphics, bounds, ForeColor, BackColor, alwaysUseTheseColors); } finally { Font = null; } } /// /// This is specifically aimed at DataGridView. By default the user can copy /// the highlighted section of the table to the clipboard. The DataGridView /// goes to each cell to ask for a string version of itself. The cell should /// come here. /// /// /// The output from a previous call to GetData() on this SAME object. /// /// A user friendly description of the data. public abstract String GetForCopy(DataNodeList data); // What about GetForSort()? We like sortable tables. Most of that's built // into DataGridView. What we need is to give an unformatted object // (presumably an integer or a double most of the time) to the DataGridView. protected int ClientWidth { get; set; } protected Font Font { get; private set; } protected bool TooWide(string text) { Size textSize = TextRenderer.MeasureText(text, Font); return textSize.Width > ClientWidth; } private string MakeTruncatedDoubleFit(double adjusted, char symbol) { string result = String.Format("{0:f2}", adjusted) + symbol; if (TooWide(result)) { result = String.Format("{0:f1}", adjusted) + symbol; } if (TooWide(result)) { result = String.Format("{0:f0}", adjusted) + symbol; } if (TooWide(result)) { result = "..."; } return result; } private string MakeDoubleFitByTruncating(double value) { if (value < 1000) return "..."; else if (value < 1000000) return MakeTruncatedDoubleFit(value / 1.0e+3, 'K'); else if (value < 1000000000) return MakeTruncatedDoubleFit(value / 1.0e+6, 'M'); else return MakeTruncatedDoubleFit(value / 1.0e+9, 'B'); } private static readonly string[] DOUBLE_FORMAT_STRINGS = new string[] { "#,##0", "#,#0.0", "#,#0.#0", "#,#0.##0", "#,#0.###0", "#,#0.####0", "#,#0.#####0" }; protected string MakeDoubleFit(double value, int digits) { if (digits >= DOUBLE_FORMAT_STRINGS.Length) digits = DOUBLE_FORMAT_STRINGS.Length - 1; else if (digits < 0) digits = 0; while (true) { if (digits < 0) return MakeDoubleFitByTruncating(value); string result = value.ToString(DOUBLE_FORMAT_STRINGS[digits]); if (!TooWide(result)) return result; digits--; } } protected static void FixColorFactories(ref DataNode.Factory foreColor, ref DataNode.Factory backColor) { if (null == foreColor) if (null == backColor) { // Nothing is specified. Always use the default colors. // Note: If we leave a factory as null, DataNodeList will act as if this was a constant factory always returning null. // Note: DataNodeLabel hard coded a specific color value here. // We can't do that. The color will get picked at run time. That way we don't need // a special event if someone changes the color of a label. Presumably a table row // might change colors if someone clicks on that row. Instead Paint() will ask for // default colors and use them if either color data node returns null. } else { // The user didn't specify a foreground color, so pick one that should look good // with the background color. Notice that the two colors sharing the GuiDataNode and the // data its holding. backColor = GuiDataNode.GetFactory(backColor); foreColor = TransformationDataNode.GetFactory(GradientInfoDataNode.AltColor, backColor); } else if (null == backColor) { // The user didn't specifiy a background color, so pick one based on the foreground. foreColor = GuiDataNode.GetFactory(foreColor); backColor = TransformationDataNode.GetFactory(GradientInfoDataNode.AltColor, foreColor); } else { // The user specified everything. foreColor = GuiDataNode.GetFactory(foreColor); backColor = GuiDataNode.GetFactory(backColor); } } protected static void MergeColors(ref Color foreColor, ref Color backColor, object dataForeColor, object dataBackColor) { Color? foreground = dataForeColor as Color?; Color? background = dataBackColor as Color?; if (foreground.HasValue && background.HasValue) { // Only change the color if both the forground and background are valid. // If you use the specified foreground color and the default background // color (or vice versa) you might make an unreadable combination. foreColor = foreground.Value; backColor = background.Value; } } /// /// /// /// /// /// We clip to this area. fatEnd and thinEnd are relative to this. /// /// /// 0.0 is the left edge of the destination. 1.0 is the right edge of the destination. /// -1.0 and 2.0 are outside of the destination, to the left and right respectively. /// /// /// 0.0 is the left edge of the destination. 1.0 is the right edge of the destination. /// -1.0 and 2.0 are outside of the destination, to the left and right respectively. /// /// protected void DrawTriangle(Graphics graphics, Rectangle destination, double fatEnd, double thinEnd, Brush brush) { Region previousClip = graphics.Clip; graphics.SetClip(destination, CombineMode.Intersect); SmoothingMode previousSmoothingMode = graphics.SmoothingMode; graphics.SmoothingMode = SmoothingMode.HighQuality; if (destination.Width < 1) return; int fatX = destination.X + (int)(destination.Width * fatEnd + 0.5); int thinX = destination.X + (int)(destination.Width * thinEnd + 0.5); Point[] points = { new Point(fatX, destination.Y), new Point(thinX, destination.Y + (destination.Height / 2)), new Point(fatX, destination.Y + destination.Height) }; graphics.FillPolygon(brush, points); graphics.Clip = previousClip; graphics.SmoothingMode = previousSmoothingMode; //must reset back to "none" to prevent gridline anomalies... } protected void DrawDouble(Graphics graphics, Rectangle destination, double value, int digits, Color color) { if (double.IsNaN(value) || double.IsNaN(value)) return; string text = MakeDoubleFit(value, digits); TextRenderer.DrawText(graphics, text, Font, destination, color, TextFormatFlags.NoPrefix | TextFormatFlags.VerticalCenter | TextFormatFlags.Right); } public static void MakeCylinder(Graphics g, Rectangle rect) { using (LinearGradientBrush brush = new LinearGradientBrush(rect, Color.Transparent, Color.Transparent, LinearGradientMode.Vertical)) { ColorBlend colorBlend = new ColorBlend(4); int alpha = 145; colorBlend.Colors[0] = Color.FromArgb(0, 255, 255, 255); colorBlend.Positions[0] = 0.0f; colorBlend.Colors[1] = Color.FromArgb(alpha, 255, 255, 255); colorBlend.Positions[1] = 0.25f; colorBlend.Colors[2] = Color.FromArgb(0, 255, 255, 255); colorBlend.Positions[2] = 0.5f; colorBlend.Colors[3] = Color.FromArgb(alpha, 0, 0, 0); colorBlend.Positions[3] = 1.0f; brush.InterpolationColors = colorBlend; g.FillRectangle(brush, rect); } } private const string ARGS = "ARGS"; static protected void Encode(XmlNode body, string type, params object[] args) { body.SetProperty(XmlSerializer.TYPE, type); XmlSerializer.Encode(new DataNode.StaticList(args), body.NewNode(ARGS)); } static protected void RegisterDecoder(string xmlType, Type internalType) { XmlSerializer.RegisterDecoder(xmlType, source => StandardDecoder(internalType, source)); } private static XmlElement GetArgs(XmlElement factory) { XmlElement result = factory.Node(ARGS); if (null == result) throw new ArgumentException("Could not find ARGS in DataNodeViewer."); return result; } private static DataNodeViewer StandardDecoder(Type type, XmlElement source) { XmlElement args = GetArgs(source); DataNode.StaticList argsAsList = new DataNode.StaticList(args); object[] argsAsArray = argsAsList.GetItems(); object result = type.InvokeMember(null, BindingFlags.Instance | BindingFlags.CreateInstance | BindingFlags.Public | BindingFlags.OptionalParamBinding, null, null, argsAsArray); return (DataNodeViewer)result; } private Hasher GetHashCodeBase() { // This is probably it. I don't think we'll add any fields to this class. return new Hasher(GetType()); } protected int MakeHashCode(params object[] fields) { Hasher hasher = GetHashCodeBase(); hasher.Add(fields); return hasher.GetHashCode(); } } /// /// Display DataNode results as text. You have to provide a DataNode.Factory for the /// string to display, the foreground color, and the background color. /// [XmlSerializerInit("*")] public class DataNodeTextViewer : DataNodeViewer, XmlSerializer.Self { private const int VALUE_INDEX = 0; private const int FOREGROUND_INDEX = 1; private const int BACKGROUND_INDEX = 2; private readonly DataNode.StaticList _effectiveFactories; public override DataNodeList GetData(Dictionary replacements) { return DataNodeList.Create(_effectiveFactories.ReplaceWith(replacements)); } protected override void Paint(DataNodeList data, Graphics graphics, Rectangle bounds, Color ForeColor, Color BackColor, bool alwaysUseTheseColors) { if (!alwaysUseTheseColors) MergeColors(ref ForeColor, ref BackColor, data[FOREGROUND_INDEX], data[BACKGROUND_INDEX]); using (Brush bgBrush = new SolidBrush(BackColor)) { graphics.FillRectangle(bgBrush, bounds); object text = data[VALUE_INDEX]; if (null != text) TextRenderer.DrawText(graphics, text.ToString(), Font, bounds, ForeColor, TextFormatFlags.EndEllipsis | TextFormatFlags.NoPrefix | TextFormatFlags.VerticalCenter); } } public override String GetForCopy(DataNodeList data) { // I think this is required for copy and paste in the table. // Can a table cell return HTML for formatting? // Should we also return a value for sorting? return data.GetString(VALUE_INDEX); } public DataNodeTextViewer(DataNode.Factory value, DataNode.Factory foreColor = null, DataNode.Factory backColor = null) { ValueFactory = value; ForegroundFactory = foreColor; BackgroundFactory = backColor; value = GuiDataNode.GetFactory(value); FixColorFactories(ref foreColor, ref backColor); _effectiveFactories = DataNode.StaticList.Create(value, foreColor, backColor); } public DataNode.Factory ValueFactory { get; private set; } public DataNode.Factory ForegroundFactory { get; private set; } public DataNode.Factory BackgroundFactory { get; private set; } private const string TYPE = "DATA_NODE_TEXT_VIEWER"; public void Encode(XmlElement body) { Encode(body, TYPE, ValueFactory, ForegroundFactory, BackgroundFactory); } private static void XmlSerializerInit() { RegisterDecoder(TYPE, typeof(DataNodeTextViewer)); } public override bool Equals(object obj) { if (ReferenceEquals(this, obj)) return true; if (!(obj is DataNodeTextViewer)) return false; DataNodeTextViewer other = (DataNodeTextViewer)obj; return Equals(ForegroundFactory, other.ForegroundFactory) && Equals(BackgroundFactory, other.BackgroundFactory) && Equals(ValueFactory, other.ValueFactory); } public override int GetHashCode() { return MakeHashCode(ForegroundFactory, BackgroundFactory, ValueFactory); } } /// /// Display DataNode results as a number. You have to provide a DataNode.Factory for the /// string to display, the foreground color, and the background color. Also provide a /// constant for the preferred number of digits after the decimal. /// [XmlSerializerInit("*")] public class DataNodeNumberViewer : DataNodeViewer, XmlSerializer.Self { private const int VALUE_INDEX = 0; private const int FOREGROUND_INDEX = 1; private const int BACKGROUND_INDEX = 2; /// /// We do various things to the request. For example, if you only specify one color, /// we pick the other for you. But that doesn't change the properties. If backColor /// was null in the constructor the BackColor property will return null, even if we /// choose to display a different color. /// /// _effectiveFactories could be recomputed at any time from the properties. /// private readonly DataNode.StaticList _effectiveFactories; public readonly int Digits; public override DataNodeList GetData(Dictionary replacements) { return DataNodeList.Create(_effectiveFactories.ReplaceWith(replacements)); } protected override void Paint(DataNodeList data, Graphics graphics, Rectangle bounds, Color ForeColor, Color BackColor, bool alwaysUseTheseColors) { if (!alwaysUseTheseColors) MergeColors(ref ForeColor, ref BackColor, data[FOREGROUND_INDEX], data[BACKGROUND_INDEX]); using (Brush bgBrush = new SolidBrush(BackColor)) { graphics.FillRectangle(bgBrush, bounds); double? value = data.GetDouble(VALUE_INDEX); if (value.HasValue) DrawDouble(graphics, bounds, value.Value, Digits, ForeColor); } } private static String GetValue(DataNodeList data) { object value = data[VALUE_INDEX]; if (null == value) return ""; else return value.ToString(); } public override String GetForCopy(DataNodeList data) { // I think this is required for copy and paste in the table. // Can a table cell return HTML for formatting? // Should we also return a value for sorting? return GetValue(data); } public DataNodeNumberViewer(DataNode.Factory value, int digits, DataNode.Factory foreColor = null, DataNode.Factory backColor = null) { ValueFactory = value; ForegroundFactory = foreColor; BackgroundFactory = backColor; value = GuiDataNode.GetFactory(value); FixColorFactories(ref foreColor, ref backColor); _effectiveFactories = DataNode.StaticList.Create(value, foreColor, backColor); Digits = digits; } public DataNode.Factory ValueFactory { get; private set; } public DataNode.Factory ForegroundFactory { get; private set; } public DataNode.Factory BackgroundFactory { get; private set; } private const string TYPE = "DATA_NODE_NUMBER_VIEWER"; public void Encode(XmlElement body) { Encode(body, TYPE, ValueFactory, Digits, ForegroundFactory, BackgroundFactory); } private static void XmlSerializerInit() { RegisterDecoder(TYPE, typeof(DataNodeNumberViewer)); } public override bool Equals(object obj) { if (ReferenceEquals(this, obj)) return true; if (!(obj is DataNodeNumberViewer)) return false; DataNodeNumberViewer other = (DataNodeNumberViewer)obj; return (Digits == other.Digits) && Equals(ForegroundFactory, other.ForegroundFactory) && Equals(BackgroundFactory, other.BackgroundFactory) && Equals(ValueFactory, other.ValueFactory); } public override int GetHashCode() { return MakeHashCode(Digits, ForegroundFactory, BackgroundFactory, ValueFactory); } } /// /// Display DataNode.Value as a series of triangles. Also display it as a number. /// The colors are fixed. /// [XmlSerializerInit("*")] public class DataNodeLogTrianglesViewer : DataNodeViewer, XmlSerializer.Self { private const int VALUE_INDEX = 0; private readonly DataNode.StaticList _effectiveFactories; public readonly int Digits; public override DataNodeList GetData(Dictionary replacements) { return DataNodeList.Create(_effectiveFactories.ReplaceWith(replacements)); } private static readonly Color TEXT_COLOR = Color.White; private static readonly Brush TRIANGLE1_BRUSH = new SolidBrush(Color.FromArgb(15, 28, 239)); private static readonly Brush TRIANGLE10_BRUSH = new SolidBrush(Color.DarkMagenta); private static readonly Brush TRIANGLE100_BRUSH = new SolidBrush(Color.FromArgb(12, 70, 21)); private static readonly Brush BACKGROUND_BRUSH = new SolidBrush(Color.FromArgb(0, 0, 9)); protected override void Paint(DataNodeList data, Graphics graphics, Rectangle bounds, Color ForeColor, Color BackColor, bool alwaysUseTheseColors) { graphics.FillRectangle(BACKGROUND_BRUSH, bounds); double? value = data.GetDouble(VALUE_INDEX); if (value.HasValue) { DrawTriangle(graphics, bounds, 0, value.Value / 100.0, TRIANGLE1_BRUSH); DrawTriangle(graphics, bounds, 0, value.Value / 1000.0, TRIANGLE10_BRUSH); DrawTriangle(graphics, bounds, 0, value.Value / 10000.0, TRIANGLE100_BRUSH); DrawDouble(graphics, bounds, value.Value, Digits, TEXT_COLOR); } } private static String GetValue(DataNodeList data) { object value = data[VALUE_INDEX]; if (null == value) return ""; else return value.ToString(); } public override String GetForCopy(DataNodeList data) { // I think this is required for copy and paste in the table. // Can a table cell return HTML for formatting? // Should we also return a value for sorting? return GetValue(data); } public DataNodeLogTrianglesViewer(DataNode.Factory value, int digits = 1) { ValueFactory = value; value = GuiDataNode.GetFactory(value); _effectiveFactories = DataNode.StaticList.Create(value); Digits = Math.Max(0, Math.Min(6, digits)); } public DataNode.Factory ValueFactory { get; private set; } private const string TYPE = "DATA_NODE_LOG_TRIANGLES_VIEWER"; public void Encode(XmlElement body) { Encode(body, TYPE, ValueFactory, Digits); } private static void XmlSerializerInit() { RegisterDecoder(TYPE, typeof(DataNodeLogTrianglesViewer)); } public override bool Equals(object obj) { if (ReferenceEquals(this, obj)) return true; if (!(obj is DataNodeLogTrianglesViewer)) return false; DataNodeLogTrianglesViewer other = (DataNodeLogTrianglesViewer)obj; return (Digits == other.Digits) && Equals(ValueFactory, other.ValueFactory); } public override int GetHashCode() { return MakeHashCode(Digits, ValueFactory); } } [XmlSerializerInit("*")] public class DataNodeRectangleViewer : DataNodeViewer, XmlSerializer.Self { public DataNodeRectangleViewer(DataNode.Factory left, DataNode.Factory foreground, DataNode.Factory background, DataNode.Factory secondValue, ThreeD threeD, SecondValueMeaning secondValueMeaning) { LeftValueFactory = left; ForegroundFactory = foreground; BackgroundFactory = background; OtherValueFactory = secondValue; left = GuiDataNode.GetFactory(left); secondValue = GuiDataNode.GetFactory(secondValue); FixColorFactories(ref foreground, ref background); _effectiveFactories = DataNode.StaticList.Create(left, foreground, background, secondValue); _threeD = threeD; _secondValueMeaning = secondValueMeaning; } private const int LEFT_VALUE_INDEX = 0; private const int FOREGROUND_INDEX = 1; private const int BACKGROUND_INDEX = 2; private const int OTHER_VALUE_INDEX = 3; private readonly DataNode.StaticList _effectiveFactories; public enum ThreeD { None, Rectangle, All } private readonly ThreeD _threeD; public enum SecondValueMeaning { Width, Right } private readonly SecondValueMeaning _secondValueMeaning; public override DataNodeList GetData(Dictionary replacements) { return DataNodeList.Create(_effectiveFactories.ReplaceWith(replacements)); } public override string GetForCopy(DataNodeList data) { if (_secondValueMeaning == SecondValueMeaning.Width) return data.GetString(LEFT_VALUE_INDEX); else return ""; } protected override void Paint(DataNodeList data, Graphics graphics, Rectangle bounds, Color ForeColor, Color BackColor, bool alwaysUseTheseColors) { if (!alwaysUseTheseColors) MergeColors(ref ForeColor, ref BackColor, data[FOREGROUND_INDEX], data[BACKGROUND_INDEX]); using (Brush bgBrush = new SolidBrush(BackColor)) { graphics.FillRectangle(bgBrush, bounds); } double? leftValue = data.GetDouble(LEFT_VALUE_INDEX); double? otherValue = data.GetDouble(OTHER_VALUE_INDEX); if (leftValue.HasValue && otherValue.HasValue) { double leftRatio; double rightRatio; if (_secondValueMeaning == SecondValueMeaning.Right) { leftRatio = leftValue.Value / 100; rightRatio = otherValue.Value / 100; } else { double width = otherValue.Value; if ((width < 0) || (width >= 100)) // Invalid leftRatio = rightRatio = -1; else { double remainingSpace = (100 - width) / 100; leftRatio = leftValue.Value / 100 * remainingSpace; rightRatio = leftRatio + width / 100; } } if (leftRatio > rightRatio) { // By default FillRectangle will ignore something with a negative width. double temp = leftRatio; leftRatio = rightRatio; rightRatio = temp; } int left = (int)(leftRatio * bounds.Width + 0.5); int right = (int)(rightRatio * bounds.Width + 0.5); using (Brush brush = new SolidBrush(ForeColor)) { Rectangle selection = new Rectangle(bounds.X + left, bounds.Y, right - left, bounds.Height); graphics.FillRectangle(brush, selection); if (_threeD == ThreeD.Rectangle) MakeCylinder(graphics, selection); } if (_threeD == ThreeD.All) // We make the entire thing a cylinder. But only if we have values for the rectangle. // If there's a null, then we're always flat. MakeCylinder(graphics, bounds); } } public DataNode.Factory LeftValueFactory { get; private set; } public DataNode.Factory ForegroundFactory { get; private set; } public DataNode.Factory BackgroundFactory { get; private set; } public DataNode.Factory OtherValueFactory { get; private set; } public ThreeD GetThreeD() { return _threeD; } public SecondValueMeaning GetSecondValueMeaning() { return _secondValueMeaning; } private const string TYPE = "DATA_NODE_RECTANGLE_VIEWER"; public void Encode(XmlElement body) { Encode(body, TYPE, LeftValueFactory, ForegroundFactory, BackgroundFactory, OtherValueFactory, _threeD, _secondValueMeaning); } private static void XmlSerializerInit() { RegisterDecoder(TYPE, typeof(DataNodeRectangleViewer)); XmlSerializer.AddEnum(typeof(ThreeD)); XmlSerializer.AddEnum(typeof(SecondValueMeaning)); } public override bool Equals(object obj) { if (ReferenceEquals(this, obj)) return true; if (!(obj is DataNodeRectangleViewer)) return false; DataNodeRectangleViewer other = (DataNodeRectangleViewer)obj; return (_threeD == other._threeD) && (_secondValueMeaning == other._secondValueMeaning) && Equals(LeftValueFactory, other.LeftValueFactory) && Equals(ForegroundFactory, other.ForegroundFactory) && Equals(BackgroundFactory, other.BackgroundFactory) && Equals(OtherValueFactory, other.OtherValueFactory); } public override int GetHashCode() { // TODO this could be cached, like in a DataNode.StaticList return MakeHashCode(LeftValueFactory, ForegroundFactory, BackgroundFactory, OtherValueFactory, _threeD, _secondValueMeaning); } }; [XmlSerializerInit("*")] public class DataNodeTriangleViewer : DataNodeViewer, XmlSerializer.Self { public DataNodeTriangleViewer(DataNode.Factory fat, DataNode.Factory foreground, DataNode.Factory background, DataNode.Factory thin) { FatValueFactory = fat; ForegroundFactory = foreground; BackgroundFactory = background; ThinValueFactory = thin; fat = GuiDataNode.GetFactory(fat); thin = GuiDataNode.GetFactory(thin); FixColorFactories(ref foreground, ref background); _effectiveFactories = DataNode.StaticList.Create(fat, foreground, background, thin); } private const int FAT_VALUE_INDEX = 0; private const int FOREGROUND_INDEX = 1; private const int BACKGROUND_INDEX = 2; private const int THIN_VALUE_INDEX = 3; private readonly DataNode.StaticList _effectiveFactories; public override DataNodeList GetData(Dictionary replacements) { return DataNodeList.Create(_effectiveFactories.ReplaceWith(replacements)); } public override string GetForCopy(DataNodeList data) { return ""; } protected override void Paint(DataNodeList data, Graphics graphics, Rectangle bounds, Color ForeColor, Color BackColor, bool alwaysUseTheseColors) { if (!alwaysUseTheseColors) MergeColors(ref ForeColor, ref BackColor, data[FOREGROUND_INDEX], data[BACKGROUND_INDEX]); using (Brush bgBrush = new SolidBrush(BackColor)) { graphics.FillRectangle(bgBrush, bounds); } double? fatValue = data.GetDouble(FAT_VALUE_INDEX); double? thinValue = data.GetDouble(THIN_VALUE_INDEX); if (fatValue.HasValue && thinValue.HasValue) using (Brush brush = new SolidBrush(ForeColor)) DrawTriangle(graphics, bounds, fatValue.Value / 100, thinValue.Value / 100, brush); } public DataNode.Factory FatValueFactory { get; private set; } public DataNode.Factory ForegroundFactory { get; private set; } public DataNode.Factory BackgroundFactory { get; private set; } public DataNode.Factory ThinValueFactory { get; private set; } private const string TYPE = "DATA_NODE_TRIANGLE_VIEWER"; public void Encode(XmlElement body) { Encode(body, TYPE, FatValueFactory, ForegroundFactory, BackgroundFactory, ThinValueFactory); } private static void XmlSerializerInit() { RegisterDecoder(TYPE, typeof(DataNodeTriangleViewer)); } public override bool Equals(object obj) { if (ReferenceEquals(this, obj)) return true; if (!(obj is DataNodeTriangleViewer)) return false; DataNodeTriangleViewer other = (DataNodeTriangleViewer)obj; return Equals(FatValueFactory, other.FatValueFactory) && Equals(ForegroundFactory, other.ForegroundFactory) && Equals(BackgroundFactory, other.BackgroundFactory) && Equals(ThinValueFactory, other.ThinValueFactory); } public override int GetHashCode() { return MakeHashCode(FatValueFactory, ForegroundFactory, BackgroundFactory, ThinValueFactory); } }; public class SimpleDataNodeControl : Control, IReplaceWithRuntime { private DataNodeList _data; /// /// This is required for the form designer. /// /// A SimpleDataNodeControl isn't useful until you set the viewer, but we /// allow you to set (and change and clear) the viewer later because sometimes /// that is conveneint. /// public SimpleDataNodeControl() { // ResizeRedraw is essential. By default Windows assumes that whatever we are drawing is anchored // to the top left corner. If this control shrinks but the top left corner doesn't move, we // never receive a call to OnPaint(). If the control grows, Windows will set a recommended clipping // region before calling OnPaint(). It will tell us only to draw the new part. We want a complete // redraw each time. Most of our drawings are centered or stretched, so the default assumption is // not true. // // ControlStyles.AllPaintingInWmPaint & ControlStyles.UserPaint should prevent windows from calling // OnPaintBackground(). I didn't have any luck with that. I left these in place anyway because they // didn't seem to hurt and everything I've read said they should be true. SetStyle(ControlStyles.ResizeRedraw | ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint, true); } public SimpleDataNodeControl(DataNodeViewer viewer) : this() { Viewer = viewer; } protected override void Dispose(bool disposing) { Viewer = null; base.Dispose(disposing); } private static readonly Brush ERROR_BACKGROUND = new SolidBrush(Color.Black); private static readonly Color ERROR_FOREGROUND = Color.Red; private static readonly Color DESIGN_MODE_FOREGROUND = Color.White; protected override void OnPaint(PaintEventArgs e) { Rectangle bounds = new Rectangle(0, 0, Width, Height); //System.Diagnostics.Debug.WriteLine(DateTime.Now.ToLongTimeString() + ": OnPaint(), Name=" + Name // + ", Bounds=" + Bounds + ", bounds =" + bounds + ", ClipRectangle=" + e.ClipRectangle); if (null == _data) { e.Graphics.FillRectangle(ERROR_BACKGROUND, bounds); if (DesignMode) TextRenderer.DrawText(e.Graphics, Name, Font, bounds, DESIGN_MODE_FOREGROUND, TextFormatFlags.NoPrefix | TextFormatFlags.VerticalCenter | TextFormatFlags.Left); else TextRenderer.DrawText(e.Graphics, "null", Font, bounds, ERROR_FOREGROUND, TextFormatFlags.NoPrefix | TextFormatFlags.VerticalCenter | TextFormatFlags.HorizontalCenter); } else if (null != _data.ErrorMessage) { e.Graphics.FillRectangle(ERROR_BACKGROUND, bounds); TextRenderer.DrawText(e.Graphics, _data.ErrorMessage, Font, bounds, ERROR_FOREGROUND, TextFormatFlags.NoPrefix | TextFormatFlags.VerticalCenter | TextFormatFlags.Left); } else _viewer.Paint(_data, CreateGraphics(), bounds, ForeColor, BackColor, _forceColors, Font); // Call any Paint delegates. base.OnPaint(e); } protected override void OnPaintBackground(PaintEventArgs pevent) { // It seems like ControlStyles.AllPaintingInWmPaint and/or ControlStyles.UserPaint // should have prevented windows from calling this method. However, we are still // gettings called. The default (a.k.a. "base") method will paint in the control's // background color. That caused a lot of flickering. //base.OnPaintBackground(pevent); // Commenting out the line above didn't stop ALL the flickering! // On closer examination, it seems that one of the parent controls was drawing its // own background each time we resized. See ViewerEditor.cs. I changed the BackColor // of these objects to something different form the container's BackColor. Then I // could see that commenting out the base.OnPaintBackground() did help some. It // prevented us from drawing our own background color. But it didn't help with // everything. // I tried double buffering in different places. That didn't help, either. I // suspect our current setup will work great as long as you are just changing values. // It seems that some flickering is inevitable if you have a complicated layout and // you resize the window. } private bool _forceColors; /// /// If this is true we always try to use the specified ForeColor and BackColor. /// If this is false, ForeColor and BackColor are defaults. /// The viewer can do what it wants with this, including ignoring it completely. /// This is mostly for testing. Similar functionality is REQUIRED in the table. /// public bool ForceColors { get { return _forceColors; } set { if (_forceColors != value) { _forceColors = value; Invalidate(); } } } private void GetData() { if (null != _data) _data.Release(); _data = null; if ((null != _viewer) && (null != _replacements)) { _data = Viewer.GetData(Replacements); _data.OnChange += Invalidate; } Invalidate(); } private Dictionary _replacements = new Dictionary(); [System.Xml.Serialization.XmlIgnore] public Dictionary Replacements { get { return _replacements; } set { _replacements = value; GetData(); } } private DataNodeViewer _viewer; /// /// Null is allowed, but it is intended as a temporary state. It might display an /// error message aimed at a developer, not an end user. Setting this to null will /// cancel any data requests. /// public DataNodeViewer Viewer { get { return _viewer; } set { _viewer = value; GetData(); } } [XmlSerializerInit("*")] public class Factory : DataNodeControlFactory, XmlSerializer.Self { public DataNodeViewer Viewer { get; set; } public Factory(DataNodeViewer viewer = null) { Viewer = viewer; } public override Control Create() { return new SimpleDataNodeControl(Viewer); } public override bool Equals(object obj) { if (!base.Equals(obj)) return false; Factory other = (Factory)obj; return Equals(Viewer, other.Viewer); } public override int GetHashCode() { return MakeHashCode(Viewer); } private const string TYPE = "SIMPLE_DATA_NODE_CONTROL"; private const string VIEWER = "VIEWER"; public void Encode(XmlElement body) { body.SetProperty(XmlSerializer.TYPE, TYPE); XmlSerializer.Encode(Viewer, body.NewNode(VIEWER)); } private static void XmlSerializerInit() { XmlSerializer.RegisterDecoder(TYPE, Decoder); } private static object Decoder(XmlElement body) { XmlElement viewerXml = body.Node(VIEWER); if (null == viewerXml) throw new ArgumentException("Could not find " + VIEWER); Factory result = new Factory(); result.Viewer = (DataNodeViewer)XmlSerializer.Decode(viewerXml); return result; } } } public class SimpleDataNodePanel : Panel, IReplaceWithRuntime { private Dictionary _replacements; [System.Xml.Serialization.XmlIgnore] public Dictionary Replacements { get { return _replacements; } set { _replacements = value; foreach (Control control in Controls) control.TryReplaceWith(Replacements); } } public SimpleDataNodePanel() { ControlAdded += DoControlAdded; } private void DoControlAdded(object sender, System.Windows.Forms.ControlEventArgs e) { e.Control.TryReplaceWith(_replacements); } /// /// My version of System.Windows.Forms.AnchorStyles. /// public enum StickTo { /// /// The distance from the bottom or right will change as you resize the window. /// First, /// /// The distance from the top or left will change as you resize the window. /// Second, /// /// The width or height will change as you resize the window. /// Both } [XmlSerializerInit("*")] public class Child : XmlSerializer.Self { /// /// All fields are null. This is not a valid state. It's just a convenient /// place to start. /// public Child() { } /// /// Set the size/position/anchors to a reasonable default, something legal. /// Copy the factory as is. /// /// May be null. public Child(DataNodeControlFactory factory) { Factory = factory; Left = 5; Width = 100; Top = 5; Height = 25; } /// /// Load from XML. /// /// public Child(XmlElement top) { XmlElement factoryXml = top.Node(FACTORY); if (null != factoryXml) Factory = (DataNodeControlFactory)XmlSerializer.Decode(factoryXml); // else throw new ArgumentException("Cannot find FACTORY for child."); // We've gone both ways on that else. Typically when I serialize something // I'm somewhat strict, and I don't like to see bad objects. Typically I'd // do a lot of checking, and the deserialization function (like this one) // would throw an exception rather than returning a bad value. However, it // seems convenient to leave this factory null while you're in the process // of building a panel. Top = top.PropertyInt32(TOP); Height = top.PropertyInt32(HEIGHT); Bottom = top.PropertyInt32(BOTTOM); Left = top.PropertyInt32(LEFT); Width = top.PropertyInt32(WIDTH); Right = top.PropertyInt32(RIGHT); if (!Valid) throw new ArgumentException("Invalid position information."); } private const string FACTORY = "FACTORY"; private const string PANEL_CHILD = "PANEL_CHILD"; private const string TOP = "TOP"; private const string HEIGHT = "HEIGHT"; private const string BOTTOM = "BOTTOM"; private const string LEFT = "LEFT"; private const string WIDTH = "WIDTH"; private const string RIGHT = "RIGHT"; private static void AddValue(XmlElement body, string Name, int? i) { if (i.HasValue) body.SetProperty(Name, i.Value); } public void Encode(XmlElement body) { body.SetProperty(XmlSerializer.TYPE, PANEL_CHILD); if (null != Factory) XmlSerializer.Encode(Factory, body.NewNode(FACTORY)); AddValue(body, TOP, Top); AddValue(body, HEIGHT, Height); AddValue(body, BOTTOM, Bottom); AddValue(body, LEFT, Left); AddValue(body, WIDTH, Width); AddValue(body, RIGHT, Right); } private static void XmlSerializerInit() { XmlSerializer.RegisterDecoder(PANEL_CHILD, top => new Child(top)); } public DataNodeControlFactory Factory; /// /// The top of the control. 0 means the top of the control touches the top /// of the parent. Positive numbers mean to go down. Exactly 1 of Top, Height, /// and Bottom should be null. /// public int? Top; /// /// The height of the control, in pixels. Exactly 1 of Top, Height, and /// Bottom should be null. /// public int? Height; /// /// The bottom of the control. 0 means the bottom of the control touches the /// bottom of the parent. Positive numbers mean to go up. (So this does NOT /// match the Bottom property of a Control.) Exactly 1 of Top, Height, and /// Bottom should be null. /// public int? Bottom; /// /// The left of the control. 0 means the left of the control touches the left /// of the parent. Positive numbers mean to go right. Exactly 1 of Left, /// Width, and Right should be null. /// public int? Left; /// /// The width of the control, in pixels. Exactly 1 of Left, Width, and Right /// should be null. /// public int? Width; /// /// The right of the control. 0 means the right of the control touches the /// right of the parent. Positive numbers mean to go left. (So this does NOT /// match the Right property of a Control.) Exactly 1 of Left, Width, and /// Right should be null. /// public int? Right; private static bool OneDimensionValid(int? a, int? b, int? c) { int count = 0; if (a.HasValue) count++; if (b.HasValue) count++; if (c.HasValue) count++; return count == 2; } public bool Valid { get { return OneDimensionValid(Top, Height, Bottom) && OneDimensionValid(Left, Width, Right); } } public AnchorStyles AnchorStyles { get { AnchorStyles result = AnchorStyles.None; if (Top.HasValue) result |= AnchorStyles.Top; if (Left.HasValue) result |= AnchorStyles.Left; if (Bottom.HasValue) result |= AnchorStyles.Bottom; if (Right.HasValue) result |= AnchorStyles.Right; return result; } } /// /// Find the value for the Top property of this control. Someone might have /// directly specified that, in which case we return that as is. Alternatively /// someone might have specified the bottom (relative to the container's bottom) /// and the height. In that case we compute the requested value. /// /// If Valid is false, the results of this function are undefined. This function /// might fail an assertion. /// /// Typically this the ClientSize of the Parent. /// The requested Top property for this control. public int GetTop(Size relativeTo) { if (Top.HasValue) return Top.Value; else if (Height.HasValue && Bottom.HasValue) return (relativeTo.Height - Bottom.Value) - Height.Value; else // Valid is false. We could throw and exception or fail and assertion. // Instead I'm returning a reasonable default. return 0; } /// /// Find the value for the Left property of this control. Someone might have /// directly specified that, in which case we return that as is. Alternatively /// someone might have specified the right (relative to the container's right) /// and the width. In that case we compute the requested value. /// /// If Valid is false, the results of this function are undefined. This function /// might fail an assertion. /// /// Typically this the ClientSize of the Parent. /// The requested Left property for this control. public int GetLeft(Size relativeTo) { if (Left.HasValue) return Left.Value; else if (Width.HasValue && Right.HasValue) return (relativeTo.Width - Right.Value) - Width.Value; else // Valid is false. We could throw and exception or fail and assertion. // Instead I'm returning a reasonable default. return 0; } public int GetHeight(Size relativeTo) { if (Height.HasValue) return Height.Value; else if (Top.HasValue && Bottom.HasValue) return relativeTo.Height - Top.Value - Bottom.Value; else throw new ArgumentException("Exactly one of Height, Top, and Bottom should be null."); } public int GetWidth(Size relativeTo) { if (Width.HasValue) return Width.Value; else if (Left.HasValue && Right.HasValue) return relativeTo.Width - Left.Value - Right.Value; else throw new ArgumentException("Exactly one of Width, Left and Right should be null."); } public void AddToParent(Control child, Panel parent) { child.Parent = null; child.Anchor = AnchorStyles; Size size = parent.ClientSize; child.Top = GetTop(size); child.Left = GetLeft(size); // TODO what if a height or width is negative? child.Height = GetHeight(size); child.Width = GetWidth(size); parent.Controls.Add(child); } /// /// This coresponds to the Right property of this object. /// /// /// public int GetRightFromRight(Size relativeTo) { if (Right.HasValue) return Right.Value; return (relativeTo.Width - Left.Value) - Width.Value; } /// /// The coresponds to the Bottom property of this object. /// /// /// public int GetBottomFromBottom(Size relativeTo) { if (Bottom.HasValue) return Bottom.Value; return (relativeTo.Height - Top.Value) - Height.Value; } /// /// This corresponds to the Right property of a normal Control. /// /// /// public int GetRightFromLeft(Size relativeTo) { return relativeTo.Width - GetRightFromRight(relativeTo); } /// /// This corresponds to the Bottom property of a normal Control. /// /// /// public int GetBottomFromTop(Size relativeTo) { return relativeTo.Height - GetBottomFromBottom(relativeTo); } public Control Create(Panel parent = null) { Control result = Factory.Create(); if (null != parent) try { AddToParent(result, parent); } catch { result.Dispose(); throw; } return result; } public void MoveLeftEdgeToTheRight(int pixelsRight) { if (Left.HasValue) Left += pixelsRight; if (Width.HasValue) Width -= pixelsRight; } public void MoveToTheRight(int pixelsRight) { if (Left.HasValue) Left += pixelsRight; if (Right.HasValue) Right += pixelsRight; } public void MoveRightEdgeToTheRight(int pixelsRight) { if (Width.HasValue) Width += pixelsRight; if (Right.HasValue) Right += pixelsRight; } public void MoveTopEdgeDown(int pixelsDown) { if (Top.HasValue) Top += pixelsDown; if (Height.HasValue) Height -= pixelsDown; } public void MoveDown(int pixelsDown) { if (Top.HasValue) Top += pixelsDown; if (Bottom.HasValue) Bottom += pixelsDown; } public void MoveBottomEdgeDown(int pixelsDown) { if (Height.HasValue) Height += pixelsDown; if (Bottom.HasValue) Bottom += pixelsDown; } public void AutoAdjustLeft(Size relativeTo) { if (!Left.HasValue) return; // We're already there. if (!Width.HasValue) Width = GetWidth(relativeTo); if (!Right.HasValue) Right = GetRightFromRight(relativeTo); Left = null; } public void AutoAdjustWidth(Size relativeTo) { if (!Width.HasValue) return; // We're already there. if (!Left.HasValue) Left = GetLeft(relativeTo); if (!Right.HasValue) Right = GetRightFromRight(relativeTo); Width = null; } public void AutoAdjustRight(Size relativeTo) { if (!Right.HasValue) return; // We're already there. if (!Left.HasValue) Left = GetLeft(relativeTo); if (!Width.HasValue) Width = GetWidth(relativeTo); Right = null; } public void AutoAdjustTop(Size relativeTo) { if (!Top.HasValue) return; // We're already there. if (!Height.HasValue) Height = GetHeight(relativeTo); if (!Bottom.HasValue) Bottom = GetBottomFromBottom(relativeTo); Top = null; } public void AutoAdjustHeight(Size relativeTo) { if (!Height.HasValue) return; // We're already there. if (!Top.HasValue) Top = GetTop(relativeTo); if (!Bottom.HasValue) Bottom = GetBottomFromBottom(relativeTo); Height = null; } public void AutoAdjustBottom(Size relativeTo) { if (!Bottom.HasValue) return; // We're already there. if (!Top.HasValue) Top = GetTop(relativeTo); if (!Height.HasValue) Height = GetHeight(relativeTo); Bottom = null; } public override int GetHashCode() { Hasher hasher = new Hasher(typeof(Child)); hasher.Add(Factory, Top, Height, Bottom, Left, Width, Right); return hasher.GetHashCode(); } public override bool Equals(object obj) { if (ReferenceEquals(this, obj)) return true; Child other = obj as Child; if (null == other) return false; return Top == other.Top && Height == other.Height && Bottom == other.Bottom && Left == other.Left && Width == other.Width && Right == other.Right && Equals(Factory, other.Factory); } } public class Factory : DataNodeControlFactory { public override Control Create() { SimpleDataNodePanel panel = new SimpleDataNodePanel(); foreach (Child child in Children) try { child.Create(panel); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine("Unable to create child in SimpleDataNodePanel: " + ex); } return panel; } public readonly List Children = new List(); public override bool Equals(object obj) { if (!base.Equals(obj)) return false; return Children.SequenceEqual(((Factory)obj).Children); } public override int GetHashCode() { return MakeHashCode(Children.ToArray()); } } } public class DataNodeColumn : DataGridViewColumn { private readonly DataNodeViewer _dataNodeViewer; public DataNodeColumn(DataNodeViewer dataNodeViewer) { _dataNodeViewer = dataNodeViewer; CellTemplate = new DataNodeCell(dataNodeViewer); } public override object Clone() { DataNodeColumn result = (DataNodeColumn)base.Clone(); result.DataNodeViewer = DataNodeViewer; // Seems like the following would have been copied automatically by base.Clone(). // But it's always null here. And that causes exceptions down the line. result.CellTemplate = CellTemplate; return result; } public DataNodeViewer DataNodeViewer { get; private set; } public DataNodeColumn() { // This is called by DataNodeGridColumn.Clone() via reflection; } } public class DataNodeCell : DataGridViewCell { private DataNodeList _dataNodeList; private DataNodeViewer _dataNodeViewer; /* protected override void Dispose(bool disposing) { //System.Diagnostics.Debug.WriteLine("DataNodeCell.Dispose(" + disposing + ") with _dataNodeList " // + ((null == _dataNodeList) ? "" : "not ") + "null, Hash=" + GetHashCode() + '.'); // This method gets called a lot by the garbage collector. disposing is always false. It gets // called more than you'd expect at the beginning, like there are some temporary values that // never had a _dataNodeList assigned to them. // // I've never seen this called because I deleted a row. I'd expect that, but I don't see it. // So I moved the following code to OnDataGridViewChanged(). if (null != _dataNodeList) _dataNodeList.Release(); _dataNodeList = null; base.Dispose(disposing); } */ private static readonly List _deferredRelease = new List(); private static int _deferredReleaseCount = 0; /// /// Normally a cell will release any data nodes it was using as soon as we remove the cell /// from the table. However, sometimes we are rebuilding a lot of things at once. So we /// might delete some rows and columns then add some of them back. Even if we don't add /// exactly the same rows and columns, there might be some overlap. /// /// This suspends any releasing of DataNodes. We will release the DataNodes after someone /// calls ReleaseNow(). /// /// You can call DeferRelease() multiple times in a row. We keep a count. We don't release /// anything until we've had a matching number of calls to ReleaseNow(). /// /// This often reduces flicker on the screen. This also reduces the number of messages /// to the server. /// public static void DeferRelease() { _deferredReleaseCount++; } /// /// This goes with DeferRelease(). /// public static void ReleaseNow() { if (0 == _deferredReleaseCount) throw new Exception(); _deferredReleaseCount--; if (0 == _deferredReleaseCount) { foreach (DataNodeList list in _deferredRelease) list.Release(); _deferredRelease.Clear(); } } private void CleanUp() { if (null != _dataNodeList) if (_deferredReleaseCount > 0) _deferredRelease.Add(_dataNodeList); else _dataNodeList.Release(); _dataNodeList = null; } protected override void OnDataGridViewChanged() { //System.Diagnostics.Debug.WriteLine("OnDataGridViewChanged() with DataGridView " // + ((null == DataGridView) ? "" : "not ") + "null, Hash=" + GetHashCode() + '.'); if (null == DataGridView) CleanUp(); base.OnDataGridViewChanged(); } protected override void Paint(Graphics graphics, Rectangle clipBounds, Rectangle cellBounds, int rowIndex, DataGridViewElementStates cellState, object value, object formattedValue, string errorText, DataGridViewCellStyle cellStyle, DataGridViewAdvancedBorderStyle advancedBorderStyle, DataGridViewPaintParts paintParts) { if ((null == _dataNodeList) || (null == _dataNodeViewer)) base.Paint(graphics, clipBounds, cellBounds, rowIndex, cellState, value, formattedValue, errorText, cellStyle, advancedBorderStyle, paintParts); else if ((cellState & DataGridViewElementStates.Selected) == DataGridViewElementStates.Selected) // This is a good start but it is incomplete. The way we display the selected row is good when we have the focus. // The background should turn gray or something when we lose the focus. _dataNodeViewer.Paint(_dataNodeList, graphics, cellBounds, SystemColors.HighlightText, SystemColors.Highlight, true, cellStyle.Font); else _dataNodeViewer.Paint(_dataNodeList, graphics, cellBounds, cellStyle.ForeColor, cellStyle.BackColor, false, cellStyle.Font); } public override object Clone() { DataNodeCell newCell = (DataNodeCell)base.Clone(); newCell._dataNodeViewer = _dataNodeViewer; return newCell; } public void Initialize() { System.Diagnostics.Debug.Assert((null == _dataNodeList) && (null != _dataNodeViewer)); Dictionary replacements = (Dictionary)OwningRow.Tag; if (null != replacements) { _dataNodeList = _dataNodeViewer.GetData(replacements); _dataNodeList.OnChange += delegate { Invalidate(); }; } } private static int _debugCountNoGrid; private void Invalidate() { if (null == DataGridView) // If this happens a lot, we probably didn't shut the dara down properly. // However, I'm not convinved that we can completely avoid this. Sometimes // data might come from a server right at the same time as we are making // a change. _debugCountNoGrid++; else DataGridView.InvalidateCell(this); } public DataNodeCell(DataNodeViewer dataNodeViewer) { _dataNodeViewer = dataNodeViewer; } public override Type FormattedValueType { get { // For now this is a placeholder so the software doesn't crash. We're not using the formatted value yet. // Presumably we'll need help from the column object to tell us more about the data. Presumably this is // used when copying cells from a table, among other things. return typeof(string); } } public DataNodeCell() { // According to StackExchange, if I don't create this I might get a MissingMethodException } } /// /// You should be able to add DataNode columns to any table. This is, in part, an example for /// those tables. But also we should be able to build this table up from scratch at runtime, /// reading from an XML file or another flexible interface. /// public class DataNodeTable : DataGridView { public static DataGridViewRow RowForSymbol(string symbol) { DataGridViewRow result = new DataGridViewRow(); Dictionary replacements = new Dictionary(); replacements[DataNode.PlaceHolder.SYMBOL.Key] = symbol; result.Tag = replacements; return result; } public DataNodeTable(IList symbols, IList columns) : this() { Columns.AddRange(columns.ToArray()); foreach (String symbol in symbols) Rows.Add(RowForSymbol(symbol)); } public DataNodeTable() { AllowUserToAddRows = false; AllowUserToDeleteRows = false; ColumnHeadersVisible = true; RowHeadersVisible = false; RowsAdded += DataNodeTable_RowsAdded; ColumnAdded += DataNodeTable_ColumnAdded; Disposed += delegate { if (null != _symbolListLink) _symbolListLink.Release(); }; } private DataNode.Link _symbolListLink; private DataNode _symbolListDataNode; private DataNode.Factory _symbolListFactory; public DataNode.Factory SymbolListFactory { get { return _symbolListFactory; } set { DataNode.Factory threadSafeValue; if (null == value) threadSafeValue = value; else threadSafeValue = GuiDataNode.GetFactory(value); if (object.Equals(threadSafeValue, _symbolListFactory)) return; DataNode.Link oldSymbolListLink = _symbolListLink; _symbolListFactory = threadSafeValue; if (null == value) { _symbolListLink = null; _symbolListDataNode = null; } else { _symbolListLink = _symbolListFactory.Find(out _symbolListDataNode, CheckSymbolListDataNode); CheckSymbolListDataNode(); } if (null != oldSymbolListLink) // Delete the old one after installing the new one. It can be more efficint if the // two data nodes shared a lot of internal structure. oldSymbolListLink.Release(); } } private void CheckSymbolListDataNode() { if (null != _symbolListDataNode) SetRows((IEnumerable)_symbolListDataNode.Value); } public void SetRows(IEnumerable symbols) { DataNodeCell.DeferRelease(); try { Rows.Clear(); List newRows = new List(); foreach (object symbol in symbols) newRows.Add(RowForSymbol((string)symbol)); Rows.AddRange(newRows.ToArray()); } finally { DataNodeCell.ReleaseNow(); } } private void DataNodeTable_RowsAdded(object sender, DataGridViewRowsAddedEventArgs e) { for (int i = e.RowIndex, count = e.RowCount; count > 0; i++, count--) { DataGridViewRow row = Rows[i]; Dictionary replacements = (Dictionary)row.Tag; foreach (DataGridViewColumn column in Columns) { ((DataNodeCell)row.Cells[column.Index]).Initialize(); } } } private void DataNodeTable_ColumnAdded(object sender, DataGridViewColumnEventArgs e) { if (e.Column is DataNodeColumn) foreach (DataGridViewRow row in Rows) { ((DataNodeCell)row.Cells[e.Column.Index]).Initialize(); } } } }