Entries:
Comments:
Posts:

Loading User Information from Channel 9

Something went wrong getting user information from Channel 9

Latest Achievement:

Loading User Information from MSDN

Something went wrong getting user information from MSDN

Visual Studio Achievements

Latest Achievement:

Loading Visual Studio Achievements

Something went wrong getting the Visual Studio Achievements

Silverlight 3 File Transfer Application

Every once in a while I will run into a situation where I need to send a file to someone, but struggle to find an easy way of doing it.  Instant messenger programs usually work until you run into a situation where someone just cannot send or receive a file from chat client whether it's due to firewalls, differing client versions, or multi-IM incompatibilities.  Other times, I will try to send the file via email just to find out that the person's email server blocks specific extensions.  This quick application will allow two users to quickly and easily connect to a Silverlight 3 client and send each other files. 

Overview

In this application, a user will connect to the Silverlight application and choose to either host or join a session.  If a user decides to host a session, he or she will be given a random eight character session key, and he or she will be in a waiting status until another user connects.  When a user wishes to connect to the host, he or she can supply the host's session key which will establish a connection between the two users.  Once connected, the users will be able to send files to one another, and will also be able to chat with each other via simple text messages.

SendFile

Polling Duplex

Sending a file from one client application to another requires a common central point in order to properly route the message from one user to another.  Since it is easy to host a Silverlight application in an ASP.NET page, we will use an ASP.NET back end to manage all communications between all connected users.  In order to do this, we must be able to host a service that is able to accept incoming messages from the Silverlight client, and perform a push back out to the intended recipient.  This is accomplished by using the WCF Polling Duplex (System.ServiceModel.PollingDuplex.dll) channel.  Silverlight 3 allows us to directly add a service reference to a service of this type, and handles all of the complexities for us.  I began by using the DuplexService.cs file from a sample application from MIX09 when Silverlight 3 beta was released.  The code starts us out with a couple of base abstract classes, and interfaces that we must inherit from in order to create our own service. 

FileSendService

The two main things we need to do in order to create our own service is to define custom message types that will be used for communication, and override the base DuplexService class to properly handle these messages.  We will create our custom message types by using DuplexMessage (defined in DuplexService.cs) as our base class.  Our message classes must be defined with the [DataContract] attribute, and the the member variables must be public and contain the [DataMember] attribute.  This will allow our Silverlight client project to share these definitions using a service reference.  Additionally, the Duplex message class must have the [KnownType] attribute for each descendant message created.

 C#

[KnownType(typeof(HostSessionMessage))]
[KnownType(typeof(JoinSessionMessage))]
[KnownType(typeof(FileBeginUploadMessage))]
[KnownType(typeof(FileTransferBytesMessage))]
public class DuplexMessage { }

[DataContract]
public class HostSessionMessage : DuplexMessage
{
[DataMember]
public string Username;
}

[DataContract]
public class JoinSessionMessage : DuplexMessage
{
[DataMember]
public string Username;
[DataMember]
public string SessionKey;
}

[DataContract]
public class FileBeginUploadMessage : DuplexMessage
{
[DataMember]
public string FileName;
[DataMember]
public long TotalBytes;
}

[DataContract]
public class FileTransferBytesMessage : DuplexMessage
{
[DataMember]
public long StartByte;
[DataMember]
public long PacketSize;
[DataMember]
public byte[] Bytes;
[DataMember]
public bool EndFile;
}

VB

<DataContract(Namespace := "http://samples.microsoft.com/silverlight2/duplex"), 
KnownType(GetType(HostSessionMessage)), KnownType(GetType(JoinSessionMessage)), KnownType(GetType(FileBeginUploadMessage)), KnownType(GetType(FileTransferBytesMessage))> _
Public Class DuplexMessage
End Class

<DataContract()> _
Public Class HostSessionMessage
Inherits DuplexMessage
<DataMember()> Public Username As String
End Class

<DataContract()> _
Public Class JoinSessionMessage
Inherits DuplexMessage
<DataMember()> Public Username As String
<DataMember()> Public SessionKey As String
End Class

<DataContract()> _
Public Class FileTransferBytesMessage
Inherits DuplexMessage
<DataMember()> Public StartByte As Long
<DataMember()> Public PacketSize As Long
<DataMember()> Public Bytes() As Byte
<DataMember()> Public EndFile As Boolean
End Class

