Introduction
This article examines the implementation of upload and download functionality with progress indication (progress bar feature) using the Windows Communication Foundation. For this sample, you will need Visual Studio 2008.
The sample code consists of three projects bundled in a solution. A brief description of these projects follows. The attached sample code is available in C# and VB.NET (conversion to VB.NET was made by Lee Galloway of Project Time and Cost).
FileService
This is the main server project.
The Server Contract
The File Server project includes FileTransferServiceContract.cs file, which contains the IFileTransferService interface. This interface describes the operations provided by our server. No actual work is done in this code file except in describing the operations provided. If you've worked with service-oriented applications before, you'll know that this job is important enough to spare a separate file for. Here are the two operations of our file transfer service:
-
DownloadFile Server Method
Accepts a DownloadRequest instance that contains the name of the file to be downloaded by the client. It returns a RemoteFileInfo instance, defined in the same code file. RemoteFileInfo contains the name of the file to be downloaded, the file stream and the length of the file in bytes. This instance of the RemoteFileInfo class will be used by the client to download the file. You notice that filename and length are marked with the MessageHeader attribute in the RemoteFileInfo class. This is because when a message contract contains a stream, this can be the only body member of the contract.
-
UploadFile Server Method
Accepts an instance of the RemoteFileInfo message contract. This is the same as used in DownloadFile, only in this case the length property is not required.
CS
[ServiceContract()]
public interface IFileTransferService
{
[OperationContract()]
void UploadFile(RemoteFileInfo request);
[OperationContract]
RemoteFileInfo DownloadFile(DownloadRequest request);
}
public class RemoteFileInfo
{
[MessageHeader(MustUnderstand = true)]
public string FileName;
[MessageHeader(MustUnderstand = true)]
public long Length;
[MessageBodyMember(Order = 1)]
public System.IO.Stream FileByteStream;
}
VB
<ServiceContract()> _
<servicecontract() />Public Interface IFileTransferService
<OperationContract()> _
Sub UploadFile(ByVal request As RemoteFileInfo)
<OperationContract()> _
Function DownloadFile(ByVal request As DownloadRequest) As RemoteFileInfo
End Interface
<MessageContract()> _
Public Class RemoteFileInfo
Implements IDisposable
<OperationContract()> _
Public FileName As String
<OperationContract()> _
Public Length As Long
<MessageBodyMember(Order:=1)> _
Public FileByteStream As System.IO.Stream
End Class
The Server Implementation
File Server also includes the FileTransferService.cs code file which contains the implementation of the contract, i.e. the actual code that does all the work. Apparently the included class implements the IFileTransferService class, which constitutes the service contract. If you have worked with streams before in .NET, you will find out that the code that handles the stream and related information for upload or download is pretty straightforward. If you are new to .NET streams, please use Google for a quick introduction.
Note here that since actual downloading of the file starts after the execution of the DownloadFile method is completed (i.e. after the client gets the RemoteFileInfo instance returned by this method), the server must close the opened stream later, after the client completes the process. An elegant approach was suggested by Buddhike. To do this, the IDisposable interface is implemented by the RemoteFileInfo contract and the stream is disposed on the corresponding Dispose method. If this is not done, the stream will remain locked and the corresponding file will be locked for writing.
ConsoleHost
FileService is a class library and hence it cannot start as a window process. Therefore it needs another executable file-process that will host it. Several types of processes can host a WCF service, such as .NET executables, IIS processes, Windows Activation Services (new feature of Vista) and many more. Our example uses a .NET executable to host our service. So, ConsoleHost is a console application that does exactly this. It has a reference to the FileService project. However, it is not related in any way with the business our service is doing, i.e. transferring files. Actually, the code you will find in Program.cs would be the same even if our service was designed to host an online grocery. Take a quick look at this code file to understand how our service is started and closed.
CS
static void Main(string[] args)
{
ServiceHost myServiceHost = new ServiceHost
(typeof(FileService.FileTransferService));
myServiceHost.Open();
Console.WriteLine("This is the SERVER console");
Console.WriteLine("Service Started!");
foreach (Uri address in myServiceHost.BaseAddresses)
Console.WriteLine("Listening on " + address.ToString());
Console.WriteLine("Click any key to close...");
Console.ReadKey();
myServiceHost.Close();
}
VB
Public Shared Sub Main()
Dim myServiceHost As New ServiceHost(_
GetType(FileService.FileTransferService))
myServiceHost.Open()
Console.WriteLine("This is the SERVER console")
Console.WriteLine("Service Started!")
For Each address As Uri In myServiceHost.BaseAddresses
Console.WriteLine("Listening on " + address.ToString())
Next
Console.WriteLine("Click any key to close...")
Console.ReadKey()
myServiceHost.Close()
End Sub
The configuration of ConsoleHost is what matters the most! It is divided into three sections, configuring the way our service will behave and how it will be exposed to the rest of the world. It is not the goal of this article to describe in detail how a WCF service is configured, so please refer to the WCF reference on MSDN for more information. Something noticeable in the configuration of our service is that it uses MTOM as message encoding and stream as transfer mode. See also the maxReceivedMessageSize property. This defines the maximum size of messages transferred by our service. Since we are transferring files, we want this property to have a large value.
XML
<binding name ="FileTransferServicesBinding"
transferMode="Streamed"
messageEncoding="Mtom"
maxReceivedMessageSize="10067108864" >
</binding>
Client
The Client project is a sample consumer of our service. You will notice that the Client project includes a folder called Service References. This folder contains a bunch of files created automatically by Visual Studio by right clicking on the Client project root and selecting "Add Service Reference." The files in this folder are the proxy of our file transfer service on the client side. Client is using these files to send requests to the server, hiding in this way the complexity of Web Service and SOAP protocols.
Again, if you have worked with streams before, you will notice that things are pretty simple in the TestForm file except for one small part, which is also the difference in implementing the progress indication when uploading rather than when downloading. When downloading, the client has control of the procedure. You can see in TestForm.cs that downloading is implemented using a loop that reads the server stream piece-by-piece. So, the client knows what part of the server stream is read and how many more remain. When uploading, that loop resides on the server. In order for the client to know how many bytes the server read, it uses the StreamWithProgress class, which inherits System.IO.Stream. An instance of this class is passed to the server, instead of the original file stream. Since this class overrides the default Read method of the stream (see code below), it can report the progress of the uploading process to the client!
CS
public override
int Read(byte[] buffer, int offset,
int count)
{
int result = file.Read(buffer, offset, count);
bytesRead += result;
if (ProgressChanged != null)
ProgressChanged(this, new ProgressChangedEventArgs(bytesRead, length));
return result;
}
VB
Public Overloads Overrides Function Read(ByVal buffer As Byte(), _
ByVal offset As Integer, ByVal count As Integer) As Integer
Dim result As Integer = file.Read(buffer, offset, count)
bytesRead += result
RaiseEvent ProgressChanged(Me, New ProgressChangedEventArgs(_
bytesRead, m_length))
Return result
End Function
History
- Updated on 2007/09/09: A more elegant approach for the server implementation was suggested by Buddhike.
- Updated on 2007/10/24: Code now also provided in VB.NET. Conversion made by Lee Galloway of Project Time and Cost.
- Updated on 2009/02/03:
- Upgraded projects to Visual Studio 2008 (.NET Framework 2.0 is still the target framework).
- Properly closing read stream on client when download is done. Failure to do this was causing the client to throw timeout exceptions after 2 or 3 downloads.
| You must Sign In to use this message board. |
|
|
 |
