Set Width of Child Group

I’m having trouble setting the width of a child group for a custom node, as shown below:

The node is arranged using a vertical GoListGroup containing 3 nested GoListGroups (header, content and footer). The header contains a horizontal GoListGroup containing 3 nested objects (icon, title, handle).

The “content” and “footer” groups contain a single GoText.

I want the background of the “content” group to be white, with a thin horizontal line separating it from the “header” and “footer” groups. Sounds simple, but is proving to be quite ornery!

As you can see, the width of the middle group is whatever the width of its child text is. I have tried everything I can think of. Obviously, I’m missing something.

One thing I tried was overriding ComputeBounds for the middle group and setting it to the width of the enclosing node. This actually made it worse, since the width of the node at that point does not include the child GoText object.

Here is the code for the content GoListGroup object.

<pre style=": white; color: black; font-family: Consolas;">            <span style="color: blue;">public</span> <span style="color: blue;">class</span> <span style="color: rgb43, 145, 175;">Content</span> : <span style="color: rgb43, 145, 175;">GoListGroup</span>
            {
                <span style="color: rgb43, 145, 175;">ComponentNode</span> Node { <span style="color: blue;">get</span>; <span style="color: blue;">set</span>; }
 
                <span style="color: blue;">public</span> Content(<span style="color: rgb43, 145, 175;">NodeBody</span> body, <span style="color: rgb43, 145, 175;">ComponentNode</span> node)
                {
                    Node = node;
                    Selectable = <span style="color: blue;">false</span>;
                    DragsNode = <span style="color: blue;">true</span>;
                    AutoRescales = <span style="color: blue;">false</span>;
                    ResizesRealtime = <span style="color: blue;">true</span>;
                    <span style="color: blue;">this</span>.Brush = <span style="color: rgb43, 145, 175;">Brushes</span>.White;
                    Add(<span style="color: blue;">new</span> <span style="color: rgb43, 145, 175;">StaticLabel</span>(<span style="color: rgb163, 21, 21;">"Content"</span>, node));
                }
            }</pre>

Questions:

  1. How do I ensure that the middle group with the white background is always drawn the full width of the node?
  2. How do I draw the thin horizontal line above and below the middle group?

Update: I tried a couple of approaches, but both of them failed with different results. First, I created an override of the PaintDecoration method as follows:

public override void PaintDecoration(Graphics g, GoView view)
{
    base.PaintDecoration(g, view);
    Rectangle rect = new RectangleF(Location, new SizeF(Parent.Bounds.Width, Height));
    g.FillRectangle(Brushes.White, rect);
    rect.Inflate(0, -1);
    g.DrawLine(Pens.Black, rect.Left,rect.Top,rect.Right,rect.Top);
    g.DrawLine(Pens.Black, rect.Left,rect.Bottom, rect.Right, rect.Bottom);
}

This seemed to work at first, but there are problems with the painting when dragging links across the surface of the node because the PaintDecoaration method is not called while the mouse is being pressed.

Next I tried reversing the strategy, leaving the background white and only painting the header. Now the same problem appears, but in the header. In other words, the header is not wide enough, leaving the right side of the node white as shown below.

Look at CollapsingRecordNode in NodeLinkDemo and see if that’s doing what you want.

The only thing I could determine from the CollapsingRecordNode example is that it “primes” the sizes of the contained items by recursively initializing the item widths to 160. It does this in the Initialize method of the embedded CollapsingRecordNodeItemList, which is called from the view after everything is constructed.

I tried the same approach, explicitly seeding the item widths of my embedded GoListGroup objects, but it did not have any effect. As you can see from the screenshot below, the widths of the header, content and collapsible sections are all set to the maximum width of their children and not to the width of their parent.


The other difference here is that I need to inherit the port functionality provided by the GoBoxNode, whereas the CollapsingRecordNode inherits directly from GoNode. Although I checked whether inheriting from GoNode would make a difference, but it did not - same behavior.

Here is the code for the class node. The ComponentNodeTitle and ComponentNodeLabel are simple GoText derivatives.

