0

My ViewControl has a method called ZoomIn(). How can I execute this method on the ViewControl by clicking a Button control without going to the code-behind?

<controls:ViewControl/>
<Button Content="Zoom In"/>

Here is the code for ViewControl.xaml.cs:

    public void ZoomIn()
    {
        double actualWidth = m_child.ActualWidth;
        double actualHeight = m_child.ActualHeight;

        double x = (0.5 * actualWidth - Dx) / Scale;
        double y = (0.5 * actualHeight - Dy) / Scale;

        float startScale = Scale;

        Scale = Math.Min(Scale * ZoomFactor, ZoomMax);

        Dx = (float)x * (startScale - Scale) + Dx;
        Dy = (float)y * (startScale - Scale) + Dy;
    }

I am trying to use MVVM for my design, but I'm not sure how to do this in this scenario because the ZoomIn() method is related to the View.

A similar situation would be if I had a Button and a TextBox, and I wanted to call the SelectAll() method on the TextBox when the Button is clicked. How can I do this without using the code-behind?

Vahid
  • 5,144
  • 13
  • 70
  • 146
  • I understood that the method ZoomIn() is in code-behind of ViewControl.xaml.cs and you have a button outside the ViewControl (assume both are placed in another User control or a window). So you want to call the method ZoomIn() on button click. Is this my understanding is correct? If so you can access the control (m_child) of ViewControl from main control on button click where you can pass the actual width and height as a parameters to the method ZoomIn(). You need to change the signature of your method. – G K May 26 '19 at 10:02
  • I hope I did not confuse you. Let me know if it is clear to you. – G K May 26 '19 at 10:03
  • @GK You have got the question correctly, but I do not want to use code-behind. The problem is this is something that is really not relevant to ViewModel, on the other hand I do not want to pollute use the code-behind. The closest thing I have come across so far is this: https://www.codeproject.com/Articles/28959/Introduction-to-Attached-Behaviors-in-WPF – Vahid May 26 '19 at 10:06
  • This sounds very much like a view responsibility and you will need code somewhere. Unless it's going to be re-used elsewhere I don't see much point in building a behaviour. – Andy May 26 '19 at 10:09
  • I agree with Andy much. I don't see this is going to pollute your view much. But having a behavior does not seems to be more appropriate here. – G K May 26 '19 at 10:16
  • @Andy, @G K, okay, I agree with you. Let's do this in code-behind. But what if I wanted to execute this method from a menu item and this button at the same time, while being able to disable them both when certain conditions are met. How can I achieve this? – Vahid May 26 '19 at 10:22
  • Maybe using RoutedCommands? – Vahid May 26 '19 at 10:33

1 Answers1

1

There are actually several different ways to do this, one solution is to bind to an event using a behavior and a wrapper class. First define a wrapper for the event that your view model will trigger:

public class EventTriggerWrapper
{
    public event EventHandler OnTriggered;

    public void Trigger()
    {
        this.OnTriggered?.Invoke(this, EventArgs.Empty);
    }
}

For the purpose of demonstration here's some XAML of a button and a WebBrowser, I'll use an instance of the wrapper in the view model to trigger the web broswer's Navigate() function whenever the button is pressed:

xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition />
    </Grid.RowDefinitions>

    <Button Content="Click Me" Command="{Binding NavigateCommand}" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="10" />        

    <WebBrowser Grid.Row="1">
        <i:Interaction.Behaviors>
            <behaviors:MyCustomBehavior EventTrigger="{Binding EventTrigger}" />
        </i:Interaction.Behaviors>
    </WebBrowser>

</Grid>

You can see that I've added a custom behaviour to the web browser control, and it's bound to a view model property called EventTrigger. You'll need to add this along with a command handler for the button to your view model:

public class MainViewModel
{
    public EventTriggerWrapper EventTrigger { get; } = new EventTriggerWrapper();

    private ICommand _NavigateCommand;
    public ICommand NavigateCommand => this._NavigateCommand ?? (this._NavigateCommand = new RelayCommand(OnNavigate));

    private void OnNavigate()
    {
        this.EventTrigger.Trigger();
    }
}

So all that's left it to create the behavior with a property that subscribes to the event and then calls whatever function in your target control you want:

public class MyCustomBehavior : Behavior<WebBrowser>
{
    public EventTriggerWrapper EventTrigger
    {
        get { return (EventTriggerWrapper)GetValue(EventTriggerProperty); }
        set { SetValue(EventTriggerProperty, value); }
    }

    // Using a DependencyProperty as the backing store for EventTrigger.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty EventTriggerProperty =
        DependencyProperty.Register("EventTrigger", typeof(EventTriggerWrapper), typeof(MyCustomBehavior), new PropertyMetadata(null, OnEventTriggerChanged));

    private static void OnEventTriggerChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var behaviour = d as MyCustomBehavior;
        var oldValue = e.OldValue as EventTriggerWrapper;
        if (oldValue != null)
            oldValue.OnTriggered -= behaviour.OnEventTriggered;
        var newValue = e.NewValue as EventTriggerWrapper;
        if (newValue != null)
            newValue.OnTriggered += behaviour.OnEventTriggered;
    }

    private void OnEventTriggered(object sender, EventArgs e)
    {
        if (this.AssociatedObject != null)
            this.AssociatedObject.Navigate("http://www.google.com");    // <-- change this to the function you want to invoke
    }
}
Mark Feldman
  • 15,731
  • 3
  • 31
  • 58
  • Thank Mark, seems a bit complicated for such a trivial thing. I tried to do it using RoutedCommands, https://stackoverflow.com/questions/56315317/routedcommand-in-usercontrol-is-not-working-as-expected Unfortunately it is not successful yet. – Vahid May 26 '19 at 16:50
  • It _is_ a bit complicated, but that's because you're trying to use the data binding engine in a way in which it wasn't designed to be used. View objects are supposed to bind to view model data and respond to changes in that data, what you're essentially trying to do is call the view from the view model. This essentially breaks the MVVM paradigm, which is why it's not as straightforward as you'd like. I've answered the question as is, but in practice challenges like this can often be solved by using proper data binding instead (although in this case I'd need to see more of the code). – Mark Feldman May 26 '19 at 21:20