Hi, Habr! We continue our experimental series of articles, watching which you can in real time influence the course of creating a game on UWP. Today on the agenda - getting user data. Indeed, in almost all applications, this is a necessary procedure. Join now!
I give the floor to the author, Alexey Plotnikov .')
User identification is the cornerstone of most modern applications and, especially, games. It is difficult to imagine a game without social functions such as rating tables, achievements and clans. In applications that lack social functions, identification is needed in order, for example, to improve the synchronization process between devices. For the most part, it was the synchronization problems that became the starting point for my research on this issue.
Each UWP application has a dedicated cloud storage (roaming data), in which it can save data available from any device, provided that the user uses the same MS account. Unfortunately, this repository has some serious limitations.
First, the data that can be placed in the cloud is limited in size. For most tasks, the allocated size is sufficient, but this is still a limitation. A far more important limitation is the principle of putting data into the cloud. The internal algorithm of this function takes into account many factors such as, for example, energy saving or limited Internet connection and ultimately the synchronization process between devices can not only take more than ten minutes, but is not guaranteed at all (more
here ).
Such restrictions are completely unsuitable for my tasks, so it was decided to implement the synchronization process on my own and the first thing I faced was the issue of identifying devices belonging to the same player.
Many games and applications use the Facebook account for synchronization, although in most cases this is due to the transfer of existing algorithms to the Microsoft Store, and not the convenience of implementation. Others have their own services, in which the player needs to register, but this is also an unnecessary complication. As a result, the most logical solution is, of course, to use a Microsoft account, which the user definitely has, since without it, it is impossible to purchase and install applications from the Windows store. It remains the case for small - to obtain user data to identify it on different devices.
Delving into the question, I learned that Microsoft account data is obtained using the Microsoft Live API, which is associated with some difficulties. The fact is that in order to receive any data about the user, he must log in to his account, and we must get an access token, which will allow us to request them. This procedure is designed for security and is part of the
OAuth authentication standard, but its self-implementation for novice developers seems complicated and inconvenient.
Fortunately, the creators of UWP understood the potential horror that OAuth can cause a novice developer and created an elegant and fairly simple way to work with a Microsoft account (and not only). This tool is called "Account Manager Web Accounts" and that is what we consider next.
Create an empty project and replace the head grid in the MainPage.xaml file with the following 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 is quite simple and does not require an explanation, so go to the event of pressing the button and insert the following code:
AccountsSettingsPane.Show()
Do not forget to add the import of the Windows.UI.ApplicationSettings namespace, otherwise this code will not work.
Now the application can be executed and look at the result.

An empty window is not exactly what we expected, but it is just a shell that you first need to fill. We will fill it with so-called “teams”, which in our case will be a list of accounts available for entry.
The Web Account Manager tool is very flexible and allows you to create these commands either manually or download them from the required provider. In the second case, the FindAccountProviderAsync function is used, where the “
login.microsoft.com ” address
acts as the account provider .
However, it is impossible to fill the panel with commands before it is initialized, so the first thing to do is to subscribe to the corresponding event. Add this line before the panel display code and immediately create the procedure called by the event:
AddHandler AccountsSettingsPane.GetForCurrentView().AccountCommandsRequested, AddressOf BuildPaneAsync … Private Sub BuildPaneAsync(sender As AccountsSettingsPane, e As AccountsSettingsPaneCommandsRequestedEventArgs) ' End Sub
By the way, in the official manual, a subscription to the AccountCommandsRequested event occurs at the moment of switching to the current page, and the reply is made when switching from it. It makes sense if you select a separate page for the authentication process, but I think this approach is not flexible enough.
Fill 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()
And immediately perform a few additional actions:
- Add the Async keyword to the procedure;
- Add import of the Windows.Security.Authentication.Web.Core namespace;
- Create a GetMsaTokenAsync procedure with a command parameter of type WebAccountProviderCommand
We will deal with this code. First, since it may take time to get the list of commands, we need to delay the display of the panel until it is full. For this, a deferred Deferral object is created. Next, using the previously mentioned function FindAccountProviderAsync, the provider is loaded. Note that a parameter with the value "consumers" is also passed to this function. This tells the vendor that we want to get a standard Microsoft account, not an organization account (then there would be “organizations”).
After we create a new command using the received provider, specify the procedure that is called when you click on the command, and add the result to the panel. The last line we inform that the panel is formed and it can be displayed.

