iNetwork Tutorial #3: Drawing Board 1.0

back to iNetwork

Readings

Prior to beginning this tutorial example, the following links lead to discussions and detailed descriptions of different aspects of the iNetwork Toolkit. It is suggested that you red through these before setting out to attempt the provided tutorials.

Introduction

Summary

The Drawing Board 1.0 tutorial example simulates a communal drawing board. In this environment a number of different users are able to draw simultaneously on the same canvas. They also see what the other user(s) are drawing in real time. In simpler terms, when one user is drawing the other users are able to see what they are doing as they are doing it.

Features

  • Multi-color palette
  • Real time sharing of the canvas with all users
  • When a new client connects in the middle of an ongoing session they will be able to see what has previously been drawn.
    • The way this is done actually allows the user to see a sort of replay of the previously drawn paths.

High Level Overview

Each client that connects to the server provides the associated user with a drawing environment that contains a color palette, brush size slider, and a canvas. In general when a user goes to draw there are two events that need to be listened to: MouseDown and MouseMove.

The first event is the MouseDown. Basically, when a user clicks on the left mouse button a message is sent to the server. This message contains the brush color, the point coordinate of the mouse pointer and the brush size. The server will then relay this message back to all clients. From this, the path object is then created.

On the MouseMove event, the idea is to add segments to the path that was created during the MouseDown event. A new message is then sent to the server , on the condition that the mouse button is still being pressed. This message only contains point coordinates that are to be used for the segments of the path.

Reminder: The executable file, called DrawClient, for this example has been provided in a folder named Tutotrial3.1 when the toolkit was installed. You will find this in the Tutorial Examples folder within the GroupLab iNetwork Toolkit directory. Be sure to extract the files before opening them.

back to top

Setting-Up the Form

  1. Begin with a . Name the client project DrawClient and the server project DrawServer.
  2. Once you have completed the initial set-up above, from the Solution Explorer open the XAML file labelled MainWindow.xaml for the DrawClient.
  3. In the designer view (XAML) add Ellipses to stand in as the color palette as shown below.
    • In the Properties tab change the name of each of the ellipses, from left to right, to the following:
      • _blackPalette
      • _yellowPalette
      • _brownPalette
      • _redPalette
      • _violetPalette
      • _bluePalette
      • _greenPalette
  4. Now add a Canvas as shown.
    • In the Properties tab, change the canvas' name to _drawingCanvas.
  5. Add a Slider and Label as shown.
    • In a similar manner as the above, change the slider's name to _brushSlider.
  6. Add a clear Button.
    • Again, in the Properties tab, change the button's name to _clearButton.
back to top

Adding Code

This part of the tutorial will build on the "skeleton" code in the client and server templates.

Client

Open the C# code file for the client.

Class Instance Variables Region
  1. Begin by declaring the following instance variables:
    1. private int _pathID;
    2. private Dictionary<int, Path> _paths;
    3. private PathSegmentCollection _pathSegments;
    4. private SolidColorBrush _selectedColor = Brushes.Black;
    5. private double _brushThickness = 1.0;
    • _pathID is an integer number that is assigned as the ID for each path drawn.
    • _paths is a dictionary that is responsible for keeping track of all paths. The keys are the path ID's and the values are the paths themselves.
    • _pathSegments is a path segment collection that is used for building the paths later on.
    • _selectedColor is the variable that tracks the user's color choice from the given color palette.
    • _brushThickness is a double value, who's default is 1.0, taken from the UI's slider and acts as the stroke thickness for the paths drawn.
Initialization Region
  1. Be sure to change Name-of-Service, in the InitializeConnection method to, Draw.
    1. Connection.Discover("Draw", new SingleConnectionDiscoveryEventHandler(OnConnectionDiscovered));
  2. In the same method, instantiate the paths dictionary.
    1. this._paths = new Dictionary<string, Path>();
Main Body Region

