Open tpitman opened 8 years ago
This is because you are trying to update the UI outside of the UI main thread.
Try using Application.Instance.Invoke()
or Application.Instance.AsyncInvoke()
from the background thread to update the UI.
E.g. updating your model to something like this would fix it, though it's up to you where you want to put the call to Invoke, perhaps inside your NotifyOfPropertyChange() method, or perhaps any call from the background thread to update the model.
public int Progress
{
get { return _Progress; }
set
{
_Progress = value;
Application.Instance.Invoke(() => NotifyOfPropertyChange (() => Progress));
}
}
You can also use SynchronizationContext.Current.Send/Post
if you want to be UI agnostic in your view model, but you need to get the current context outside of your background thread and keep a reference to it. For example:
public ProgressViewModel ()
{
var ctx = SynchronizationContext.Current;
Task.Run (async () => {
while (true)
{
await Task.Delay (500);
ctx.Send(s => Progress++, null);
Debug.WriteLine (string.Format ("Progress: {0}", Progress));
if (Progress >= 100)
break;
}
});
}
or, better yet, use async/await to its fullest, where only the long running code runs in the background thread, but all the other code runs in the main thread:
async Task StartLongTask()
{
while (true)
{
await Task.Delay (500);
Progress++;
Debug.WriteLine (string.Format ("Progress: {0}", Progress));
if (Progress >= 100)
break;
}
}
Thank you for the reply. I was suspecting this might be the case, but didn't want to muddy my ViewModel code with Eto specific stuff. In fact my ViewModel code is in a library that doesn't even know about Eto.
So in the example I changed my startup code to do this:
var vm = new ProgressViewModel ();
Task.Run (async () => {
await Task.Delay (5000);
Application.Instance.AsyncInvoke (() => vm.UpdateProgress ());
});
new Application (Platforms.XamMac2).Run (new ProgressView (vm));
And then in the vm I changed it to be like this:
public async void UpdateProgress ()
{
while (true)
{
await Task.Delay (500);
Progress++;
Debug.WriteLine (string.Format ("Progress: {0}", Progress));
if (Progress >= 100)
break;
}
}
This works, so now I need to adapt it to my main app.
Thanks!
As I am working with this a question has come up. Why doesn't the ETO binding framework ensure that all requests are done on the Application thread? This would make things so much easier because then my ViewModel would not have to have specific knowledge of Eto or the Application object...
I did some digging around and found a place in ObjectBinding.cs that handles the change event.
If I change the following:
void HandleChangedEvent(object sender, EventArgs e)
{
OnDataValueChanged (e);
}
to:
void HandleChangedEvent(object sender, EventArgs e)
{
Application.Instance.AsyncInvoke (() => {
OnDataValueChanged (e);
});
}
Then everything works the way I like. Please let me know your thoughts on this change. Would this handle all binding updates in a way that is good and correct or will this cause problems?
Is there a reason this is not already part of the code base?
Should I use Invoke instead of AsyncInvoke?
Thank you for taking the time to consider this. Maybe this could be an option setting if it isn't appropriate for all situations?
Just trying to understand...
Using Invoke()
may be preferred vs AsyncInvoke()
as it will execute directly on the thread if it is called from the main thread. AsyncInvoke()
will always schedule it for later.
I'm not sure of adding overhead to use Invoke()
by default would be preferred, as the common pattern of a View Model is to have it 'run' in the same thread as the UI.
This would also confuse things with two-way binding - it'd get/set the ViewModel property in the UI thread when the UI is poked by the user, but when you are updating the ViewModel in the background thread you'll have to add appropriate locking so that the property can be read and written at potentially the exact same time. It makes more sense to me to have your thread interaction logic in the same place and take appropriate precautions as you would with any multi-threaded app... or, use async/await which is a much more robust mechanism and you don't have to worry about any of this.
However, adding an opt-in for this on each binding may be a good way to make this easier in this particular scenario.. for example, perhaps something like
Progress.BindDataContext(c => c.Value, (ProgressViewModel m) => m.Progress).WithInvoke();
I have a project with a progress bar in it. I have a secondary process running using Task to update the progress bar using binding and notify property change with a view model.
The progress bar does not update unless you change focus to another app and come back.
My real app runs on Linux and Mac and it does the same thing on both.
I have created a simple Xamarin.Mac application that demonstrates it.
You can download the zip of it here:
https://drive.google.com/file/d/0B3P6IntD-KkaNnNVdUZFcDRmV28/view?usp=sharing
Can someone tell me why it isn't updating and how to fix it?