Reputation: 7097
I have a class that I created to consume a REST API. I wrote the class to communicate asynchronously with the web service, since I didn't originally think I needed to have anything run synchronized. Now I am experiencing a situation where I realized using an asynchronous method is not ideal for one particular situation in my application since it runs out of order and causes exceptions since the application is attempting to call a method that it is not ready for. I'm not 100% sure why this is happening, but I think it's due to these methods being called in async void
events within my UI. Here are some code snippets that show an example of the situation:
class MyForm : Form
{
private RestConnection connection;
private async void MyForm_Load(object sender, EventArgs e)
{
if(connection == null)
using (LogOnDialog logOnDialog = new LogOnDialog())
{
var result = logOnDialog.ShowDialog(this);
if(result == DialogResult.OK)
{
connection = logOnDialog.Connection;
}
}
formComboBox.DataSource = await connection.GetChoices();
}
}
class LogOnDialog : Form
{
public RestConnection Connection {private set;get;}
private async void saveButton_Click(object sender, EventArgs e)
{
RestConnection conn = new RestConnection(userNameTB.Text, passwordTb.Text);
await conn.LogIn();
if(conn.LoggedIn) //issue here
{
Connection = conn;
DialogResult = DialogResult.OK;
this.Close();
}
else
{
Connection = null;
DialogResult = DialogResult.Abort;
MessageBox.Show("Invalid Credentials, Try Again.");
}
}
}
What's happening is that the application is attempting to call connection.GetOptions(), but connection is still null because the LogOnDialog's async event that creates the connection and check's for a successful login before allowing the connection to be offered to the caller. However, since connection is null since the Click event hasn't completed a NullReferenceException is called. Additionally, if I continue past and ignore the exception an ObjectDisposedException is thrown since we're now outside of the using block.
I attempted to force the logon to be synchronous by removing the async keyword from the event, and calling Wait() on the login Method. This caused a deadlock. I also tried to capture the task using the below code, and spinwait for it:
Task t = conn.LogOn();
while(!t.IsCompleted)
Thread.Sleep(50);
This didn't deadlock, but it did spin forever. Every time I checked the breakpoint on the While condition the Task's status was always WAITINGFORACTIVATION and essentially locked up the application. In order to get this working I'm going to create some synchronous methods for this situation, but what would allow this to work properly and be async all the way?
EDIT: Additional Code Snippet as Requested for LogOn() and GetOptions()
class RestConnection
{
private string user;
private string password
private XDocument convertToXDoc(string functionName, IDictionary<string,string> parameters) {} //not shown, but this just creates an XML document in the required format for the REST service to consume.
private async Task<XDocument> SendCommand(XDocument commandDocument)
{
XDocument responseData = null;
byte[] data = Encoding.UTF8.GetBytes(commandDoc.ToString());
HttpWebRequest request = WebRequest.CreateHttp(this.serverUrl);
request.Method = "POST";
request.ContentType = "text/xml";
request.ContentLength = data.Length;
using (var requestStream = await request.GetRequestStreamAsync())
{
await requestStream.WriteAsync(data, 0, data.Length);
}
HttpWebResponse response = await request.GetResponseAsync() as HttpWebResponse;
using (var responseStream = response.GetResponseStream())
{
responseData = XDocument.Load(responseStream);
}
return responseData;
}
public async Task LogIn()
{
var parameters = new Dictionary<string, string>();
parameters.Add("USERNAME", userName);
parameters.Add("PASSWORD", passWord);
parameters.Add("CORELICTYPE", String.Empty);
parameters.Add("REMOTEAUTH", "False");
var xmlCommand = ConvertMethodToXml("LoginUserEx3", parameters);
var response = await SendCommand(xmlCommand);
//read response
switch (response.Root.Element("RESULTS").Element("RESULTVAL").Value)
{
case "0":
sessionId = response.Root.Element("SESSIONID").Value;
pingRequired = response.Root.Element("PINGTIME").Value != "0";
if (pingRequired)
{
pingInterval = int.Parse(response.Root.Element("PINGTIME").Value);
pingTimer = new Timer(pingInterval);
pingTimer.Elapsed += PingServerRequired;
pingTimer.Start();
}
loggedIn = true;
break;
//removed other cases for example since they all throw exceptions
default:
loggedIn = false;
throw new ConnectionException("Error");
}
}
}
The GetOptions() in the same format as the LogIn() method, except it returns a Task<List<Options>>
from parsing the returned XDocument.
Upvotes: 1
Views: 172
Reputation: 13976
The problem is here:
{
Connection = null;
DialogResult = DialogResult.Abort; //<<------ this
MessageBox.Show("Invalid Credentials, Try Again.");
}
Assigning DialogResult
will automatically close your form with the result you pass in. Remove that line and you will be fine (especially if you want the dialog to never close).
Upvotes: 1