in
Forums
Blogs
DevExpress.com
Client Center
Support Center
DevExpress Channel

The One With

Silverlight Data Grid - Creating an IM/Chat Server and Client using Sockets

One of the things I love about Silverlight 2 Beta 2+, is that cross domain networking is now possible and all you have to do is provide a policy file on your target server that will dictate the security permissions. For your HTTP traffic it is just a matter of dropping a clientaccesspolicy.xml file at the root of your domain and for raw sockets you provide this file via a special port 943. Having support for cross-domain networking means that real time clients are now easy to develop. In this article we will use Sockets to build a simple chat application.

The Server

Before any Socket connection could be made by a Silverlight client, the runtime will try to connect to a special port 943 to get the cross domain policy information. It will send a <policy-file-request/> string for the request so we will need to reply back with a valid cross-domain policy xml.

In our case we will send back a simple "allow all" configuration.

<?xml version="1.0" encoding ="utf-8"?>
<access-policy>
    <cross-domain-access>
        <policy>
            <allow-from>
                <domain uri="*" />
            </allow-from>
            <grant-to>
                <socket-resource port="4510" protocol="tcp" />
            </grant-to>
        </policy>
    </cross-domain-access>
</access-policy>
 
Policy Server implementation:
 
public class PolicyServer : SocketServer {
    public static readonly byte[] PolicyBuffer = System.IO.File.ReadAllBytes("clientaccesspolicy.xml");

    public PolicyServer()
        : base(943) {
    }

    protected override void OnAccept(Socket client) {
        PostReceive(client);
    }

    protected override void OnReceive(Socket client, byte[] receivedBytes, int count) {
        string request = Encoding.UTF8.GetString(receivedBytes);
        if (!string.IsNullOrEmpty(request) && request.StartsWith("<policy-file-request/>")) {
            client.Send(PolicyBuffer);
        }
        client.Close();
    }
}

At a minimum, there are 3 commands that our server will need to handle.

  • Login
  • RefreshUsers
  • Message

I will use XML to send the commands back and forth since it is easy to serialize them using the XML Serialization facilities provided by the .NET framework.

public enum Operation {
    Unknown,
    Login,
    Message,
    RefreshUsers,
}

[XmlRoot("Packet")]
public class Packet {
    public Packet() {
        this.Children = new List<User>();
    }
    public Guid UserId { get; set; }
    public string UserName { get; set; }
    public Operation Operation { get; set; }
    public string Payload { get; set; }
    public List<User> Children { get; set; }
}
  • UserId - A unique ID for a specific user.
  • Children - List of User objects for the RefreshUsers command.
  • Payload - Text for the Message command.

A helper class to convert our Packet object to and from XML:

public static class Protocol {

    public static byte[] GetPacket(Packet packet) {
        XmlSerializer serializer = new XmlSerializer(typeof(Packet));
        using (MemoryStream stream = new MemoryStream()) {
            serializer.Serialize(stream, packet);
            return stream.GetBuffer();
        }
    }

    public static Packet GetPacket(byte[] buffer, int count) {
        XmlSerializer serializer = new XmlSerializer(typeof(Packet));
        using (MemoryStream stream = new MemoryStream()) {
            stream.Write(buffer, 0, count);
            stream.Position = 0;
            return serializer.Deserialize(stream) as Packet;
        }
    }
}

Handling the requests:

protected override void OnReceive(Socket client, byte[] receivedBytes, int count) {
    try {
        DXTalk.Packet packet = DXTalk.Protocol.GetPacket(receivedBytes, count);
        switch (packet.Operation) {
            case Operation.Login:
                UpdateUserInfo(client, packet);
                BroadcaseUserList();
                break;
            case Operation.RefreshUsers:
                BroadcaseUserList();
                break;
            case Operation.Message:
                ForwardMessage(packet);
                break;
        }
    } finally {
        PostReceive(client);
    }
}

Now we just start up the 2 listening sockets: one for our policy xml and another for handling the protocol requests and we are done with the server side. There is one restriction on the port usage however, it must in 4502-4534 range. We will use 4510 for our talk server.

 

Establishing the connection

The Socket API is a little bit different in Silverlight then in the full .NET framework. It only supports asynchronous calls and all the calls use a special SocketAsyncEventArgs class for completion events and state.

