Refreshing a binded property from another Thread doesn't work - c#

In a Xamarin project, I am trying to update a label showed in a loading page.
The loading page contains also an activity indicator control and is showed after the user clicks OK on a display alert and the alert is showed after the user clicks on a button in the UI.
The loading page is necessary because some heavy activity happens in background. Every seconds I query the status of the activity and I want this status be showed on a loading page, giving the user some information about the activity. This activity is wrappred in a ICommand in the ViewModel.
When I run the command I want a variable in the viewModel referred by the page be updated.
The command is executed in a Task.Run method otherwise the loading page is not showed untill the command has done.
This is the code:
View code behind
private async void SelfLearningButton_OnClicked(object sender, EventArgs e)
{
var bindingContext = BindingContext as AutomationViewModel;
if (await DisplayAlert("message",
"OK", "Cancel"))
{
bindingContext.IsLearning = true; //a variable that drives the ISVisibleProperty of the loading page
//Task.Run because the loading page must be showed otherwise I wait for the command to be completed (with async/await too)
_ = Task.Run(() => bindingContext.TheCommand.Execute(null));
}
}
View.xaml
<Label IsVisible="{Binding IsLearning}"
TextColor="#f1c00e"
Padding="0, 20, 0, 0"
HorizontalOptions="CenterAndExpand"
VerticalOptions="FillAndExpand"
FontAttributes="Bold"
x:Name="LearningStatus"
Text="{Binding Status}">
View Model
//The variable that must be updated
public string Status
{
private set
{
if (_status != value)
{
_status = value;
NotifyPropertyChanged();
}
}
get { return _status ; }
}
TheCommand= new Command(
async () =>
{
try
{
int i = 0;
while (i < 10000 )
{
++i;
//Doesn't work
Device.BeginInvokeOnMainThread( ()=> _status= i.ToString()); //sorry for the boxing
}
}
catch(Exception e)
{
//
}
finally
{
IsLearning = false;
}
});
When I run the code of the command the BeginInvokeOnMainThread doesn't update the variable. I use BeginInvokeOnMainThread because view items must be updated in the UI thread. At every iteration _status has the same value used for init it, and setter code of the field is not called. What is going wrong with this code?
Thank you

Related

How to make activity indicator wait for a function

I would like to use activity indicator to show that my function is loading. It goes so fast that I can see my activity inndicator but the function have not finished loading
Question : How to use make my activity indicator to false when the functions have really finished to run
Here my code :
public MainPage()
{
InitializeComponent();
On<iOS>().SetUseSafeArea(true);
activityIndicator.IsRunning = true;
MyPageName = "home";
var tasks = new List<Task>();
tasks.Add(Task.Run(async () => {
GetMyLeafLanguage(); //p
}));
tasks.Add(Task.Run(async () => {
if (App.CheckConnection() == true)
{
MyAds(); // doit ĂȘtre apres l'initialisation //
LoadtestPage();
}
}));
Task.WhenAll(tasks);
#endregion My Initial Tasks
OnGoTutorial();
MyNotificationActivate();
activityIndicator.IsRunning = false;
}
The way you wrote it, the tasks had to finish their work, before the page could display. So the page (finally) displayed, but since the work was already done, the activity indicator immediately disappeared.
These functions should run after the page is displayed.
Otherwise, while the tasks run, user will be looking at a blank screen (or at whatever page was showing before you starting creating this page).
Unfortunately, there is no cross-platform event that runs after a page displays for the first time.
Here is one way to let the page display while the background work is being done:
public partial class MyPage : ...
{
protected override void OnAppearing()
{
base.OnAppearing();
BeginWorkWithIndicator();
}
private void BeginWorkWithIndicator()
{
activityIndicator.IsRunning = true;
// The delay gives Xamarin UI a little time, before your background work begins.
// Without this delay, under some circumstances, the page might not show up as quickly.
Device.StartTimer(TimeSpan.FromMilliseconds(100), () => {
OneTimeBackgroundWork();
// Run timer only once.
return false;
});
}
private void OneTimeBackgroundWork()
{
... Long-running work - MUST NOT TOUCH UI ...
// When done.
Device.BeginInvokeOnMainThread(() => {
// NOW MAKE UI CHANGES, based on variables set by work above.
...
activityIndicator.IsRunning = false;
});
}
}

How to keep UWP UI responsive?

