Bug when working TextBox.GetLineText in .NET WPF

    There are a lot of different tools for conducting research on the operation of programs and operating systems. Virtual machines, IDE, smart notebooks, IDA, radare, hex editors, pe editors, and even more than a hundred Sysinternals utilities are all done to facilitate many routine operations. But sometimes there comes a time when you realize that among all this variety you lack a small utility that just does the banal and simple work. You can write scripts on python or Powershell on your knee, but often you can’t look at such crafts without tears and share it with your colleagues.

    Recently, this situation has come to me again. And I decided it was time to just take and write a neat utility. I will tell you about the utility itself in one of the upcoming articles, but I’ll tell you about one of the problems during development.

    The error manifests itself in the following way: if in a WPF application, sticking many lines of text into a standard TextBox control, then calls to the GetLineText () function, starting from a certain index, will return incorrect lines.

    The flaw lies in the fact that although the lines will be from the set text, but located further, in fact, GetLineText () will simply skip some lines. The error manifests itself with a very large number of lines. So I met her - I tried to display 25 megabytes of text in TextBox. Working with the latest lines revealed an unexpected effect.

    Google suggests that the error exists since 2011 and Microsoft is not particularly in a hurry to fix something.


    There are no requirements for the .NET version. Create a standard WPF project and populate the files like this:

    <Windowx:Class="wpf_textbox.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"xmlns:local="clr-namespace:wpf_textbox"mc:Ignorable="d"Title="WTF, WPF?"Height="350"Width="525"><Grid><Grid.RowDefinitions><RowDefinitionHeight="*"/><RowDefinitionHeight="20"/><RowDefinitionHeight="20"/></Grid.RowDefinitions><TextBoxGrid.Row="0"Margin="5"Name="txt"AcceptsReturn="True"AcceptsTab="True" /><ButtonGrid.Row="1"Content="Fire 1!"Click="btn_OnClick" /><ButtonGrid.Row="2"Content="Fire 2!"Click="btn2_OnClick" /></Grid></Window>

    MainWindow.cs (skipping using and namespace)

    publicpartialclassMainWindow : Window
        privatevoidbtn_OnClick(object sender, RoutedEventArgs e)
            var sb = new StringBuilder();
            for (int i = 0; i < 90009; i++)
            txt.Text = sb.ToString();
        privatevoidbtn2_OnClick(object sender, RoutedEventArgs e)
            var sb = new StringBuilder();
            for (var i = 1; i < 7; i++)
                sb.AppendLine("req: " + 150 * i + ", get: " + txt.GetLineText(150 * i).Trim());
            for (var i = 1; i < 7; i++)
                sb.AppendLine("req: " + 15000 * i + ", get: " + txt.GetLineText(15000 * i).Trim());
            txt.Text = sb.ToString();

    The application consists of a TextBox and two buttons. First, click “Fire 1!” (Fill in the TextBox with numbers), then “Fire 2!” (Ask for lines by numbers and output).

    Expected Result:
    req: 150, get: 150
    req: 300, get: 300
    req: 450, get: 450
    req: 600, get: 600
    req: 750, get: 750
    req: 900, get: 900
    req: 15000, get: 15000
    req : 30000, get: 30000
    req: 45000, get: 45000
    req: 60000, get: 60000
    req: 75000, get: 75000
    req: 90000, get: 90000


    It can be seen that for indexes less than 1000 - everything is fine, and for large 15,000 - shifts have begun. And the further, the more.

    Investigate the bug

    We uncover that part of the resharper, which is responsible for viewing the .NET source code and the special class “Extender of capabilities and override of constraints based on Reflection”.

    Extender of capabilities and override of restrictions on the basis of Reflection
        publicstatic T GetFieldValue<T>(thisobject obj, string name)
            var bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
            var field = obj.GetType().GetField(name, bindingFlags);
            if (field == null)
                field = obj.GetType().BaseType.GetField(name, bindingFlags);
            return (T)field?.GetValue(obj);
        publicstaticobjectInvokeMethod(thisobject obj, string methodName, paramsobject[] methodParams)
            var methodParamTypes = methodParams?.Select(p => p.GetType()).ToArray() ?? new Type[] { };
            var bindingFlags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static;
            MethodInfo method = null;
            var type = obj.GetType();
            while (method == null && type != null)
                method = type.GetMethod(methodName, bindingFlags, Type.DefaultBinder, methodParamTypes, null);
                var intfs = type.GetInterfaces();
                if (method != null)
                foreach (var intf in intfs)
                    method = intf.GetMethod(methodName, bindingFlags, Type.DefaultBinder, methodParamTypes, null);
                    if (method != null)
                type = type.BaseType;
            return method?.Invoke(obj, methodParams);

    It is established experimentally that in a given example, the problem begins in the area of ​​8510 lines. If you request txt.GetLineText (8510) , then “8510” will return. For 8511 - 8511, and for 8512 - suddenly, 8513.

    We look at the implementation of GetLineText () for TextBox: We

    skip the checks in the first lines and see the call to GetStartPositionOfLine () . It seems that the problem should be in this function, because for the wrong line the wrong position of the beginning of the line should return.

    Call in your code:

    var o00 = txt.InvokeMethod("GetStartPositionOfLine", 8510);
    var o01 = txt.InvokeMethod("GetStartPositionOfLine", 8511);
    var o02 = txt.InvokeMethod("GetStartPositionOfLine", 8512);

    And the truth is that the offset of the first object (the beginning of the 8510th line) is indicated as 49950 characters, for the second object - 49956, and the third - 49968. Between the first two 6 characters, and between the next 12. Disorder - this is the missing line.

    Let's go inside GetStartPositionOfLine () :

    Again, skip the start checks and look at the real actions. First, the point is calculated, which should fall on the line number lineIndex . The height of all lines is taken and a half of the line height is added - in order to get to its center. We do not look at this.VerticalOffset and this.HorizontalOffset - they are by zeros.

    We consider in our code:

    var lineHeight = (double) txt.InvokeMethod("GetLineHeight", null);
    var y0 = lineHeight * (double)8510 + lineHeight / 2.0 - txt.VerticalOffset;
    var y1 = lineHeight * (double)8511 + lineHeight / 2.0 - txt.VerticalOffset;
    var y2 = lineHeight * (double)8512 + lineHeight / 2.0 - txt.VerticalOffset;

    Values ​​are reasonable, correlated with logic, everything is in order. Go ahead with the GetStartPositionOfLine () code - we are interested in the next meaningful line (the first one inside the condition), which is like a crocodile and ends with a call to GetTextPositionFromPoint () .

    We reveal the challenges and pull them through reflection. Note that some interfaces are not available to us due to the restriction of visibility, so you have to refer to them using the same Reflection.

    var renderScope = (txt.GetFieldValue<FrameworkElement>("_renderScope") as IServiceProvider);
    // 7 - тип интерфейся ITextViewvar textView = renderScope.GetService(renderScope.GetType().GetInterfaces()[7]);
    var o10 = textView.InvokeMethod("GetTextPositionFromPoint", new Point(-txt.HorizontalOffset, y0), true);
    var o11 = textView.InvokeMethod("GetTextPositionFromPoint", new Point(-txt.HorizontalOffset, y1), true);
    var o12 = textView.InvokeMethod("GetTextPositionFromPoint", new Point(-txt.HorizontalOffset, y2), true);

    The resulting objects show all the same offsets - 49950, 49956, 49568. Going deeper into the implementation of GetTextPositionFromPoint () inside TextBoxView.

    In, GetLineIndexFromPoint () looks promising. Call in your code.

    var o20 = textView.InvokeMethod("GetLineIndexFromPoint", new Point(-txt.HorizontalOffset, y0), true);
    var o21 = textView.InvokeMethod("GetLineIndexFromPoint", new Point(-txt.HorizontalOffset, y1), true);
    var o22 = textView.InvokeMethod("GetLineIndexFromPoint", new Point(-txt.HorizontalOffset, y2), true);

    We get 8510, 8511 and 8513 - bingo! To implementation:

    Even with the naked eye it is clear that this is a binary search. _lineMetrics - a list of characteristics of the lines (beginning, length, width of the border). I happily rub my hands - I thought that, as is often the case, somewhere we forgot to stick a +1 or set > instead of > = . Copy the function into the code and debug it. Because of the closeness of the _lineMetrics types, we pull out through the reflections, we already got _lineHeight earlier. Total:

    var lm = textView.GetFieldValue<object>("_lineMetrics");
    var c = (int)lm.InvokeMethod("get_Count");
    var lineMetrics = new List<Tuple<int,int,int,double>>();
    for (var i = 0; i < c; i++)
        var arr_o = lm.InvokeMethod("get_Item", i);
        var contLength = arr_o.GetFieldValue<int>("_contentLength");
        var length = arr_o.GetFieldValue<int>("_length");
        var offset = arr_o.GetFieldValue<int>("_offset");
        var width = arr_o.GetFieldValue<double>("_width");
        lineMetrics.Add(new Tuple<int, int, int, double>(contLength, length, offset, width));
    var o30 = GetLineIndexFromPoint(lineMetrics, lineHeight, new Point(-txt.HorizontalOffset, y0), true);
    var o31 = GetLineIndexFromPoint(lineMetrics, lineHeight, new Point(-txt.HorizontalOffset, y1), true);
    var o32 = GetLineIndexFromPoint(lineMetrics, lineHeight, new Point(-txt.HorizontalOffset, y2), true);
    /*<...>*/privateintGetLineIndexFromPoint(List<Tuple<int, int, int, double>> lm, double _lineHeight, Point point, bool snapToText)
        if (point.Y < 0.0)
            return !snapToText ? -1 : 0;
        if (point.Y >= _lineHeight * (double)lm.Count)
            if (!snapToText)
            return lm.Count - 1;
        int index = -1;
        int num1 = 0;
        int num2 = lm.Count;
        while (num1 < num2)
            index = num1 + (num2 - num1) / 2;
            var lineMetric = lm[index];
                double num3 = _lineHeight * (double)index;
            if (point.Y < num3)
                num2 = index;
            elseif (point.Y >= num3 + _lineHeight)
                num1 = index + 1;
                if (!snapToText && (point.X < 0.0 || point.X >= lineMetric.Item4))
                    index = -1;
        if (num1 >= num2)
        return index;

    We do not get to debugging. o30, o31, and o32 are 8510, 8511, and 8512, respectively. Such as they should be! But o20, o21 and o22 do not agree with them. How so? We almost did not change the code. Nearly? And here comes the insight.

    var lh = textView.GetFieldValue<double>("_lineHeight");

    Here it is the reason - the difference is 0.0009375 . And if we estimate the accumulation of the error - we multiply by 8511, then we get 7.9790625. This is just about half of the lineHeight, and therefore, when calculating the coordinates, the point flies out beyond the bounds of the desired line and falls on the next one. The same variable (meaning) was calculated in two different ways and, suddenly, did not match.

    On this I decided to stop. It is possible to really get to the bottom of why the height of the column turned out to be different, but I do not see much point. It is doubtful that Microsoft will fix this, so we are looking at crutches for workarounds. Reflection crutch - set the correct _lineHeigheither in one or in another place. It sounds dumb, probably slowly and most likely unreliable. Or you can maintain your own set of lines, parallel to TextBox and take lines from it, the benefit of getting the line number for the cursor position works correctly.


    From novice programmers, you can often hear something about errors in the compiler or standard components. In reality, they are not as common, but none of them are insured. Do not be afraid to look inside the tool that you need - it is fascinating and interesting.

    Write a good code!

    Other blog articles

    Machine Training in Offensive Security
    No car will replace me. Muhaha-ha. I hope.

    Where to insert a quotation mark in IPv6
    Guys know where and what to shove in order to make it good. After these words, they will replace me with a robot, for sure. Wed! UHF!

    Also popular now: