Skip to content

Instantly share code, notes, and snippets.

@softlion
Last active February 26, 2016 13:49
Show Gist options
  • Save softlion/d73a11abac5348f40569 to your computer and use it in GitHub Desktop.
Save softlion/d73a11abac5348f40569 to your computer and use it in GitHub Desktop.
ShowViewModelAsync and InvokeOnMainThreadAsync for MvvmCross. Also adds OnBack/OnBack2 methods on viewmodels.
public class BaseViewModel : MvxViewModel, IBackViewModel
{
#region InvokeOnMainThreadAsync
protected Task InvokeOnMainThreadAsync(Action action)
{
var tcs = new TaskCompletionSource<bool>();
if (Dispatcher != null)
{
Dispatcher.RequestMainThreadAction(() =>
{
action();
tcs.TrySetResult(true);
});
}
else
tcs.TrySetCanceled();
return tcs.Task;
}
protected Task<T> InvokeOnMainThreadAsync<T>(Func<T> action)
{
var tcs = new TaskCompletionSource<T>();
if (Dispatcher != null)
{
Dispatcher.RequestMainThreadAction(() =>
{
tcs.TrySetResult(action());
});
}
else
tcs.TrySetCanceled();
return tcs.Task;
}
#endregion
#region Infrastructure to handle return values from a viewmodel
protected class ReturnedResult<T>
{
public T Result;
public Guid MessageId;
}
public const string ReturnedMessageIdKey = "returnedMessageId";
private readonly ConcurrentDictionary<Guid, MvxSubscriptionToken> tokens = new ConcurrentDictionary<Guid, MvxSubscriptionToken>();
/// <summary>
/// Store viewmodel parameters
/// </summary>
private IMvxBundle viewModelParameters;
protected void SetReturnResult<T>(T result)
{
Guid messageId;
string messageIdstring;
if (viewModelParameters == null || viewModelParameters.Data == null || !viewModelParameters.Data.TryGetValue(ReturnedMessageIdKey, out messageIdstring) || !Guid.TryParse(messageIdstring, out messageId))
throw new Exception(String.Format("Can not find {0} in the viewmodel's parameters {1}. Did you override InitFromBundle without calling base ?", ReturnedMessageIdKey, this.GetType().Name));
SetReturnResult(messageId, result);
}
/// <summary>
/// For unit tests
/// </summary>
protected void SetReturnResult<T>(Guid messageId, T result)
{
var messenger = Mvx.Resolve<IMvxMessenger>();
messenger.Publish(new MvxMessage<ReturnedResult<T>>(this, new ReturnedResult<T> { Result = result, MessageId = messageId }));
}
protected void Close<T>(T result)
{
if(result is IMvxViewModel)
base.Close((IMvxViewModel)result);
else
{
SetReturnResult(result);
base.Close(this);
}
}
/// <summary>
/// TViewModel must call SetReturnResult in any method that closes the viewmodel.
/// This includes BackCommand (OnBack), BackCommand2 (OnBack2), this.Close() and calls to ShowViewModel which replaces the back stack.
///
/// NOTE: if the app is tombstoned, the task is lost.
/// </summary>
protected Task<TResult> ShowViewModelAsync<TViewModel, TResult>(object parameterValuesObject = null, IMvxBundle presentationBundle=null)
where TViewModel: IMvxViewModel
{
var tcs = new TaskCompletionSource<TResult>();
var messenger = Mvx.Resolve<IMvxMessenger>();
var messageId = Guid.NewGuid();
var token = messenger.Subscribe<MvxMessage<ReturnedResult<TResult>>>(message =>
{
if (message.Data.MessageId != messageId)
return;
MvxSubscriptionToken token2;
if (tokens.TryRemove(messageId, out token2))
token2.Dispose();
else
Mvx.Error("Result message from {0} with same id received 2 times. Remove your multiple calls to ReturnResult in {0}.", typeof(TViewModel).Name);
tcs.TrySetResult(message.Data.Result);
});
tokens.TryAdd(messageId, token);
var parameters = parameterValuesObject.ToSimplePropertyDictionary();
if(parameters.ContainsKey(ReturnedMessageIdKey))
throw new ArgumentException(String.Format("parameters can not include a {0} key when using a return value", ReturnedMessageIdKey), "parameterValuesObject");
parameters.Add(ReturnedMessageIdKey, messageId.ToString());
ShowViewModel<TViewModel>(parameters, presentationBundle);
return tcs.Task;
}
protected override void InitFromBundle(IMvxBundle parameters)
{
viewModelParameters = parameters;
base.InitFromBundle(parameters);
}
#endregion
#region handle "back" events
/*
Handling of back events requires the use of a BaseView<TViewModel> class in android and iOS.
This BaseView class will correctly bind mvvm actions BackCommand and BackCommand2
*/
protected CancellationTokenSource BackCancel = new CancellationTokenSource();
/// <summary>
/// A cancellable back command (Android, and some ios screens where back is replaced)
/// </summary>
public virtual ICommand BackCommand2
{
get
{
return new MvxCommand(async () =>
{
Mvx.Trace("BackCommand2 detected on {0}", GetType().Name);
if (await OnBack2().ConfigureAwait(false))
{
BackCancel.Cancel();
Close(this);
}
else
Mvx.Trace("BackCommand2 NOT cancelled on {0}", GetType().Name);
});
}
}
/// <summary>
/// A non cancellable back command (ios back navigation button)
/// </summary>
public virtual ICommand BackCommand
{
get
{
return new MvxCommand(async () =>
{
Mvx.Trace("BackCommand detected on {0}", GetType().Name);
if (await OnBack().ConfigureAwait(false))
{
BackCancel.Cancel();
}
else
Mvx.Trace("BackCommand NOT cancelled on {0}", GetType().Name);
});
}
}
/// <summary>
/// A cancellable back command (Android, and some ios screens where back is replaced). Return false to prevent the back command.
/// </summary>
/// <returns></returns>
public virtual Task<bool> OnBack2()
{
return Task.FromResult(true);
}
/// <summary>
/// A non cancellable back command (ios back navigation button). Return false to prevent the "BackCancel" cancellationtoken from being cancelled.
/// </summary>
/// <returns></returns>
public virtual Task<bool> OnBack()
{
return Task.FromResult(true);
}
#endregion
}
[Preserve(AllMembers = true)]
[Activity(ScreenOrientation = ScreenOrientation.Portrait)]
public class CustomBaseActivity<T> : BaseActivity where T : class, IBackViewModel
{
private CancellationTokenSource backPressedSource = new CancellationTokenSource();
protected new T ViewModel => base.ViewModel as T;
protected CancellationToken BackPressed { get { return backPressedSource.Token; } }
public ICommand BackPressedCommand { get; set; }
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
BindBackPressed();
backPressedSource.Token.Register(() =>
{
var backPressedCommand = BackPressedCommand;
backPressedCommand?.Execute(null);
});
}
public override bool OnOptionsItemSelected(IMenuItem item)
{
switch (item.ItemId)
{
//Action bar: home button will back
case Android.Resource.Id.Home:
OnBackPressed();
return true;
}
return base.OnOptionsItemSelected(item);
}
public override void OnBackPressed()
{
backPressedSource.Cancel();
backPressedSource = new CancellationTokenSource();
backPressedSource.Token.Register(() =>
{
var backPressedCommand = BackPressedCommand;
if (backPressedCommand != null)
backPressedCommand.Execute(null);
});
}
/// <summary>
/// Required to put that here for correct linking in release mode
/// </summary>
protected void BindBackPressed()
{
var set = this.CreateBindingSet<CustomBaseActivity<T>, IBackViewModel>();
set.Bind(this).For(s => s.BackPressedCommand).To(vm => vm.BackCommand2);
set.Apply();
}
}
public class CustomBaseView<T> : MvxViewController where T: MvxViewModel, IBackViewModel
{
private readonly CancellationTokenSource backPressedSource = new CancellationTokenSource();
protected CancellationToken BackPressed => backPressedSource.Token;
public ICommand BackPressedCommand { get; set; }
protected new T ViewModel
{
get { return base.ViewModel as T; }
set { base.ViewModel = value; }
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
backPressedSource.Token.Register(() =>
{
var backPressedCommand = BackPressedCommand;
backPressedCommand?.Execute(null);
});
var set = this.CreateBindingSet<MvxSignupBaseView<T>, T>();
set.Bind(this).For(t => t.BackPressedCommand).To(vm => vm.BackCommand);
set.Apply();
}
/// <summary>
/// Not called if vc is popped up
/// </summary>
public override void WillMoveToParentViewController(UIViewController parent)
{
if (parent == null)
{
#if DEBUG
Mvx.Trace("{0}: back detected on WillMoveToParentViewController", GetType().Name);
#endif
backPressedSource.Cancel();
}
base.WillMoveToParentViewController(parent);
}
public override void DidMoveToParentViewController(UIViewController parent)
{
if (parent == null)
{
#if DEBUG
Mvx.Trace("{0}: back detected on DidMoveToParentViewController", GetType().Name);
#endif
backPressedSource.Cancel();
}
base.DidMoveToParentViewController(parent);
}
}
public interface IBackViewModel : IMvxViewModel
{
ICommand BackCommand { get; }
ICommand BackCommand2 { get; }
}
@lothrop
Copy link

lothrop commented Jan 27, 2016

The 2 suffix reminds me a little of the Win32 API.

@softlion
Copy link
Author

👍 yeah i'm not proud of it. There should be an easy way to merge OnBack and OnBack2.

@roubachof
Copy link

anyway good job ! I like these extensions 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment