How to properly filter the different actions that may result from a mouse event?

96 views Asked by At

I am designing a CAD application and am unsure how to design the event handling for mouse events in a clean way.

For simplicity, let's say I have two buttons in the UI: CreateSquare, and CreateCircle. When the user clicks one of these, I want the application to wait for a click in the canvas, and after the user clicks, to create either a square or a circle in the identified position.

From what I understand, it's necessary to listen to the MouseDown event in my canvas, and in the handler, write something like:

bool CreatingSquaresMode;
bool CreatingCirclesMode;

private void ModelViewport_MouseDown(object sender, MouseButtonEventArgs e)
{
    if (CreatingSquaresMode) { 
        // create square at selected position. 
    }
    if (CreatingCirclesMode) { 
        // create circle at selected position. 
    }
}

However that seems smelly, specially because I will have many different create commands and that quickly gets out of hand. It also breaks MVVM as I would like to have the CreateSquare and CreateCircle buttons to be bound to commands, and not have to worry about the MouseDown event.

One alternative I've considered is a state-machine, where I would determine all the possible modes of the application and replicate the above if-nest or switch-case with a little more elegance. It still feels like it's not the correct solution but it would be nicer.

The second alternative--which is the one I feel is the correct one--would be to somehow, within my CreateSquare command, to listen to the MouseDown event:

   private void CreateSquare() {
       //do something here
       //wait for user mouse input
       //create square
   }

But I don't know if that's even possible.

What is the correct way to handle this situation? I'm sure there's a design pattern that ought to help me out here.

2

There are 2 answers

1
mark_h On BEST ANSWER

There are many different ways to solve your problem so there is no "correct" way however there are certainly better ways. I include one for you to think about however I can never say that mine (or anyone elses) design is the "best", thats up to you to decide.

In your solution you are using the first button press to set properties named "CreatingShapeMode". Do you only want one mode to be set at any particular time? If so consider using an enum

In my opinion your MouseDown event handler should not be doing the work of drawing the shape (as you point out this list is only going to get bigger). Instead I would look at using some custom shape drawing classes that all inherited from a base "ObjectCreator" type. Your first button click would set the correct type e.g. circleType or squareType; the second click on the canvas (that would fire your mouse down event) would only call the createShape method; it would not need any if statements because it is the objectcreator that would know how to draw the shape.

Here is a console application that has the minimum amount of code to demonstrate what I am trying to say. Obviously it will need a lot of adaptation to fit your problem.

using System;

namespace ConsoleApplication27
{
    class Program
    {
        private static ObjectCreator creator;
        static void Main(string[] args)
        {
            creator = new CircleCreator();//change this to change the "shape" you get

            Console.ReadLine();
        }

        private static void YourMouseDownMethod()
        {
            creator.CreateObject();//no if statements required, the createObject method does the work
        }
    }



    abstract class ObjectCreator
    {
        public abstract void CreateObject();
    }

    class SquareCreator:ObjectCreator
    {
        public override void CreateObject()
        {
            Console.WriteLine("Create Square");
        }
    }

    class CircleCreator:ObjectCreator
    {
        public override void CreateObject()
        {
            Console.WriteLine("Create Circle");
        }
    }
}

You mention that you would like your buttons to be bound to commands, you would be able to do this using the command objects to set the correct ObjectCreator type.

    class SetObjectCreatorCommand:ICommand 
    {
        private readonly ObjectCreator _creator;

        public SetObjectCreatorCommand(ObjectCreator creator)
        {
            _creator = creator;//for each shape type you would create an instance of this class
            //e.g. pass in a circleCreator for circle shapes etc...
        }

        public bool CanExecute(object parameter)
        {
            return true;
        }

        public void Execute(object parameter)
        {
            //Set the creator class here, you can bind a button's command property to a Command object
        }

        public event EventHandler CanExecuteChanged;
    }
0
celsound On

For those reading, this is how I solved this problem. The answer above helped confirm what I was thinking about.

First, I created a state machine (I used Stateless) with the following states and triggers:

public enum States
{
    Idle,
    CreatingShape
}

public enum Triggers
{
    CreateShape,
    CancelCurrentAction
}

public class AppStateMachine : ObservableObject
{
    public StateMachine<States, Triggers> _stateMachine = new StateMachine<States, Triggers>(States.Idle);

    public AppStateMachine()
    {
        _stateMachine.Configure(States.Idle)
            .Permit(Triggers.CreateShape, States.CreatingJunction)
            .Ignore(Triggers.CancelCurrentAction);

        _stateMachine.Configure(States.CreatingShape)
            .Permit(Triggers.CancelCurrentAction, States.Idle)
            .Ignore(Triggers.CreateShape);
    }

And then, in an effort to preserve MVVM the best I could, I bound the UI button to a "SetCreationMode" command, with the command parameter being the type of shape to create. The command goes something like this:

XAML:

<RibbonButton x:Name="CreateSquare" Command="{Binding SetShapeCreationCommand}" CommandParameter="{Binding SquareShape}" Label="Create Square"/>

ViewModel:

    private void SetShapeCreationMode(ShapeTypeEnum ShapeType)
    {
        SelectedShapeType = ShapeType;
        _mainViewModel.AppState._stateMachine.Fire(Triggers.CreateShape);
    }

So that way, when you press the button the App State Machine fires the "CreatingShape" state, and the "SelectedShapeType" variable is set to your shape (in the example a square).

Afterwards, you still have to handle the canvas click as an event (I am toying with using EventToCommand from MVVMLight, but haven't made the leap yet.) The event handling method goes something like:

private void ModelViewport_MouseDown(object sender, MouseButtonEventArgs e)
    {
        if (_mainViewModel.AppState.IsInState(States.CreatingShape))
        {
            // retrieve mouse position

            // Execute command with retrieved position
            _CreateShapeViewModel.CreateShapeGraphicallyCommand.Execute(Position);
        }
    }

That way you get to use the command, with the selected mouse position, without having the bulk of the code in the EventHandler, which is in the ViewModel instead.

Is it the best way to handle it? I don't know, but I hope it helps.