Wpf Mouse event set on canvas, but targets child object - wpf

I have a Canvas that contains a Rectangle. On that canvas, I bind a mousedown event to a command on the ViewModel. In that command, I am being passed the MouseEventArgs, but there the Target element is either the Canvas or the Rectangle. Where can I find in the MouseEventArgs the Canvas this event was fired from?
My code is more or less:
<Canvas Background="White">
<i:EventTrigger EventName="MouseLeftButtonDown">
<local:InteractiveCommand Command="{Binding CmdMouseLeftButtonDown}"/>
</i:EventTrigger>
<Rectangle Width="50" Height="50" />
</Canvas>
And in the ViewModel:
ICommand CmdMouseLeftButtonDown => new DelegateCommand<MouseEventArgs>(e =>
{
e.??? // <= Where do I find the Canvas here, whether I click on the Rectangle or Canvas?
}
Please do not answer with some hackish solution like e.MouseDevice.Target.Parent. This needs to work however complicated the element in the canvas is. It could contain another canvas for instance.

A view model is not supposed to have a reference to a UI element such as a Canvas or a Rectangle at all in the first place. This effectively breaks the MVVM pattern and that's why it makes no sense to pass the sender argument to the command.
You might as well get rid of the EventTrigger and invoke the command programmatically from the code-behind of the view:
<Canvas Background="White" MouseLeftButtonDown="Canvas_MouseLeftButtonDown">
<Rectangle Width="50" Height="50" Fill="Red" />
</Canvas>
private void Canvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var yourViewModel vm = DataContext as YourClass;
vm.CmdMouseLeftButtonDown.Execute(sender as Canvas); //<-- pass the Canvas as a command argument or create a new command argument type that holds a reference to the Canvas
}
This is certainly not any worse than your current approach as far as the MVVM pattern is concerned. You are still invoking the very same command from the very same view and MVVM is not about eliminating code. It is about separation of concerns.

Your MouseEventArgs.Source will reference to the Canvas in any case but the MouseEventArgs.OriginalSource will referece to the Rectange if you have clicked on its area. It will be the control determined by pure hit testing.

Set <Canvas Background="Transparent" ... />
as answered in the following question by #Rob Fonseca-Ensor:
WPF: Canvas mouse events not firing on empty space

Related

WPF - show an image and set different clickable areas on it

the case is this one:
I have an image representing a schema, let's say a cross like the following
I need to include the image in a WPF UserControl and let the user click on each of the branches (red, green or blue...) and, according to the branch selected, do something different.
What would be the best way to solve this?
I tried with canvas but I don't find a way to trace correctly the background image with shapes (also because the real image is not so simple as the sample cross here)
thanks for any suggestion
It depends on the comlexity of shapes but it is not difficult to find the shape where mouse up event is fired by VisualTreeHelper.HitTest method.
Let's say there is a Canvas which has two Rectangles inside.
<Canvas Background="Transparent"
PreviewMouseUp="Canvas_PreviewMouseUp">
<Rectangle x:Name="Rect1" Canvas.Left="20" Canvas.Top="20"
Width="20" Height="20" Fill="Red"/>
<Rectangle x:Name="Rect2" Canvas.Left="80" Canvas.Top="80"
Width="20" Height="20" Fill="Green"/>
</Canvas>
Catching its PreviewMouseUp event, you can tell the Rectangle where that event is fired.
private void Canvas_PreviewMouseUp(object sender, MouseButtonEventArgs e)
{
var position = e.GetPosition((IInputElement)sender);
var result = VisualTreeHelper.HitTest((Visual)sender, position);
if (result.VisualHit is Rectangle rect)
{
Debug.WriteLine($"Hit {rect.Name}");
}
}

How to position TextBlock in Canvas after window resize?

