Удостоверяването на Azure AD B2C отпадна при навигиране до базирана на MVVM страница

Въпрос от две части: При стартиране My Xamarin.Forms app.cs навигира до страница за вход (ContentPage) с бутон. Щраквам върху бутона и влизам успешно с този манипулатор на събития в кода на приложението отзад:

async void OnLoginButtonClicked(object sender, EventArgs e)
    {
        try
        {
            bool authenticated = await App.AuthenticationProvider.LoginAsync();
            if (authenticated)
            {
                Application.Current.MainPage = new PapMobLandingPage();
            }
            else
            {
                await DisplayAlert("Authentication", "Authentication", "OK");
            }
        }
        catch (MsalException ex)
        {
            if (ex.ErrorCode == "authentication_canceled")
            {
                await DisplayAlert("Authentication", "Authentication was cancelled by the user.", "OK");
            }
            else
            {
                await DisplayAlert("An error has occurred", "Exception message: " + ex.Message, "OK");
            }
        }
        catch (Exception ex)
        {
            await DisplayAlert("Authentication", "Authentication failed in a big way. Exception: " + ex.Message, "OK");
        }
    }
}

След това бъда пренасочен към страница 2 (PapMobLandingPage), която също има бутон. Щраквам върху бутона PapMobLandingPage и се пренасочвам към TabbedPage с подробности за влезлия потребител, който лети точно там от моята база данни на Azure SQL, няма проблеми. Всичко страхотно до сега! Ето манипулатора на събитието:

public async void GoToTabbedPage(object sender, EventArgs args)
        {
            await Xamarin.Forms.Application.Current.MainPage.Navigation.PushModalAsync(new BotInTabbedPage());
        }

Въпрос 1: Няма OnAppearing() в кода на BotInTabbedPage (TabbedPage), който проверява дали потребителят е влязъл... няма придобиване на токени при инициализацията на TabbedPage, така че как приложението знае, че аз влязох ли в тази страница??

Разгледах подобен въпрос от @Creepin и използвайки връзката, не можах да разбера отговора на първоначалния му въпрос дали трябва да удостоверявате всяка страница поотделно. Линк тук:

използване на AcquireTokenSilentAsync

Причината да задам въпрос 1 е, че страницата с раздели по-горе е един от шестте избора, които потребителят получава при влизане, така че имах нужда от страница на таблото (базирана на MVVM) с шест плочки. Когато вмъкна това между страница 2 (PapMobLandingPage) и BotInTabbedPage (TabbedPage) и щракна върху плочката TabbedPage, командата tap, свързана с плочката, ме отвежда до BotInTabbedPage (TabbedPage)... НО...

НЯМА КЛИЕНТСКИ ДАННИ! БЯХ ИЗЛЕЗЕН!

Така че да обобщим:

Страница за влизане -> PapMobLandingPage -> BotInTabbedPage = Остава удостоверен. Страница за влизане -> PapMobLandingPage -> Табло -> BotInTabbedPage = Премахва удостоверяването.

Ако използвам "ванилия" ContentPage с бутон:

Страница за вход -> PapMobLandingPage -> ContentPage -> BotInTabbedPage = Остава удостоверен!

Така че вторият въпрос е: някой има ли представа защо?

Когато казвам, че таблото за управление е базирано на MVVM, имам предвид, че има XAML ContentPage, .cs код отзад, със стойностни обвързвания към модел на изглед, който също има шаблон на модел на изглед и база на шаблон. Това е кодът:

XAML табло за управление:

<?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:local="clr-namespace:PapWine;assembly=PapWine" 
             xmlns:artina="clr-namespace:UXDivers.Artina.Shared;assembly=UXDivers.Artina.Shared"
             x:Class="PapWine.DashboardTaskMultipleTilesPage"
             BackgroundColor="Black"
             Title="{ artina:Translate PageTitleDashboardTaskMultipleTiles }">

    <ContentPage.Resources>
        <ResourceDictionary>
            <artina:BoolMemberTemplateSelector
                x:Key="Selector"
                MemberName="IsNotification">

                <artina:BoolMemberTemplateSelector.TrueDataTemplate>
                    <DataTemplate>
                        <local:DashboardAppNotificationItemTemplate
                            WidthRequest="145"
                            HeightRequest="145" />
                    </DataTemplate>
                </artina:BoolMemberTemplateSelector.TrueDataTemplate>

                <artina:BoolMemberTemplateSelector.FalseDataTemplate>
                    <DataTemplate>
                        <local:TaskTilesItemTemplate
                            ShowBackgroundImage="true"
                            ShowBackgroundColor="true"
                            ShowiconColoredCircleBackground="false"
                            TextColor="{ DynamicResource DashboardIconColor }"
                            WidthRequest="145"
                            HeightRequest="145"
                            />
                    </DataTemplate>
                </artina:BoolMemberTemplateSelector.FalseDataTemplate>

            </artina:BoolMemberTemplateSelector>
        </ResourceDictionary>
    </ContentPage.Resources>

    <ScrollView
        Orientation="Both">
        <artina:GridOptionsView
            WidthRequest="320"
            Margin="0"
            Padding="10"
            ColumnSpacing="10"
            RowSpacing="10"
            ColumnCount="2"
            ItemsSource="{Binding DashboardTaskMultipleTilesList}"
            ItemTemplate="{StaticResource Selector}"
            />
    </ScrollView>
