Camilo Terevinto
Camilo Terevinto

Reputation: 32068

Inconsistent dispatcher.Invoke behavior

I have created a wallboard application for a service desk team, which uses WPF for front-end and the Cisco database of the phones in the back-end. The application is made of two screens that show different information, and these are displayed in the same screen and change between each other with a System.Timers.Timer.
The application is made so that if WindowA is visible, WindowB is shown and then WindowA is hidden. The moment one of the Windows becomes visible, that Window's timer become active again which resumes the database calls, while the other Window's timer becomes disabled:

private static void InterfaceChanger_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
    if (WindowA.Visibility == Visibility.Visible)
    {
        WindowAEnabled = false;
        ChangeVisibility(Visibility.Visible, WindowB);
        WindowBEnabled = true;
        WindowB_Elapsed(null, null); // force the call of the timer's callback
        ChangeVisibility(Visibility.Collapsed, WindowA);
    }
    else
    {
        WindowBEnabled = false;
        ChangeVisibility(Visibility.Visible, WindowA);
        WindowAEnabled = true;
        WindowA_Elapsed(null, null);  // force the call of the timer's callback
        ChangeVisibility(Visibility.Collapsed, WindowB);
    }
}

private static void ChangeVisibility(Visibility visibility, Window window)
{
    window.Dispatcher.Invoke(DispatcherPriority.Normal, (SendOrPostCallback)delegate
    {
        window.Visibility = visibility;
    }, null);
}

The problem is that this works perfectly... at most 90% of the time. The problem is that sometimes, if for example WindowA's visibility is changed to Visible and WindowB's visibility is changed to Collapsed, WindowB collapses but WindowA takes 2-3 seconds to become visible, while most times WindowA becomes visible and it's not seen when WindowB collapses. This (when it doesn't work) results in the Desktop being visible instead of the application.
I originally used DispatcherPriority.Background but that resulted in the screen changer working 70-80% of the time, so I decided to change it for DispatcherPriority.Normal (DispatcherPriority.Sendresults basically in the same situation as Normal).

Questions:

  1. Is this the normal behavior to be expected by the Dispatcher, taking into account this is running in x64 mode in a quad-core CPU?
  2. Knowing that the queries are performed in async methods not awaited, shouldn't the Dispatcher take priority over the methods?
  3. Is there another way (without using the Dispatcher, or using another Window property) to accomplish what I'm looking for?

This is the code used to access/start the Windows:

//WindowA:
<Application x:Class="MyNamespace.App"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         StartupUri="WindowA.xaml">

//WindowA class:
public static WindowA WindowAInstance;
public WindowA()
{
    // unnecessary code hidden
    WindowAInstance = this;
    WindowB b = new WindowB;
}

// WindowB class
public static WindowB WindowBInstance;
public WindowB()
{
    // unnecessary code hidden
    WindowBInstance = this;
}

// this is the code that starts the timers
public static void StartTimersHandling()
{
    Database.RemoveAgents();

    InterfaceChangerTimer = new System.Timers.Timer();
    InterfaceChangerTimer.Interval = ApplicationArguments.InterfaceChangerTime;
    InterfaceChangerTimer.Elapsed += InterfaceChanger_Elapsed;
    InterfaceChangerTimer.AutoReset = true;
    InterfaceChangerTimer.Start();

    WindowATimer = new System.Timers.Timer();
    WindowATimer.Interval = 1000;
    WindowATimer.Elapsed += WindowATimer_Elapsed;
    WindowATimer.AutoReset = true;
    WindowATimer.Start();

    WindowBTimer = new System.Timers.Timer();
    WindowBTimer.Interval = 1000;
    WindowBTimer.Elapsed += WindowBTimer_Elapsed;
    WindowBTimer.AutoReset = true;
    WindowBTimer.Start();
}

Upvotes: 2

Views: 951

Answers (2)

Steven Rands
Steven Rands

Reputation: 5421

It sounds like you're writing a kiosk application (ie. full-screen, non-interactive). If this is the case I think you would be better off having a single window and switching the views inside it, rather than switching between two separate windows. Also, you need to separate the database query work from the refreshing of the window content. Furthermore, I think it would help if the views knew nothing about each other: at the moment your first window is tightly coupled to your second, which is not really a good idea.

In my opinion, if you changed your architecture a little, a lot of the problems you are having would disappear. Here's what I would recommend:

First, just go with a single window. Create two user controls (Project > Add User Control), and move your XAML layout from your existing windows into these two new controls. Then make your main window look something like this:

