Note: This tutorial is fairly extensive, if you are after something shorter please also see our Getting Started and How To Create a Client Server Application In Minutes tutorials.
Note: This example is also included in the example bundle when you download NetworkComms.Net. You are welcome to open that example and simply follow this tutorial to see how it was put together.
Before we get started ensure you have Visual Studio 2010 Express or later installed, which should come with .Net 4 or later.
1. Create Visual Studio Project
- Create a new visual studio solution containing a Visual C# ‘WPF Application‘ project naming it ‘WPFFileTransferExample‘.
2. Add NetworkComms.Net DLL to Project
- The NetworkComms.Net download contains DLLs for all supported platforms but we are only interested in the Net40 > Release > Complete DLL. Copy this DLL to the same location as the solution we created in step 1.
- We now need to add a project reference to the NetworkComms.Net DLL we just downloaded. Right click on the ‘WPFFileTransferExample‘ project and select ‘Add Reference…‘. Within the window that opens select the Browse tab and select the DLL we just downloaded.
- If you expand the ‘References‘ folder within the project you should now see the NetworkComms .Net reference you just added like this:
3. Add WPF Elements
- We need to add the text boxes and buttons that we intend to interact with to the WPF layout. To get started double click the ‘MainWindow.xaml‘ file so that it opens in the main viewer.
- If you wanted to you could now add each individual text box and button by hand. To save a little time however we have provided a base layout that you can copy and paste. Copy and paste the following code to replace ALL EXISTING code in the XAML view of ‘MainWindow.xaml‘:
<Window x:Class="WPFFileTransferExample.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="NetworkComms.Net File Transfer Example" Height="450" Width="513" MinWidth="513" MinHeight="450" Background="#FF7CA0FF"> <Window.Resources> <DataTemplate x:Key="UserTemplate" > <Grid HorizontalAlignment="Stretch"> <Grid.Background> <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0"> <GradientStop Color="#FFCFDCFF" Offset="1"/> <GradientStop Color="White" Offset="0"/> </LinearGradientBrush> </Grid.Background> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="110" /> <ColumnDefinition Width="90" /> <ColumnDefinition Width="82" /> </Grid.ColumnDefinitions> <Grid Grid.Column="0"> <TextBlock Margin="5,1,0,0" Text="{Binding Path=Filename}" FontWeight="Bold"/> </Grid> <Grid Grid.Column="1"> <TextBlock Margin="0,1,0,0" HorizontalAlignment="Right" Text="{Binding Path=SourceInfoStr}"/> </Grid> <Grid Grid.Column="2"> <ProgressBar Maximum="1" Value="{Binding Path=CompletedPercent}" HorizontalAlignment="Stretch" Height="15" Margin="5,2,0,0" Foreground="#FFFFDC00" VerticalAlignment="Top"/> </Grid> <Grid Grid.Column="3"> <Button Margin="0,-1,4,0" Height="20" HorizontalAlignment="Right" IsEnabled="{Binding Path=IsCompleted}" Content="Delete"/> <Button Margin="0,-1,46,0" Height="20" HorizontalAlignment="Right" IsEnabled="{Binding Path=IsCompleted}" Content="Save"/> </Grid> </Grid> </DataTemplate> </Window.Resources> <Grid> <Grid.RowDefinitions> <RowDefinition Height="45" /> <RowDefinition Height="27" /> <RowDefinition Height="30" /> <RowDefinition Height="*" /> <RowDefinition Height="170" /> </Grid.RowDefinitions> <TextBlock Grid.Row="0" Height="38" Width="430" HorizontalAlignment="Center" Margin="0,6,0,0" TextWrapping="Wrap" VerticalAlignment="Top" FontWeight="Bold" >Note: NetworkComms.Net is freely available under the terms of the GPLv3. Please see https://networkcomms.net/licensing/ for more information.</TextBlock> <Grid Grid.Row="1"> <Grid.ColumnDefinitions> <ColumnDefinition Width="262" /> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Grid Grid.Column="0"> <TextBox Height="23" HorizontalAlignment="Left" Margin="75,2,0,0" Name="remoteIP" VerticalAlignment="Top" Width="97" /> <Label Content="Remote IP:" Height="28" HorizontalAlignment="Left" Margin="8,0,0,0" Name="label1" VerticalAlignment="Top" /> <TextBox Height="23" HorizontalAlignment="Left" Margin="205,2,0,0" Name="remotePort" VerticalAlignment="Top" Width="47" /> <Label Content="Port:" Height="28" HorizontalAlignment="Left" Margin="172,0,0,0" Name="label2" VerticalAlignment="Top" /> </Grid> <Grid Grid.Column="1"> <CheckBox Content="Use Compression" Name="UseCompression" HorizontalAlignment="Left" Margin="0,5,0,0" VerticalAlignment="Top"/> </Grid> </Grid> <Grid Grid.Row="2"> <Grid.ColumnDefinitions> <ColumnDefinition Width="138" /> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> <Grid Grid.Column="0"> <Button Content="Send File To Remote" HorizontalAlignment="Left" Margin="8,3,0,0" Name="sendFileButton" VerticalAlignment="Top" Height="25" Width="120"/> </Grid> <Grid Grid.Column="1"> <ProgressBar Maximum="1" Name="sendProgress" HorizontalAlignment="Stretch" Height="15" Margin="0,8,8,0" VerticalAlignment="Top" Foreground="#FFFFDC00"/> </Grid> </Grid> <Grid Grid.Row="3"> <Label Content="Received Files:" HorizontalAlignment="Left" Margin="8,0,0,0" VerticalAlignment="Top"/> <ScrollViewer Name ="fileScroller" Margin="0,25,0,0" > <ListBox HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch" IsSynchronizedWithCurrentItem="True" Name="lbReceivedFiles" ItemsSource="{Binding}" ItemTemplate="{StaticResource UserTemplate}" VerticalAlignment="Stretch" Background="#FFCFDCFF" /> </ScrollViewer> </Grid> <Grid Grid.Row="4"> <Label Content="Client Log:" HorizontalAlignment="Left" Margin="8,0,0,0" VerticalAlignment="Top"/> <ScrollViewer Name ="scroller" Margin="0,25,0,0" Background="#FFCFDCFF" > <TextBlock Margin="5,0,0,0" ToolTip="Client Log Output" TextWrapping="Wrap" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Name="logBox" /> </ScrollViewer> </Grid> </Grid> </Window>
- The design window should now show the equivalent of the XAML you have just pasted from above. This gives us the very basic layout of the file transfer application:
- Press ‘F6′ on your keyboard to ensure that the project builds successfully (i.e. the Errors List window in Visual studio remains empty). If the project does not build at this point please go back over this tutorial and make sure you have completed all of the necessary steps.
4. Add ReceivedFile Class
- The next step is to create a class for the files we will be receiving. Right click on the project and select ‘Add‘ > ‘New Item…‘. This should bring up the ‘Add New Item‘ window, a list of options that you can add to the project. Ensure that ‘Class‘ item is selected, and at the bottom of the window enter the name ‘ReceivedFile.cs‘. Now click ‘Add‘. The new class file should open automatically and you should now have something like this:
- Copy and paste the following code, replacing ALL EXISTING code in the class we just created, ‘ReceivedFile.cs‘:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using NetworkCommsDotNet; using System.ComponentModel; using System.IO; namespace WPFFileTransferExample { /// <summary> /// A local class which can be used to populate the WPF list box /// </summary> class ReceivedFile : INotifyPropertyChanged { /// <summary> /// The name of the file /// </summary> public string Filename { get; private set; } /// <summary> /// The connectionInfo corresponding with the source /// </summary> public ConnectionInfo SourceInfo { get; private set; } /// <summary> /// The total size in bytes of the file /// </summary> public long SizeBytes { get; private set; } /// <summary> /// The total number of bytes received so far /// </summary> public long ReceivedBytes { get; private set; } /// <summary> /// Getter which returns the completion of this file, between 0 and 1 /// </summary> public double CompletedPercent { get { return (double)ReceivedBytes / SizeBytes; } //This set is required for the application to work set { throw new Exception("An attempt to modify read-only value."); } } /// <summary> /// A formatted string of the SourceInfo /// </summary> public string SourceInfoStr { get { return "[" + SourceInfo.RemoteEndPoint.ToString() + "]"; } } /// <summary> /// Returns true if the completed percent equals 1 /// </summary> public bool IsCompleted { get { return ReceivedBytes == SizeBytes; } } /// <summary> /// Private object used to ensure thread safety /// </summary> object SyncRoot = new object(); /// <summary> /// A memory stream used to build the file /// </summary> Stream data; /// <summary> ///Event subscribed to by GUI for updates /// </summary> public event PropertyChangedEventHandler PropertyChanged; /// <summary> /// Create a new ReceivedFile /// </summary> /// <param name="filename">Filename associated with this file</param> /// <param name="sourceInfo">ConnectionInfo corresponding with the file source</param> /// <param name="sizeBytes">The total size in bytes of this file</param> public ReceivedFile(string filename, ConnectionInfo sourceInfo, long sizeBytes) { this.Filename = filename; this.SourceInfo = sourceInfo; this.SizeBytes = sizeBytes; //We create a file on disk so that we can receive large files data = new FileStream(filename, FileMode.Create, FileAccess.ReadWrite, FileShare.Read, 8 * 1024, FileOptions.DeleteOnClose); } /// <summary> /// Add data to file /// </summary> /// <param name="dataStart">Where to start writing this data to the internal memoryStream</param> /// <param name="bufferStart">Where to start copying data from buffer</param> /// <param name="bufferLength">The number of bytes to copy from buffer</param> /// <param name="buffer">Buffer containing data to add</param> public void AddData(long dataStart, int bufferStart, int bufferLength, byte[] buffer) { lock (SyncRoot) { data.Seek(dataStart, SeekOrigin.Begin); data.Write(buffer, (int)bufferStart, (int)bufferLength); ReceivedBytes += (int)(bufferLength - bufferStart); //Ensure the data is correctly flushed if we have received everything if (ReceivedBytes == SizeBytes) data.Flush(); } NotifyPropertyChanged("CompletedPercent"); NotifyPropertyChanged("IsCompleted"); } /// <summary> /// Saves the completed file to the provided saveLocation /// </summary> /// <param name="saveLocation">Location to save file</param> public void SaveFileToDisk(string saveLocation) { if (ReceivedBytes != SizeBytes) throw new Exception("Attempted to save out file before data is complete."); if (!File.Exists(Filename)) throw new Exception("The transferred file should have been created within the local application directory. Where has it gone?"); File.Delete(saveLocation); File.Copy(Filename, saveLocation); } /// <summary> /// Closes and releases any resources maintained by this file /// </summary> public void Close() { try { data.Dispose(); } catch (Exception) { } try { data.Close(); } catch (Exception) { } } /// <summary> /// Triggers a GUI update on a property change /// </summary> /// <param name="propertyName"></param> private void NotifyPropertyChanged(string propertyName = "") { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } }
5. Add SendInfo Class
- The next step is to create a wrapper class for the data we will be sending. Right click on the project and select ‘Add‘ > ‘New Item…‘. This should bring up the ‘Add New Item‘ window, a list of options that you can add to the project. Ensure that ‘Class‘ item is selected, and at the bottom of the window enter the name ‘SendInfo.cs‘.
- As we just did in step 4 copy and paste the following code, replacing ALL EXISTING code in the class we just created, ‘SendInfo.cs‘:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using ProtoBuf; namespace WPFFileTransferExample { /// <summary> /// Information class used to associate incoming data with the correct ReceivedFile /// </summary> [ProtoContract] class SendInfo { /// <summary> /// Corresponding filename /// </summary> [ProtoMember(1)] public string Filename { get; private set; } /// <summary> /// The starting point for the associated data /// </summary> [ProtoMember(2)] public long BytesStart { get; private set; } /// <summary> /// The total number of bytes expected for the whole ReceivedFile /// </summary> [ProtoMember(3)] public long TotalBytes { get; private set; } /// <summary> /// The packet sequence number corresponding to the associated data /// </summary> [ProtoMember(4)] public long PacketSequenceNumber { get; private set; } /// <summary> /// Private constructor required for deserialisation /// </summary> private SendInfo() { } /// <summary> /// Create a new instance of SendInfo /// </summary> /// <param name="filename">Filename corresponding to data</param> /// <param name="totalBytes">Total bytes of the whole ReceivedFile</param> /// <param name="bytesStart">The starting point for the associated data</param> /// <param name="packetSequenceNumber">Packet sequence number corresponding to the associated data</param> public SendInfo(string filename, long totalBytes, long bytesStart, long packetSequenceNumber) { this.Filename = filename; this.TotalBytes = totalBytes; this.BytesStart = bytesStart; this.PacketSequenceNumber = packetSequenceNumber; } } }
6. Adding Functionality To Code Element of MainWindow.xaml
- We now turn our attention to the code element of ‘MainWindow.xaml‘. To access the code element right click on ‘MainWindow.xaml‘ and select ‘View Code‘ from the context menu. You should see a code file that contains something like follows, all of the code we are subsequently going to add will be within the ‘MainWindow‘ class that currently looks like this:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; namespace WPFFileTransferExample { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } } }
- Our first step is to add the namespaces we will be using, so at the top of ‘MainWindow.xaml‘ underneath the existing ‘using …’ statements we need to add:
using System.IO; using System.Net; using Microsoft.Win32; using System.Threading.Tasks; using System.Collections.ObjectModel; using NetworkCommsDotNet; using NetworkCommsDotNet.DPSBase; using NetworkCommsDotNet.Tools; using NetworkCommsDotNet.DPSBase.SevenZipLZMACompressor; using NetworkCommsDotNet.Connections; using NetworkCommsDotNet.Connections.TCP;
- Next we are going to add some class variables to help us keep track of the current application state. We want to track:
- Received files.
- Temporary caches for incoming data and data information.
- Current send options to allow optional compression.
- Locker object to ensure thread safe operations where necessary.
- If the application is closing.
- To track these items add the following code at the top of (but within) the MainWindow class:
#region Private Fields /// <summary> /// Data context for the GUI list box /// </summary> ObservableCollection<ReceivedFile> receivedFiles = new ObservableCollection<ReceivedFile>(); /// <summary> /// References to received files by remote ConnectionInfo /// </summary> Dictionary<ConnectionInfo, Dictionary<string, ReceivedFile>> receivedFilesDict = new Dictionary<ConnectionInfo, Dictionary<string, ReceivedFile>>(); /// <summary> /// Incoming partial data cache. Keys are ConnectionInfo, PacketSequenceNumber. Value is partial packet data. /// </summary> Dictionary<ConnectionInfo, Dictionary<long, byte[]>> incomingDataCache = new Dictionary<ConnectionInfo, Dictionary<long, byte[]>>(); /// <summary> /// Incoming sendInfo cache. Keys are ConnectionInfo, PacketSequenceNumber. Value is sendInfo. /// </summary> Dictionary<ConnectionInfo, Dictionary<long, SendInfo>> incomingDataInfoCache = new Dictionary<ConnectionInfo, Dictionary<long, SendInfo>>(); /// <summary> /// Custom sendReceiveOptions used for sending files. Can be changed via GUI. /// </summary> SendReceiveOptions customOptions = new SendReceiveOptions<ProtobufSerializer>(); /// <summary> /// Object used for ensuring thread safety. /// </summary> object syncRoot = new object(); /// <summary> /// Boolean used for suppressing errors during GUI close /// </summary> static volatile bool windowClosing = false; #endregion
- Next we add three methods that can be used to update parts of the user interface:
#region GUI Updates /// <summary> /// Adds a line to the GUI log window /// </summary> /// <param name="logLine"></param> private void AddLineToLog(string logLine) { //Use dispatcher incase method is not called from GUI thread logBox.Dispatcher.BeginInvoke(new Action(() => { logBox.Text += DateTime.Now.ToShortTimeString() + " - " + logLine + "\n"; //Update the scroller so that we are always at the bottom scroller.ScrollToBottom(); })); } /// <summary> /// Updates the send file progress bar /// </summary> /// <param name="percentComplete"></param> private void UpdateSendProgress(double percentComplete) { //Use dispatcher incase method is not called from GUI thread sendProgress.Dispatcher.BeginInvoke(new Action(() => { sendProgress.Value = percentComplete; })); } /// <summary> /// Adds a new ReceivedFile to the list box data context /// </summary> /// <param name="file"></param> private void AddNewReceivedItem(ReceivedFile file) { //Use dispatcher incase method is not called from GUI thread lbReceivedFiles.Dispatcher.BeginInvoke(new Action(() => { receivedFiles.Add(file); })); } #endregion
- Next four methods that will be triggered by interactions in the user interface, such as buttons clicks etc:
#region GUI Events /// <summary> /// Delete the selected ReceivedFile from the application /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void DeleteFile_Clicked(object sender, RoutedEventArgs e) { Button cmd = (Button)sender; if (cmd.DataContext is ReceivedFile) { ReceivedFile fileToDelete = (ReceivedFile)cmd.DataContext; lock (syncRoot) { //Delete the ReceivedFile from the listbox data context receivedFiles.Remove(fileToDelete); //Delete the ReceivedFile from the internal cache if (receivedFilesDict.ContainsKey(fileToDelete.SourceInfo)) receivedFilesDict[fileToDelete.SourceInfo].Remove(fileToDelete.Filename); fileToDelete.Close(); } AddLineToLog("Deleted file '" + fileToDelete.Filename + "' from '" + fileToDelete.SourceInfoStr + "'"); } } /// <summary> /// Save the selected file to disk /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void SaveFile_Clicked(object sender, RoutedEventArgs e) { Button cmd = (Button)sender; if (cmd.DataContext is ReceivedFile) { //Use a SaveFileDialog to request the save location ReceivedFile fileToSave = (ReceivedFile)cmd.DataContext; SaveFileDialog saveDialog = new SaveFileDialog(); saveDialog.FileName = fileToSave.Filename; //If the user selected to save the file we write it to disk if (saveDialog.ShowDialog() == true) { fileToSave.SaveFileToDisk(saveDialog.FileName); AddLineToLog("Saved file '" + fileToSave.Filename + "' from '" + fileToSave.SourceInfoStr + "'"); } } } /// <summary> /// Toggles the use of compression for sending files /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void UseCompression_Changed(object sender, RoutedEventArgs e) { if (this.UseCompression.IsChecked == true) { //Set the customOptions to use ProtobufSerializer as a serialiser and LZMACompressor as the only data processor customOptions = new SendReceiveOptions<ProtobufSerializer, LZMACompressor>(); AddLineToLog("Enabled compression."); } else if (this.UseCompression.IsChecked == false) { //Set the customOptions to use ProtobufSerializer as a serialiser without any data processors customOptions = new SendReceiveOptions<ProtobufSerializer>(); AddLineToLog("Disabled compression."); } } /// <summary> /// Correctly shutdown NetworkComms.Net if the application is closed /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) { //Close all files lock (syncRoot) { foreach (ReceivedFile file in receivedFiles) file.Close(); } windowClosing = true; NetworkComms.Shutdown(); } #endregion
- Next are the four methods that interact with NetworkComms.Net. See method xml and comments for further explanations:
#region Comms /// <summary> /// Start listening for new TCP connections /// </summary> private void StartListening() { //Trigger IncomingPartialFileData method if we receive a packet of type 'PartialFileData' NetworkComms.AppendGlobalIncomingPacketHandler<byte[]>("PartialFileData", IncomingPartialFileData); //Trigger IncomingPartialFileDataInfo method if we receive a packet of type 'PartialFileDataInfo' NetworkComms.AppendGlobalIncomingPacketHandler<SendInfo>("PartialFileDataInfo", IncomingPartialFileDataInfo); //Trigger the method OnConnectionClose so that we can do some clean-up NetworkComms.AppendGlobalConnectionCloseHandler(OnConnectionClose); //Start listening for TCP connections //We want to select a random port on all available adaptors so provide //an IPEndPoint using IPAddress.Any and port 0. Connection.StartListening(ConnectionType.TCP, new IPEndPoint(IPAddress.Any, 0)); //Write out some useful debugging information the log window AddLineToLog("Initialised WPF file transfer example. Accepting TCP connections on:"); foreach (IPEndPoint listenEndPoint in Connection.ExistingLocalListenEndPoints(ConnectionType.TCP)) AddLineToLog(listenEndPoint.Address + ":" + listenEndPoint.Port); } /// <summary> /// Handles an incoming packet of type 'PartialFileData' /// </summary> /// <param name="header">Header associated with incoming packet</param> /// <param name="connection">The connection associated with incoming packet</param> /// <param name="data">The incoming data</param> private void IncomingPartialFileData(PacketHeader header, Connection connection, byte[] data) { try { SendInfo info = null; ReceivedFile file = null; //Perform this in a thread safe way lock (syncRoot) { //Extract the packet sequence number from the header //The header can also user defined parameters long sequenceNumber = header.GetOption(PacketHeaderLongItems.PacketSequenceNumber); if (incomingDataInfoCache.ContainsKey(connection.ConnectionInfo) && incomingDataInfoCache[connection.ConnectionInfo].ContainsKey(sequenceNumber)) { //We have the associated SendInfo so we can add this data directly to the file info = incomingDataInfoCache[connection.ConnectionInfo][sequenceNumber]; incomingDataInfoCache[connection.ConnectionInfo].Remove(sequenceNumber); //Check to see if we have already received any files from this location if (!receivedFilesDict.ContainsKey(connection.ConnectionInfo)) receivedFilesDict.Add(connection.ConnectionInfo, new Dictionary<string, ReceivedFile>()); //Check to see if we have already initialised this file if (!receivedFilesDict[connection.ConnectionInfo].ContainsKey(info.Filename)) { receivedFilesDict[connection.ConnectionInfo].Add(info.Filename, new ReceivedFile(info.Filename, connection.ConnectionInfo, info.TotalBytes)); AddNewReceivedItem(receivedFilesDict[connection.ConnectionInfo][info.Filename]); } file = receivedFilesDict[connection.ConnectionInfo][info.Filename]; } else { //We do not yet have the associated SendInfo so we just add the data to the cache if (!incomingDataCache.ContainsKey(connection.ConnectionInfo)) incomingDataCache.Add(connection.ConnectionInfo, new Dictionary<long, byte[]>()); incomingDataCache[connection.ConnectionInfo].Add(sequenceNumber, data); } } //If we have everything we need we can add data to the ReceivedFile if (info != null && file != null && !file.IsCompleted) { file.AddData(info.BytesStart, 0, data.Length, data); //Perform a little clean-up file = null; data = null; GC.Collect(); } else if (info == null ^ file == null) throw new Exception("Either both are null or both are set. Info is " + (info == null ? "null." : "set.") + " File is " + (file == null ? "null." : "set.") + " File is " + (file.IsCompleted ? "completed." : "not completed.")); } catch (Exception ex) { //If an exception occurs we write to the log window and also create an error file AddLineToLog("Exception - " + ex.ToString()); LogTools.LogException(ex, "IncomingPartialFileDataError"); } } /// <summary> /// Handles an incoming packet of type 'PartialFileDataInfo' /// </summary> /// <param name="header">Header associated with incoming packet</param> /// <param name="connection">The connection associated with incoming packet</param> /// <param name="data">The incoming data automatically converted to a SendInfo object</param> private void IncomingPartialFileDataInfo(PacketHeader header, Connection connection, SendInfo info) { try { byte[] data = null; ReceivedFile file = null; //Perform this in a thread safe way lock (syncRoot) { //Extract the packet sequence number from the header //The header can also user defined parameters long sequenceNumber = info.PacketSequenceNumber; if (incomingDataCache.ContainsKey(connection.ConnectionInfo) && incomingDataCache[connection.ConnectionInfo].ContainsKey(sequenceNumber)) { //We already have the associated data in the cache data = incomingDataCache[connection.ConnectionInfo][sequenceNumber]; incomingDataCache[connection.ConnectionInfo].Remove(sequenceNumber); //Check to see if we have already received any files from this location if (!receivedFilesDict.ContainsKey(connection.ConnectionInfo)) receivedFilesDict.Add(connection.ConnectionInfo, new Dictionary<string, ReceivedFile>()); //Check to see if we have already initialised this file if (!receivedFilesDict[connection.ConnectionInfo].ContainsKey(info.Filename)) { receivedFilesDict[connection.ConnectionInfo].Add(info.Filename, new ReceivedFile(info.Filename, connection.ConnectionInfo, info.TotalBytes)); AddNewReceivedItem(receivedFilesDict[connection.ConnectionInfo][info.Filename]); } file = receivedFilesDict[connection.ConnectionInfo][info.Filename]; } else { //We do not yet have the necessary data corresponding with this SendInfo so we add the //info to the cache if (!incomingDataInfoCache.ContainsKey(connection.ConnectionInfo)) incomingDataInfoCache.Add(connection.ConnectionInfo, new Dictionary<long, SendInfo>()); incomingDataInfoCache[connection.ConnectionInfo].Add(sequenceNumber, info); } } //If we have everything we need we can add data to the ReceivedFile if (data != null && file != null && !file.IsCompleted) { file.AddData(info.BytesStart, 0, data.Length, data); //Perform a little clean-up file = null; data = null; GC.Collect(); } else if (data == null ^ file == null) throw new Exception("Either both are null or both are set. Data is " + (data == null ? "null." : "set.") + " File is " + (file == null ? "null." : "set.") + " File is " + (file.IsCompleted ? "completed." : "not completed.")); } catch (Exception ex) { //If an exception occurs we write to the log window and also create an error file AddLineToLog("Exception - " + ex.ToString()); LogTools.LogException(ex, "IncomingPartialFileDataInfo"); } } /// <summary> /// If a connection is closed we clean-up any incomplete ReceivedFiles /// </summary> /// <param name="conn">The closed connection</param> private void OnConnectionClose(Connection conn) { ReceivedFile[] filesToRemove = null; lock (syncRoot) { //Remove any associated data from the caches incomingDataCache.Remove(conn.ConnectionInfo); incomingDataInfoCache.Remove(conn.ConnectionInfo); //Remove any non completed files if (receivedFilesDict.ContainsKey(conn.ConnectionInfo)) { filesToRemove = (from current in receivedFilesDict[conn.ConnectionInfo] where !current.Value.IsCompleted select current.Value).ToArray(); receivedFilesDict[conn.ConnectionInfo] = (from current in receivedFilesDict[conn.ConnectionInfo] where current.Value.IsCompleted select current).ToDictionary(entry => entry.Key, entry => entry.Value); } } //Update the GUI lbReceivedFiles.Dispatcher.BeginInvoke(new Action(() => { lock (syncRoot) { if (filesToRemove != null) { foreach (ReceivedFile file in filesToRemove) { receivedFiles.Remove(file); file.Close(); } } } })); //Write some useful information the log window AddLineToLog("Connection closed with " + conn.ConnectionInfo.ToString()); } /// <summary> /// Sends requested file to the remoteIP and port set in GUI /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void SendFileButton_Click(object sender, RoutedEventArgs e) { //Create an OpenFileDialog so that we can request the file to send OpenFileDialog openDialog = new OpenFileDialog(); openDialog.Multiselect = false; //If a file was selected if (openDialog.ShowDialog() == true) { //Disable the send and compression buttons sendFileButton.IsEnabled = false; UseCompression.IsEnabled = false; //Parse the necessary remote information string filename = openDialog.FileName; string remoteIP = this.remoteIP.Text; string remotePort = this.remotePort.Text; //Set the send progress bar to 0 UpdateSendProgress(0); //Perform the send in a task so that we don't lock the GUI Task.Factory.StartNew(() => { try { //Create a fileStream from the selected file FileStream stream = new FileStream(filename, FileMode.Open, FileAccess.Read); //Wrap the fileStream in a threadSafeStream so that future operations are thread safe StreamTools.ThreadSafeStream safeStream = new StreamTools.ThreadSafeStream(stream); //Get the filename without the associated path information string shortFileName = System.IO.Path.GetFileName(filename); //Parse the remote connectionInfo //We have this in a separate try catch so that we can write a clear message to the log window //if there are problems ConnectionInfo remoteInfo; try { remoteInfo = new ConnectionInfo(remoteIP, int.Parse(remotePort)); } catch (Exception) { throw new InvalidDataException("Failed to parse remote IP and port. Check and try again."); } //Get a connection to the remote side Connection connection = TCPConnection.GetConnection(remoteInfo); //Break the send into 20 segments. The less segments the less overhead //but we still want the progress bar to update in sensible steps long sendChunkSizeBytes = (long)(stream.Length / 20.0) + 1; //Limit send chunk size to 500MB long maxChunkSizeBytes = 500L*1024L*1024L; if (sendChunkSizeBytes > maxChunkSizeBytes) sendChunkSizeBytes = maxChunkSizeBytes; long totalBytesSent = 0; do { //Check the number of bytes to send as the last one may be smaller long bytesToSend = (totalBytesSent + sendChunkSizeBytes < stream.Length ? sendChunkSizeBytes : stream.Length - totalBytesSent); //Wrap the threadSafeStream in a StreamSendWrapper so that we can get NetworkComms.Net //to only send part of the stream. StreamTools.StreamSendWrapper streamWrapper = new StreamTools.StreamSendWrapper(safeStream, totalBytesSent, bytesToSend); //We want to record the packetSequenceNumber long packetSequenceNumber; //Send the select data connection.SendObject("PartialFileData", streamWrapper, customOptions, out packetSequenceNumber); //Send the associated SendInfo for this send so that the remote can correctly rebuild the data connection.SendObject("PartialFileDataInfo", new SendInfo(shortFileName, stream.Length, totalBytesSent, packetSequenceNumber), customOptions); totalBytesSent += bytesToSend; //Update the GUI with our send progress UpdateSendProgress((double)totalBytesSent / stream.Length); } while (totalBytesSent < stream.Length); //Clean up any unused memory GC.Collect(); AddLineToLog("Completed file send to '" + connection.ConnectionInfo.ToString() + "'."); } catch (CommunicationException) { //If there is a communication exception then we just write a connection //closed message to the log window AddLineToLog("Failed to complete send as connection was closed."); } catch (Exception ex) { //If we get any other exception which is not an InvalidDataException //we log the error if (!windowClosing && ex.GetType() != typeof(InvalidDataException)) { AddLineToLog(ex.Message.ToString()); LogTools.LogException(ex, "SendFileError"); } } //Once the send is finished reset the send progress bar UpdateSendProgress(0); //Once complete enable the send button again sendFileButton.Dispatcher.BeginInvoke(new Action(() => { sendFileButton.IsEnabled = true; UseCompression.IsEnabled = true; })); }); } } #endregion
- The last thing we need to add within the Code Elements of ‘MainWindow.xaml‘ is the correct initialisation of NetworkComms.Net. In order to correctly initialise the network library we need to:
- Set the data context of the received files list box to our receivedFiles field.
- Start listening for TCP connections.
- We perform these initialisation tasks in the MainWindow class constructor by replacing it with the following code:
public MainWindow() { InitializeComponent(); //Set the listbox datacontext lbReceivedFiles.DataContext = receivedFiles; //Start listening for new TCP connections StartListening(); }
7. Add Events To WPF Layout
- The final step in the application is to add the necessary interactivity to send and receive files. This is done by editing the XAML of the MainWindow. This is the same XAML edited in step 3 of this tutorial, but to recap, access the XAML by double clicking ‘MainWindow.xaml‘ in the Solution Explorer window.
- When the application closes we want to run the method Window_Closing. Replace the top section of the XAML which currently looks like this:
<Window x:Class="WPFFileTransferExample.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="NetworkComms.Net File Transfer Example" Height="450" Width="513" MinWidth="513" MinHeight="450" Background="#FF7CA0FF">
with this (note the addition of Closing=”Window_Closing” at the end):
<Window x:Class="WPFFileTransferExample.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="NetworkComms.Net File Transfer Example" Height="450" Width="513" MinWidth="513" MinHeight="450" Background="#FF7CA0FF" Closing="Window_Closing">
- We want to send a file when we click the button labelled ‘Send File To Remote’. Replace the following line:
<Button Content="Send File To Remote" HorizontalAlignment="Left" Margin="8,3,0,0" Name="sendFileButton" VerticalAlignment="Top" Height="25" Width="120"/>
with
<Button Content="Send File To Remote" HorizontalAlignment="Left" Margin="8,3,0,0" Name="sendFileButton" VerticalAlignment="Top" Height="25" Width="120" Click="SendFileButton_Click"/>
- We want to change the compression property when we check and uncheck the checkbox labelled ‘Use Compression’. Replace the following line:
<CheckBox Content="Use Compression" Name="UseCompression" HorizontalAlignment="Left" Margin="0,5,0,0" VerticalAlignment="Top"/>
with
<CheckBox Content="Use Compression" Name="UseCompression" HorizontalAlignment="Left" Margin="0,5,0,0" VerticalAlignment="Top" Checked="UseCompression_Changed" Unchecked="UseCompression_Changed"/>
- When a file has been received we want to be able to delete it. Each received file creates an entry in the user interface list box. Replace the following line:
<Button Margin="0,-1,4,0" Height="20" HorizontalAlignment="Right" IsEnabled="{Binding Path=IsCompleted}" Content="Delete"/>
with
<Button Margin="0,-1,4,0" Height="20" HorizontalAlignment="Right" IsEnabled="{Binding Path=IsCompleted}" Content="Delete" Click="DeleteFile_Clicked"/>
- Finally once a file has been received we want to be able to save it to disk somewhere. Replace the following line:
<Button Margin="0,-1,46,0" Height="20" HorizontalAlignment="Right" IsEnabled="{Binding Path=IsCompleted}" Content="Save"/>
with
<Button Margin="0,-1,46,0" Height="20" HorizontalAlignment="Right" IsEnabled="{Binding Path=IsCompleted}" Content="Save" Click="SaveFile_Clicked"/>
8. Test Your File Transfer Application
- We’ve finally arrived at the testing phase. We now want to open at least two instances of the File Transfer Application. To do that we first need to build the project in debug mode (make sure Visual Studio shows ‘Debug‘ in the top menu), either by right clicking on the solution and selecting ‘Build Solution‘ or pressing ‘F6‘ on the keyboard.
- Now browse to the build location of the application. One way is to right click on the project in Visual Studio and select ‘Open Folder in Windows Explorer‘. Look for a folder called ‘bin‘ and within that ‘Debug‘.
- You should now see an executable named ‘WPFFileTransferExample.exe‘, double click on this twice to open two instances of the example. Note: When you open the applications you may get a notification from your system firewall. It is important to provide the necessary permissions (see firewall documentation) otherwise the example will not be able to communicate.
- Both applications should look something as followed. The important thing to check is the list of IP addresses and ports selected for listening in the client log window:
- Choose which one of the applications you would like to send a file too (Application A). Choose an appropriate IP address and port (e.g. 127.0.0.1 or 192.168.*.*) from the output shown in application A and enter this information into ‘Remote IP’ and ‘Port’ text boxes in the other application (application B).
- Click the ‘Send File To Remote’ in application B and select a file.
- If everything has gone to plan the selected file should appear in application A.
If Everything Worked
- If you found this article useful or have any ideas as to how we could improve it please leave us a comment below.
If You Have Problems
- Please experiment with the completed/working WPF file transfer application example available in the bundles examples of the download.
- Ensure you have correctly configured your firewall to allow the necessary traffic.
- If you are still have issues please post on our forums and we will be more than happy to help.
For More Information
- See our other tutorials.
- See our online API Reference which explains what all of the methods do.
- Ask any questions on our forums.
Thank you very much for making this MarcF. I didn’t think it would be made and posted so quickly! I will go through the tutorial later today.