WPF MVVM Movable Shapes with Controls

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; //shape.ReleaseMouseCapture(); } } /**** More Commands for MouseLeftButtonDown and MouseMove ****/ 

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!

+6
source share
2 answers

Well, the first problem is that the ROI does not support INotifyPropertyChange, so changing them will not update your graphics. This must first be fixed.

Given the event question, check out my Perfy project. More specifically, look at the MouseCaptureBehavior class, which is responsible for capturing mouse messages and packing them for consumption by a view model, including providing capture and release. In my more complex applications, I create a contract between the view and presentation model, which usually looks something like this:

 public interface IMouseArgs { Point Pos { get; } Point ParentPos { get; } void Capture(bool parent = false); void Release(bool parent = false); bool Handled { get; set; } object Data { get;} bool LeftButton { get; } bool RightButton { get; } bool Shift { get; } void DoDragDrop(DragDropEffects allowedEffects); } 

There are a few notes in this interface. Firstly, the presentation model does not care what the implementation is, which is completely left to look. Secondly, there are Capture / Release handlers for invoking the view model, again provided by the view. Finally, there is a data field that I usually set to contain the DataContext of any object that was clicked, useful to return to the view so that it knows what object (s) you are talking about.

As for things from the view model side, now we need to implement this side of the view. I usually do this with an event trigger, which binds directly to the command handlers in the view model, but uses a converter to accept event arguments of a specific kind and turns them into something that supports the IMouseArgs object that my view model expects:

 <!--- this is in the resources block --> <conv:MouseEventsConverter x:Key="ClickConverter" /> <!-- and this is in the ItemControl ItemTemplate --> <ContentControl> <i:Interaction.Triggers> <i:EventTrigger EventName="PreviewMouseDown"> <cmd:EventToCommand Command="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:CanvasView}, Path=DataContext.MouseDownCommand}" PassEventArgsToCommand="True" EventArgsConverter="{StaticResource ClickConverter}" EventArgsConverterParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:SchedulePanel}}"/> </i:EventTrigger> </i:Interaction.Triggers> </ContentControl> 

The binding itself needs an explanation ...

1) Team. This binding is done with the object that the user clicks on (for example, your rectangles), but the command handler is usually located in the parent ObjectControl DataContext. Therefore, we need to use RelativeBinding to find this object and associate it with a handler.

2) Path=DataContext.MouseDownCommand . It should be pretty simple.

3) PassEventArgsToCommand="True" . This tells EventToCommand that our handler expects a parameter of type MouseArgs. We could do this if we wanted to, but then our view model will mess with GUI objects, so we need to convert it to our own IMouseArgs type.

4) EventArgsConverter="{StaticResource ClickConverter}" This converter will perform this translation for us, I will show the code below.

5) EventArgsConverterParameter="... Sometimes we need to transfer other data to our converter as an additional parameter. I will not go into all specific cases when it is necessary, but keep this in mind. In this specific case, I needed to find a point relative to the parent ItemControl, instead of the ItemControl itself.

The converter class itself may have everything you like, a simple implementation would be something like this:

 public class MouseEventsConverter : IEventArgsConverter { public object Convert(object value, object parameter) { var args = value as MouseEventArgs; var element = args.Source as FrameworkElement; var parent = parameter as IInputElement; var result = new ConverterArgs { Args = args, Element = element, Parent = parent, Data = element.DataContext }; return result; } 

Please note that in my specific implementation (ConverterArgs) the Framework element is actually stored here, you can always get it using the cast if the view model returns this object back to the view anywhere.

Make sense? It looks complicated, but actually it is pretty simple when you code it.

0
source

The ideal solution here is using the behavior: Move elements in the canvas using MVVM . I tested it to make it work fine.

0
source

All Articles