I have a UWP app in which one of the pages needs to do three tasks - the first is to load the main content for the page (a collection of 'Binders' objects retrieved from our API) then after that load some other content which are not dependent on the first task in any way.
My page is backed with a ViewModel (I'm using the default Template10 MVVM model) and when the page is navigated to I do this in the VM OnNavigatedToAsync method:
public async override Task OnNavigatedToAsync(object parameter, NavigationMode mode, IDictionary<string, object> state)
{
if (mode == NavigationMode.New || mode == NavigationMode.Refresh)
{
IsBusy = true; //Show progress ring
CreateServices(); //Create API service
//Download binders for board and populate ObservableCollection<Binder>
//This has a cover image and other info I want to show in the UI immediately
await PopulateBinders();
//Get files and calendar events for board
//Here I want to run this on a different thread so it does
//not stop UI from updating when PopulateBinders() is finished
await Task.WhenAll(new[]
{
PopulateBoardFiles(),
PopulateBoardEvents()
});
IsBusy = false;
await base.OnNavigatedToAsync(parameter, mode, state);
return;
}
}
So the main task is PopulateBinders() - this calls the API, returns the data and loads it into an ObservableCollection of Binder. When this has run I want the UI to update it's bindings and show the Binder objects immediately but instead it waits until the two other tasks in the WhenAll Task) have run before updating the UI. (All three of these Tasks are defined as private async Task<bool>...)
I realise I'm missing something basic here - but I thought calling a Task from an async method would allow the UI to update? Since it clearly doesn't how do I refactor this to make my page bindings update after the first method?
I tried Task.Run(() => PopulateBinders()); but it made no difference.
Instead of running it inside OnNavigatedToAsync(), run the asynchronous task when the page is already loaded since you are unintentionally "block" the app to run base.OnNavigatedToAsync() for several seconds untill Task.WhenAll finished running.
Running on loaded event in MVVM can be achieved by implementing Microsoft.Xaml.Interactivity to bind Page.Loaded event with a DelegateCommand class in your viewmodel.
XAML Page ( assuming you are using Prism as your MVVM framework )
<Page ...
xmlns:core="using:Microsoft.Xaml.Interactions.Core"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity">
<interactivity:Interaction.Behaviors>
<core:EventTriggerBehavior EventName="Loaded">
<core:InvokeCommandAction Command="{x:Bind Path=Vm.PageLoaded}" />
</core:EventTriggerBehavior>
</interactivity:Interaction.Behaviors>
</Page>
and inside your viewmodel:
public class PageViewModel : ... //some interface or else
{
public DelegateCommand PageLoaded;
public PageViewModel(...)
{
PageLoaded = new DelegateCommand(async () =>
{
IsBusy = true;
CreateServices();
await PopulateBinders();
await Task.WhenAll(new[]
{
PopulateBoardFiles(),
PopulateBoardEvents()
});
IsBusy = false;
});
}
}
Read more : Binding UWP Page Loading/ Loaded to command with MVVM
I hope this code will help you to update the UI as expected:
public async override Task OnNavigatedToAsync(object parameter, NavigationMode mode, IDictionary<string, object> state)
{
if (mode == NavigationMode.New || mode == NavigationMode.Refresh)
{
IsBusy = true; //Show progress ring
CreateServices(); //Create API service
//Download binders for board and populate ObservableCollection<Binder>
//This has a cover image and other info I want to show in the UI immediately
await PopulateBinders();
await PouplateBoardData();
await base.OnNavigatedToAsync(parameter, mode, state);
return;
}
}
private async void PopulateBoardData()
{
await Task.WhenAll(new[]
{
PopulateBoardFiles(),
PopulateBoardEvents()
});
IsBusy = false;
}

WPF MVVM: Async method does not always update the UI

I bound a button to a command in the view model that triggers a long-running operation. The operation is run in a separate task. Before and after the task, a property is set to reflect it running in the UI.
My problem now is: Sometimes, when I click the button, the change of the property BEFORE the long operation runs is registered (i.e., the UI changes accordingly), but when the task finishes, it is not immediately. I have to click somewhere in the UI to make the change to be reflected.
Here is how I did it (using MVVM Light):
_longRunningOpCommand = new RelayCommand(async () => await DoLongRunningThingAsync(), CanDoLongRunningThing);
// ...
public ICommand LongRunningOpCommand { get { return _longRunningOpCommand; } }
// ...
private async Task DoLongRunningThingAsync() {
try {
IsDoingStuff = true; // triggers a PropertyChangeEvent, is bound to UI
await Task.Factory.StartNew(async () => {
await _something.DoLenghtyOperation();
}, TaskCreationOptions.LongRunning);
} finally {
IsDoingStuff = false;
}
}
Now, the UI elements update as soon as IsDoingStuff is becoming false as they should, but they are not updated when IsDoingStuff is becoming true. I have to click into the UI to have them updated.
I think I'm doing something wrong, but can't track down what.

UWP C# pass a variable to another thread

Im new to programming, and after 7 days of searching I couldn't find a solution.
My "MainPage" opens, and I click to Open a "SecondaryPage" which opens on a new thread window with core Dispatcher. My "MainPage" has a button Click event which updates "TextBlock1". What I cant achieve is to pass this value to my "SecondPage" "TextBlock2".
MainPage.xaml below.
<StackPanel>
<Button Click="Button_Click_NewWindow" Content="Start a new window" FontSize="30"/>
<TextBlock Name="Textblock1" Text="Empty" FontSize="30"/>
<Button Click="Button_Click_Add" Content="+1" FontSize="30"/>
</StackPanel>
Code Behind SecondPage.xaml
<StackPanel>
<TextBlock Name="Textblock2" Text="Empty" FontSize="30"/>
</StackPanel>
MainPage.xaml.cs
namespace MulitViewV1
{
public sealed partial class MainPage : Page
{
public int T1G = 0;
public MainPage()
{
this.InitializeComponent();
}
private async void Button_Click_NewWindow(object sender, RoutedEventArgs e)
{
CoreApplicationView newView = CoreApplication.CreateNewView();
int newViewId = 0;
await newView.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
Frame frame = new Frame();
frame.Navigate(typeof(SecondPage), null);
Window.Current.Content = frame;
// You have to activate the window in order to show it later.
Window.Current.Activate();
newViewId = ApplicationView.GetForCurrentView().Id;
});
bool viewShown = await ApplicationViewSwitcher.TryShowAsStandaloneAsync(newViewId);
}
private async void Button_Click_Add(object sender, RoutedEventArgs e)
{
T1G = T1G + 1;
Textblock1.Text = T1G.ToString();
await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
//UI code here
// NOT WORKING error "TextBlock2" does not exist in the current context
Textblock2.Text = T1G.ToString();
});
}
}
}
I see the Dispatcher is working to my MainPage View, but I cant figure out code to direct it to my SecondPage "newViewID".
There's no need to call Dispatcher all the time. You button clicks are coming to you on the UI thread so the handlers are also going to be on the UI thread.
A way to think about it is that you're always on the UI thread unless you're doing something with *Async. If you await async stuff, you're good. This is not true all of the time, but it's a place to start and handle the problems you might encounter as the come.
Generally you only want a single Frame in your application. Multiple Frames (and multiple windows) is an advanced topic; save it for later.
By default, you UWP app will contain a Frame. Pages are hosted in this frame. The frame can navigate between the pages it hosts. Think of the frame as you browser window, and the pages are like web pages.
You can get the Frame by getting the Frame property on the page. To navigate to a new page you do this:
private async void Button_Click_NewWindow(object sender, RoutedEventArgs e)
{
Frame.Navigate(typeof(SecondPage), null);
}
Next you want to change the text of TextBlock2. TextBlock2 is located on SecondPage, but your code is in MainPage, which has no TextBlock2, and the compiler is telling you that TextBlock2 isn't a thing in MainPage.
Inside your SecondPage.xaml.cs you are however able to get to TextBlock2 as it is present there. Remember though, MainPage will not be visible after navigation to SecondPage (just as you can't see two web pages at the same time when navigating from one to another).
In your Button_Click_NewWindow where you are calling
frame.Navigate(typeof(SecondPage), null);
instead of null, pass the text from TextBlock like below
string TextBoxText = Textblock1.Text;
frame.Navigate(typeof(SecondPage), TextBoxText);
And in your SecondPage, Override OnNavigated to method and you can get the Text like below.
string PassedText=e.parameter.ToString();
and you can assign this to your TextBlock as below
Textblock2.Text=PassedText;
Update
Change your Button_Click_NewWindow to below and add the two variables on top.
int newViewId = 0;
Window SecondWindow;
private async void Button_Click_NewWindow(object sender, TappedRoutedEventArgs e)
{
string TextBoxText = Textblock1.Text;
int mainViewId = ApplicationView.GetApplicationViewIdForWindow(CoreApplication.MainView.CoreWindow);
if (newViewId == 0)
{
CoreApplicationView newCoreView = CoreApplication.CreateNewView();
ApplicationView newAppView = null;
await newCoreView.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
newAppView = ApplicationView.GetForCurrentView();
Window.Current.Content = new Frame();
(Window.Current.Content as Frame).Navigate(typeof(SecondPage), TextBoxText);
Window.Current.Activate();
SecondWindow = Window.Current;
});
newViewId = newAppView.Id;
}
else
{
await SecondWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
SecondWindow.Content = new Frame();
(SecondWindow.Content as Frame).Navigate(typeof(SecondPage), TextBoxText);
SecondWindow.Activate();
});
}
await ApplicationViewSwitcher.TryShowAsStandaloneAsync(newViewId, ViewSizePreference.UseHalf, mainViewId, ViewSizePreference.UseHalf);
}
And your output

