0

Background: I have an application that collects data, does calculations and presents them to the user in graphs in a window. For each set of data I take a picture of the window and store it as a .png on the harddrive so that the user can go back and check the result later.

Problem: Currently, I update the viewmodel with the new data and then have a Task.Delay(...) as to give the application some time to render the new content on the view. But sometimes I will get a picture of the previous dataset if the delay wasn't enough, I can increase the delay time to make it happen less often but that in turn will slow down the program unneccesarilly. I'm basically looking for a smart way to check if the view have been rendered with the new dataset rather than have a dumb delay.

I've looked into Window.ContentRendered event. But that only seems to fire the first time a window is rendered, so I would have to close and re-create a new window for every picture if I want to use that one and that just feels like unneccesary overhead to me. I would need something similar that fires everytime it is re-rendered, or some other way to know if the view is ready for the picture?

LFH
  • 9
  • 1
  • An alternative could be to [render the controls directly to a bitmap](https://stackoverflow.com/questions/5189139/how-to-render-a-wpf-usercontrol-to-a-bitmap-without-creating-a-window) yourself. It would be more work, but you would have more control. – JonasH Jan 29 '21 at 11:48

1 Answers1

-1

Short Answer: Yes, you can do this by calling your picture-saving method on the Dispatcher thread when it is idle by giving it a priority of DispatcherPriority.ApplicationIdle.

Long Answer: Here's a sample showing this at work. I have here an app that updates a viewmodel's text property when you click a button, but it takes a couple of seconds for it to update the control that is bound to it because the text is huge.

The moment I know the new data is trying to be shown, I issue a Dispatcher command to wait for the UI to be idle before I do something:

Dispatcher.Invoke((Action)(() => { // take your picture here }), DispatcherPriority.ApplicationIdle);

MainWindowViewModel.cs

public class MainWindowViewModel : INotifyPropertyChanged
{
    private string messages;
    private string controlText;
    public MainWindowViewModel Parent { get; private set; }
    public string Messages { get => this.messages; set { this.messages = value; OnPropertyChanged(); } }
    public string ControlText { get => this.controlText; set { this.controlText = value; OnPropertyChanged(); } }

    public void UpdateWithNewData()
    {
        var strBuilder = new StringBuilder();
        for (int i = 0; i < 100000; i++)
        {
            strBuilder.AppendLine($"{DateTime.Now:HH:mm:ss.ffffff}");
        }

        // This will update the TextBox that is bound to this property, 
        // but it will take awhile because the text is HUUUUGE.
        this.ControlText = strBuilder.ToString();
    }

    public MainWindowViewModel()
    {
        this.ControlText = "This area will take a while to render when you click the button below.";
    }

    public event PropertyChangedEventHandler PropertyChanged;
    public void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

MainWindow.xaml

<Window x:Class="_65951670.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid Background="LightSalmon">
            <Grid.RowDefinitions>
                <RowDefinition/>
                <RowDefinition/>
            </Grid.RowDefinitions>
            <TextBox IsReadOnly="True" Text="{Binding ControlText,UpdateSourceTrigger=PropertyChanged}"  TextWrapping="Wrap" Margin="5" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Visible"/>
            <Button Grid.Row="1" HorizontalAlignment="Center" VerticalAlignment="Center" Padding="15,5" Content="Update Above With Lots Of Text" Click="Button_Click"/>
        </Grid>
        <Grid Grid.Row="1">
            <TextBox Text="{Binding Messages}" TextWrapping="Wrap" VerticalScrollBarVisibility="Visible" HorizontalScrollBarVisibility="Disabled" Margin="5" IsReadOnly="True"/>
        </Grid>
    </Grid>
</Window>

MainWindow.xaml.cs

public partial class MainWindow : Window
{
    private MainWindowViewModel viewModel;
    public MainWindow()
    {
        InitializeComponent();
        viewModel = new MainWindowViewModel();
        this.DataContext = viewModel;
        this.viewModel.PropertyChanged += ViewModel_PropertyChanged;
    }

        private void ViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            if (e.PropertyName == nameof(this.viewModel.ControlText))
            {
                var sw = new Stopwatch();
                sw.Start();
                this.viewModel.Messages += $"Property Changed: {DateTime.Now:HH:mm:ss.ffffff}\n";

                // If you got here, you know that the DataContext has changed, but you don't know when it will be done rendering.
                // So use Dispatcher and wait for it to be idle before performing another action.
                // Put your picture-saving method inside of the 'Action' here.
                Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, (Action)(() => 
                { 
                    this.viewModel.Messages += $"UI Became Idle At: {DateTime.Now:HH:mm:ss.ffffff}\nIt took {sw.ElapsedMilliseconds} ms to render, Take Picture Now!"; 
                }));
            }
        }
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        this.viewModel.UpdateWithNewData();
    }
}
Tam Bui
  • 2,940
  • 2
  • 18
  • 27