How to use busy indicator in wpf - wpf

My page within a frame takes to time to load meaning the controls take some time to appear on the page for the first time. Where in my main window.cs file should I set the IsBusy = true.I have no idea how to use busy indicator.When should I switch it to true or false. please guide me how should I use it ? Thanks in advance.

Generally you would set the busy indicator before you start doing a load of heavy processing so that is dependant on your code.
It would normally before just before you spawn a background thread to do a load of work leaving the UI to say it's busy at the moment and when the thread is completing then "unbusy" the UI.

Wrap you Xaml with a busy indicator . Assuming you are using MVVM
<xctk:BusyIndicator BusyContent="{Binding BusyText}" IsBusy="{Binding IsBusy}">
<Grid>
<!--Your controls and content here-->
</Grid>
</xctk:BusyIndicator>
In your viewmodel
/// <summary>
/// To handle the Busy Indicator's state to busy or not
/// </summary>
private bool _isBusy;
public bool IsBusy
{
get
{
return _isBusy;
}
set
{
_isBusy = value;
RaisePropertyChanged(() => IsBusy);
}
}
private string _busyText;
//Busy Text Content
public string BusyText
{
get { return _busyText; }
set
{
_busyText = value;
RaisePropertyChanged(() => BusyText);
}
}
Command and Command Handler
//A Command action that can bind to a button
private RelayCommand _myCommand;
public RelayCommand MyCommand
{
get
{
return _myCommand??
(_myCommand= new RelayCommand(async () => await CommandHandler(), CanExecuteBoolean));
}
}
internal async Task CommandHandler()
{
Isbusy = true;
BusyText = "Loading Something...";
Thread.Sleep(3000); // Do your operation over here
Isbusy = false;
}

Related

Datagrid remains empty after asynchronous initialization in view model constructor

I have a WPF application with a view containing a data grid and a view model with an observable collection that is initialized by calling an asynchronous method in the constructor. But the data grid remains empty upon running the code.
The view model class looks like this.
internal class MainWindowViewModel : INotifyPropertyChanged
{
private readonly IBookingRecordService service;
public event PropertyChangedEventHandler? PropertyChanged;
private ObservableCollection<BookingRecord> bookingRecords = new();
public ObservableCollection<BookingRecord> BookingRecords
{
get => bookingRecords;
set
{
bookingRecords = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(BookingRecords)));
}
}
public MainWindowViewModel()
{
service = new BookingRecordService();
Task.Run(() => LoadBookingRecords());
}
private async Task LoadBookingRecords()
{
BookingRecords = new ObservableCollection<BookingRecord>(await service.Get());
}
}
I all LoadBookingRecords() in the constructor, so that the data starts loading on initialization of the view model already but I do it asynchronously, so it does not block the UI thread and makes the application unresponsive.
I have tried waiting for the completion of the task in the constructor via
Task.Run(() => LoadBookingRecords()).Wait();
to check that this has something to do with the asynchronous function call. And indeed, if I wait for the method to finish in the constructor, the data grid displays correctly. But I don't want to wait for the task to finish on the UI thread because it blocks the UI.
I have read that you must raise the PropertyChanged event on the UI thread to trigger a UI update and I suppose that is the problem here. I have also read that one can use
Application.Current.Dispatcher.BeginInvoke()
to schedule a delegate to run on the UI thread as soon as possible, so I tried the following.
private async Task LoadBookingRecords()
{
await Application.Current.Dispatcher.BeginInvoke(new Action(async () =>
{
BookingRecords = new ObservableCollection<BookingRecord>(await service.Get());
}));
}
But this leaves the DataGrid empty as well.
"'asynchronous ... in constructor" is something you must avoid.
Async calls must be awaited, which can not be done in a constructor. Declare an awaitable Initialize method instead
public Task Initialize()
{
return LoadBookingRecords();
}
and call it in an async Loaded event handler in your MainWindow:
private static async void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
await viewModel.Initialize();
}
Alternatively, create a factory method like
public static async Task<MainWindowViewModel> Create()
{
var viewModel = new MainWindowViewModel();
await viewModel.LoadBookingRecords();
return viewModel;
}
and call that in the Loaded handler:
private static async void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
DataContext = await MainWindowViewModel.Create();
}
Building on Clemens' answer, I tried something a little different in order to avoid touching the MainWindow code-behind.
I removed the call on LoadBookingRecords in the constructor and instead created a delegate command as a property that holds this method.
internal class MainWindowViewModel : INotifyPropertyChanged
{
private readonly IBookingRecordService service;
private ObservableCollection<BookingRecord> bookingRecords = new();
public ICommand LoadBookingRecordsCommand { get; set; }
public ObservableCollection<BookingRecord> BookingRecords
{
get => bookingRecords;
set
{
bookingRecords = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(BookingRecords)));
}
}
public MainWindowViewModel()
{
service = new BookingRecordService();
LoadBookingRecordsCommand = new DelegateCommand(async _ => await LoadBookingRecords());
}
private async Task LoadBookingRecords()
{
BookingRecords = new ObservableCollection<BookingRecord>(await service.Get());
}
}
I then added the NuGet package Microsoft.Xaml.Behaviors.Wpf to the project and added the following namespace to the MainWindow XAML.
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
Finally, I bound the delegate command to the MainWindow's Loaded event.
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<i:InvokeCommandAction Command="{Binding LoadBookingRecordsCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
Now the data grid displays correctly after being loaded.

