Using VS2013, WPF 4.5, Caliburn Micro. I want to replace the old method for multithreading for tcp data listener on my working client app with the new "async await".
Since I am not familiar with them, I bought and read the book "Concurrency in C# cookbook" (Author: Mr. Stephen Cleary) and C# in Depth Third Edition (Author: Mr. Jon Skeet). Then I write a sample code (see below) first, because I don't want to mess up my client app that run on customer machines.
The problem: After I click the Connect-Button, the GUI is blocked/frozen. Click any button even the "x" button can't help. I need to kill the process using Task Manager.
What I have tried till now but still find no answer nor sample code that helps:
- read the book of Mr. Cleary and also his blog for TCP Socket FAQ.
- read the book of Mr. Skeet Part 5 Chapter 15.
- contacted Mr. Cleary via E-Mail (get it from his blog), still no answer; maybe he is still busy.
Therefore I ask here. Please tell me how to solve the problem and feel free to modify my code. Thank you in advance
ShellViewModel
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using Caliburn.Micro;
namespace TcpClientAsyncAwait.ViewModels
{
public class ShellViewModel : Conductor<Screen>
{
private const Int64 MAX_DATA_LENGTH = 1024;
private const String CRLF = "\r\n";
private TcpClient _client;
private List<String> _responseData;
private Boolean _shouldStop;
private NetworkStream _stream;
private String _cmdConnect = "<StartConnect/>";
public TcpClient Client
{
get { return _client; }
set
{
_client = value;
NotifyOfPropertyChange(() => Client);
}
}
public List<String> ResponseData
{
get { return _responseData; }
set
{
_responseData = value;
NotifyOfPropertyChange(() => ResponseData);
}
}
public ShellViewModel()
{
}
public void Connect()
{
var hostIp = "127.0.0.1";
var port = 16770;
_client = new TcpClient(hostIp, port);
var listenTask = StartListenerAsync();
SendCmd(_cmdConnect);
}
private async Task StartListenerAsync()
{
_shouldStop = false;
if (_client != null)
{
while (!_shouldStop)
{
await GetMessageAsync().ConfigureAwait(false);
}
}
}
private async Task GetMessageAsync()
{
try
{
if (_stream == null) _stream = _client.GetStream();
if (_stream.DataAvailable)
{
Byte[] buffer = new Byte[MAX_DATA_LENGTH];
Int32 readBytes = await _stream.ReadAsync(buffer, 0, buffer.Length);
var receivedData = UTF8Encoding.UTF8.GetString(buffer, 0, readBytes);
Trace.WriteLine(receivedData);
}
}
catch (Exception ex)
{
Trace.WriteLine(ex.Message);
}
}
private async Task SendCmd(string cmd)
{
if (_client != null)
{
// Manipulate command to fulfill format, that can be accepted by server
Byte[] utf8Source = UTF8Encoding.UTF8.GetBytes(cmd);
Byte[] suffix = UTF8Encoding.UTF8.GetBytes(CRLF);
Byte[] utf8Result = new byte[utf8Source.Length + suffix.Length];
Buffer.BlockCopy(utf8Source, 0, utf8Result, 0, utf8Source.Length);
Buffer.BlockCopy(suffix, 0, utf8Result, utf8Source.Length, suffix.Length);
if (_stream == null) _stream = _client.GetStream();
using (var sw = new StreamWriter(_stream))
{
var data = UTF8Encoding.UTF8.GetString(utf8Result).ToCharArray();
await sw.WriteAsync(data, 0, utf8Result.Length).ConfigureAwait(false);
await sw.FlushAsync().ConfigureAwait(false);
}
}
}
public void Disconnect()
{
_shouldStop = true;
if (_client != null) _client.Close();
}
}
}
ShellView
<Window x:Class="TcpClientAsyncAwait.Views.ShellView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="300"
d:DesignWidth="300"
mc:Ignorable="d">
<Grid Width="300" Height="300">
<StackPanel Width="130" Height="120"
HorizontalAlignment="Center" VerticalAlignment="Center">
<Button x:Name="Connect"
Width="120" Height="50" Margin="5"
HorizontalAlignment="Center" VerticalAlignment="Center"
Content="Connect" />
<Button x:Name="DisConnect"
Width="120" Height="50" Margin="5"
HorizontalAlignment="Center" VerticalAlignment="Center"
Content="Disconnect" />
</StackPanel>
</Grid>
</Window>
EDIT:
Sample code is edited by adding namespace to ViewModel and XAML header to View due to completeness.
EDIT1: OK, I get a tcp server sample code from MSDN, that works as server for my client sample code. Here the steps to compile the server sample code.
- Download the C# version of that project from the link.
- Using VS2013: open the ClientApp.sln.
- In Solution Explorer: open project ServerApp and open Form1.cs to edit.
- Goto line 30 and edit the port 3000 to 16770. Save the change.
- Make the project ServerApp as startup project.
- Build and run it.
Now the steps how to create and compile my client sample code, so you can reproduce the problem I have. Please follow the steps exactly.
- Open VS2013
- Get "SideWaffle Template Pack" Extension from Visual Studio Gallery, install it and restart VS2013.
- Create "New Project" - choose "Caliburn.Micro WPF Application" project from SideWafle (see here)
Set the solution name as TcpClientAsyncAwait
- Click "Tools - NuGet Package Manager - Package Manager Console". On the Window of Package Manage Console you will see a warning "Some NuGet packages are missing from this solution. Click to restore from your online package sources.". Just follow the instruction by clicking the Restore-Button
- After the download is completed, build the solution (usually I press "F6").
- In Solution Explorer: open "View" folder and open the ShellView.xaml. You will see ShellView.xaml.cs (the code behind). Just delete that file.
- Open ShellView.xaml and delete all code in it. Now you can copy & paste my sample code ShellView.xaml completely there.
- Open "ViewModels" folder and open ShellViewModel.cs to edit.
- Delete all code in it and copy & paste my sample code ShellViewModel.cs there.
- Build it.
Note: Please run the server before you run the client, because my sample code is so simple and has no reconnect and no "no-server" detection mechanism.
EDIT2:
According suggestion from TedyPranolo it seems the loop could be the problem.
If I remove the loop the await GetMessageAsync().ConfigureAwait(false);
is executed only once then ends. When the server sends another message later time, the client doesn't get the message anymore.
Q1: Now I want to know how to keep GetMessageAsync alive and always be ready to listen incoming message from server all the time till the application is closed. of course still in context "async await"
Q2: Or maybe "async await" does not fit for such purpose at all?
I am glad to get some solution suggestions from you all. Thank you in advance.