question

Patches avatar image
Patches asked

Not receiving API receipts in C# using Microsoft Forms

Hi there, I am testing out PlayFab and wanted to learn by making a simple C# app using Microsoft Forms in Visual Studio. I followed the instructions from the C# console application tutorial, with a few modifications to use Forms and a GUI instead of command line.

For this test, I made a simple app using NuGet to install NewtonSoft.JSON and PlayFab.AllSDKs.

The form is very simple: there is one button to submit the login command and a textbox to populate with log messages in the GUI. There is also a textbox which is used to enter a custom userID for testing the function "PlayFabClientAPI.LoginWithCustomIDAsync(request);"

My issue is that although everything seems to work in regards to sending the API call out, the application never receives the asynchronous reply with the confirmation receipt. I am new to asynch requests in C#, so I haven't yet been able to figure out why these responses aren't coming. To prevent my application from hanging forever in the wait loop, I added a timeout counter of 25 seconds. Even with all this time, the application doesn't seem to receive the async reply.

I have pasted the two relevant .cs files below. My project.cs and my form.cs. If you are familiar with the C# Getting Started tutorial where you build a console app that calls the API, most of this code will be familiar to you.

This is the Form's cs file which calls functions in the program and updates the GUI

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;


namespace CSharpGettingStarted
{
    public partial class FormMainWindow : Form
    {
        public FormMainWindow()
        {
            InitializeComponent();
        }


        private void FormMainWindow_Load(object sender, EventArgs e)
        {
            Console.WriteLine("Form Loaded Successfully!");
            string[] startupMessages = { "Test App Loaded.", "Type in a new username or your old one.", "Then press the button to log in." };
            printMultiGuiLog(startupMessages);

        }


        public void printGuiLog(string message)
        {
            if (message != null)
                textBoxGuiConsole.Text = textBoxGuiConsole.Text + message + Environment.NewLine;
            else
                textBoxGuiConsole.Text = "Error: no message to display.";
        }


        public void printMultiGuiLog(params string[] theMessages)
        {
            string newLog = string.Join(Environment.NewLine, theMessages);
            Console.Write(newLog);
            printGuiLog(newLog);
        }


        private void buttonLogIn_Click(object sender, EventArgs e)
        {
            string CustomUserID = textBoxUsernameField.Text;
            
            Program.OnLoginAttempt(CustomUserID);
        }
    }
}


This is the Program.cs file with the functions that use the PlayFab SDK

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using PlayFab;
using PlayFab.Internal;
using PlayFab.ClientModels;
using CSharpGettingStarted;
using System.Windows.Forms;


public static class Program
{
    private static bool _running = true;
    public static FormMainWindow myForm; // <-- this is the main GUI


    [STAThread]
    static void Main(string[] args)
    {
        PlayFabSettings.TitleId = "1234"; // NOTE: I have changed this value to my own titleId just not for pasting in the forums


        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);


        myForm = new FormMainWindow();
        Application.Run(myForm);


    }




    public static void OnLoginAttempt(string CustomUserID)
    {


        myForm.printGuiLog("Attempting to Log In as " + CustomUserID + "...");
        


        _running = true;


        var request = new LoginWithCustomIDRequest { CustomId = CustomUserID, CreateAccount = true};
        var loginTask = PlayFabClientAPI.LoginWithCustomIDAsync(request);


        int n = 1; // n is the timeout counter
        while (_running && n < 25000) //we're only waiting 25 seconds to get a reply. If none comes, don't let the program hang.
        {
            if (loginTask.IsCompleted) // You would probably want a more sophisticated way of tracking pending async API calls in a real game
            {
                OnLoginComplete(loginTask);
            }
            n++; //increment our timeout counter
            Thread.Sleep(1); 
        }


        Console.Write("It looks like you timed out");
        string[] timeOutMessages = { "API Request timed out after 25000 ms.", "Login attempted with UserID:" + CustomUserID + "." };
        myForm.printMultiGuiLog(timeOutMessages);
    }


    private static void OnLoginComplete(Task<PlayFabResult<LoginResult>> taskResult)
    {
        var apiError = taskResult.Result.Error;
        var apiResult = taskResult.Result.Result;


        if (apiError != null)
        {
            myForm.printGuiLog(PlayFabUtil.GetErrorReport(apiError));
        }
        else if (apiResult != null)
        {
            myForm.printGuiLog("You made a successfull API call!");
        }
        else
        {
            Console.Write("ERROR: You called the OnLoginComplete without a result!");
        }


        _running = false; // Because this is just an example, successful login triggers the end of the program
    }
}

