Over the past two days, I'm stuck in one (simple?) Problem. I searched a lot on the Internet, but I can’t find an example that solves a situation that I completely (each time one aspect is missing, which is a violation in my own implementation).
What I need:
Create your own WPF control that displays an image with upper rectangles (or actually forms a whole) that remain fixed when zoomed in and panned. In addition, these rectangles must be resized (todo yet) and be movable (now).
I want this control to fit the MVVM design pattern.
What I need:
I have a XAML file with an ItemsControl element. This shows the dynamic number of rectangles (which appear from my ViewModel). It is bound to the RectItems of my ViewModel (ObservableCollection) model. I want the objects to be like Rectangles. These rectangles must be moved by the user with the mouse. When moving, it should update my model objects 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;
ROI class (this will be inside the 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, I see the following:
ItemsControl maps an ROI to a rectangle. Mouse events on the rectangle are handled by commands in the ViewModel. The ViewModel, after receiving the mouse event, processes the updates directly on the ROI. Then the view should be redrawn (provided that the ROI object has changed) and, therefore, generate new rectangles, etc.
What is the problem?
In Mouse event handlers, I need to call the CaptureMouse () method on the Rectangle on which the mouse event occurred. How to access this rectangle?
Most likely, the problem is that my perspective on MVVM is wrong here. Should I try to update the ROIs in the mouse event handlers in the ViewModel? Or do I only need to update the Rectangle objects? If the latter, then how do updates apply to real ROIs?
I checked many other questions, among which are below, but I still could not solve my problem:
Add n rectangles to canvas with MVVM in WPF
Dragable objects in WPF in ItemsControl?
EDIT: Thank you all for your answers. Your input has been very helpful. Unfortunately, I still can't get this working (I'm new to WPF, but I can't imagine it being so complicated).
Two new attempts for which I implemented the INotifyPropertyChanged interface in the ROI class.
Attempt 1: I implemented drag and drop using MouseDragElementBehavior as follows:
<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>
It worked great! Everything is free, even when I zoom in on the border (which is the parent for all of this).
problem:. After dragging a rectangle, I see this in the user interface, but my ROI object (associated with this rectangle) is not updated? I pointed to TwoWay binding with PropertyChanged UpdateSourceTrigger, but still this does not work.
the question here is: How do I get in this situation my ROI objects are updated? I can implement the DragFinished MouseDragElementBehavior event, but here I do not get the corresponding ROI object? @Chris W., where can I bind a UIElement.Drop handler? Can I get the corresponding ROI object?
Attempt 2: Same as attempt 1, but now I have also implemented EventTriggers (as in my original post). There I did nothing to update the rectangle in the user interface, but I only updated the corresponding ROI object.
the problem is here: This did not work, because the Rectangles we moved the "double" vector, which they should. Probably the first movement comes from the drag itself, and the second movement is a manual update (from me) in the corresponding ROI.
Attempt 3: Instead of using MouseDragElementBehavior "just" implement drag and drop yourself using EventTriggers (as in my original post). I did not update the Rectangle (UI), but only the ROI (offset their TopLeftX and TopLeftY).
problem here: It really worked, except in cases of scaling. In addition, the drag was not pleasant because it “flickered” a little, and if you move the mouse too fast, it would lose its rectangle. Obviously, in MouseDragElementBehavior more logic has been done to make this smooth.
@Mark Feldman: Thank you for your reply. This has not yet solved my problem, but I like the idea of not using the GUI classes in my ViewModel (e.g. Mouse.GetPosition to implement Draggin). To decouple the use of the converter, I will implement it later if the functionality works.
@Will: you're right (MVVM! = No code). Unfortunately, I don’t see right now how I can use this, since the event handlers from MouseDragElementBehavior are not aware of the ROI (and I would need to update the ViewModel).
EDIT 2:
Something that works (at least it seems), 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 linked the X and Y properties of MouseDragElementBehavior with the corresponding ViewModel property. When I drag the rectangle around, I see that the values in the corresponding ROI are updated! In addition, there is no flicker or other problems when dragging.
problem still: I had to delete the code in ItemContainerStyle used to initialize the positions of the rectangles. Probably the update in the MouseDragElementBehavior binding also causes the update. This is visible when dragging (the rectangle quickly jumps between two positions).
question: using this approach, how can I initialize the position of the rectangles? It also seems like a hack, so is there a better approach?
EDIT 3:
The following code also initializes the rectangles at 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>
Another problem: It seems to be "hacked." Is there a “better” way to do this (while it works)?
EDIT 4: I ended up using Thumbs. Using DataTemplates, I was able to determine the appearance of an element from my ViewModel (so that I could use, for example, Thumbs), and with ControlTemplates I was able to determine how my thumbs should look (for example, a rectangle). The advantage of Thumbs is that drag & drop is already implemented!