Приложение передачи файлов на Silverlight 3

Опубликовано 3 сентября 2009 23:43:00 | Coding4Fun

Введение

Раз за разом я сталкиваюсь с ситуацией, когда надо отправить кому-то файл, но сделать это оказывается не так-то просто. Программы мгновенного обмена сообщениями часто не могут обеспечить обмен из-за наличия брандмауэров, различий в версиях клиентских программ и других несовместимостей. При использовании электронной почты я сталкивался с тем, что почтовый сервер моего корреспондента блокировал файлы определенных типов. Предлагаемая программа позволяет двум пользователям быстро и просто связываться посредством клиентов, написанных на Silverlight 3 и слать друг другу файлы.

Обзор

В данном приложении пользователь сначала выбирает, инициировать ли ему самому сеанс или подключиться к существующему. Если он решает сам управлять сеансом, ему будет предоставлен случайный восьмисимвольный ключ, и программа будет находиться в состоянии ожидания до тех пор, пока не подключится другой пользователь. Когда какой-то пользователь захочет подключиться к данному сеансу, ему потребуется данный ключ для установления связи. Соединившись между собой, пользователи смогут обмениваться файлами и простыми текстовыми сообщениями.

Опрос в дуплексном режиме

Для обмена сообщениями между двумя клиентскими приложениями требуется некая центральная общая точка для маршрутизации сообщений. Поскольку Silverlight-приложение легко можно расположить на странице ASP.NET, мы будем использовать серверные возможности ASP.NET для управления коммуникациями между пользователями. Нам нужна служба, которая будет принимать входящие сообщения от Silverlight-клиента и переправлять их требуемому адресату. Это делается с помощью WCF-канала опроса в дуплексном режиме Polling Duplex (System . ServiceModel . PollingDuplex . dll). Silverlight 3 позволяет добавить ссылку на такую службу и скрывает от нас все сложные подробности ее работы. Я начал с использования файла DuplexService . cs из демонстрационного приложения, опубликованного на MIX09, когда вышла бета-версия Silverlight 3. Там есть пара абстрактных базовых классов и интерфейсов, от которых мы будем наследовать классы собственной службы.

FileSendService

Две главные вещи для создания собственной службы — это определение специальных типов сообщений, которые будут применяться в наших коммуникациях, и переопределение класса DuplexService таким образом, чтобы он правильно обрабатывал эти сообщения. Для создания собственных типов сообщений мы используем в качестве базового класс DuplexMessage , который определен в DuplexService . cs. Наши сообщения должны быть определены с атрибутом [ DataContract ] , а переменные-члены должны быть открытыми и иметь атрибут [ DataMember ] . Это позволит проекту Silverlight-клиента предоставлять доступ к этим определениям через ссылку службы. Кроме того, класс сообщения Duplex должен иметь атрибут [ KnownType ] для каждого созданного сообщения-наследника.

C#

    1: [KnownType(typeof(HostSessionMessage))]
    2: [KnownType(typeof(JoinSessionMessage))]
    3: [KnownType(typeof(FileBeginUploadMessage))]
    4: [KnownType(typeof(FileTransferBytesMessage))]
    5: public class DuplexMessage { }
    6: [DataContract]
    7: public class HostSessionMessage : DuplexMessage
    8: {
    9:     [DataMember]
   10:     public string Username;
   11: }
   12:  
   13: [DataContract]
   14: public class JoinSessionMessage : DuplexMessage
   15: {
   16:     [DataMember]
   17:     public string Username;
   18:     [DataMember]
   19:     public string SessionKey;
   20: }
   21:  
   22: [DataContract]
   23: public class FileBeginUploadMessage : DuplexMessage
   24: {
   25:     [DataMember]
   26:     public string FileName;
   27:     [DataMember]
   28:     public long TotalBytes;
   29: }
   30:  
   31: [DataContract]
   32: public class FileTransferBytesMessage : DuplexMessage
   33: {
   34:     [DataMember]
   35:     public long StartByte;
   36:     [DataMember]
   37:     public long PacketSize;
   38:     [DataMember]
   39:     public byte[] Bytes;
   40:     [DataMember]
   41:     public bool EndFile;
   42: }
   43:  

