Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Update UI thread from portable class library

I have an MVVM Cross application running on Windows Phone 8 which I recently ported across to using Portable Class Libraries.

The view models are within the portable class library and one of them exposes a property which enables and disables a PerformanceProgressBar from the Silverlight for WP toolkit through data binding.

When the user presses a button a RelayCommand kicks off a background process which sets the property to true which should enable the progress bar and does the background processing.

Before I ported it to a PCL I was able to invoke the change from the UI thread to ensure the progress bar got enabled, but the Dispatcher object isn't available in a PCL. How can I work around this?

Thanks

Dan

like image 768
Dan Sadler Avatar asked Jan 20 '13 17:01

Dan Sadler


3 Answers

All the MvvmCross platforms require that UI-actions get marshalled back on to the UI Thread/Apartment - but each platform does this differently....

To work around this, MvvmCross provides a cross-platform way to do this - using an IMvxViewDispatcherProvider injected object.

For example, on WindowsPhone IMvxViewDispatcherProvider is provided ultimately by MvxMainThreadDispatcher in https://github.com/slodge/MvvmCross/blob/vnext/Cirrious/Cirrious.MvvmCross.WindowsPhone/Views/MvxMainThreadDispatcher.cs

This implements the InvokeOnMainThread using:

    private bool InvokeOrBeginInvoke(Action action)
    {
        if (_uiDispatcher.CheckAccess())
            action();
        else
            _uiDispatcher.BeginInvoke(action);

        return true;
    }

For code in ViewModels:

  • your ViewModel inherits from MvxViewModel
  • the MvxViewModel inherits from an MvxApplicationObject
  • the MvxApplicationObject inherits from an MvxNotifyPropertyChanged
  • the MvxNotifyPropertyChanged object inherits from an MvxMainThreadDispatchingObject

MvxMainThreadDispatchingObject is https://github.com/slodge/MvvmCross/blob/vnext/Cirrious/Cirrious.MvvmCross/ViewModels/MvxMainThreadDispatchingObject.cs

public abstract class MvxMainThreadDispatchingObject
    : IMvxServiceConsumer<IMvxViewDispatcherProvider>
{
    protected IMvxViewDispatcher ViewDispatcher
    {
        get { return this.GetService().Dispatcher; }
    }

    protected void InvokeOnMainThread(Action action)
    {
        if (ViewDispatcher != null)
            ViewDispatcher.RequestMainThreadAction(action);
    }
}

So... your ViewModel can just call InvokeOnMainThread(() => DoStuff());


One further point to note is that MvvmCross automatically does UI thread conversions for property updates which are signalled in a MvxViewModel (or indeed in any MvxNotifyPropertyChanged object) through the RaisePropertyChanged() methods - see:

    protected void RaisePropertyChanged(string whichProperty)
    {
        // check for subscription before going multithreaded
        if (PropertyChanged == null)
            return;

        InvokeOnMainThread(
            () =>
                {
                    var handler = PropertyChanged;

                    if (handler != null)
                        handler(this, new PropertyChangedEventArgs(whichProperty));
                });
    }

in https://github.com/slodge/MvvmCross/blob/vnext/Cirrious/Cirrious.MvvmCross/ViewModels/MvxNotifyPropertyChanged.cs


This automatic marshalling of RaisePropertyChanged() calls works well for most situations, but can be a bit inefficient if you Raise a lot of changed properties from a background thread - it can lead to a lot of thread context switching. It's not something you need to be aware of in most of your code - but if you ever do find it is a problem, then it can help to change code like:

 MyProperty1 = newValue1;
 MyProperty2 = newValue2;
 // ...
 MyProperty10 = newValue10;

to:

 InvokeOnMainThread(() => {
      MyProperty1 = newValue1;
      MyProperty2 = newValue2;
      // ...
      MyProperty10 = newValue10;
 });

If you ever use ObservableCollection, then please note that MvvmCross does not do any thread marshalling for the INotifyPropertyChanged or INotifyCollectionChanged events fired by these classes - so it's up to you as a developer to marshall these changes.

The reason: ObservableCollection exists in the MS and Mono code bases - so there is no easy way that MvvmCross can change these existing implementations.

like image 110
Stuart Avatar answered Jan 15 '23 11:01

Stuart


If you don't have access to the Dispatcher, you can just pass a delegate of the BeginInvoke method to your class:

public class YourViewModel
{
    public YourViewModel(Action<Action> beginInvoke)
    {
        this.BeginInvoke = beginInvoke;
    }

    protected Action<Action> BeginInvoke { get; private set; }

    private void SomeMethod()
    {
        this.BeginInvoke(() => DoSomething());
    }
}

Then to instanciate it (from a class that has access to the dispatcher):

var dispatcherDelegate = action => Dispatcher.BeginInvoke(action);

var viewModel = new YourViewModel(dispatcherDelegate);

Or you can also create a wrapper around your dispatcher.

First, define a IDispatcher interface in your portable class library:

public interface IDispatcher
{
    void BeginInvoke(Action action);
}

Then, in the project who has access to the dispatcher, implement the interface:

public class DispatcherWrapper : IDispatcher
{
    public DispatcherWrapper(Dispatcher dispatcher)
    {
        this.Dispatcher = dispatcher;
    }

    protected Dispatcher Dispatcher { get; private set; }

    public void BeginInvoke(Action action)
    {
        this.Dispatcher.BeginInvoke(action);
    }
}

Then you can just pass this object as a IDispatcher instance to your portable class library.

like image 21
Kevin Gosse Avatar answered Jan 15 '23 11:01

Kevin Gosse


Another option that could be easier is to store a reference to SynchronizationContext.Current in your class's constructor. Then, later on, you can use _context.Post(() => ...) to invoke on the context -- which is the UI thread in WPF/WinRT/SL.

class MyViewModel
{
   private readonly SynchronizationContext _context;
   public MyViewModel()
   {
      _context = SynchronizationContext.Current.
   }

   private void MyCallbackOnAnotherThread()
   {
      _context.Post(() => UpdateTheUi());
   }
}
like image 35
Claire Novotny Avatar answered Jan 15 '23 12:01

Claire Novotny