views:

1320

answers:

6

I have a custom user control with a textbox on it and I'd like to expose the baseline (of the text in the textbox) snapline outside of the custom control. I know that you create a designer (inherited from ControlDesigner) and override SnapLines to get access to the snaplines, but I'm wondering how to get the text baseline of a control that I have exposed by my custom user control.

+1  A: 

You're on the right track. You will need to override the SnapLines property in your designr and do something like this:

Public Overrides ReadOnly Property SnapLines() As System.Collections.IList
    Get
        Dim snapLinesList As ArrayList = TryCast(MyBase.SnapLines, ArrayList)

        Dim offset As Integer
        Dim ctrl As MyControl = TryCast(Me.Control, MyControl)
        If ctrl IsNot Nothing AndAlso ctrl.TextBox1 IsNot Nothing Then
            offset = ctrl.TextBox1.Bottom - 5
        End If

        snapLinesList.Add(New SnapLine(SnapLineType.Baseline, offset, SnapLinePriority.Medium))

        Return snapLinesList

    End Get
End Property

In this example the usercontrol contains a textbox. The code adds a new snapline that represents the baseline for the textbox. The important thing is to calculate the offset correctly.

BenR
+10  A: 

I just had a similar need, and I solved it like this:

 public override IList SnapLines
{
    get
    {
        IList snapLines = base.SnapLines;

        MyControl control = Control as MyControl;
        if (control == null) { return snapLines; }

        IDesigner designer = TypeDescriptor.CreateDesigner(
            control.textBoxValue, typeof(IDesigner));
        if (designer == null) { return snapLines; }
        designer.Initialize(control.textBoxValue);

        using (designer)
        {
            ControlDesigner boxDesigner = designer as ControlDesigner;
            if (boxDesigner == null) { return snapLines; }

            foreach (SnapLine line in boxDesigner.SnapLines)
            {
                if (line.SnapLineType == SnapLineType.Baseline)
                {
                    snapLines.Add(new SnapLine(SnapLineType.Baseline,
                        line.Offset + control.textBoxValue.Top,
                        line.Filter, line.Priority));
                    break;
                }
            }
        }

        return snapLines;
    }
}

This way it's actually creating a temporary sub-designer for the subcontrol in order to find out where the "real" baseline snapline is.

This seemed reasonably performant in testing, but if perf becomes a concern (and if the internal textbox doesn't move) then most of this code can be extracted to the Initialize method.

This also assumes that the textbox is a direct child of the UserControl. If there are other layout-affecting controls in the way then the offset calculation becomes a bit more complicated.

Miral
Awesome. That did what we needed it to do. Thanks!
Mike
We have several controls that we needed this to work for so we ended up factoring this into a common designer for controls that we have implement a "ISnapable" interface that we explicitly implement and exposed the controls. This way the logic is still encapsulated in the designer but generalized.
Mike
+10  A: 

As an update to the Miral's answer.. here are a few of the "missing steps", for someone new that's looking how to do this. :) The C# code above is almost 'drop-in' ready, with the exception of changing a few of the values to reference the UserControl that will be modified.

Possible References Needed:
System.Design (@robyaw)

Usings needed:

using System.Windows.Forms.Design;
using System.Windows.Forms.Design.Behavior;
using System.ComponentModel;
using System.ComponentModel.Design;

On your UserControl you need the following Attribute:

[Designer(typeof(MyCustomDesigner))]

Then you need a "designer" class that will have the SnapLines override:

private class MyCustomerDesigner : ControlDesigner {
  public override IList SnapLines {
    get {
     /* Code from above */
    IList snapLines = base.SnapLines;

    // *** This will need to be modified to match your user control
    MyControl control = Control as MyControl;
    if (control == null) { return snapLines; }

    // *** This will need to be modified to match the item in your user control
    // This is the control in your UC that you want SnapLines for the entire UC
    IDesigner designer = TypeDescriptor.CreateDesigner(
        control.textBoxValue, typeof(IDesigner));
    if (designer == null) { return snapLines; }

    // *** This will need to be modified to match the item in your user control
    designer.Initialize(control.textBoxValue);

    using (designer)
    {
        ControlDesigner boxDesigner = designer as ControlDesigner;
        if (boxDesigner == null) { return snapLines; }

        foreach (SnapLine line in boxDesigner.SnapLines)
        {
            if (line.SnapLineType == SnapLineType.Baseline)
            {
                // *** This will need to be modified to match the item in your user control
                snapLines.Add(new SnapLine(SnapLineType.Baseline,
                    line.Offset + control.textBoxValue.Top,
                    line.Filter, line.Priority));
                break;
            }
        }
    }

    return snapLines;
}

    }
  }
}
Matthew M.
A big up vote (wish I could give more) because you bothered to mention, you need to put the ControlDesigner class INSIDE your UserControl AND decorate your UserControl class with the Designer attribute which dictates the ControlDesigner class. All makes sense once you see that, but not before. Cheers.
Stuart Helwig
Stuart - I had the same problem, which is why I added to this question. I'm glad the extra info was able to help you!
Matthew M.
+1  A: 