VB

    1: <DataContract(Namespace := "https://samples.microsoft.com/silverlight2/duplex"), 
    2: KnownType(GetType(HostSessionMessage)), KnownType(GetType(JoinSessionMessage)), KnownType(GetType(FileBeginUploadMessage)), KnownType(GetType(FileTransferBytesMessage))> _
    3: Public Class DuplexMessage
    4: End Class
    5:  
    6: <DataContract()> _
    7: Public Class HostSessionMessage
    8:     Inherits DuplexMessage
    9:     <DataMember()> Public Username As String
   10: End Class
   11:  
   12: <DataContract()> _
   13: Public Class JoinSessionMessage
   14:     Inherits DuplexMessage
   15:     <DataMember()> Public Username As String
   16:     <DataMember()> Public SessionKey As String
   17: End Class
   18:  
   19: <DataContract()> _
   20: Public Class FileTransferBytesMessage
   21:     Inherits DuplexMessage
   22:     <DataMember()> Public StartByte As Long
   23:     <DataMember()> Public PacketSize As Long
   24:     <DataMember()> Public Bytes() As Byte
   25:     <DataMember()> Public EndFile As Boolean
   26: End Class
   27:  
   28: <DataContract()> _
   29: Public Class FileBeginUploadMessage
   30:     Inherits DuplexMessage
   31:     <DataMember()> Public FileName As String
   32:     <DataMember()> Public TotalBytes As Long
   33: End Class

Следующий шаг — создание класса FileSendService, производного от DuplexService, как было сказано выше. Переопределим метод OnMessage чтобы обрабатывать сообщения собственных типов:

C#

    1: [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
    2: public class FileSendService : DuplexService
    3: {
    4:     private List<SessionConnectionInfo> sessionConnections = new List<SessionConnectionInfo>();
    5:  
    6:     {...}
    7:  
    8:     protected override void OnMessage(string sessionId, DuplexMessage data)
    9:     {
   10:         if (data is HostSessionMessage)
   11:             CreateHostSession(data as HostSessionMessage);
   12:         else if (data is JoinSessionMessage)
   13:             JoinSession(data as JoinSessionMessage);
   14:         else if (data is FileBeginUploadMessage)
   15:             StartSendFile(data as FileBeginUploadMessage);
   16:         else
   17:             SendMessage(data);
   18:     }
   19:     
   20: }
   21:  
   22:         else if (data is JoinSessionMessage)
   23:             JoinSession(data as JoinSessionMessage);
   24:         else if (data is FileBeginUploadMessage)
   25:             StartSendFile(data as FileBeginUploadMessage);
   26:         else
   27:             SendMessage(data);
   28:     }
   29:     
   30: }
   31:  
   32:     }
   33:     
   34: }
   35:  

VB

    1: Public Class FileSenderServiceFactory
    2:     Inherits DuplexServiceFactory(Of FileSendService)
    3: End Class
    4:  
    5: <AspNetCompatibilityRequirements(RequirementsMode := AspNetCompatibilityRequirementsMode.Allowed)> _
    6: Public Class FileSendService
    7:     Inherits DuplexService
    8:     Private sessionConnections As New List(Of SessionConnectionInfo)()
    9:  
   10:     ...
   11:  
   12:     Protected Overrides Sub OnMessage(ByVal sessionId As String, ByVal data As DuplexMessage)
   13:         If TypeOf data Is HostSessionMessage Then
   14:             CreateHostSession(TryCast(data, HostSessionMessage))
   15:         ElseIf TypeOf data Is JoinSessionMessage Then
   16:             JoinSession(TryCast(data, JoinSessionMessage))
   17:         ElseIf TypeOf data Is FileBeginUploadMessage Then
   18:             StartSendFile(TryCast(data, FileBeginUploadMessage))
   19:         Else
   20:             SendMessage(data)
   21:         End If
   22:     End Sub
   23:  
   24: End Class