<DataContract()> _
Public Class FileBeginUploadMessage
Inherits DuplexMessage
<DataMember()> Public FileName As String
<DataMember()> Public TotalBytes As Long
End Class

The next step is to create our FileSendService class which descends from the DuplexService class as described above.  We override the OnMessage method so that we can do custom processing based on the type of message sent as shown below. 

C#

[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class FileSendService : DuplexService
{
private List<SessionConnectionInfo> sessionConnections = new List<SessionConnectionInfo>();

{...}

protected override void OnMessage(string sessionId, DuplexMessage data)
{
if (data is HostSessionMessage)
CreateHostSession(data as HostSessionMessage);
else if (data is JoinSessionMessage)
JoinSession(data as JoinSessionMessage);
else if (data is FileBeginUploadMessage)
StartSendFile(data as FileBeginUploadMessage);
else
SendMessage(data);
}

}

else if (data is JoinSessionMessage)
JoinSession(data as JoinSessionMessage);
else if (data is FileBeginUploadMessage)
StartSendFile(data as FileBeginUploadMessage);
else
SendMessage(data);
}

}

}

}

 VB

Public Class FileSenderServiceFactory
Inherits DuplexServiceFactory(Of FileSendService)
End Class

<AspNetCompatibilityRequirements(RequirementsMode := AspNetCompatibilityRequirementsMode.Allowed)> _
Public Class FileSendService
Inherits DuplexService
Private sessionConnections As New List(Of SessionConnectionInfo)()

...

Protected Overrides Sub OnMessage(ByVal sessionId As String, ByVal data As DuplexMessage)
If TypeOf data Is HostSessionMessage Then
CreateHostSession(TryCast(data, HostSessionMessage))
ElseIf TypeOf data Is JoinSessionMessage Then
JoinSession(TryCast(data, JoinSessionMessage))
ElseIf TypeOf data Is FileBeginUploadMessage Then
StartSendFile(TryCast(data, FileBeginUploadMessage))
Else
SendMessage(data)
End If
End Sub

End Class

Since we are going to have to keep track of each host and user connected to that host, we will create a class,  SessionConnectionInfo, to manage the relevant data.  When a user decides to host a session, our OnMessage method will receive a HostSessionMessage which will scan our List<SessionConnectionInfo> to see if there is another host with the same username.  If not found, a new SessionConnectionInfo object is created along with a randomly generated session key.  When a user attempts to join a session, the OnMessage method will receive a JoinSessionMessage containing a session key and the name of the user joining the session.  The service will search for a SessionConnectionInfo object containing that session key, and if found, establishes a connection between the two users. 

 C#

public class SessionConnectionInfo
{
public string HostUserName { get; set; }
public string ConnectedUsername { get; private set; }
public string SessionKey { get; set; }

public string ConnectedUserInternalSession { get; private set; }
public string HostInternalSession { get; set; }

public bool UserConnected
{
get { return ConnectedUserInternalSession != string.Empty; }
}

public SessionConnectionInfo()
{
ConnectedUserInternalSession = string.Empty;
ConnectedUsername = string.Empty;
}

....
}

VB 

Public Class SessionConnectionInfo
Private privateHostUserName As String
Public Property HostUserName() As String
Get
Return privateHostUserName
End Get
Set(ByVal value As String)
privateHostUserName = value
End Set
End Property
Private privateConnectedUsername As String
Public Property ConnectedUsername() As String
Get
Return privateConnectedUsername
End Get
Private Set(ByVal value As String)
privateConnectedUsername = value
End Set
End Property
Private privateSessionKey As String
Public Property SessionKey() As String
Get
Return privateSessionKey
End Get
Set(ByVal value As String)
privateSessionKey = value
End Set
End Property

Private privateConnectedUserInternalSession As String
Public Property ConnectedUserInternalSession() As String
Get
Return privateConnectedUserInternalSession
End Get
Private Set(ByVal value As String)
privateConnectedUserInternalSession = value
End Set
End Property
Private privateHostInternalSession As String
Public Property HostInternalSession() As String
Get
Return privateHostInternalSession
End Get
Set(ByVal value As String)
privateHostInternalSession = value
End Set
End Property

