7

From MVVM Design pattern, the viewmodel should not know the view. But in my case, I need the view and the model, I mean :

In my window, I've an Image component. I'd like to get mouse position when mouse moves over the Image component and save it into my model.

The code behind would have been :

void Foo_MouseMove(objet sender, MouseEventArgs e)
{
  model.x = e.getPosition(this.imageBox).X; 
  model.y = e.getPosition(this.imageBox).Y;
}

The problem is : I need this.imageBox and MouseEventArgs, so two View element.

My question is : How to deal with this case using the MVVM approach ?

I use MVVM light framework

Muds
  • 4,006
  • 5
  • 31
  • 53
Titouan56
  • 6,932
  • 11
  • 37
  • 61
  • *I need this.imageBox and MouseEventArgs, so two View element*... if they are view elements, then handle them in the view code behind, regardless of whether you're using MVVM or not. View elements do *not* belong in the view model. – Sheridan May 05 '15 at 13:14

3 Answers3

10

I would use an attached behaviour here. This will allow you to continuously monitor the mouse position, rather than simply responding to an event such as MouseDown. You'll need to add a reference to the System.Windows.Interactivity assembly.

The code below provides a simple example of this in action.

XAML

<Window x:Class="MouseMoveMvvm.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:mouseMoveMvvm="clr-namespace:MouseMoveMvvm"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <DockPanel>
            <StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
                <TextBlock Text="{Binding PanelX, StringFormat='X={0}'}" />
                <TextBlock Text="{Binding PanelY, StringFormat='y={0}'}" />
            </StackPanel>
            <Canvas DockPanel.Dock="Bottom" Background="Aqua">
                <i:Interaction.Behaviors>
                    <mouseMoveMvvm:MouseBehaviour MouseX="{Binding PanelX, Mode=OneWayToSource}" MouseY="{Binding PanelY, Mode=OneWayToSource}" />
                </i:Interaction.Behaviors>
            </Canvas>
        </DockPanel>
    </Grid>
</Window>

Note that, in the above XAML, the MouseBehaviour is pushing the mouse position down to the ViewModel through a OneWayToSource binding, while the two TextBlocks are reading the mouse positions from the ViewModel.

ViewModel

public class MainWindowViewModel : INotifyPropertyChanged
{
    private double _panelX;
    private double _panelY;
    public event PropertyChangedEventHandler PropertyChanged;

    public double PanelX
    {
        get { return _panelX; }
        set
        {
            if (value.Equals(_panelX)) return;
            _panelX = value;
            OnPropertyChanged();
        }
    }

    public double PanelY
    {
        get { return _panelY; }
        set
        {
            if (value.Equals(_panelY)) return;
            _panelY = value;
            OnPropertyChanged();
        }
    }


    [NotifyPropertyChangedInvocator]
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        var handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }
}

Attached Behaviour

public class MouseBehaviour : System.Windows.Interactivity.Behavior<FrameworkElement>
{
    public static readonly DependencyProperty MouseYProperty = DependencyProperty.Register(
        "MouseY", typeof (double), typeof (MouseBehaviour), new PropertyMetadata(default(double)));

    public double MouseY
    {
        get { return (double) GetValue(MouseYProperty); }
        set { SetValue(MouseYProperty, value); }
    }

    public static readonly DependencyProperty MouseXProperty = DependencyProperty.Register(
        "MouseX", typeof(double), typeof(MouseBehaviour), new PropertyMetadata(default(double)));

    public double MouseX
    {
        get { return (double) GetValue(MouseXProperty); }
        set { SetValue(MouseXProperty, value); }
    }

    protected override void OnAttached()
    {
        AssociatedObject.MouseMove += AssociatedObjectOnMouseMove;
    }

    private void AssociatedObjectOnMouseMove(object sender, MouseEventArgs mouseEventArgs)
    {
        var pos = mouseEventArgs.GetPosition(AssociatedObject);
        MouseX = pos.X;
        MouseY = pos.Y;
    }

