Привет, Хабр! Мы продолжаем нашу экспериментальную серию статей, наблюдая за которой вы можете в реальном времени влиять на ход создания игры на UWP. Сегодня на повестке дня — получение данных пользователя. Ведь почти во всех приложениях это — нужная процедура. Присоединяйтесь!
![](https://habrastorage.org/r/w780q1/webt/8p/vr/ts/8pvrtsu9xoqdearjdnkptp9b1eg.jpeg)
Передаю слово автору, Алексею Плотникову.
Идентификации пользователя является краеугольным камнем большинства современных приложений и, тем более, игр. Сложно представить игру без социальных функций таких как рейтинговые таблицы, достижения и кланы. В приложениях же лишенных социальных функций, идентификация нужна, чтобы, например, усовершенствовать процесс синхронизации между устройствами. По большей части, именно проблемы синхронизации и стали отправной точкой для моих изысканий в этом вопросе.
Каждое UWP приложение имеет выделенное облачное хранилище (roaming data), в которое оно может сохранить данные доступные с любого устройства при условии, что пользователь использует тот же MS аккаунт. К сожалению, это хранилище имеет ряд серьезных ограничений.
Во-первых, данные, которые можно поместить в облако, ограничены по размеру. Для большинства задач, выделенного размера достаточно, но это все равно ограничение. Куда более важным ограничением является принцип помещения данных в облако. Внутренний алгоритм работы данной функции учитывает множество факторов таких как, например, энергосбережение или лимитированное подключение к интернету и в конечном счете процесс синхронизации между устройствами не только может занять больше десяти минут, но и вовсе не гарантирован (подробнее здесь).
Такие ограничения совсем не подходят для моих задач, поэтому процесс синхронизации было решено реализовать самостоятельно и первым делом передо мной стал вопрос в определении устройств, принадлежащих одному и тому же игроку.
Многие игры и приложения используют для синхронизации аккаунт пользователя на Facebook, хотя в большинстве случаев это вызвано переносом имеющихся алгоритмов в Microsoft Store, а не удобством реализации. Другие имеют собственные сервисы, в которых игроку нужно регистрироваться, но это тоже лишнее усложнение. В итоге самое логичное решение, конечно же, использовать учетную запись Microsoft, которая однозначно есть у пользователя, так как без нее невозможно приобретение и установка приложений из магазина Windows. Осталось дело за малым – получить данные пользователя, чтобы идентифицировать его на разных устройствах.
Углубляясь в вопрос, я узнал, что данные учетной записи Microsoft получаются с помощью API Microsoft Live, что связано с некоторыми трудностями. Дело в том, что для получения любых данных о пользователе, он должен осуществить вход в свой аккаунт, а мы должны получить токен доступа, который и позволит нам их запрашивать. Такая процедура создана для безопасности и являются частью стандарта аутентификации OAuth, однако его самостоятельная реализация для начинающих разработчиков кажется сложной и неудобной.
К счастью, создатели UWP понимали потенциальный ужас, который OAuth способен вызвать у начинающего разработчика и создали изящный и довольно простой способ работы с учетной записью Microsoft (и не только). Называется этот инструмент «Диспетчер учетных веб-записей» и именно его мы рассмотрим далее.
Создаем пустой проект и заменяем в файле MainPage.xaml головной Grid, на следующий XAML:
XAML довольно простой и объяснений не требует, поэтому переходим в событие нажатия кнопки и вставляем следующий код:
Не забудьте добавить импорт пространства имен Windows.UI.ApplicationSettings, иначе данный код не сработает.
Теперь приложение можно выполнить и посмотреть на результат.
![](https://habrastorage.org/r/w1560/webt/xv/an/fs/xvanfss1vdulzinru56wcgf7m6o.png)
Пустое окно не совсем то, что мы ожидали, но это всего лишь оболочка, которую сначала нужно наполнить. Наполнять его мы будем так называемыми «командами», которыми в нашем случае будут перечнем доступных для входа аккаунтов.
Инструмент «Диспетчер учетных веб-записей» очень гибкий и позволяет создать эти команды как вручную, так и загрузить их от требуемого поставщика. Во втором случае используется функция FindAccountProviderAsync, где поставщиком для учетных записей выступит адрес «login.microsoft.com».
Однако наполнить панель командами невозможно до ее инициализации, поэтому первым делом нужно подписаться на соответствующее событие. Добавим эту строчку перед кодом отображения панели и сразу же сформируем процедуру, вызываемую событием:
К слову, в официальном руководстве подписка на событие AccountCommandsRequested происходит в момент перехода на текущую страницу, а отписка при переходе с нее. Это имеет смысл, если под процесс аутентификации вы выделяете отдельную страницу, но я считаю такой подход недостаточно гибким.
Заполним BuildPaneAsync:
И сразу же выполним несколько дополнительных действий:
Разберемся с этим кодом. Во-первых, так как для получения списка команд может потребоваться время, нам необходимо отсрочить отображение панели, пока она не будет наполнена. Для этого создается отсроченный объект Deferral. Далее с помощью ранее упомянутой функции FindAccountProviderAsync происходит загрузка поставщика. Обратите внимание, что в эту функцию так же передается параметр со значением «consumers». Это сообщает поставщику, что мы хотим получить стандартную учетную запись Microsoft, а не учетную запись организации (тогда было бы «organizations»).
После мы создаем новую команду с помощью полученного провайдера, указываем процедуру, вызываемую при нажатии на команду, и добавляем результат на панель. Последней строчкой мы сообщаем, что панель сформирована и ее можно отображать.
![](https://habrastorage.org/r/w1560/webt/bx/bj/oq/bxbjoqm-xlilrjxcvxyvpuupfus.png)
Теперь, после запуска и нажатия на кнопку «Вход», отображается заполненная панель с несколькими вариантами входа. Вас может ввести в замешательство тот факт, что мы добавляли одну команду на панель, но на ней находится две. На самом деле это еще одна из причин по которой мне нравится «Диспетчер учетных веб-записей», так как он самостоятельно определил текущий аккаунт пользователя и предложил его в качестве основного варианта входа, а на случай, если пользователь хочет использовать другую учетную запись, создан отдельный вариант. С точки зрения внутренней логики оба варианта идентичны и в итоге отсылают нас в процедуру GetMsaTokenAsync, которую мы создали ранее.
Дальнейшие действия можно разделить на несколько этапов. Для начала внутри GetMsaTokenAsync нужно выполнить запрос к поставщику данных с указанием типа данных, которые мы запрашиваем, а затем сформировать результат этого запроса:
Обратите внимание на параметр «wl.basic», что передается в первую функцию. Это указание на разрешения, которые получает приложение при работе с данными пользователя. Указывая разные разрешения, мы можем работать с адресной книгой, календарем, фотографиями и многими другими данными, но нам нужны только имя, аватар и ID пользователя, поэтому используется wl.basic, которого в данном случае достаточно. Полный список разрешений можно посмотреть здесь.
После успешного получения результата запроса, мы можем сформировать объект типа WebAccount, который поможет получить маркеры доступа к учетной записи:
Для работы этого кода понадобится импорт пространств имен Windows.Security.Credentials и Windows.Storage.
Полученные маркеры актуальны для текущего пользователя на протяжении длительного времени, а значит у нас нет необходимости повторять процедуру входа постоянно. Вместо этого мы сохраняем эти маркеры в локальные настройки приложения, чтобы использовать их в будущем для фоновой аутентификации. Собственно, к фоновой аутентификации мы и переходим, вызвав процедуру BackgroundConnectUser.
Сначала добавим импорты, что понадобятся нам для работы дальнейшего кода:
Теперь переходим в BackgroundConnectUser и загружаем сохраненные маркеры:
Если маркеры не пусты, то формируем из них объекты провайдера и аккаунта для получения токена, а также осуществляем запрос:
В случае удачи, можно получить токен доступа к API и выполнять непосредственные запросы:
Адреса запросов для получения данных пользователя можно узнать в документации по API Microsoft Live, но нас интересуют всего два: «https://apis.live.net/v5.0/me» для получения общих данных и «https://apis.live.net/v5.0/me/picture» для получения аватара. Возвращаются данные пользователя в формате Json, поэтому для их удобного разбора используется парсер.
Наконец можно запустить программу и убедится, что все равно ничего не работает. Связано это с тем, что API Microsoft Live не предоставляют данные просто так. Для их использования приложение должно иметь идентификатор в службе Microsoft Live к которому затем привязываются разрешения на доступ к данным. К счастью, идентификатор формируется автоматически при создании нового приложения в информационной панели разработчика. Чтобы загрузить этот идентификатор в текущий проект, достаточно просто связать его с магазином Windows. Для этого перейдите в меню «Проект > Магазин > Связать приложение с Магазином…» и выберете нужное имя из списка (или зарегистрируйте новое). Больше нам делать ничего не нужно, так как необходимые данные в API Microsoft Live будут переданы автоматически.
Повторно запускаем проект и наконец наблюдаем результат.
![](https://habrastorage.org/r/w1560/webt/vy/lx/my/vylxmy8essj0q3y0wyykhradhsu.png)
На этом рассмотрение получения данных пользователя можно было бы завершить, если бы я ставил перед собой цель просто пересказать материал из официального руководства.
Ниже я приведу полный код проекта с внесенными в него изменениями, а далее объясню и обосную каждое такое изменение.
XAML:
Разметка страницы претерпела не очень большие корректировки. Самое важное отличие — это добавление кнопки выхода из аккаунта, а также строки для отслеживания статуса подключения. Остальные изменение сделали отображение данных более наглядным ведь в конечном итоге задача страницы продемонстрировать результат выполнения кода, в котором и состоялись основные изменения. Рассмотрим их.
Во-первых, я полностью вынес логику работы с диспетчером учетных веб-записей в отдельный класс. Это сделано для того, чтобы иметь доступ к этому коду из любого места приложения. Например, в статье «Расширенный экран-заставка» я упоминал о длительных операциях, которые имеет смыл выполнять внутри экрана-заставки и процесс получения данных пользователя в BackgroundConnectUser как раз является такой операцией.
В результате переноса кода в отдельный класс, в классе самой страницы практически ничего не осталось. Вся работа внутри страницы сводится к реакции на нажатие двух кнопок и установке контекста данных для страницы (в процедуре MainPage_Loaded). Не сложно догадаться, что в качестве контекста данных выступает экземпляр класса UserManager в котором уже и происходит основная работа. Класс реализует интерфейс INotifyPropertyChanged, что позволяет осуществлять привязку к его полям из разметки страницы.
Перенесенный код получил некоторые изменения, и я постараюсь разобрать их в том же порядке, в котором создавался первоначальный пример:
На этом моя реализация для диспетчера учетных веб-записей заканчивается. Само-собой в процессе разработки будут внесены какие-то доработки, которые будут отвечать тем задачам, что лично я ставлю перед данным инструментом, однако приведенный класс вполне работоспособен и вы можете смело копировать его в свой проект, чтобы приступать к реализации ваших задач, связанных с получением данных пользователя.
Теперь, когда данные пользователя получены, можно приступать к изучению вопроса синхронизации данных между устройствами и начну я с поиска походящей площадки и технологии. Именно об этом мы и поговорим в следующей статье.
![](https://habrastorage.org/webt/8p/vr/ts/8pvrtsu9xoqdearjdnkptp9b1eg.jpeg)
- Часть 1: С чего стоит начать
- Часть 2: Расширенный экран-заставка
- Часть 3: Стиль сына маминой подруги
- Часть 4: Получение данных пользователя (добровольное)
- Часть 5: А где вы храните данные?
Передаю слово автору, Алексею Плотникову.
Идентификации пользователя является краеугольным камнем большинства современных приложений и, тем более, игр. Сложно представить игру без социальных функций таких как рейтинговые таблицы, достижения и кланы. В приложениях же лишенных социальных функций, идентификация нужна, чтобы, например, усовершенствовать процесс синхронизации между устройствами. По большей части, именно проблемы синхронизации и стали отправной точкой для моих изысканий в этом вопросе.
Каждое UWP приложение имеет выделенное облачное хранилище (roaming data), в которое оно может сохранить данные доступные с любого устройства при условии, что пользователь использует тот же MS аккаунт. К сожалению, это хранилище имеет ряд серьезных ограничений.
Во-первых, данные, которые можно поместить в облако, ограничены по размеру. Для большинства задач, выделенного размера достаточно, но это все равно ограничение. Куда более важным ограничением является принцип помещения данных в облако. Внутренний алгоритм работы данной функции учитывает множество факторов таких как, например, энергосбережение или лимитированное подключение к интернету и в конечном счете процесс синхронизации между устройствами не только может занять больше десяти минут, но и вовсе не гарантирован (подробнее здесь).
Такие ограничения совсем не подходят для моих задач, поэтому процесс синхронизации было решено реализовать самостоятельно и первым делом передо мной стал вопрос в определении устройств, принадлежащих одному и тому же игроку.
Многие игры и приложения используют для синхронизации аккаунт пользователя на Facebook, хотя в большинстве случаев это вызвано переносом имеющихся алгоритмов в Microsoft Store, а не удобством реализации. Другие имеют собственные сервисы, в которых игроку нужно регистрироваться, но это тоже лишнее усложнение. В итоге самое логичное решение, конечно же, использовать учетную запись Microsoft, которая однозначно есть у пользователя, так как без нее невозможно приобретение и установка приложений из магазина Windows. Осталось дело за малым – получить данные пользователя, чтобы идентифицировать его на разных устройствах.
Углубляясь в вопрос, я узнал, что данные учетной записи Microsoft получаются с помощью API Microsoft Live, что связано с некоторыми трудностями. Дело в том, что для получения любых данных о пользователе, он должен осуществить вход в свой аккаунт, а мы должны получить токен доступа, который и позволит нам их запрашивать. Такая процедура создана для безопасности и являются частью стандарта аутентификации OAuth, однако его самостоятельная реализация для начинающих разработчиков кажется сложной и неудобной.
К счастью, создатели UWP понимали потенциальный ужас, который OAuth способен вызвать у начинающего разработчика и создали изящный и довольно простой способ работы с учетной записью Microsoft (и не только). Называется этот инструмент «Диспетчер учетных веб-записей» и именно его мы рассмотрим далее.
Создаем пустой проект и заменяем в файле MainPage.xaml головной Grid, на следующий XAML:
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<Button x:Name="LoginButton" Content="Вход" Click="LoginButton_Click" />
<TextBlock x:Name="UserIdTextBlock"/>
<TextBlock x:Name="UserNameTextBlock"/>
<Image x:Name="UserAvatarImage" Height="50"/>
</StackPanel>
XAML довольно простой и объяснений не требует, поэтому переходим в событие нажатия кнопки и вставляем следующий код:
AccountsSettingsPane.Show()
Не забудьте добавить импорт пространства имен Windows.UI.ApplicationSettings, иначе данный код не сработает.
Теперь приложение можно выполнить и посмотреть на результат.
![](https://habrastorage.org/webt/xv/an/fs/xvanfss1vdulzinru56wcgf7m6o.png)
Пустое окно не совсем то, что мы ожидали, но это всего лишь оболочка, которую сначала нужно наполнить. Наполнять его мы будем так называемыми «командами», которыми в нашем случае будут перечнем доступных для входа аккаунтов.
Инструмент «Диспетчер учетных веб-записей» очень гибкий и позволяет создать эти команды как вручную, так и загрузить их от требуемого поставщика. Во втором случае используется функция FindAccountProviderAsync, где поставщиком для учетных записей выступит адрес «login.microsoft.com».
Однако наполнить панель командами невозможно до ее инициализации, поэтому первым делом нужно подписаться на соответствующее событие. Добавим эту строчку перед кодом отображения панели и сразу же сформируем процедуру, вызываемую событием:
AddHandler AccountsSettingsPane.GetForCurrentView().AccountCommandsRequested, AddressOf BuildPaneAsync
…
Private Sub BuildPaneAsync(sender As AccountsSettingsPane, e As AccountsSettingsPaneCommandsRequestedEventArgs)
'Тут будет код добавления команд
End Sub
К слову, в официальном руководстве подписка на событие AccountCommandsRequested происходит в момент перехода на текущую страницу, а отписка при переходе с нее. Это имеет смысл, если под процесс аутентификации вы выделяете отдельную страницу, но я считаю такой подход недостаточно гибким.
Заполним BuildPaneAsync:
Dim Deferral = e.GetDeferral
Dim msaProvider = Await WebAuthenticationCoreManager.FindAccountProviderAsync("https://login.microsoft.com", "consumers")
Dim command = New WebAccountProviderCommand(msaProvider, AddressOf GetMsaTokenAsync)
e.WebAccountProviderCommands.Add(command)
Deferral.Complete()
И сразу же выполним несколько дополнительных действий:
- Добавить ключевое слово Async к процедуре;
- Добавить импорт пространства имен Windows.Security.Authentication.Web.Core;
- Сформировать процедуру GetMsaTokenAsync с параметром command типа WebAccountProviderCommand
Разберемся с этим кодом. Во-первых, так как для получения списка команд может потребоваться время, нам необходимо отсрочить отображение панели, пока она не будет наполнена. Для этого создается отсроченный объект Deferral. Далее с помощью ранее упомянутой функции FindAccountProviderAsync происходит загрузка поставщика. Обратите внимание, что в эту функцию так же передается параметр со значением «consumers». Это сообщает поставщику, что мы хотим получить стандартную учетную запись Microsoft, а не учетную запись организации (тогда было бы «organizations»).
После мы создаем новую команду с помощью полученного провайдера, указываем процедуру, вызываемую при нажатии на команду, и добавляем результат на панель. Последней строчкой мы сообщаем, что панель сформирована и ее можно отображать.
![](https://habrastorage.org/webt/bx/bj/oq/bxbjoqm-xlilrjxcvxyvpuupfus.png)
Теперь, после запуска и нажатия на кнопку «Вход», отображается заполненная панель с несколькими вариантами входа. Вас может ввести в замешательство тот факт, что мы добавляли одну команду на панель, но на ней находится две. На самом деле это еще одна из причин по которой мне нравится «Диспетчер учетных веб-записей», так как он самостоятельно определил текущий аккаунт пользователя и предложил его в качестве основного варианта входа, а на случай, если пользователь хочет использовать другую учетную запись, создан отдельный вариант. С точки зрения внутренней логики оба варианта идентичны и в итоге отсылают нас в процедуру GetMsaTokenAsync, которую мы создали ранее.
Дальнейшие действия можно разделить на несколько этапов. Для начала внутри GetMsaTokenAsync нужно выполнить запрос к поставщику данных с указанием типа данных, которые мы запрашиваем, а затем сформировать результат этого запроса:
Dim request As WebTokenRequest = New WebTokenRequest(command.WebAccountProvider, "wl.basic")
Dim result As WebTokenRequestResult = Await WebAuthenticationCoreManager.RequestTokenAsync(request)
Обратите внимание на параметр «wl.basic», что передается в первую функцию. Это указание на разрешения, которые получает приложение при работе с данными пользователя. Указывая разные разрешения, мы можем работать с адресной книгой, календарем, фотографиями и многими другими данными, но нам нужны только имя, аватар и ID пользователя, поэтому используется wl.basic, которого в данном случае достаточно. Полный список разрешений можно посмотреть здесь.
После успешного получения результата запроса, мы можем сформировать объект типа WebAccount, который поможет получить маркеры доступа к учетной записи:
If result.ResponseStatus = WebTokenRequestStatus.Success Then
Dim account As WebAccount = result.ResponseData(0).WebAccount
ApplicationData.Current.LocalSettings.Values("CurrentUserProviderId") = account.WebAccountProvider.Id
ApplicationData.Current.LocalSettings.Values("CurrentUserId") = account.Id
BackgroundConnectUser()
End If
Для работы этого кода понадобится импорт пространств имен Windows.Security.Credentials и Windows.Storage.
Полученные маркеры актуальны для текущего пользователя на протяжении длительного времени, а значит у нас нет необходимости повторять процедуру входа постоянно. Вместо этого мы сохраняем эти маркеры в локальные настройки приложения, чтобы использовать их в будущем для фоновой аутентификации. Собственно, к фоновой аутентификации мы и переходим, вызвав процедуру BackgroundConnectUser.
Сначала добавим импорты, что понадобятся нам для работы дальнейшего кода:
Imports System.Net.Http
Imports Windows.Data.Json
Теперь переходим в BackgroundConnectUser и загружаем сохраненные маркеры:
Dim providerId As String = ApplicationData.Current.LocalSettings.Values("CurrentUserProviderId")
Dim accountId As String = ApplicationData.Current.LocalSettings.Values("CurrentUserId")
Если маркеры не пусты, то формируем из них объекты провайдера и аккаунта для получения токена, а также осуществляем запрос:
Dim provider As WebAccountProvider = Await WebAuthenticationCoreManager.FindAccountProviderAsync(providerId)
Dim account As WebAccount = Await WebAuthenticationCoreManager.FindAccountAsync(provider, accountId)
Dim request As WebTokenRequest = New WebTokenRequest(provider, "wl.basic")
Dim result As WebTokenRequestResult = Await WebAuthenticationCoreManager.GetTokenSilentlyAsync(request, account)
В случае удачи, можно получить токен доступа к API и выполнять непосредственные запросы:
If result.ResponseStatus = WebTokenRequestStatus.Success Then
Dim token As String = result.ResponseData(0).Token
Dim restApi = New Uri("https://apis.live.net/v5.0/me?access_token=" + token)
Dim client = New HttpClient()
Dim infoResult = Await client.GetAsync(restApi)
Dim Content As String = Await infoResult.Content.ReadAsStringAsync()
Dim jsonO = JsonObject.Parse(Content)
UserIdTextBlock.Text = jsonO("id").GetString
UserNameTextBlock.Text = jsonO("name").GetString
UserAvatarImage.Source = New BitmapImage(New Uri("https://apis.live.net/v5.0/me/picture?access_token=" + token))
End If
Адреса запросов для получения данных пользователя можно узнать в документации по API Microsoft Live, но нас интересуют всего два: «https://apis.live.net/v5.0/me» для получения общих данных и «https://apis.live.net/v5.0/me/picture» для получения аватара. Возвращаются данные пользователя в формате Json, поэтому для их удобного разбора используется парсер.
Наконец можно запустить программу и убедится, что все равно ничего не работает. Связано это с тем, что API Microsoft Live не предоставляют данные просто так. Для их использования приложение должно иметь идентификатор в службе Microsoft Live к которому затем привязываются разрешения на доступ к данным. К счастью, идентификатор формируется автоматически при создании нового приложения в информационной панели разработчика. Чтобы загрузить этот идентификатор в текущий проект, достаточно просто связать его с магазином Windows. Для этого перейдите в меню «Проект > Магазин > Связать приложение с Магазином…» и выберете нужное имя из списка (или зарегистрируйте новое). Больше нам делать ничего не нужно, так как необходимые данные в API Microsoft Live будут переданы автоматически.
Повторно запускаем проект и наконец наблюдаем результат.
![](https://habrastorage.org/webt/vy/lx/my/vylxmy8essj0q3y0wyykhradhsu.png)
На этом рассмотрение получения данных пользователя можно было бы завершить, если бы я ставил перед собой цель просто пересказать материал из официального руководства.
Ниже я приведу полный код проекта с внесенными в него изменениями, а далее объясню и обосную каждое такое изменение.
XAML:
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<StackPanel.Resources>
<local:ConnectStatusToEnabledConverter x:Key="ConnectStatusToEnabledConverter"/>
</StackPanel.Resources>
<StackPanel Orientation="Horizontal">
<Button x:Name="LoginButton" Content="Вход" Click="LoginButton_Click" IsEnabled="{Binding ConnectStatus, Converter={StaticResource ConnectStatusToEnabledConverter}, ConverterParameter=0}"/>
<Button x:Name="LogOutButton" Margin="10,0,0,0" Content="Выход" Click="LogOutButton_Click" IsEnabled="{Binding ConnectStatus, Converter={StaticResource ConnectStatusToEnabledConverter}, ConverterParameter=2}"/>
</StackPanel>
<TextBlock Margin="0,0,0,20" Text="{Binding ConnectStatus}"/>
<TextBlock>ИД пользователя: <Run FontWeight="Bold" Text="{Binding UserId}"/></TextBlock>
<TextBlock>Имя пользователя: <Run FontWeight="Bold" Text="{Binding UserName}"/></TextBlock>
<Image Height="50" Source="{Binding UserAvatar}" HorizontalAlignment="Left"/>
</StackPanel>
Код Окна
Imports Windows.Security.Authentication.Web.Core
Imports Windows.UI.ApplicationSettings
Imports Windows.Security.Credentials
Imports Windows.Storage
Imports System.Net.Http
Imports Windows.Data.Json
Imports Windows.System
Public NotInheritable Class MainPage
Inherits Page
Private CurUser As New UserManager
Private Sub MainPage_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded
DataContext = CurUser
End Sub
Private Async Sub LoginButton_Click(sender As Object, e As RoutedEventArgs)
If Not Await CurUser.BackgroundConnectUser Then
CurUser.ConnectUser()
End If
End Sub
Private Async Sub LogOutButton_Click(sender As Object, e As RoutedEventArgs)
CurUser.LogOutUser(Await CurUser.GetWebAccount)
End Sub
End Class
Public Class UserManager
Implements INotifyPropertyChanged
#Region "Реализация интерфейса"
Public Event PropertyChanged(sender As Object, e As PropertyChangedEventArgs) Implements INotifyPropertyChanged.PropertyChanged
Private Sub OnPropertyChanged(PropertyName As String)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(PropertyName))
End Sub
#End Region
Private UserIdValue As String
Private UserNameValue As String
Private UserAvatarValue As BitmapImage
Private ConnectStatusValue As UserConnectStatusEnum = UserConnectStatusEnum.None
#Region "Свойства"
''' <summary>
''' ID текущего пользователя
''' </summary>
''' <returns></returns>
Public Property UserId As String
Get
Return UserIdValue
End Get
Set(value As String)
UserIdValue = value
OnPropertyChanged("UserId")
End Set
End Property
''' <summary>
''' Имя текущего пользователя
''' </summary>
''' <returns></returns>
Public Property UserName As String
Get
Return UserNameValue
End Get
Set(value As String)
UserNameValue = value
OnPropertyChanged("UserName")
End Set
End Property
''' <summary>
''' Аватар пользователя
''' </summary>
''' <returns></returns>
Public Property UserAvatar As BitmapImage
Get
Return UserAvatarValue
End Get
Set(value As BitmapImage)
UserAvatarValue = value
OnPropertyChanged("UserAvatar")
End Set
End Property
''' <summary>
''' Статус подключения
''' </summary>
''' <returns></returns>
Public Property ConnectStatus As UserConnectStatusEnum
Get
Return ConnectStatusValue
End Get
Set(value As UserConnectStatusEnum)
ConnectStatusValue = value
OnPropertyChanged("ConnectStatus")
End Set
End Property
#End Region
#Region "Основыне функции и процедуры"
''' <summary>
''' Запускает процедуру входа пользователя
''' </summary>
Public Sub ConnectUser()
AddHandler AccountsSettingsPane.GetForCurrentView().AccountCommandsRequested, AddressOf BuildPaneAsync
AccountsSettingsPane.Show()
End Sub
Public Async Sub LogOutUser(account As WebAccount)
ApplicationData.Current.LocalSettings.Values.Remove("CurrentUserProviderId")
ApplicationData.Current.LocalSettings.Values.Remove("CurrentUserId")
Await account.SignOutAsync()
UserId = ""
UserName = ""
UserAvatar = New BitmapImage
ConnectStatus = UserConnectStatusEnum.None
End Sub
Public Async Function BackgroundConnectUser() As Task(Of Boolean)
Dim result As Boolean = False
Dim account As WebAccount = Await GetWebAccount()
If account Is Nothing Then Return result
ConnectStatus = UserConnectStatusEnum.Logon
Dim request As WebTokenRequest = New WebTokenRequest(account.WebAccountProvider, "wl.basic")
Dim requestResult As WebTokenRequestResult = Await WebAuthenticationCoreManager.GetTokenSilentlyAsync(request, account)
If requestResult.ResponseStatus = WebTokenRequestStatus.Success Then
Try
Dim token As String = requestResult.ResponseData(0).Token
Dim restApi = New Uri("https://apis.live.net/v5.0/me?access_token=" + token)
Dim client = New HttpClient()
Dim infoResult = Await client.GetAsync(restApi)
Dim Content As String = Await infoResult.Content.ReadAsStringAsync()
Dim jsonO = JsonObject.Parse(Content)
UserId = jsonO("id").GetString
UserName = jsonO("name").GetString
UserAvatar = New BitmapImage(New Uri("https://apis.live.net/v5.0/me/picture?access_token=" + token))
ConnectStatus = UserConnectStatusEnum.Ssuccessful
result = True
Catch ex As Exception
ConnectStatus = UserConnectStatusEnum.None
End Try
Else
ConnectStatus = UserConnectStatusEnum.None
End If
Return result
End Function
#End Region
#Region "Внутренние функции, процедуры и типы"
''' <summary>
''' Создает варианты логина на панели
''' </summary>
''' <param name="sender"></param>
''' <param name="e"></param>
Private Async Sub BuildPaneAsync(sender As AccountsSettingsPane, e As AccountsSettingsPaneCommandsRequestedEventArgs)
RemoveHandler AccountsSettingsPane.GetForCurrentView().AccountCommandsRequested, AddressOf BuildPaneAsync
Dim Deferral = e.GetDeferral
Dim msaProvider = Await WebAuthenticationCoreManager.FindAccountProviderAsync("https://login.microsoft.com", "consumers")
Dim command = New WebAccountProviderCommand(msaProvider, AddressOf GetMsaTokenAsync)
e.WebAccountProviderCommands.Add(command)
e.HeaderText = "Для доступа к ферме требуется выполнить вход в ваш аккаунт Microsoft"
Dim settingsCmd As SettingsCommand = New SettingsCommand("settings_privacy", "Политика конфиденциальности", Async Sub()
Await Launcher.LaunchUriAsync(New Uri("https://privacy.microsoft.com/ru-ru/"))
End Sub)
e.Commands.Add(settingsCmd)
Deferral.Complete()
End Sub
''' <summary>
''' Логин и получение токина
''' </summary>
''' <param name="command"></param>
Private Async Sub GetMsaTokenAsync(command As WebAccountProviderCommand)
ConnectStatus = UserConnectStatusEnum.Logon
Dim request As WebTokenRequest = New WebTokenRequest(command.WebAccountProvider, "wl.basic")
Dim result As WebTokenRequestResult = Await WebAuthenticationCoreManager.RequestTokenAsync(request)
If result.ResponseStatus = WebTokenRequestStatus.Success Then
Dim account As WebAccount = result.ResponseData(0).WebAccount
ApplicationData.Current.LocalSettings.Values("CurrentUserProviderId") = account.WebAccountProvider.Id
ApplicationData.Current.LocalSettings.Values("CurrentUserId") = account.Id
Await BackgroundConnectUser()
Else
ConnectStatus = UserConnectStatusEnum.None
End If
End Sub
''' <summary>
''' Получает аккаунт пользователя на основе сохраненных маркеров
''' </summary>
''' <returns></returns>
Public Async Function GetWebAccount() As Task(Of WebAccount)
Dim providerId As String = ApplicationData.Current.LocalSettings.Values("CurrentUserProviderId")
Dim accountId As String = ApplicationData.Current.LocalSettings.Values("CurrentUserId")
If (providerId Is Nothing And accountId Is Nothing) Then
Return Nothing
End If
Dim provider As WebAccountProvider = Await WebAuthenticationCoreManager.FindAccountProviderAsync(providerId)
Dim account As WebAccount = Await WebAuthenticationCoreManager.FindAccountAsync(provider, accountId)
Return account
End Function
''' <summary>
''' Перечислитесь статуса подключения
''' </summary>
Public Enum UserConnectStatusEnum
''' <summary>
''' Вход не выполнен
''' </summary>
None = 0
''' <summary>
''' Осуществляется вход
''' </summary>
Logon = 1
''' <summary>
''' Вход выполнен
''' </summary>
Ssuccessful = 2
End Enum
#End Region
End Class
Public Class ConnectStatusToEnabledConverter
Implements IValueConverter
Public Function Convert(value As Object, targetType As Type, parameter As Object, language As String) As Object Implements IValueConverter.Convert
Return CStr(parameter).IndexOf(CStr(value)) > -1
End Function
Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, language As String) As Object Implements IValueConverter.ConvertBack
Throw New NotImplementedException()
End Function
End Class
Разметка страницы претерпела не очень большие корректировки. Самое важное отличие — это добавление кнопки выхода из аккаунта, а также строки для отслеживания статуса подключения. Остальные изменение сделали отображение данных более наглядным ведь в конечном итоге задача страницы продемонстрировать результат выполнения кода, в котором и состоялись основные изменения. Рассмотрим их.
Во-первых, я полностью вынес логику работы с диспетчером учетных веб-записей в отдельный класс. Это сделано для того, чтобы иметь доступ к этому коду из любого места приложения. Например, в статье «Расширенный экран-заставка» я упоминал о длительных операциях, которые имеет смыл выполнять внутри экрана-заставки и процесс получения данных пользователя в BackgroundConnectUser как раз является такой операцией.
В результате переноса кода в отдельный класс, в классе самой страницы практически ничего не осталось. Вся работа внутри страницы сводится к реакции на нажатие двух кнопок и установке контекста данных для страницы (в процедуре MainPage_Loaded). Не сложно догадаться, что в качестве контекста данных выступает экземпляр класса UserManager в котором уже и происходит основная работа. Класс реализует интерфейс INotifyPropertyChanged, что позволяет осуществлять привязку к его полям из разметки страницы.
Перенесенный код получил некоторые изменения, и я постараюсь разобрать их в том же порядке, в котором создавался первоначальный пример:
- Процедура ConnectUser. Она по сути дублирует код, что ранее вызывался по нажатию на кнопку «Вход». Вызов это процедуры понадобится только в том случае, если у нас нет маркеров для фонового подключения.
- BuildPaneAsync получила несколько значимых, и не очень изменений. Для начала отписка от события AccountCommandsRequested, которое после вызова данной процедуры уже нам не нужно. Далее следует уже известный код, который дополняется установкой подзаголовка в окне диспетчера и добавлением ссылки на политику конфиденциальности. Оба этих пункта не обязательны и реализуются полностью по вашему усмотрению. Мало того, во втором случае можно добавить ссылку, выполняющую любой ваш код и не обязательно это переход на страницу с политикой конфиденциальности.
- После нажатия на кнопку «Продолжить» начинается выполнение процедуры GetMsaTokenAsync, поэтому первой же строкой в ней устанавливается статус Logon, на который можно реагировать для отображения индикаторов выполнения или блокировки элементов интерфейса. Затем идет уже знакомый нам код, но с добавлением реакции на ситуации, когда вход был отменен или не прошел успешно. Признаюсь, над этим, казалось бы, банальным местом я бился целый день. Загвоздка в том, что при первом выполнении данного кода, сразу после окна с выбором вариантов входа, появляется еще одно окно с запросом разрешения на доступ к данным. Между переключениями этих двух окон может пройти относительно большой промежуток времени (до 2х секунд), поэтому логично сохранить статус Logon в этот период, чтобы исключить повторное нажатие на кнопку «Вход». Однако пользователь может отказаться предоставлять доступ к данным, либо просто закрыть второе окно (нажать кнопку «Назад» на мобильном устройстве), поэтому в случае таких действий нужно вернуть исходный статус None, что мы и делаем в блоке Else.
- Процедура BackgroundConnectUser в новой вариации превратилась в асинхронную функцию, чтобы мы могли соответствующим образом отреагировать на неудачную (или удачную) попытку фонового получения данных. Так как данная функция может вызываться из разных мест (например, из экрана-заставки), то мы должны убедится, что загруженные маркеры доступа не являются пустыми. Для удобства загрузки сохраненных маркеров, создана отдельная функция GetWebAccount и, если в локальных настройках маркеров нет, то она возвращает Nothing. При неудаче получения объекта WebAccount, возвращаем результат по умолчанию (False), а если он получен удачно, то устанавливаем статус в Logon и продолжаем выполнение процедуры. Дале повторяем уже известные действия с той лишь разницей, что в случае отказа в получении токена, нужно вернуть статус None, а при удачном запросе Ssuccessful.
- Процедура LogOutUser содержит код, не используемый ранее. В ней реализуется возможность выхода из аккаунта и удаления маркеров из сохраненных настроек. Обязательно реализуйте такую возможность в вашем приложении, так как это даст полную свободу пользователю с точки зрения обеспечения конфиденциальности, не говоря уже о потенциальной необходимости войти с другими учетным данными.
- Ну последнее, что следует упомянуть это свойство ConnectStatus, которое я создал в данном классе. Это свойство имеет тип собственного перечислителя UserConnectStatusEnum и необходима для установки статуса входа. Это важный элемент взаимодействия с классом, так как благодаря привязке к этому свойству мы можем заблокировать кнопки, нажатие на которые не желательно в момент выполнения определенных участков кода или, напротив, разблокировать те, что доступны только после выполнения входа. Чтобы привязка свойства IsEnabled к ConnectStatus интерпретировалась правильно, так же создан конвертер ConnectStatusToEnabledConverter.
На этом моя реализация для диспетчера учетных веб-записей заканчивается. Само-собой в процессе разработки будут внесены какие-то доработки, которые будут отвечать тем задачам, что лично я ставлю перед данным инструментом, однако приведенный класс вполне работоспособен и вы можете смело копировать его в свой проект, чтобы приступать к реализации ваших задач, связанных с получением данных пользователя.
Теперь, когда данные пользователя получены, можно приступать к изучению вопроса синхронизации данных между устройствами и начну я с поиска походящей площадки и технологии. Именно об этом мы и поговорим в следующей статье.