Public ReadOnly Property UserConnected() As Boolean
Get
Return ConnectedUserInternalSession <> String.Empty
End Get
End Property

Public Sub New()
ConnectedUserInternalSession = String.Empty
ConnectedUsername = String.Empty
End Sub


End Class

Client Side

Now that we have the basic server side complete, we can add a service reference, and create the necessary service reference.  Our service is hosted within ASP.NET via a simple xml file (see FileSendService.svc), which allows our Silverlight project to see it.

Add Service Ref

Be sure to mark the checkbox “Always generate message contracts”.  Note that your web portion of the project must be compiled before you will be able to “Discover” the service.

We will now have access to the DuplexServiceClient class which will allow us to send and receive messages to the server.  We instantiate the service as shown below.  In order to do this, you must manually add a reference to System.ServiceModel.PollingDuplex assembly.

C#

private DuplexServiceClient fileDuplexService;

private CustomBinding binding = new CustomBinding(
new PollingDuplexBindingElement(),
new BinaryMessageEncodingBindingElement(),
new HttpTransportBindingElement());

public MainPage()
{
InitializeComponent();
fileDuplexService = new DuplexServiceClient(binding, new EndpointAddress("http://localhost:9797/FileSendService.svc"));
...
}

VB

Private fileDuplexService As DuplexServiceClient

Private binding As New CustomBinding(New PollingDuplexBindingElement(), New BinaryMessageEncodingBindingElement(), New HttpTransportBindingElement())


Public Sub New()
    InitializeComponent()
fileDuplexService = New DuplexServiceClient(binding, New EndpointAddress("http://localhost:9797/FileSendService.svc"))
End Sub


Note that at the time of this writing, adding the service reference does not properly create the ServiceReferences.ClientConfig file.  This is the reason the above is done using code.

Now that our service is setup, we need to set it up to handle sending and receiving of messages.   In order to send a message, we must first create a DupexMessage (or a descendant), and use the SendToServiceAsync method.  This requires a SendToService object which will contain the message, and an optional userState object which we can use to tag the request.  In this case, we will pass an enum value which represents the status of our upload.  In the example below, when the user clicks on the send file button, after choosing the file, we open our file, and send a FileBeginUpload message which contains the file name and its size.  Note that the server is set to deny any file that is greater than 20 million bytes.

C#

private void btnSendFile_Click(object sender, RoutedEventArgs e)
{
OpenFileDialog openFileDialog = new OpenFileDialog();
openFileDialog.Multiselect = false;
openFileDialog.ShowDialog();
if (openFileDialog.File != null)
{
fileToSend = openFileDialog.File.OpenRead();

FileBeginUploadMessage fsm = new FileBeginUploadMessage();
fsm.FileName = openFileDialog.File.Name;
fsm.TotalBytes = openFileDialog.File.Length;
fileDuplexService.SendToServiceAsync(new SendToService(fsm), FileSendState.FileStart);
....
}
}

VB

Private Sub btnSendFile_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
Dim openFileDialog As New OpenFileDialog()
openFileDialog.Multiselect = False
openFileDialog.ShowDialog()
If openFileDialog.File IsNot Nothing Then
fileToSend = openFileDialog.File.OpenRead()
Dim fsm As New FileBeginUploadMessage()
fsm.FileName = openFileDialog.File.Name
fsm.TotalBytes = openFileDialog.File.Length
totalBytesSent = 0
fileDuplexService.SendToServiceAsync(New SendToService(fsm), FileSendState.FileStart)
....
End If
End Sub

The service object fires two main events that need to be handled: SendToServiceCompleted and SendToClientReceived. SendToServiceCompleted is the event which is fired after the server acknowledges that it has received and processed a message that the client has sent.  Once the server receives and processes the FileBeginUploadMessage, the event handler below will receive the results.  In this case, if there isn't an error, and the value of userState is FileSendState.FileStart,  the method will send a FileTransferBytesMessage which will send the bytes from the file. 

C#