Connection to our chat server:

void Login(string userName) {
    if (_socket == null) {
        _socket = new Socket(AddressFamily.InterNetwork,
            SocketType.Stream, ProtocolType.Tcp);

        SocketAsyncEventArgs sendArgs = new SocketAsyncEventArgs();
        sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnConnected);
        sendArgs.RemoteEndPoint = new DnsEndPoint("localhost", 4510);

        _loginId = Guid.NewGuid();
        _loginName = userName;
        _socket.ConnectAsync(sendArgs);
    }
}

void OnConnected(object sender, SocketAsyncEventArgs e) {
    SocketError error = e.SocketError;
    if (error == SocketError.Success) {
        _socket.BeginReceive(new EventHandler<SocketAsyncEventArgs>(OnReceiveComplete));
        _socket.BeginSend(new Packet() {
            UserId = this._loginId,
            UserName = this._loginName,
            Operation = Operation.Login,
        }, null);
    }
}

BeginReceive and BeginSend?! Don' worry, those are just extension methods on the Socket class that call ReceiveAsync and SendAsync :)

public static class SocketExtentions {
    
    #if SILVERLIGHT

    public static void BeginSend(this Socket socket, 
        Packet packet,
        EventHandler<SocketAsyncEventArgs> completionEvent) {            
        SocketAsyncEventArgs sendArgs = new SocketAsyncEventArgs();
        sendArgs.Completed += completionEvent;
        byte[] buffer = Protocol.GetPacket(packet);
        sendArgs.SetBuffer(buffer, 0, buffer.Length);
        socket.SendAsync(sendArgs);
    }
    
    public static void BeginReceive(this Socket socket,
        EventHandler<SocketAsyncEventArgs> completionEvent) {
        SocketAsyncEventArgs sendArgs = new SocketAsyncEventArgs();
        byte[] buffer = new byte[1024];
        sendArgs.SetBuffer(buffer, 0, buffer.Length);
        sendArgs.Completed += completionEvent;
        socket.ReceiveAsync(sendArgs);
    }
    
    #endif

}

When we receive the reply from the server, OnReceiveComplete event is invoked:

void OnReceiveComplete(object sender, SocketAsyncEventArgs e) {
    SocketError error = e.SocketError;
    if (error == SocketError.Success) {
        try {
            Packet packet = Protocol.GetPacket(e.Buffer, e.BytesTransferred);
            if (packet.Operation == Operation.RefreshUsers) {
                this.Dispatcher.BeginInvoke(delegate() {
                    Users.DataSource = packet.Children;
                    Users.ExpandAll();
                });
            } else if (packet.Operation == Operation.Message) {
                this.Dispatcher.BeginInvoke(delegate() {
                    ShowChatWindow(packet.Children[0], packet.Payload);
                });
            }
        } catch {
        }
    }
    _socket.BeginReceive(new EventHandler<SocketAsyncEventArgs>(OnReceiveComplete));
}

Two things to note here. 1: "Users" is an AgDataGrid control:

<TalkControl:AgDataGrid Grid.Row="1" x:Name="Users"
                       ShowGroupPanel="Visible"
                       ColumnsAutoWidth="True"
                       AllowEditing="False">
    <TalkControl:AgDataGrid.Columns>
        <DevExpress:AgDataGridTextColumn HeaderContent="User Name" FieldName="UserName"/>
        <DevExpress:AgDataGridTextColumn HeaderContent="User ID" FieldName="UserId"/>
    </TalkControl:AgDataGrid.Columns>
</TalkControl:AgDataGrid>        

And 2, we must synchronize access to the main thread via the Dispatcher object.

 

Preparing the UI

Once the client is connected, a conversation could be started either by receiving a message or by double clicking the a record in the data grid. In both cases, the same ShowChatWindow function is called:

private void ShowChatWindow(User peer, string message) {
    ChatWindow window = ChatWindow.FindWindow(peer.UserId);
    if (window == null) {
        Popup popup = new Popup();
        window = new ChatWindow(popup,
            new User() { UserId = _loginId, UserName = _loginName },
            peer, _socket);
        window.Title.Text = peer.UserName;
        popup.VerticalOffset = 100;
        popup.HorizontalOffset = 100;
        popup.Child = window;
        popup.IsOpen = true;
    }
    if (!string.IsNullOrEmpty(message)) {
        window.AddLine(message, Colors.Red, peer);
    }
    window.parent.IsOpen = true;
    window.Focus();
}

