0

I'm working on making a screenshot program in WPF. What I don't understand is how I should mouse events. All of my mouse events are handled in the code-behind, which works great, but I want to convert my project to MVVM-style. Here is all of my ScreenshotTab code, which is the usercontrol which hosts the screenshot (it's actually designed to be placed into a tabcontrol tabitem):

public partial class ScreenshotTab : UserControl
{
 public ScreenshotTab()
 {
 InitializeComponent();
 }
 public ScreenshotTab(BitmapImage screenshot, Window window)
 {
 InitializeComponent();
 imgControl.Source = screenshot;
 imgControl.MaxWidth = screenshot.Width;
 imgControl.MaxHeight = screenshot.Height;
 canvas.Height = screenshot.Height;
 canvas.MinHeight = screenshot.Height;
 canvas.MaxHeight = screenshot.Height;
 canvas.Width = screenshot.Width;
 canvas.MinWidth = screenshot.Width;
 canvas.MaxWidth = screenshot.Width;
 mainWindow = window as MainWindow;
 }
 public bool isSaved
 {
 get;
 set;
 }
 public BitmapImage Screenshot
 {
 get
 {
 if (imgControl.Source is BitmapImage)
 {
 return imgControl.Source as BitmapImage;
 }
 else
 {
 return null;
 }
 }
 }
 // Listed in the order they are needed in the logic flow inside the event handlers.
 private MainWindow mainWindow;
 private Ellipse circle;
 private Polyline polyLine;
 private Point startPoint;
 private Point endPoint;
 private Line line;
 private Rectangle rectangle;
 private Point topLeft;
 // I probably don't need a bool.
 private bool isDrawing;
 private void imgControl_MouseDown(object sender, MouseButtonEventArgs e)
 {
 // MessageBox.Show("Mouse down event is working...");
 // MessageBox.Show("mainwindow.toolselected = " + mainWindow.toolSelected.ToString());
 // Sets the starting point of the line, which doesn't change
 // as the user moves their mouse.
 // Turns on a flag that represents that we are drawing when on.
 isDrawing = true;
 switch (mainWindow.toolSelected)
 { 
 case DrawingTool.FreeDraw:
 polyLine = new Polyline();
 SetStrokeProperties(polyLine);
 polyLine.Points.Add(e.GetPosition(canvas));
 canvas.Children.Add(polyLine);
 break;
 case DrawingTool.Line:
 line = new Line();
 SetStrokeProperties(line);
 line.X1 = e.GetPosition(canvas).X;
 line.Y1 = e.GetPosition(canvas).Y;
 line.X2 = e.GetPosition(canvas).X;
 line.Y2 = e.GetPosition(canvas).Y;
 canvas.Children.Add(line);
 break;
 case DrawingTool.Rectangle:
 rectangle = new Rectangle();
 SetStrokeProperties(rectangle);
 startPoint = e.GetPosition(this);
 endPoint = e.GetPosition(this);
 Canvas.SetLeft(rectangle, startPoint.X);
 Canvas.SetTop(rectangle, startPoint.X);
 canvas.Children.Add(rectangle);
 break;
 case DrawingTool.Circle:
 circle = new Ellipse();
 SetStrokeProperties(circle);
 startPoint = e.GetPosition(this);
 endPoint = e.GetPosition(this);
 Canvas.SetLeft(circle, startPoint.X);
 Canvas.SetTop(circle, startPoint.X);
 canvas.Children.Add(circle);
 break;
 case DrawingTool.Eraser:
 break;
 default:
 break;
 }
 }
 private void SetStrokeProperties(Shape shape)
 {
 shape.Stroke = new SolidColorBrush(mainWindow.color);
 shape.StrokeThickness = 3;
 }
 // This is our eraser event.
 // It simply removes the element from the object when the tool selected
 // is the eraser tool and we have the mouse down.
 private void OnMouseOver(object sender, MouseEventArgs e)
 {
 // MessageBox.Show("Yay, mouse over event called...");
 if (isDrawing && (mainWindow.toolSelected == DrawingTool.Eraser))
 {
 canvas.Children.Remove(sender as UIElement);
 }
 }
 private void imgControl_MouseMove(object sender, MouseEventArgs e)
 {
 if (isDrawing)
 {
 switch (mainWindow.toolSelected)
 {
 case DrawingTool.FreeDraw:
 polyLine.Points.Add(e.GetPosition(canvas));
 break;
 case DrawingTool.Line:
 line.X2 = e.GetPosition(canvas).X;
 line.Y2 = e.GetPosition(canvas).Y;
 break;
 case DrawingTool.Rectangle:
 endPoint = e.GetPosition(this);
 rectangle.Width = Math.Abs(endPoint.X - startPoint.X);
 rectangle.Height = Math.Abs(endPoint.Y - startPoint.Y);
 // down and to the right.
 if ((endPoint.X >= startPoint.X) && (endPoint.Y >= startPoint.Y))
 {
 // In this case, the start point is the top left point of the rectangle.
 topLeft.X = startPoint.X;
 topLeft.Y = startPoint.Y;
 Canvas.SetLeft(rectangle, topLeft.X);
 Canvas.SetTop(rectangle, topLeft.Y);
 }
 // up and to the right.
 else if ((endPoint.X >= startPoint.X) && (endPoint.Y <= startPoint.Y))
 {
 topLeft.X = endPoint.X - rectangle.Width;
 topLeft.Y = endPoint.Y;
 Canvas.SetLeft(rectangle, topLeft.X);
 Canvas.SetTop(rectangle, topLeft.Y);
 }
 // up and to the left...
 else if ((endPoint.X <= startPoint.X) && (endPoint.Y <= startPoint.Y))
 {
 topLeft.X = endPoint.X;
 topLeft.Y = endPoint.Y;
 Canvas.SetLeft(rectangle, topLeft.X);
 Canvas.SetTop(rectangle, topLeft.Y);
 }
 // down to the left...
 else if ((endPoint.X <= startPoint.X) && (endPoint.Y >= startPoint.Y))
 {
 topLeft.X = startPoint.X - rectangle.Width;
 topLeft.Y = endPoint.Y - rectangle.Height;
 Canvas.SetLeft(rectangle, topLeft.X);
 Canvas.SetTop(rectangle, topLeft.Y);
 }
 break;
 case DrawingTool.Circle:
 endPoint = e.GetPosition(this);
 circle.Width = Math.Abs(endPoint.X - startPoint.X);
 circle.Height = Math.Abs(endPoint.Y - startPoint.Y);
 // down and to the right.
 if ((endPoint.X >= startPoint.X) && (endPoint.Y >= startPoint.Y))
 {
 // In this case, the start point is the top left point of the rectangle.
 topLeft.X = startPoint.X;
 topLeft.Y = startPoint.Y;
 Canvas.SetLeft(circle, topLeft.X);
 Canvas.SetTop(circle, topLeft.Y);
 }
 // up and to the right.
 else if ((endPoint.X >= startPoint.X) && (endPoint.Y <= startPoint.Y))
 {
 topLeft.X = endPoint.X - circle.Width;
 topLeft.Y = endPoint.Y;
 Canvas.SetLeft(circle, topLeft.X);
 Canvas.SetTop(circle, topLeft.Y);
 }
 // up and to the left...
 else if ((endPoint.X <= startPoint.X) && (endPoint.Y <= startPoint.Y))
 {
 topLeft.X = endPoint.X;
 topLeft.Y = endPoint.Y;
 Canvas.SetLeft(circle, topLeft.X);
 Canvas.SetTop(circle, topLeft.Y);
 }
 // down to the left...
 else if ((endPoint.X <= startPoint.X) && (endPoint.Y >= startPoint.Y))
 {
 topLeft.X = startPoint.X - circle.Width;
 topLeft.Y = endPoint.Y - circle.Height;
 Canvas.SetLeft(circle, topLeft.X);
 Canvas.SetTop(circle, topLeft.Y);
 }
 break;
 default:
 break;
 }
 }
 }
 private void imgControl_MouseUp(object sender, MouseButtonEventArgs e)
 {
 isSaved = false;
 switch (mainWindow.toolSelected)
 {
 case (DrawingTool.FreeDraw):
 polyLine.MouseEnter += OnMouseOver;
 break;
 case (DrawingTool.Line):
 line.MouseEnter += OnMouseOver;
 break;
 case (DrawingTool.Rectangle):
 rectangle.MouseEnter += OnMouseOver;
 break;
 case (DrawingTool.Circle):
 circle.MouseEnter += OnMouseOver;
 break;
 }
 isDrawing = false;
 // MessageBox.Show("Mouse up event fired...");
 }
}

Obviously, that is quite a mess of code to handle, especially if I decide to add more shapes to draw in the future. So how would I go about setting up this up in MVVM if I want the user to be able to draw different kinds of shapes? Thanks for taking the time to look over this.

asked Jul 26, 2018 at 23:05
1
  • I'm going out on a limb to say that your current approach is fine. MVVM is a declarative approach. Drawing lines, circles, etc. is inherently imperative. Thus I believe the code behind is really where this code should go. Commented Jul 27, 2018 at 3:54

1 Answer 1

1

I handle this with an interface to a class that my view model can interact with:

public interface IMouseArgs
{
 Point Pos { get; }
 void Capture();
 void Release();
 bool Handled { get; set; }
 ... lots of other stuff
}

So basically whenever a mouse event or drag-n-drop operation occurs my view layer creates an object that implements this interface and passes it to the view model. If the view model decides to Capture or Release then mouse (e.g. a drag operation) then there are functions for it to request that. Similarly, it can set the Handled flag to indicate to the view that it is handling the mouse now (so that the view doesn't bubble the message up to parent controls in the view hierarchy.

As for the view, I use an interaction trigger to convert from the mouse events to the view model command handlers:

<SomeControl>
 <i:Interaction.Triggers>
 <i:EventTrigger EventName="MouseDown">
 <cmd:EventToCommand Command="{Binding MouseDownCommand}"
 PassEventArgsToCommand="True"
 EventArgsConverter="{StaticResource ClickConverter}"
 EventArgsConverterParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:SomeParentWindow}}"/>
 </i:EventTrigger>

Again, this is a simplified example, but shows the general idea. You could just do only the cmd:EventToCommand and PassEventArgsToCommand="True", but then your view model will receive the Windows data objects associated with the event (which isn't good SOC and means your view model is now forced to reference the Windows libraries). Specifying an EventArgsConverter, with optional EventArgsConverterParameter, means you can write a converter in the view to accept the event arg and convert it into something that implements IMouseArgs.

Sounds a bit complex, but it allows for very complex mouse interactions (drags, resizes, drag-n-drop etc) and is fully unit-testable, all while maintaining complete separation between your view model and view layers.

answered Jul 27, 2018 at 4:47
Sign up to request clarification or add additional context in comments.

3 Comments

Wow...maybe MVVM isn't appropriate for this then...I guess I should wait, I don't know anything about any of those tags in XAML.
MVVM is perfectly suited to this type of thing, it's just a bit more verbose than you'd expect. This article on the Microsoft site gives a good explanation of EventToCommand and may help clarify things a bit.
Okay -- I'll definitely take a look at that! I now moved a lot of the logic of drawing into a class library, which definitely helped because now I can separate things easier...I'll mark your answer as correct, thanks!

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.