1

I made a subclass for TextBox and tested the following method in a separate test project.

internal static int NumberOfPhysicalLinesInTextBox(TextBox tb)
{
    int lc = 0;
    while (tb.GetFirstCharIndexFromLine(lc) != -1)
    {
        ++lc;
    }
    return lc;
}

The code Works well, but in my subclass it does not. The above static method is called only from the method UpdateVisibleScrollBars, which is called only in the following places:

  • from the subclass' c-tor
  • OnTextChanged
  • OnFontChanged
  • OnResize

The only speciality of this subclass is that it has a placeholder when the user did not enter anything in the TextBox, and this UpdateVisibleScrollBars. In this subclass NumberOfPhysicalLinesInTextBox does not return, it loops indefinitely because the GetFirstCharIndexFromLine always returns 0 when the text is the placeholder: "Enter text here...".

Update: I do not use Lines because I need the physical lines (the lines that result after Word-wrapping), so I can know if I need to show or hide the vertical scrollbar. The TextBox is set with WordWrap = true. Here is the GetFirstCharIndexFromLine method's official documentation.

Update 2: All the class' code is below (without non-English comments):

class EnhancedTextBox : TextBox
{
    internal string PlaceholderText = "Enter text here...";

    internal string ActualText
    {
        get
        {
            return PlaceholderShown ? "" : Text;
        }
        set
        {
            if (value == "" || value == null)
            {
                if (Text == PlaceholderText)
                {
                    PlaceholderShown = true;

                    ActualTextChanged?.Invoke(this, EventArgs.Empty);
                }
                else
                {
                    if (!Focused)
                    {
                        BeforeActualTextChanged?.Invoke(this, EventArgs.Empty);

                        ProgrammaticTextChange = true;
                        Text = PlaceholderText;
                        ProgrammaticTextChange = false;

                        PlaceholderShown = true;

                        ActualTextChanged?.Invoke(this, EventArgs.Empty);
                    }
                    else
                    {
                        PlaceholderShown = false;

                        ActualTextChanged?.Invoke(this, EventArgs.Empty);
                    }
                }
            }
            else
            {
                if (Text != value)
                {
                    BeforeActualTextChanged?.Invoke(this, EventArgs.Empty);

                    ProgrammaticTextChange = true;
                    Text = value;
                    ProgrammaticTextChange = false;
                }

                PlaceholderShown = false;

                ActualTextChanged?.Invoke(this, EventArgs.Empty);
            }
        }
    }

    internal Color _PlaceholderForeColor = Utils.GrayByPercent(50);
    internal Color PlaceholderForeColor
    {
        get
        {
            return _PlaceholderForeColor;
        }
        set
        {
            if (_PlaceholderForeColor != value)
            {
                _PlaceholderForeColor = value;
                Invalidate();
            }
        }
    }

    internal Color _NormalForeColor = Color.Empty;
    internal Color NormalForeColor
    {
        get
        {
            return _NormalForeColor;
        }
        set
        {
            if (_NormalForeColor != value)
            {
                _NormalForeColor = value;
                Invalidate();
            }
        }
    }

    internal bool _PlaceholderShown = true;
    internal bool PlaceholderShown
    {
        get
        {
            return _PlaceholderShown;
        }
        set
        {
            if (_PlaceholderShown != value)
            {
                _PlaceholderShown = value;
                ForceUpdatePlaceholderShown(value);
            }
        }
    }

    internal void ForceUpdatePlaceholderShown(bool value)
    {
        ForeColor = value ? PlaceholderForeColor :
                    NormalForeColor;
        Invalidate();
    }

    public EnhancedTextBox() : base()
    {
        NormalForeColor = ForeColor;

        ProgrammaticTextChange = true;
        Text = PlaceholderText;
        ProgrammaticTextChange = false;

        PlaceholderShown = true;
        ForceUpdatePlaceholderShown(true);

        UpdateVisibleScrollBars();
    }

    protected override void OnEnter(EventArgs e)
    {
        HidePlaceholder();

        base.OnEnter(e);
    }

