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() );
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();
}
};
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);
}
Related
I'm not really sure if this is a WPF or RxUI issue, so I just show my code and then explain my problem.
MainWindowView.xaml
<reactiveui:ReactiveWindow ...
Height="840"
Width="800"
ResizeMode="CanMinimize">
<DockPanel>
<!-- Some stuff in the main dock panel -->
<Border BorderThickness="5"
DockPanel.Dock="Top">
<DockPanel>
<!-- Other stuff in inner dock panel -->
<Grid Height="100"
Background="Black"
DockPanel.Dock="Top"
Margin="0 10 0 0">
<Image x:Name="UI_WaveformImage"
Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Border}}, Path=Width}"
Stretch="Fill">
</Image>
</Grid>
</DockPanel>
</Border>
</DockPanel>
</reactiveui:ReactiveWindow>
MainWindoeView.xaml.cs
public MainWindowView ()
{
InitializeComponent();
ViewModel = new MainWindowViewModel();
this.WhenActivated(d =>
{
this.OneWayBind(ViewModel, vm => vm.Waveform.Image, v => v.UI_WaveformImage.Source).DisposeWith(d);
this.WhenAnyValue(x => x.UI_WaveformImage.ActualWidth).BindTo(this, x => x.ViewModel.WaveformWidth).DisposeWith(d);
});
}
MainWindowVieModel.cs
public class MainWindowViewModel : ReactiveObject, IEnableLogger
{
public ReactiveCommand<string, AudioWaveformImage> GenerateNewWaveform { get; }
private readonly ObservableAsPropertyHelper<AudioWaveformImage> _waveform;
public AudioWaveformImage Waveform => _waveform.Value;
public int WaveformWidth { get; set; }
public MainWindowViewModel ()
{
GenerateNewWaveform = ReactiveCommand.CreateFromTask<string, AudioWaveformImage>(p => GenerateNewWaveformImageImpl(p));
GenerateNewWaveform.ThrownExceptions.Subscribe(ex => {
this.Log().Error("GenerateNewWaveform command failed to execute.", ex);
});
_waveform = GenerateNewWaveform.ToProperty(this, nameof(Waveform), scheduler: RxApp.MainThreadScheduler);
(1) GenerateNewWaveform.Execute(AudioFilePath).ObserveOn(RxApp.MainThreadScheduler);
(2) this.WhenAnyValue(x => x.AudioFilePath)
.Where(p => !string.IsNullOrWhiteSpace(p))
.InvokeCommand(GenerateNewWaveform);
}
private async Task<AudioWaveformImage> GenerateNewWaveformImageImpl (string path)
{
UIOutput.AppendLine($"Waveform width start: {WaveformWidth}");
var wf = new AudioWaveformImage(WaveformWidth == 0 ? 100: WaveformWidth, 100, 100, Colors.CornflowerBlue, Colors.Red, path);
await Task.Delay(10);
//await wf.Update();
UIOutput.AppendLine($"Waveform width end: {WaveformWidth}");
return wf;
}
}
Just a bit of explanation:
UIOutput.AppendLine() just displays info in a textbox for me. I use it for debugging, but its purpose is to let the end-user know the progress of what's going on.
AudioWaveformImage is initialized with the desired size, colors, and path to the file that I want the waveform of. So, I need to know the size of the Image element I want to display it in.
So here's my problem:
The first time GenerateNewWaveformImageImpl() is executed on program startup, via line (1), UIOutput shows "Waveform width start: 0" and "Waveform width end: 0". After I change AudioFilePath and GenerateNewWaveformImageImpl() gets run a second time, via line (2), I get the correct width as UIOutput shows "Waveform width start: 776" and "Waveform width end: 776". Now I know I could hard code that, but that doesn't help when the window gets resized.
Why is the correct Width not being reported when the app first runs?
Try to invoke the command once the view model has been activated:
public class MainWindowViewModel : ReactiveObject, IEnableLogger, IActivatableViewModel
{
public ViewModelActivator Activator { get; } = new ViewModelActivator();
public MainWindowViewModel()
{
this.WhenActivated((CompositeDisposable disp) =>
{
//invoke command...
});
}
}
Okay, the main problem in your original code is that you're invoking GenerateNewWaveform in the VM constructor. At this point, your Width has NOT been bound to your property yet, as witnessed here:
public MainWindowView () {
// Constructor happens first.
ViewModel = new MainWindowViewModel();
this.WhenActivated(d => {
// Binding to Width happens later..
this.WhenAnyValue(v => v.UI_WaveformImage.ActualWidth)
.BindTo(ViewModel, vm => vm.WaveformWidth)
.DisposeWith(d);
});
}
The answer that suggested using a ViewModelActivator will not work either, because ViewModel.WhenActivated is triggered before View.WhenActivated. I.e. Same issue as above. The command in the VM will be invoked before the Width binding in the View.
One way to solve this is to invoke your command after the Width binding.
public MainWindowView () {
ViewModel = new MainWindowViewModel(); // Constructor..
this.WhenActivated(d => {
// Bind to Width..
// Tell the VM to invoke, after Width is bound..
ViewModel.GenerateNewWaveform.Execute(...);
});
}
If you don't like having the View telling the VM what to do, because it's iffy, you could have the VM do WhenAnyValue(_ => _.WaveformWidth), filtering by > 0, and doing a Take(1). This will allow you to invoke when your VM receives the first non-zero width after the binding.
This question already has answers here:
Wait for animation, render to complete - XAML and C#
(2 answers)
In WPF, is there a "render complete" event?
(3 answers)
How to detect when a WPF control has been redrawn?
(2 answers)
Is there a DataGrid "rendering complete" event?
(1 answer)
Showing a WPF loading message until after UI finishes updating
(2 answers)
Closed 2 years ago.
Background:
I have an application that collects data, does calculations and presents them to the user in graphs in a window. For each set of data I take a picture of the window and store it as a .png on the harddrive so that the user can go back and check the result later.
Problem:
Currently, I update the viewmodel with the new data and then have a Task.Delay(...) as to give the application some time to render the new content on the view. But sometimes I will get a picture of the previous dataset if the delay wasn't enough, I can increase the delay time to make it happen less often but that in turn will slow down the program unneccesarilly. I'm basically looking for a smart way to check if the view have been rendered with the new dataset rather than have a dumb delay.
I've looked into Window.ContentRendered event. But that only seems to fire the first time a window is rendered, so I would have to close and re-create a new window for every picture if I want to use that one and that just feels like unneccesary overhead to me. I would need something similar that fires everytime it is re-rendered, or some other way to know if the view is ready for the picture?
Short Answer: Yes, you can do this by calling your picture-saving method on the Dispatcher thread when it is idle by giving it a priority of DispatcherPriority.ApplicationIdle.
Long Answer: Here's a sample showing this at work. I have here an app that updates a viewmodel's text property when you click a button, but it takes a couple of seconds for it to update the control that is bound to it because the text is huge.
The moment I know the new data is trying to be shown, I issue a Dispatcher command to wait for the UI to be idle before I do something:
Dispatcher.Invoke((Action)(() => { // take your picture here }), DispatcherPriority.ApplicationIdle);
MainWindowViewModel.cs
public class MainWindowViewModel : INotifyPropertyChanged
{
private string messages;
private string controlText;
public MainWindowViewModel Parent { get; private set; }
public string Messages { get => this.messages; set { this.messages = value; OnPropertyChanged(); } }
public string ControlText { get => this.controlText; set { this.controlText = value; OnPropertyChanged(); } }
public void UpdateWithNewData()
{
var strBuilder = new StringBuilder();
for (int i = 0; i < 100000; i++)
{
strBuilder.AppendLine($"{DateTime.Now:HH:mm:ss.ffffff}");
}
// This will update the TextBox that is bound to this property,
// but it will take awhile because the text is HUUUUGE.
this.ControlText = strBuilder.ToString();
}
public MainWindowViewModel()
{
this.ControlText = "This area will take a while to render when you click the button below.";
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
MainWindow.xaml
<Window x:Class="_65951670.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid Background="LightSalmon">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBox IsReadOnly="True" Text="{Binding ControlText,UpdateSourceTrigger=PropertyChanged}" TextWrapping="Wrap" Margin="5" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Visible"/>
<Button Grid.Row="1" HorizontalAlignment="Center" VerticalAlignment="Center" Padding="15,5" Content="Update Above With Lots Of Text" Click="Button_Click"/>
</Grid>
<Grid Grid.Row="1">
<TextBox Text="{Binding Messages}" TextWrapping="Wrap" VerticalScrollBarVisibility="Visible" HorizontalScrollBarVisibility="Disabled" Margin="5" IsReadOnly="True"/>
</Grid>
</Grid>
</Window>
MainWindow.xaml.cs
public partial class MainWindow : Window
{
private MainWindowViewModel viewModel;
public MainWindow()
{
InitializeComponent();
viewModel = new MainWindowViewModel();
this.DataContext = viewModel;
this.viewModel.PropertyChanged += ViewModel_PropertyChanged;
}
private void ViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(this.viewModel.ControlText))
{
var sw = new Stopwatch();
sw.Start();
this.viewModel.Messages += $"Property Changed: {DateTime.Now:HH:mm:ss.ffffff}\n";
// If you got here, you know that the DataContext has changed, but you don't know when it will be done rendering.
// So use Dispatcher and wait for it to be idle before performing another action.
// Put your picture-saving method inside of the 'Action' here.
Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, (Action)(() =>
{
this.viewModel.Messages += $"UI Became Idle At: {DateTime.Now:HH:mm:ss.ffffff}\nIt took {sw.ElapsedMilliseconds} ms to render, Take Picture Now!";
}));
}
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
this.viewModel.UpdateWithNewData();
}
}
Normally you can call void function(from ViewModel) in View by simply:
Button Name = VoidFunctionInViewModel
Button Command={Binding Path=VoidFunctionInViewModel}
But when accessing this function inside a DialogHost, the void function isn't triggered.
I have tried retrieving a string field to the DialogHost and it works fine, but when it comes to commands on buttons it doesn't work.
MainViewModel.cs Commands:
public async void OpenDialog()
{
var confirm = new ConfirmationView{DataContext = this};
await DialogHost.Show(confirm);
}
public void Accept()
{
Console.WriteLine("It Failed!");
}
public string Success {get; set;} = "Success"
ConfirmationView.xaml:
<Button Command="{Binding Path=Accept}" Content="Accept"/>
<Button Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}" Content="Cancel"/>
<TextBlock Name="Success"/>
MainView.xaml:
<materialDesign:DialogHost DialogTheme="Inherit" CloseOnClickAway="True">
</materialDesign:DialogHost>
The Property "Success" is successfully used and shown by the DialogHost.
But the Button "Accept" with the command Accept isn't triggered by the DialogHost.
The Button "Cancel" is working, with the command from materialDesign.
Shouldnt it be this way for caliburn.micro?
<Button x:Name="Accept" Content="Accept"/>
Using the Command="" syntax works only by using ICommand or RelayCommand in the ViewModel, or? At least that is how i understood caliburn.micro so far.
If that doesnt work, you could try this. This worked for me in the drawerHost, where caliburn.micro Command binding failed for me:
<Button cal:Message.Attach="[Event Click] = [Action Accept()]" Content="Accept" />
other sources with dialog examples, which might be usefull:
https://github.com/MaterialDesignInXAML/MaterialDesignInXamlToolkit/wiki/Dialogs
https://github.com/Keboo/MaterialDesignInXaml.Examples
This error happen when I open new usercontrol into dialoghost during opening dialoghost :
when I press submit button
this is my code xaml:
MainWindow:
<Grid>
<Button Content="Show"
Command="{Binding OpenRegisCommand}"
VerticalAlignment="Bottom"
Margin="0 0 0 40"
Foreground="White"
Width="100"
Background="#23A9F2">
</Button>
<md:DialogHost Identifier="RootDialogHostId">
</md:DialogHost>
</Grid>
my usercontrol and my mainwindow using 1 viewmodel:
public MainViewModel()
{
OpenRegisCommand = new DelegateCommand(OpenFormRegis);
Submit = new DelegateCommand(Check);
}
private async void Check()
{
if(Name.Equals("admin")&&Pass.Equals("123456"))
{
var view = new DialogOk();
await DialogHost.Show(view, DialogHostId);
}
else
{
var view = new DialogNo();
await DialogHost.Show(view, DialogHostId);
}
}
private async void OpenFormRegis()
{
var view = new FormRegis();
await DialogHost.Show(view, DialogHostId);
}
button submit in my usercontrol binding to DelegateCommand Submit in my viewmodel
Each dialog hosts can be used to show one view at a time.
If you have two views that you wanted to shot at the same time (less likely), you will need two dialog hosts.
In your case, if you're trying to open the "OpenFormRegis" window in your dialog host, I would suggest you to use Windows instead.
In my case, I did the following:
1- Get the dialogs that are active, using DialogHost.GetDialogSession("RootDialog").
2- If it is different from null, set the content with the UpdateContent(alertBox) method which updates the content of the dialog.
3- If it is null, establish the usual flow to show the content of the dialog.
AlertBoxView alertBox = new AlertBoxView();
alertBox.DataContext = this;
var dialog = DialogHost.GetDialogSession(IdentifierDialog);
if (dialog != null)
{
dialog.UpdateContent(alertBox);
}
else
{
await DialogHost.Show(alertBox, IdentifierDialog);
}
*AlertBoxView: is my custom view of DialogHost
*IdentifierDialog: is the variable that takes the Identifier of the dialog
I'm currently learning WPF/MVVM, and have been using the code in the following question to display dialogs using a Dialog Service (including the boolean change from Julian Dominguez):
Good or bad practice for Dialogs in wpf with MVVM?
Displaying a dialog works well, but the dialog result is always false despite the fact that the dialog is actually being shown. My DialogViewModel is currently empty, and I think that maybe I need to "hook up" my DialogViewModel to the RequestCloseDialog event. Is this the case?
does your DialogViewmodel implement IDialogResultVMHelper? and does your View/DataTemplate has a Command Binding to your DialogViewmodel which raise the RequestCloseDialog?
eg
public class DialogViewmodel : INPCBase, IDialogResultVMHelper
{
private readonly Lazy<DelegateCommand> _acceptCommand;
public DialogViewmodel()
{
this._acceptCommand = new Lazy<DelegateCommand>(() => new DelegateCommand(() => InvokeRequestCloseDialog(new RequestCloseDialogEventArgs(true)), () => **Your Condition goes here**));
}
public event EventHandler<RequestCloseDialogEventArgs> RequestCloseDialog;
private void InvokeRequestCloseDialog(RequestCloseDialogEventArgs e)
{
var handler = RequestCloseDialog;
if (handler != null)
handler(this, e);
}
}
anywhere in your Dialog control:
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" MinHeight="30">
<Button IsDefault="True" Content="Übernehmen" MinWidth="100" Command="{Binding AcceptCommand}"/>
<Button IsCancel="True" Content="Abbrechen" MinWidth="100"/>
</StackPanel>
and then your result should work in your viewmodel
var dialog = new DialogViewmodel();
var result = _dialogservice.ShowDialog("My Dialog", dialog );
if(result.HasValue && result.Value)
{
//accept true
}
else
{
//Cancel or false
}