There is no support for double-click in Silverlight so we will have to fake it using a special MouseHelper class that I copied from the AgDataGrid source code.

public delegate void DblClickEvent(object sender, MouseEventArgs e);

public class AgDataGrid : DevExpress.Windows.Controls.AgDataGrid {
    public void Refresh() {
        base.DataController.DoRefresh();
    }

    private static class MouseHelper {
        static int Timeout = 500;
        static bool clicked = false;
        static Point position;
        public static bool IsDoubleClick(MouseButtonEventArgs e) {
            if (clicked) {
                clicked = false;
                return position.Equals(e.GetPosition(null));
            }
            clicked = true;
            position = e.GetPosition(null);
            ParameterizedThreadStart threadStart = new ParameterizedThreadStart(ResetThread);
            Thread thread = new Thread(threadStart);
            thread.Start();
            return false;
        }
        private static void ResetThread(object state) {
            Thread.Sleep(Timeout);
            clicked = false;
        }
    }

    public event DblClickEvent DblClick;

    public override void OnApplyTemplate() {
        base.OnApplyTemplate();
        this.Surface.MouseLeftButtonUp += new MouseButtonEventHandler(Surface_MouseLeftButtonUp);
    }

    void Surface_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) {
        if (MouseHelper.IsDoubleClick(e)) {
            if (DblClick != null) {
                DblClick(sender, e);
            }
            e.Handled = true;
        }
    }
}

And there no build-in windows or dialogs in Silverlight 2. But there is a System.Windows.Controls.Primitives.Popup container and it's all we need to create a conversation window. A ChatWindow is a simple UserControl and in the ChatWindow.xaml I have one special element TitleBar.

<Grid Background="Black" Grid.Row="0" x:Name="TitleBar">
    <Border BorderBrush="White" BorderThickness="0.3"></Border>
    <TextBlock  IsHitTestVisible="False" x:Name="Title" Text="Title" VerticalAlignment="Center" HorizontalAlignment="Left"
               FontFamily="Tahoma" Foreground="White" FontSize="14" 
                FontWeight="Bold"
                Margin="8,0,0,0"></TextBlock>
    <Button Width="32" Margin="4" HorizontalAlignment="Right" Content="X"
        Click="CloseButtonClick"></Button>
</Grid>

It is used to display the NC (non-client) area of the window with the close button and it is also used to control the dragging of the window. We must have cool UI :). Implementing the window dragging is simple:

public ChatWindow()
    : base() {
    InitializeComponent();
    this.MouseLeftButtonDown += new MouseButtonEventHandler(ChatWindow_MouseLeftButtonDown);
    this.MouseLeftButtonUp += new MouseButtonEventHandler(ChatWindow_MouseLeftButtonUp);
    this.MouseMove += new MouseEventHandler(ChatWindow_MouseMove);
}

bool _hasNCHitTest = false;
Point _initalNCHitTest = new Point();

void ChatWindow_MouseMove(object sender, MouseEventArgs e) {
    if (e.Handled == true)
        return; 
    if (_hasNCHitTest && parent != null) {
        Point hitTest;
        hitTest = e.GetPosition(Title);

        Point delta = new Point(
            hitTest.X - _initalNCHitTest.X,
            hitTest.Y - _initalNCHitTest.Y);

        this.parent.HorizontalOffset += delta.X;
        this.parent.VerticalOffset += delta.Y;
        
        e.Handled = true;
    }
}

void ChatWindow_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) {
    if (e.Handled == true)
        return; 
    if (_hasNCHitTest) {
        _hasNCHitTest = false;
        TitleBar.ReleaseMouseCapture();
        e.Handled = true;
    }
}

void ChatWindow_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) {
    if (e.Handled == true)
        return; 
    if (e.Source == TitleBar && parent != null) {
        _hasNCHitTest = true;                
        _initalNCHitTest = e.GetPosition(TitleBar);
        TitleBar.CaptureMouse();
        e.Handled = true;
    }
}

For conversation area, I use the AgDataGrid as well with a custom preview template. Where the grid is called ChatBox and is bound to a list of Message objects.

