0

So, I have a RichTextBox which can contain one or more InlineUIContainer containing a small UserControl. When one of them is deleted, a StackOverflowException is directly thrown (none of my code run between the Backspace press and the exception).

The exception is raised in internal code, I managed to recover the StackTrace thanks to WinDbg and here is the important part of the result :

...
00bce210 51048111 System.Windows.Markup.Primitives.MarkupWriter.RecordNamespaces(Scope, System.Windows.Markup.Primitives.MarkupObject, System.Windows.Markup.IValueSerializerContext, Boolean)
00bce25c 51048111 System.Windows.Markup.Primitives.MarkupWriter.RecordNamespaces(Scope, System.Windows.Markup.Primitives.MarkupObject, System.Windows.Markup.IValueSerializerContext, Boolean)
00bce2a8 51048111 System.Windows.Markup.Primitives.MarkupWriter.RecordNamespaces(Scope, System.Windows.Markup.Primitives.MarkupObject, System.Windows.Markup.IValueSerializerContext, Boolean)
00bce2f4 51048111 System.Windows.Markup.Primitives.MarkupWriter.RecordNamespaces(Scope, System.Windows.Markup.Primitives.MarkupObject, System.Windows.Markup.IValueSerializerContext, Boolean)
00bce340 51048111 System.Windows.Markup.Primitives.MarkupWriter.RecordNamespaces(Scope, System.Windows.Markup.Primitives.MarkupObject, System.Windows.Markup.IValueSerializerContext, Boolean)
00bce38c 51048111 System.Windows.Markup.Primitives.MarkupWriter.RecordNamespaces(Scope, System.Windows.Markup.Primitives.MarkupObject, System.Windows.Markup.IValueSerializerContext, Boolean)
00bce3d8 5104829a System.Windows.Markup.Primitives.MarkupWriter.WriteItem(System.Windows.Markup.Primitives.MarkupObject)
00bce3f0 51047ebd System.Windows.Markup.Primitives.MarkupWriter.SaveAsXml(System.Xml.XmlWriter, System.Windows.Markup.Primitives.MarkupObject)
00bce41c 51047e15 System.Windows.Markup.Primitives.MarkupWriter.SaveAsXml(System.Xml.XmlWriter, System.Object)
00bce428 51024246 System.Windows.Markup.XamlWriter.Save(System.Object, System.IO.TextWriter)
00bce43c 510241c6 System.Windows.Markup.XamlWriter.Save(System.Object)
00bce46c 51132783 System.Windows.Documents.TextTreeDeleteContentUndoUnit.CopyObjectNode(System.Windows.Documents.TextTreeObjectNode, ContentContainer ByRef)
00bce484 511325c8 System.Windows.Documents.TextTreeDeleteContentUndoUnit.CopyContent(System.Windows.Documents.TextTreeNode, System.Windows.Documents.TextTreeNode)
00bce4ac 5113290e System.Windows.Documents.TextTreeDeleteContentUndoUnit.CopyElementNode(System.Windows.Documents.TextTreeTextElementNode, ContentContainer ByRef)
00bce4dc 51132632 System.Windows.Documents.TextTreeDeleteContentUndoUnit.CopyContent(System.Windows.Documents.TextTreeNode, System.Windows.Documents.TextTreeNode)
00bce504 511323a0 System.Windows.Documents.TextTreeDeleteContentUndoUnit..ctor(System.Windows.Documents.TextContainer, System.Windows.Documents.TextPointer, System.Windows.Documents.TextPointer)
00bce52c 51406de0 System.Windows.Documents.TextTreeUndo.CreateDeleteContentUndoUnit(System.Windows.Documents.TextContainer, System.Windows.Documents.TextPointer, System.Windows.Documents.TextPointer)
00bce548 50729cc7 System.Windows.Documents.TextContainer.DeleteContentInternal(System.Windows.Documents.TextPointer, System.Windows.Documents.TextPointer)
00bce57c 50728c51 System.Windows.Documents.TextRangeEdit.DeleteContentBetweenPositions(System.Windows.Documents.TextPointer, System.Windows.Documents.TextPointer)
00bce590 50728bdc System.Windows.Documents.TextRangeEdit.DeleteEquiScopedContent(System.Windows.Documents.TextPointer, System.Windows.Documents.TextPointer)
00bce5c0 50728ae7 System.Windows.Documents.TextRangeEdit.DeleteParagraphContent(System.Windows.Documents.ITextPointer, System.Windows.Documents.ITextPointer)
00bce5dc 50fe4172 System.Windows.Documents.TextRangeEditTables.DeleteContent(System.Windows.Documents.TextPointer, System.Windows.Documents.TextPointer)
00bce620 50fa7fa1 System.Windows.Documents.TextPointer.System.Windows.Documents.ITextPointer.DeleteContentToPosition(System.Windows.Documents.ITextPointer)
00bce644 5103afb6 System.Windows.Documents.TextRangeBase.SetText(System.Windows.Documents.ITextRange, System.String)
00bce68c 50fdf012 System.Windows.Documents.TextSelection.System.Windows.Documents.ITextRange.set_Text(System.String)
00bce6b0 50fdb214 System.Windows.Documents.TextEditorTyping.OnBackspace(System.Object, System.Windows.Input.ExecutedRoutedEventArgs)