</ContentPage>

Табло XAML.cs

using Microsoft.Identity.Client;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Xamarin.Forms;

namespace PapWine
{
    public partial class DashboardTaskMultipleTilesPage : ContentPage
    {
        public DashboardTaskMultipleTilesPage()
        {           
            InitializeComponent();
            NavigationPage.SetHasNavigationBar(this, true);
            BindingContext = new DashboardTaskMultipleTilesViewModel();  
        }

        protected override async void OnAppearing()
        {
            base.OnAppearing();

            PublicClientApplication PCA = new PublicClientApplication(Constants.ClientID, Constants.Authority);
            IEnumerable<IAccount> accounts = await PCA.GetAccountsAsync();
            AuthenticationResult authenticationResult = await PCA.AcquireTokenSilentAsync(Constants.Scopes, GetAccountByPolicy(accounts, Constants.PolicySignUpSignIn), Constants.Authority, false);
            JObject user = ParseIdToken(authenticationResult.IdToken);
            var currentuseroid = user["oid"]?.ToString();
        }

        private IAccount GetAccountByPolicy(IEnumerable<IAccount> accounts, string policy)
        {
            foreach (var account in accounts)
            {
                string userIdentifier = account.HomeAccountId.ObjectId.Split('.')[0];
                if (userIdentifier.EndsWith(policy.ToLower())) return account;
            }
            return null;
        }

        JObject ParseIdToken(string idToken)
        {
            // Get the piece with actual user info
            idToken = idToken.Split('.')[1];
            idToken = Base64UrlDecode(idToken);
            return JObject.Parse(idToken);
        }

        string Base64UrlDecode(string str)
        {
            str = str.Replace('-', '+').Replace('_', '/');
            str = str.PadRight(str.Length + (4 - str.Length % 4) % 4, '=');
            var byteArray = Convert.FromBase64String(str);
            var decoded = Encoding.UTF8.GetString(byteArray, 0, byteArray.Count());
            return decoded;
        }    
   }
}   

Модел на изглед на таблото:

using System.Collections.Generic;
using System.Globalization;
using System.Threading.Tasks;
using Xamarin.Forms;

namespace PapWine
{
    public class DashboardTaskMultipleTilesViewModel : ObservableObject
    {
        private List<DashboardTaskMultipleTileItem> _dashboardTaskMultipleTilesList;

        public DashboardTaskMultipleTilesViewModel()
            : base(listenCultureChanges: true)
        {
            LoadData();
        }

        public List<DashboardTaskMultipleTileItem> DashboardTaskMultipleTilesList
        {
            get { return _dashboardTaskMultipleTilesList; }
            set { SetProperty(ref _dashboardTaskMultipleTilesList, value); }
        }

        protected override void OnCultureChanged(CultureInfo culture)
        {
            LoadData();
        }

        public async Task LogMeOut()
        {
            bool loggedOut = await App.AuthenticationProvider.LogoutAsync();

            if (loggedOut)
            {
                Application.Current.MainPage = new LandingPagePreLogin();
            }
        }