Note: This region is called Drawing within the executable files provided during installation.

  1. Next, in the OnMessageReceived event handler method create the following message cases in addition to the default within switch (msg.Name).
    1. case "AllowToDraw":
    2.     ...
    3.     break;
    4. case "StartPath":
    5.     ...
    6.     break;
    7. case "ContinuePath":
    8.     ...
    9.     break;
    10. case "RedrawPath":
    11.     ...
    12.     break;
    13. case "Clear":
    14.     ...
    15.     break;
  2. If the message is the AllowToDraw message, a number of event listeners are added. This will allow users to start drawing.
    1. this._clearButton.Click += new RoutedEventHandler(OnClearButtonClick);
    2.  
    3. this._drawingCanvas.MouseDown += new MouseButtonEventHandler(OnMouseDown);
    4. this._drawingCanvas.MouseMove += new MouseEventHandler(OnMouseMove);
    5.  
    6. this._brushSlider.ValueChanged += new RoutedPropertyChangedEventHandler<double>(OnThicknessChanged);
    7.  
    8. this._blackPalette.MouseLeftButtonUp += new MouseButtonEventHandler(OnBlackSelect);
    9. this._yellowPalette.MouseLeftButtonUp += new MouseButtonEventHandler(OnYellowSelect);
    10. this._brownPalette.MouseLeftButtonUp += new MouseButtonEventHandler(OnBrownSelect);
    11. this._redPalette.MouseLeftButtonUp += new MouseButtonEventHandler(OnRedSelect);
    12. this._violetPalette.MouseLeftButtonUp += new MouseButtonEventHandler(OnVioletSelect);
    13. this._bluePalette.MouseLeftButtonUp += new MouseButtonEventHandler(OnBlueSelect);
    14. this._greenPalette.MouseLeftButtonUp += new MouseButtonEventHandler(OnGreenSelect);
  3. If the message is the StartParth message five fields are retrieved.
    1. string startID = msg.GetStringField("id");
    2. string colorcode = msg.GetStringField("fill");
    3. double thickness = msg.GetDoubleField("thickness");
    4. double x = msg.GetDoubleField("x");
    5. double y = msg.GetDoubleField("y");
    • startID distinguishes one path from another
    • colorcode is the stroke color of the path
    • thickness is the stroke thickness of the path
    • x and y are the coordinates of the starting point of the path
  4. Since the stroke color of the path is contained in the message as a string it needs to be converted into a Color object.
    1. Color fill = (Color)ColorConverter.ConvertFromString(colorcode);
  5. Call the CreateNewPath method, which, using the above fields, creates the path.
    1. CreaNewPath(startID, fill, thickness, x,y);
  6. If the message is the ContinuePath message it retrieves three fields.
    1. string currentID = msg.GetStringField("id");
    2. double nextX = msg.GetDoubleField("x");
    3. double nextY = msg.GetDoubleField("y");
    • currentID, ensures that path segments are being added to the correct path previously created
    • x and y are the coordinates of the endpoint of the segment
  7. Then the BuildOnPath method is called.
    1. BuildOnPath(currentID, nextX, nextY);
  8. If the message is the RedrawPath message the DrawCanvas method is called.
    1. DrawCanvas();
  9. The CreateNewPath method uses the GUI thread to create new paths. Create the method as shown:
    1. private void CreateNewPath(string id, Color fill, double thickness, double x, double y)
    2. {
    3.     this.Dispatcher.Invoke(
    4.         new Action(
    5.             delegate()
    6.             {...}
    7.     ));
    8. }
  10. Within delegate() [line 6, step 12], create a new SolidColorBrush object and assign its color to be fill.
    1. SolidColorBrush brushColor = new SolidColorBrush();
    2. brushColor.Color = fill;
  11. Make a new point using the 'x' and 'y' coordinates.
    1. Point position = new Point(x, y);
  12. Now, it's time to create a new path. Included in this task is to assign the stroke color, stroke thickness and the stroke caps.
    1. Path path = new Path();
    2. path.Stroke = brushColor;
    3. path.StrokeThickness = thickness;
    4. path.StrokeStartLineCap = PenLineCap.Round;
    5. path.StrokeEndLineCap = PenLineCap.Round;
  13. In a path there is something called path geometry and within that are collections of path figures. Create new PathGeometry and PathFigure objects then instantiate theFigures in the path geometry to be a new PathFirgureCollection.
    1. PathGeometry pathGeometry = new PathGeometry();
    2. PathFigure pathFigure = new PathFigure();
    3.  
    4. pathGeometry.Figures = new PathFigureCollection();
  14. Assign the point that was created earlier to be the start point of the path figure then add that figure to the path figures collection.
    1. pathFigure.StartPoint = position;
    2. pathGeometry.Figures.Add(pathFigure);
  15. Assign the path figure segments to _pathSegments, it is to be used later on when building on the path.
    1. this._pathSegments = pathFigure.Segments;
  16. Lastly, the path geometry needs to be assigned as the path data then the path itself needs to be added to the canvas as well as to the dictionary, along with its ID.
    1. path.Data = pathGeometry;
    2. this._paths.Add(id, path);
    3. this._drawingCanvas.Children.Add(path);
  17. Now, the BuildOnPath method uses the GUI thread to add segments to a path.
    1. private void BuildOnPath(string id, double x, double y)
    2. {
    3.     this.Dispatcher.Invoke(
    4.         new Action(
    5.             delegate()
    6.             {...}
    7.     ));
  18. In order to add more segments to an already existing path the path ID is used to retrieve the corresponding path object from the dictionary to ensure that the segments are being added to the right path. This avoids drawing issues when there are more than one users drawing at a time. To start off, assign the data of the correct path to a PathGeometry object and make sure that a point is created using the 'x' and 'y' coordinates.
    1. PathGeometry geometry = (PathGeometry)this._paths[id].Data;
    2. Point position = new Point(x, y);
  19. Add if and else statements in delegate() [line 6, step 20] that are to be used to check whether or not the path segments actually contain at least one segment.
    1. if (this._pathSegments.Count > 0)
    2. {...}
    3. else
    4. {...}
  20. In the case that the path segments collection really does contain at least one segment, within the if statement [line 4, step 22], assign _pathSegments to be the segment collection of the given path.
    1. this._pathSegments = geometry.Figures[0].Segments;
  21. From the segments collection, retrieve the last or most recent QuadraticBezierSegment object and its end point.
    1. QuadraticBezierSegment lastLine = this._pathSegments[this._pathSegments.Count - 1] as QuadraticBezierSegment;
    2. Point lastEndPoint = lastLine.Point2;
  22. Using the end point from the previous segment and the current coordinate position, create a new QuadraticBezierSegment object.
    1. QuadraticBezierSegment seg = new QuadraticBezierSegment(lastEndPoint, position, true);
  23. Add the next line of code to ensure that the point at which the segments join together are smooth. Once that is done the segment can be added to the collection.
    1. seg.IsSmoothJoin = true;
    2. geometry.Figures[0].Segments.Add(seg);
  24. In the case that the segments collection was emtpy, within the else statement [line 6, step 22] create a new QuadraticBezierSegment using the path's start point and the current coordinate then add it to the collection.
    1. QuadraticBezierSegment seg = new QuadraticBezierSegment(geometry.Figures[0].StartPoint, position, true);
    2. geometry.Figures[0].Segments.Add(seg);
  25. The other method is the DrawCanvas method which, in a nutshell, recreates the drawing board for a client the connects in the middle of an ongoing session. Here, each path stored in the dictionary is added to their canvas. Create this method as shown below:
    1. private void DrawCanvas()
    2. {
    3.     foreach (var pair in this._paths)
    4.     {
    5.         this._drawingCanvas.Children.Add(pair.Value);
    6.     }
    7. }
  26. In order for all of the above to occur the client makes use of the event handler methods to respond and send the appropriate messages to the server. On a MouseDown event the OnMouseDown event handler method is called.
    1. private void OnMouseDown(object sender, MouseButtonEventArgs e)
    2. {...}
  27. A unique ID, using the Guid (globally unique identifier) object, is generated for the path that is going to be created within this method [line 2, step 29].
    1. Guid id = Guid.NewGuid();
    2. this._pathID = id.ToString();
  28. Create a new message named StartPath that contains the path ID, stroke color, stroke thickness and the 'x' and 'y' coordinates of the mouse pointer when the mouse button was down.
    1. Message msg = new Message("StartPath");
    2. msg.AddField("id", this._pathID);
    3. msg.AddField("fill", this._selectedColor.ToString());
    4. msg.AddField("thickness", this._brushThickness);
    5. msg.AddField("x", e.GetPosition(this._drawingCanvas).X);
    6. msg.AddField("y", e.GetPosition(this._drawingCanvas).Y);
  29. Be sure to send the message to the server.
    1. this._connection.SendMessage(msg);
  30. On a MouseMove event the OnMouseMove' event handler method is called. However, the only time anything happens when the mouse is moved is if the mouse button is being pressed at the same time.
    1. private void OnMouseMove(object sender, MouseEventArgs e)
    2. {
    3.     if (e.LeftButton == MouseButtonState.Pressed)
    4.     {...}
    5. }
  31. ]At this point create a new message named ContinuePath that contains the path ID and the 'x' and 'y' coordinates within the if statement of that method [line 4, step 33''].
    1. Message msg = new Message("ContinuePath");
    2. msg.AddField("id", this._pathID);
    3. msg.AddField("x", e.GetPosition(this._drawingCanvas).X);
    4. msg.AddField("y", e.GetPosition(this._drawingCanvas).Y);
  32. Be sure to send the message to the server.
    1. this._connection.SendMessage(msg);
  33. Now when the _clearButton is clicked the OnClearButton event handler method is called. Create a new message called Clear and send it to the server.
    1. private void OnClearButtonClick(object sender, RoutedEventArgs e)
    2. {
    3.     Message msg = new Message("Clear");
    4.     this._connection.SendMessage(msg);
    5. }
  34. Lastly, event handlers need to be made for each of the color palette selections. On a MouseLeftButtonUp event a corresponding event handler method for each of the colors is called. Create an event handler method for the seven color choices by using the code below. Be sure to change *color* into the color name and to change _colorName to the name of the corresponding ellipse (see the list below).
    1. private void On*color*Select(object sender, MouseButtonEventArgs e)
    2. {
    3.     this._selectedColor = (SolidColorBrush)this._colorName.Fill;
    4. }
    • Black, _blackPalette
    • Yellow, _yellowPalette
    • Brown, _brownPalette
    • Red, _redPalette
    • Violet, _violetPalette
    • Blue, _bluekPalette
    • Green, _greenPalette
