Home > front end >  Correct usage of delegated commands using Prism-MVVM
Correct usage of delegated commands using Prism-MVVM

Time:09-06

I'm tring to get the mouse postion related to a wpf control (a Canvas in this case) using MVVM Framework with Prism Library.

I already got a solution but I'm not sure if it's a correct way to use the MVVM framework.

Main Window:

<Grid Grid.Row="1">
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition Width="250"/>
        </Grid.ColumnDefinitions>
        <Border Grid.Column="0" BorderBrush="Gray" BorderThickness="1">
            <Canvas HorizontalAlignment="Center" VerticalAlignment="Center"  
            Width="{Binding CanvasWidth}" Height="{Binding CanvasHeight}">
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="MouseMove">
                        <prism:InvokeCommandAction Command="{Binding MouseMove}"/>
                    </i:EventTrigger>
                    <i:EventTrigger EventName="Loaded">
                        <prism:InvokeCommandAction Command="{Binding Loaded}"/>
                    </i:EventTrigger>
                </i:Interaction.Triggers>
                <Image Source="{Binding Image}" />
            </Canvas>
        </Border>
        <TextBlock Grid.Column="1" Text="{Binding Text}"/>
        <StackPanel  Grid.Column="1">
            <TextBlock Text="{Binding MouseX, StringFormat='X={0}'}" Grid.Column="1" />
            <TextBlock Text="{Binding MouseY, StringFormat='Y={0}'}" Grid.Column="1" />
        </StackPanel>
    </Grid>

In this XAML snipped code the canvas has 2 Event triggers that I use for converting:

  • the "MouseMove" event to give the XY pointer position
  • and the "Loaded" event where the tricky part is. Here I pass the instance obj from Canvas to the controller through this EventTrigger, the in the controller I use this code:

Loaded and MouseMove commands definition:

    public DelegateCommand<MouseEventArgs> MouseMove { get; private set; } 
    public DelegateCommand<RoutedEventArgs> Loaded { get; private set; }

Constructor:

public MainWindowViewModel()
    {
        MouseMove = new DelegateCommand<MouseEventArgs>(GetMousePosition);
        Loaded = new DelegateCommand<RoutedEventArgs>(GetCanvas);
    }

Properties definition:

private string _mouseX;
public string MouseX
{
    get { return _mouseX; }
    set { SetProperty(ref _mouseX, value); }
}

private string _mouseY;
public string MouseY
{
    get { return _mouseY; }
    set { SetProperty(ref _mouseY, value); }
}

private System.Windows.Controls.Canvas _canvas;
public System.Windows.Controls.Canvas Canvas
{
   get { return _canvas; }
   set { SetProperty(ref _canvas, value); }
}

Methods called by commands:

private void GetCanvas(RoutedEventArgs obj)
{
    Canvas = (System.Windows.Controls.Canvas)obj.Source;
}

private void GetMousePosition(MouseEventArgs eventParam)
{
    Point position = eventParam.GetPosition(Canvas);
    MouseX = position.X.ToString();
    MouseY = position.Y.ToString();
}

Is this way a correct usage? even this working I feel like passing the Canvas obj to the controller I'm doing something like "code behind".

Thanks in advanced!

CodePudding user response:

I'm using a converter to do the GetPosition. That gets passed the source and the event args, so you can get away without the LoadedCommand and you keep the MouseEventArgs out of your view model.

xaml:

<i:Interaction.Triggers>
    <i:EventTrigger EventName="MouseMove">
        <i:InvokeCommandAction Command="{Binding MouseMoveCommand}" PassEventArgsToCommand="True" EventArgsConverter="{StaticResource GetPositionConverter}"/>
    </i:EventTrigger>
</i:Interaction.Triggers>

view model:

public DelegateCommand<Point?> MouseMoveCommand { get; }

converter:

internal class GetPositionConverter : IValueConverter
{
    public object Convert( object value, Type targetType, object parameter, CultureInfo culture )
    {
        var mouseEventArgs = (MouseEventArgs)value;
        return mouseEventArgs.GetPosition( (IInputElement)mouseEventArgs.Source );
    }

    public object ConvertBack( object value, Type targetType, object parameter, CultureInfo culture )
    {
        throw new NotImplementedException();
    }
}

The converter should have at least minimal error handling, though, this is just an example :-)

CodePudding user response:

Yes, you are violating MVVM.. You should not reference any UI-type in ViewModel.

I'd suggest the following (you can use the same approach to pass whatever data you want from the View to the ViewModel and keep them separated from each other):

First. Remove the DelegateCommands from your ViewModel! along with all the code snippets you've there!

Second. remove the <i:Interaction.Triggers/> code-block from .xaml

Third. Define MouseMove normally in Canvas like this: <Canvas MouseMove="Canvas_OnMouseMove", then define the handler in .xaml.cs like this (define MousePosition vars here)

private string _mouseX;
private string _mouseY;

private void MainWindow_OnMouseMove(object sender, MouseEventArgs e)
{
    Point position = e.GetPosition(sender as Canvas);
    _mouseX = position.X.ToString();
    _mouseY = position.Y.ToString();
}

Finally, when ViewModel wants to get to know about the Mouse coordinates, it could do the following:

  1. Define Interface in your ViewModel's namespace:
public interface IUiServices
{
    (string mouseX, string mouseY) GetMouseCoordinates();
}
  1. Let your Window (or UserControl) that hosts the <Canvas/> implement this interface

public partial class TheView : IUiServices { }

  1. Implement GetMouseCoordinates() function right below MainWindow_OnMouseMove callback!
public (string mouseX, string mouseY) GetMouseCoordinates() => (_mouseX, _mouseY);

Either:

  1. In protected override void RegisterTypes(IContainerRegistry containerRegistry){} method, register IUiServices like this:
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
    // ...
    containerRegistry.RegisterSingleton<IUiServices, TheView>();
    // ...
}

And in ViewModel, you could just inject it in the constructor

public TheViewModel(.. , IUiServices UiServices){
  //..
}

Or:

  1. Define a method in ViewModel to allow injecting the service
private IUiServices UiServices {set; get;}
public SetUiService(IUiServices s){
    UiServices = s;
}

and do the injection in TheView.xaml.cs's constructor (make sure the DataContext is set).

(this.DataContext as TheViewModel)?.SetUiService(this);
  1. Call it whenever you need it in ViewModel
private void SomeMethod(){
    // ..
    var tuple = UiServices.GetMouseCoordinates();
    // ..
}

From now on, any service ViewModel wants from View, just define it in IUiServices interface, implement it in View and use it in ViewModel

  • Related