[Updated, see bottom!]
There is a memory leak in our WinForms application hosting a WPF FlowDocumentReader
in an ElementHost
. I have recreated this issue in a simple project and added the code below.
What the application does
When I press button1
:
- A
UserControl1
which just contains aFlowDocumentReader
is created and set to be theElementHost
'sChild
- A
FlowDocument
is created from a text file (it just contains aFlowDocument
with aStackPanel
with a few thousand lines of<TextBox/>
) - The
FlowDocumentReader
'sDocument
property is set to thisFlowDocument
At this point, the page renders the FlowDocument
correctly. A lot of memory is used, as expected.
The problem
If
button1
is clicked again, memory usage increases, and keeps increasing every time the process repeats! The GC isn't collecting despite loads of new memory being used! There are no references which shouldn't be there, because:If I press
button2
which setselementHost1.Child
to null and invokes the GC (see the code below), another weird thing happens - it will not clean up the memory, but if I keep clicking it for a few seconds, it will eventually free it!
It is unacceptable for us that all this memory stays used. Also, removing the ElementHost
from the Controls
collection, Disposing
it, setting the reference to null, and then invoking the GC does not free up the memory.
What I want
- if
button1
is clicked mutiple times, memory usage shouldn't keep going up - I should be able to free all the memory (this is just one window in the "real" application, and I want to do this when it is closed)
This is not something where the memory usage doesn't matter and I can just let the GC collect it whenever. It actually ends up noticeably slowing down the machine.
The code
If you would rather just download the VS project, I've uploaded it here: http://speedy.sh/8T5P2/WindowsFormsApplication7.zip
Otherwise, here is the relevant code. Just add 2 buttons to the form in the designer and hook them up to the events. Form1.cs:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Windows.Documents;
using System.IO;
using System.Xml;
using System.Windows.Markup;
using System.Windows.Forms.Integration;
namespace WindowsFormsApplication7
{
public partial class Form1 : Form
{
private ElementHost elementHost;
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
string rawXamlText = File.ReadAllText("in.txt");
using (var flowDocumentStringReader = new StringReader(rawXamlText))
using (var flowDocumentTextReader = new XmlTextReader(flowDocumentStringReader))
{
if (elementHost != null)
{
Controls.Remove(elementHost);
elementHost.Child = null;
elementHost.Dispose();
}
var uc1 = new UserControl1();
object document = XamlReader.Load(flowDocumentTextReader);
var fd = document as FlowDocument;
uc1.docReader.Document = fd;
elementHost = new ElementHost();
elementHost.Dock = DockStyle.Fill;
elementHost.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
Controls.Add(elementHost);
elementHost.Child = uc1;
}
}
private void button2_Click(object sender, EventArgs e)
{
if (elementHost != null)
elementHost.Child = null;
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
}
}
}
UserControl1.xaml
<UserControl x:Class="WindowsFormsApplication7.UserControl1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<FlowDocumentReader x:Name="docReader"></FlowDocumentReader>
</UserControl>
Edit:
I finally have time to deal with this again. What I tried is instead of reusing the ElementHost
, disposing and recreating it each time the button is pressed. While this does help a bit, in the sense that the memory is going up and down when you spam click button1 instead of just going up, it still doesn't solve the problem - the memory is going up overall and it is not freed when the form is closed. So now I am putting a bounty up.
As there seems to have been some confusion about what is wrong here, here are the exact steps to reproduce the leak:
1) open task manager
2) click the "START" button to open the form
3) spam a dozen or two clicks on the "GO" button and watch the memory usage - now you should notice the leak
4a) close the form - the memory won't be released.
or
4b) spam the "CLEAN" button a few times, the memory will be released, indicating that this is not a reference leak, it is a GC/finalization problem
What I need to do is prevent the leak at step 3) and free the memory at step 4a). The "CLEAN" button isn't there in the actual application, it is just here to show that there are no hidden references.
I used the CLR profiler to check the memory profile after hitting the "GO" button a few times (memory usage was ~350 MB at this point). It turns out, there were 16125 (5x the amount in the document) Controls.TextBox
and 16125 Controls.TextBoxView
both rooted in 16125 Documents.TextEditor
objects that are rooted in the finalization queue - see here:
https://i.stack.imgur.com/9RBmV.png
Any insight appreciated.
Another update - Solved (kind of)
I just ran into this again in a different, pure WPF application which does not use an ElementHost
or a FlowDocument
, so in retrospect, the title is misleading. As explained by Anton Tykhyy, this is simply a bug with the WPF TextBox
itself, it does not correctly dispose of its TextEditor
.
I did not like the workarounds Anton suggested, but his explanation of the bug was useful for my rather ugly, but short solution.
When I am about to destroy an instance of a control that contains TextBoxes
, I do this (in the code-behind of the control):
var textBoxes = FindVisualChildren<TextBox>(this).ToList();
foreach (var textBox in textBoxes)
{
var type = textBox.GetType();
object textEditor = textBox.GetType().GetProperty("TextEditor", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(textBox, null);
var onDetach = textEditor.GetType().GetMethod("OnDetach", BindingFlags.NonPublic | BindingFlags.Instance);
onDetach.Invoke(textEditor, null);
}
Where FindVisualChildren
is:
public static IEnumerable<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject
{
if (depObj != null)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
if (child != null && child is T)
{
yield return (T)child;
}
foreach (T childOfChild in FindVisualChildren<T>(child))
{
yield return childOfChild;
}
}
}
}
Basically, I do what the TextBox
should be doing. In the end I also call GC.Collect()
(not strictly necessary but helps free the memory faster). This is a very ugly solution but it seems to solve the problem. No more TextEditors
stuck in the finalization queue.