WPF Update UI From Background Thread

914 Views Asked by At

I know similar questions have been asked many times but I have not been able to find anything that works in this situation. I have an application that runs minimized to the taskbar using wpf-notifyicon. The main window opens, authenticates and is then hidden. A process runs in the background and I would like it to send updates to the main thread. Most of the time it works. However, the taskbar icon has a context menu that allows the user to open application settings. If I open then close the settings window, the next time I try to update the balloon, I get a a null reference error System.NullReferenceException: 'Object reference not set to an instance of an object.' System.Windows.Application.MainWindow.get returned null.

It's like once I open another window, the main window is lost and I can't figure out how to find it again.

This is how I am updating the balloon. This code is in a notification service and is called from inside the MainWindow View Model and from inside other services.

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

The notification icon is declared inside the XAML for the main 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>

How can I safely update the balloon icon from background threads?

Update 1:

The context menu is bound to commands in the view model. To open the settings window

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

The command in the VM is:

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

To close the settings window, I have an action in the window code behind

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


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

There are 2 best solutions below

1
On BEST ANSWER

You are overriden the main window. You should not do that. Just create a new instance of it and call Show().

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

Don't show the window from the view model. Use an event handler instead. Also don't override the value of Application.MainWindow.

Don't use Dispatcher to show progress. Since .NET 4.5 the recommended pattern is to use IProgres<T>. The frameworks provides a default implementation Progress<T>: Progress model to hold progress data

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

Main UI thread

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

Background thread

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

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