How to force a UI update during a lengthy task on the UI thread

I have a window, and the window contains it's content and a "Loading" overlay as follows
<Grid>
<Grid>
<!-- STUFF -->
</Grid>
<Rectangle Fill="White" Opacity="0.7" Visibility="{Binding VisibilityWait, Mode=TwoWay,
Converter={StaticResource BoolToVisibilityConverter}}" />
</Grid>
My ViewModel then implements INotifyPropertyChanged and includes the property;
private bool visibilityWait;
public bool VisibilityWait
{
get
{
return visibilityWait;
}
set
{
visibilityWait = value;
OnPropertyChanged("VisibilityWait");
}
}
I know that this is set up correctly because if I set the VisibilityWait to true in the constructor, the window displays with the "Loading" overlay as expected and vice versa... However, if i try to do this during a method e.g.;
private void someMethod()
{
VisibilityWait = true;
//... Do things
VisibilityWait = false;
}
Then during the time that the program is in the "do things" section, the UI does not update to show the loading overlay.
How do I solve this issue?
EDIT: I found my own solution to this problem. See the answer below.
Usually when a function is taking place, any updates to the UI are blocked until the end of the function. This is because the frame does not get pushed until the end of the function. You can force this update by calling a method like this;
void AllowUIToUpdate()
{
DispatcherFrame frame = new DispatcherFrame();
Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Render, new DispatcherOperationCallback(delegate (object parameter)
{
frame.Continue = false;
return null;
}), null);
Dispatcher.PushFrame(frame);
//EDIT:
Application.Current.Dispatcher.Invoke(DispatcherPriority.Background,
new Action(delegate { }));
}
EDIT: I added in an extra line to the AllowUIToUpdate function, and now everything functions as expected!
An improvement of #Lewis_Heslop, this seams to work perfectly with MVVM:
private static void AllowUIToUpdate()
{
DispatcherFrame frame = new();
// DispatcherPriority set to Input, the highest priority
Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Input, new DispatcherOperationCallback(delegate (object parameter)
{
frame.Continue = false;
Thread.Sleep(20); // Stop all processes to make sure the UI update is perform
return null;
}), null);
Dispatcher.PushFrame(frame);
// DispatcherPriority set to Input, the highest priority
Application.Current.Dispatcher.Invoke(DispatcherPriority.Input, new Action(delegate { }));
}

Show RadBusyIndicator during Frame Navigation WPF

For our application, we need to display the busy indicator during navigation since some controls taking time to load. Normally for long running operation i will create a separate task and will trigger busy indicator in UI Thread but in this case I couldn't do that.
Once the busy indicator started to spin, it got disturbed by Frame.Source or Frame.Navigate which also executes in ui thread. so busy indictor hidden away .
The below is the piece of code i have used for navigation. This is executed as separate task .
public virtual void NavigateTo(string pageKey, object parameter)
{
Frame frame = null;
Action actionToExecuteOnUIContext = () =>
{
frame = GetDescendantFromName(Application.Current.MainWindow, "ContentFrame") as Frame;
if (frame != null)
{
frame.LoadCompleted -= frame_LoadCompleted;
frame.LoadCompleted += frame_LoadCompleted;
frame.Source = _pagesByKey[pageKey];
frame.DataContext = parameter;
}
};
Application.Current.Dispatcher.BeginInvoke(actionToExecuteOnUIContext, DispatcherPriority.Normal);
}
}
Any way to fix this or an alternative
WPF & RadBusyIndicator & .Net 4.5
In order to display a busy indicater you have to use a background thread, because the navigation also needs the UI thread.
I suggest to use something like this :
protected async void Navigation(string pageKey, object parameter)
{
await NavigateTo(pageKey,parameter).ContinueWith((t) =>
{// do UI stuff
Dispatcher.CurrentDispatcher.InvokeAsync(delegate
{
IsBusy = false;
});
},
//sync with UI thread
TaskScheduler.FromCurrentSynchronizationContext());
}
/// <summary>
/// This method needs to be async to perform async binding
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
private async Task NavigateTo(string pageKey, object parameter)
{
//your async method
}
where IsBusy is the value of the BusyIndicator. You have to set it to true, on navigation started.
Hope this post help you#!