And then it's 13000 more calls to MarkupWriter.RecordNamespaces until the exception. I did some research and when the InlineUIContainer is deleted, the framework tries to serialize it as XAML in order to be able to perform an Undo command. But in this case, the serialization is looping on the MarkupWriter.RecordNamespaces call.

Because it's internal code, I don't know how to prevent this loop. What comes to my mind is to completly prevent the serialization of this InlineUIContainer, but I haven't found a way to do so.

Is there one ?

EDIT :

As requested, here is the definition of the RichTextBox and the UserControl.

CurveFormulaTextBox.xaml

<UserControl x:Class="StruCAT.Views.CurveFormulaTextBox"
             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:prop="clr-namespace:StruCAT.Properties"
             xmlns:viewModels="clr-namespace:StruCAT.ViewModels"
             x:Name="ThisCurveFormulaTextBox"
             d:DataContext="{x:Type viewModels:ParameterViewModel}"
             DataContextChanged="UserControl_DataContextChanged"
             mc:Ignorable="d">

    <DockPanel>
        <RichTextBox x:Name="RichTextBox"
                     MinWidth="250"
                     Padding="1"
                     VerticalContentAlignment="Center"
                     IsDocumentEnabled="True"
                     LostFocus="RichTextBox_LostFocus"
                     PreviewKeyDown="RichTextBox_PreviewKeyDown"
                     SelectionChanged="RichTextBox_SelectionChanged">
            <RichTextBox.Document>
                <FlowDocument LineHeight="24" PageWidth="10000">
                    <Paragraph x:Name="Paragraph" Padding="0,0,0,0" />
                </FlowDocument>
            </RichTextBox.Document>
        </RichTextBox>
        <Popup x:Name="InsertionPopup"
               AllowsTransparency="True"
               IsOpen="{Binding IsFocused, ElementName=RichTextBox, Mode=OneWay}"
               PlacementTarget="{Binding ElementName=RichTextBox}">
            <StackPanel>
                <StackPanel.Effect>
                    <DropShadowEffect BlurRadius="2"
                                      Opacity="0.5"
                                      ShadowDepth="0" />
                </StackPanel.Effect>
                <Polygon HorizontalAlignment="Center"
                         Fill="White"
                         Points="4,0 0,8, 8,8" />
                <Border Margin="1,0,1,1"
                        Padding="1"
                        Background="White"
                        BorderThickness="0"
                        CornerRadius="3">
                    <StackPanel Orientation="Horizontal">
                        <Button Margin="0"
                                Padding="1,1,0,0"
                                Content="{StaticResource CurveIcon}"
                                PreviewMouseLeftButtonDown="InsertCurve"
                                Style="{StaticResource CustomButton}"
                                ToolTip="{x:Static prop:Resources.AddCurve}" />
                        <Button Margin="0"
                                Content="{StaticResource VariableIcon}"
                                PreviewMouseLeftButtonDown="InsertVariable"
                                Style="{StaticResource CustomButton}"
                                ToolTip="{x:Static prop:Resources.AddVariable}" />
                    </StackPanel>
                </Border>
            </StackPanel>
        </Popup>
    </DockPanel>
</UserControl>

CurveFormulaTextBox.xaml.cs

using StruCAT.ViewModels;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;

namespace StruCAT.Views
{
    /// <summary>
    /// Logique d'interaction pour CurveFormulaTextBox.xaml
    /// </summary>
    public partial class CurveFormulaTextBox : UserControl
    {
        public CurveFormulaTextBox()
        {
            InitializeComponent();
        }

        public bool Working { get; set; } = false;

        public string Formula
        {
            get { return (string)GetValue(FormulaProperty); }
            set { SetValue(FormulaProperty, value); }
        }
        public static readonly DependencyProperty FormulaProperty = DependencyProperty.Register("Formula", typeof(string), typeof(CurveFormulaTextBox), new PropertyMetadata("", FormulaChanged));