|
 |
hello, I have another service and I want to add this one into mine. but this one uses another type of bindings.
<wsHttpBinding> <binding name="defBinding"> <security mode="None"> <transport clientCredentialType="None"/> <message establishSecurityContext="false"/> </security> </binding> </wsHttpBinding>
can you tell me how can I merge these 2 services? thanks
No sleep for you yet, I wanna see smoke from those keyboards!!.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Most probably you don't want to use streamed transfer mode for your other service. So you need two binding configurations. So just append the binding configuration you see in the article after yours (after yours ). <wsHttpBinding> <binding name="yourBindingForYourService" .......... </binding> <binding name="bindingFromThisArticleForFileTransfer" .......... </binding> </wsHttpBinding>
Dimitris Papadimitriou Software Development Professional
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
I already found solution, I just use basic binding for both cases. and works fine with me!
thanks anyway, your project works nice with my current solution over internet and hosted on IIS.
I've modified it to use HostingEnvironment.ApplicationPhysicalPath for server..in case anyone needs I can post it.
No sleep for you yet, I wanna see smoke from those keyboards!!.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
 |
|
 |
I'm using VB.Net, so this will not work as simply on C#.
I have placed the ConsoleHost baseAddresses into the project settings, and removed the baseAddresses in app.config...
<services> <service behaviorConfiguration="MyServiceTypeBehaviors" name="FileService.FileTransferService"> <endpoint address="mex" binding="basicHttpBinding" bindingConfiguration="FileTransferServicesBinding" contract="FileService.IFileTransferService" /> <host> <baseAddresses> <!----> </baseAddresses> </host> </service> </services>
<applicationSettings> <ConsoleHost.My.MySettings> <setting name="BaseAddress" serializeAs="String"> <value>http://localhost:8080/FileTranfer</value> </setting> </ConsoleHost.My.MySettings> </applicationSettings>
This allows me to do the following in ConsoleHost:
Dim myServiceHost As New ServiceHost(GetType(FileService.FileTransferService), New Uri(My.Settings.BaseAddress))
myServiceHost.Open()
This works without issue. Now I am trying to do the same with the Client.
Client App.Config:
<applicationSettings> <Client.My.MySettings> <setting name="EndpointAddress" serializeAs="String"> <value>http://localhost:8080/FileTranfer/mex</value> </setting> <setting name="DownloadDirectory" serializeAs="String"> <value>Download</value> </setting> </Client.My.MySettings> </applicationSettings>
I am having problems creating the ServiceHost programmatically in TestForm
Any suggestions?
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Well, what problems you have? What is the exception you get?
Dimitris Papadimitriou Software Development Professional
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
I have fixed the error. My code is now as per the following:
#Region " Upload "
Private Sub UploadButton_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles UploadButton.Click Cursor = Cursors.WaitCursor
Dim EndpointURL As String = Me.txtServerPath.Text
If EndpointURL = "" Then EndpointURL = My.Settings.EndpointAddress
Try Dim fileInfo As New FileInfo(txtUploadFilename.Text)
LogText("Starting uploading " + fileInfo.Name) LogText("Size : " + fileInfo.Length.ToString())
Using stream As New FileStream(txtUploadFilename.Text, FileMode.Open, FileAccess.Read) Using uploadStreamWithProgress As New StreamWithProgress(stream) AddHandler uploadStreamWithProgress.ProgressChanged, AddressOf uploadStreamWithProgress_ProgressChanged
Dim Binding As New BasicHttpBinding Binding.MessageEncoding = WSMessageEncoding.Mtom Binding.TransferMode = TransferMode.Streamed Binding.MaxReceivedMessageSize = 10067108864
Dim Endpoint As New EndpointAddress(EndpointURL)
Dim WCFClient As New Client.FileTransferClient.FileTransferServiceClient(Binding, Endpoint)
WCFClient.Endpoint.Contract.Name = "FileTransferClient.IFileTransferService"
WCFClient.UploadFile(fileInfo.Name.ToString(), fileInfo.Length.ToString(), uploadStreamWithProgress)
LogText("Done!")
WCFClient.Close() End Using End Using Catch ex As Exception LogText("Exception : " + ex.Message) If ex.InnerException IsNot Nothing Then LogText("Inner Exception : " + ex.InnerException.Message) Finally Cursor = Cursors.Default End Try
End Sub
#Region " Download "
Private Sub DownloadButton_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles DownloadButton.Click Cursor = Cursors.WaitCursor
Dim EndpointURL As String = Me.txtServerPath.Text
If EndpointURL = "" Then EndpointURL = My.Settings.EndpointAddress
Try Dim Binding As New BasicHttpBinding Binding.MessageEncoding = WSMessageEncoding.Mtom Binding.TransferMode = TransferMode.Streamed Binding.MaxReceivedMessageSize = 10067108864
Dim Endpoint As New EndpointAddress(EndpointURL)
Dim WCFClient As New Client.FileTransferClient.FileTransferServiceClient(Binding, Endpoint)
WCFClient.Endpoint.Contract.Name = "FileTransferClient.IFileTransferService" LogText("Start")
Dim DestinationFilePath As String = Path.Combine(Path.Combine(Application.StartupPath, My.Settings.DownloadDirectory), txtDownloadFilename.Text.ToString) If File.Exists(DestinationFilePath) Then File.Delete(DestinationFilePath)
Dim inputStream As Stream = Nothing Dim DestinationFileName As String = Path.Combine(Path.Combine(Application.StartupPath, My.Settings.DownloadDirectory), txtDownloadFilename.Text.ToString) Dim length As Long = WCFClient.DownloadFile(txtDownloadFilename.Text.ToString, inputStream)
Using writeStream As New FileStream(DestinationFileName, FileMode.CreateNew, FileAccess.Write) Dim chunkSize As Integer = 2048 Dim buffer As Byte() = New Byte(chunkSize - 1) {}
Do Dim bytesRead As Integer = inputStream.Read(buffer, 0, chunkSize) If bytesRead = 0 Then Exit Do End If
writeStream.Write(buffer, 0, bytesRead)
progressBar1.Value = CInt((writeStream.Position * 100 / length)) Loop While True
LogText("Done!")
inputStream.Dispose() writeStream.Close() End Using
WCFClient.Close() Catch ex As Exception LogText("Exception : " + ex.Message) If ex.InnerException IsNot Nothing Then LogText("Inner Exception : " + ex.InnerException.Message) Finally Cursor = Cursors.Default End Try
End Sub
#End Region
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Try this 'http://localhost:8080/FileTranfer' in your client settings (meaning the address without the 'mex' suffix. Since you are defining the address in code using the ServiceHost constructor, I think that address ('mex') defined in endpoint is not used.
Dimitris Papadimitriou Software Development Professional
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Dimitris Papadimitriou wrote: Try this 'http://localhost:8080/FileTranfer' in your client settings (meaning the address without the 'mex' suffix. Since you are defining the address in code using the ServiceHost constructor, I think that address ('mex') defined in endpoint is not used.
No, I don't think that would have fixed it.
But, no worry.
I'm now adding the ability to send messages between the client and server. The required end result is essentially a peer to peer messaging and file upload\download program. Obviously the host will need to be turned into a WinForms app.
A new question I have is regarding the FileTransferClient.map. How do I update this to reflect the service changes?
My new service contract looks like the following:
Imports System Imports System.ServiceModel
<ServiceContract()> _ Public Interface IFileTransferService
<OperationContract()> _ Sub UploadFile(ByVal request As RemoteFileInfo)
<OperationContract()> _ Function DownloadFile(ByVal request As DownloadRequest) As RemoteFileInfo
<OperationContract()> _ Sub SendMessage(ByVal ThisMessage As Message)
End Interface
<MessageContract()> _ Public Class DownloadRequest
<MessageBodyMember()> _ Public FileName As String
End Class
<MessageContract()> _ Public Class Message
<MessageBodyMember()> _ Public Text As String
<MessageBodyMember()> _ Public Timestamp As Date
End Class
<MessageContract()> _ Public Class RemoteFileInfo Implements IDisposable
<MessageHeader(MustUnderstand:=True)> _ Public FileName As String
<MessageHeader(MustUnderstand:=True)> _ Public Length As Long
<MessageBodyMember(Order:=1)> _ Public FileByteStream As System.IO.Stream
Public Sub Dispose() Implements IDisposable.Dispose ' close stream when the contract instance is disposed. this ensures that stream is closed when file download is complete, since download procedure is handled by the client and the stream must be closed on server. ' thanks Bhuddhike! http: If FileByteStream IsNot Nothing Then FileByteStream.Close() FileByteStream = Nothing End If End Sub
End Class
Thanks in advance.
Dave
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Maybe I'm kind of new in WCF. I need a progress bar when downloading SOAP (XML). This is my scenario: I have a WCF Web Service which returns a DataSet with 1 mb of data aprox. It can hold up to 34000 rows. This has to be sent over internet to customers, most of them don't have great bandwidth. I want to show them how is the download coming out, if possible the transfer rate. Besides this, I have added GZipMessageEncoderFactory to compress the messages between the client and the server.
I've searched over on internet but can't find the way to go, maybe you can give me a light on this, please... It will be greatly appreciated.
Main Reason... Programming
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
 |
|
|
 |
|
 |
And you gave him 1? Man, dont be pathetic... As mentioned you need VS 2008. And it doesnt worth the issue...
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
 |
|
|
 |
|
 |
Well done, The code works very well on local machine (and of course can be optimized on network architecture). Clean, and understandable. I have a question, I have read through your code and not quite sure why WCF did that. The generated proxy file, generates this method
public long DownloadFile(ref string FileName, out System.IO.Stream FileByteStream) { Client.FileTransferClient.DownloadRequest inValue = new Client.FileTransferClient.DownloadRequest(); inValue.FileName = FileName; Client.FileTransferClient.RemoteFileInfo retVal = ((Client.FileTransferClient.IFileTransferService)(this)).DownloadFile(inValue); FileName = retVal.FileName; FileByteStream = retVal.FileByteStream; return retVal.Length; }
I have notice, every time my service method return or use MessageContract, WCF generates such methods (obviously incase of Streaming, it returns the file length)...
Can you please explain this behavior?
Thank you
Tuan Jinn
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Hi! If you like the code I would appreciate it if you could evaluate it using the rating bar on top. Regarding your question, automated proxy generation tries to convert the message format of classes such as RemoteFileInfo into more easy to use method calls. The reason that this particular method returns long (stream size) it that RemoteFileInfo class contains Length property. And for some reason proxy generator chose this property of the class to be the return value of the corresponding method. If you remove that property probably the generator will create a void function, or some other property will thak its place to be the return value.
Dimitris Papadimitriou Software Development Professional
|
| Sign In·View Thread·PermaLink | 4.00/5 |
|
|
|
 |
|
 |
Hi Dimitris, I appreciate your quick response.
Dimitris Papadimitriou wrote: And for some reason proxy generator chose this property of the class to be the return value of the corresponding method. If you remove that property probably the generator will create a void function, or some other property will thak its place to be the return value
That's what I am concerning about, in other cases which my message will contains variety properties (system derived), I dont know if I can control the generated method.
But it's a good point you just figured (I was certainly unsure if my understanding was correct until that).
Thank you,
btw, I rated maximum point yesterday when I first looked at the code already... And today I came back with a question. I voted again though
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
I tried to move this ot a service but it won't start. Any ideas.The project is GREAT. Thanks. Would just like to run it as a service.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
When a windows service does not start (unhandled exception on start) there is an error written in Windows Event log. Do you see anything there?
Dimitris Papadimitriou Software Development Professional
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Error code 7034 in System event log The My Hybrid Service service terminated unexpectedly. It has done this 1 time(s).
Nothing in the App log.
It installs, but won't start
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Would it be possible to run the project VS 2005? If I installed .net 3.5 and converted the solution to VS 2005 format?
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Hi, as you can see from the history of the article the original code was written using VS 2005. So, yes you can run the project in VS 2005 if you manage to convert the solution and project files to that format.
Dimitris Papadimitriou Software Development Professional
|
| Sign In·View Thread·PermaLink | 2.00/5 |
|
|
|
 |
|
 |
Hi Dimitris,
Nice Article!
I want to have some suggestions from you regarding my problem.
Actually in you article you have put the SteamWithProgress.cs(which is derrived from Steam class) file at client's end but I want to put all the logic at the Server(webservice) so that if we want to upload some file we just need to call upload method and pass some parameter. And for progress bar we just need to write some event of webservice or any class.
So i wanted to have everything at server side and on the client side i wanted to call a function for upload and a event in which i can adjust the progress bar. Is this possible any way?
Any help will be appreciated.
Thanks Sachin
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Hi Sachin, I'm not sure I completely understand what you want to do. You want to have progress indication on client or not?
Dimitris Papadimitriou Software Development Professional
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|