Busy indicator not working

I'm building an MVVM Light WPF app in Visual Studio 2015. It has a user control that contains a WindowsFormsHost with a ReportViewer for SQL Server Reporting Services local report. A button in ReportView.xaml calls a command, which in turn sends a message to the code-behind of MasterListView.xaml to generate the report.
Here's the command called by the button in ReportViewModel.cs:
public ICommand RunReportRelayCommand =>
new RelayCommand(async ()=> { await RunReport(); });
private async Task RunReport()
{
try
{
IsBusy = true;
await Task.Run(() =>
{
Messenger.Default.Send(true, "RunMasterListReport");
});
}
finally
{
IsBusy = false;
}
}
Here's the definition of IsBusy property in ReportViewModel.cs:
private bool _isBusy;
public bool IsBusy
{
get { return _isBusy; }
set
{
if (value == _isBusy) return;
_isBusy = value;
RaisePropertyChanged();
}
}
The same view, ReportView.xaml, which contains the button calling the above command also contains the following Extended WPF Toolkit Busy Indicator:
<UserControl>
<UserControl.Resources>
<DataTemplate x:Key="MasterListViewTemplate">
<view:MasterListView />
</DataTemplate>
</UserControl.Resources>
<xctk:BusyIndicator IsBusy="{Binding IsBusy}">
<StackPanel>
<!-- Other XAML here -->
<ContentControl ContentTemplate="{StaticResource MasterListViewTemplate}" />
</StackPanel>
</xctk:BusyIndicator>
</UserControl>
Then in the MasterListView.cs code-behind, we have this:
public partial class MasterListView : UserControl
{
public MasterListView()
{
InitializeComponent();
Messenger.Default.Register<bool>(this, "RunMasterListReport", RunMasterListReport);
}
public async void RunMasterListReport(bool val)
{
await Task.Run(() =>
{
var dataSet = new DrugComplianceDataSet();
dataSet.BeginInit();
ReportViewer.ProcessingMode = ProcessingMode.Local;
ReportViewer.LocalReport.ShowDetailedSubreportMessages = true;
ReportViewer.LocalReport.DataSources.Clear();
var dataSource = new ReportDataSource
{
Name = "MasterListRandomDataSet",
Value = dataSet.MasterListRandom
};
ReportViewer.LocalReport.DataSources.Add(dataSource);
ReportViewer.LocalReport.ReportEmbeddedResource = "MasterListRandom.rdlc";
dataSet.EndInit();
var adapter = new MasterListRandomTableAdapter { ClearBeforeFill = true }
.Fill(dataSet.MasterListRandom);
Dispatcher.Invoke((MethodInvoker)(() => { ReportViewer.RefreshReport(); }));
});
}
}
However, the busy indicator doesn't trigger, though the report does show after 5 seconds or so. What am I doing wrong? Thanks.
You got a whole soup of wat in there. Things like
await Task.Run(() =>
suggest you don't really understand how async/await works. I'd step down and find some nice documentation to read. Also, you appear to be doing all your work on the UI thread.
Dispatcher.Invoke((MethodInvoker)(() => { ReportViewer.RefreshReport(); }));
You shouldn't be touching a dispatcher in a background worker unless you're updating the UI. It appears you're offloading the actual work you intend to do in a background thread (refreshing the report) on the UI thread.
Maybe your report viewer HAS to run in the UI thread. If that's so (maybe it was designed a while ago and doesn't take advantage of multitasking) there isn't much you can do about this situation. Your UI will be locked while it's processing.
If all your long-running work has to run on the UI thread, then strip out all that nonsense. Before kicking off your work, update IsBusy, then offload execution of ReportViewer.RefreshReport() onto the Dispatcher, but using a low priority DispatcherPriority, so that it runs after the UI updates and shows that it is busy. Your UI will be frozen during processing, but at least you'll give the user an indication of what's going on.

Categories