<TalkConrol:AgDataGrid Grid.Row="1" x:Name="ChatBox" AllowEditing="False" ShowColumnHeaders="Collapsed" ShowHorizontalLines="False" ShowVerticalLines="False" PreviewVisibility="ForAllRows" ColumnsAutoWidth="True"> <TalkConrol:AgDataGrid.DataRowTemplate> <ControlTemplate TargetType="DevExpress:AgDataGridRow"> <Grid Name="RootElement"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions> <DevExpressInternal:AgLineStackPanel x:Name="CellsPresenterElement" Opacity="0" Height="1" /> <ContentControl x:Name="PreviewPresenterElement" Height="0"/> </Grid> </ControlTemplate> </TalkConrol:AgDataGrid.DataRowTemplate> <TalkConrol:AgDataGrid.PreviewTemplate> <DataTemplate> <StackPanel Orientation="Vertical" HorizontalAlignment="Left" Margin="6,0,0,0"> <TextBlock Text="{Binding Timestamp}" TextWrapping="NoWrap" Margin="2,0,0,0" Width="300" Foreground="Gray" FontFamily="lucida grande,tahoma,verdana,arial,sans-serif" FontSize="11"></TextBlock> <TextBlock Text="{Binding Line}" TextWrapping="Wrap" Margin="16,1,0,0" Width="300" Foreground="{Binding Color}" FontFamily="lucida grande,tahoma,verdana,arial,sans-serif" FontSize="11"></TextBlock> </StackPanel> </DataTemplate> </TalkConrol:AgDataGrid.PreviewTemplate> </TalkConrol:AgDataGrid>

public class Message {
    public string Timestamp { get; set; }
    public string Line { get; set; }
    public Brush Color { get; set; }
}

internal void AddLine(string message, Color color, User peer) {
    var list = this.ChatBox.DataSource as List<Message>;
    list.Add(new Message() {
        Line = string.Format("{0}: {1}", peer.UserName, message),
        Timestamp = DateTime.Now.ToString(), Color = new SolidColorBrush(color)
    });
    ChatBox.Refresh();
}

One last thing, sending the actual message and we are done:

private void Send_Click(object sender, RoutedEventArgs e) {
    if (!string.IsNullOrEmpty(MessageBox.Text)) {
        Packet packet = new Packet() {
            UserId = user.UserId,
            UserName = user.UserName,
            Operation = Operation.Message,
            Payload = MessageBox.Text,
            Children = new List<User>() { new User() { UserId = peer.UserId, UserName = peer.UserName } },
        };
        AddLine(MessageBox.Text, Colors.Black, this.user);
        socket.BeginSend(packet, null);
    }
}

 

image 

Download Source Code

 

Happy chatting :)

Azret

Published Jul 16 2008, 02:41 AM by Azret Botash (Developer Express)
Filed under: ,
Technorati tags: Silverlight, AgDataGrid

Comments

 

Dmitry_K said:

I couldn't find class SocketServer (which your policy server inherits from). Where did you find it ?

July 17, 2008 7:53 AM
 

Azret Botash (Developer Express) said:

Hello Dmitry,

It is included in the source code.

Azret

July 17, 2008 11:44 AM
 

Alex Hoffman said:

Azret,  what a great post!  I'm just getting into SilverLight and found it far more interesting than the typical (yawn) photo viewer examples one finds everywhere.  Nice grid!  -- Alex

July 21, 2008 5:02 AM
 

Pete Magsig said:

Azret,

Thanks so much for the tip on the double-click. It seems to me that this would be a useful addition to the base functionality of the AgDataGrid. Are there any plans to add this to the core distribution?

August 26, 2008 9:17 AM
 

Suman Kondreddi said:

Azret,

Thank you for this excellent post.

-Suman

September 18, 2008 5:01 PM
 

Recent Faves Tagged With "xmlserializer" : MyNetFaves said:

Pingback from  Recent Faves Tagged With "xmlserializer" : MyNetFaves

December 9, 2008 7:51 AM
 

Suman Modi said:

Nice post man..

i was searching this thing for so so many days...

keep posting

January 15, 2009 12:39 AM

Leave a Comment

(required)  
(optional)
(required)  
Verification code: Required
   
Add
Copyright © 1998-2009 Developer Express Inc.
ALL RIGHTS RESERVED