不一致的 dispatcher.Invoke 行为
Inconsistent dispatcher.Invoke behavior
我为服务台团队创建了一个墙板应用程序,它在前端使用 WPF,在后端使用 Cisco 电话数据库。该应用程序由显示不同信息的两个屏幕组成,这些屏幕显示在同一屏幕中,并以 System.Timers.Timer
.
在彼此之间切换
制作应用程序,以便如果 WindowA
可见,则显示 WindowB
,然后隐藏 WindowA
。 Windows 中的一个变得可见时,Window 的计时器再次激活,恢复数据库调用,而另一个 Window 的计时器被禁用:
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
可见性更改为 Visible,WindowB's
可见性更改为 Collapsed,WindowB
崩溃但 WindowA
需要 2-3 秒才能变为可见,而大多数时候 WindowA
变得可见,而当 WindowB
崩溃时则看不到。这(当它不起作用时)导致桌面而不是应用程序可见。
我最初使用 DispatcherPriority.Background
但这导致换网器在 70-80% 的时间内工作,所以我决定将其更改为 DispatcherPriority.Normal
(DispatcherPriority.Send
结果基本上与普通情况相同).
问题:
- 考虑到这是 运行 在四核 CPU 的 x64 模式下,这是 Dispatcher 预期的正常行为吗?
- 知道查询是在未等待的异步方法中执行的,Dispatcher 不应该优先于这些方法吗?
- 是否有其他方法(不使用 Dispatcher,或使用另一个 Window 属性)来完成我正在寻找的东西?
这是用于 access/start 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();
}
尝试使用 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();
听起来您正在编写自助服务终端应用程序(即全屏、非交互式)。如果是这种情况,我认为您最好拥有一个 window 并在其中切换视图,而不是在两个单独的 windows 之间切换。另外,您需要将数据库查询工作与 window 内容的刷新分开。此外,我认为如果视图彼此一无所知,那将会有所帮助:目前您的第一个 window 与您的第二个紧密耦合,这并不是一个好主意。
在我看来,如果你稍微改变一下你的架构,你遇到的很多问题都会消失。以下是我的建议:
首先,只需要一个 window。创建两个 用户控件 (项目 > 添加用户控件),并将 XAML 布局从现有的 windows 移动到这两个新控件中。然后让你的主要 window 看起来像这样:
<Window x:Class="Whosebug.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:my="clr-namespace:Whosebug"
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>
这是一个没有 chrome 的全屏 window,其中包含您的两个用户控件(基本上是您现有 windows 的内容)。它们在 Grid
元素中分层,以便一个位于另一个之上:我正在使用 Panel.ZIndex
属性 将第一个控件强制到堆的顶部。最后,我使用了一个动画(在 window 加载时触发)切换其中一个控件的可见性以在一段时间后将其隐藏。动画设置为重复和自动反转,其效果是隐藏其中一个控件,然后使其再次可见。您可以更改 Duration
属性值来控制每个控件 "stays" 可见多长时间;在此示例中设置为 5 秒,这意味着切换之间有 10 秒的延迟。
这项工作的关键是第一个用户控件在可见时必须完全遮住位于其下方的另一个用户控件。这很容易通过设置控件的背景颜色来实现。
您的用户控件可以包含 window 可以包含的任何内容。这是我使用的示例用户控件 XAML:
<UserControl x:Class="Whosebug.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
,这样我就可以知道在任何给定时间哪个控件是可见的。
代码隐藏看起来像这样(两者都一样,除了 class 名称):
using System;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Controls;
using System.Windows.Threading;
namespace Whosebug
{
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 计时器都可以完成。
worker 使用 Task.Run
调度到线程池 运行,然后异步 运行s。我正在通过 Task.Delay
等待一段时间来模拟一个长时间 运行ning 的工作。这个工作者方法将是你的数据库查询被调用的地方。您可以通过设置计时器的 Interval
属性 来改变连续查询之间的间隔。没什么好说的,查询之间的差距需要与你的 UI 的刷新间隔相同(即两个视图切换的速度);事实上,由于您的查询需要花费不同的时间,因此无论如何同步两者都会很棘手。
我为服务台团队创建了一个墙板应用程序,它在前端使用 WPF,在后端使用 Cisco 电话数据库。该应用程序由显示不同信息的两个屏幕组成,这些屏幕显示在同一屏幕中,并以 System.Timers.Timer
.
在彼此之间切换
制作应用程序,以便如果 WindowA
可见,则显示 WindowB
,然后隐藏 WindowA
。 Windows 中的一个变得可见时,Window 的计时器再次激活,恢复数据库调用,而另一个 Window 的计时器被禁用:
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
可见性更改为 Visible,WindowB's
可见性更改为 Collapsed,WindowB
崩溃但 WindowA
需要 2-3 秒才能变为可见,而大多数时候 WindowA
变得可见,而当 WindowB
崩溃时则看不到。这(当它不起作用时)导致桌面而不是应用程序可见。
我最初使用 DispatcherPriority.Background
但这导致换网器在 70-80% 的时间内工作,所以我决定将其更改为 DispatcherPriority.Normal
(DispatcherPriority.Send
结果基本上与普通情况相同).
问题:
- 考虑到这是 运行 在四核 CPU 的 x64 模式下,这是 Dispatcher 预期的正常行为吗?
- 知道查询是在未等待的异步方法中执行的,Dispatcher 不应该优先于这些方法吗?
- 是否有其他方法(不使用 Dispatcher,或使用另一个 Window 属性)来完成我正在寻找的东西?
这是用于 access/start 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();
}
尝试使用 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();
听起来您正在编写自助服务终端应用程序(即全屏、非交互式)。如果是这种情况,我认为您最好拥有一个 window 并在其中切换视图,而不是在两个单独的 windows 之间切换。另外,您需要将数据库查询工作与 window 内容的刷新分开。此外,我认为如果视图彼此一无所知,那将会有所帮助:目前您的第一个 window 与您的第二个紧密耦合,这并不是一个好主意。
在我看来,如果你稍微改变一下你的架构,你遇到的很多问题都会消失。以下是我的建议:
首先,只需要一个 window。创建两个 用户控件 (项目 > 添加用户控件),并将 XAML 布局从现有的 windows 移动到这两个新控件中。然后让你的主要 window 看起来像这样:
<Window x:Class="Whosebug.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:my="clr-namespace:Whosebug"
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>
这是一个没有 chrome 的全屏 window,其中包含您的两个用户控件(基本上是您现有 windows 的内容)。它们在 Grid
元素中分层,以便一个位于另一个之上:我正在使用 Panel.ZIndex
属性 将第一个控件强制到堆的顶部。最后,我使用了一个动画(在 window 加载时触发)切换其中一个控件的可见性以在一段时间后将其隐藏。动画设置为重复和自动反转,其效果是隐藏其中一个控件,然后使其再次可见。您可以更改 Duration
属性值来控制每个控件 "stays" 可见多长时间;在此示例中设置为 5 秒,这意味着切换之间有 10 秒的延迟。
这项工作的关键是第一个用户控件在可见时必须完全遮住位于其下方的另一个用户控件。这很容易通过设置控件的背景颜色来实现。
您的用户控件可以包含 window 可以包含的任何内容。这是我使用的示例用户控件 XAML:
<UserControl x:Class="Whosebug.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
,这样我就可以知道在任何给定时间哪个控件是可见的。
代码隐藏看起来像这样(两者都一样,除了 class 名称):
using System;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Controls;
using System.Windows.Threading;
namespace Whosebug
{
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 计时器都可以完成。
worker 使用 Task.Run
调度到线程池 运行,然后异步 运行s。我正在通过 Task.Delay
等待一段时间来模拟一个长时间 运行ning 的工作。这个工作者方法将是你的数据库查询被调用的地方。您可以通过设置计时器的 Interval
属性 来改变连续查询之间的间隔。没什么好说的,查询之间的差距需要与你的 UI 的刷新间隔相同(即两个视图切换的速度);事实上,由于您的查询需要花费不同的时间,因此无论如何同步两者都会很棘手。