I have a TextBlock which I move within a Canvas via DoubleAnimation(). On the enclosing Window SizeChanged event, I am able to properly resize the TextBlock.FontSize and inner Canvas, but I am having problems getting the position of the TextBlock correctly within the Canvas. (I was trying to do some form of Canvas.SetTop(NameQueueTextBlock, <newVal>) but that didn't work.)
<Canvas Grid.Column="1" ClipToBounds="True">
<Canvas Name="NameQueueCanvas" ClipToBounds="True" Height="79" Width="309">
<TextBlock Canvas.Top="0" Name="NameQueueTextBlock" FontSize="19" Text="
"/>
</Canvas>
</Canvas>
I'm gonna guess your DoubleAnimation is the culprit.
If it hold's the end value(which is the default) of Canvas.Top while moving the TextBlock any future updates of Canvas.Top according to the WPF priority system will "appear" to be ignored.
Solution:
switch
Canvas.SetTop(NameQueueTextBlock, /*newVal*/);
with
NameQueueTextBlock.BeginAnimation(Canvas.TopProperty, null);
Canvas.SetTop(NameQueueTextBlock, /*newVal*/);
and you should be sorted.
Alternate approach:
Assuming your Storyboard is called sb, Just before calling sb.Begin();
add something like:
sb.Completed += (o, args) => {
var finalVal = Canvas.GetTop(NameQueueTextBlock);
NameQueueTextBlock.BeginAnimation(Canvas.TopProperty, null);
Canvas.SetTop(NameQueueTextBlock, finalVal);
};
I'd prefer this as it then allows you to not keep track of which code-fragment might potentially change the Canvas.Top on the TextBlock first and reset the property with a null animation before-hand.

Draw from data of the VM

I'm learning to create WPF applications and I got a homework.
I have to create a wpf mvvm "tron lightcycle game", but unfortunately got stuck.
In the View (Mainwindow.xaml) there is a Canvas. I should draw here.
...
<Canvas Name="cnvs_game" Margin="5,5,5,5">
...
In the ViewModel there is a GameData class and a Timer.
Every tick the GameData updates(GameTime,Player1CanvasPosition (Point),... ).
I bid the Gametime to the View like this:
Mainwindow.xaml.cs:
...
<TextBlock Text="{Binding GameTime}" />
...
ViewModel.cs:
...
private GameData _GameData;
...
public String GameTime { get { return _GameData.GameTime.ToString(); } }
...
private void GameTimer_Tick(object sender, EventArgs e)
{
_GameData.Step();
OnPropertyChanged("GameTime"); // PropertyChanged with error handling
OnPropertyChanged("Player1CanvasPosition ");
OnPropertyChanged("Player2CanvasPosition ");
}
The GameTime refresh in the View. It wasn't hard. But I still have no idea how to draw.
How should I get the Player1CanvasPosition and draw there a Rectangle (in the Canvas). What is the best way to do this? Help Me Please! :S
You can do this the same way you did With the GameTime, for example:
<Canvas Name="cnvs_game" Margin="5,5,5,5">
<Rectangle Canvas.Left="{Binding Player1CanvasPositionX}" Canvas.Top="{Binding Player1CanvasPositionY}" ... />
<Rectangle Canvas.Left="{Binding Player2CanvasPositionX}" Canvas.Top="{Binding Player2CanvasPositionY}" ... />
...
And create the Player1CanvasPositionX property in the ViewModel which call the OnPropertyChanged, then when changing the properties in the ViewModel the rectangles will move.
Edit:
For dynamically adding rectangles I would use an ItemsControl which is bound to an ObservableCollection of positions. The ItemsControl datatemplate would contain a rectangle which would bind to the position. Look at this link for more details WPF Canvas, how to add children dynamically with MVVM code behind.

"Modal Dialog" in WPF - make overlay block key events

I'm creating a WPF application containing a "Main-Content" - Layer containing a TabControl and a "Dialog" - Layer containing an ItemsControl.
The XAML looks like this:
<Grid>
<TabControl>
..Some Tabs
</TabControl>
<ItemsControl>
<ContentControl Content={Binding Dialog1Property} />
<ContentControl Content={Binding Dialog2Property} />
</ItemsControl>
</Grid>
Usually "Dialog1Property" and "Dialog2Property" are null which means the ItemsControl is invisible. Whenever I assign a Control to one of them, it is shown in front of the TabControl which is exactly what I want. If I assign a gray Rectangle with an opacity of 0.7 to one of the Dialog - Properties it creates a Gray overlay.
If I click on the Tab, which is slightly visible through the overlay, nothing happens - the Rectangle blocks Mouse Events. It is, however, still possible to focus the TabControl behind the overlay using the Tab-Key and therefore it is also possible to switch tabs even though a Dialog is shown.
Is there an easy way to tell the rectangle to somehow block key events as it allready does with Mouseclicks?
Regards
BBRain
Yes, on your Rectangle, subscribe to the event PreviewKeyDown.
<Rectangle Opacity="0.7" Fill="Green" PreviewKeyDown="Rectangle_PreviewKeyDown" />
In its handler, simply set e.Handled = true;
private void Rectangle_PreviewKeyDown(object sender, KeyEventArgs e)
{
e.Handled = true;
}
Since routed events prefixed with "Preview..." are tunneling, the elements under your rectangle won't recieve the input.

How to create stretching clipping rectangle in Silverlight

Since Silverlight doesn't have the comfy feature of 'ClipToBounds' properties on controls, I have to define clipping shapes by myself. I was wondering if I could create a clipping rectangle that's following the size of my control. Any suggestions?
If there is an existing control in you layout that you want to dynamically clip then use its SizeChanged event. For example lets say you want to clip this Grid:-
<Grid SizeChanged="Grid_SizeChanged" Width="50" Height="20">
<Grid.Clip>
<RectangleGeometry />
</Grid.Clip>
<TextBlock Margin="0 -9 0 0" Text="This text should not be legible" />
</Grid>
With the code-behind:-
private void Grid_SizeChanged(object sender, SizeChangedEventArgs e)
{
((RectangleGeometry)((Grid)sender).Clip).Rect = new Rect(0.0, 0.0, e.NewSize.Width, e.NewSize.Height);
}
For a your own custom control you might consider handling the clip rectangle in the ArrangeOverride instead of relying on the SizeChanged event. In this case you probably want to assign RectangleGeometry to the Clip property in code rather than relying on it being assigned in the Xaml of the default template.
Silverlight supports that:
try using HorisontalAlignment and vertical alignment propertys. Set them to stretch.
If this doesn't work then you will have to post xaml example.

Resources