Чтобы отслеживать все хосты и подключенных к ним пользователей, создадим класс SessionConnectionInfo для управления соответствующими данными. Когда пользователь решает создать сеанс и управлять им, наш метод OnMessage получает сообщение HostSessionMessageи просматривает списокList < SessionConnectionInfo > на предмет наличия в нем другого хоста с тем же именем пользователя. Если таковой не обнаруживается, создается новый объект SessionConnectionInfo и сеансовый ключ, сгенерированный из случайных значений. Когда пользователь пытается подключиться к сеансу, метод OnMessage получает сообщение JoinSessionMessage , содержащее сеансовый ключ и имя подключающегося к сеансу пользователя. Наша служба ищет объект SessionConnectionInfo с таким сеансовым ключом и, если находит, связывает двух пользователей между собой.

C#

    1: public class SessionConnectionInfo
    2: {
    3:     public string HostUserName { get; set; }
    4:     public string ConnectedUsername { get; private set; }
    5:     public string SessionKey { get; set; }
    6:  
    7:     public string ConnectedUserInternalSession { get; private set; }
    8:     public string HostInternalSession { get; set; }
    9:  
   10:     public bool UserConnected
   11:     {
   12:         get { return ConnectedUserInternalSession != string.Empty; }
   13:     }
   14:  
   15:     public SessionConnectionInfo()
   16:     {
   17:         ConnectedUserInternalSession = string.Empty;
   18:         ConnectedUsername = string.Empty;
   19:     }
   20:  
   21:     ....
   22: }

VB

    1: Public Class SessionConnectionInfo
    2:     Private privateHostUserName As String
    3:     Public Property HostUserName() As String
    4:         Get
    5:             Return privateHostUserName
    6:         End Get
    7:         Set(ByVal value As String)
    8:             privateHostUserName = value
    9:         End Set
   10:     End Property
   11:     Private privateConnectedUsername As String
   12:     Public Property ConnectedUsername() As String
   13:         Get
   14:             Return privateConnectedUsername
   15:         End Get
   16:         Private Set(ByVal value As String)
   17:             privateConnectedUsername = value
   18:         End Set
   19:     End Property
   20:     Private privateSessionKey As String
   21:     Public Property SessionKey() As String
   22:         Get
   23:             Return privateSessionKey
   24:         End Get
   25:         Set(ByVal value As String)
   26:             privateSessionKey = value
   27:         End Set
   28:     End Property
   29:  
   30:     Private privateConnectedUserInternalSession As String
   31:     Public Property ConnectedUserInternalSession() As String
   32:         Get
   33:             Return privateConnectedUserInternalSession
   34:         End Get
   35:         Private Set(ByVal value As String)
   36:             privateConnectedUserInternalSession = value
   37:         End Set
   38:     End Property
   39:     Private privateHostInternalSession As String
   40:     Public Property HostInternalSession() As String
   41:         Get
   42:             Return privateHostInternalSession
   43:         End Get
   44:         Set(ByVal value As String)
   45:             privateHostInternalSession = value
   46:         End Set
   47:     End Property
   48:  
   49:     Public ReadOnly Property UserConnected() As Boolean
   50:         Get
   51:             Return ConnectedUserInternalSession <> String.Empty
   52:         End Get
   53:     End Property
   54:  
   55:     Public Sub New()
   56:         ConnectedUserInternalSession = String.Empty
   57:         ConnectedUsername = String.Empty
   58:     End Sub
   59:  
   60:  
   61: End Class
   62:  

Клиентская часть

Основы серверной части готовы и можем добавить ссылку на нашу службу. Наша служба базируется в ASP.NET посредством простого xml-файла (см. FileSendService . svc), который позволяет нашему Silverlight-проекту ее видеть.

clip_image001

Не забудьте установить флажок «Всегда создавать контракты сообщений» (“Always generate message contracts”). Имейте в виду: часть проекта, связанная с веб, должна быть скомпилирована, чтобы можно было обнаружить службу.