public class ComponentClassNode : GoBoxNode, IGoCollapsible { static Font _titleFont = new Font("Segoe UI", 9f, FontStyle.Regular); static Font _labelFont = new Font("Segoe UI Light", 8.5f, FontStyle.Regular); static Font _subLabelFont = new Font("Segoe UI Light", 8f, FontStyle.Italic);</p><p> public ComponentClassNode(string title, string description) { this.Selectable = false; this.Resizable = false; this.PortBorderMargin = new SizeF(1, 1); this.LinkPointsSpread = false; Title = title; Description = description; Icon = Resources.Widget32; Body = new NodeBody(this); }</p><p> public string Title { get; set; } public string Description { get; set; } public Image Icon { get; set; } public Pen Foreground { get { return Pens.White; } } public Brush Background { get { return Brushes.Orange; } } public Font LabelFont { get { return _labelFont; } } public Font TitleFont { get { return _titleFont; } } public Font SubLabelFont { get { return _subLabelFont; } } public override string ToolTipText { get { return Description; } }</p><p> /// <summary> /// Describes the component fields. /// </summary> List<FieldDescriptor> _fields; public List<FieldDescriptor> Fields { get { if (_fields == null) { _fields = new List<FieldDescriptor> { new FieldDescriptor {Title="Title", FieldType="String"}, new FieldDescriptor{Title="Description",FieldType="String"} }; } return _fields; } }</p><p> /// <summary> /// Describes the component permissions. /// </summary> List<PermissionDescriptor> _permissions; public List<PermissionDescriptor> Permissions { get { if (_permissions == null) { _permissions = new List<PermissionDescriptor> { new PermissionDescriptor{ Title="First Role", Permissions="View,Read"}, new PermissionDescriptor{ Title="Designers", Permissions="View,Read,Design"} }; } return _permissions; } }</p><p> /// <summary> /// Defines the node body. /// </summary> public class NodeBody : GoListGroup { public HeaderPanel Header { get; set; } public ContentPanel Content { get; set; } private ComponentNodePanel InnerPanel;</p><p> /// <summary> /// Base class for all the panels. /// </summary> public class ComponentNodePanel : GoListGroup { protected ComponentClassNode Node { get; set; } public ComponentNodePanel(ComponentClassNode node) { Node = node; Selectable = false; DragsNode = true; AutoRescales = false; ResizesRealtime = true; TopLeftMargin = new SizeF(0, 0); BottomRightMargin = new SizeF(0, 0); }</p><p> internal void SetAllItemWidth(float w) { foreach (GoObject obj in this) SetItemWidth(obj, w); }</p><p> public void SetItemWidth(GoObject obj, float w) { ComponentNodePanel panel = obj as ComponentNodePanel; if (panel != null) panel.SetAllItemWidth(w); else { if (obj.Resizable) obj.Width = w; } } }</p><p> /// <summary> /// Creates a panel with an icon, label and handle. /// </summary> public class HeaderPanel : ComponentNodePanel { GoImage _icon; NodeBody _body; ComponentNodeTitle _label; ComponentClassNodeHandle _handle;</p><p> public HeaderPanel(NodeBody body, ComponentClassNode node) : base(node) { _body = body;</p><p> Brush = node.Background; Orientation = Orientation.Horizontal;</p><p> Add(_icon = new GoImage { Image = node.Icon, Selectable = false, Resizable = false, AutoRescales = false }); Add(_label = new ComponentNodeTitle(node.Title, node)); Add(_handle = new ComponentClassNodeHandle(node));</p><p> // notice when the label is changed, so we can layout the children _label.AddObserver(this); }</p><p> // trap changes to the label so we can resize the node. protected override void OnObservedChanged(GoObject observed, int subhint, int oldI, object oldVal, RectangleF oldRect, int newI, object newVal, RectangleF newRect) { if (subhint == GoText.ChangedText && observed == _label) { SizeF minSize = new SizeF(_body.Width - _handle.Width - _icon.Width, _label.Height); _label.ComputeResize(_label.Bounds, _label.Location, MiddleRight, minSize, minSize, false); LayoutChildren(null); } }</p><p> public override void LayoutChildren(GoObject childchanged) { if (this.Initializing) return; base.LayoutChildren(childchanged); if (_label != null) { // move the label down from the top _label.Position = new PointF(_label.Left, _label.Top + (32 - _label.Height) / 2); } } }</p><p> /// <summary> /// Creates a panel with collapsible sections. /// </summary> public class ContentPanel : ComponentNodePanel { public ContentPanel(NodeBody body, ComponentClassNode node) : base(node) { Brush = Brushes.White; Add(new ComponentNodeCollapsingPanel("Fields", node, node.Fields)); Add(new ComponentNodeCollapsingPanel("Permissions", node, node.Permissions)); } }</p><p> /// <summary> /// Displays a section header with a collapsible handle. /// </summary> public class ComponentNodeSectionHeader : ComponentNodePanel { ComponentNodeLabel _label; GoCollapsibleHandle _handle;</p><p> public ComponentNodeSectionHeader(string title, ComponentClassNode node) : base(node) { Orientation = Orientation.Horizontal; Add(_handle = new ComponentNodeSectionHandle(node)); Add(_label = new ComponentNodeLabel(title, node)); }</p><p> public override void LayoutChildren(GoObject childchanged) { if (this.Initializing) return; base.LayoutChildren(childchanged); if (_label != null) _label.Position = new PointF(_label.Left + 4, _label.Top); } }</p><p> /// <summary> /// A generic collapsing panel. /// </summary> public class ComponentNodeCollapsingPanel : ComponentNodePanel, IGoCollapsible { GoListGroup _content; public ComponentNodeCollapsingPanel(string title, ComponentClassNode node, IList items) : base(node) { BorderPen = Pens.Orange; Add(new ComponentNodeSectionHeader(title, node)); _content = new ComponentNodePanel(node); foreach (var item in items) _content.Add(new ComponentNodeSubLabel(item.ToString(), node)); Add(_content); Collapse(); }</p><p> // IGoCollapsible public bool Collapsible { get { return true; } set { } } public void Collapse() { _content.Visible = false; _content.Printable = false; LayoutChildren(null); } public void Expand() { _content.Visible = true; _content.Printable = true; LayoutChildren(null); } public bool IsExpanded { get { return _content.Visible; } } }</p><p> /// <summary> /// ctor /// </summary> public NodeBody(ComponentClassNode node) { Resizable = false; TopLeftMargin = new SizeF(2, 2); BottomRightMargin = new SizeF(2, 2); Brush = node.Background; BorderPen = Pens.Orange;</p><p> Add(InnerPanel = new ComponentNodePanel(node) { Selectable = false, Brush = Brushes.White, Alignment = MiddleLeft });</p><p> InnerPanel.Add(Header = new HeaderPanel(this, node)); InnerPanel.Add(Content = new ContentPanel(this, node)); }</p><p> // IGoCollapsible public bool Collapsible { get { return true; } set { } } public void Expand() { Content.Visible = true; InnerPanel.LayoutChildren(null); } public void Collapse() { Content.Visible = false; InnerPanel.LayoutChildren(null); } public bool IsExpanded { get { return Content.Visible; } } }</p><p> // IGoCollapsible public bool Collapsible { get { return true; } set { } } public void Collapse() { ((NodeBody)Body).Collapse(); } public void Expand() { ((NodeBody)Body).Expand(); } public bool IsExpanded { get { return ((NodeBody)Body).IsExpanded; } } }

Look at how CollapsingRecordNode initializes the GoText objects.

Update:
I have studied the CollapsingRecordNode example extensively. There is one major difference between that example and this scenario.

The CollapsingRecordNode example assumes that all of the child items are of a fixed width. This allows it to simply traverse the tree of nodes setting the widths of every text item that it finds.

Here, the logic is reversed. We need to adjust the width of each container to the maximum width of any node it contains. While this appears to work for images and shapes, it does not appear to work for embedded GoTextNode or GoText objects.

Thus, we can easily create a vertical or horizontal GoListGroup that contains other types of nodes, and the default LayoutChildren/ComputeBounds mechanism works as expected. But if text nodes are contained within the group, then the size of the text is apparently ignored. This fact is borne out by the observation that for a GoGroup containing a single GoText object, the value returned by ComputeBounds is always 10.

If I set the width of the text objects to an arbitrary width (say, for instance, 160 Wink), then the mechanism seemed to work, even though the width was arbitrary and did not produce the desired results.

Following this theory, I adjusted the derived GoText constructor using the following approach:

public class ComponentNodeTitle : GoText {
   public ComponentNodeTitle(string text, Font font) {
      Selectable=false; AutoRescales=false; AutoResizes=false;
      Font=font; DragsNode=true; Text=text; Editable=true;
      <strong>Width=Graphics.FromImage(Resources.Widget32).MeasureString(Text,Font).Width;</strong>
      }}

The results were mixed, as shown below. Although the default width of 10 was no longer returned from ComputeBounds, the actual width is still off.

I feel I am close, but it really shouldn’t be this hard.

Well, it easier with GoXam and GoJS to do this kind of node design… we get a little smarter each time we start from scratch with a new product.

I don’t really know if that Width calculation you’re doing there is going to match what we compute. I think you’d be better off letting GoDiagram compute the Width and then adjusting the size of the node around that in LayoutChildren.

Thanks, Jake. It is clear that I’m doing something wrong, since I know it shouldn’t be this hard to do what I’m trying to do. It seems that the LayoutChildren approach must be the right way to go, and not try to set the width explicitly as I’m doing.

Although the CollapsibleRecordNode approach is not what I need, as explained earlier, I’ll take a closer look at how I can use LayoutChildren after the node is initialized. Initial results show that the label width is actually being computed at some point before LayoutChildren is called, so it should be possible to simply place the label where I want it to go.