private void FileDuplexServiceSendToServiceCompleted(object sender, System.ComponentModel.AsyncCompletedEventArgs e)
{
if (e.Error == null)
{
{...}
if ((FileSendState)e.UserState == FileSendState.FileEnd)
{
fileToSend.Close();
fileProgress.Value = 100;
return;
}
if ((FileSendState)e.UserState == FileSendState.FileStart || (FileSendState)e.UserState == FileSendState.FileContinue)
{


            ...
FileTransferBytesMessage fileMessage = new FileTransferBytesMessage();
fileMessage.StartByte = totalBytesSent;
fileMessage.EndFile = false;
fileMessage.PacketSize = CHUNK;

...

byte[] bytes = new byte[numBytesToRead];
fileToSend.Read(bytes, 0, numBytesToRead);
totalBytesSent += numBytesToRead;
fileMessage.Bytes = bytes;

if (fileMessage.EndFile)
fileDuplexService.SendToServiceAsync(new SendToService(fileMessage), FileSendState.FileEnd);
else
fileDuplexService.SendToServiceAsync(new SendToService(fileMessage), FileSendState.FileContinue);
}
}
}

VB

Private Sub FileDuplexServiceSendToServiceCompleted(ByVal sender As Object, ByVal e As System.ComponentModel.AsyncCompletedEventArgs)
If e.Error Is Nothing Then
If e.UserState Is Nothing Then
Return
End If
If CType(e.UserState, FileSendState) = FileSendState.FileEnd Then
fileToSend.Close()
fileProgress.Value = 100
Return
End If
If CType(e.UserState, FileSendState) = FileSendState.FileStart OrElse CType(e.UserState, FileSendState) = FileSendState.FileContinue Then
...
Dim fileMessage As New FileTransferBytesMessage()
fileMessage.StartByte = totalBytesSent
fileMessage.EndFile = False
fileMessage.PacketSize = CHUNK
...
Dim bytes(numBytesToRead - 1) As Byte
fileToSend.Read(bytes, 0, numBytesToRead)
totalBytesSent += numBytesToRead
fileMessage.Bytes = bytes

If fileMessage.EndFile Then
fileDuplexService.SendToServiceAsync(New SendToService(fileMessage), FileSendState.FileEnd)
Else
fileDuplexService.SendToServiceAsync(New SendToService(fileMessage), FileSendState.FileContinue)
End If
End If
End If
End Sub

The SendToClientReceived event is fired when a message has been sent from the service.  In this case, the client will check to see which message was received and process it accordingly.  The method below shows that when a FileBeginUpload message is received, the user is prompted to accept or deny the file.  If the user does not accept the file, a FileDenyMessage will be sent which will notify the sender to stop sending bytes.  If accepted, file bytes are added to a buffer. 

C#

private void FileDuplexServiceSendToClientReceived(object sender, SendToClientReceivedEventArgs e)
{
if (e.Error == null)
{
if (e.request.msg is ClientConnectedMessage)
{
ClientConnectedMessage msg = (ClientConnectedMessage)e.request.msg;
AddMsgToListbox(msg.Username + " has just connected.");
connectedTo = msg.Username;
UIState = UIState.Chat;
}

else if (e.request.msg is HostSessionServerMessage)
{
HostSessionServerMessage hssm = e.request.msg as HostSessionServerMessage;
if (hssm.Failed) {...}
SessionCreated(hssm);

}
else if (e.request.msg is JoinSessionServerMessage)
{
JoinSessionServerMessage jssm = e.request.msg as JoinSessionServerMessage;
if (jssm.Failed) {....}
SessionJoined(jssm);
}
else if (e.request.msg is FileBeginUploadMessage )
{
FileBeginUploadMessage fsm = (FileBeginUploadMessage)e.request.msg;

int sizeInKB = (int)fsm.TotalBytes / 1024;
totalRevd = 0;
if (MessageBox.Show(connectedTo + " would like to send you the file: " + fsm.FileName + ", Size: " + sizeInKB + ". Would you like to receive this file?", "File Upload", MessageBoxButton.OKCancel) == MessageBoxResult.OK)
{
bytesReceived = new List<byte>((int)fsm.TotalBytes);
fileNameReceiving = fsm.FileName;
....
}
else
{
fileDuplexService.SendToServiceAsync(new SendToService(new FileDenyMessage()));
}
}
else if (e.request.msg is FileTransferBytesMessage)
{
if (bytesReceived == null)
return;
FileTransferBytesMessage fm = (FileTransferBytesMessage)e.request.msg;
bytesReceived.AddRange(fm.Bytes);
....
}
else {....}

}
}

