Reputation: 185
I've got a class that inherits from Panel. It contains a second panel nested inside of it. When controls are added to this control, I actually want them added to the inner panel. The goal here is to draw a special border using the outer panel, but then be able to host controls inside of the inner panel as if it were any other panel.
Here is the basic code I'm using:
public class TestPanel2 : Panel
{
private Panel innerPanel = new Panel();
public TestPanel2()
{
this.Controls.Add(innerPanel);
this.ControlAdded += new ControlEventHandler(TestPanel2_ControlAdded);
}
void TestPanel2_ControlAdded(object sender, ControlEventArgs e)
{
if (e.Control != innerPanel) {
innerPanel.Controls.Add(e.Control);
}
}
}
When using this control in the Designer, dragging a child control (such as a CheckBox) into it results in the designer reporting:
'child' is not a child control of this parent
My theory is that the Designer is calling Controls.SetChildIndex() or Controls.GetChildIndex() for its own purposes, and that is triggering the error. So I tried adding the following property to the class:
public new ControlCollection Controls
{
get { return innerPanel.Controls; }
}
When I did this, I also changed all internal references of this.Controls to base.Controls. However, this didn't resolve the problem.
Is there a way to add a nested panel that automatically receives controls that are dragged into it? If I change the code so that the child controls are only added to innerControl at runtime, it works, but the position of the child controls ends up being wrong, so it isn't much of a solution.
UPDATE:
For whatever it is worth, here is a simplified diagram of what I am trying to do. I'm creating a toolkit that will be used by other developers. It is a specialized panel that contains a custom border and title block. Think of it as being functionally similar to a "GroupBox" control. I want them to be able to drag this specialized panel onto their form, and then add controls to it, all within the Designer. The "innerPanel" needs to be its own panel so that it is the only region scrolled (when scrolling is necessary).
(source: cosmicjive.net)
Upvotes: 2
Views: 5460
Reputation: 3109
This what worked for me. Inspired on Przemyslaw's answer.
The idea is to use async code in case if the control is in design mode.
I am using TableLayoutPanel as control holder.
The user control is decorated with [Designer(typeof(ParentControlDesigner))]
attribute.
private async void Editor_ControlAdded(object sender, ControlEventArgs e)
{
if(this.IsDesignMode())//Extension method to check if the control is in design mode
await Task.Yield();
Content = e.Control;
}
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
public Control Content
{
get => _table.GetControlFromPosition(0, 1);
set
{
var old = Content;
if (old == value)
return;
if (old != null)
_table.Controls.Remove(old);
if(value!=null)
_table.Controls.Add(value, 0, 1);
}
}
And here is IsDesignMode extension method:
public static bool IsDesignMode(this Control control)
{
while (control != null)
{
if (control.Site != null && control.Site.DesignMode)
return true;
control = control.Parent;
}
return false;
}
Upvotes: 0
Reputation: 1
I used PostMessage to tell design time mechanism to call my function AFTER mechanism finished. So this way
[Designer(typeof(ParentControlDesigner))]
public partial class FilterPanel : UserControl
{
const int WM_SET_PARENT_MESSAGE = 0x0400;
[DllImport("user32.dll")]
public static extern int PostMessage(IntPtr hWnd, int wMsg, IntPtr wParam, IntPtr lParam);
public FilterPanel()
{
InitializeComponent();
this.ControlAdded += new ControlEventHandler(FilterPanel_ControlAdded);
}
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public Panel ContentsPanel
{
get { return contentsPanel; }
}
protected override void WndProc(ref Message m)
{
if (m.Msg == WM_SET_PARENT_MESSAGE)
{
// collect dropped controls
// lblCaption && lblBottom && contentsPanel are my UserControl controls - dont change parent
List<Control> list = new List<Control>();
foreach (Control c in this.Controls)
{
if (c != this.lblCaption && c != this.lblBottom && c != this.contentsPanel)
list.Add(c);
}
// move controls to subpanel
foreach (Control c in list)
{
this.Controls.Remove(c);
this.contentsPanel.Controls.Add(c);
c.Parent = this.contentsPanel;
}
}
base.WndProc(ref m);
}
private void FilterPanel_ControlAdded(object sender, ControlEventArgs e)
{
// dont change parent now. do it after mechanism finished
// ofc you can pass e.Control through the message parameters and modify WndProc to use e.Control exactly
PostMessage(this.Handle, WM_SET_PARENT_MESSAGE, (IntPtr)0, (IntPtr)0);
}
}
Upvotes: 0
Reputation: 1155
I know this is several months old, but see this blog entry about how to make your inner control the content control for your user control. It will make your life a lot easier as controls will only be able to be dropped on your inner panel.
How can I drop controls within UserControl at Design time? A couple things to consider: 1. You want your inner panel to be moveable and sizeable when designing the User Control, but not when you have dropped the UserControl on another design surface
A. Here is the designer for the outer panel - My implementation nests it within the outer panel so it can be referenced with a Designer attribute like so which is applied to the outer class
[Designer(typeof(YourUserControl.Designer))]
public partial class YourUserControl : UserControl
#region Designer - Friend class
/// <summary>
/// Exposes the internal panel as content at design time,
/// allowing it to be used as a container for other controls.
///
/// Adapted
/// From: How can I drop controls within UserControl at Design time?
/// Link: http://blogs.msdn.com/b/subhagpo/archive/2005/03/21/399782.aspx
/// </summary>
internal class Designer : ParentControlDesigner
{
public override void Initialize(IComponent component)
{
base.Initialize(component);
var parent = (YourUserControl)component;
EnableDesignMode(parent.Content, "Content");
}
}
#endregion
B. Here is the Content property which needs to be added to the outer panel
#region Content - Used by the designer class
/// <summary>
/// Defines the control which can act as a container at design time.
/// In conjunction with other design time attributes and the designer
/// defined below, allows the user control to act as a container at
/// design time. This means other controls can be sited on the content
/// panel, such as a text boxes or labels, etc.
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public ContentPanel Content
{
get
{
return this.contentPanel1;
}
}
#endregion
C. Here is the inner panel which is used as the content panel. In your case you would need to switch the Dock and Anchor properties in the code below. I wanted it to always dock whereas you will want it to always anchor because of the header and border you reference in your post.
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Linq;
using System.Windows.Forms;
using System.Windows.Forms.Design;
using System.Text;
using System.Threading.Tasks;
namespace j2associates.Tools.Winforms.Controls
{
[Designer(typeof(ContentPanel.Designer))]
public class ContentPanel : Panel
{
private ScrollableControl _parentScrollableControl;
public ContentPanel()
: base()
{
// Dock is always fill.
this.Dock = DockStyle.Fill;
}
protected override void OnParentChanged(EventArgs e)
{
base.OnParentChanged(e);
if (this.Parent != null)
{
Control parent = this.Parent;
while (parent != null && this.Parent.GetType() != typeof(ScrollableControl))
{
parent = parent.Parent;
}
if (parent != null && parent.GetType() == typeof(ScrollableControl))
{
_parentScrollableControl = (ScrollableControl)parent;
// Property value is retrieved from scrollable control panel.
this.AutoScroll = _parentScrollableControl.AutoScroll;
}
}
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
if (_parentScrollableControl != null)
{
this.AutoScroll = _parentScrollableControl.AutoScroll;
}
}
protected override void OnEnter(EventArgs e)
{
base.OnEnter(e);
this.AutoScroll = _parentScrollableControl != null ? _parentScrollableControl.AutoScroll : false;
}
#region Designer - Friend class
/// <summary>
/// Allows us to handle special cases at design time.
/// </summary>
internal class Designer : ParentControlDesigner
{
private IDesignerHost _designerHost = null;
private Control _parent = null;
#region Overrides
#region Initialize
public override void Initialize(IComponent component)
{
base.Initialize(component);
// Used to determine whether the content panel is sited on a form or on a user control.
_designerHost = (IDesignerHost)this.GetService(typeof(IDesignerHost));
_parent = ((ContentPanel)component).Parent;
}
#endregion
#region SelectionRules
public override SelectionRules SelectionRules
{
get
{
SelectionRules selectionRules = base.SelectionRules;
// When hosted on a form, remove all resizing and moving grips at design time
// because the content panel is part of a composed user control and it cannot
// be moved nor can the dock property change.
//
// When not hosted on a form, then it is part of a user control which is being
// composed and it can be moved or the dock property changed.
if (!ReferenceEquals(_designerHost.RootComponent, _parent))
{
selectionRules = SelectionRules.Visible | SelectionRules.Locked;
}
return selectionRules;
}
}
#endregion
#region PreFilterProperties
protected override void PreFilterProperties(System.Collections.IDictionary properties)
{
base.PreFilterProperties(properties);
// The Anchor property is not valid for a ContentPanel so just get rid of it.
properties.Remove("Anchor");
}
#endregion
#region PostFilterProperties
protected override void PostFilterProperties(System.Collections.IDictionary properties)
{
// Hide the Anchor property so it cannot be changed by the developer at design time.
PropertyDescriptor dockDescriptor = (PropertyDescriptor)properties["Dock"];
dockDescriptor = TypeDescriptor.CreateProperty(dockDescriptor.ComponentType, dockDescriptor, new Attribute[] { new BrowsableAttribute(false), new EditorBrowsableAttribute(EditorBrowsableState.Never)} );
properties[dockDescriptor.Name] = dockDescriptor;
// Hide the AutoScroll property so it cannot be changed by the developer at design time
// because it is set from the nearest panel of type scrollable control.
PropertyDescriptor autoScrollDescriptor = (PropertyDescriptor)properties["AutoScroll"];
autoScrollDescriptor = TypeDescriptor.CreateProperty(autoScrollDescriptor.ComponentType, autoScrollDescriptor, new Attribute[] { new ReadOnlyAttribute(true) });
properties[autoScrollDescriptor.Name] = autoScrollDescriptor;
// Make the Name property read only so it cannot be changed by the developer at design time
// because it is set from the nearest panel of type scrollable control.
PropertyDescriptor nameDescriptor = (PropertyDescriptor)properties["Name"];
nameDescriptor = TypeDescriptor.CreateProperty(nameDescriptor.ComponentType, nameDescriptor, new Attribute[] { new ReadOnlyAttribute(true) });
properties[nameDescriptor.Name] = nameDescriptor;
// Always call the base method last.
base.PostFilterProperties(properties);
}
#endregion
#endregion
}
#endregion
}
}
Enjoy...
Upvotes: 1
Reputation: 185
Hans Passant pointed me in the right direction by linking to this discussion:
I also found a sample project that demonstrates almost the exact control I am trying to create:
http://www.codeproject.com/Articles/37830/Designing-Nested-Controls
Here is my revised version of the control:
[Designer(typeof(TestUserControlDesigner))]
public partial class TestPanel3 : UserControl
{
private Panel innerPanel = new Panel();
public TestPanel3()
{
InitializeComponent();
this.Controls.Add(innerPanel);
}
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public Panel ContentPanel
{
get { return innerPanel; }
}
}
internal class TestUserControlDesigner : ParentControlDesigner
{
public override void Initialize(System.ComponentModel.IComponent component)
{
base.Initialize(component);
EnableDesignMode((this.Control as TestPanel3).ContentPanel, "ContentPanel");
}
}
This methodology works, although the "innerPanel" can be "dragged out" of the control in the Designer. But there are other solutions for that problem, and this is otherwise a good solution.
Upvotes: 2
Reputation: 27944
You have to remove the control first and then add it to the innerpanel. A control can't be on two channels at the same time:
void TestPanel2_ControlAdded(object sender, ControlEventArgs e)
{
if (e.Control != innerPanel) {
this.Controls.Remove(e.Control);
innerPanel.Controls.Add(e.Control);
}
}
Upvotes: 0