1

I am creating a Signal R app that has a drawing section on the UI, right now I am working on the WPF client but there will eventually be a MVC and Android client as well.

The drawing events are handled perfectly, but with my method for handling erasing the other clients end up erasing entire strokes rather than only the points.

The client doing the erasing is using the EraseByPoint method and everything works fine on his side.

The drawing update details are passed to the clients via the DrawingDto:

public class DrawingDto
{
    public DrawingDto()
    {
        NewStrokes = new List<StrokeDto>();
        DeletedStrokes = new List<StrokeDto>();
    }
    public IList<StrokeDto> NewStrokes { get; set; }
    public IList<StrokeDto> DeletedStrokes { get; set; }
}

public class StrokeDto
{
    public StrokeDto()
    {
        Points = new List<Point>();
    }
    public IList<Point> Points { get; set; }
}

And the code the handles adding new strokes and trying to remove erased ones:

private void OnUpdateDrawing(DrawingDto drawing)
    {
        Execute.OnUiThread(() =>
        {
            (Strokes as INotifyCollectionChanged).CollectionChanged -= StrokesOnCollectionChanged;
            var strokeCollection = new StrokeCollection();
            foreach (var newStroke in drawing.NewStrokes)
            {
                var pointCollection = new StylusPointCollection();
                foreach (var point in newStroke.Points)
                {
                    pointCollection.Add(new StylusPoint(point.X, point.Y));
                }
                strokeCollection.Add(new Stroke(pointCollection));
            }
            Strokes.Add(strokeCollection);

            foreach (var deletedStroke in drawing.DeletedStrokes)
            {
                Strokes.Erase(deletedStroke.Points.Select(x=> new System.Windows.Point(x.X, x.Y)), new RectangleStylusShape(1,1));
            }
            (Strokes as INotifyCollectionChanged).CollectionChanged += StrokesOnCollectionChanged;
        });
    }

In this code Execute.OnUiThread uses the dispatcher to invoke the action, the event listener is being removed to avoid sending recursive updates to the server.

And Finally the code the creates the Dto in the first place:

public async void StrokesOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
    {
        var dto = new DrawingDto();
        if (args.NewItems !=null)
        {
            foreach (Stroke newStroke in args.NewItems)
            {
                dto.NewStrokes.Add(new StrokeDto { Points = newStroke.StylusPoints.Select(x => new Point((int)x.X, (int)x.Y)).ToList() });
            }
        }

        if (args.OldItems!= null)
        {
            foreach (Stroke deletedStroke in args.OldItems)
            {
                dto.DeletedStrokes.Add(new StrokeDto { Points = deletedStroke.StylusPoints.Select(x => new Point((int)x.X, (int)x.Y)).ToList() });
            }
        }


        await _signalRManager.UpdateDrawing(dto);
    }

Edit: As requested here is the signal r code.

The Signal R Hub:

public class DrawingHub : Hub
{
    public void UpdateDrawing(DrawingDto drawing)
    {
        Clients.Others.UpdateDrawing(drawing);
    }
}

Client side code to send updates to Hub:

public async void StrokesOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
    {
        var dto = new DrawingDto();
        if (args.NewItems !=null)
        {
            foreach (Stroke newStroke in args.NewItems)
            {
                dto.NewStrokes.Add(new StrokeDto { Points = newStroke.StylusPoints.Select(x => new Point((int)x.X, (int)x.Y)).ToList() });
            }
        }

        if (args.OldItems!= null)
        {
            foreach (Stroke deletedStroke in args.OldItems)
            {
                dto.DeletedStrokes.Add(new StrokeDto { Points = deletedStroke.StylusPoints.Select(x => new Point((int)x.X, (int)x.Y)).ToList() });
            }
        }


        await _signalRManager.UpdateDrawing(dto);
    }

Snippet from SignalRManager (this is a singleton class that contains all the proxies):

public async Task UpdateDrawing(DrawingDto dto)
    {
        await DrawingProxy.Invoke("UpdateDrawing", dto);
    }

public event Action<DrawingDto> UpdateDrawingEvent; 


private void OnUpdateDrawing(DrawingDto drawing)
    {
        if(UpdateDrawingEvent != null) UpdateDrawingEvent.Invoke(drawing);
    } 
Zword
  • 6,605
  • 3
  • 27
  • 52

2 Answers2

1

Ok the problem here was a misunderstanding with how the InkCanvas handles erasing.

Within the StrokesOnCollectionChanged handler the OldItems collection will contain the entirety of any stroke that was touched by the eraser, and NewItems will contain new strokes that are split where the eraser hit the original.

Because of this in the OnUpdateDrawing code by handling the new items first and then erasing the old items I was effectively erasing the new strokes that were just added.

Now by handling the deleted strokes first everything works as expected, the code snippet:

private void OnUpdateDrawing(DrawingDto drawing)
    {
        Execute.OnUiThread(() =>
        {
            (Strokes as INotifyCollectionChanged).CollectionChanged -= StrokesOnCollectionChanged;
            var strokeCollection = new StrokeCollection();

            foreach (var deletedStroke in drawing.DeletedStrokes)
            {
                Strokes.Erase(deletedStroke.Points.Select(x => new System.Windows.Point(x.X, x.Y)), new RectangleStylusShape(0.01, 0.01));
            }

            foreach (var newStroke in drawing.NewStrokes)
            {
                var pointCollection = new StylusPointCollection();
                foreach (var point in newStroke.Points)
                {
                    pointCollection.Add(new StylusPoint(point.X, point.Y));
                }
                strokeCollection.Add(new Stroke(pointCollection));
            }
            Strokes.Add(strokeCollection);

            (Strokes as INotifyCollectionChanged).CollectionChanged += StrokesOnCollectionChanged;
        });
    }
0

I don't get it, why don't you simply do :

 foreach (var deletedStroke in drawing.DeletedStrokes)
    Strokes.Remove(deletedStroke);

?

franssu
  • 2,422
  • 1
  • 20
  • 29
  • For two reasons, 1. if you look at the first code block in the question you will see that the deleted stroke is a custom object and only holds collection of x and y coordinates. 2 Even if it was the same object the user that is erasing is Erasing By Point, so if he were to erase one point on every stroke the other users would see the entire drawing erased, which is essentially exactly what is currently happening. And a third I just thought of, the server side of this needs to be agnostic from the Ink system as it needs to also support an asp page and an android app. –  Feb 06 '14 at 16:24
  • Still not sure I understand everything - but did you notice I mentionned .Remove and not .Erase ? – franssu Feb 06 '14 at 18:27
  • I did, it would have the same effect, it would remove the entire matching stroke, rather than only the points affected. –  Feb 06 '14 at 19:08
  • But... do we agree you have only wpf clients ATM ? So whatever happens to the InkCanvas.Strokes collection of one client, you have to mirror it in your OnUpdateDrawing callback of the other client, right ? If the sending client said the stroke was deleted, you have to trust it and delete it on the receiving client. You will notice that, yes, that stroke is deleted, but at the same time other strokes are added, the visual result being your initial stroke minus the deleted points... – franssu Feb 06 '14 at 21:18
  • Oh ok, So if I understand correctly you are saying that the delete will remove the previously existing strokes, and add new strokes as a result of the delete action? –  Feb 06 '14 at 21:57
  • Yes that is what I am saying :) – franssu Feb 06 '14 at 22:01
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/46969/discussion-between-phaeze-and-franssu) –  Feb 06 '14 at 22:18
  • If my goal was to only support WPF clients this answer would work, however because I need to support sending erase changes from MVC to WPF and eventually android I cannot use it. –  Feb 07 '14 at 00:06