back to top

Server

Open up the C# code file for the server.

Class Instance Variables Region
  1. Begin with creating a list of messages, which will be responsible for remembering all the messages that have to do with drawing.
    private List<Message> _drawnPaths;
Initialization Region
  1. In the InitializeServer method change Name-of-Service to Draw. You are also free to change the port as long as the port number is larger than 1024 and does not exceed 65535
    this._server = new Server("Draw", 12348);
  2. Add the lines of code below to the OnServerConnection event handler method after the line: e.Connection.MessageReceived += new ConnectionMessageEventHandler(OnConnectionMessage);.
    1. RedrawPath(e.Connection);
    2. Message msg = new Message("AllowToDraw");
    3. this._server.BroadcastMessage(msg);
    • The RedrawPath method is called to ensure that the client gets the most up to date drawings showing on its UI.
    • AllowToDraw is a message sent to the client to signal that the client is now allowed to start drawing on the canvas. This avoids any confusion with building paths in the case that a user decides to draw while the canvas is being recreated.
Main Body Region

Note: This region is called Drawing within the executable files provided during installation.

  1. Create the different cases as shown below. Do this in the OnConnectionMessage.
    1. case "StartPath":
    2.     ...
    3.     break;
    4. case "ContinuePath":
    5.     ...
    6.     break;
    7. case "Clear":
    8.     ...
    9.     break;
  2. In the case that the message is the StartPath message or the ContinuePath message simply broadcast the message to all clients and add it to the list of messages.
    1. this._server.BroadcastMessage(msg);
    2. this._drawnPaths.Add(msg);
  3. In the case that the message is the Clear message broadcast the message to all client and remove all messages from the list.
    1. this._server.BroadcastMessage(msg);
    2. this._drawnPaths.Clear();
  4. Now in the RedrawPath method the only task that needs to be executed is to broadcast each message that was saved in the messages list. From there on the client will take care of recreating the canvas. Create the method as shown below.
    1. private void RedrawPath(Connection newClient)
    2. {
    3.     foreach (Message msg in this._drawnPaths)
    4.     {
    5.         this._server.BroadcastMessage(msg, this._clients.Where(client => client != newClient).ToList());
    6.     }
    7.  
    8. }
back to top

Result

Reminder: Right click on DrawServer in Solution Explorer and select Set as StartUp Project. Now run the server then right click on DrawClient in the Solution Explorer and select Debug then Start a new instance to run/add a new instance of the client.

You should get something like the following:

back to top