Теперь у нас есть доступ к классу DuplexServiceClient , который позволит нам обмениваться сообщениями с сервером. Создадим экземпляр службы, как показано ниже. Предварительно надо вручную добавить ссылку на сборку System . ServiceModel . PollingDuplex.

C#

 

    1: private DuplexServiceClient fileDuplexService;
    2:  
    3: private CustomBinding binding = new CustomBinding(
    4:     new PollingDuplexBindingElement(),
    5:     new BinaryMessageEncodingBindingElement(),
    6:     new HttpTransportBindingElement());
    7:  
    8: public MainPage()
    9: {
   10:     InitializeComponent();
   11:     fileDuplexService = new DuplexServiceClient(binding, new EndpointAddress("https://localhost:9797/FileSendService.svc"));
   12:     ...
   13: }

VB

    1: Private fileDuplexService As DuplexServiceClient
    2:  
    3: Private binding As New CustomBinding(New PollingDuplexBindingElement(), New BinaryMessageEncodingBindingElement(), New HttpTransportBindingElement())
    4:     
    5: Public Sub New()
    6:   InitializeComponent()
    7:     fileDuplexService = New DuplexServiceClient(binding, New EndpointAddress("https://localhost:9797/FileSendService.svc"))
    8: End Sub

Обратите внимание: на момент написания этой статьи, при добавлении ссылки на службу, файл ServiceReferences . ClientConfig не создавался. По этой причине в приведенном выше коде это делается программно.

Настроим службу таким образом, чтобы она обрабатывала отправляемые и получаемые сообщения. Для отправки сообщения сначала надо создать сообщение DupexMessage (или его производную) и применить метод SendToServiceAsync. Для этого требуется объект SendToService , содержащий данное сообщение, а также необязательный объект userState , который мы можем применять для маркирования запроса. В данном случае мы передаем перечисление, описывающее состояние передачи. В приведенном ниже примере, когда пользователь щелкает кнопку отправки, предварительно выбрав файл, мы открываем наш файл и отправляем сообщение FileBeginUpload , содержащее имя файла и его размер. Заметьте: сервер настроен на отказ от отправки файлов размером более 20 миллионов байт.

C#

    1: private void btnSendFile_Click(object sender, RoutedEventArgs e)
    2:  {
    3:      OpenFileDialog openFileDialog = new OpenFileDialog();
    4:      openFileDialog.Multiselect = false;
    5:      openFileDialog.ShowDialog();
    6:      if (openFileDialog.File != null)
    7:      {
    8:          fileToSend = openFileDialog.File.OpenRead();
    9:  
   10:          FileBeginUploadMessage fsm = new FileBeginUploadMessage();
   11:          fsm.FileName = openFileDialog.File.Name;
   12:          fsm.TotalBytes = openFileDialog.File.Length;
   13:          fileDuplexService.SendToServiceAsync(new SendToService(fsm), FileSendState.FileStart);
   14:          ....
   15:      }
   16:  }

VB

    1: Private Sub btnSendFile_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
    2:     Dim openFileDialog As New OpenFileDialog()
    3:     openFileDialog.Multiselect = False
    4:     openFileDialog.ShowDialog()
    5:     If openFileDialog.File IsNot Nothing Then
    6:         fileToSend = openFileDialog.File.OpenRead()
    7:         Dim fsm As New FileBeginUploadMessage()
    8:         fsm.FileName = openFileDialog.File.Name
    9:         fsm.TotalBytes = openFileDialog.File.Length
   10:         totalBytesSent = 0
   11:         fileDuplexService.SendToServiceAsync(New SendToService(fsm), FileSendState.FileStart)
   12:         ....
   13:     End If
   14: End Sub

Наша служба генерирует два события, требующие обработки: SendToServiceCompleted и SendToClientReceived. Событие SendToServiceCompleted возникает после того как сервер подтверждает получение и завершение обработки отправленного клиентом сообщения. После того, как сервер получил и обработал сообщение FileBeginUploadMessage, приведенный ниже обработчик события получает результаты. В данном случае, если нет ошибки, а userState имеет значение FileSendState . FileStart, метод отправляет сообщение FileTransferBytesMessage , которое передает данные файла.