        private static void FormulaChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            var cftb = (CurveFormulaTextBox)sender;
            if (e.NewValue != null && !cftb.Working)
                cftb.FillParagraph();
        }

        private void UpdateFormula()
        {
            Working = true;
            var formula = "";
            foreach (var inline in Paragraph.Inlines)
            {
                if (inline.GetType() == typeof(Run))
                    formula += ((Run)inline).Text;
                else if (inline.GetType() == typeof(InlineUIContainer) && ((InlineUIContainer)inline).Child.GetType() == typeof(KeywordComboBox))
                {
                    var kw = ((KeywordComboBox)((InlineUIContainer)inline).Child).SelectedKeyword;
                    if (kw != null)
                        formula += "{" + kw.Model.Type + ";" + kw.Model.ID + "}";
                }
            }
            Formula = formula;
            Working = false;
        }

        public void FillParagraph()
        {
            Working = true;
            foreach (var match in Regex.Matches(Formula, "{.*?}|[^{}]+"))
            {
                if (match.ToString().StartsWith("{"))
                {
                    var targetType = match.ToString().Replace("{", "").Replace("}", "").Trim().Split(';')[0];
                    var keywordID = match.ToString().Replace("{", "").Replace("}", "").Trim().Split(';')[1];

                    var cbo = CreateKCB(targetType);
                    cbo.SelectedKeyword = (DataContext as ParameterViewModel).Parent.Parent.Parent.Keywords.FirstOrDefault(x => x.Model.ID == keywordID);
                    Paragraph.Inlines.Add(cbo);
                }
                else
                {
                    Paragraph.Inlines.Add(new Run(match.ToString()));
                }
            }
            Working = false;
        }

        private KeywordComboBox CreateKCB(string type)
        {
            var kcb = new KeywordComboBox() { Type = type, DataContext = this.DataContext };
            kcb.SelectionChanged += () => UpdateFormula();
            return kcb;
        }

        private void UserControl_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e) { 
            FillParagraph(); 
        }

        private void RichTextBox_LostFocus(object sender, RoutedEventArgs e) 
        { 
            UpdateFormula(); 
        }

        private void RichTextBox_PreviewKeyDown(object sender, KeyEventArgs e) 
        { 
            if (e.Key == Key.Enter) e.Handled = true; 
        }

        private void RichTextBox_SelectionChanged(object sender, RoutedEventArgs e)
        {
            Rect positionRect = RichTextBox.CaretPosition.GetCharacterRect(LogicalDirection.Backward);
            InsertionPopup.HorizontalOffset = positionRect.Left - ((FrameworkElement)InsertionPopup.Child).ActualWidth / 2.0;
            UpdateFormula();
        }

        private void InsertCurve(object sender, MouseButtonEventArgs e)
        {
            new InlineUIContainer(CreateKCB("Curve"), RichTextBox.CaretPosition);
        }

        private void InsertVariable(object sender, MouseButtonEventArgs e)
        {
            new InlineUIContainer(CreateKCB("Float"), RichTextBox.CaretPosition);
        }


    }
}

KeywordComboBox.xaml

<UserControl x:Class="StruCAT.Views.KeywordComboBox"
             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:viewModels="clr-namespace:StruCAT.ViewModels"
             x:Name="ThisKeywordCombobox"
             Margin="1,1,1,-6"
             d:DataContext="{x:Type viewModels:ParameterViewModel}"
             mc:Ignorable="d">

    <DockPanel Height="22">
        <Border Background="WhiteSmoke"
                BorderBrush="Silver"
                BorderThickness="1,1,0,1"
                CornerRadius="2,0,0,2"
                UseLayoutRounding="True">
            <ContentPresenter DockPanel.Dock="Left">
                <ContentPresenter.Style>
                    <Style TargetType="{x:Type ContentPresenter}">
                        <Setter Property="Content" Value="{StaticResource VariableIcon}" />
                        <Style.Triggers>
                            <DataTrigger Binding="{Binding Type, ElementName=ThisKeywordCombobox}" Value="Curve">
                                <Setter Property="Content" Value="{StaticResource CurveIcon}" />
                            </DataTrigger>
                        </Style.Triggers>
                    </Style>
                </ContentPresenter.Style>
            </ContentPresenter>
        </Border>
        <ComboBox Padding="3,3,0,3"
                  DisplayMemberPath="Name"
                  IsEnabled="{Binding Path=ItemsSource, RelativeSource={RelativeSource Self}, Converter={StaticResource EmptyToFalseConverter}}"
                  IsSynchronizedWithCurrentItem="True"
                  SelectedItem="{Binding SelectedKeyword, ElementName=ThisKeywordCombobox, UpdateSourceTrigger=PropertyChanged}"
                  SelectionChanged="ComboBox_SelectionChanged">
            <ComboBox.ItemsSource>
                <MultiBinding Converter="{StaticResource KeywordsFilter}">
                    <Binding Path="Parent.Parent.Parent.Keywords" />
                    <Binding ElementName="ThisKeywordCombobox" Path="Type" />
                    <Binding Path="Parent.Parent.KeywordViewModel" />
                    <Binding Path="Parent.Keywords" />
                    <!--  USED ONLY TO TRIGGER UPDATE  -->
                    <Binding Path="Parent.Parent.Parent.Keywords.Count" />
                    <Binding Path="Parent.Keywords.Count" />
                </MultiBinding>
            </ComboBox.ItemsSource>
        </ComboBox>
    </DockPanel>
