For the last two days I have been stuck on one (simple?) problem. I searched a lot on the internet, but I cannot find an example that solves the situation I have completely (everytime one aspect is left out, which is the breaking factor in my own implementation).
What do I want:
Create my own WPF control, that displays an image with on top rectangles (or actually shapes in general) that stay fixed while zooming and panning. Further, those rectangles need to be resized (todo yet) and be moveable (doing right now).
I want this control to adhere to the MVVM design pattern.
What do I have:
I have a XAML file with a ItemsControl. This to show a dynamic amount of rectangles (that come from my ViewModel). It is bound to the RectItems of my ViewModel (an ObservableCollection). I want to render the items as Rectangles. These rectangles must be moveable by a user using his mouse. Upon moving it should update my model entities in the ViewModel.
XAML:
<ItemsControl ItemsSource="{Binding RectItems}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas IsItemsHost="True">
</Canvas>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Canvas.Left" Value="{Binding TopLeftX}"/>
<Setter Property="Canvas.Top" Value="{Binding TopLeftY}"/>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Rectangle Stroke="Black" StrokeThickness="2" Fill="Blue" Canvas.Left="0" Canvas.Top="0"
Height="{Binding Height}" Width="{Binding Width}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseLeftButtonUp">
<i:InvokeCommandAction Command="{Binding ElementName=border,Path=DataContext.MouseLeftButtonUpCommand}" CommandParameter="{Binding}" />
</i:EventTrigger>
<i:EventTrigger EventName="MouseLeftButtonDown">
<i:InvokeCommandAction Command="{Binding ElementName=border,Path=DataContext.MouseLeftButtonDownCommand}" CommandParameter="{Binding}" />
</i:EventTrigger>
<i:EventTrigger EventName="MouseMove">
<i:InvokeCommandAction Command="{Binding ElementName=border,Path=DataContext.MouseMoveCommand}" CommandParameter="{Binding}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Rectangle>
</DataTemplate>
</ItemsControl.ItemTemplate>
ViewModel:
public class PRDisplayViewModel : INotifyPropertyChanged
{
private PRModel _prModel;
private ObservableCollection<ROI> _ROIItems = new ObservableCollection<ROI>();
public PRDisplayViewModel()
{
_prModel = new PRModel();
ROI a = new ROI();
a.Height = 100;
a.Width = 50;
a.TopLeftX = 50;
a.TopLeftY = 150;
ROI b = new ROI();
b.Height = 200;
b.Width = 200;
b.TopLeftY = 200;
b.TopLeftX = 200;
_ROIItems.Add(a);
_ROIItems.Add(b);
_mouseLeftButtonUpCommand = new RelayCommand<object>(MouseLeftButtonUpInner);
_mouseLeftButtonDownCommand = new RelayCommand<object>(MouseLeftButtonDownInner);
_mouseMoveCommand = new RelayCommand<object>(MouseMoveInner);
}
public ObservableCollection<ROI> RectItems
{
get { return _ROIItems; }
set { }
}
private bool isShapeDragInProgress = false;
double originalLeft = Double.NaN;
double originalTop = Double.NaN;
Point originalMousePos;
private ICommand _mouseLeftButtonUpCommand;
public ICommand MouseLeftButtonUpCommand
{
get { return _mouseLeftButtonUpCommand; }
set { _mouseLeftButtonUpCommand = value; }
}
public void MouseLeftButtonUpInner(object obj)
{
Console.WriteLine("MouseLeftButtonUp");
isShapeDragInProgress = false;
if (obj is ROI)
{
var shape = (ROI)obj;
//shape.ReleaseMouseCapture();
}
}
/**** More Commands for MouseLeftButtonDown and MouseMove ****/
Class ROI (this would reside inside PRModel later):
public class ROI
{
public double Height { get; set; }
public double TopLeftX { get; set; }
public double TopLeftY { get; set; }
public double Width { get; set; }
}
So the way I see it is the following:
ItemsControl renders an ROI object into a Rectangle. Mouse events on the Rectangle are handled by Commands in the ViewModel. The ViewModel, upon receiving a mouse event, handles updates directly on the ROI object. Then, the view should redraw (assuming the ROI object has changed), and therefore generate new Rectangles, etc.
What is the problem?
In the Mouse event handlers, I need to call the CaptureMouse() method of the Rectangle on which the mouse event occurred. How do I get access to this Rectangle?
Most probably the actual problem is that my perspective on MVVM here is wrong. Should I actually try to update the ROI objects in the mouse event handlers in the ViewModel? Or should I only update the Rectangle objects? If the latter, then how do updates propagate to the actual ROI objects?
I checked many other questions, among which the ones below, but I still did not manage to solve my problem:
Add n rectangles to canvas with MVVM in WPF
Dragable objects in WPF in an ItemsControl?
EDIT: Thank you all for your replies. Your input helped a lot. Unfortunately, I am still not able to get this working (I am new to WPF, but I cannot imagine this to be so hard).
Two new attempts, for which I both implemented the INotifyPropertyChanged interface in the ROI class.
Attempt 1: I implemented the dragging with MouseDragElementBehavior like this:
<ItemsControl ItemsSource="{Binding RectItems}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas IsItemsHost="True">
</Canvas>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<!-- //-->
<Setter Property="Canvas.Left" Value="{Binding TopLeftX, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<Setter Property="Canvas.Top" Value="{Binding TopLeftY, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Rectangle Stroke="Black" StrokeThickness="2" Canvas.Left="0" Canvas.Top="0"
Height="{Binding Height}" Width="{Binding Width}">
<i:Interaction.Behaviors>
<ei:MouseDragElementBehavior/>
</i:Interaction.Behaviors>
</Rectangle>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
That worked perfect! Everything is for free, even when I zoom in on a border (which is a parent element to all of this).
problem here: After dragging a rectangle, I see this in the UI, but my ROI object (linked to this rectangle) is not updated? I specified TwoWay binding with the PropertyChanged UpdateSourceTrigger, but still that does not work.
question here: How do I get, in this situation, my ROI objects updated? I can implement the DragFinished event of MouseDragElementBehavior, but here I do not get the corresponding ROI object? @Chris W., where could I bind a UIElement.Drop handler? Could I get the corresponding ROI object there?
Attempt 2: Same as attempt 1, but now I also implemented the EventTriggers (like in my original post). There, I did not do anything to update the rectangle in the UI, but I only updated the corresponding ROI object.
problem here: This did not work, because the Rectangles we moved 'twice' the vector they should. Probably the first movement is from the dragging itself, and the second movement is the manual update (from me) in the corresponding ROIs.
Attempt 3: Instead of using MouseDragElementBehavior, "simply" implement the dragging myself using the EventTriggers (like in my original post). I did not update any Rectangle (UI), but only the ROIs (shifted their TopLeftX and TopLeftY).
problem here: This actually worked, except in the case of zooming. Further, the dragging was not nice, because it was 'flickering' a bit, and while moving the mouse too fast, it would loose its rectangle. Clearly, in the MouseDragElementBehavior, more logic has been put to make this smooth.
@Mark Feldman: thank you for your answer. It did not yet solve my problem, but I like the idea of not using GUI classes in my ViewModel (like Mouse.GetPosition to implement draggin myself). To decouple using a converter I will implement later, if the functionality is working.
@Will: you are right (MVVM != no code behind). Unfortunately I do not see right now how I can exploit this, as the event handlers from MouseDragElementBehavior do not know about a ROI (and this I would need to update the ViewModel).
EDIT 2:
Something that is working (at least it seems so) is the following (using MouseDragElementBehavior):
<ItemsControl ItemsSource="{Binding RectItems}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas IsItemsHost="True">
</Canvas>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Rectangle Stroke="Black" StrokeThickness="2" Canvas.Left="0" Canvas.Top="0"
Height="{Binding Height}" Width="{Binding Width}">
<i:Interaction.Behaviors>
<ei:MouseDragElementBehavior X="{Binding TopLeftX, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Y="{Binding TopLeftY, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
</ei:MouseDragElementBehavior>
</i:Interaction.Behaviors>
</Rectangle>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
I bound the X and Y property of MouseDragElementBehavior to the appropriate property of the ViewModel. When I drag the rectangle around, I see the values in the corresponding ROI updated! Further, there is no 'flickering' or other problems while dragging.
the problem still: I had to remove the code in ItemContainerStyle, used to initialize the positions of the rectangles. Probably an update in the binding of MouseDragElementBehavior causes an update here as well. This is visible while dragging (the rectangle jumps quickly between two positions).
question: using this approach, how can I initialize the position of the rectangles? Further, this feels like an hack, so is there a more appropriate approach?
EDIT 3:
The following code will also initialize the rectangles on the appropriate position:
<ItemsControl ItemsSource="{Binding RectItems}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas IsItemsHost="True">
</Canvas>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<!-- //-->
<Setter Property="Canvas.Left" Value="{Binding TopLeftX, Mode=OneTime}"/>
<Setter Property="Canvas.Top" Value="{Binding TopLeftY, Mode=OneTime}"/>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Rectangle Stroke="Black" StrokeThickness="2" Canvas.Left="0" Canvas.Top="0"
Height="{Binding Height}" Width="{Binding Width}">
<i:Interaction.Behaviors>
<ei:MouseDragElementBehavior X="{Binding TopLeftX, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Y="{Binding TopLeftY, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
</ei:MouseDragElementBehavior>
</i:Interaction.Behaviors>
</Rectangle>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Problem still: This feels 'hacky'. Is there a 'better' way of doing this (for so far it seems to work)?
EDIT 4: I ended up using Thumbs. Using DataTemplates I was able to define the appearance of an item from my ViewModel (so to use e.g. Thumbs) and via ControlTemplates I was able to define how the thumbs actually should look like (an rectangle, for example). Advantage of Thumbs is that drag&drop is already implemented!
MouseDragElementBehavior
and using theUIElement.Drop
event? – Chris W.