    private void HidePlaceholder()
    {
        if (PlaceholderShown)
        {
            ProgrammaticTextChange = true;
            Text = "";
            ProgrammaticTextChange = false;

            PlaceholderShown = false;
        }
    }

    protected override void OnLeave(EventArgs e)
    {
        ShowPlaceholder();

        base.OnLeave(e);
    }

    private void ShowPlaceholder()
    {
        if (Text == "")
        {
            ProgrammaticTextChange = true;
            Text = PlaceholderText;
            ProgrammaticTextChange = false;

            PlaceholderShown = true;
        }
    }

    internal static int NumberOfPhysicalLinesInTextBox(TextBox tb)
    {
        int lc = 0;
        while (tb.GetFirstCharIndexFromLine(lc) != -1)
        {
            ++lc;
        }
        return lc;
    }

    internal bool ProgrammaticTextChange = false;

    /// <summary>
    /// Do not use this event using handlers. Use ActualTextChanged instead.
    /// </summary>
    /// <param name="e"></param>
    protected override void OnTextChanged(EventArgs e)
    {
        if (ProgrammaticTextChange)
        {
            return;
        }

        ActualText = Text;

        base.OnTextChanged(e);

        UpdateVisibleScrollBars();
    }

    private bool busy = false;
    private void UpdateVisibleScrollBars()
    {
        if (busy) return;
        busy = true;

        bool c1 = false; // chars == Text.Length; // TODO: this not working for WordWrap = false
        bool c2 = NumberOfPhysicalLinesInTextBox(this) > 2;

        if (c1 && c2)
        {
            ScrollBars = ScrollBars.Both;
        }
        else if (c1)
        {
            ScrollBars = ScrollBars.Horizontal;
        }
        else if (c2)
        {
            ScrollBars = ScrollBars.Vertical;
        }
        else
        {
            ScrollBars = ScrollBars.None;
        }
        ScrollToCaret();

        busy = false;
    }

    protected override void OnFontChanged(EventArgs e)
    {
        base.OnFontChanged(e);

        UpdateVisibleScrollBars();
    }

    protected override void OnResize(EventArgs e)
    {
        base.OnResize(e);

        UpdateVisibleScrollBars();
    }

    public event EventHandler ActualTextChanged, BeforeActualTextChanged;
}

If I replace bool c2 = NumberOfPhysicalLinesInTextBox(this) > 2; with bool c2 = false; there is no non-ending while loop, although I see that the OnResize handler is called often in debugging with a breakpoint put on the c2 line, and repeatedly clicking Continue. Then If I press Continue really fast a few times, the program starts and is usable.

Update 3: Commenting out the UpdateVisibleScrollBars call inside the OnResize handler makes everything work. How can I make the scrollbars' visibility be changed when the TextBox is just resized?

Current code:

class EnhancedTextBox : TextBox
{
    internal string _PlaceholderText = "Enter text here...";
    internal string PlaceholderText
    {
        get
        {
            return _PlaceholderText;
        }
        set
        {
            _PlaceholderText = value;
            Invalidate();
        }
    }

    internal Color _PlaceholderForeColor = SystemColors.GrayText;
    public Color PlaceholderForeColor
    {
        get
        {
            return _PlaceholderForeColor;
        }
        set
        {
            _PlaceholderForeColor = value;
            Invalidate();
        }
    }

    [Obsolete]
    internal string ActualText
    {
        get
        {
            return Text;
        }
        set
        {
            if (Text != value)
            {
                Text = value;
            }
        }
    }

    internal Color _NormalForeColor = Color.Empty;
    internal Color NormalForeColor
    {
        get
        {
            return _NormalForeColor;
        }
        set
        {
            if (_NormalForeColor != value)
            {
                _NormalForeColor = value;
                ForeColor = value;
            }
        }
    }

    public EnhancedTextBox() : base()
    {
        NormalForeColor = ForeColor;

        WordWrap = true;
    }

