2

I have two custom WPF controls (with custom rendering logic) placed on top of each other (transparent background and stuff). At some point i want to re-draw those controls. My first idea was to call:

foreach(var control in Controls)
{
    control.InvalidateVisual();
}

But that did not quite work. I mean it does force the rendering of my controls, but visual update does not happen simultaneously. The controls update one by one, which does not look nice and can lead to confusion (in cases where one control displays an updated visual, while the other still displays an old one). The documentation on InvalidateVisual method is really poor, but i guess it is an async method, which is why i am having this issue.

So the question is: is there a way to synchronize rendering of multiple controls? Apart from creating one mega control containing all the rendering logic (i would like to avoid that).

SharpGobi
  • 144
  • 1
  • 13
Nikita B
  • 3,303
  • 1
  • 23
  • 41

3 Answers3

2

You generally shouldn't use InvalidateVisual().

To update a control which is staying the same size, you can create a DrawingGroup and put that into the visual-tree during OnRender() -- then you can update the drawing group anytime you like using DrawingGroup.Open(), and WPF will update the UI.

For your case, this would look something like:

class Control1 : UIElement {
    DrawingGroup backingStore = new DrawingGroup();

    protected override void OnRender(DrawingContext drawingContext) {      
        base.OnRender(drawingContext);            

        Render(); // put content into our backingStore
        drawingContext.DrawDrawing(backingStore);
    }
    private void Render() {            
        var drawingContext = backingStore.Open();
        Render(drawingContext);
        drawingContext.Close();            
    }
    private void Render(DrawingContext) {
        // put your existing drawing commands here.
    }

}

class Control2 : UIElement {
    DrawingGroup backingStore = new DrawingGroup();

    protected override void OnRender(DrawingContext drawingContext) {      
        base.OnRender(drawingContext);            

        Render(); // put content into our backingStore
        drawingContext.DrawDrawing(backingStore);
    }
    private void Render() {            
        var drawingContext = backingStore.Open();
        Render(drawingContext);
        drawingContext.Close();            
    }
    private void Render(DrawingContext) {
        // put your existing drawing commands here.
    }

}


class SomeOtherClass {
    public void SomeOtherMethod() {
         control1.Render();
         control2.Render();
    }

}    
David Jeske
  • 2,306
  • 24
  • 29
  • Thanks, that's an interesting approach. Do you know by any chance, whether it is safe to call `backingStore.Open()` on non-UI thread? – Nikita B Jun 11 '17 at 15:40
  • I doubt it, but you could create a drawing in another thread, and add that drawing to the DrawingGroup in the main thread – David Jeske Jun 12 '17 at 06:21
  • Thanks. I am looking for ways to off-load some of the heavy rendering to background thread. Currently I only use background thread to construct and freeze geometry objects, which are then rendered on UI which still takes a considerable time. Possibility to create a drawing on non-UI thread looks promising, I'll check it out. – Nikita B Jun 13 '17 at 07:39
1

You need to suspend refreshing control until you finish invalidating all of them one way is like this :

class DrawingControl
{
    [DllImport("user32.dll")]
    public static extern int SendMessage(IntPtr hWnd, Int32 wMsg, bool wParam, Int32 lParam);

    private const int WM_SETREDRAW = 11; 

    public static void SuspendDrawing( Control parent )
    {
        SendMessage(parent.Handle, WM_SETREDRAW, false, 0);
    }

    public static void ResumeDrawing( Control parent )
    {
        SendMessage(parent.Handle, WM_SETREDRAW, true, 0);
        parent.Refresh();
    }
}

see here for other ways : How do I suspend painting for a control and its children?

Nikita B
  • 3,303
  • 1
  • 23
  • 41
Shachaf.Gortler
  • 5,655
  • 14
  • 43
  • 71
0

Another approach is to use data-binding to trigger the rendering if control has some Data property that is bound to viewmodel.

public static readonly DependencyProperty DataProperty 
    = DependencyProperty.Register("Data", typeof(DataType), typeof(MyView), 
       new FrameworkPropertyMetadata(default(DataType), 
           FrameworkPropertyMetadataOptions.AffectsRender));
public DataType Data
{
    get { return (DataType)GetValue(DataProperty); }
    set { SetValue(DataProperty, value); }
}

Note the AffectsRender flag. Then you can re-draw all controls simultaneously by simultaneously updating bound properties:

foreach(var viewModel in ViewModels)
{
    viewModel.Data = ...;
}
Nikita B
  • 3,303
  • 1
  • 23
  • 41