Thanks for reading.

apis
10 |1200

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

1807605288 avatar image
1807605288 answered

The tricky part is that a console application is inherently synchronous, and our API is asynchronous. Asynchronous is the accepted pattern for C#, but console applications didn't support it properly for a long time.

One possible hangup with the console example is that if your PlayFab API call throws an error (for example if you do not set a TitleId), then the exception is not captured and displayed, because it's in another thread. The main thread spins forever (it is not hung, but it doesn't do anything, so it seems to be hung), because it's waiting for an API call that never happens.

We will make a backlog task to improve the CSharp example and demonstrate some error-case handling. There is no ETA for this however, sorry.

1 comment
10 |1200

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

Patches avatar image Patches commented ·

Thanks for the reply. It would be great to have an example that is not just a console application but one that uses more event handlers like a Forms application. That way it would be easier to see how to call those async functions from something like a button click.

I will make a new reply below with my code now that I think I've figured out a pattern that can work.

0 Likes 0 ·
Patches avatar image
Patches answered

I am still not getting responses from the API call in my code above using a Form. I do, however, get apiResults from the Console application version of the code from the "Getting Started" example. I can receive the apiResult and parse it for a PlayFabId to feed back to the console.

Going through Microsoft's documentation on C# async programming patterns as I still try to wrap my head around async. It appears that the console application code example has a few workarounds that don't work as soon as you want to separate functions out via normal UI functions like a button click. Can someone provide a clear pattern for calling async API calls using a form control or let me know why the above code doesn't ever return an apiResult/apiError?

10 |1200

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

Patches avatar image
Patches answered

Finally solved this after about 10 hours of going through MSDN and reading several long blog posts about asynch patterns. It's really disappointing that the playfab "Getting Started" guide doesn't use a setup that shows how to properly call and listen for asynchronous methods. Using a while loop and listening for a bool is a pattern that falls apart the moment you want any kind of complexity.

I ended up rewriting the above code to use asynch calls all the way down and get away from the coroutine pattern. It works fine now. If anyone's interested I will post the revised code. However, I want to stress again that it would be really nice to see a doc on some recommended methods for asynch playfab API calls in C#.

10 |1200

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

Patches avatar image
Patches answered

In response to my own post, I think I've figured out a pattern that will allow the UI controls in the windows form to call events that call asynchronous functions on the program which don't hang the program. I'm leaving the code here for other people who come across it. (At least until there are more examples in the C# getting started demo - as mentioned by Paul above).

Here's the new code for the Form.cs file. Note in particular the events called from the button click are now async. It's async all the way down...

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;




namespace CSharpGettingStarted
{
    public partial class FormMainWindow : Form
    {
        public FormMainWindow()
        {
            InitializeComponent();
        }


        private void FormMainWindow_Load(object sender, EventArgs e)
        {
            Console.WriteLine("Form Loaded Successfully!");
            string[] startupMessages = { "Test App Loaded.", "Type in a new username or your old one.", "Then press the button to log in." };


            printMultiGuiLog(startupMessages);
        }