Handling WPF button double-click in MVVM pattern

I have a button in an MVVM application that is hooked up to a command in the view model. The handler for the view model command does some file I/O (particularly calling File.Copy to create or overwrite an existing file).
Now, it seems that when I double-click on the button, this command handler gets called twice. Since both handlers are now trying to access the same file to copy/overwrite it at the same time, I'm getting an IOException.
Is there anyway to deal with this situation short of catching the IOException and ignoring it? This does not seem to be a guaranteed catch although there may be unrelated problems with the rest of the system that causes that to happen.
Use a value in the the ViewModel to protect the code that would be running when a click occurs. Set a value like: bool bFileIO = false;
Then in your handler function:
if (!bFileIO)
{
bFileIO = true;
//file IO here
bFileIO = false;
}
Something like that would protect the multi-clicking from trying to run multiple times.
The easiest way of doing this is to have your command return false in CanExecute while you're executing. This will prevent the second click from happening (as your button will be disabled). If using a DelegateCommand from prism:
private readonly DelegateCommand<object> copyCommand;
private bool isCopying = false;
public MyViewModel()
{
copyCommand = new DelegateCommand<object>(
_ => !isCopying,
_ =>
{
if (isCopying) return; // this shouldn't be required, but here for safety
isCopying = true;
copyCommand.RaiseCanExecuteChanged();
// do copy
isCopying = false;
copyCommand.RaiseCanExecuteChanged();
});
}
I think you should use the CanExecute of your command to control your button.
<Button Command="{Binding WriteFileCommand}" Content="Button" Height="23" HorizontalAlignment="Left" Margin="273,194,0,0" Name="button1" VerticalAlignment="Top" Width="75" />
and the viewmodel
public class MyViewModel
{
private bool isWritingFile = false;
public DelegateCommand WriteFileCommand
{
get;
private set;
}
public bool IsWritingFile
{
get
{
return isWritingFile;
}
set
{
isWritingFile = value;
WriteFileCommand.RaiseCanExecuteChanged();
}
}
public MyViewModel()
{
WriteFileCommand = new DelegateCommand(WriteFile, CanWriteFile);
}
private void WriteFile()
{
IsWritingFile = true;
// write the file
// FileStream stream = new FileStrem(...)
//
IsWritingFile = false;
}
}

telerik Busy Indicator is not visible

Hi I am trying to use telerik Busy indicator with MVVM. I have the Busy indicator in Mainwindow. When there is an action(button click) on one of the user controls that is in the window, the user controls view model sends an message to the MinwindowviewModel. On the message the is busy indicator should show up. But this is not working. Why is this not working?
User controls view model
public class GetCustomerVM : ViewModelBase
{
private int _CustomerId;
public int CustomerId
{
get { return _CustomerId; }
set
{
if (value != _CustomerId)
{
_CustomerId = value;
RaisePropertyChanged("CustomerId");
}
}
}
public RelayCommand StartFetching { get; private set; }
public GetCustomerVM()
{
StartFetching = new RelayCommand(OnStart);
}
private void OnStart()
{
Messenger.Default.Send(new Start());
AccountDetails a = AccountRepository.GetAccountDetailsByID(CustomerId);
Messenger.Default.Send(new Complete());
}
}
The User Control View model is:
private bool _IsBusy;
public bool IsBusy
{
get { return _IsBusy; }
set
{
if (value != _IsBusy)
{
_IsBusy = value;
RaisePropertyChanged("IsBusy");
}
}
}
public WRunEngineVM()
{
RegisterForMessages();
}
private void RegisterForMessages()
{
Messenger.Default.Register<Start>(this, OnStart);
Messenger.Default.Register<Complete>(this, OnComplete);
}
private void OnComplete(Complete obj)
{
IsBusy = false;
}
private void OnStart(Start obj)
{
IsBusy = true;
}
In the Main window View, the root element is
<telerik:RadBusyIndicator IsBusy="{Binding IsBusy}" telerik:StyleManager.Theme="Windows7">
What does AccountDetails a = AccountRepository.GetAccountDetailsByID(CustomerId); do? My guess is that whatever is happeneing there is running on the UI thread. Because it is all happeneing on the UI thread, there is never a chance of the UI to refresh and show the RadBusyIndicator. Try moving all of the work on in OnStart to a BackgroundWorker, including sending the messages. You will run into issues here, because the messages will be updating the UI thread from a background thread, so you will need to use the Dispatcher to set IsBusy to true or false.

Resources