Question

OK, I've figured out how to get my Grid of UI elements to zoom, by using LayoutTransform and ScaleTransform. What I don't understand is how I can get my View to respond to CTRL+MouseWheelUp\Down to do it, and how to fit the code into the MVVM pattern.

My first idea was to store the ZoomFactor as a property, and bind to a command to adjust it.

I was looking at something like:

<UserControl.InputBindings>
 <MouseBinding Command="{Binding ZoomGrid}" Gesture="Control+WheelClick"/>
</UserControl.InputBindings>

but I see 2 issues:

1) I don't think there is a way to tell whether the wheel was moved up or down, nor can I see how to determine by how much. I've seen MouseWheelEventArgs.Delta, but have no idea how to get it.

2) Binding to a command on the viewmodel doesn't seem right, as it's strictly a View thing.

Since the zoom is strictly UI View only, I'm thinking that the actual code should go in the code-behind.

How would you guys implement this?

p.s., I'm using .net\wpf 4.0 using Cinch for MVVM.

Was it helpful?

Solution

I would suggest that you implement a generic zoom command in your VM. The command can be parameterized with a new zoom level, or (perhaps even simpler) you could implement an IncreaseZoomCommand and DecreaseZoomCommand. Then use the view's code behind to call these commands after you have processed the event arguments of the Mouse Wheel event. If the delta is positive, zoom in, if negative zoom out.

There is no harm in solving this problem by using a few lines of code behind. The main idea of MVVM is that, you are able to track and modify nearly the complete state of your view in an object that does not depend on the UI (enhances the testability). In consequence, the calculation of the new viewport which is the result of the zoom should be done in the VM and not in code behind.

The small gap of testability that exists in the code behind can either be disregarded or covered by automatic UI tests. Automatic UI tests, however, can be very expensive.

OTHER TIPS

the real anwser is to write your own MouseGesture, which is easy.

<MouseBinding Gesture="{x:Static me:MouseWheelGesture.CtrlDown}"  
              Command="me:MainVM.SendBackwardCommand" />

public class MouseWheelGesture : MouseGesture
{
    public static MouseWheelGesture CtrlDown
        => new MouseWheelGesture(ModifierKeys.Control) { Direction = WheelDirection.Down


    public MouseWheelGesture(): base(MouseAction.WheelClick)
    {
    }

    public MouseWheelGesture(ModifierKeys modifiers) : base(MouseAction.WheelClick, modifiers)
    {
    }

    public WheelDirection Direction { get; set; }

    public override bool Matches(object targetElement, InputEventArgs inputEventArgs)
    {
        if (!base.Matches(targetElement, inputEventArgs)) return false;
        if (!(inputEventArgs is MouseWheelEventArgs args)) return false;
        switch (Direction)
        {
            case WheelDirection.None:
                return args.Delta == 0;
            case WheelDirection.Up:
               return args.Delta > 0;
            case WheelDirection.Down:
                return args.Delta < 0;
            default:
                return false;
        }
    }



    public enum WheelDirection
    {
      None,
      Up,
      Down,
    }

}

If you don't want to use code behind you can use the EventToCommand functionality of mvvm light:

View:

 <...
     xmlns:cmd="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.WPF4"
 xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" 
     ...> 
<i:Interaction.Triggers>
         <i:EventTrigger EventName="PreviewMouseWheel">
             <cmd:EventToCommand Command="{Binding
     Path=DataContext.ZoomCommand,
     ElementName=Root, Mode=OneWay}"
         PassEventArgsToCommand="True"   />
         </i:EventTrigger> </i:Interaction.Triggers>

ViewModel:

ZoomCommand = new RelayCommand<RoutedEventArgs>(Zoom);
...
public void Zoom(RoutedEventArgs e)
{
    var originalEventArgs = e as MouseWheelEventArgs;
    // originalEventArgs.Delta contains the relevant value
}

I hope this helps someone. I know the question is kind of old...

I think what your trying to do is very much related to the view so there is no harm in putting code in your code behind (in my opinion at least), although I'm sure there are elegant ways of handling this such that it is more viewmodel based.

You should be able to register to the OnPrevewMouseWheel event, check if the user has got the control key pressed and change your zoom factor accordingly to get the zooming effect you are looking for.

I agree with both answers, and would only add that to use code behind is the only way in this case, so you don't even have to think about if it breaks any good practices or not.

Fact is, the only way to get hold of the MouseEventArgs (and thus the Delta) is in the code behind, so grab what you need there (no logic needed for that) and pass it on to your view model as olli suggested.

On the flip side you may want to use a more generic delta (e.g. divide it by 120 before you pass it as a step to the view model) in order to keep it ignorant of any conventions related to the view or the OS. This will allow for maximum reuse of your code in the view model.

To avoid the whole problem, there is one more option : -use a ContentPresenter in the xaml and let it's content be bound to a viewmodel object. -handle the mousewheel events within the viewmodel.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top