VB

Private Sub FileDuplexServiceSendToClientReceived(ByVal sender As Object, ByVal e As SendToClientReceivedEventArgs)
If e.Error Is Nothing Then
If TypeOf e.request.msg Is ClientConnectedMessage Then
Dim msg As ClientConnectedMessage = CType(e.request.msg, ClientConnectedMessage)
AddMsgToListbox(msg.Username & " has just connected.")
connectedTo = msg.Username
UIState = UIState.Chat

ElseIf TypeOf e.request.msg Is HostSessionServerMessage Then
Dim hssm As HostSessionServerMessage = TryCast(e.request.msg, HostSessionServerMessage)
If hssm.Failed Then ...

SessionCreated(hssm)

ElseIf TypeOf e.request.msg Is JoinSessionServerMessage Then
Dim jssm As JoinSessionServerMessage = TryCast(e.request.msg, JoinSessionServerMessage)
If jssm.Failed Then ...

SessionJoined(jssm)
ElseIf TypeOf e.request.msg Is FileBeginUploadMessage Then
Dim fsm As FileBeginUploadMessage = CType(e.request.msg, FileBeginUploadMessage)

Dim sizeInKB As Integer = CInt(Fix(fsm.TotalBytes)) / 1024
totalRevd = 0
If MessageBox.Show(connectedTo & " would like to send you the file: " & fsm.FileName & ", Size: " & sizeInKB & " KB. Would you like to receive this file?", "File Upload", MessageBoxButton.OKCancel) = MessageBoxResult.OK Then
bytesReceived = New List(Of Byte)(CInt(Fix(fsm.TotalBytes)))
fileNameReceiving = fsm.FileName
...
Else
fileDuplexService.SendToServiceAsync(New SendToService(New FileDenyMessage()))
End If
ElseIf TypeOf e.request.msg Is FileTransferBytesMessage Then
If bytesReceived Is Nothing Then
Return
End If
Dim fm As FileTransferBytesMessage = CType(e.request.msg, FileTransferBytesMessage)
bytesReceived.AddRange(fm.Bytes)
...
ElseIf
...

End If
End Sub

 

When the file is complete, the user will be presented with two buttons.  One button will allow the user to save, and the other will allow the user to discard/ignore the file.  If the user chooses to save, a SaveFileDialog is created, and we use the filename to assign the default extension and filter so that when the user saves the file, no extension needs to be typed in.  Once the dialog appears and the user enters the file name, the bytes will be written to disk.  Finally we send a message back to the server which will notify the sender that user has done something with the file.  During file send operations, the Send File button is disabled, and becomes re-enabled once a file has been received, cancelled, or denied.

C#

private void btnSaveFile_Click(object sender, RoutedEventArgs e)
{
SaveFileDialog sfd = new SaveFileDialog();
string extension = GetExtension(fileNameReceiving);
sfd.DefaultExt = extension;
sfd.Filter = extension + " Files|" + extension;

if (sfd.ShowDialog() == true)
{
using (Stream fsx = sfd.OpenFile())
{
byte[] fBytes = bytesReceived.ToArray();
fsx.Write(fBytes, 0, fBytes.Length);
fsx.Close();
}

fileDuplexService.SendToServiceAsync(new SendToService(new FileReceivedMessage()));
UIState = UIState.Chat;
btnSendFile.IsEnabled = true;
}
}

 

VB

Private Sub btnSaveFile_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
Dim sfd As New SaveFileDialog()
Dim extension As String = GetExtension(fileNameReceiving)
sfd.DefaultExt = extension
sfd.Filter = extension & " Files|" & extension

If sfd.ShowDialog() = True Then
Using fsx As Stream = sfd.OpenFile()
Dim fBytes() As Byte = bytesReceived.ToArray()
fsx.Write(fBytes, 0, fBytes.Length)
fsx.Close()
End Using

fileDuplexService.SendToServiceAsync(New SendToService(New FileReceivedMessage()))
UIState = UIState.Chat
btnSendFile.IsEnabled = True
End If
End Sub

 