        private void LoadData()
        {
            DashboardTaskMultipleTilesList = new List<DashboardTaskMultipleTileItem> 
            {
                 //1 line
                new DashboardTaskMultipleTileItem
                {
                    Title = "Log Out",
                    Body = "",
                    Avatar = "",
                    BackgroundColor = "transparent",
                    ShowBackgroundColor = false,
                    IsNotification = false,
                    BackgroundImage = "Tiles/DarkBlackTile.jpg",
                    Icon = FontAwesomeFont.Lock,
                    IconColour = "White"
                },
                new DashboardTaskMultipleTileItem
                {
                    Title = "User Settings",
                    Body = "",
                    Avatar = "",
                    BackgroundColor = "transparent",
                    ShowBackgroundColor = false,
                    IsNotification = false,
                    BackgroundImage = "Tiles/DarkBlackTile.jpg",
                    Icon = FontAwesomeFont.Gear,
                    IconColour = "White"
                },
                 //2 line
                new DashboardTaskMultipleTileItem
                {
                    Title = "User Info",
                    Body = "",
                    Avatar = "",
                    BackgroundColor = "transparent",
                    ShowBackgroundColor = false,
                    IsNotification = false,
                    BackgroundImage = "Tiles/DarkBlackTile.jpg",
                    Icon = FontAwesomeFont.User,
                    Badge = 12,
                    IconColour = "White"
                },

                new DashboardTaskMultipleTileItem
                {
                    Title = "Papillon Shop",
                    Body = "",
                    Avatar = "",
                    BackgroundColor = "transparent",
                    ShowBackgroundColor = false,
                    IsNotification = false,
                    BackgroundImage = "Tiles/DarkBlackTile.jpg",
                    Icon = FontAwesomeWeb511Font.store,
                    Badge = 2,
                    IconColour = "White"
                },

                //3 line
                new DashboardTaskMultipleTileItem
                {
                    Title = "Check Bottles In",
                    Body = "",
                    Avatar = "",
                    BackgroundColor = "transparent",
                    ShowBackgroundColor = false,
                    IsNotification = false,
                    BackgroundImage = "Tiles/DarkBlackTile.jpg",
                    Icon = FontAwesomeFont.Book,
                    IconColour = "White"
                },

                new DashboardTaskMultipleTileItem
                {
                    Title = "Lay Bottles Down",
                    Body = "",
                    Avatar = "",
                    BackgroundColor = "transparent",
                    ShowBackgroundColor = false,
                    IsNotification = false,
                    BackgroundImage = "Tiles/DarkBlackTile.jpg",
                    Icon = FontAwesomeFont.Bed,
                    Badge = 2,
                    IconColour = "White"
                },

            };
        }
    }

    public class DashboardTaskMultipleTileItem
    {
        public string Title { get; set; }
        public string Body { get; set; }
        public string Avatar { get; set; }
        public string BackgroundColor { get; set; }
        public string BackgroundImage { get; set; }
        public bool ShowBackgroundColor { get; set; }
        public bool IsNotification { get; set; }
        public string Icon { get; set; }
        public int Badge { get; set; }
        public string NavigPage { get; set; }


        private Xamarin.Forms.Command _tapCommand;
        public Xamarin.Forms.Command TapCommand
        {                

        get
            {
                if (_tapCommand == null)
                {
                    switch (this.Title) {

                        case "Log Out":
                            App.AuthenticationProvider.LogoutAsync();
                            _tapCommand = new Xamarin.Forms.Command(() =>
                            Xamarin.Forms.Application.Current.MainPage.Navigation.PushModalAsync(new LogoutSuccessPage()));
                            break;
                        case "User Settings":                            
                            _tapCommand = new Xamarin.Forms.Command(() =>
                             Xamarin.Forms.Application.Current.MainPage.Navigation.PushAsync(new UserSettingsPage()));
                            break;
                        case "User Info":
                            _tapCommand = new Xamarin.Forms.Command(() =>
                            Xamarin.Forms.Application.Current.MainPage.Navigation.PushAsync(new UserProfilePage()));
                            break;
                        case "Papillon Shop":
                            _tapCommand = new Xamarin.Forms.Command(() =>

                            Xamarin.Forms.Application.Current.MainPage.Navigation.PushAsync(new ProductFamilyMultipleTilesPage()));
                            break;
                        case "Check Bottles In":
                            _tapCommand = new Xamarin.Forms.Command(() =>

                            Xamarin.Forms.Application.Current.MainPage = new SignBottlesIn());
                            break;
                        case "Check Bottles Out":
                            _tapCommand = new Xamarin.Forms.Command(() =>

                            Xamarin.Forms.Application.Current.MainPage = new SignBottlesIn());
                            break;
                    } 

                  }

                return _tapCommand;
            }
        }   
    }    

}

Шаблон на елемент от плочки:

using Xamarin.Forms;

namespace PapWine
{
    public partial class TaskTilesItemTemplate : TaskTilesItemTemplateBase
    {

        public TaskTilesItemTemplate()
        {
            InitializeComponent();                        
        }

        private async void OnTileTapped(object sender, ItemTappedEventArgs e)
        {
            if (e.Item == null)
                return;
            var content = e.Item as DashboardTaskMultipleTileItem;
            //await Navigation.PushAsync(new LayBottlesDown()); //pass content if you want to pass the clicked item object to another page
        }       
   }

}

