I am trying to implement somesting as a "Lazy" VisualBrush right now. Does anyone have an idea how to do this? Meaning: something that behaves like a VisualBrush, but not updated every time it changes in Visual, but at max once per second (or something else).
I better give some prerequisites why I am doing this, and that I, as I try, I think :)
Problem: Now my job is to improve the performance of a fairly large WPF application. I tracked the main performance issues (at the user interface level) in any case with some of the visual brushes used in the application. The application consists of a “Desktop” area with some rather complicated UserControls and a navigation area containing a smaller version of the desktop. In the navigation area, visual brushes are used to complete the task. Everything is fine if the desktop elements are more or less static. But if elements change frequently (because they contain animation, for example), VisualBrushes goes into the wild. They will be updated along with the frame rate of the animation. Of course, lowering the frame rate helps, but I'm looking for a more general solution to this problem. Although the “source” control only displays a small area subject to animation, the visual container of the brush is fully rendered, causing the application's performance to go to hell. I already tried using BitmapCacheBrush. Unfortunately, it doesn’t help. The animation is inside the control. So the brush needs to be updated anyway.
Possible solution: I created a control more or less similar to VisualBrush. This requires some visual (like VisualBrush), but uses DiapatcherTimer and RenderTargetBitmap to do the job. Right now, I am subscribing to the LayoutUpdated event of the control, and whenever it changes, it will be assigned to "render" (using RenderTargetBitmap). The actual rendering is then triggered by DispatcherTimer. Thus, the control will redraw as much as possible at the DispatcherTimer frequency.
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 }
It works very well so far. The only problem is the trigger. When I use LayoutUpdated, rendering starts constantly, even if the visual itself does not change at all (possibly due to animation in other parts of the application or something else). LayoutUpdated just starts often. In fact, I could just skip the trigger and just update the control with a timer without any trigger. It does not matter. I also tried to override OnRender in Visual and create a custom event to trigger the update. It does not work either because OnRender is not called when something deep inside VisualTree changes. This is my best shot right now. It works much better than the original VisualBrush solution already (at least in terms of performance). But I'm still looking for an even better solution.
Does anyone have an idea how to a) initiate an update only when nessasarry or b) perform a completely differentiated approach?
Thanks!!!