<Window x:Class="StackOverflow.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:my="clr-namespace:StackOverflow"
        WindowState="Maximized" WindowStyle="None">
    <Grid>
        <my:UserControl1 x:Name="_first" Panel.ZIndex="1" />
        <my:UserControl2 Panel.ZIndex="0" />
    </Grid>
    <Window.Triggers>
        <EventTrigger RoutedEvent="Loaded">
            <BeginStoryboard>
                <Storyboard AutoReverse="True" RepeatBehavior="Forever">
                    <ObjectAnimationUsingKeyFrames BeginTime="0:0:5" Duration="0:0:5"
                        Storyboard.TargetName="_first"
                        Storyboard.TargetProperty="Visibility">
                        <DiscreteObjectKeyFrame KeyTime="0:0:0"
                            Value="{x:Static Visibility.Hidden}" />
                    </ObjectAnimationUsingKeyFrames>
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </Window.Triggers>
</Window>

This is a full-screen window with no chrome that contains your two user controls (essentially the contents of your existing windows). They are layered in a Grid element so that one sits on top of the other: I'm using the Panel.ZIndex property to force the first control to the top of the pile. Finally, I'm using an animation (triggered when the window loads) that toggles the visibility of one of the controls to hide it after a certain period of time. The animation is set to repeat and auto-reverse, the effect of which is to hide one of the controls, then make it visible again. You can change the Duration attribute value to control how long each control "stays" visible; it's set to 5 seconds in this example, which means a 10 second delay between switches.

The key to this working is that the first user control, when visible, must fully obscure the other user control that lies beneath it. This is easy to accomplish by setting the background colour of the control.

Your user controls can contain anything that a window would contain. Here's the example user control XAML that I used:

<UserControl x:Class="StackOverflow.UserControl1"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             Background="White" Padding="40">
    <TextBlock Text="{Binding Number}" FontSize="60"
        TextAlignment="Center" VerticalAlignment="Top" />
</UserControl>

As you can see it's just a TextBlock element whose Text property binds to a Number property defined in the user control's code-behind. I used the same XAML for both user controls, just varying the VerticalAlignment of the text so that I could tell which control was visible at any given time.

The code-behind looks like this (it's the same for both, with the exception of the class name):

using System;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Controls;
using System.Windows.Threading;

namespace StackOverflow
{
    public partial class UserControl1 : UserControl, INotifyPropertyChanged
    {
        public UserControl1()
        {
            InitializeComponent();
            DataContext = this;

            _timer = new DispatcherTimer
                { Interval = TimeSpan.FromSeconds(5), IsEnabled = true };
            _timer.Tick += (sender, e) => Task.Run(async () => await DoWorkAsync());
        }

        readonly DispatcherTimer _timer;
        readonly Random _random = new Random();

        public event PropertyChangedEventHandler PropertyChanged;

        public int Number
        {
            get
            {
                return _number;
            }
            private set
            {
                if (_number != value)
                {
                    _number = value;
                    if (PropertyChanged != null)
                    {
                        PropertyChanged(this, new PropertyChangedEventArgs("Number"));
                    }
                }
            }
        }
        int _number;

        async Task DoWorkAsync()
        {
            // Asynchronous code started on a thread pool thread

            Console.WriteLine(GetType().Name + " starting work");
            _timer.IsEnabled = false;
            try
            {
                await Task.Delay(TimeSpan.FromSeconds(_random.Next(4, 12)));
                Number++;
            }
            finally
            {
                _timer.IsEnabled = true;
            }
            Console.WriteLine(GetType().Name + " finished work");
        }
    }
}

It basically contains a single Number property (which implements INotifyPropertyChanged) that gets incremented by a "worker" method. The worker method is invoked by a timer: here, I'm using a DispatcherTimer, but as I'm not changing any UI elements directly any of the .NET timers would have done.

The worker is scheduled to run on the thread pool using Task.Run, and then runs asynchronously. I'm simulating a long-running job by waiting for a period of time with Task.Delay. This worker method would be where your database query gets called from. You can vary the gap between successive queries by setting the timer's Interval property. There's nothing to say that the gap between queries need be the same as the refresh interval of your UI (ie. the speed at which the two views are switched); indeed, as your query takes a variable amount of time, syncing the two would be tricky anyway.

Upvotes: 2

Vadim Martynov
Vadim Martynov

Reputation: 8892

Try to use Dispatcher.CurrentDispatcher instead of window.Dispatcher and BeginInvoke:

Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.DataBind, new Action(() =>
    {
        window.Visibility = visibility;
    }));

Updated Switch your timer to DispatcherTimer:

timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(5) };
timer.Tick += (sender, args) => InterfaceChanger_Elapsed();
timer.Start();

Upvotes: 0

Related Questions