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 DelegateCommand
s 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:
- Define Interface in your
ViewModel
's namespace:
public interface IUiServices
{
(string mouseX, string mouseY) GetMouseCoordinates();
}
- Let your Window (or UserControl) that hosts the
<Canvas/>
implement this interface
public partial class TheView : IUiServices { }
- Implement
GetMouseCoordinates()
function right belowMainWindow_OnMouseMove
callback!
public (string mouseX, string mouseY) GetMouseCoordinates() => (_mouseX, _mouseY);
Either:
- In
protected override void RegisterTypes(IContainerRegistry containerRegistry){}
method, registerIUiServices
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:
- 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);
- 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