        //Event Handler - Click the Log In button to call async login function
        private async void buttonTestAsync_Click(object sender, EventArgs e)
        {
            //Since we asynchronously wait, the UI thread is not blocked by the API request.
            string ID = await Program.OnLoginAttemptAsync(textBoxUsernameField.Text);
            printGuiLog(Environment.NewLine + "Async Test Success with ID: " + ID);   //GUI log of result


            string newsUpdate = await Program.GetNewsUpdate();
            printMultiGuiLog(Environment.NewLine + "---TITLE NEWS BEGIN---" + Environment.NewLine);
            printGuiLog(newsUpdate);    //GUI log of Title News
            printMultiGuiLog(Environment.NewLine + "---END OF TITLE NEWS---" + Environment.NewLine);


            string[] inventoryItems = await Program.GetPlayerInventory();
            printMultiGuiLog(Environment.NewLine + "---PLAYER INVENTORY---" + Environment.NewLine);
            printMultiGuiLog(inventoryItems);
            printMultiGuiLog(Environment.NewLine + "---END INVENTORY---" + Environment.NewLine);
        }


        //GUI Function - log messages on GUI
        public void printGuiLog(string message)
        {
            if (message != null)
                textBoxGuiConsole.Text = textBoxGuiConsole.Text + message + Environment.NewLine;
            else
                textBoxGuiConsole.Text = "Error: no message to display.";
        }
        //GUI Function - accept array of strings and display as log messages with new lines
        public void printMultiGuiLog(params string[] theMessages)
        {
            string newLog = string.Join(Environment.NewLine, theMessages);
            Console.Write(newLog);
            printGuiLog(newLog);
        }


    }
}



Here's the code for the Program.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using PlayFab;
using PlayFab.Internal;
using PlayFab.ClientModels;
using CSharpGettingStarted;
using System.Windows.Forms;


public static class Program
{
    //private static bool _running = true;
    public static FormMainWindow myForm; // <-- this is the main GUI


    [STAThread]
    static void Main(string[] args)
    {
        PlayFabSettings.TitleId = "1234"; // Please change this value to your own titleId from PlayFab Game Manager


        //Forms stuff to make these work
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);


        //Start the Form GUI
        myForm = new FormMainWindow();
        Application.Run(myForm);
    }


    //Call the PlayFab API with a login request asynchronously and return the user's playfab ID as a string
    public static async Task<string> OnLoginAttemptAsync (string CustomUserID)
    {
        var request = new LoginWithCustomIDRequest { CustomId = CustomUserID, CreateAccount = true };
        var loginTask = await PlayFab.PlayFabClientAPI.LoginWithCustomIDAsync(request).ConfigureAwait(false);
        string sessionTicket = loginTask.Result.SessionTicket;
        string myID = loginTask.Result.PlayFabId;
        return myID;
    }


    public static async Task<string> GetNewsUpdate()
    {
        var newsRequest = new GetTitleNewsRequest();
        var newsTask = await PlayFab.PlayFabClientAPI.GetTitleNewsAsync(newsRequest).ConfigureAwait(false);
        string newsString = newsTask.Result.News[0].Title + Environment.NewLine + newsTask.Result.News[0].Body;
        return newsString;
    }


    public static async Task<string[]> GetPlayerInventory()
    {
        var inventoryRequest = new GetUserInventoryRequest();
        var inventoryTask = await PlayFab.PlayFabClientAPI.GetUserInventoryAsync(inventoryRequest).ConfigureAwait(false);


        int inventoryLen = inventoryTask.Result.Inventory.Count;
        string[] itemNames = new string[0];
        if (inventoryLen <= 0)
        {
            //nothign
        }
        else
        {


            ItemInstance[] items = inventoryTask.Result.Inventory.ToArray();
            // Display elements with ForEach.
            itemNames = new string[inventoryLen];
            int n = 0;
            foreach (ItemInstance i in items)
            {
                itemNames[n] = i.DisplayName;
                    n++;
            }
        }


        return itemNames;
    }




}
1 comment
10 |1200

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

1807605288 avatar image 1807605288 ♦ commented ·

I will definitely reference this when I make the official guide, maybe verbatim.

Thanks for posting it.

I'll make sure you get an acknowledgement once it's done.

1 Like 1 ·

Write an Answer

Hint: Notify or tag a user in this post by typing @username.

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.