C#

    1: private void FileDuplexServiceSendToServiceCompleted(object sender, System.ComponentModel.AsyncCompletedEventArgs e)
    2: {
    3:     if (e.Error == null)
    4:     {
    5:         {...}
    6:         if ((FileSendState)e.UserState == FileSendState.FileEnd)
    7:         {
    8:             fileToSend.Close();
    9:             fileProgress.Value = 100;
   10:             return;
   11:         }
   12:         if ((FileSendState)e.UserState == FileSendState.FileStart || (FileSendState)e.UserState == FileSendState.FileContinue)
   13:         {
   14:  
   15:       ...
   16:             FileTransferBytesMessage fileMessage = new FileTransferBytesMessage();
   17:             fileMessage.StartByte = totalBytesSent;
   18:             fileMessage.EndFile = false;
   19:             fileMessage.PacketSize = CHUNK;
   20:  
   21:             ...
   22:  
   23:             byte[] bytes = new byte[numBytesToRead];
   24:             fileToSend.Read(bytes, 0, numBytesToRead);
   25:             totalBytesSent += numBytesToRead;
   26:             fileMessage.Bytes = bytes;
   27:  
   28:             if (fileMessage.EndFile)
   29:                 fileDuplexService.SendToServiceAsync(new SendToService(fileMessage), FileSendState.FileEnd);
   30:             else
   31:                 fileDuplexService.SendToServiceAsync(new SendToService(fileMessage), FileSendState.FileContinue);
   32:         }
   33:     }
   34: }

VB

    1: Private Sub FileDuplexServiceSendToServiceCompleted(ByVal sender As Object, ByVal e As System.ComponentModel.AsyncCompletedEventArgs)
    2:     If e.Error Is Nothing Then
    3:         If e.UserState Is Nothing Then
    4:             Return
    5:         End If
    6:         If CType(e.UserState, FileSendState) = FileSendState.FileEnd Then
    7:             fileToSend.Close()
    8:             fileProgress.Value = 100
    9:             Return
   10:         End If
   11:         If CType(e.UserState, FileSendState) = FileSendState.FileStart OrElse CType(e.UserState, FileSendState) = FileSendState.FileContinue Then
   12:             ...
   13:             Dim fileMessage As New FileTransferBytesMessage()
   14:             fileMessage.StartByte = totalBytesSent
   15:             fileMessage.EndFile = False
   16:             fileMessage.PacketSize = CHUNK
   17:             ...
   18:             Dim bytes(numBytesToRead - 1) As Byte
   19:             fileToSend.Read(bytes, 0, numBytesToRead)
   20:             totalBytesSent += numBytesToRead
   21:             fileMessage.Bytes = bytes
   22:  
   23:             If fileMessage.EndFile Then
   24:                 fileDuplexService.SendToServiceAsync(New SendToService(fileMessage), FileSendState.FileEnd)
   25:             Else
   26:                 fileDuplexService.SendToServiceAsync(New SendToService(fileMessage), FileSendState.FileContinue)
   27:             End If
   28:         End If
   29:     End If
   30: End Sub
   31:  

Событие SendToClientReceived генерируется после отправки сообщения нашей службой. Клиент выясняет тип этого сообщения и соответствующим образом его обрабатывает. В приведенном ниже методе пользователь принимает или отклоняет получение файла при поступлении сообщения FileBeginUpload. Если пользователь не принимает файл, отправляется сообщение FileDenyMessage , указывающее отправителю, что надо прекратить передавать данные. В противном случае данные добавляются в буфер.

