1

I'm trying to create a login flow in Xamarin Forms Shell that consists of a first page, where the user enters their email address, and a second page where they enter a password. I want the user to be able to go back from the password page and alter the entered email address.

This all works fine, if when initially navigating to the first page, I DONT set the email via a query parameter... If I do, when the user goes back form the password page to the email address page, the initially passed in email is shown - regardless of any changes the user may have made to it.

EG:

If the email page is initially navigated to using this code:

await Shell.Current.GoToAsync($"{nameof(test1)}");

The email address is blank by default. If the user changes it to hello@world.com and "continues" to the password page, then navigates back, the email address is still hello@world.com as expected.

However, if the email page is initially navigated to using this code:

var testEmail = "invalid$$3";    
await Shell.Current.GoToAsync($"{nameof(test1)}?EmailAddress={testEmail}");

The email address shows as invalid$$3 as expected. If the user then changes it to hello@world.com and "continues" to the password page, the password page shows hello@world.com in the label as expected, but if the user navigates back, the email address reverts to invalid$$3 on the email entry page (first page).

Here's my viewmodel:

[QueryProperty(nameof(EmailAddress), nameof(EmailAddress))]
    public class LoginViewModel : BaseViewModel
    {
        string emailAddress;
        public string EmailAddress
        {
            get
            {
                return emailAddress;
            }
            set
            {
                SetProperty(ref emailAddress, Uri.UnescapeDataString(value));
            }
        }
    }

Heres the first page (email entry):

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:d="http://xamarin.com/schemas/2014/forms/design"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"             
             xmlns:vm="clr-namespace:The_In_Plaice.ViewModels"             
             mc:Ignorable="d"
             x:Class="The_In_Plaice.Views.test1">
    <ContentPage.BindingContext>
        <vm:LoginViewModel/>
    </ContentPage.BindingContext>
    <ContentPage.Content>
        <StackLayout>
            <Entry x:Name="txtEmail" Text="{Binding EmailAddress}" Placeholder="email"/>
            <Button Clicked="Button_Clicked"/>
        </StackLayout>
    </ContentPage.Content>
</ContentPage>

(code behind)