    protected override void WndProc(ref Message m)
    {
        base.WndProc(ref m);

        if (m.Msg == 0xf)
        {
            if (!this.Focused && string.IsNullOrEmpty(this.Text)
                && !string.IsNullOrEmpty(this.PlaceholderText))
            {
                using (var g = this.CreateGraphics())
                {
                    TextRenderer.DrawText(g, this.PlaceholderText, this.Font,
                        this.ClientRectangle, this.PlaceholderForeColor, this.BackColor,
                         TextFormatFlags.Top | TextFormatFlags.Left);
                }
            }
        }
    }

    internal static int NumberOfPhysicalLinesInTextBox(TextBox tb)
    {
        int lc = 0;
        while (tb.GetFirstCharIndexFromLine(lc) != -1)
        {
            ++lc;
        }
        return lc;
    }

    internal bool ProgrammaticTextChange = false;

    protected override void OnTextChanged(EventArgs e)
    {
        base.OnTextChanged(e);

        ActualTextChanged?.Invoke(this, e);

        UpdateVisibleScrollBars();
    }

    private bool busy = false;
    private Size textSize = Size.Empty;
    private void UpdateVisibleScrollBars()
    {
        if (busy) return;
        busy = true;

        bool c1 = false; // chars == Text.Length; // TODO: this not working for WordWrap = false
        bool c2 = NumberOfPhysicalLinesInTextBox(this) > 2;

        if (c1 && c2)
        {
            ScrollBars = ScrollBars.Both;
        }
        else if (c1)
        {
            ScrollBars = ScrollBars.Horizontal;
        }
        else if (c2)
        {
            ScrollBars = ScrollBars.Vertical;
        }
        else
        {
            ScrollBars = ScrollBars.None;
        }
        ScrollToCaret();

        busy = false;
    }

    protected override void OnFontChanged(EventArgs e)
    {
        base.OnFontChanged(e);

        UpdateVisibleScrollBars();
    }

    protected override void OnResize(EventArgs e)
    {
        base.OnResize(e);

        //UpdateVisibleScrollBars();
    }

    [Obsolete]
    public event EventHandler ActualTextChanged;
}
silviubogan
  • 3,343
  • 3
  • 31
  • 57
  • Why do you need a `while (true)` loop? Can't you use the `Lines[]` array instead? It has a `Length` property... – Jimi Feb 08 '19 at 14:43
  • @Jimi I updated the question. The `while (true)` was useless and I said why I do not use `Lines[]`. – silviubogan Feb 08 '19 at 14:50
  • Then, maybe, you need `GetPositionFromCharIndex`, which returns a `Point`. The char can be the last one (`[TextBoxBase].Text.Length -1`). `Point.Y` can be `> [Control].Height`. – Jimi Feb 08 '19 at 14:57
  • Are you looking for [Watermark for multiline textbox](https://stackoverflow.com/a/36534068/3110834)? – Reza Aghaei Feb 08 '19 at 15:30
  • Can you post all of the code for testing? This may be something like a recursive issue calling the method rather than it being stuck in the while loop. Can you add logging or use a break to make sure it's only being called once and never returning? – Michael Puckett II Feb 08 '19 at 15:51
  • @Jimi I posted another question: https://stackoverflow.com/q/54605400/258462 . – silviubogan Feb 09 '19 at 11:05
  • @MichaelPuckettII I updated the question with code and details. Thank you. – silviubogan Feb 09 '19 at 11:20
  • Using `Text` property for placeholder is definitely wrong. For example if you use databinding, then the placeholder will try to sit in the bound property while it may be invalid or it may lead into unwanted data update. I highly advise you to change the implementation to what I shared in above comment. – Reza Aghaei Feb 09 '19 at 15:16
  • ok. So I think the problem is everytime you call SetScrollbars you force a resize event. Assuming bc Im on my phone and not at pc to verify. This does look like a recursive issue at a glance. – Michael Puckett II Feb 09 '19 at 16:12
  • @MichaelPuckettII I updated my question. – silviubogan Feb 12 '19 at 12:08

0 Answers0