调度程序不一致.Invoke行为

时间:2015-12-15 13:22:50

标签: c# wpf dispatcher

我为服务台团队创建了一个墙板应用程序,该应用程序使用前端的WPF和后端的电话的思科数据库。该应用程序由两个显示不同信息的屏幕组成,它们显示在同一屏幕中,并通过System.Timers.Timer在彼此之间进行更改。
制作应用程序时,如果显示WindowA,则会显示WindowB,然后隐藏WindowA。当其中一个Windows变为可见时,该窗口的计时器再次激活,恢复数据库调用,而另一个窗口的计时器被禁用:

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);
}

问题在于这种方法完美无缺...最多90%的时间。问题是,有时,如果例如WindowA's可见性更改为可见且WindowB's可见性更改为折叠,则WindowB会崩溃,但WindowA需要2-3秒才能成为可见,大多数时候WindowA变得可见,WindowB崩溃时看不到它。这(当它不起作用时)导致桌面可见而不是应用程序 我最初使用的是DispatcherPriority.Background但是导致换网器工作的时间为70-80%,因此我决定将其更改为DispatcherPriority.NormalDispatcherPriority.Send结果基本上与正常情况相同)。

问题:

  1. 这是Dispatcher预期的正常行为,考虑到这是在四核CPU中以x64模式运行吗?
  2. 知道查询是在等待的异步方法中执行的,Dispatcher不应该优先于这些方法吗?
  3. 是否有其他方法(不使用Dispatcher,或使用其他Window属性)来完成我正在寻找的内容?
  4. 这是用于访问/启动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();
    }
    

2 个答案:

答案 0 :(得分:2)

听起来你正在编写一个自助服务终端应用程序(即全屏,非交互式)。如果是这种情况,我认为你最好只有一个窗口并切换其中的视图,而不是在两个单独的窗口之间切换。此外,您需要将数据库查询工作与刷新窗口内容分开。此外,我认为如果观点对彼此一无所知会有所帮助:目前你的第一个窗口与你的第二个窗口紧密耦合,这不是一个好主意。

在我看来,如果你稍微改变了你的架构,那么你遇到的很多问题就会消失。以下是我的建议:

首先,只需一个窗口即可。创建两个用户控件(项目&gt;添加用户控件),并将XAML布局从现有窗口移动到这两个新控件中。然后让你的主窗口看起来像这样:

<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>

这是一个没有镶边的全屏窗口,其中包含两个用户控件(实际上是现有窗口的内容)。它们分层放在Grid元素中,以便一个位于另一个元素的顶部:我使用Panel.ZIndex属性强制第一个控件到堆顶部。最后,我正在使用一个动画(在窗口加载时触发),切换其中一个控件的可见性,以便在一段时间后隐藏它。动画设置为重复和自动反转,其效果是隐藏其中一个控件,然后再次显示。您可以更改Duration属性值以控制每个控件“保持”可见的时间长度;在这个例子中它被设置为5秒,这意味着开关之间有10秒的延迟。

这项工作的关键是第一个用户控件在可见时必须完全遮盖位于其下方的其他用户控件。通过设置控件的背景颜色很容易实现。

您的用户控件可以包含窗口包含的任何内容。这是我使用的示例用户控件XAML:

<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>

正如您所看到的,它只是一个TextBlock元素,其Text属性绑定到用户控件的代码隐藏中定义的Number属性。我对两个用户控件使用了相同的XAML,只是改变了文本的VerticalAlignment,这样我就可以知道在任何给定时间哪个控件都可见。

代码隐藏看起来像这样(两者都是一样的,但类名除外):

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");
        }
    }
}

它基本上包含一个Number属性(实现INotifyPropertyChanged),该属性通过“worker”方法递增。 worker方法由一个计时器调用:在这里,我使用的是DispatcherTimer,但由于我没有直接更改任何UI元素,所以任何.NET计时器都可以完成。

计划使用Task.Run在线程池上运行worker,然后异步运行。我正在用Task.Delay等待一段时间来模拟长期工作。此worker方法将是从中调用数据库查询的位置。您可以通过设置计时器的Interval属性来改变连续查询之间的差距。没有什么可以说查询之间的差距需要与UI的刷新间隔相同(即两个视图切换的速度);实际上,由于您的查询需要花费不同的时间,因此无论如何同步两者都会很棘手。

答案 1 :(得分:0)

尝试使用Dispatcher.CurrentDispatcher代替window.Dispatcher和BeginInvoke:

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

<强>更新 将您的计时器切换到DispatcherTimer:

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