C#

    1: private void FileDuplexServiceSendToClientReceived(object sender, SendToClientReceivedEventArgs e)
    2: {
    3:     if (e.Error == null)
    4:     {
    5:         if (e.request.msg is ClientConnectedMessage)
    6:         {
    7:             ClientConnectedMessage msg = (ClientConnectedMessage)e.request.msg;
    8:             AddMsgToListbox(msg.Username + " has just connected.");
    9:             connectedTo = msg.Username;
   10:             UIState = UIState.Chat;
   11:         }
   12:  
   13:         else if (e.request.msg is HostSessionServerMessage)
   14:         {
   15:             HostSessionServerMessage hssm = e.request.msg as HostSessionServerMessage;
   16:             if (hssm.Failed) {...}
   17:             SessionCreated(hssm);
   18:  
   19:         }
   20:         else if (e.request.msg is JoinSessionServerMessage)
   21:         {
   22:             JoinSessionServerMessage jssm = e.request.msg as JoinSessionServerMessage;
   23:             if (jssm.Failed) {....}
   24:             SessionJoined(jssm);
   25:         }
   26:         else if (e.request.msg is FileBeginUploadMessage )
   27:         {
   28:             FileBeginUploadMessage fsm = (FileBeginUploadMessage)e.request.msg;
   29:  
   30:             int sizeInKB = (int)fsm.TotalBytes / 1024;
   31:             totalRevd = 0;
   32:             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)
   33:             {
   34:                 bytesReceived = new List<byte>((int)fsm.TotalBytes);
   35:                 fileNameReceiving = fsm.FileName;
   36:                 ....
   37:             }
   38:             else
   39:             {
   40:                 fileDuplexService.SendToServiceAsync(new SendToService(new FileDenyMessage()));
   41:             }
   42:         }
   43:         else if (e.request.msg is FileTransferBytesMessage)
   44:         {
   45:             if (bytesReceived == null)
   46:                 return;
   47:             FileTransferBytesMessage fm = (FileTransferBytesMessage)e.request.msg;
   48:             bytesReceived.AddRange(fm.Bytes);
   49:             ....
   50:         }
   51:         else {....}
   52:  
   53:     }
   54: }

VB

    1: Private Sub FileDuplexServiceSendToClientReceived(ByVal sender As Object, ByVal e As SendToClientReceivedEventArgs)
    2:     If e.Error Is Nothing Then
    3:         If TypeOf e.request.msg Is ClientConnectedMessage Then
    4:             Dim msg As ClientConnectedMessage = CType(e.request.msg, ClientConnectedMessage)
    5:             AddMsgToListbox(msg.Username & " has just connected.")
    6:             connectedTo = msg.Username
    7:             UIState = UIState.Chat
    8:  
    9:         ElseIf TypeOf e.request.msg Is HostSessionServerMessage Then
   10:             Dim hssm As HostSessionServerMessage = TryCast(e.request.msg, HostSessionServerMessage)
   11:             If hssm.Failed Then ...
   12:  
   13:             SessionCreated(hssm)
   14:  
   15:         ElseIf TypeOf e.request.msg Is JoinSessionServerMessage Then
   16:             Dim jssm As JoinSessionServerMessage = TryCast(e.request.msg, JoinSessionServerMessage)
   17:             If jssm.Failed Then ...
   18:  
   19:             SessionJoined(jssm)
   20:         ElseIf TypeOf e.request.msg Is FileBeginUploadMessage Then
   21:             Dim fsm As FileBeginUploadMessage = CType(e.request.msg, FileBeginUploadMessage)
   22:  
   23:             Dim sizeInKB As Integer = CInt(Fix(fsm.TotalBytes)) / 1024
   24:             totalRevd = 0
   25:             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
   26:                 bytesReceived = New List(Of Byte)(CInt(Fix(fsm.TotalBytes)))
   27:                 fileNameReceiving = fsm.FileName
   28:                 ...
   29:             Else
   30:                 fileDuplexService.SendToServiceAsync(New SendToService(New FileDenyMessage()))
   31:             End If
   32:         ElseIf TypeOf e.request.msg Is FileTransferBytesMessage Then
   33:             If bytesReceived Is Nothing Then
   34:                 Return
   35:             End If
   36:             Dim fm As FileTransferBytesMessage = CType(e.request.msg, FileTransferBytesMessage)
   37:             bytesReceived.AddRange(fm.Bytes)
   38:             ...
   39:         ElseIf 
   40:           ...
   41:  
   42:     End If
   43: End Sub