[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class test1 : ContentPage
{
    public test1()
    {
        InitializeComponent();
    }

    private async void Button_Clicked(object sender, EventArgs e)
    {
        var email = txtEmail.Text;
        await Shell.Current.GoToAsync($"{nameof(test2)}?EmailAddress={email}");
    }
}

and the second page:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:d="http://xamarin.com/schemas/2014/forms/design"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"             
             xmlns:vm="clr-namespace:The_In_Plaice.ViewModels"             
             mc:Ignorable="d"
             x:Class="The_In_Plaice.Views.test2">
    <ContentPage.BindingContext>
        <vm:LoginViewModel/>
    </ContentPage.BindingContext>
    <ContentPage.Content>
        <StackLayout>
            <Label Text="{Binding EmailAddress}"/>
            <Entry Text="{Binding Password}" IsPassword="True" Placeholder="password"/>
            <Button/>
        </StackLayout>
    </ContentPage.Content>
</ContentPage>

(code behind)

 [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class test2 : ContentPage
    {
        public test2()
        {
            InitializeComponent();
        }
    }

Any ideas what I'm doing wrong here?

Sk93
  • 3,676
  • 3
  • 37
  • 67
  • is there any code in `OnAppearing` in the login page? – Jason Nov 02 '21 at 12:10
  • nope. only a button click (which validates the email and, if valid, navigates to the password page) – Sk93 Nov 02 '21 at 12:13
  • (added all code) – Sk93 Nov 02 '21 at 12:25
  • I don't see any place you're using the email address in the query string when loading the email page – Andrew H Nov 02 '21 at 19:35
  • @AndrewH - see the second code snippet: "await Shell.Current.GoToAsync($"{nameof(test1)}?EmailAddress={testEmail}");" – Sk93 Nov 02 '21 at 20:35
  • Ah, right, I forgot about the automatic query string usage. Your `test1` class fails to define a BindingContext, which means that `LoginViewModel.EmailAddress` won't be set by Shell when navigating to the page. – Andrew H Nov 02 '21 at 21:21
  • @AndrewH see the forth code block: – Sk93 Nov 02 '21 at 21:24
  • Like I say.. this works 100% perfect.. IF I don't use the query string. If I DO USE the query string, it overwrites whats in the viewmodel / page when I navigate back to the default value passed in via the query string. – Sk93 Nov 02 '21 at 21:25
  • (appreciate the help tho!) – Sk93 Nov 02 '21 at 21:29
  • 1
    I haven't tried to do what you are doing. But an idea, if no one has a better answer: override page's OnAppearing and OnDisappearing. With breakpoints on those, and on EmailAddress's setter: does login page's 2nd OnAppearing happen BEFORE or AFTER EmailAddress is set back to the (undesired) initial value? If AFTER, then you could cache in OnDisappearing, restore in OnAppearing. To avoid messing up the first call, skip the restore if your cached value is null. – ToolmakerSteve Nov 03 '21 at 02:39
  • ... if OnAppearing happens BEFORE the value gets reset, a hacky solution would be to force it back to desired value, after a delay. As last few lines of OnAppearing: `if (cachedEmail != null) Device.BeginInvokeOnMainThread( async () => { await Task.Delay(100); if (BindingContext != null) ((LoginViewModel)BindingContext).EmailAddress = cachedEmail; });` – ToolmakerSteve Nov 03 '21 at 03:04
  • 1
    @ToolmakerSteve so, OnAppearing happens AFTER the viewmodel is updated to the original value, so I implemented your first hack idea. That works. I don't like it lol.. but it certainly gets me passed this issue. Thanks for the idea! – Sk93 Nov 03 '21 at 10:40
  • 1
    Glad that hack worked! If you get a chance, please add "Your Answer" below. This will show others in future how to work around this. [While it was my idea, what will help people the most is seeing actual working code - and I don't want to take the time to type in the exact code!] (After 48 hours, you can come back and "accept" your own answer.) – ToolmakerSteve Nov 03 '21 at 21:34
  • @ToolmakerSteve will do. was giving you time to post your hack as the answer :) – Sk93 Nov 04 '21 at 08:10

2 Answers2

1

Don't use QueryPropertyAttribute to pass the data, maybe when navagting to login page , it will trigger the setter method and revert to the original value .

Possible Workaround :

Make the viewmodel inherit from IQueryAttributable, you can retrieve the value from ApplyQueryAttributes method .

public class LoginViewModel : IQueryAttributable, BaseViewModel
{
    string emailAddress;
    public string EmailAddress
    {
        get
        {
           return emailAddress;
        }
        set
        {
           SetProperty(ref emailAddress, Uri.UnescapeDataString(value));
        }
   }

    public void ApplyQueryAttributes(IDictionary<string, string> query)
    {
        // The query parameter requires URL decoding.
        string email = HttpUtility.UrlDecode(query["EmailAddress"]);
        EmailAddress = email ;
    }
}

Details refer to https://learn.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/shell/navigation#process-navigation-data-using-a-single-method.

ColeX
  • 14,062
  • 5
  • 43
  • 240
  • 1
    Just tried this workaround, and I get the same results unfortunately. Whenever I hit back from the second page, it still reverts to what was originally passed in. Starting to think this may be a bug with Xamarin, as I can't imagine this is the desired outcome? – Sk93 Nov 03 '21 at 10:33
  • This answer may not have solved @Sk93's problem, but it solved mine and is so much better than what I was doing (declaring query params at the top of each ViewModel and creating setters to parse the string values). – PhillFox Dec 07 '21 at 02:25
1

The solution I went for here was to implement a "hack" based on ToolMakerSteve's comment.

The changes below were added to the first page code behind, which basically just stores the current Email Address on disappearing, and replaces whatever is in the viewmodel in the on appearing (if it has been set).

The only caveat would be if you wanted to change it in a subsequent page, this hack wouldn't work, but for my purposes, it works just fine.

        private string EditedEmailAddress;
        protected override void OnAppearing()
        {
            if (!string.IsNullOrEmpty(EditedEmailAddress))
                ViewModel.EmailAddress = EditedEmailAddress;            
            base.OnAppearing();
        }

        protected override void OnDisappearing()
        {
            EditedEmailAddress = ViewModel.EmailAddress;
            base.OnDisappearing();
        }
Sk93
  • 3,676
  • 3
  • 37
  • 67