In WPF, using MVVM design, I've created a screen designed to load large numbers of logs into a ListView on the click of a button. Upon return, a label is updated to display the number of logs returned. This process can sometimes take a while. Our DA is in the process of optimizing things, but meanwhile I am required to make the following changes to indicate to the user that the search is running:
Display the mouse as a WaitCursor.
Update the label text to display "Searching...".
I have a class which implements ICommand and I have the WaitCursor working correctly. However, I cannot get the desired behavior for updating the label to display when the search is running. My current code:
MyScreen.xaml
<Button
Name="DisplayButton"
Content="Display Logs"
Command="{Binding DisplayLogsCommand}"
Margin="0,64,10,0"
VerticalAlignment="Top"
HorizontalAlignment="Right"
Width="112"/>
<Label
Content="{Binding LogsShowingText}"
Margin="0,0,127,8"
Foreground="#FF3C3B3B"
HorizontalAlignment="Right"
Width="145" Height="24"
VerticalAlignment="Bottom"
HorizontalContentAlignment="Right"
FontSize="11"/>
MyScreenVM.cs
private Command displayLogsCommand;
private string logShowingText;
public Command DisplayLogsCommand
{
get
{
if (this.displayLogsCommand == null)
{
// Attempt 3 made here.
bool useWaitCursor = true;
Func<bool> canExecute = () => this.ValidateFields();
Action execute = () =>
{
/*
* TODO: Update this.LogsShowingText to read "Searching..."
*/
// Attempt 1 and 2 made here.
LogEntry[] entries = this.ClientConnection.GetLogs();
this.LogsShowingText = string.Format("Showing {0} Logs", entries.Length);
this.FilteredEntries = new ObservableCollection<LogEntry>(entries);
};
this.displayLogsCommand = new Command(useWaitCursor, canExecute, execute);
}
return this.displayLogsCommand;
}
}
public string LogsShowingText
{
get
{
return this.logsShowingText;
}
set
{
this.logsShowingText= value;
OnPropertyChanged("LogsShowingText");
}
}
Thus far the results and my associated failed attempts are below:
After logs are returned, the Label only reads "Searching...".
Application.Current.Dispatcher.Invoke(new Action(() => this.LogsShowingText = "Searching..."));
After logs are returned, the Label only reads "Showing N Logs".
this.LogsShowingText = string.Format("Searching...");
Before and during search, the Label reads "Searching...", then after logs are returned, Label reads "Showing N Logs". Same code as #2, different location.
I understand this probably has something to do with the UI being blocked until the Action completes, which clearly explains attempt 1 showing the last queued update to the label and attempt 2 showing the last hard coded update to the label. Attempt 3 almost works, but the Label should not be updated until the user has clicked the button to perform the search. How can I do this?
Since this is an expensive command, meaning the UI hangs while it processes, you should convert it to an async command.
public Command DisplayLogsCommand
{
get
{
if (this.displayLogsCommand == null)
{
bool useWaitCursor = true;
Func<bool> canExecute = () => this.ValidateFields();
Action execute = async () =>
{
this.LogsShowingText = "Searching";
// Attempt 1 and 2 made here.
LogEntry[] entries = await Task<LogEntry[]>.Run(() => this.ClientConnection.GetLogs());
this.LogsShowingText = string.Format("Showing {0} Logs", entries.Length);
this.FilteredEntries = new ObservableCollection<LogEntry>(entries);
};
this.displayLogsCommand = new Command(useWaitCursor, canExecute, execute);
}
return this.displayLogsCommand;
}
}
By making the Action delegate async, you can now await within the Action. This lets you wrap the call to your DataLayer in a Task and await it. Now that the expensive operation is running off the UI thread, your label will be updated properly before and after. No need to marshall the changes using a Dispatcher.
This will update the labels only when the command has been executed by the user clicking the button.
Related
We are seeing some strange behavior surrounding the SelectedItem property in our DataGrid. Some background information:
The DataGrid displays the result of a query to our database.
There is a button that allows the user to manually refresh the results in the DataGrid. There is an auto-refresh mechanism whereby the results will automatically refresh every 30 seconds.
What we are seeing is the SelectedItem property will always become index 0 of the ItemsSource for the Datagrid when the auto-refresh occurs. But we want the currently selected row to remain the selected row after the refresh. However, if the user manually clicks refresh, the selected row remains the same after the refresh which is strange because the same code is running for the refresh logic. And yes, we have code that remembers the currently selected item which then gets set again after the refresh has been completed.
Here is some of the relevant code:
<UserControl.Resources>
<CollectionViewSource Source="{Binding DataGridResults}" x:Key="ReferralItemsSource"/>
</UserControl.Resources>
<customControls:CustomDataGrid x:Name="GridControl"
ItemsSource="{Binding Source={StaticResource ReferralItemsSource}}"
SelectedItem="{Binding DataContext.SelectedReferral, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
IsReadOnly="False"
IsSynchronizedWithCurrentItem="True"
SelectionMode="Single">
private async void RefreshWorklist(bool invokedByAutoRefresh = false)
{
try
{
if (Initialising || ShowSpinner || IsProcessing || ShowRefreshSpinner || IsCurrentWorklistDeleted || !_sessionData.IsActive()) return;
IsProcessing = true;
RefreshWorklistCommand.RaiseCanExecuteChanged();
if (CurrentWorklistId != null)
{
var selectedReferralId = SelectedReferral.pk_Referral_ID;
if (invokedByAutoRefresh)
{
// Refresh has been invoked by _timer, so show spinner on the results page only
ShowRefreshSpinner = true;
}
else
{
// User has manually clicked refresh button so show app wide spinner
ShowSpinner = true;
if (_timer != null)
{
SetupWorklistRefreshTimer(); // Setup _timer again so that it will refresh again at an appropriate time
}
}
Referrals = await _referralRepository.GetReferralsFromWorklistAsync(CurrentWorklistId.Value, invokedByAutoRefresh);
if (Filters.Count > 0)
{
var listOfReferralPks = ReferralFiltering.GetFilteredResults(Referrals, Filters.Where(f => f.HasBeenApplied).ToList());
var filteredResults = Referrals.Where(r => listOfReferralPks.Contains(r.pk_Referral_ID)).ToList();
DataGridResults = MapReferralLookupItemsToReferralLookupItemViewModels(filteredResults);
}
else
{
DataGridResults = MapReferralLookupItemsToReferralLookupItemViewModels(Referrals);
}
SelectedReferral = DataGridResults.FirstOrDefault(r => r.pk_Referral_ID == selectedReferralId);
}
}
catch (Exception e)
{
_errorHandler.DisplayError(e);
}
}
As explained earlier, RefreshWorklist() is called by the manual refresh invoked through a Command:
private void Execute_RefreshWorklist()
{
RefreshWorklist();
}
Or automatically through the use of a Timer:
private void SetupWorklistRefreshTimer()
{
_timer?.Dispose();
var refreshInterval = _userSettingsRepository.GetIntegerSystemSetting("ReferralsWorklistRefreshInterval");
if (refreshInterval <= 0) return; // If this is 0 or below then the refresh should be disabled
if (refreshInterval < 10) // If it is less than 10 then set it to 10 to avoid too many MT calls
{
refreshInterval = 10;
}
var timeUntilFirstTick = refreshInterval * 1000;
_timer = new Timer((s) => RefreshWorklist(true), null, timeUntilFirstTick, refreshInterval * 1000);
}
And finally the SelectedItem property view model binding property:
public ReferralLookupItemViewModel SelectedReferral
{
get { return _selectedReferral; }
set
{
if (_selectedReferral != value)
{
_selectedReferral = value;
OnPropertyChanged();
}
}
}
Does anybody have any idea as to why this behavior is occurring? Is it something to do with the Timer? I appreciate this is not a simple question so please ask away for more information.
You need to assign properties in Binding with the UI on the UI thread.
Replace your Timer with a DispatcherTimer or use Dispatcher.Invoke or Dispatcher.BeginInvoke inside the existing Timer callback when calling RefreshWorklist.
By pressing the Button you are already on the UI thread, but Timer has its own thread that is different from the UI thread.
DispatcherTimer callback are called on the UI thread instead https://learn.microsoft.com/en-us/dotnet/api/system.windows.threading.dispatchertimer?view=netframework-4.0
I am having a rotator having label in each slide indicating the count to be displayed there. I have bounded it property which performs an API operation for fetching the count. I getting the getting the value fetched but the problem is that when displayed on the label it visible only partially. I am familiar with the rules about updating UI elements on the UI thread using the Device.BeginInvokeOnMainThread. I have called with Device.BeginInvokeOnMainThread. For eg if the count is 25. but the label shows only 5. but when the page is refreshed the the same command gets executed and displays the correct value.I have followed viewmodel approach i.e i have command for fetching the count.
I have called that command during the page on appearing event also To refresh the count when I am back to home page even after initial load.
Xaml
<Label FontFamily="{StaticResource Montserrat-Regular}"
HorizontalOptions="StartAndExpand"
Text="{Binding Count}"
FontSize="60"
Style="{StaticResource RotorLabelStyle}" />
viewmodel
await Task.Run(async () =>
{
var result = await _matchingTrialsService.GetMatchingTrialsCount();
Device.BeginInvokeOnMainThread(() =>
{
item.Count = $"{result:0}";
});
});
xaml.cs
protected override void OnAppearing()
{
base.OnAppearing();
Device.BeginInvokeOnMainThread(() => {
((PatientHomeViewModel)this.BindingContext).CountDetilsCommand.Execute(null);
});
}
Does anybody have a workaround for this issue
left is the result is truncated result
right is the correct result
Pulling my hair out at the point. My Picker is showing an annoying line on the UI and/or the Picker's Title property if that's enabled. I simply want the Picker, not the stuff showing on the UI beneath it. Any idea on how to achieve this? Do I have to use a custom renderer or is there something simple I'm missing?
Note: The list is intentionally empty in the below examples.
Without the title, I click the Existing button, the line shows, click it again and the modal appears:
With the title, I click the Existing button, the line and title show, click it again and the modal appears:
Don't know why I have to click the button twice. But it's only on the initial page load. If I exit the modal and click the button again, it immediately appears, no double-click. Not sure if that's related to my original question, but thought I'd include it for additional information.
NewSubjectPage.xaml (chopped for brevity)
<ContentPage.Content>
<StackLayout x:Name="NewSubjectMainLay">
<ScrollView>
<StackLayout x:Name="NewSubjectChildLay">
<Grid>
<Button
x:Name="NewSubjectExisChrtBtn"
Clicked="NewSubjectExisChrtBtn_Clicked"
Grid.Column="2"
Text="Existing" />
</Grid>
</StackLayout>
</ScrollView>
<Picker
x:Name="NewSubjectExisChrtPck"
IsVisible="False"
ItemsSource="{Binding Charts}"
ItemDisplayBinding="{Binding Name}"
SelectedIndexChanged="NewSubjectExisChrtPck_SelectedIndexChanged"
Title="Select chart"
Unfocused="NewSubjectExisChrtPck_Unfocused"/>
</StackLayout>
</ContentPage.Content>
NewSubjectPage.xaml.cs (chopped for brevity)
public partial class NewSubjectPage : ContentPage
{
private string chartName;
private readonly NewSubjectViewModel _viewModel;
public string ChartName
{
get => chartName;
private set
{
chartName = value;
OnPropertyChanged();
}
}
public NewSubjectPage()
{
InitializeComponent();
BindingContext = _viewModel = new NewSubjectViewModel();
chartName = "";
}
private void NewSubjectExisChrtBtn_Clicked(object sender, EventArgs e)
{
_viewModel.LoadChartsCommand.Execute(null);
NewSubjectExisChrtPck.IsVisible = true;
NewSubjectExisChrtPck.Focus();
}
private void NewSubjectExisChrtPck_SelectedIndexChanged(object sender, EventArgs e)
{
var picker = (Picker)sender;
int selectedIndex = picker.SelectedIndex;
if (selectedIndex != -1)
{
ChartName = picker.Items[picker.SelectedIndex];
}
}
private void NewSubjectExisChrtPck_Unfocused(object sender, FocusEventArgs e)
{
NewSubjectExisChrtPck.IsVisible = false;
NewSubjectExisChrtPck.Unfocus();
}
}
NewSubjectViewModel.cs (chopped for brevity)
class NewSubjectViewModel : BaseViewModel
{
private ObservableCollection<Chart> charts;
public ObservableCollection<Chart> Charts
{
get { return charts; }
private set
{
charts = value;
OnPropertyChanged();
}
}
public Command LoadChartsCommand { get; set; }
public NewSubjectViewModel()
{
LoadChartsCommand =
new Command(
async () => await ExecuteLoadChartsCommand()
);
}
private async Task ExecuteLoadChartsCommand()
{
try
{
IndicatorRunning = true;
var list = await App.Database.GetChartsAsync();
Charts = new ObservableCollection<Chart>(list);
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
}
}
Thanks for your help! Let me know if you need to see anything else.
First, I was not able to reproduce the issue of the modal not showing until a second click of the button. You might need to provide more code for that to happen. To even use your code sample I had to replace var list = await App.Database.GetChartsAsync(); with something else to simulate a long running task that returns an empty list. Also had to create a Chart type with a Name property. Not to mention BaseViewModel. In the future, please provide all code to reproduce the issue so there is minimal work required of the person who is trying to help you. There is concept on Stack Overflow called the MCVE (minimal, complete, verifiable example): http://stackoverflow.com/help/mcve
That said, perhaps the first click is actually focusing the emulator and making it the active app, and then the second is the first actual click on the button? This I can reproduce. IOW, if the emulator is not the foreground app, you have to click it once to make it active and then your app will handle clicks.
As for the undesirable UI, you do realize that the Picker UI is basically a clickable label that when clicked displays the actual picker modal? So when you make it visible, what you are making visible is the label UI, which has the line and the Title (if set), and when you focus that label, then the actual picker dialog is displayed. If you don't want to see the UI Label at all, then why make it visible? You can focus it without making it visible, so just remove the line NewSubjectExisChrtPck.IsVisible = true;
As a side note, when you call _viewModel.LoadChartsCommand.Execute(null); that calls an async method, var list = await App.Database.GetChartsAsync(); , so the LoadChartsCommand returns before you set the Charts property, and also then the code following the call to _viewModel.LoadChartsCommand.Execute(null); also executes before LoadChartsCommand really finishes, so you are making the picker visible and focusing it before the LoadChartsCommand finishes as well, so if you were loading actual items for the picker to display, they may not be there the first time. Maybe it's just the sample code, but I see no reason to use a command here, but rather you should just call an awaitable task. You are not binding to the LoadChartsCommand, so I see no reason for you to even use a Command in this scenario. Instead I suggest making ExecuteLoadChartsCommand public and calling it directly, e.g.:
private async void NewSubjectExisChrtBtn_Clicked(object sender, EventArgs e)
{
//_viewModel.LoadChartsCommand.Execute(null); // Returns immediately, so picker not loaded with items yet.
await _viewModel.ExecuteLoadChartsCommand(); // Waits for method to finish before before presenting the picker.
//NewSubjectExisChrtPck.IsVisible = true;
NewSubjectExisChrtPck.Focus();
}
I am trying to execute a bound command from my code behind utilizing the UiElement. button.Command.Execute(button.CommandParameter)
However, at this point the Command property of the button is null. simultaneously when I check the command in my View Model the property is set. The only diagnosis I can come up with is that until the window is actually visible the command is not bound to the command property of the button. I feel like may I'm missing a step somewhere or my implementation is not sound. below is some snipits of the code, please let me know if you need more.
Window constructor:
public PlottingViewModel ViewModel { get; set; }
public PlottingGUI()
{
InitializeComponent();
DataContext = (ViewModel = new PlottingViewModel());
_setDefaultSelections();
}
IList<RadioButton> buttons;
Setting default selections:
private void _setDefaultSelections()
{
buttons = new List<RadioButton>();
_getRadioButtons(this);
foreach (var setting in ViewModel.Settings.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public))
{
var settingValue = setting.GetValue(ViewModel.Settings);
var button = buttons.FirstOrDefault(btn => btn.Content.Equals(settingValue)
|| ((string)btn.CommandParameter).Equals(settingValue));
if (button == null)
continue;
button.IsChecked = true;
// NullReference here
// button.Command.Execute(button.CommandParameter);
}
}
one of the RadioButtons XAML:
<RadioButton Content="None"
Grid.Row="0"
Command="{Binding StampedCommand}"
CommandParameter="None"
Foreground="WhiteSmoke"/>
I feel, the only way i may be able to successfully complete this task is to execute the command directly from my viewmodel. (Which i don't want to do)
Thanks for reading..
To sum up comments at the point when you're calling _setDefaultSelections() bindings have not been updated yet, hence Command is still null, so you have to wait until everything is loaded. You can call _setDefaultSelections during Loaded event
Occurs when the element is laid out, rendered, and ready for interaction.
I am doing some TPL in VS2012, WPF with MVVM. I have a question that I think I know the answer to but wanted to know for sure. Consider this snippet:
TaskCanceller = new CancellationTokenSource();
TaskLoader = Task<object>.Factory.StartNew(() =>
{
//Test the current query
DataRepository dr = new DataRepository(DataMappingSelected);
string test = dr.TestMappingConnection();
if (test.IsNotNullEmpty())
throw new DataConnectionException(test);
//Create the CSV File
DataQueryCsvFile CsvFile = new DataQueryCsvFile();
CsvFile.FileName = IO.Path.GetFileName(FilePath);
CsvFile.FilePath = IO.Path.GetDirectoryName(FilePath);
CsvFile.DataMapping = DataMappingSelected;
CsvFile.DataBrowserQuery = DataQueryHolder;
//Allow for updates to the UI
CsvFile.PageExportComplete += (s, e) =>
{
if (TaskCanceller.IsCancellationRequested)
(s as DataQueryCsvFile).IsSaveCancellationRequested = true;
StatusData = String.Format("{0} of {1} Complete", e.ProgressCount, e.TotalCount);
StatusProgress = (100 * e.ProgressCount / e.TotalCount);
};
CsvFile.SaveFile();
return CsvFile;
});
I have a class DataQueryCsvFile. Its intent is to create a CSV text file based off a passed set of query parameters the results of which can be very large. So the export "paginates" the table produced by the query so it does not blow the users memory. Among its members is an Event called PageExportComplete which is called whenever a "Page" is written to a file - say 1000 records at a time. The code below uses this event to update a progress indicator on the UI.
The progress indicators (StatusData and StatusProgress) are declared in the VM with appropriate Notification to let the View know when they are changed. For example:
public string StatusData
{
get { return _StatusData; }
set { NotifySetProperty(ref _StatusData, value, () => StatusData); }
}
private string _StatusData;
Here is my question - as is, this works very well. But why since I did NOT declare the Task to run or update via the UI thread (FromCurrentSynchronizationContext) in a ContinueWith.
Is it because the MVVM pattern? In other words, because the properties being updated are local to the VM and because they have the notification to update the View and because of the lose coupling via bindings its works? Or am I just lucky due to the circumstances and I should go through the trouble of declaring a ContinueWith to update progress on the UI thread?
UI related stuff can only be updated from UI thread whereas any CLR property binded to UI can be updated from background thread, they don't have thread affinity issue.
Like you posted in your sample, you are only updating View model properties from background thread which is perfectly fine but if you try updating Progress bar text directly, it will fall miserably since progressBar is UI component and can only be updated from UI thread.
Say you have TextBlock binded to Name property in ViewModel class:
<TextBlock x:Name="txt" Text="{Binding Name}"/>
If you try to update text directly, you will get famous thread affinity issue:
Task.Factory.StartNew(() => txt.Text = "From background");
But in case you try to update ViewModel Name property, it will work fine since no UI stuff is access from background thread here:
ViewModelClass vm = DataContext as ViewModelClass;
Task.Factory.StartNew(() => vm.Name = "From background");