По завершении передачи файла пользователь увидит две кнопки. Одна позволяет сохранить файл, а другая — уничтожить полученные данные. Если пользователь выбирает сохранение, появляется диалоговое окно SaveFileDialog в котором можно задать имя файла, а расширение водить нет необходимости. После задания пользователем имени фала, данные записываются на диск. В завершении серверу отправляется ответное сообщение, которое позволит ему уведомить пользователя, что принимающая сторона сделала что-то с файлом. Во время передачи файла кнопка Send File остается недоступной и разблокируется только после того, как был получен или отклонен или операция была прервана.

C#

    1: private void btnSaveFile_Click(object sender, RoutedEventArgs e)
    2: {
    3:     SaveFileDialog sfd = new SaveFileDialog();
    4:     string extension = GetExtension(fileNameReceiving);
    5:     sfd.DefaultExt = extension;
    6:     sfd.Filter = extension +  " Files|" + extension;
    7:  
    8:     if (sfd.ShowDialog() == true)
    9:     {
   10:         using (Stream fsx = sfd.OpenFile())
   11:         {
   12:             byte[] fBytes = bytesReceived.ToArray();
   13:             fsx.Write(fBytes, 0, fBytes.Length);
   14:             fsx.Close();
   15:         }
   16:  
   17:         fileDuplexService.SendToServiceAsync(new SendToService(new FileReceivedMessage()));
   18:         UIState = UIState.Chat;
   19:         btnSendFile.IsEnabled = true;
   20:     }
   21: }
   22:  

VB

    1: Private Sub btnSaveFile_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
    2:     Dim sfd As New SaveFileDialog()
    3:     Dim extension As String = GetExtension(fileNameReceiving)
    4:     sfd.DefaultExt = extension
    5:     sfd.Filter = extension & " Files|" & extension
    6:  
    7:     If sfd.ShowDialog() = True Then
    8:         Using fsx As Stream = sfd.OpenFile()
    9:             Dim fBytes() As Byte = bytesReceived.ToArray()
   10:             fsx.Write(fBytes, 0, fBytes.Length)
   11:             fsx.Close()
   12:         End Using
   13:  
   14:         fileDuplexService.SendToServiceAsync(New SendToService(New FileReceivedMessage()))
   15:         UIState = UIState.Chat
   16:         btnSendFile.IsEnabled = True
   17:     End If
   18: End Sub
Завершение

В целом приложение выполняет мои задумки. Пользователи могут обмениваться файлами, и есть даже элементарный чат. Конечно же, есть много вариантов усовершенствования этой программы и увеличения ее надежности. Сейчас поддерживается список соединений, но не выполняется простое пингование, позволяющее убедиться в том, что клиент еще на месте. Единственный способ узнать, что клиент отключился, это сбой в передаче сообщения или когда пользователь щелкает кнопку отключения. Требуется лучшая поддержка этого списка. Данные файла хранятся в памяти, что заставило меня ввести ограничения по объему, но я уверен, что с использованием таких вещей, как локальное хранилище Silverlight, это ограничение можно облегчить.

Благодарности

Я хочу поблагодарить Брайана Пика (Brian Peek (EN)), который нашел время рецензировать мою статью и проверить код программы.

Дополнительные замечания

В проекте ASP.NET необходимо указать в качестве ссылки файл System.ServiceModel.PollingDuplex.dll. Где-то в промежутке между Silverlight 3 Beta и Silverlight 3 RTW, этот файл исчез из числа доступных в основном списке ссылок .NET. Я его добавлял из % ProgramFiles %\ MicrosoftSDKs \ Silverlight \ v3.0\ Libraries \ Server. Для пользователей 64-разрядной версии это будет папка ProgramFiles ( x86) .

Для простоты использования я в данном проекте работал со статическим портом 9797. При установке на сервере вам надо переименовать все ссылки https://localhost:9797 в Silverlight-проекте. Это останется в силе, пока поддерживается файл config.

В пользовательском интерфейсе задействована тема TwilightBlue из Silverlight Toolkit. Дополнительные сведения по этому вопросу см. на странице https://www.codeplex.com/Silverlight (EN).