База шаблони за елементи на плочки:

using System;
using System.Threading.Tasks;
using Xamarin.Forms;


namespace PapWine
{
    public class TaskTilesItemTemplateBase : ContentView
    {
        public uint animationDuration = 250;
        public bool _processingTag = false;

        public static readonly BindableProperty ShowBackgroundImageProperty =
            BindableProperty.Create(
                nameof(ShowBackgroundImage),
                typeof(bool),
                typeof(TaskTilesItemTemplate),
                true,
                defaultBindingMode: BindingMode.OneWay
            );

        public bool ShowBackgroundImage
        {
            get { return (bool)GetValue(ShowBackgroundImageProperty); }
            set { SetValue(ShowBackgroundImageProperty, value); }
        }

        public static readonly BindableProperty ShowBackgroundColorProperty =
            BindableProperty.Create (
                nameof( ShowBackgroundColor ), 
                typeof ( bool ),
                typeof ( TaskTilesItemTemplate ),
                false,
                defaultBindingMode  : BindingMode.OneWay
            );

        public bool ShowBackgroundColor {
            get { return ( bool )GetValue( ShowBackgroundColorProperty ); }
            set { SetValue ( ShowBackgroundColorProperty, value ); }
        }

        public static readonly BindableProperty ShowiconColoredCircleBackgroundProperty =
            BindableProperty.Create (
                nameof( ShowiconColoredCircleBackground ),
                typeof ( bool ),
                typeof (TaskTilesItemTemplate),
                true,
                defaultBindingMode  : BindingMode.OneWay
            );

        public bool ShowiconColoredCircleBackground {
            get { return ( bool )GetValue( ShowiconColoredCircleBackgroundProperty ); }
            set { SetValue ( ShowiconColoredCircleBackgroundProperty, value ); }
        }

        public static readonly BindableProperty TextColorProperty =
            BindableProperty.Create (
                nameof( TextColor ),
                typeof ( Color ),
                typeof (TaskTilesItemTemplate),
                defaultValue        : Color.White,
                defaultBindingMode  : BindingMode.OneWay
            );

        public Color TextColor {
            get { return ( Color )GetValue( TextColorProperty ); }
            set { SetValue ( TextColorProperty, value ); }
        }
        //added start
        public Color IconColour
        {
            get { return (Color)GetValue(TextColorProperty); }
            set { SetValue(TextColorProperty, value); }
        }
        //added end
        public async void OnWidgetTapped(object sender, EventArgs e)
        {
            if (_processingTag) 
            {
                return;
            }

            _processingTag = true;

            try{
                await AnimateItem (this, animationDuration  );

                await SamplesListFromCategoryPage.NavigateToCategory ((SampleCategory)BindingContext, Navigation);
            }finally{
                _processingTag = false;
            }
        }

        private async Task AnimateItem(View uiElement, uint duration ){
            var originalOpacity = uiElement.Opacity;

            await uiElement.FadeTo(.5, duration/2, Easing.CubicIn);
            await uiElement.FadeTo(originalOpacity, duration/2, Easing.CubicIn);

    }
}
}

LoginAsync изглежда така:

