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.
When I press button1
:
UserControl1
which just contains a FlowDocumentReader
is created and set to be the ElementHost
's Child
FlowDocument
is created from a text file (it just contains a FlowDocument
with a StackPanel
with a few thousand lines of <TextBox/>
)FlowDocumentReader
's Document
property is set to this FlowDocument
At this point, the page renders the FlowDocument
correctly. A lot of memory is used, as expected.
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 sets elementHost1.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.
button1
is clicked mutiple times, memory usage shouldn't keep going upThis 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.
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>
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:
http://i.imgur.com/m28Aiux.png
Any insight appreciated.
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.
I found this blog post here: Memory Leak while using ElementHost when using a WPF User control inside a Windows Forms project
So, try this in your Button2 click event:
if (elementHost1 != null)
{
elementHost1.Child = null;
elementHost1.Dispose();
elementHost1.Parent = null;
elementHost1 = null;
}
I found that calling GC.Collect() after this it may not reduce memory usage instantly, but it doesn´t increase behind a certain point. For better reproducing i made a second form, which opens your Form1
. With this i tried opening your form about 20 times, always clicking on Button1 and then Button2 then closing the form, memory usage stayed constant.
Edit: The strange thing is, the memory seems to be released after opening the form again, not on GC.Collect(). I can´t help but finding this a bug with the ElementHost
control.
Edit2, my Form1
:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
m_uc1 = new UserControl1();
elementHost1.Child = m_uc1;
}
private UserControl1 m_uc1;
private void button1_Click(object sender, EventArgs e)
{
string rawXamlText = File.ReadAllText(@"in.txt");
var flowDocumentStringReader = new StringReader(rawXamlText);
var flowDocumentTextReader = new XmlTextReader(flowDocumentStringReader);
object document = XamlReader.Load(flowDocumentTextReader);
var fd = document as FlowDocument;
m_uc1.docReader.Document = fd;
flowDocumentTextReader.Close();
flowDocumentStringReader.Close();
flowDocumentStringReader.Dispose();
}
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
if (elementHost1 != null)
{
elementHost1.Child = null;
elementHost1.Dispose();
elementHost1.Parent = null;
elementHost1 = null;
}
}
Even without explicit GC.Collect() i don´t experience any memory leak anymore. Remember, i tried this opening this form multiple times from another form.
Indeed, PresentationFramework.dll!System.Windows.Documents.TextEditor
has a finalizer and so it gets stuck on the finalizer queue (together with all the stuff hanging off it) unless disposed of properly. I scrounged around in PresentationFramework.dll
a bit, and unfortunately I don't see how to get the TextBox
es to dispose of their attached TextEditor
s. The only relevant call to TextBox.OnDetach
is in TextBoxBase.InitializeTextContainer()
. There you can see that once a TextBox
creates a TextEditor
, it only disposes of it in exchange for creating a new one. Two other conditions under which the TextEditor
disposes itself is when the application domain unloads or the WPF dispatcher shuts down. The former looks slightly more hopeful, as I found no way to restart a shut-down WPF dispatcher. WPF objects cannot be directly shared across application domains because they do not derive from MarshalByRefObject
, but Windows Forms controls do. Try putting your ElementHost
in a separate application domain and tear it down when clearing your form (you may need to shut down the dispatcher first). Another approach is to use MAF add-ins to put your WPF control into a different application domain; see this SO question.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With