I'm trying to implement somesting like an "Lazy" VisualBrush right now. Does anybody have an idea how to to that? Meaning: Something that behaves like an VisualBrush but does not update on every change in the Visual but at max once a second (or whatever).
I better should give some background why I'm doing this and what I alreay tried I guess :)
Problem: My job right now is to improve the performance of an rather big WPF application. I tracked down the main performance issue (at the UI level anyway) to some visual brushes used in the application. The application consists of an "Desktop" area with some rather complex UserControls and an Navigation area containing a scaled down version of the Desktop. The navigation area is using visual brushes to get the job done. Everything is fine as long as the Desktop items are more or less static. But if the elements are changing frequently (because they contain an animation for example) the VisualBrushes go wild. They will update along with the framerate of the animations. Lowering the framerate helps of course, but I'm looking for an more general solution to this problem. While the "source" control only renders the small area affected by the animation the visual brush container is rendered completly causing the application performance to go to hell. I already tried to use BitmapCacheBrush instead. Doesn't help unfortunately. The animation is inside the control. So the brush have to be refreshed anyway.
Possible solution: I created a Control behaving more or less like an VisualBrush. It takes some visual (as the VisualBrush) but is using a DiapatcherTimer and RenderTargetBitmap to do the job. Right now I'm subscribing to the LayoutUpdated event of the control and whenever it changes it will be scheduled for "rendering" (using RenderTargetBitmap). The actual rendering then is triggered by the DispatcherTimer. This way the control will repaint itself at maximum in the frequency of the DispatcherTimer.
Here is the code:
public sealed class VisualCopy : Border
{
#region private fields
private const int mc_mMaxRenderRate = 500;
private static DispatcherTimer ms_mTimer;
private static readonly Queue<VisualCopy> ms_renderingQueue = new Queue<VisualCopy>();
private static readonly object ms_mQueueLock = new object();
private VisualBrush m_brush;
private DrawingVisual m_visual;
private Rect m_rect;
private bool m_isDirty;
private readonly Image m_content = new Image();
#endregion
#region constructor
public VisualCopy()
{
m_content.Stretch = Stretch.Fill;
Child = m_content;
}
#endregion
#region dependency properties
public FrameworkElement Visual
{
get { return (FrameworkElement)GetValue(VisualProperty); }
set { SetValue(VisualProperty, value); }
}
// Using a DependencyProperty as the backing store for Visual. This enables animation, styling, binding, etc...
public static readonly DependencyProperty VisualProperty =
DependencyProperty.Register("Visual", typeof(FrameworkElement), typeof(VisualCopy), new UIPropertyMetadata(null, OnVisualChanged));
#endregion
#region callbacks
private static void OnVisualChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
var copy = obj as VisualCopy;
if (copy != null)
{
var oldElement = args.OldValue as FrameworkElement;
var newelement = args.NewValue as FrameworkElement;
if (oldElement != null)
{
copy.UnhookVisual(oldElement);
}
if (newelement != null)
{
copy.HookupVisual(newelement);
}
}
}
private void OnVisualLayoutUpdated(object sender, EventArgs e)
{
if (!m_isDirty)
{
m_isDirty = true;
EnqueuInPipeline(this);
}
}
private void OnVisualSizeChanged(object sender, SizeChangedEventArgs e)
{
DeleteBuffer();
PrepareBuffer();
}
private static void OnTimer(object sender, EventArgs e)
{
lock (ms_mQueueLock)
{
try
{
if (ms_renderingQueue.Count > 0)
{
var toRender = ms_renderingQueue.Dequeue();
toRender.UpdateBuffer();
toRender.m_isDirty = false;
}
else
{
DestroyTimer();
}
}
catch (Exception ex)
{
}
}
}
#endregion
#region private methods
private void HookupVisual(FrameworkElement visual)
{
visual.LayoutUpdated += OnVisualLayoutUpdated;
visual.SizeChanged += OnVisualSizeChanged;
PrepareBuffer();
}
private void UnhookVisual(FrameworkElement visual)
{
visual.LayoutUpdated -= OnVisualLayoutUpdated;
visual.SizeChanged -= OnVisualSizeChanged;
DeleteBuffer();
}
private static void EnqueuInPipeline(VisualCopy toRender)
{
lock (ms_mQueueLock)
{
ms_renderingQueue.Enqueue(toRender);
if (ms_mTimer == null)
{
CreateTimer();
}
}
}
private static void CreateTimer()
{
if (ms_mTimer != null)
{
DestroyTimer();
}
ms_mTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(mc_mMaxRenderRate) };
ms_mTimer.Tick += OnTimer;
ms_mTimer.Start();
}
private static void DestroyTimer()
{
if (ms_mTimer != null)
{
ms_mTimer.Tick -= OnTimer;
ms_mTimer.Stop();
ms_mTimer = null;
}
}
private RenderTargetBitmap m_targetBitmap;
private void PrepareBuffer()
{
if (Visual.ActualWidth > 0 && Visual.ActualHeight > 0)
{
const double topLeft = 0;
const double topRight = 0;
var width = (int)Visual.ActualWidth;
var height = (int)Visual.ActualHeight;
m_brush = new VisualBrush(Visual);
m_visual = new DrawingVisual();
m_rect = new Rect(topLeft, topRight, width, height);
m_targetBitmap = new RenderTargetBitmap((int)m_rect.Width, (int)m_rect.Height, 96, 96, PixelFormats.Pbgra32);
m_content.Source = m_targetBitmap;
}
}
private void DeleteBuffer()
{
if (m_brush != null)
{
m_brush.Visual = null;
}
m_brush = null;
m_visual = null;
m_targetBitmap = null;
}
private void UpdateBuffer()
{
if (m_brush != null)
{
var dc = m_visual.RenderOpen();
dc.DrawRectangle(m_brush, null, m_rect);
dc.Close();
m_targetBitmap.Render(m_visual);
}
}
#endregion
}
This works pretty good so far. Only problem is the trigger. When I use LayoutUpdated then the Rendering is triggered constantly even if the Visual itself is not changed at all (propably because of animations in other parts of the application or whatever). LayoutUpdated is just fired way to often. As a matter of fact I could just skip the trigger and just update the control using the timer without any trigger. It doesn't matter. I also tried to override OnRender in the Visual and raise an custom event to trigger the update. Doesn't work either because OnRender is not called when something deep inside the VisualTree changes. This is my best shot right now. It's working much better then the original VisualBrush solution already (from the performance point of view at least). But I'm, still looking for an even better solution.
Does anyone have an idea how to a) trigger the update only when nessasarry or b) get the job done with an altogether differen approach?
Thanks!!!
UpdateBuffer
triggers another LayoutUpdated). That alone would not be so bad but there is 2nd: LayoutUpdated doesn't get raised when there is an animation in the visual. So eventually the problem is this one. – Triolein