public async Task<bool> LoginAsync(bool useSilent = false)
        {
            bool success = false;
            //AuthenticationResult authResult = null;

            try
            {
                AuthenticationResult authenticationResult;

                if (useSilent)
                {
                    authenticationResult = await ADB2CClient.AcquireTokenSilentAsync(
                        Constants.Scopes,
                        GetAccountByPolicy(await ADB2CClient.GetAccountsAsync(), Constants.PolicySignUpSignIn),
                        Constants.Authority,
                        false);
                    UpdateUserInfo(authenticationResult);
                }
                else
                {
                    authenticationResult = await ADB2CClient.AcquireTokenAsync(
                        Constants.Scopes,
                        GetAccountByPolicy(await ADB2CClient.GetAccountsAsync(), Constants.PolicySignUpSignIn),
                        App.UiParent);
                }

                if (User == null)
                {
                    var payload = new JObject();
                    if (authenticationResult != null && !string.IsNullOrWhiteSpace(authenticationResult.IdToken))
                    {
                        payload["access_token"] = authenticationResult.IdToken;
                    }

                    User = await TodoItemManager.DefaultManager.CurrentClient.LoginAsync(
                        MobileServiceAuthenticationProvider.WindowsAzureActiveDirectory,
                        payload);
                    success = true;
                }
            }

person Mark    schedule 12.01.2019    source източник
comment
защо таблото за управление извиква AcquireTokenSilentAsync? Всяка страница прави ли това?   -  person Jason    schedule 12.01.2019
comment
Tbh, това беше просто тест, за да видя дали мога да вкарам жетона в страницата, когато се появи. Не проработи, трябваше да го изтрия, съжалявам. Но проблемът съществуваше преди да добавя онзи метод onappearing().   -  person Mark    schedule 12.01.2019
comment
Искам да добавя, че това ме измъчва от 4 дни и единствената разлика, която виждам, е фактът, че страницата, която се опитвам да вмъкна, е MVVM по структура.   -  person Mark    schedule 12.01.2019
comment
откъде знаеш, че вече не си удостоверен? Дали защото заявка за данни на Azure е неуспешна?   -  person Jason    schedule 13.01.2019
comment
Когато щракна върху плочката Проверка на бутилките, получавам грешката:   -  person Mark    schedule 13.01.2019
comment
Нямам представа какво прави Check Bottles In. Трябва да публикувате конкретния ред код. И специфичното изключение, което генерира.   -  person Jason    schedule 13.01.2019
comment
Когато щракна върху плочката „Проверка на бутилки в“, когато страницата с раздели се зареди успешно, получавам грешката: Microsoft.Identity.Client.MsalUiRequiredException: Нулев акаунт беше предаден в AcquiretokenSilent API. Предайте обект на акаунт или извикайте acceptToken за удостоверяване. Така че поставих метода Onappearing в Опитах се да хвана съобщението за грешка, когато страницата се зареди, и това все още беше: Microsoft.Identity.Client.MsalUiRequiredException: Нулев акаунт беше предаден в AcquiretokenSilent API. Предайте обект на акаунт или извикайте acceptToken за удостоверяване.   -  person Mark    schedule 13.01.2019
comment
Съжалявам, Джейсън, натиснах бутона за въвеждане твърде бързо и той въведе коментара... след това отне повече от 5 минути, за да стартира приложението и така не можах да променя коментара... аааааа   -  person Mark    schedule 13.01.2019
comment
Проверка на бутилки в натисната плочка задейства този код: case Проверка на бутилки в: _tapCommand = new Xamarin.Forms.Command(() =› //Xamarin.Forms.Application.Current.MainPage.Navigation.PushAsync(new SignBottlesIn())) ; Xamarin.Forms.Application.Current.MainPage = нов SignBottlesIn()); прекъсване;   -  person Mark    schedule 13.01.2019
comment
Тогава бих предположил, че изключението се случва в конструктора или OnAppearing на SignBottlesIn. Прави ли някой от тези методи нещо с Auth? Опитахте ли да разгледате трасирането на стека на изключението? Трябва да ви каже кой конкретен номер на ред го е причинил. И гугъл MsalUiRequiredException? Първото попадение, което получих, беше пълно с полезна информация.   -  person Jason    schedule 13.01.2019
comment
Но абсолютно същият метод onappearing работи във всички страници преди... ще потърся отново в Google, както беше предложено, но вече го опитах.   -  person Mark    schedule 13.01.2019
comment
Изключението причинено ли е от извикване на AcquireTokenSilentAsync? Защото първият въпрос, който ви зададох, е дали всяка страница прави това?   -  person Jason    schedule 13.01.2019
comment
Не, всяка страница не прави това. Поставих това само за да проверя изключението в тази конкретна страница. Така че, ако извадя това, страницата се зарежда добре и получавам изключение само когато натисна плочката. Отива добре до страницата с раздели, но не се изтеглят данни и изключението се задейства при извикване на данните. Ако извикам точно същата страница от страница със съдържание на ванилия с бутон, страницата с раздели се зарежда добре и данните идват хубаво. Така че определено нещо се случва в страницата с плочки на таблото.   -  person Mark    schedule 13.01.2019
comment
Нека продължим тази дискусия в чата.   -  person Jason    schedule 13.01.2019
comment
Здравей Джейсън, уведоми ме, когато си свободен, подготвих всичко.   -  person Mark    schedule 15.01.2019
comment
@Марк. Можете ли да ни кажете малко повече за това какво е App.AuthenticationProvider.LoginAsync(). Има ли някакъв код, който сте написали? или рамка SDK, която сте използвали? Използва ли се MSAL?   -  person Jean-Marc Prieur    schedule 17.01.2019
comment
@Jean-MarcPrieur Здравей, добавих LoginAsync() към въпроса (в края). Това е от една от вашите проби, ще се опитам да разбера коя ;-)   -  person Mark    schedule 17.01.2019