    protected override void OnDetaching()
    {
        AssociatedObject.MouseMove -= AssociatedObjectOnMouseMove;
    }
}
Barracoder
  • 3,696
  • 2
  • 28
  • 31
  • 2
    This ought to be the accepted answer works like a treat for me. Am I right in saying it doesn't necessarily have to be a canvas control, it can be any control you wish to monitor the coordinates of, even a Button, so long as this is specified in the attached behaviour class? – AndyUK May 26 '17 at 08:47
  • Absolutely. The code I wrote should properly have had the behaviour inheriting from System.Windows.Interactivity.Behavior, rather than System.Windows.Interactivity.Behavior because MouseMove is a property of Control but I'll leave Panel in my answer because it's more closely associated to the question. – Barracoder May 27 '17 at 23:21
7

Finnally found an answer, using a EventConverter :

  public class MouseButtonEventArgsToPointConverter : IEventArgsConverter
    {
        public object Convert(object value, object parameter)
        {
            var args = (MouseEventArgs)value;
            var element = (FrameworkElement)parameter;
            var point = args.GetPosition(element);
            return point;
        }
    }

This converter allows me to deal with Point and not with graphics components.

Here goes the XML :

        <i:Interaction.Triggers>
            <i:EventTrigger EventName="MouseMove">
                <cmd:EventToCommand
                 Command="{Binding Main.MouseMoveCommand, Mode=OneWay}"
                 EventArgsConverter="{StaticResource MouseButtonEventArgsToPointConverter}"
                 EventArgsConverterParameter="{Binding ElementName=Image1}"
                 PassEventArgsToCommand="True" />
            </i:EventTrigger>
        </i:Interaction.Triggers>
Titouan56
  • 6,932
  • 11
  • 37
  • 61
  • 3
    Could you please share whole XAML? What is cmd? What is i? – Gabriel May 04 '18 at 12:35
  • It's been more than 3 years now, I've no idea – Titouan56 Jun 20 '18 at 12:39
  • I think it's: `xmlns:cmd="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Platform"` See [this link](https://stackoverflow.com/questions/5868589/mvvm-light-adding-eventtocommand-in-xaml-without-blend-easier-way-or-snippet) – BugsFixer Mar 06 '19 at 07:46
  • @Gabriel, I think "i " is interaction namespace such as this : xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" In practice, i is the name that we select for the namespace to use in Xaml. And as BugsFixer said cmd is the namespace used for MVVM Light namespace. – Ehsan Jul 09 '19 at 15:26
6

Mark Greens solution is the best (I found).

If you want to make his solution reusable for any WPF control (which I suggest), inheriting from System.Windows.Interactivity.Behavior<Control> actually won't work for Panel, because Panel does not inherit from Control. Only those classes inherit from Control: https://msdn.microsoft.com/de-de/library/system.windows.controls.control(v=vs.110).aspx

Instead, inherit from System.Windows.Interactivity.Behavior<FrameworkElement>. FrameworkElement is the ancestor of all WPF control classes: https://msdn.microsoft.com/de-de/library/system.windows.frameworkelement(v=vs.110).aspx. I have tested it on Grid, Panel and Image btw.

I use it to keep a Popup in sync with the mouse cursor:

<Image x:Name="Image1">        
  <i:Interaction.Behaviors>
    <myNamespace:MouseBehaviour
      MouseX="{Binding ElementName=Popup1, Path=HorizontalOffset, Mode=OneWayToSource}"
      MouseY="{Binding ElementName=Popup1, Path=VerticalOffset, Mode=OneWayToSource}">
    </myNamespace:MouseBehaviour>
  </i:Interaction.Behaviors>
</Image>

<Popup x:Name="Popup1" PlacementTarget="{Binding ElementName=Image1}"/>

P.S.: I would have commented on the solution, but my answer is too long.

Julian
  • 265
  • 5
  • 10