Conclusion

Overall, the application does what I set out for it to.  It allows a user to send a file to another user and even does basic chat.  There is definitely a lot of room for improvement, and some general additions that can make this more robust.  Right now we keep a list of connections, but never do basic pinging to see if the clients are still there.  The only way to know if a client has disconnected is when a message fails, or when the user decides to hit the disconnect button. Better maintenance of this list is needed.  The file bytes are basically being stored in memory, which is why i set a file size limit, but I'm sure with things like Silverlight local storage, we can increase the limit. 

Thanks

I would like to thank Brian Peek for taking the time to review my article and test the code.

Additional Notes

The ASP.NET project requires a file called System.ServiceModel.PollingDuplex.dll to be added as a reference.  Somewhere between Silverlight 3 Beta and Silverlight 3 RTW, the file was no longer available via the basic add .NET reference link.  In order to add the file, I had to browse for it in %Program Files%\Microsoft SDKs\Silverlight\v3.0\Libraries\Server.  For 64 bit users, it will be the Program Files (x86) folder. 

For ease of use, I used a static port (9797) for this project. When deploying to a server, you must rename all references of http://localhost:9797 in the Silverlight project.  This will remain true until the config file is supported.

The user interface uses the Silverlight Toolkit TwilightBlue theme.  For more information about this,  see http://www.codeplex.com/Silverlight

Tags:

Follow the Discussion

  • senthilnathan sksenthilnath​an sk

    cool application.. works very nicely.. user interface also looking good.. thanks for the project..

  • Eisen626Eisen626

    If I understand this correctly the file you are transferring passes through the server. Any ideas on how to make this a true P2P transfer?

  • bahno77bahno77

    error Sad : Cannot register duplicate Name 'UIStateConv' in this scope.

      at System.Windows.NameScope.RegisterName(String name, Object scopedElement)

      at Microsoft.Expression.DesignModel.InstanceBuilders.ClrObjectInstanceBuilder.ModifyValue(IInstanceBuilderContext context, ViewNode target, IProperty propertyKey, Object value, PropertyModification modification)

      at Microsoft.Expression.DesignModel.InstanceBuilders.ClrObjectInstanceBuilder.UpdateProperty(IInstanceBuilderContext context, ViewNode viewNode, IProperty propertyKey, DocumentNode valueNode) ... please help me Sad my mail is : bahno77@gmail.com ... thank you

  • Clint RutkasClint I'm a "developer"

    @bahno77 is this from the source or the physical app itself?  I need more to go on.  If it is from within Visual Studio, did you alter the program at all?

  • Clint RutkasClint I'm a "developer"

    @bahno77 <SLFileSender:UIStateConverter x:Key="UIStateConv" />

    x:Name is causing it, should be a Key

    I'll get this corrected in the project.

  • bahno77bahno77

    @Coding4Fun Hello, it is from source here is screenshot : http://yfrog.com/0aerrnp .... on designer it saying : Cannot register duplicate Name 'UIStateConv' in this scope. Please help me

  • GiovanniGiovanni

    Did you open up the service reference files and change all references from the path: http://localhost:9797/FileSendService.svc  to point to your location where you're hosting the web app?

    In the SL project, look under Service References folder, then you'll see the FileSendService.  Set VS to show ALL files.  This will allow you to expand FileSendService and look and edit the files within.  Search for the localhost string above and replace.  Aftewards, recompile and upload the new xap file.

  • bahno77bahno77

    Hello try my web site here : api.aspone.cz/SLFileSenderTestPage.aspx ... there is original download source code and it won't work Sad ....

    (it won't make new session try it please as soon as possible , thank you)

  • bahno77bahno77

    @Coding4Fun yeah thank you Smiley ,but try to download source from link on this site and then upload it to another asp.net site becose i downloaded source and uploaded it and if i created session , nothing happened :/ ... is only two options , first i'm stupid or second somenone uploaded bad code.

  • bahno77bahno77

    @Giovanni , THANK YOU SO MUCH !!! it works Smiley) thank you Smiley

Remove this comment

Remove this thread

close

Comments Closed

Comments have been closed since this content was published more than 30 days ago, but if you'd like to continue the conversation, please create a new thread in our Forums,
or Contact Us and let us know.