Now, after launching and clicking on the “Login” button, a filled-in panel with several login options is displayed. You may be confused by the fact that we added one command to the panel, but there are two on it. In fact, this is another reason why I like the “Web Account Manager”, as it has independently determined the current user account and suggested it as the main login option, and in case the user wants to use another account, it’s created separate option. From the point of view of internal logic, both options are identical and ultimately send us to the GetMsaTokenAsync procedure that we created earlier.
Further actions can be divided into several stages. First, inside GetMsaTokenAsync, you need to make a request to the data provider indicating the type of data we request, and then form the result of this request:
Dim request As WebTokenRequest = New WebTokenRequest(command.WebAccountProvider, "wl.basic") Dim result As WebTokenRequestResult = Await WebAuthenticationCoreManager.RequestTokenAsync(request)
Notice the “wl.basic” parameter that is passed to the first function. This is an indication of the permissions that the application receives when working with user data. By specifying different permissions, we can work with the address book, calendar, photos, and many other data, but we need only the name, avatar, and user ID, so wl.basic is used, which is sufficient in this case. Full list of permissions can be found
here .
After successfully obtaining the result of the request, we can create an object of type WebAccount, which will help to get access tokens for the account:
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
To use this code, you will need to import the
Windows.Security.Credentials and
Windows.Storage namespaces.
The resulting markers are relevant for the current user for a long time, which means we do not need to repeat the login procedure all the time. Instead, we save these tokens to the local settings of the application, in order to use them in the future for background authentication. Actually, we proceed to background authentication by calling the BackgroundConnectUser procedure.
First, add the imports that we need to work further code:
Imports System.Net.Http Imports Windows.Data.Json
Now go to BackgroundConnectUser and load the saved tokens:
Dim providerId As String = ApplicationData.Current.LocalSettings.Values("CurrentUserProviderId") Dim accountId As String = ApplicationData.Current.LocalSettings.Values("CurrentUserId")
If the markers are not empty, then we form the provider and account objects from them to get the token, and also execute the query:
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)
In case of success, you can get an API access token and execute immediate requests:
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
Addresses of requests for receiving user data can be found in the Microsoft Live API documentation, but we are only interested in two: “https://apis.live.net/v5.0/me” for general data and “https: // apis. live.net/v5.0/me/picture ”to get an avatar. The user data is returned in Json format, so a parser is used for convenient parsing.
Finally, you can run the program and make sure that nothing works anyway. This is due to the fact that the Microsoft Live API does not provide data just like that. To use them, an application must have an identifier in Microsoft Live to which data access permissions are then bound. Fortunately, the identifier is generated automatically when creating a new application in the developer’s dashboard. To download this identifier in the current project, simply connect it with the Windows store. To do this, go to the menu "Project> Store> Link application to Store ..." and select the desired name from the list (or register a new one). We don’t need to do anything else, because the necessary data in the Microsoft Live API will be transferred automatically.
Re-run the project and finally see the result.

At this point, consideration of receiving user data could be completed if I set myself the goal of simply retelling material from the official manual.
Below, I will give the complete code of the project with the changes made, and then I will explain every valid change and justify it.
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>
Window Code 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
The page layout has not undergone very large adjustments. The most important difference is the addition of a logout button, as well as strings to track connection status. The remaining changes made the mapping of data more visual because in the end, the task of the page is to demonstrate the result of executing the code in which the main changes took place. Consider them.
First of all, I completely took the logic of working with the web account manager into a separate class. This is done in order to have access to this code from anywhere in the application. For example, in the article “
Advanced Splash
Screen ” I mentioned the lengthy operations that I had to perform inside the splash screen and the process of getting user data in the BackgroundConnectUser is just such an operation.
As a result of transferring the code to a separate class, practically nothing remained in the class of the page itself. All the work inside the page is reduced to the reaction to pressing two buttons and setting the data context for the page (in the procedure MainPage_Loaded). It is not difficult to guess that the instance of the UserManager class in which the main work already takes place acts as the data context. The class implements the INotifyPropertyChanged interface, which allows binding to its fields from the page markup.
The migrated code received some changes, and I will try to parse them in the same order in which the initial example was created:
- ConnectUser procedure. It essentially duplicates the code that was previously called by clicking on the "Login" button. Calling this procedure is needed only if we do not have markers for the background connection.
- BuildPaneAsync has received some significant, and not so changes. To begin, unsubscribe from the AccountCommandsRequested event, which we do not need after calling this procedure. Next comes the already known code, which is complemented by the installation of a subtitle in the dispatcher window and the addition of a link to the privacy policy. Both of these items are optional and are implemented entirely at your discretion. Moreover, in the second case, you can add a link that executes any of your code, and it is not necessarily a link to a page with a privacy policy.

- After clicking on the “Continue” button, the execution of the GetMsaTokenAsync procedure starts, so the first line in it sets the Logon status, to which you can respond to display progress indicators or block interface elements. Then comes the code already familiar to us, but with the addition of a reaction to situations where the input was canceled or did not pass successfully. I admit, over this seemingly banal place I fought the whole day. The hitch is that when you first execute this code, immediately after the window with the choice of login options, another window appears asking for permission to access the data. Between switching between these two windows, a relatively long period of time (up to 2 seconds) can pass, therefore it is logical to maintain the Logon status during this period in order to avoid re-pressing the “Login” button. However, the user may refuse to provide access to the data, or simply close the second window (press the "Back" button on the mobile device), so in the case of such actions you need to return the initial status to None, which is what we are doing in the Else block.
- The BackgroundConnectUser procedure in the new variation has become an asynchronous function, so that we can appropriately respond to an unsuccessful (or successful) attempt to get background data. Since this function can be called from different places (for example, from the splash screen), we must make sure that the loaded access tokens are not empty. For the convenience of loading saved markers, a separate GetWebAccount function has been created and, if there are no markers in the local settings, then it returns Nothing. If the WebAccount object is unsuccessful, we return the default result (False), and if it is received successfully, set the status in the Logon and continue the procedure. Then we repeat the already known actions with the only difference that in case of a denial of receipt of the token, you need to return the status to None, and with a successful request, Ssuccessful.
- The LogOutUser procedure contains code not used previously. It implements the ability to exit your account and delete markers from saved settings. Be sure to implement this feature in your application, as this will give the user complete freedom in terms of ensuring privacy, not to mention the potential need to log in with other credentials.
- Well, the last thing to mention is the ConnectStatus property that I created in this class. This property is of the type of its own enumerator UserConnectStatusEnum and is required to set the login status. This is an important element of interaction with the class, because by binding to this property we can block buttons that are not desirable to be pressed at the moment of executing certain sections of the code or, on the contrary, unblock those that are available only after completing the input. So that the binding of the IsEnabled property to ConnectStatus is interpreted correctly, the ConnectStatusToEnabledConverter converter is also created.
This is where my implementation for Web Account Manager ends. - - , , , , , .
, , . .