Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Zooming a plot via the MVVM pattern?

Tags:

c#

mvvm

wpf

I have a complex Plot RenderingControl that I have placed into a View. What would be the ideal way to handle zooming with respect to the MVVM pattern? I want the user to be able to zoom by clicking and dragging on the plot.

One approach I see would be to take the MouseMove, MouseUp, MouseDown events of the Plot control and wire them up to commands in the PlotViewModel. Now in response to the commands the ViewModel could update it's ZoomLevel property, which could be bound to the view, and cause the View to zoom in. While the user is clicking and dragging I would also like to display a rectangle indicating the region that will be zoomed. Would it make sense to keep an AnnotationViewModel in PlotViewModel for the zoom preview?

Another approach would be to handle it all in the View and not involve the ViewModel at all.

The main difference I see is that capturing the behavior in the ViewModel will make that behavior much more re-useable than in the View. Though I have a feeling that the underlying Plot control and the resulting View are complex enough that there isn't going to be much of chance for re-use anyway. What do you think?

like image 355
Fredrick Avatar asked Feb 07 '26 02:02

Fredrick


1 Answers

I think there are several ways to solve your problem. HighCore right, when he says that Zoom applies to View, so it is advisable to leave it on the side View. But there are alternatives, we consider them below. Unfortunately, I did not deal with Plot RenderingControl, so I will describe a solution based on an abstract, independent of the control.

AttachedBehavior

In this case, I would have tried to identify possible all the work with the control via an attached behavior, it is ideally suited for the MVVM pattern, and it can be used in the Blend.

Example of work

In your View, control is defined and an attached behavior, like so:

<RenderingControl Name="MyPlotControl"
                  AttachedBehaviors:ZoomBehavior.IsStart="True" ... />

And in code-behind:

public static class ZoomBehavior
{
    public static readonly DependencyProperty IsStartProperty;

    public static void SetIsStart(DependencyObject DepObject, bool value)
    {
        DepObject.SetValue(IsStartProperty, value);
    }

    public static bool GetIsStart(DependencyObject DepObject)
    {
        return (bool)DepObject.GetValue(IsStartProperty);
    }

    static ZoomBehavior()
    {
        IsStartMoveProperty = DependencyProperty.RegisterAttached("IsStart",
                                                            typeof(bool),
                                                            typeof(ZoomBehavior),
                                                            new UIPropertyMetadata(false, IsStart));
    }

    private static void IsStart(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        UIElement uiElement = sender as UIElement;

        if (uiElement != null)
        {
            if (e.NewValue is bool && ((bool)e.NewValue) == true)
            {
                uiElement.MouseDown += new MouseButtonEventHandler(ObjectMouseDown);
                uiElement.MouseMove += new MouseEventHandler(ObjectMouseMove);
                uiElement.MouseUp += new MouseButtonEventHandler(ObjectMouseUp);
            }
        }
    }

    // Below is event handlers
}

Once you're set to true for property IsStart, PropertyChanged handler is triggered and it set the handlers for events that contain the basic logic.

For the transmission of additional data in you behavior register additional dependency properties, for example:

<RenderingControl Name="MyPlotControl"
                  AttachedBehaviors:ZoomBehavior.IsStart="True"
                  AttachedBehaviors:ZoomBehavior.ZoomValue="50" />

In code-behind:

// ... Here registered property

public static void SetZoomValue(DependencyObject DepObject, int value)
{
    DepObject.SetValue(ZoomValueProperty, value);
}

public static int GetZoomValue(DependencyObject DepObject)
{
    return (int)DepObject.GetValue(ZoomValueProperty);
}

// ... Somewhere in handler

int value = GetZoomValue(plotControl);

To retrieve data on the behavior, I use a singleton pattern. This pattern represents global static access point to the object and must guarantee the existence of a single instance of the class.

Example of using this pattern (taken from the behavior, who worked with the time display in the View):

public class TimeBehavior : INotifyPropertyChanged
{
    // Global instance
    private static TimeBehavior _instance = new TimeBehavior();

    public static TimeBehavior Instance
    {
        get
        {
            return _instance;
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private string _currentTime = DateTime.Now.ToString("HH:mm");

    public string CurrentTime
    {
        get
        {
            return _currentTime;
        }

        set
        {
            if (_currentTime != value)
            {
                _currentTime = value;

                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs("CurrentTime"));
                }
            }
        }
    }

    private string _currentDayString = ReturnDayString();

    public string CurrentDayString
    {
        get
        {
            return _currentDayString;
        }

        set
        {
            if (_currentDayString != value)
            {
                _currentDayString = value;

                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs("CurrentDayString"));
                }
            }
        }
    }

    private string _currentMonthAndDayNumber = ReturnMonthAndDayNumber();

    public string CurrentMonthAndDayNumber
    {
        get
        {
            return _currentMonthAndDayNumber;
        }

        set
        {
            if (_currentMonthAndDayNumber != value)
            {
                _currentMonthAndDayNumber = value;

                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs("CurrentMonthAndDayNumber"));
                }
            }
        }
    }

    public static readonly DependencyProperty IsTimerStartProperty;

    public static void SetIsTimerStart(DependencyObject DepObject, bool value)
    {
        DepObject.SetValue(IsTimerStartProperty, value);
    }

    public static bool GetIsTimerStart(DependencyObject DepObject)
    {
        return (bool)DepObject.GetValue(IsTimerStartProperty);
    }

    static void OnIsTimerStartPropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
    {
        if (e.NewValue is bool && ((bool)e.NewValue) == true)
        {
            DispatcherTimer timer = new DispatcherTimer();
            timer.Interval = TimeSpan.FromMilliseconds(1000);
            timer.Tick += new EventHandler(timer_Tick);
            timer.Start();
        }
    }

    static TimeBehavior() 
    {
        IsTimerStartProperty = DependencyProperty.RegisterAttached("IsTimerStart",
                                                                typeof(bool),
                                                                typeof(TimeBehavior),
                                                                new PropertyMetadata(new PropertyChangedCallback(OnIsTimerStartPropertyChanged)));
    }

    private static void timer_Tick(object sender, EventArgs e)
    {
        _instance.CurrentTime = DateTime.Now.ToString("HH:mm");
        _instance.CurrentDayString = ReturnDayString();
        _instance.CurrentMonthAndDayNumber = ReturnMonthAndDayNumber();
    }
}

Access to data in the View:

<TextBlock Name="WidgetTimeTextBlock"
           Text="{Binding Path=CurrentTime,
                          Source={x:Static Member=AttachedBehaviors:TimeBehavior.Instance}}" />

Alternatives

Work in View via Interface

The point of this way is that we call a method in View via ViewModel, which does all the work, and he does not know about the View. This is accomplished by the operation of the interface and the well described here:

Talk to View

Using ServiceLocator

ServiceLocator allows you to work in the ViewModel, without violating the principles of MVVM. You have a RegisterService method where you register the instance of the service you want to provide and a GetService method which you would use to get the service you want.

More information can be found here:

Service Locator in MVVM

like image 149
RobotFuse Avatar answered Feb 12 '26 04:02

RobotFuse



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!