Correct usage of delegated commands using Prism-MVVM

Viewed 84

I'm trying to get the mouse position 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 code snippet 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".

2 Answers

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 :-)

I think you are violating MVVM.. because you are referencing UI-type (i.e. System.Windows.Controls.Canvas) in ViewModel.

I'd suggest an approach to keep the ViewModel clean and get whatever data needed from View..

First, Define an interface in ViewModel's namespace, everything ViewModel wants from View will be defined in this interface..

public interface IUiServices
{
    (string mouseX, string mouseY) GetMouseCoordinates();
}

Next, Let your Window (or UserControl) that hosts the <Canvas/> implement this interface

public partial class TheWindow : IUiServices { 
    // ..
    
    private string _mouseX;
    private string _mouseY;
    public (string mouseX, string mouseY) GetMouseCoordinates() => (_mouseX, _mouseY);
}

Now, Let the Canvas subscribe to MouseMove event

<Canvas MouseMove="Canvas_OnMouseMove"

And add the handler to set the mouse coords variables

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

Finally, you can register IUiServices in Prism

protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
    // ...
    containerRegistry.RegisterSingleton<IUiServices, TheView>();
    // ...
}

And inject it in ViewModel's constructor

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

Now, wherever you want to get the coordinates, just call uiServices.GetMouseCoordinates().


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

Note2: you might not use want to pass the service to the ViewModel via Prism, then you could inject it via setter injection

private IUiServices UiServices {set; get;}
public SetUiService(IUiServices s){
    UiServices = s;
}

And in TheView, you can do the injection (DataContext as TheViewModel)?.SetUiService(this);

Note3: you can remove all of these from your code: the DelegateCommands in your ViewModel and all the code snippets you've there + <i:Interaction.Triggers/> code-block in your .xaml.

Related