VB.Net Version: Note: you have to change the txtDescription to the Textbox or another internal control name that you use. and ctlUserControl to your usercontrol name

<Designer(GetType(ctlUserControl.MyCustomDesigner))> _
Partial Public Class ctlUserControl
   '... 
   'Your Usercontrol class specific code
   '... 
    Class MyCustomDesigner
        Inherits ControlDesigner
        Public Overloads Overrides ReadOnly Property SnapLines() As IList
            Get
                ' Code from above 

                Dim lines As IList = MyBase.SnapLines

                ' *** This will need to be modified to match your user control
                Dim control__1 As ctlUserControl = TryCast(Me.Control, ctlUserControl)
                If control__1 Is Nothing Then Return lines

                ' *** This will need to be modified to match the item in your user control
                ' This is the control in your UC that you want SnapLines for the entire UC
                Dim designer As IDesigner = TypeDescriptor.CreateDesigner(control__1.txtDescription, GetType(IDesigner))
                If designer Is Nothing Then
                    Return lines
                End If

                ' *** This will need to be modified to match the item in your user control
                designer.Initialize(control__1.txtDescription)

                Using designer
                    Dim boxDesigner As ControlDesigner = TryCast(designer, ControlDesigner)
                    If boxDesigner Is Nothing Then
                        Return lines
                    End If

                    For Each line As SnapLine In boxDesigner.SnapLines
                        If line.SnapLineType = SnapLineType.Baseline Then
                            ' *** This will need to be modified to match the item in your user control
                            lines.Add(New SnapLine(SnapLineType.Baseline, line.Offset + control__1.txtDescription.Top, line.Filter, line.Priority))
                            Exit For
                        End If
                    Next
                End Using

                Return lines
            End Get
        End Property
    End Class

End Class
Braian Bressan
+1  A: 

Re: @Matthew M.'s answer, the System.Windows.Forms.Design.Behavior namespace can be found in the System.Design assembly.

robyaw
Thanks, I added that to my answer just in case.
Matthew M.
A: 

Thanks to all those for the help. This was a tough one to swallow. The thought having a private sub-class in every UserControl wasn't very palatable.

I came up with this base class to help out..

[Designer(typeof(UserControlSnapLineDesigner))]
public class UserControlBase : UserControl
{
    protected virtual Control SnapLineControl { get { return null; } }

    private class UserControlSnapLineDesigner : ControlDesigner
    {
        public override IList SnapLines
        {
            get
            {
                IList snapLines = base.SnapLines;

                Control targetControl = (this.Control as UserControlBase).SnapLineControl;

                if (targetControl == null)
                    return snapLines;

                using (ControlDesigner controlDesigner = TypeDescriptor.CreateDesigner(targetControl,
                    typeof(IDesigner)) as ControlDesigner)
                {
                    if (controlDesigner == null)
                        return snapLines;

                    controlDesigner.Initialize(targetControl);

                    foreach (SnapLine line in controlDesigner.SnapLines)
                    {
                        if (line.SnapLineType == SnapLineType.Baseline)
                        {
                            snapLines.Add(new SnapLine(SnapLineType.Baseline, line.Offset + targetControl.Top,
                                line.Filter, line.Priority));
                            break;
                        }
                    }
                }
                return snapLines;
            }
        }
    }
}

Next, derive your UserControl from this base:

public partial class MyControl : UserControlBase
{
    protected override Control SnapLineControl
    {
        get
        {
            return txtTextBox;
        }
    }

    ...

}

Thanks again for posting this.

Robert H.