locked
PushModalAsnyc await, does not wait, how to fix? RRS feed

  • Question

  • User206855 posted

    VS 2015, PCL, happens in UWP desktop, and Android (haven't gotten to IOS yet).

    In calling page, when button is clicked, this code is executed...

    private async void addressButton_Clicked(object sender, EventArgs e) { var addressPage = new AddressPage();

            //await Task.Run(async () =>
            //{
            //    await Navigation.PushModalAsync(addressPage);
            //});
    
            await Navigation.PushModalAsync(addressPage);
            addressLabel.Text = MyGlobals.propertyAddress;
        }
    

    What happens, is that it executes the 'await Navigation.PushModalAsync(addressPage);', then immediately executes the next line (which does not get the global value), and then the modal page appears. The addressLabel.text view on the calling page, never gets populated. You can see from the commented out code, that the Task code did no better.

    The following code is from the called page. I added the OnBackButtonPressed override code, because on Android, it was allowing the user to exit without entering an address.

    using System; using Xamarin.Forms; using Xamarin.Forms.Xaml;

    namespace LousTest { [XamlCompilation(XamlCompilationOptions.Compile)] public partial class AddressPage : ContentPage { public AddressPage() { InitializeComponent(); //NavigationPage.SetHasNavigationBar(this, false); //does not work }

        public async void returnButton_Clicked(object sender, EventArgs e)
        {
            if (string.IsNullOrEmpty(addressEntry.Text) || string.IsNullOrWhiteSpace(addressEntry.Text))
            {
                await DisplayAlert("Property Address", "Property address cannot be blank", "OK");
            }
            else
            {
                MyGlobals.propertyAddress = addressEntry.Text;
                await Navigation.PopModalAsync();
            }
        }
    
        protected override bool OnBackButtonPressed()
        {
            //do not allow the use of the back button
            if (string.IsNullOrEmpty(addressEntry.Text) || string.IsNullOrWhiteSpace(addressEntry.Text))
            {
                DisplayAlert("Property Address", "Property address cannot be blank", "OK");
                return true;
            }
            else
            {
                MyGlobals.propertyAddress = addressEntry.Text;
                Navigation.PopModalAsync();
            }
    
            return true;
        }
    }
    

    }

    Sunday, February 26, 2017 9:45 PM

All replies

  • User180523 posted

    PushModal ##Async##

    Its an asynchronous action. The await is just until the async call returns, not that the entire operation within the async call returns. If that were the case, it wouldn't be... well... asynchronous... would it?

    The problem here is not the call to the page, but rather how you're trying to populate the UI elements directly in a very 1998 fashion.

    addressLabel.Text = MyGlobals.propertyAddress; As well as doing all this in the code behind of the page... responding to Button.Click like that... Globals like this MyGlobals.propertyAddress = addressEntry.Text; This whole 1990's approach is the problem here, not the call to push the page.

    You probably want to stop... read up on MVVM and data binding... Familiarize yourself with modern coding patterns and practices... then re-examin how you approach this app, and coding in general.

    Sunday, February 26, 2017 9:53 PM
  • User206855 posted

    Putting my 1990's approach to populating variables aside for now.., how do you make the code wait until the user has returned from the modal page (actually, it works the same way whether modal or not)?

    Sunday, February 26, 2017 11:03 PM
  • User180523 posted

    You don't. That's problem with the approach. It isn't just variables: Its the whole paradigm of how you're treating the UI as if it were the controlling apparatus of the application: Basically like it was treated under the old Windows Forms.

    Here's the difference in thinking. You're approaching this as if the UI is the workflow or controller: The user opens a page, work is done, they close a page triggering the next unit of work.

    That's not how things work in today's systems. The user starts a unit of work, as a side effect a page might open. While there they do something then they finish. As a side effect of them finishing, the page closes. Your program reacts to the work they do, updating UI to reflect the action, not the other way around where changing the UI updates the workflow.

    You have to accept that UI is a reflection of data and state. The UI presents data, but is not the data itself. It is a way to interact with data. If the state of the app is that it is navigating, then the UI reflects that by presenting a map page. But it does not do navigation because its on the map page.

    Getting back to the situation you've described... The page closing is completely a UI function and should not affect the flow of operation because the ViewModel that is the data and logic doesn't even know the page exists.

    One approach - but a half dozen right ways spring to mind: When the user does whatever it is to say "I'm done with this piece of work", the ViewModel could then raise a Command (think of commands as next-generation Events). When the command is raised then the code that handles that command can then: 1) Close the page because its not needed 2) Carry out the next piece of workflow.

    Monday, February 27, 2017 12:01 AM
  • User206855 posted

    Tomorrow, I will read this more carefully, I am tired tonight. In the meanwhile, I have determined that most examples of using a modal page, have the call to await Navigation.PushModalAsync();, as the last line of code in that method. The user clicked a button, the modal page was called and then returned from, and until the user does something else, no code is executed. So I have removed the line "addressLabel.Text = MyGlobals.propertyAddress;". I have to do this in the ViewModel?

    Your last paragraph...

    When the user does whatever it is to say "I'm done with this piece of work", the ViewModel could then raise a Command (think of commands as next-generation Events). When the command is raised then the code that handles that command can then: 1) Close the page because its not needed 2) Carry out the next piece of workflow.

    In the downloadable Xamarin samples, which sample is the best example of this?

    Monday, February 27, 2017 1:56 AM
  • User180523 posted

    In the downloadable Xamarin samples, which sample is the best example of this? Sorry. I have no idea. I do not have a guide or catalog or breakdown of what the samples are, or what their code contains. I don't know any more about the sample than does the next man.

    I can suggest that dissecting code isn't the best route to take for learning this. There are many good books that take a guided step-by-step education approach that will teach these concepts in the most efficient way. Amazon has many if you search "MVVM". And of course the Charsle Petzold book is free for downloading. And I recently learned many of the Xamarin University videos are uploaded to YouTube by Xamarin for their own learning channel.

    Monday, February 27, 2017 11:55 AM
  • User89714 posted

    @louwho - If you want to use event-driven code in your view layer, you may want to look at:

    NavigationPage
        Popped
        Pushed
        Focused
        Unfocused
    
    Page
        Appearing
        Disappearing
        OnAppearing
        OnDisappearing
    

    You might also want to look at MessagingCenter.

    As to whether this is the best thing to do, I'll leave that to others - I'm just saying you can do this if you want to (I do, as I've written what is effectively my own framework over the top of Xamarin.Forms).

    Regarding MVVM books - Amazon will bring up a list of them if you search, but amongst the ones that are already available, there are some real shockers. Definitely room in the market for a good one for the mobile space, but I'm not sure how much money there is to be made from tech books anymore. I've written one tech book (30+ years ago) which did rather well, but I suspect the tech book market has changed so much in the intervening years that it's dubious whether writing one about MVVM & Xamarin would be worth the effort. Pity really - if somebody else wrote one, I'd read it even if it were half-way decent.

    Monday, February 27, 2017 6:02 PM
  • User206855 posted

    I actually already have some binding\MVVM in my app already. I have a ListView on my main page, and added the Address property to this code…

    public class LinesViewModel : INotifyPropertyChanged { private ObservableCollection linesList = new ObservableCollection(); public ObservableCollection LinesList { get { return _linesList; } set { if (linesList != value) _linesList = value; OnPropertyChanged("LinesList"); } }

    private string _address;
       public string Address 
       {
           get
           {
               return _address;
           }
    
           {
               _address = value;
    
               OnPropertyChanged(nameof(Address));
           }
       }
    
    public event PropertyChangedEventHandler PropertyChanged;
    
       protected virtual void OnPropertyChanged(string propertyName)
       {
          PropertyChanged ?.Invoke(this, new PropertyChangedEventArgs(propertyName));
       }
    

    }

    The MainPage has this line after the InitializeComponent(), (I have no problems with the ListView)…

    BindingContext = new LinesViewModel();
    

    It also has the following, but executing the code, or commenting it out, it seems to make no difference…

    LinesViewModel LV1 = new LinesViewModel();
    addressLabel.SetBinding(Label.TextProperty, "Address", BindingMode.TwoWay);
    addressLabel.Text = LV1.Address;
    

    The address page has this code in it…

    LinesViewModel Lines1 = new LinesViewModel();
    Lines1.Address = addressEntry.Text;
     //MyGlobals.propertyAddress = addressEntry.Text;
     await Navigation.PopModalAsync();
    

    The Main page XAML code for the address label is…

    "<Label x:Name="addressLabel" HorizontalTextAlignment="Start" Text="{Binding Address}"/>"
    

    I walk through the code, enter an address, and see everything being executed, but the label text is never populated. I am not sure what I am doing wrong, too much, or missing code? I have been trying every combination that I can think of, all day today. I tried to get this to line up here, some success, but not all. What to do next time?

    Monday, February 27, 2017 10:08 PM
  • User89714 posted

    @louwho - you probably ought to ask that in a new thread, as it is significantly different to the title of your original question.

    Monday, February 27, 2017 11:06 PM
  • User180523 posted

    Not trying to come down on you. Just trying to give you a couple rules of thumb to help guide you as you code.

    • If you have lots of code in your page code behind there is probably a problem. 95% of my page code behind (.xaml.cs) has nothing more than the default constructor that calls InitializeComponents and there is hardly ever a reason to even open the .xaml.cs file.

    • If you are directly accessing UI elements from the C# like: addressLabel.Text = LV1.Address; and Lines1.Address = addressEntry.Text; You have missed the whole point to MVVM and databinding.

    • If you can even have those lines accessing your page UI elements and your data properties in the same class, then you have data in the code behind of the page instead of in a ViewModel.

    Data binding basics from the Xamarin site: https://developer.xamarin.com/guides/xamarin-forms/xaml/xaml-basics/databindingbasics/

    Introduction to data binding from the Xamarin blog: https://blog.xamarin.com/introduction-to-data-binding/

    Revisiting MVVM - (A 30,000 foot concept overview, not a step-by-step walk-thru) https://redpillxamarin.com/2017/01/08/401-revisiting-mvvm/

    Monday, February 27, 2017 11:58 PM
  • User200104 posted

    Well this was entertaining to read lol.

    Anyway nobody even gave this man the right answer so I will:

    https://forums.xamarin.com/discussion/65041/modal-return

    This is probably what you are looking :) Using the TaskCompletionSource to retrieve and get the results from the modal page when finished.

    I have this one running in my app currently, but in my viewmodel ofcourse here's the sample:

    Viewmodal A (Page 1)

    /// <summary>
            /// ICommand that is bound to the TapGesture. When a user taps the RelationsSelectOverview this Command will trigger. 
            /// This Command will trigger a Task that tries to capture a value. The value is stored inside the _selectedRelation variable. This value will get awaited so two things can happen:
            /// 1. User selects a relation and this value is then set inside the _selectedRelation.
            /// 2. User cancels and the Cancel Command is triggered. This command will set the TaskCompletion to null. tcs.SetResult(null); 
            /// - iOS Cancel Command will be found inside the RelationPageViewModel this is bound to the cancel button.
            /// - Android Cancel Command can be found in the code behind of the RelationPage. This will be triggered when a user pressed the hardware back button.
            /// </summary>
            public ICommand SelectRelationCommand {
                get {
                    return new Command(async () => {
                        try {
                            if (IsBusyFlag == true) {
                                return;
                            }
                            IsBusyFlag = true;
    
                            Bedrijf newBedrijf = await GetSelectedRelation(_navigation);
                            if (newBedrijf != null) {
                                _selectedRelation = newBedrijf;
                            }
                            PopulateRelation(_selectedRelation);
                            IsBusyFlag = false;
                        } catch (Exception ex) {
                            IsBusyFlag = false;
                        }
                    });
                }
            }
    
    
            /// <summary>
            /// Task that is a TaskCompletionSource of TResult Bedrijf. The parameter parsed with this Task enabled navigation to push and pop the page.
            /// The task pushes the RelationPage ontop and waits for the user to select a relation and gets this value with the GetValue method..
            /// </summary>
            /// <param name="navigation"></param>
            /// <returns></returns>
            public static Task<Bedrijf> GetSelectedRelation(INavigation navigation) {
                var tcs = new TaskCompletionSource<Bedrijf>();
                Device.BeginInvokeOnMainThread(async () => {
                    var page = new BNNavigationPage(new RelationsPage(false));
                    await navigation.PushModalAsync(page);
                    // Needed to get acces to the viewmodel methods.
                    var pageViewmodel = (page.CurrentPage.BindingContext) as RelationsPageViewModel;
                    if (pageViewmodel != null) {
                        var value = await pageViewmodel.GetValue();
                        await navigation.PopModalAsync();
                        tcs.SetResult(value);
                    }
                });
                return tcs.Task;
            }
    

    Viewmodel B (Page 2 the modal page)

     /// <summary>
            /// TaskCompletionSource of TResult Bedrijf. This task will be used to return a value when a user selects a relation from the editPage.
            /// </summary>
            private TaskCompletionSource<Bedrijf> tcs = new TaskCompletionSource<Bedrijf>();
    
    /// <summary>
            /// Task of TResult Bedrijf that returns the TaskCompletionSource of TResult Bedrijf. After a result has been set it will trigger and return the task.
            /// </summary>
            /// <returns></returns>
            public Task<Bedrijf> GetValue() {
               return tcs.Task;
            }
    
     /// <summary>
            /// Cancels the PopOver relationPage and sets the result to null. This is used to cancel.
            /// </summary>
            public ICommand CancelCommand {
                get {
                    return new Command(() => {
                        try {
                            if (_isBusyFlag == true) {
                                return;
                            }
                            _isBusyFlag = true;
                            tcs.SetResult(null);
                            _isBusyFlag = false;
                        } catch (Exception ex) {
                            _isBusyFlag = false;
                        }
                    });
                }
            }
    
      /// <summary>
            /// ICommand of type ItemTappedEvent that casts the parameter as Bedrijf into variable tappedItem. tappedItem with all it's data is then parsed in the parameter of TabbedOverviewParentPage to get used.
            /// </summary>
            public ICommand ItemSelectedCommand {
                get {
                    return new Command<ItemTappedEventArgs>(async parameter => {
                        try {
                            if (_isBusyFlag == true) {
                                return;
                            }
                            _isBusyFlag = true;
                            var tappedItem = parameter.Item as Bedrijf;
    
                            if (tappedItem != null && _isRelationPageFlag)
                            {
                                await this._bnPage.Navigation.PushAsync(new TabbedOverviewRelationParentPage(tappedItem));
                            } else if (tappedItem != null && !_isRelationPageFlag) {
                                tcs.SetResult(tappedItem);
    
                            }
                            _isBusyFlag = false;
                        } catch (Exception ex) {
                            _isBusyFlag = false;
                        }
    
                    });
                }
            }
    

    Just a brief flow explanation:

    1. User taps something to open the modal page
    2. Modal page is being triggered with the TaskCompletionSource and the getvalue (GetValue is awaited)
    3. Either the user selects a item from listview inside the modal page or not
    4. tcs.setvalue will set the value
    5. value gets retrieved and set inside viewmodel A
    6. This is either null or something

    That is it. If you wonder what if value is null? This is taken care of inside the PopulateRelation() method, you can do whatever you please. Your implementation will probably never get a value of null.

    Also the only code behind that I use is for the Android hardware backbutton

    I have the following:

            /// <summary>
            /// Will trigger the Cancel Command to remove the task from the memory when back button is pressed. This will only occurs when the page is a editpage.
            /// </summary>
            /// <returns></returns>
            protected override bool OnBackButtonPressed() {
                if (!_isRelationFlag && Device.OS == TargetPlatform.Android) {
                    ((RelationsPageViewModel)BindingContext).CancelCommand.Execute(null);
                }
                return base.OnBackButtonPressed();
    
            }
    

    Anyway good luck getting your implementation to work :)!

    Tuesday, February 28, 2017 9:46 AM
  • User200104 posted

    @louwho said: I actually already have some binding\MVVM in my app already. I have a ListView on my main page, and added the Address property to this code…

    public class LinesViewModel : INotifyPropertyChanged { private ObservableCollection linesList = new ObservableCollection(); public ObservableCollection LinesList { get { return _linesList; } set { if (linesList != value) _linesList = value; OnPropertyChanged("LinesList"); } }

    private string _address;
       public string Address 
       {
           get
           {
               return _address;
           }
    
           {
               _address = value;
    
               OnPropertyChanged(nameof(Address));
           }
       }
    

    public event PropertyChangedEventHandler PropertyChanged;

       protected virtual void OnPropertyChanged(string propertyName)
       {
          PropertyChanged ?.Invoke(this, new PropertyChangedEventArgs(propertyName));
       }
    

    }

    The MainPage has this line after the InitializeComponent(), (I have no problems with the ListView)…

    BindingContext = new LinesViewModel();

    It also has the following, but executing the code, or commenting it out, it seems to make no difference…

    LinesViewModel LV1 = new LinesViewModel(); addressLabel.SetBinding(Label.TextProperty, "Address", BindingMode.TwoWay); addressLabel.Text = LV1.Address;


    The address page has this code in it…

    LinesViewModel Lines1 = new LinesViewModel(); Lines1.Address = addressEntry.Text; //MyGlobals.propertyAddress = addressEntry.Text; await Navigation.PopModalAsync();


    The Main page XAML code for the address label is…

    "

    I walk through the code, enter an address, and see everything being executed, but the label text is never populated. I am not sure what I am doing wrong, too much, or missing code? I have been trying every combination that I can think of, all day today. I tried to get this to line up here, some success, but not all. What to do next time?

    The adress page is probably a different page am I right?

    The values are reset, because you are Initialize a new LinesViewModel for the adress page. Mobile pages are basicly new blank pages everytime you navigate to them. So what you could do is following:

    1. Parse the object containing all data that is needed from page A to B. This can be done by doing the following:

      • PageA.Navigation.PushAsync(Data YourData) (make sure your viewmodel does accept parameters so overload the constructor/ edit the current)

    You are basicly using a modal push which is harder but doable would suggest you to follow my sample / link from my previous comment

    Good luck :)

    Tuesday, February 28, 2017 9:57 AM
  • User89714 posted

    @louwho @LuukJongebloet - Yes, the solution provided by @MichaelRumpler at https://forums.xamarin.com/discussion/65041/modal-return is a good way of doing it, although not the only way :-)

    Tuesday, February 28, 2017 10:04 AM
  • User76049 posted

    Scenarios like this is why I like Prism, makes this scenario easy to implement with navigation parameters.

    Tuesday, February 28, 2017 10:09 AM
  • User200104 posted

    @JohnHardman said: @louwho @LuukJongebloet - Yes, the solution provided by @MichaelRumpler at https://forums.xamarin.com/discussion/65041/modal-return is a good way of doing it, although not the only way :-)

    That is very true. This is what I used in my solutions that works. I also read something about using event triggers to subscribe and unsubscribe etc. but this is still a little vague for me to understand sadly :(

    Tuesday, February 28, 2017 10:09 AM
  • User200104 posted

    @NMackay said: Scenarios like this is why I like Prism, makes this scenario easy to implement with navigation parameters.

    Yeah Prism does help alot with simplifying and slimming down complex coding. With the deep linking that they provide etc. I am going to probably use Prism in my next project :)

    Tuesday, February 28, 2017 10:15 AM
  • User91860 posted

    @NMackay I am trying to use Prism, but given your comment, I'm not sure I am following the best or easiest approach.

    I'm trying to integrate an MVVM approach using Prism into an example application by Microsoft. Basically, I'm using the example provided by Microsoft as the basis for my applications authentication. https://github.com/azure-appservice-samples/ContosoMoments

    In my App.xaml file I call the below method which calls my login page and awaits the GetResultAsync method with the chosen authentication type.

    private async Task DoLoginAsync()
    {
        await NavigationService.NavigateAsync("Login", null, true, true);
        LoginViewModel vm = Container.Resolve<Login>().BindingContext as LoginViewModel;            
        //Device.BeginInvokeOnMainThread(async () =>
        //{
            Settings.Current.AuthenticationType = await vm.GetResultAsync();
        //});
    
        await SyncAsync(notify: true);
    }
    

    In my LoginViewModel.cs file I create the tcs.

    private TaskCompletionSource<Settings.AuthOption> tcs;
    
    public Task<Settings.AuthOption> GetResultAsync()
    {
        tcs = new TaskCompletionSource<Settings.AuthOption>();
        return tcs.Task;
    }
    

    After the user has authenticated with Facebook, the following method is called to pass back the authentication option used.

    private async Task LoginComplete(Settings.AuthOption option)
    {
        Xamarin.Forms.DependencyService.Get<IPlatform>().LogEvent("Login" + option);
    
        await _navigationService.GoBackAsync(null, true, true);
        tcs.TrySetResult(option);
    }
    

    When GoBackAsync is called, the application crashes with the following.

    I'm stuck and don't know if this is just my bad coding or a thread issue.

    Any pointers would be much appreciated.

    Thanks

    Steven

    Friday, June 16, 2017 3:11 PM
  • User365995 posted

    Scenarios like this are why I liked OpenVMS (a PROPER real-time programming system!). Could do this in 3 lines... =:-)

    Tuesday, April 6, 2021 4:10 PM