Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensuring Start completed before initializing Xamarin Forms #2674

Merged
merged 40 commits into from
Mar 18, 2018

Conversation

nickrandolph
Copy link
Contributor

@nickrandolph nickrandolph commented Mar 10, 2018

✨ What kind of change does this PR introduce? (Bug fix, feature, docs update...)

bug fix

⤵️ What is the current behavior?

Start calls asynchronous navigation method. If this method invokes a method that takes some time to complete, it is possible for XF to crash because the page attempts to load XF before MainPage is set

🆕 What is the new behavior (if this is a feature change)?

Setup blocks after calling Start until the async navigation completes

💥 Does this PR introduce a breaking change?

No

🐛 Recommendations for testing

Run playground forms

📝 Links to relevant issues/docs

Related PR (#2665) - this PR should no longer be required after the changes in this PR which ensure the current application is set whenever the FormsApplicaiton property is accessed.
Related Issues (these need to be tested against these changes)
#2622
#2624

🤔 Checklist before submitting

@martijn00 martijn00 added this to the 6.0.0 milestone Mar 10, 2018
namespace MvvmCross.ViewModels
{
public interface IMvxAppStart
{
void Start(object hint = null);

Task WaitForStart();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rather make void Start(object hint = null); a Task. That might be difficult to make async all the way, so maybe another idea is to create a IMvxFormsAppStart for now and make it a Task there or add WaitForStart there. Also we could cast it to make it available.

martijn00
martijn00 previously approved these changes Mar 10, 2018
@nickrandolph
Copy link
Contributor Author

@martijn00 can you take another look - somehow in making changes I ended up re-surfacing the nullreferenceexception when calling LoadApplication for Xamarin Forms on Android. Any suggestions on why this might be happening?

Copy link
Contributor

@nmilcoff nmilcoff left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really sure this is the best approach to solve the issue... We're plugging into the framework some async void methods in critical places. Wouldn't it be better to just mention that you shouldn't run any long operations at startup? Because the same recommendation applies to traditional Xamarin.

try {
await NavigationService.Navigate<TViewModel>();
_startTaskNotifier = MvxNotifyTask.Create(async ()=> await NavigationService.Navigate<TViewModel>());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The try/catch here won't work, as MvxNotifyTask doesn't re-throw exceptions. You can use the newly added onException callback for that.

@@ -31,7 +31,6 @@ private MvxNotifyTask(Task task, Action<Exception> onException)
{
try
{
await Task.Yield();
Copy link
Contributor

@nmilcoff nmilcoff Mar 11, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this Task.Yield removed? It was added in purpose.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two things:

  • my understanding is that calling Yield will cause an unnecessary latency in the task being invoked
  • if it is there for a reason (eg best practice or some other guidance) we should add it back with an appropriate comment and/or reference

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the point of the Yield call to force the code to run asynchronously? If so, this should be an option that's exposed when creating the MvxNotifyTask, rather than being the default.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was added here #2642 (also I don't see any mention to this change on this PR other than the change in the source code).

You're right about why is it there. The problem is that if the code isn't run asynchronously, there is a (quite high) chance the exception will be thrown before MvxNotifyTask starts monitoring the task. This is highly unexpected when you are delegating all error handling responsibilities to the object.

I agree this adds a (very) small latency to all operations running inside of it, but MvxNotifyTask is not just common async/await.

InitializeForms(bundle);
}

public virtual void InitializeForms(Bundle bundle)
protected virtual async Task StartSetup()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this method should be called something like RunAppStart, the Setup keyword is a bit confusing in this context

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. Will fix

@@ -48,6 +49,8 @@ public IMvxViewModel ViewModel
}
}

private MvxNotifyTask _startTaskNotifier;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This member isn't used anywhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Will clean it up now that it’s no longer used

@nickrandolph
Copy link
Contributor Author

@nmilcoff The point of these changes are to ensure that exceptions aren't lost deep within mvx where developers can't intercept them. We also want to give developers a known point where they can run code after the first navigation has completed. Both of these scenarios are difficult to accomplish as the Start method is async void with no way of capturing exceptions, nor waiting for it to complete.
The ideal solution is to change to StartAsync but that's a much larger piece of work. I'm looking at this as an interim solution with a view to switching to StartAsync in a future revision.

@@ -82,6 +82,8 @@ where type.IsConventional()
{
try
{
if (pair.Type.ContainsGenericParameters) continue;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this do?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It prevents exceptions being raised when the Type is a generic - In the playground forms droid sample it is throwing an exception.

try {
await NavigationService.Navigate<TViewModel>();
NavigationService.Navigate<TViewModel>().Wait();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think using GetAwaiter().GetResult() is prefered over .Wait()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

return base.FinishedLaunching(app, options);
}

protected override MvxIosSetup CreateSetup(IMvxApplicationDelegate applicationDelegate, UIWindow window)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably move this into the normal appdelegate as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@@ -31,11 +33,45 @@ public override UIWindow Window

public override bool FinishedLaunching(UIApplication uiApplication, NSDictionary launchOptions)
{
Window = new UIWindow(UIScreen.MainScreen.Bounds);

UINavigationBar.Appearance.BarTintColor = UIColor.FromRGB(65, 105, 225);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be in here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no - I've removed these lines

{
base.OnCreate(bundle);

InternalOnCreate(bundle);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InternalOnCreate sounds a bit weird as name

Copy link
Contributor

@nmilcoff nmilcoff left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks really good! Only a few comments about code style we better change before getting this IN

@@ -82,8 +82,7 @@ protected virtual void OnViewModelSet()

protected override void AttachBaseContext(Context @base)
{
if (this is IMvxAndroidSplashScreenActivity)
{
if (this is IMvxAndroidSplashScreenActivity) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{ should be placed in a new line

_formsApplication = _formsApplication ?? CreateFormsApplication();
_formsApplication = CreateFormsApplication();
}
if (Application.Current != _formsApplication) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{ should be placed in a new line

@@ -84,8 +84,7 @@ public override void SetContentView(int layoutResId)

protected override void AttachBaseContext(Context @base)
{
if (this is IMvxAndroidSplashScreenActivity)
{
if (this is IMvxAndroidSplashScreenActivity) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{ should be placed in a new line

@@ -53,8 +54,7 @@ protected Application FormsApplication
{
get
{
if (_formsApplication == null)
{
if (_formsApplication == null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{ should be placed in a new line

public virtual void InitializeForms(Bundle bundle)
{
if (FormsApplication.MainPage != null)
{
if (!Xamarin.Forms.Forms.IsInitialized) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{ should be placed in a new line

public static TSetup CreateSetup<TSetup>(Assembly assembly, params object[] parameters) where TSetup : MvxSetup
{
var setupType = FindSetupType<TSetup>(assembly);
if (setupType == null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{ should be placed in a new line

throw new MvxException("Could not find a Setup class for application");
}

try {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{ should be placed in a new line

{
if (rootFrame.Content == null) {
Setup = CreateSetup(rootFrame, activationArgs, nameof(Suspend));
if (RootFrame.Content == null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{ should be placed in a new line

{
// Check whether Start has commenced, and return if it has
if (Interlocked.CompareExchange(ref startHasCommenced, 1, 0) == 1) return;

if (hint != null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{ should be placed in a new line

if (hint != null) {
MvxLog.Instance.Trace("Hint ignored in default MvxAppStart");
}

try {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{ should be placed in a new line


protected virtual void CreateSetup(IMvxApplicationDelegate applicationDelegate, UIWindow window)
{
var setupType = FindSetupType();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not be doing this - reflection is slow. We can have this as a fallback but the need the ability for the setup class to be specified (see UWP)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is outdated code. Look at the whole PR.

martijn00
martijn00 previously approved these changes Mar 12, 2018
Cheesebaron
Cheesebaron previously approved these changes Mar 13, 2018
Updated Android to support new setup logic
Copy link
Contributor

@nmilcoff nmilcoff left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of it looks really good now!

I'm just not sure if there will be some side effects caused by maintaining a static instance of Setup... especially considering users will be adding their own stuff to it

@nickrandolph
Copy link
Contributor Author

For the most part Setup was treated as a singleton from what I can see. Particularly on Android where it was wrapped in a singleton class to call initialise. I should have sometime over the weekend if any further changes are needed

@nickrandolph
Copy link
Contributor Author

@martijn00 do you think this PR is ready, or are there other adjustments to be made? It'd be great to get this merged so we can get a beta out with these changes. I'll then start to work through the issues to see what else we can get resolved before v6 is done

Copy link
Contributor

@nmilcoff nmilcoff left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like the new shape Setup has! I think this after all this it will be easier to make Mvx work on iOS extensions for example.

I left some comments around, please check them.

Also we'll need to update docs & some samples for all this before hitting stable

public abstract class MvxAppCompatSetup<TApplication> : MvxAppCompatSetup
where TApplication : IMvxApplication, new()
{
protected override IMvxApplication CreateApp() => Mvx.IocConstruct<TApplication>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nickrandolph are you sure this will work? At first glance looks like this won't be ready at this point

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ran up ok for me on Android - this is late executed, so the IoC container has initialized by the time this is called.

{
if (_setup == null)
_setup = CreateSetup(this, MainWindow);
return _setup;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make Mac work the same way as Android and iOS? (use a InitializePlatform method instead of ctor arguments) It seems cleaner to me to unify all setup changes on the same PR

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done - I've made the changes but can someone please test both Mac and Mac Forms as I don't have access to a Mac this weekend.

get
{
if (_setup == null)
_setup = CreateSetup(this, Window);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make tvOS work the same way as Android and iOS? (use a InitializePlatform method instead of ctor arguments) It seems cleaner to me to unify all setup changes on the same PR

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done - as per mac, can someone please test to make sure my changes work

get
{
if (_setup == null)
_setup = CreateSetup(Dispatcher, MainWindow);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make WPF work the same way as Android and iOS? (use a InitializePlatform method instead of ctor arguments) It seems cleaner to me to unify all setup changes on the same PR

@nickrandolph
Copy link
Contributor Author

Ok, I think I've covered all the platforms with the updated setup structure. I'd be great if someone could test the iOS, iOS Forms, Mac, Mac Forms, TvOS builds

@martijn00
Copy link
Contributor

This is a huge improvement for new developers starting on MvvmCross! We might need to fix up some more Activity's in Android but for now this will be good to get merged!

@martijn00 martijn00 dismissed nmilcoff’s stale review March 18, 2018 12:58

Changes are implemented

@martijn00 martijn00 merged commit 7e0af53 into MvvmCross:develop Mar 18, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

4 participants