</UserControl>

KeywordComboBox.xaml.cs

using StruCAT.ViewModels;
using System.Windows;
using System.Windows.Controls;

namespace StruCAT.Views
{
    /// <summary>
    /// Logique d'interaction pour KeywordComboBox.xaml
    /// </summary>
    public partial class KeywordComboBox : UserControl
    {
        public KeywordComboBox()
        {
            InitializeComponent();
        }

        public KeywordViewModel SelectedKeyword
        {
            get { return (KeywordViewModel)GetValue(SelectedKeywordProperty); }
            set { SetValue(SelectedKeywordProperty, value); }
        }
        public static readonly DependencyProperty SelectedKeywordProperty = DependencyProperty.Register("SelectedKeyword", typeof(KeywordViewModel), typeof(KeywordComboBox), new PropertyMetadata(null));

        public string Type
        {
            get { return (string)GetValue(TypeProperty); }
            set { SetValue(TypeProperty, value); }
        }
        public static readonly DependencyProperty TypeProperty = DependencyProperty.Register("Type", typeof(string), typeof(KeywordComboBox), new PropertyMetadata("Float"));

        private void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            SelectionChanged();
        }

        public delegate void SelectionChange();
        public event SelectionChange SelectionChanged;

    }
}

A first solution provided in a comment which has disappeared since (?) is to completly prevent the Undo command by setting UndoLimit of the RichTextBox to 0. It effectively prevents the exception on deletion of an InlineUIContainer. If there is no other solution I'll use this one.

  • You can see the source for MarkupWriter [here](http://www.dotnetframework.org/default.aspx/DotNET/DotNET/8@0/untmp/WIN_WINDOWS/lh_tools_devdiv_wpf/Windows/wcp/Framework/System/Windows/Markup/Primitives/MarkupWriter@cs/2/MarkupWriter@cs) if that can give any clues – GazTheDestroyer Nov 10 '20 at 15:18
  • 1
    Could you show the contents of the RichTextBox and the definition of the UserControl that reproduces this problem? – Klaus Gütter Nov 10 '20 at 15:59
  • Post edited to add elements definition. – Simon Dubois Nov 12 '20 at 13:27

1 Answers1

0

Old question, but just to give a background to what is going on. Here is the method RecordNamespaces, found here (see below). When you delete an InlineUIContainer from a richtextbox, the object is serialized to xaml so it can restore it when the user presses undo (Ctrl + Z). In order to serialize the object, it loops through all the properties of the object. The issue here, is the RecordNamespaces recurses, which isn't an issue for simple objects, but when you have a circular reference, such as a child has a reference to its parent and vice-versa, the recursion will never stop. In your case, it appears your ParameterViewModel contains a Parent property which leads me to believe you have a circular reference in the code.

private bool RecordNamespaces(Scope scope, MarkupObject item, IValueSerializerContext context, bool
            lastWasString)
{
    // I removed some lines to show just the main part
    // Note how it recurses on the last line I showed
    foreach (MarkupProperty property in item.Properties)
    {
        if (property.IsComposite)
        {
            bool isCollection = IsCollectionType(property.PropertyType);
            foreach (MarkupObject subItem in property.Items)
                lastWasString = RecordNamespaces(scope, subItem, context, lastWasString || isCollection);
        }
    }

Now in order to solve this issue, you have 2 options:

  1. Set the UndoLimit of richtextbox to 0 as you have already mentioned.
  2. When using an InlineUIContainer, make sure its contents do not contain any circular references (in your case setting the DataContext to ParameterViewModel is causing this problem)
  • On a side note, the contents of InlineUIContainer are required to be serializable, meaning you cannot have generic properties, require a default parameterless constructor, etc., so there are a lot of restrictions.
Jesse Good
  • 50,901
  • 14
  • 124
  • 166