Reputation: 392
I'm just starting to deal with Asynchronous programming in c#, and I started reading about async methods and await.
In this block of code below, the WPF application takes an input from the user, saves it to a file in the Bin directory, and reads it back to the textbox. I had to use async
methods to read and write, but I also need to implement await
in the methods inside the WriteText
and ReadText
methods.
Can you give me a brief explanation on how I should implement the use of async and await in this code?
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private async void btnWriteFile_Click(object sender, RoutedEventArgs e)
{
await WriteFile();
}
private async void btnReadFile_Click(object sender, RoutedEventArgs e)
{
await ReadFile();
}
public async Task WriteFile()
{
string filePath = @"SampleFile.txt";
string text = txtContents.Text;
Task task1 = new Task( () => WriteTextAsync(filePath, text));
}
private async Task WriteTextAsync(string filePath, string text)
{
byte[] encodedText = Encoding.Unicode.GetBytes(text);
using (FileStream sourceStream = new FileStream(filePath,
FileMode.Create, FileAccess.Write, FileShare.None,
bufferSize: 4096, useAsync: true))
{
//sourceStream.BeginWrite(encodedText, 0, encodedText.Length);
await ?? sourceStream.BeginWrite(encodedText, 0, encodedText.Length, null, null);
};
}
public async Task ReadFile()
{
string filePath = @"SampleFile.txt";
if (File.Exists(filePath) == false)
{
MessageBox.Show(filePath + " not found", "File Error", MessageBoxButton.OK);
}
else
{
try
{
string text = await ReadText(filePath);
txtContents.Text = text;
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
}
private async Task<string> ReadText(string filePath)
{
using (FileStream sourceStream = new FileStream(filePath,
FileMode.Open, FileAccess.Read, FileShare.Read,
bufferSize: 4096))
{
StringBuilder sb = new StringBuilder();
byte[] buffer = new byte[0x1000];
int numRead;
while ((numRead = sourceStream.Read(buffer, 0, buffer.Length)) != 0)
{
string text = Encoding.Unicode.GetString(buffer, 0, numRead);
sb.Append(text);
}
return sb.ToString();
}
}
}
Upvotes: 1
Views: 922
Reputation: 113392
Let's take them one at a time:
public async Task WriteFile()
{
string filePath = @"SampleFile.txt";
string text = txtContents.Text;
Task task1 = new Task( () => WriteTextAsync(filePath, text));
}
What's task1 doing here? You need to actually have run it and await on it:
public async Task WriteFile()
{
string filePath = @"SampleFile.txt";
string text = txtContents.Text;
Task task1 = new Task( () => await WriteTextAsync(filePath, text));
await task1;
}
But wait! We're creating a Task
that creates a Task
and then waits on that Task
. Why not just return the Task
in the first place?
public Task WriteFile()
{
string filePath = @"SampleFile.txt";
string text = txtContents.Text;
return WriteTextAsync(filePath, text);
}
Remember, async
makes it easier for us to create methods that perform something in a Task
, but if you have already have a Task
then it's a waste of time.
Also, as a matter of convention you should name your asynchronous methods to all end in Async
. This is even more so here, because you differ from the other WriteTextAsync
only in signature:
public Task WriteTextAsync()
{
return WriteTextAsync(@"SampleFile.txt", txtContents.Text);
}
Really this is no different to how if you had a non-async void WriteText(string filePath, string text)
you would call it from a non-async void WriteText()
. Nothing actually new here.
Now, onto that more involved WriteTextAsync
:
Because we've now got Tasks we don't need to use the old BeginWrite
at all (but see below), we just use WriteAsync
analogously to how we use Write
:
private async Task WriteTextAsync(string filePath, string text)
{
byte[] encodedText = Encoding.Unicode.GetBytes(text);
using (FileStream sourceStream = new FileStream(filePath,
FileMode.Create, FileAccess.Write, FileShare.None,
bufferSize: 4096, useAsync: true))
{
await sourceStream.WriteAsync(encodedText, 0, encodedText.Length);
}
}
ReadFile()
is fine. Let's look at what it calls into:
private async Task<string> ReadText(string filePath)
{
using (FileStream sourceStream = new FileStream(filePath,
FileMode.Open, FileAccess.Read, FileShare.Read,
bufferSize: 4096))
{
StringBuilder sb = new StringBuilder();
byte[] buffer = new byte[0x1000];
int numRead;
while ((numRead = sourceStream.Read(buffer, 0, buffer.Length)) != 0)
{
string text = Encoding.Unicode.GetString(buffer, 0, numRead);
sb.Append(text);
}
return sb.ToString();
}
}
This will work, but it won't gain anything. However, we can replace the Read
with await
ing ReadAsync
:
private async Task<string> ReadText(string filePath)
{
using (FileStream sourceStream = new FileStream(filePath,
FileMode.Open, FileAccess.Read, FileShare.Read,
bufferSize: 4096))
{
StringBuilder sb = new StringBuilder();
byte[] buffer = new byte[0x1000];
int numRead;
while ((numRead = await sourceStream.ReadAsync(buffer, 0, buffer.Length)) != 0)
{
string text = Encoding.Unicode.GetString(buffer, 0, numRead);
sb.Append(text);
}
return sb.ToString();
}
}
A better overall solution to the non-async version, simpler and more resilient in the face of encodings that might have characters split by such a Read
, would have been to use ReadToEnd()
. Likewise a better version here is to use ReadToEndAsync()
:
private async Task<string> ReadText(string filePath)
{
using (FileStream sourceStream = new FileStream(filePath,
FileMode.Open, FileAccess.Read, FileShare.Read,
bufferSize: 4096))
{
using(var rdr = new StreamReader(sourceStream, Encoding.Unicode))
{
return await rdr.ReadToEndAsync();
}
}
}
Note that while we're returning the result of await
ing a task, we cannot in this case just replace this with return rdr.ReadToEndAsync()
because then we'll leave the using
before the ReadToEndAsync()
has actually finished. We need await
to make sure we've got the actual result of that and then do the IDisposable.Dispose()
calls that leaving a using
invokes.
BeginXxx
… EndXxx
):Let's imagine we didn't have stream.WriteAsync()
and had to use stream.BeginWrite()
and stream.EndWrite()
. Something like this can happen if you are using an older library.
We can use TaskFactory.FromAsync
to create a Task
that wraps the old approach in the new. Hence:
private async Task WriteTextAsync(string filePath, string text)
{
byte[] encodedText = Encoding.Unicode.GetBytes(text);
using (FileStream sourceStream = new FileStream(filePath,
FileMode.Create, FileAccess.Write, FileShare.None,
bufferSize: 4096, useAsync: true))
{
await Task.Factory.FromAsync(sourceStream.BeginWrite, sourceStream.EndWrite, encodedText, 0, encodedText.Length, null);
}
}
And:
private async Task<string> ReadText(string filePath)
{
using(FileStream sourceStream = new FileStream(filePath,
FileMode.Open, FileAccess.Read, FileShare.Read,
bufferSize:4096))
{
StringBuilder sb = new StringBuilder();
byte[] buffer = new byte[0x1000];
int numRead;
while((numRead = await Task<int>.Factory.FromAsync(sourceStream.BeginRead, sourceStream.EndRead, buffer, 0, buffer.Length, null)) != 0)
{
sb.Append(Encoding.Unicode.GetString(buffer, 0, numRead);
}
return sb.ToString();
}
}
This is a clearly a lot more convoluted than just having an XxxAsync
method we can await
, but it's still simpler than calling BeginXxx
and then handling EndXxx
in a callback, especially in cases like the ReadText
above where this then has to lead to another loop into BeginXxx
.
Upvotes: 4
Reputation: 1683
The FileStream class has asynchronous methods for reading and writing to the stream: ReadAsync
and WriteAsync
, so all you need to do is swap out those methods in your code, and prefix them with await:
private async Task<string> ReadText(string filePath)
{
using (FileStream sourceStream = new FileStream(filePath,
FileMode.Open, FileAccess.Read, FileShare.Read,
bufferSize: 4096))
{
StringBuilder sb = new StringBuilder();
byte[] buffer = new byte[0x1000];
int numRead;
while ((numRead = await sourceStream.ReadAsync(buffer, 0, buffer.Length)) != 0)
{
string text = Encoding.Unicode.GetString(buffer, 0, numRead);
sb.Append(text);
}
return sb.ToString();
}
}
and
private async Task WriteTextAsync(string filePath, string text)
{
byte[] encodedText = Encoding.Unicode.GetBytes(text);
using (FileStream sourceStream = new FileStream(filePath,
FileMode.Create, FileAccess.Write, FileShare.None,
bufferSize: 4096, useAsync: true))
{
await sourceStream.WriteAsync(encodedText, 0, encodedText.Length);
};
}
I'm sure these two methods can be simplified further, but this should get you started on the async methods.
And just so you're aware, if you were trying to use a class that didn't have async methods, and you wanted to perform that task on a separate thread, you could still make use of async/await in this fashion:
private async Task<string> ReadText(string filePath)
{
return await Task.Run(() =>
{
return File.ReadAllText("textfilepath.txt");
});
}
Upvotes: 0