WPF 更新 UI 来自后台线程

WPF Update UI From Background Thread

我知道类似的问题已被问过很多次,但我无法找到在这种情况下有效的任何方法。我有一个使用 wpf-notifyicon 最小化到任务栏运行的应用程序。主要 window 打开、验证然后隐藏。一个进程在后台运行,我希望它向主线程发送更新。大部分时间它都有效。但是,任务栏图标有一个上下文菜单,允许用户打开应用程序设置。如果我打开然后关闭设置 window,下次我尝试更新气球时,我会收到空​​引用错误 System.NullReferenceException: 'Object reference not set to an instance of an object.' System.Windows.Application.MainWindow.get returned null.

就像我打开另一个window,主window丢失了,我不知道如何再次找到它。

这就是我更新气球的方式。此代码位于通知服务中,并从主窗口视图模型内部和其他服务内部调用。

// Show balloon update on the main thread
Application.Current.Dispatcher.Invoke( new Action( () =>
{
    var notifyIcon = ( TaskbarIcon )Application.Current.MainWindow.FindName( "NotifyIcon" );
    notifyIcon.ShowBalloonTip( title, message, balloonIcon );
} ), DispatcherPriority.Normal );

通知图标在 XAML 内声明为主 window。

<tb:TaskbarIcon
    x:Name="NotifyIcon"
    IconSource="/Resources/Icons/card_16x16.ico"
    ToolTipText="Processor"
    MenuActivation="LeftOrRightClick"
    DoubleClickCommand="{Binding ShowStatusCommand}">

    <tb:TaskbarIcon.ContextMenu>
        <ContextMenu>
            <MenuItem Header="Settings" Command="{Binding ShowSettingsCommand}" />
            <Separator />                    
            <MenuItem Header="Exit" Command="{Binding ExitApplicationCommand}" />
        </ContextMenu>
    </tb:TaskbarIcon.ContextMenu>

    <tb:TaskbarIcon.TrayToolTip>
        <Border Background="Gray"
                BorderBrush="DarkGray"
                BorderThickness="1"
                CornerRadius="3"
                Opacity="0.8"
                Width="180"
                Height="20">
            <TextBlock Text="{Binding ListeningMessage }" HorizontalAlignment="Center" VerticalAlignment="Center" />
        </Border>
    </tb:TaskbarIcon.TrayToolTip>
</tb:TaskbarIcon>

如何从后台线程安全地更新气球图标?

更新 1:

上下文菜单绑定到视图模型中的命令。打开设置window

<ContextMenu>
    <MenuItem Header="Settings" Command="{Binding ShowSettingsCommand}" />
    <Separator />
    <MenuItem Header="Exit" Command="{Binding ExitApplicationCommand}" />
</ContextMenu>

VM中的命令是:

    public ICommand ShowSettingsCommand => new DelegateCommand
    {
        CommandAction = () =>
        {
            Application.Current.MainWindow = new Views.SettingsWindow( _logger, _hidservice, _certificateService );
            Application.Current.MainWindow.Show();
        }
    };

要关闭设置window,我在window代码后面有一个动作

public ICommand CancelSettingsCommand => new DelegateCommand
{
    CommandAction = () => CloseAction()
};


// In Code Behind
vm.CloseAction = new Action( () =>  this.Close()  );

你被覆盖了主要 window。你不应该那样做。只需创建它的一个新实例并调用 Show().

public ICommand ShowSettingsCommand => new DelegateCommand
{
    CommandAction = () =>
    {
        var settingsWindow = = new Views.SettingsWindow( _logger, _hidservice, _certificateService );
        settingsWindow.Show();
    }
};

不显示视图模型中的 window。请改用事件处理程序。 也不要覆盖 Application.MainWindow.

的值

不要使用 Dispatcher 来显示进度。从 .NET 4.5 开始,推荐的模式是使用 IProgres<T>。框架提供了默认实现 Progress<T>保存进度数据的进度模型

class ProgressArgs 
{
  public string Title { get; set; }
  public string Message { get; set; }
  public object Icon { get; set; }
}

主要UI线程

private void ShowSettings_OnMenuClicked(object sender, EventArgs e)
{
  // Pass this IProgress<T> instance to every class/thread 
  // that needs to report progress (execute the delegate)
  IProgres<ProgressArgs> progressReporter = new Progress<ProgressArgs>(ReportPropgressDelegate);

  var settingsWindow = new Views.SettingsWindow(_logger, _hidservice, _certificateService, progressReporter);
}

private void ReportPropgressDelegate(ProgressArgs progress)
{
  var notifyIcon = (TaskbarIcon) Application.Current.MainWindow.FindName("NotifyIcon");
    notifyIcon.ShowBalloonTip(progress.Title, progress.Message, progress.Icon);
}

后台线程

private void DoWork(IProgress<ProgressArgs> progressReporter)
{
  // Do work

  // Report progress
  var progress = new ProgressArgs() { Title = "Title", Message = "Some message", Icon = null };
  progressReporter.Report(progress);
}